From 89e32db6562da5afb2be916cfd2f670a3f7bf7ef Mon Sep 17 00:00:00 2001 From: Ouroboros Date: Thu, 27 Aug 2026 11:26:40 +0300 Subject: [PATCH] fix(review): preserve typed terminal facts and routes Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com> --- ouroboros/gateway/history.py | 2 ++ ouroboros/tools/plan_review_references.py | 8 ++++-- supervisor/message_bus.py | 2 ++ supervisor/workers.py | 7 ++++- tests/test_gateway_history.py | 25 ++++++++++++++++++ tests/test_inflight_indicator_seams.py | 1 + tests/test_plan_review_references.py | 32 +++++++++++++++++++++++ 7 files changed, 74 insertions(+), 3 deletions(-) diff --git a/ouroboros/gateway/history.py b/ouroboros/gateway/history.py index d2f64b7df..3d0ed4bc6 100644 --- a/ouroboros/gateway/history.py +++ b/ouroboros/gateway/history.py @@ -880,6 +880,8 @@ def _collect_chat_rows( rec["mime"] = str(entry.get("mime") or "") rec["download_url"] = str(entry["download_url"]) rec["caption"] = str(entry.get("caption") or "") + if "task_terminal_status" in entry: + rec["task_terminal_status"] = str(entry.get("task_terminal_status") or "") _copy_task_summary_metadata(rec, entry) for field in SUBAGENT_MESSAGE_FIELDS: if field in entry: diff --git a/ouroboros/tools/plan_review_references.py b/ouroboros/tools/plan_review_references.py index b7206d1dc..ca0b81781 100644 --- a/ouroboros/tools/plan_review_references.py +++ b/ouroboros/tools/plan_review_references.py @@ -43,9 +43,13 @@ def _emit_plan_review_reference( serialized = json.dumps(state, ensure_ascii=False, sort_keys=True, default=str) revision = sha256(serialized.encode("utf-8")).hexdigest() try: - chat_id = int(getattr(ctx, "current_chat_id", 0)) + from supervisor.message_bus import notification_chat_route + + chat_id = notification_chat_route(getattr(ctx, "current_chat_id", None), 1) + if chat_id is None: + chat_id = 1 except (TypeError, ValueError): - chat_id = 0 + chat_id = 1 ts = utc_now_iso() payload = { "type": "review_reference", "surface": "plan_review", diff --git a/supervisor/message_bus.py b/supervisor/message_bus.py index 0dbfc2fbf..f343658e4 100644 --- a/supervisor/message_bus.py +++ b/supervisor/message_bus.py @@ -915,6 +915,8 @@ def log_chat( for key in ("project_id", "project_name", "target_label", "status"): if key in meta: record[key] = meta[key] + if "task_terminal_status" in meta: + record["task_terminal_status"] = str(meta.get("task_terminal_status") or "") if filename: record["filename"] = filename if mime: diff --git a/supervisor/workers.py b/supervisor/workers.py index f11ba3778..6229c8d82 100644 --- a/supervisor/workers.py +++ b/supervisor/workers.py @@ -1479,7 +1479,12 @@ def _run_chat_task( ) except Exception: log.debug("Failed-turn typing announce failed", exc_info=True) - send_with_budget(chat_id, err_msg, task_id=failed_task_id) + send_with_budget( + chat_id, + err_msg, + task_id=failed_task_id, + progress_meta={"task_terminal_status": "failed"}, + ) except Exception: log.debug("Suppressed exception", exc_info=True) diff --git a/tests/test_gateway_history.py b/tests/test_gateway_history.py index 718330d89..7d7cc86cf 100644 --- a/tests/test_gateway_history.py +++ b/tests/test_gateway_history.py @@ -41,6 +41,31 @@ def test_chat_history_preserves_subagent_lane_group_metadata(tmp_path): assert rec["task_group_id"] == "group1" +def test_chat_history_replays_typed_direct_error_terminal_status(tmp_path): + logs = tmp_path / "logs" + logs.mkdir() + (logs / "chat.jsonl").write_text( + json.dumps({ + "ts": "2026-08-27T00:00:00Z", + "direction": "out", + "chat_id": 1, + "user_id": 7, + "text": "error", + "task_id": "failed-task", + "task_terminal_status": "failed", + }) + "\n", + encoding="utf-8", + ) + (logs / "progress.jsonl").write_text("", encoding="utf-8") + + endpoint = make_chat_history_endpoint(tmp_path) + response = asyncio.run(endpoint(SimpleNamespace(query_params={"limit": "10"}))) + payload = json.loads(response.body.decode("utf-8"))["messages"] + + rec = next(item for item in payload if item.get("task_id") == "failed-task") + assert rec["task_terminal_status"] == "failed" + + def test_chat_history_replays_delivered_document_row(tmp_path): """A persisted document chat row is replayed as a msg_type=document record so the frontend rebuilds the file bubble on reload from the durable URL.""" diff --git a/tests/test_inflight_indicator_seams.py b/tests/test_inflight_indicator_seams.py index fa1019cb6..7d4e24c87 100644 --- a/tests/test_inflight_indicator_seams.py +++ b/tests/test_inflight_indicator_seams.py @@ -158,6 +158,7 @@ def test_run_chat_task_unregisters_and_keys_error_final_when_agent_raises(tmp_pa args, kwargs = sent[0] assert args[0] == 5 assert kwargs.get("task_id") == str(agent.task["id"]) + assert kwargs.get("progress_meta") == {"task_terminal_status": "failed"} # The failed turn re-announces the activity<->client_message_id link right # before the keyed final, so the client retires the `Sending...` submission # even when the start announce never reached it. diff --git a/tests/test_plan_review_references.py b/tests/test_plan_review_references.py index 89596c558..b59fac382 100644 --- a/tests/test_plan_review_references.py +++ b/tests/test_plan_review_references.py @@ -55,6 +55,38 @@ def test_plan_reference_uses_shared_best_effort_log_event_seam(monkeypatch): } +def test_plan_reference_defaults_unbound_context_to_main_chat(monkeypatch): + events: queue.Queue = queue.Queue() + ctx = SimpleNamespace(event_queue=events) + state = {"current_attempt": {"fingerprint": "review-fingerprint"}, "waves": []} + calls = [] + + monkeypatch.setattr( + plan_review_references, + "emit_log_event", + lambda _queue, payload, **_kwargs: calls.append(payload), + ) + plan_review_references._emit_plan_review_reference(ctx, "task-1", state) + + assert calls[0]["chat_id"] == 1 + + +def test_plan_reference_preserves_explicit_panel_chat_zero(monkeypatch): + events: queue.Queue = queue.Queue() + ctx = SimpleNamespace(event_queue=events, current_chat_id=0) + state = {"current_attempt": {"fingerprint": "review-fingerprint"}, "waves": []} + calls = [] + + monkeypatch.setattr( + plan_review_references, + "emit_log_event", + lambda _queue, payload, **_kwargs: calls.append(payload), + ) + plan_review_references._emit_plan_review_reference(ctx, "task-1", state) + + assert calls[0]["chat_id"] == 0 + + def test_attempt_helper_publishes_immediately_after_the_canonical_write(monkeypatch): ctx = SimpleNamespace(event_queue=queue.Queue()) calls = []