Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")}</p>
<p>{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."}</p>
{state.configured ? <p>{zh ? "下次计划:" : "Next deadline: "}{stamp(state.next_due_at)}<br/>
{state.configured ? <p>{zh ? "下次日历计划:" : "Next calendar deadline: "}{stamp(state.next_due_at)}<br/>
{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")}</p> : null}
<div className="personal-capability-fields"><label>{zh ? "汇报时区" : "Report timezone"}<input value={zone} aria-invalid={!validZone} disabled={disabled} onChange={event => {
const value = event.target.value; setZone(value);
Expand Down
8 changes: 8 additions & 0 deletions docs/product/periodic-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- |
| ![Cancelled generation is not completed or delivered](../assets/personal-workspace/steward-reports/failed-window-cancelled.png) | ![Retained cancelled window in a narrow viewport](../assets/personal-workspace/steward-reports/cancelled-window-narrow.png) |
Expand Down
12 changes: 12 additions & 0 deletions examples/personal-workspace-browser/steward_report_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"'))
Expand Down
17 changes: 14 additions & 3 deletions loopx/capabilities/native_chat/steward_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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():
Expand Down
19 changes: 19 additions & 0 deletions tests/test_native_steward_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading