P2-S6: Name the host's own typed failure and the leaf's model at a nanny terminal

A nanny that died on its own Codex lane was reported by the reviewer role it
played (73c216b8, "c-final-fable-review"), so the owner asked why Fable was
broken while the Fable run was alive and then cancelled by I2. Two facts
were missing, and they are separate facts: the HOST's last typed error, and
the LEAF's identity.

Both land where the reader already looks, on projections that already exist,
with no new host_route sub-dict (disposition R7). model_execution_projection
gains exactly one typed key, last_llm_error_kind, beside used_model and
provider; it reads the same usage['_last_llm_error_kind'] the loop already
keeps and a successful send already clears, so it names the failure of the
host's LAST model call rather than a sticky stale one. Each terminal_runs
row grows from {run_id, state} to carry model, profile_id and
selected_subagent_id, all already on the replayed RunCustody, so the sharing
costs zero extra reads; terminal_custody_notice names the leaf model when
the row carried one.

The precondition is covered, not assumed: half of the leaf exists only once
delegate_terminal_reconciliation has been persisted, and on the live row it
was null. With no persisted audit the notice renders nothing and the host
half stands alone; an older custody row without a model simply says less.
Deliberately not in outcome_axes: outcomes.py is at its line ceiling and the
axis statuses are not this reader's path. terminal_runs is in
_ENVELOPE_DISCLOSURE_FIELDS, so stored envelopes compare unequal once and
are rewritten by the audit-only refresh. gateway/contracts.py is frozen and
needs no edit: model_execution is already NotRequired[Dict[str, Any]].

Tests: the pinned terminal_runs shapes carry the leaf fields and the notice
names the leaf model with no live join; a dead host lane carries the typed
error kind on model_execution while an absent reconciliation renders only
the host half; the solve half is still preserved through an empty call,
which now visibly moves the error kind alone.
This commit is contained in:
Ouroboros 2026-09-12 01:54:53 +03:00
parent 0a1b5464e0
commit e9e739c11e
5 changed files with 65 additions and 7 deletions

View file

@ -128,8 +128,13 @@ def _audit_task_custody(drive_root: Any, mine: str, result: Dict[str, Any], *,
terminal_runs = []
try:
if not audit_failure:
# The LEAF's own identity, already on the replayed row (zero extra
# reads): without it a nanny terminal names only the role the host
# played and the reader asks why the leaf's model was broken (I9).
terminal_runs = sorted((
{"run_id": str(row.run_id), "state": str(row.terminal_state)}
{"run_id": str(row.run_id), "state": str(row.terminal_state),
"model": str(row.model), "profile_id": str(row.profile_id),
"selected_subagent_id": str(row.selected_subagent_id)}
for row in state.values()
if row.task_id == mine and row.settled and row.terminal_state in custody.TERMINAL_STATES
), key=lambda row: row["run_id"])
@ -182,7 +187,12 @@ def terminal_custody_notice(result: Mapping[str, Any]) -> str:
return ""
lines = []
if non_success:
shown = "; ".join(f"{row.get('run_id')}: {row.get('state')}" for row in non_success[:10])
# The leaf's model when the replayed row carried one, never a live join
# and never a guess: an older row without it simply says less.
shown = "; ".join(
f"{row.get('run_id')}: {row.get('state')}"
+ (f" on {row.get('model')}" if row.get("model") else "")
for row in non_success[:10])
omitted = len(non_success) - 10
lines.append("Confirmed delegated terminal receipts: " + shown
+ (f" (+{omitted} more in task details)" if omitted > 0 else "") + ".")

View file

@ -527,6 +527,10 @@ def model_execution_projection(usage: Dict[str, Any]) -> Dict[str, Any] | None:
"used_local": call.get("use_local"),
"provider": call.get("provider"),
"llm_call_id": call.get("llm_call_id"),
# The host's OWN last typed failure, beside the model it was running.
# A nanny that died on its own lane used to be reported by the reviewer
# role it played, so its death read as the delegated leaf's fault (I9).
"last_llm_error_kind": usage.get("_last_llm_error_kind") or None,
"source": "usable_solve_response" if call else "not_observed",
}

View file

@ -133,7 +133,8 @@ def test_boot_backfill_fixes_row_settled_in_a_previous_generation(tmp_path):
assert custody == {
"unreconciled": [], "trigger": "boot_backfill", "audit_status": "ok",
"open_run_ids": [], "pending_invocation_ids": [], "undisposed_patch_run_ids": [],
"terminal_runs": [{"run_id": "run-1", "state": "succeeded"}],
"terminal_runs": [{"run_id": "run-1", "state": "succeeded", "model": "",
"profile_id": "", "selected_subagent_id": ""}],
}

View file

@ -40,17 +40,27 @@ def test_real_dispatch_preserves_last_solve_through_empty_and_forced_calls(tmp_p
"used_model": "fallback", "reported_model": "Provider display alias",
"used_local": False, "provider": "openrouter",
"llm_call_id": ctx.accumulated_usage["llm_call_refs"][-1]["llm_call_id"],
"last_llm_error_kind": None,
"source": "usable_solve_response",
}
ctx.round_idx += 1
failed = dispatch(ctx, "empty", message={"role": "assistant", "content": "", "tool_calls": []})
assert failed[0] is None
assert model_execution_projection(ctx.accumulated_usage) == observed
after_empty = model_execution_projection(ctx.accumulated_usage)
# The solve half is preserved. The host's OWN last typed failure is a
# separate fact on the same projection and does move (I9): a nanny that
# died on its own lane must be readable without guessing at the leaf.
assert {k: v for k, v in after_empty.items() if k != "last_llm_error_kind"} == {
k: v for k, v in observed.items() if k != "last_llm_error_kind"}
assert after_empty["last_llm_error_kind"] == "provider_incomplete_response"
assert observed["last_llm_error_kind"] is None
assert not ctx.accumulated_usage["llm_call_refs"][-1].get("usable_solve_response")
# Forced/post-task calls use the same call recorder but not ordinary dispatch.
call_llm_with_retry(Model({"role": "assistant", "content": "wrap up"}, {}),
ctx.messages, "forced", [], "high", 1, ctx.drive_logs,
ctx.task_id, 3, None, ctx.accumulated_usage, attempt_cap=1)
# ...and a successful send clears the stale typed error, so the projection
# returns to the solve half alone.
assert model_execution_projection(ctx.accumulated_usage) == observed
refs = collect_trace_refs(ctx.accumulated_usage, {})["llm_call_refs"]
assert len(refs) == 3
@ -194,3 +204,28 @@ def test_owner_wait_source_retains_initial_route_and_marked_calls(tmp_path, monk
resume_native_loop(registry, saved, [], {}, restored, set())
assert model_execution_projection(restored) == expected
assert ctx.active_model == "fallback" and ctx.owner_wait_resume is None
def test_dead_host_lane_renders_its_own_half_without_inventing_the_leaf():
"""I9: the host's typed failure and the leaf's identity are SEPARATE facts.
A nanny that died on its own Codex lane used to be reported by the reviewer
role it played, so the owner asked why the leaf's model was broken while
that leaf was alive. The host half now rides model_execution beside the
model it was running; when the delegated reconciliation was never persisted
(it was null on the live row) the custody notice stays silent rather than
guessing at the leaf.
"""
from ouroboros.task_finalization import terminal_host_notice_text
usage = {
"initial_model_request": {"model": "primary", "use_local": False},
"llm_call_refs": [{"model": "host-lane-model", "llm_call_id": "call-1",
"provider": "claudexor", "usable_solve_response": True}],
"_last_llm_error_kind": "provider_outcome_unknown",
}
projected = model_execution_projection(usage)
assert projected["used_model"] == "host-lane-model"
assert projected["provider"] == "claudexor"
assert projected["last_llm_error_kind"] == "provider_outcome_unknown"
assert terminal_host_notice_text({"model_execution": projected}) == ""

View file

@ -17,6 +17,8 @@ def _cancelled_with_patch(tmp_path):
continuation_narrative={"text": text})
assert custody.emit(tmp_path, custody.STARTED, {
"run_id": "run-one", "task_id": "root", "route": "fixture",
"model": "fixture-model", "profile_id": "fixture-profile",
"selected_subagent_id": "fixture-actor",
"snapshot_id": "snapshot-one", "shape": {},
})
assert custody.emit(tmp_path, custody.SETTLED, {
@ -32,7 +34,10 @@ def test_cleanup_receipt_is_carried_before_final_delivery_without_rewriting_answ
audit = stored["delegate_terminal_reconciliation"]
assert audit["open_run_ids"] == audit["pending_invocation_ids"] == []
assert audit["undisposed_patch_run_ids"] == ["run-one"]
assert audit["terminal_runs"] == [{"run_id": "run-one", "state": "cancelled"}]
assert audit["terminal_runs"] == [{
"run_id": "run-one", "state": "cancelled", "model": "fixture-model",
"profile_id": "fixture-profile", "selected_subagent_id": "fixture-actor",
}]
before = copy.deepcopy(stored)
usage = {"terminal_origin": "model_final", "terminal_host_notice": "Budget stop retained."}
event = prepare_terminal_send_event(
@ -42,7 +47,9 @@ def test_cleanup_receipt_is_carried_before_final_delivery_without_rewriting_answ
)
assert event["text"] == text
assert event["terminal_host_notice"].startswith("Budget stop retained.")
assert "run-one: cancelled" in event["terminal_host_notice"]
# The leaf's own model rides the replayed row, so the nanny's terminal is
# not read as a verdict about the role the host played (I9).
assert "run-one: cancelled on fixture-model" in event["terminal_host_notice"]
assert "Pending patch decisions: run-one" in event["terminal_host_notice"]
assert load_task_result(tmp_path, "root") == before
@ -91,7 +98,8 @@ def test_custody_notice_is_bounded_and_does_not_duplicate_on_public_projection()
"delegate_terminal_reconciliation": {
"audit_status": "ok", "open_run_ids": [], "pending_invocation_ids": [],
"undisposed_patch_run_ids": [f"run-{i}" for i in range(12)],
"terminal_runs": [{"run_id": f"run-{i}", "state": "cancelled"} for i in range(12)],
"terminal_runs": [{"run_id": f"run-{i}", "state": "cancelled",
"model": "leaf-model"} for i in range(12)],
}}
public = public_task_result(row)
assert "+2 more in task details" in public["terminal_host_notice"]