From a115391277fc787815d031ddc6e1f4d96bfdfd13 Mon Sep 17 00:00:00 2001 From: DJConnect Date: Sat, 8 Aug 2026 11:37:20 +0200 Subject: [PATCH] Finalize dismissed executions --- tests/engineering/test_dashboard.py | 2 +- tests/engineering/test_execution_host.py | 29 ++++++++ tests/engineering/test_inbox_watcher.py | 11 ++- tests/engineering/test_prompt_history.py | 4 ++ .../engineering/ENGINEERING_INBOX_PROTOCOL.md | 13 ++-- tools/engineering/assets/dashboard.js | 4 +- .../engineering/assets/dashboard_locales.mjs | 25 +++++++ tools/engineering/execution_host.py | 3 + tools/engineering/inbox_watcher.py | 34 ++++++--- tools/engineering/prompt_history.py | 10 ++- tools/engineering/storage.py | 70 ++++++++++++++++++- 11 files changed, 181 insertions(+), 24 deletions(-) diff --git a/tests/engineering/test_dashboard.py b/tests/engineering/test_dashboard.py index bc81690d..42ef13c9 100644 --- a/tests/engineering/test_dashboard.py +++ b/tests/engineering/test_dashboard.py @@ -761,7 +761,7 @@ def test_engineering_database_details_are_read_only_and_report_the_schema(self) self.assertRegex(details["size"], r"^\d+,\d{2} MB$") self.assertNotEqual(details["size"], "0,00 MB") - self.assertEqual(details["schema_version"], "17") + self.assertEqual(details["schema_version"], "18") @patch("tools.engineering.dashboard.subprocess.run") def test_tracked_file_count_counts_recursive_git_index_entries(self, run: object) -> None: diff --git a/tests/engineering/test_execution_host.py b/tests/engineering/test_execution_host.py index 253c4995..502c77cb 100644 --- a/tests/engineering/test_execution_host.py +++ b/tests/engineering/test_execution_host.py @@ -860,6 +860,35 @@ def test_repository_mismatch_fails_closed_on_resume(self) -> None: with self.assertRaisesRegex(RunnerError, "conflicts"): runner.run(self.prompt, run_id="resume-run", resume=True) + def test_resume_rejects_a_dismissed_execution_without_invoking_the_agent(self) -> None: + from tools.engineering.prompt_history import record_prompt_execution + from tools.engineering.storage import record_execution_dismissal + + run_id = "dismissed-resume-run" + self.store.save(TransactionState(run_id, "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT")) + record_prompt_execution( + self.root, + run_id=run_id, + terminal_state="BLOCKED", + prompt_title="Dismissed blocked execution", + executed_at="2026-08-08T10:00:00+00:00", + ) + record_execution_dismissal( + self.root, + run_id=run_id, + terminal_state="BLOCKED", + dismissed_at="2026-08-08T10:01:00+00:00", + dismissed_by="test_operator", + ) + agent = FakeAgent(AgentResult("COMPLETE")) + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None) + + with self.assertRaisesRegex(RunnerError, "already been dismissed"): + runner.run(self.prompt, run_id=run_id, resume=True) + + self.assertEqual(agent.prompts, []) + self.assertEqual(self.store.load(run_id).phase, "EXECUTE_AGENT") + def test_resume_recomputes_waiting_phase_from_pr_evidence(self) -> None: self.store.save(TransactionState("resume-run", "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT", pull_request=11, diagnostic="Prior waiting diagnostic.")) pending = PullRequestEvidence(11, "OPEN", False, False) diff --git a/tests/engineering/test_inbox_watcher.py b/tests/engineering/test_inbox_watcher.py index 323ceb5d..4b2a2c55 100644 --- a/tests/engineering/test_inbox_watcher.py +++ b/tests/engineering/test_inbox_watcher.py @@ -852,7 +852,7 @@ def test_execution_retry_supports_failed_and_refuses_non_retryable_or_duplicate_ with self.assertRaisesRegex(inbox_watcher.RetrySubmissionError, "staat al in de wachtrij"): inbox_watcher.submit_execution_retry(self.repo, self.root, run_id) - def test_dismiss_terminal_execution_clears_operational_state_and_preserves_audit(self) -> None: + def test_dismiss_terminal_execution_persists_immutable_handling_and_blocks_retry(self) -> None: run_id = "inbox-dismissed" runs = self.repo / ".engineering" / "engineering-runs" runs.mkdir(parents=True, exist_ok=True) @@ -864,8 +864,13 @@ def test_dismiss_terminal_execution_clears_operational_state_and_preserves_audit self.assertTrue(outcome["dismissed"]) self.assertEqual(json_status(self.repo)["watcher_state"], "WATCHER_IDLE") self.assertIsNone(json_status(self.repo)["last_executed_run"]) - audit = json.loads((status / "execution_dismissals.json").read_text(encoding="utf-8")) - self.assertEqual(audit[-1]["run_id"], run_id) + self.assertEqual(outcome["terminal_state"], "BLOCKED") + self.assertEqual(outcome["handling_state"], "DISMISSED") + with self.assertRaisesRegex(inbox_watcher.RetrySubmissionError, "al afgesloten"): + inbox_watcher.submit_execution_retry(self.repo, self.root, run_id) + history = __import__("tools.engineering.prompt_history", fromlist=["prompt_history"]).prompt_history(self.repo) + self.assertTrue(history[0]["dismissed"]) + self.assertEqual(history[0]["status"], "BLOCKED") def test_migration_moves_legacy_archives_and_removes_iCloud_status(self) -> None: (self.root / "Completed").mkdir() diff --git a/tests/engineering/test_prompt_history.py b/tests/engineering/test_prompt_history.py index b57dfa3f..5d584372 100644 --- a/tests/engineering/test_prompt_history.py +++ b/tests/engineering/test_prompt_history.py @@ -82,6 +82,10 @@ def test_records_terminal_run_and_serves_only_its_local_report(self) -> None: "producer_submission_contract_version": None, "execution_context_version": None, "execution_context": None, + "dismissed": False, + "handling_state": "OPEN", + "dismissed_at": None, + "dismissed_by": None, "retry_child_run_id": None, "retry_status": None, "queued_retry_child": False, diff --git a/tools/engineering/ENGINEERING_INBOX_PROTOCOL.md b/tools/engineering/ENGINEERING_INBOX_PROTOCOL.md index 60e4c9ae..759a4fde 100644 --- a/tools/engineering/ENGINEERING_INBOX_PROTOCOL.md +++ b/tools/engineering/ENGINEERING_INBOX_PROTOCOL.md @@ -216,10 +216,13 @@ and clears Active Execution so the watcher returns to idle without changing the queue. The confirmation shows Run ID, prompt title and terminal state, and explains that no work will restart. -Dismiss records `dismissed`, `dismissed_at` and `dismissed_by` in local audit -evidence. Engineering Reports, terminal evidence, telemetry, Prompt History -and retry relationships remain immutable. A dismissed `BLOCKED` execution may -still be retried later, while Queue Recovery remains the separate explicit -operation for dependent Inbox work. Dismiss never resumes that queue. +Dismiss records `dismissed`, `dismissed_at` and `dismissed_by` as immutable +operator-handling evidence in the canonical SQLite datastore. Engineering +Reports, terminal evidence, telemetry, Prompt History and retry relationships +remain immutable. A dismissed terminal execution is read-only: Retry, Resume, +Dismiss and every other lifecycle-mutating action are unavailable and rejected +server-side, including requests from a stale client. Queue Recovery remains the +separate explicit operation for dependent Inbox work. Dismiss never resumes +that queue. Commands: `python3 -m tools.engineering.inbox_watcher once|run|status|install|uninstall|doctor|migrate-icloud-archives`. diff --git a/tools/engineering/assets/dashboard.js b/tools/engineering/assets/dashboard.js index 392448c4..b0d22367 100644 --- a/tools/engineering/assets/dashboard.js +++ b/tools/engineering/assets/dashboard.js @@ -2758,7 +2758,7 @@ function renderPromptHistory() { button.addEventListener("click", () => openPromptHistoryChat(entry)); chat.append(button); } else chat.textContent = "—"; - if (entry.can_retry === true && entry.run_id) { + if (entry.can_retry === true && !entry.dismissed && entry.run_id) { const retry = document.createElement("button"); retry.type = "button"; retry.className = "predecessor-retry execution-history-action"; @@ -3523,6 +3523,8 @@ function promptDetailExecutionSection(history) { ] : [detailField(t("execution_context.snapshot"), t("execution_context.not_supplied"))]; return promptDetailCard(t("detail.execution"), [ promptDetailStatusField(history.status), + detailField(t("detail.operator_handling"), history.dismissed ? t("handling.dismissed") : t("handling.open")), + ...(history.dismissed_at ? [detailField(t("detail.dismissed_at"), history.dismissed_at)] : []), detailField(t("detail.prompt_title"), history.title), detailField(t("detail.run_id"), history.run_id, true), detailField( diff --git a/tools/engineering/assets/dashboard_locales.mjs b/tools/engineering/assets/dashboard_locales.mjs index 86626a2c..91c1c2c9 100644 --- a/tools/engineering/assets/dashboard_locales.mjs +++ b/tools/engineering/assets/dashboard_locales.mjs @@ -74,6 +74,8 @@ export const DASHBOARD_MESSAGES = { "detail.not_recorded": "Not recorded.", "detail.output_tokens": "Output tokens", "detail.prompt_status": "Execution status", + "detail.operator_handling": "Operator handling", + "detail.dismissed_at": "Dismissed at", "detail.prompt_title": "Execution title", "detail.provider_usage": "AI provider usage", "detail.plan_remaining": "Remaining in plan", @@ -94,6 +96,9 @@ export const DASHBOARD_MESSAGES = { "dismiss.details": "Run ID: {run_id}\nExecution title: {title}\nTerminal state: {state}\n\nExecution history, reports, telemetry and retry relationships are preserved. Only operational active state is cleared. No engineering work will restart.", "dismiss.failed": "Dismiss Execution could not be completed.", "dismiss.title": "Dismiss Execution", + "handling.dismissed": "Dismissed / Closed", + "handling.open": "Open", + "retry.dismissed": "This execution has already been dismissed; retry is unavailable.", "enum.CAPABILITY": "Capability", "enum.ENGINEERING": "Engineering", "enum.FAIL": "Failed", @@ -502,6 +507,8 @@ export const DASHBOARD_MESSAGES = { "detail.not_recorded": "Niet vastgelegd.", "detail.output_tokens": "Uitvoertokens", "detail.prompt_status": "Uitvoeringsstatus", + "detail.operator_handling": "Operatorafhandeling", + "detail.dismissed_at": "Afgesloten op", "detail.prompt_title": "Uitvoeringstitel", "detail.provider_usage": "AI-providergebruik", "detail.plan_remaining": "Resterend in plan", @@ -522,6 +529,9 @@ export const DASHBOARD_MESSAGES = { "dismiss.details": "Run-ID: {run_id}\nUitvoeringstitel: {title}\nEindstatus: {state}\n\nUitvoeringsgeschiedenis, rapporten, telemetrie en retry-relaties blijven bewaard. Alleen de actieve operationele status wordt gewist. Engineering wordt niet opnieuw gestart.", "dismiss.failed": "De uitvoering kon niet worden afgesloten.", "dismiss.title": "Uitvoering afsluiten", + "handling.dismissed": "Afgesloten", + "handling.open": "Open", + "retry.dismissed": "Deze uitvoering is al afgesloten; opnieuw proberen is niet beschikbaar.", "enum.CAPABILITY": "Capability", "enum.ENGINEERING": "Engineering", "enum.FAIL": "Mislukt", @@ -930,6 +940,8 @@ export const DASHBOARD_MESSAGES = { "detail.not_recorded": "Nicht erfasst.", "detail.output_tokens": "Ausgabetoken", "detail.prompt_status": "Ausführungsstatus", + "detail.operator_handling": "Operatorbearbeitung", + "detail.dismissed_at": "Geschlossen am", "detail.prompt_title": "Ausführungstitel", "detail.provider_usage": "KI-Anbieternutzung", "detail.plan_remaining": "Im Tarif verbleibend", @@ -950,6 +962,9 @@ export const DASHBOARD_MESSAGES = { "dismiss.details": "Run-ID: {run_id}\nAusführungstitel: {title}\nEndstatus: {state}\n\nAusführungshistorie, Berichte, Telemetrie und Wiederholungsbeziehungen bleiben erhalten. Nur der aktive Betriebsstatus wird gelöscht. Es wird keine Engineering-Arbeit neu gestartet.", "dismiss.failed": "Die Ausführung konnte nicht geschlossen werden.", "dismiss.title": "Ausführung schließen", + "handling.dismissed": "Geschlossen", + "handling.open": "Offen", + "retry.dismissed": "Diese Ausführung wurde bereits geschlossen; eine Wiederholung ist nicht verfügbar.", "enum.CAPABILITY": "Capability", "enum.ENGINEERING": "Engineering", "enum.FAIL": "Fehlgeschlagen", @@ -1305,6 +1320,8 @@ export const DASHBOARD_MESSAGES = { "detail.not_recorded": "Non enregistré.", "detail.output_tokens": "Jetons de sortie", "detail.prompt_status": "État de l’exécution", + "detail.operator_handling": "Traitement opérateur", + "detail.dismissed_at": "Clôturée le", "detail.prompt_title": "Titre de l’exécution", "detail.provider_usage": "Utilisation du fournisseur IA", "detail.plan_remaining": "Restant dans le forfait", @@ -1325,6 +1342,9 @@ export const DASHBOARD_MESSAGES = { "dismiss.details": "ID d’exécution : {run_id}\nTitre de l’exécution : {title}\nÉtat final : {state}\n\nL’historique, les rapports, la télémétrie et les relations de relance sont conservés. Seul l’état opérationnel actif est effacé. Aucun travail d’ingénierie ne redémarrera.", "dismiss.failed": "L’exécution n’a pas pu être clôturée.", "dismiss.title": "Clore l’exécution", + "handling.dismissed": "Clôturée", + "handling.open": "Ouvert", + "retry.dismissed": "Cette exécution a déjà été clôturée ; la relance est indisponible.", "enum.CAPABILITY": "Capacité", "enum.ENGINEERING": "Ingénierie", "enum.FAIL": "Échec", @@ -1680,6 +1700,8 @@ export const DASHBOARD_MESSAGES = { "detail.not_recorded": "No registrado.", "detail.output_tokens": "Tokens de salida", "detail.prompt_status": "Estado de la ejecución", + "detail.operator_handling": "Gestión del operador", + "detail.dismissed_at": "Cerrada el", "detail.prompt_title": "Título de la ejecución", "detail.provider_usage": "Uso del proveedor de IA", "detail.plan_remaining": "Restante en el plan", @@ -1700,6 +1722,9 @@ export const DASHBOARD_MESSAGES = { "dismiss.details": "ID de ejecución: {run_id}\nTítulo de la ejecución: {title}\nEstado final: {state}\n\nSe conservan el historial, los informes, la telemetría y las relaciones de reintento. Solo se borra el estado operativo activo. No se reiniciará ningún trabajo de ingeniería.", "dismiss.failed": "No se pudo cerrar la ejecución.", "dismiss.title": "Cerrar ejecución", + "handling.dismissed": "Cerrada", + "handling.open": "Abierta", + "retry.dismissed": "Esta ejecución ya se cerró; no se puede reintentar.", "enum.CAPABILITY": "Capacidad", "enum.ENGINEERING": "Ingeniería", "enum.FAIL": "Fallido", diff --git a/tools/engineering/execution_host.py b/tools/engineering/execution_host.py index 4dc837a2..64e6d0d6 100644 --- a/tools/engineering/execution_host.py +++ b/tools/engineering/execution_host.py @@ -83,6 +83,7 @@ from .execution_finalization import FinalizationCoordinator from .execution_reporting import ReportingCoordinator from .storage import load_readiness_evaluation, record_readiness_evaluation +from .storage import dismissal_for_run # Compatibility exports remain at this façade while implementation resides in # the dedicated context, repository and executor modules. @@ -320,6 +321,8 @@ def run( ) -> TransactionState: objective = prompt_path.read_text(encoding="utf-8") state = self.store.load(run_id) if resume else None + if resume and state is not None and dismissal_for_run(self.root, state.run_id): + raise RunnerError("This execution has already been dismissed and cannot be resumed.") try: context = resolve_execution_context(objective, self.root) except RunnerError as error: diff --git a/tools/engineering/inbox_watcher.py b/tools/engineering/inbox_watcher.py index 8404a1ca..86e01b92 100644 --- a/tools/engineering/inbox_watcher.py +++ b/tools/engineering/inbox_watcher.py @@ -43,7 +43,7 @@ from .capability_preflight import execute as execute_capability_preflight from .producer import ProducerSubmissionError, parse_producer_metadata, parse_producer_submission from .drift_diagnostics import summary as drift_summary -from .storage import EngineeringStorageError, load_projection, open_storage, record_artifact, record_submission +from .storage import EngineeringStorageError, dismissal_for_run, load_projection, open_storage, record_artifact, record_execution_dismissal, record_submission from .execution_lease import liveness as lease_liveness, reconcile_stale LABEL = "com.djconnect.engineering-inbox" @@ -608,6 +608,8 @@ def retry_admission_preflight(repo: Path, run_id: str) -> None: """ if not re.fullmatch(r"inbox-[a-z0-9-]{6,64}", run_id): raise RetrySubmissionError("De opgegeven run-ID is ongeldig.") + if dismissal_for_run(repo, run_id): + raise RetrySubmissionError("Deze uitvoering is al afgesloten; opnieuw proberen is niet beschikbaar.") archived = _archived_prompt_for_run(repo, run_id) if archived is None: raise RetrySubmissionError("De oorspronkelijke terminale prompt is lokaal niet beschikbaar.") @@ -655,19 +657,27 @@ def dismiss_execution(repo: Path, run_id: str, *, dismissed_by: str = "dashboard phase = _terminal_phase_for_run(repo, run_id) if phase not in TERMINAL_PHASES or current.get("last_executed_run") != run_id: raise RetrySubmissionError("Alleen de huidige terminale uitvoering kan worden bevestigd.") + if dismissal_for_run(repo, run_id): + raise RetrySubmissionError("Deze uitvoering is al afgesloten.") timestamp = datetime.now(timezone.utc).isoformat() - audit_path = status_path.with_name("execution_dismissals.json") + connection = open_storage(repo) try: - records = json.loads(audit_path.read_text(encoding="utf-8")) if audit_path.exists() else [] - except (OSError, json.JSONDecodeError) as error: + history_exists = connection.execute( + "SELECT 1 FROM prompt_execution_history WHERE run_id=?", (run_id,) + ).fetchone() is not None + finally: + connection.close() + if not history_exists: + record_prompt_execution( + repo, run_id=run_id, terminal_state=phase, + prompt_title=current.get("last_executed_title") or run_id, executed_at=timestamp, + ) + try: + record = record_execution_dismissal( + repo, run_id=run_id, terminal_state=phase, dismissed_at=timestamp, dismissed_by=dismissed_by, + ) + except EngineeringStorageError as error: raise RetrySubmissionError("De dismissal-audit is niet veilig beschikbaar.") from error - if not isinstance(records, list): - raise RetrySubmissionError("De dismissal-audit is ongeldig.") - record = {"run_id": run_id, "terminal_state": phase, "dismissed": True, "dismissed_at": timestamp, "dismissed_by": dismissed_by} - records.append(record) - temporary = audit_path.with_suffix(".tmp") - temporary.write_text(json.dumps(records, separators=(",", ":"), sort_keys=True) + "\n", encoding="utf-8") - os.replace(temporary, audit_path) status( repo, "WATCHER_IDLE", @@ -687,6 +697,8 @@ def submit_execution_retry(repo: Path, root: Path, run_id: str, *, queue_recover if not re.fullmatch(r"inbox-[a-z0-9-]{6,64}", run_id): raise RetrySubmissionError("De opgegeven run-ID is ongeldig.") with _lock(repo): + if dismissal_for_run(repo, run_id): + raise RetrySubmissionError("Deze uitvoering is al afgesloten; opnieuw proberen is niet beschikbaar.") terminal_phase = _terminal_phase_for_run(repo, run_id) if terminal_phase not in BLOCKING_PREDECESSOR_PHASES: raise RetrySubmissionError("Alleen een terminal geblokkeerde of mislukte uitvoering kan opnieuw worden uitgevoerd.") diff --git a/tools/engineering/prompt_history.py b/tools/engineering/prompt_history.py index cb72599e..baa68bf3 100644 --- a/tools/engineering/prompt_history.py +++ b/tools/engineering/prompt_history.py @@ -339,11 +339,13 @@ def prompt_history( COALESCE(submission.engineering_action_id, runs.engineering_action_id), runs.execution_constraint_version, submission.submission_id, submission.contract_version, submission.execution_context_version, - submission.execution_context_snapshot + submission.execution_context_snapshot, dismissal.terminal_state, + dismissal.handling_state, dismissal.dismissed_at, dismissal.dismissed_by FROM prompt_execution_history AS history LEFT JOIN execution_runs AS runs ON runs.run_id = history.run_id LEFT JOIN execution_submission_links AS submission_link ON submission_link.run_id = history.run_id LEFT JOIN execution_submissions AS submission ON submission.submission_id = submission_link.submission_id + LEFT JOIN execution_dismissals AS dismissal ON dismissal.run_id = history.run_id ORDER BY history.executed_at DESC, history.run_id DESC LIMIT ? """, @@ -379,6 +381,10 @@ def prompt_history( "producer_submission_contract_version": row[23], "execution_context_version": row[24], "execution_context": json.loads(row[25]) if isinstance(row[25], str) else None, + "dismissed": row[26] is not None, + "handling_state": row[27] or "OPEN", + "dismissed_at": row[28], + "dismissed_by": row[29], } for row in rows ] @@ -401,7 +407,7 @@ def prompt_history( record["retry_timestamp"] = child.get("retry_timestamp") if child else record["retry_timestamp"] record["queued_retry_child"] = bool(child and child.get("status") == "QUEUED") record["active_retry_child"] = bool(child and child.get("status") == "ACTIVE") - record["can_retry"] = record.get("status") in {"BLOCKED", "FAILED"} and child is None + record["can_retry"] = record.get("status") in {"BLOCKED", "FAILED"} and child is None and not record["dismissed"] chain = [record["run_id"]] cursor = record while cursor.get("retry_of") and cursor["retry_of"] not in chain: diff --git a/tools/engineering/storage.py b/tools/engineering/storage.py index fd61b981..e526fdfd 100644 --- a/tools/engineering/storage.py +++ b/tools/engineering/storage.py @@ -18,7 +18,7 @@ WORKSPACE_DIRECTORY = ".engineering" DATABASE_FILENAME = "engineering.db" -ENGINEERING_STORAGE_SCHEMA_VERSION = 17 +ENGINEERING_STORAGE_SCHEMA_VERSION = 18 JOURNAL_MODES = frozenset({"DELETE", "MEMORY"}) @@ -556,6 +556,27 @@ def _schema_v17(connection: sqlite3.Connection) -> None: connection.execute("ALTER TABLE execution_submissions ADD COLUMN engineering_action_id TEXT") +def _schema_v18(connection: sqlite3.Connection) -> None: + """Persist immutable operator handling separately from terminal outcome.""" + connection.execute( + "CREATE TABLE execution_dismissals (" + "run_id TEXT PRIMARY KEY REFERENCES prompt_execution_history(run_id) ON DELETE RESTRICT," + "terminal_state TEXT NOT NULL CHECK(terminal_state IN ('COMPLETE','BLOCKED','FAILED')) ," + "handling_state TEXT NOT NULL CHECK(handling_state='DISMISSED')," + "dismissed_at TEXT NOT NULL,dismissed_by TEXT NOT NULL)" + ) + connection.execute( + "CREATE TRIGGER execution_dismissals_immutable_update " + "BEFORE UPDATE ON execution_dismissals BEGIN " + "SELECT RAISE(ABORT, 'Execution dismissal evidence is immutable.'); END" + ) + connection.execute( + "CREATE TRIGGER execution_dismissals_immutable_delete " + "BEFORE DELETE ON execution_dismissals BEGIN " + "SELECT RAISE(ABORT, 'Execution dismissal evidence is immutable.'); END" + ) + + MIGRATIONS: dict[int, Migration] = { 1: _schema_v1, 2: _schema_v2, @@ -574,9 +595,56 @@ def _schema_v17(connection: sqlite3.Connection) -> None: 15: _schema_v15, 16: _schema_v16, 17: _schema_v17, + 18: _schema_v18, } +def dismissal_for_run(root: Path, run_id: object) -> dict[str, object] | None: + """Return immutable operator-handling evidence from canonical SQLite.""" + if not isinstance(run_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", run_id): + return None + connection = open_storage(root) + try: + row = connection.execute( + "SELECT terminal_state,handling_state,dismissed_at,dismissed_by " + "FROM execution_dismissals WHERE run_id=?", (run_id,) + ).fetchone() + finally: + connection.close() + if row is None: + return None + return { + "run_id": run_id, + "terminal_state": row[0], + "dismissed": True, + "handling_state": row[1], + "dismissed_at": row[2], + "dismissed_by": row[3], + } + + +def record_execution_dismissal(root: Path, *, run_id: str, terminal_state: str, + dismissed_at: str, dismissed_by: str) -> dict[str, object]: + """Record one immutable dismissal after its terminal history row exists.""" + if terminal_state not in {"COMPLETE", "BLOCKED", "FAILED"}: + raise EngineeringStorageError("Dismissal requires a terminal execution outcome.") + connection = open_storage(root) + try: + connection.execute( + "INSERT INTO execution_dismissals(run_id,terminal_state,handling_state,dismissed_at,dismissed_by) " + "VALUES(?,?,?,?,?)", + (run_id, terminal_state, "DISMISSED", dismissed_at, dismissed_by), + ) + except sqlite3.IntegrityError as error: + raise EngineeringStorageError("Execution dismissal is already recorded or has no terminal history.") from error + finally: + connection.close() + return { + "run_id": run_id, "terminal_state": terminal_state, "dismissed": True, + "handling_state": "DISMISSED", "dismissed_at": dismissed_at, "dismissed_by": dismissed_by, + } + + def _encoded_payload(payload: dict[str, object]) -> str: return json.dumps(payload, separators=(",", ":"), sort_keys=True)