From ddd385cc4dfb7166d8660b0a498abd010f692667 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Thu, 29 Oct 2026 18:13:31 +0800
Subject: [PATCH] fix(chat): expose retained report execution failures
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
apps/presentation/dashboard/src/data/chat.ts | 2 +-
.../personal-workspace/steward-report.tsx | 3 ++-
docs/product/periodic-reports.md | 8 ++++++++
.../steward_report_fixture.py | 12 ++++++++++++
.../native_chat/steward_reports.py | 17 ++++++++++++++---
tests/test_native_steward_reports.py | 19 +++++++++++++++++++
6 files changed, 56 insertions(+), 5 deletions(-)
diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts
index 7649ac4c3a..df720acba4 100644
--- a/apps/presentation/dashboard/src/data/chat.ts
+++ b/apps/presentation/dashboard/src/data/chat.ts
@@ -2768,7 +2768,7 @@ const privateConversationSchema = z.object({
z.object({available: z.literal(true), enabled: z.boolean(), configured: z.boolean(),
schedule: z.record(z.string(), z.unknown()).nullable().optional(), next_due_at: z.string().nullable().optional(),
retained_window_due_at: z.string().nullable().optional(), generation_id: z.string().nullable().optional(),
- delivery_status: z.string().optional(), publication_verified: z.boolean().optional(),
+ delivery_status: z.string().optional(), execution_failed: z.boolean().optional(), publication_verified: z.boolean().optional(),
destination: z.string().optional()}),
z.object({available: z.literal(false), error_code: z.string()}),
]).optional(),
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/steward-report.tsx b/apps/presentation/dashboard/src/features/personal-workspace/steward-report.tsx
index da24073516..19a7c6114d 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/steward-report.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/steward-report.tsx
@@ -32,12 +32,13 @@ export function StewardReport({state, zh, busy, available, goalActive, onChange}
: state.configured ? (zh ? "已暂停 · 原窗口和回执保留" : "Paused · Original windows and receipts retained")
: (zh ? "尚未启用" : "Not enabled")}
{zh ? "汇总全部获准本机 Goal,包括停止项和读取缺口。每天汇报前24小时,生成与实际送达分别核验;状态监控照常运行。" : "Covers every authorized local Goal, including stopped Goals and read gaps. Daily reports cover the previous 24 hours; generation and delivery are verified separately. Monitoring continues."}
- {state.configured ? {zh ? "下次计划:" : "Next deadline: "}{stamp(state.next_due_at)}
+ {state.configured ?
{zh ? "下次日历计划:" : "Next calendar deadline: "}{stamp(state.next_due_at)}
{zh ? "最近窗口:" : "Latest window: "}{stamp(state.retained_window_due_at)} · {state.delivery_status === "delivered"
? (zh ? "原私聊全文已核验送达" : "Full private reply verified")
: state.delivery_status === "generation_failed" ? (zh ? "本期生成失败,证据保留;取消失败窗口后,后续到期可继续" : "Generation failed; evidence retained. Cancel this failed window to continue at a later deadline.")
: state.delivery_status === "generation_cancelled" ? (zh ? "失败窗口已取消,未完成或送达;原证据保留" : "Failed window cancelled, not completed or delivered; evidence retained")
: state.delivery_status === "unconfirmed" ? (zh ? "发送结果待确认,不重复发送" : "Send unconfirmed; no blind resend")
+ : state.execution_failed ? (zh ? "本期执行失败;原窗口保留,未自动重试" : "Execution failed; original window retained, no automatic retry")
: state.generation_id ? (zh ? "已生成,投递待核验" : "Generated; delivery pending") : (zh ? "尚未生成" : "Not generated")}
: null}
{zh ? "汇报时区" : "Report timezone"} {
const value = event.target.value; setZone(value);
diff --git a/docs/product/periodic-reports.md b/docs/product/periodic-reports.md
index acb9176bbe..8a1ae9edde 100644
--- a/docs/product/periodic-reports.md
+++ b/docs/product/periodic-reports.md
@@ -632,6 +632,14 @@ not replay the failed attempt; a later due window can progress. A changed future
plan is also allowed after retirement. Unknown model outcomes and partial or
unknown sends cannot take this exit; they keep the original recovery boundary.
+Execution failure is visible even when it happens before an editorial response:
+the original steward and managed Session health expose `execution_failed` for
+the exact current actor, window and subscription revision. The steward settings
+show the retained failed execution instead of “Not generated”; the next calendar
+deadline is not a promise that the held window can run. This observation grants
+no cancellation, retirement or retry. A correlated quota continuation that
+succeeds clears the current failure projection while retaining its failed predecessor.
+
| Cancelled failure on the original settings page | Narrow retained-failure state |
| --- | --- |
|  |  |
diff --git a/examples/personal-workspace-browser/steward_report_fixture.py b/examples/personal-workspace-browser/steward_report_fixture.py
index 9d1d1d6054..539070fa1d 100644
--- a/examples/personal-workspace-browser/steward_report_fixture.py
+++ b/examples/personal-workspace-browser/steward_report_fixture.py
@@ -22,11 +22,14 @@ def main() -> None:
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--baseline-assets-dir", type=Path)
parser.add_argument("--invalid-editorial", action="store_true", help="Return a known invalid synthetic report to inspect owner cancellation and retained failure.")
+ parser.add_argument("--host-failure", action="store_true", help="Fail the synthetic native model before an editorial result; retain the original window without retry authority.")
parser.add_argument("--managed-session", action="store_true", help="Exercise ordinary Goal Session native work with the existing task validator, instead of a scheduled report.")
parser.add_argument("--managed-report", action="store_true", help="Configure a calendar report for an existing local Agent Session through the packaged Goal settings.")
args = parser.parse_args()
if args.managed_session and args.managed_report:
parser.error("select one managed Session fixture journey")
+ if args.host_failure and (args.invalid_editorial or args.managed_session):
+ parser.error("select the pre-editorial report failure journey alone")
repo = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(repo), str(repo / "tests")]
from test_native_steward_private import steward
@@ -74,6 +77,15 @@ def main() -> None:
expected_plan_id=plan["plan_id"])["actor"]
install_model(data, root, sid)
transport.reconcile()
+ if args.host_failure:
+ from loopx.chat_agent import CodexChatAgentError
+ from loopx.capabilities.native_chat.codex_adapter import CodexAppServerAdapter
+
+ def fail_report(_adapter, _prompt, event_sink):
+ event_sink("turn.started", {"upstream_turn_id": "synthetic-unknown-report"})
+ raise CodexChatAgentError("Synthetic report host failure.")
+
+ CodexAppServerAdapter.start_turn = fail_report
if args.invalid_editorial:
helper = root / "report_model.py"
helper.write_text(helper.read_text().replace('"request_digest": request["request_digest"]', '"request_digest": "known-invalid"'))
diff --git a/loopx/capabilities/native_chat/steward_reports.py b/loopx/capabilities/native_chat/steward_reports.py
index 4355017740..05e4c626d3 100644
--- a/loopx/capabilities/native_chat/steward_reports.py
+++ b/loopx/capabilities/native_chat/steward_reports.py
@@ -88,6 +88,18 @@ def _known_generation_failure(self, actor: dict[str, Any], window: dict[str, Any
return {"turn_id": turn["turn_id"], "todo_id": request["todo_id"]}
return None
+ def _execution_failed(self, actor: dict[str, Any], window: dict[str, Any]) -> bool:
+ """Observe the current attempt; this grants no cancellation or retry."""
+ from .steward_turn import report_turn
+ turn = report_turn(self.controller.store, actor, window)
+ request = (turn or {}).get("loopx_request") or {}
+ native = ((turn or {}).get("response") or {}).get("native_loopx") or {}
+ return bool(turn and request.get("actor") == actor and request.get("work_kind") == "report"
+ and request.get("report_window_id") == window["window_id"]
+ and request.get("report_subscription_revision") == window["subscription_revision"]
+ and (turn.get("status") in {"failed", "interrupted", "timed_out"}
+ or native.get("status") == "failed"))
+
def _cancel_failed_window(self, actor: dict[str, Any], window: dict[str, Any],
failure: dict[str, str], *, cadence_locked: bool = False) -> None:
from ..periodic_report.cadence_journal import (
@@ -246,6 +258,7 @@ def health(self, binding_id: str) -> dict[str, Any]:
config.get("schedule"), now=datetime.now(timezone.utc)) if config.get("enabled") else None,
"retained_window_due_at": journal["window"]["due_at"] if journal else None,
"generation_id": generation, "delivery_status": delivery or "not_generated",
+ "execution_failed": bool(journal and self._execution_failed(actor, journal["window"])),
"publication_verified": bool(journal and self._published_window(journal["window"])),
"destination": "original_owner_private_chat_full_body"}
except (OSError, ValueError, TypeError, KeyError, StopIteration):
@@ -575,9 +588,7 @@ def health(self, session_id: str) -> dict[str, Any]:
folder = attempt_directory(self.root, window) if window else None
from .steward_turn import report_turn
turn = report_turn(self.controller.store, actor, window) if window else None
- native = ((turn or {}).get("response") or {}).get("native_loopx") or {}
- failed = bool(turn and (turn["status"] in {"failed", "interrupted", "timed_out"}
- or native.get("status") == "failed"))
+ failed = bool(window and self._execution_failed(actor, window))
published = bool(window and self._published_window(window))
returned = []
if published and turn and folder and (folder / "report.md").is_file():
diff --git a/tests/test_native_steward_reports.py b/tests/test_native_steward_reports.py
index a5af79e9db..93c12b5a02 100644
--- a/tests/test_native_steward_reports.py
+++ b/tests/test_native_steward_reports.py
@@ -551,6 +551,24 @@ def exhausted(adapter, _prompt, event_sink):
journal_path = turn_journal_path(reports.root, goal_id=actor["goal_id"], turn_key=native["resume_turn_key"])
frozen = original_path.read_bytes(), journal_path.read_bytes()
_, window = reports.current_window(owner)
+ failed_health = reports.health(owner)
+ assert failed_health["execution_failed"] and not failed_health["publication_verified"]
+ assert reports._known_generation_failure(actor, window) is None
+ if not local_session:
+ assert failed_health["delivery_status"] == "not_generated"
+ # Failure visibility must stay bound to this exact actor/window, including
+ # when the original Chat envelope itself says completed.
+ import copy
+ from loopx.capabilities.native_chat import steward_turn
+ for field, value in [("actor", {**actor, "agent_id": "another-agent"}),
+ ("work_kind", "monitor"), ("report_window_id", "another-window"),
+ ("report_subscription_revision", "another-revision")]:
+ foreign = copy.deepcopy(original)
+ foreign["loopx_request"][field] = value
+ with monkeypatch.context() as changed:
+ changed.setattr(steward_turn, "report_turn", lambda *_: foreign)
+ assert not reports.health(owner)["execution_failed"]
+ assert frozen == (original_path.read_bytes(), journal_path.read_bytes())
observations = []
proof = {"thread_id": store.load_session(sid)["upstream_thread_id"],
"turn_id": original["upstream_turn_id"], "only_original_user_item": True,
@@ -584,6 +602,7 @@ def observe(**kwargs):
assert request["actor"] == original["loopx_request"]["actor"]
assert request["turn_instance_id"] == original["loopx_request"]["turn_instance_id"] + ".quota-recovery"
assert result["response"]["native_loopx"]["status"] == ("committed" if recovery_succeeds else "failed"), result
+ assert reports.health(owner)["execution_failed"] is not recovery_succeeds
assert frozen == (original_path.read_bytes(), journal_path.read_bytes())
assert observations and all(x["turn_id"] == original["upstream_turn_id"] for x in observations)
assert all(x["thread_ids"] == [proof["thread_id"]] for x in observations)