Merge pull request #1252 from razzant/presentation/truthful-cards

Truthful cards and history: presentation batch (#1011 #1087 #1110 #1154 #1061 #931 #1073 #516 #869 #498)
This commit is contained in:
Ouroboros 2026-09-24 16:42:23 +03:00 • committed by GitHub
commit 553d5ac3e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 3508 additions and 454 deletions

View file

@ -63,7 +63,7 @@ COST_RESULT_FIELDS = (
"accounted_upper_bound_usd", "accounted_upper_bound_usd_with_children", "cost_known",
"cost_accounting_status", "cost_accounting_error", "cost_final", "cost_with_children_partial",
"unknown_unmetered", "non_final_rows", "reserved_usd", "unresolved_upper_bound_usd",
"ledger_integrity_degraded",
"ledger_integrity_degraded", "cost_presentation",
)

View file

@ -302,6 +302,13 @@ pointer alone; a card in another chat keeps the excerpt.
- **Status renders as dot + text.** The dot carries the state at a glance, so
the sentence does not have to shout it in saturated colour and can sit at
ordinary reading contrast.
- **A known outcome owns the status; an unfinished lifecycle stands beside it.**
When a task's outcome is already settled while post-task work still runs, the
chip states that outcome from the five-word family and `Finalizing…` is a
SECOND, quieter fact next to it — not a replacement, not a sixth status word,
and never something the card title has to carry instead. The pair is one
accessible name, so a screen reader hears the outcome and the hold together.
An owner stop outranks the hold entirely and speaks for itself.
- **Neutral is a real state**, not an absence of one. A classification chip
(which agent, which family) is neutral: it is a tag, not an alarm.
A tone value the code actually emits (`muted`) must have a rule; falling
@ -767,7 +774,18 @@ the wait controls stay, and its first row of work gives it the title. The
collapsed header carries the tool count live and, once the turn ends, cost and
duration (a replayed header carries the count and cost; duration is a live
fact); its `updated` stamp follows the turn's own narration, never a host note
and never a tool call. The
and never a tool call. Money on that header is ONE line whose wording carries
its own openness, and it never renders an unknown as a number: `$0.00` only
where priced rows evidenced that zero, `up to $1.20` while the tracked subtotal
is still inexact, `Tracked: $1.20` with the reason stated in words
beside it (`some steps have no price`) when part of the same scope has no price
at all (`Tracked: up to $1.20` when that subtotal itself is inexact), and
`Cost unknown` for unpriced rows with no tracked amount. An empty, intact ledger
has no money line; an empty view with an integrity gap stays unknown. Neither
proves a free result. The explanation is text on the line,
never a hover-only title, because a tooltip is invisible on touch, to assistive
technology and in a copied line. Every part of that meta line is separated by a
real ` · ` text part for the same reason. The
host's `_is_direct_chat` fact keeps its host jobs (routing, census `kind`, Stop
custody, terminal rows) and, on the client, only the header pill (a direct turn
keeps the census verdict beside its block). A block whose only reason to exist

View file

@ -24,11 +24,11 @@ The manifest is the SSOT of the module→domain assignment (1:1, complete over t
| D14 | Skills & extensions | 56 | 0 |
| D15 | Memory, knowledge, consciousness & self-evolution | 22 | 0 |
| D16 | Observability, usage accounting & cost | 11 | 0 |
| D17 | Projects, workspaces & task results | 22 | 0 |
| D17 | Projects, workspaces & task results | 23 | 0 |
| D18 | Launcher, packaging, platform & shared substrate | 15 | 0 |
| D19 | Frozen contracts (ABI) | 10 | 0 |
| D20 | Presence | 10 | 0 |
| **total** | | **569** | **0** |
| **total** | | **570** | **0** |
## Dependency direction matrix (strict, pinned)
@ -755,6 +755,7 @@ No function body (≥ 10 normalized lines) is shared verbatim across domains. Ne
- `ouroboros/task_result_schema.py`
- `ouroboros/task_results.py`
- `ouroboros/task_status.py`
- `ouroboros/terminal_projection.py`
- `ouroboros/tools/project_journal.py`
- `ouroboros/workspace_admission.py`
- `ouroboros/workspace_executor.py`

View file

@ -1,6 +1,6 @@
# 1. High-Level Architecture
This chapter is the structural index of the running system: the module tree from the desktop launcher down through the supervisor to every core package, the gateway and CLI boundaries, the two-role process topology of launcher and server, the platform substrate their locks and fingerprints rest on, and the on-disk data layout. It exists so a reader can find the owner of any behaviour by name before reading its code, and so a renamed module or a moved durable file surfaces as a documentation change instead of silent drift. A tree row is an address — what the module does, the typed codes, events, state files and constants to grep for, and the section that owns its mechanism and rationale; a WHY stays in a row only where no section carries it.
This chapter indexes runtime owners: the launcher, supervisor, core modules, gateway and CLI, process topology, platform substrate and data layout. Module and durable-file moves must update this map. Each row names the owner, grep targets and the section explaining its mechanism; rationale stays in a row only when no section carries it.
```
User
@ -23,6 +23,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
│ ├── modules/project_activity.js ← pure `active_chat_activities` census projection for Project navigation dots; complete supervisor-ready snapshots clear absences, while partial/unavailable/disconnected reads retain explicitly unknown rows (§3 Liveness census and the chat header)
│ ├── modules/project_work_pointer.js ← Project-room pointer to an already loaded root card; no execution or history authority (§3 Project rooms)
│ ├── modules/model_wait.js ← Model-wait views and owner actions inside existing chat cards, through the shared decision ingress (§6 Quota and auth waits)
│ ├── modules/task_checkpoints.js, cancel_presentation.js ← Typed task checkpoints and recorded cancellation-cause text shared by Chat and Logs (§3)
│ ├── modules/dashboard.js, logs.js, costs.js, files.js ← Dashboard tab host; Logs (backfill plus live-stream duplicate guard); Costs (an open zero is never shown as free); Files browser over `/api/files/*` (§3 Dashboard, Files)
│ ├── modules/skills.js, marketplace.js, skill_review_card.js, skill_publish_flow.js ← Installed-skills UI; the ClawHub marketplace inside Skills; Skill Review chat cards; publish-dialog detail rows (§3 Skills and Widgets)
│ ├── modules/settings_ui.js, settings_catalog.js, settings_controls.js, settings_local_model.js, mcp_settings.js ← Settings page (Accounts → Secrets → Models → Agents); model-catalog refresh with a 25-second bound and a sequence guard; control binders; the local-model form; MCP cards that keep masked tokens until edited (§3 Settings and onboarding)
@ -94,6 +95,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
├── mutation_attribution.py ← Root-task baseline capture; clean-at-baseline candidates plus exact explicit predecessor adoption without erasing original dirt; terminal content fingerprints and committed interval delta (§6 Git and commit review)
├── process_interpreters.py ← Interpreter resolvers for the process launch surfaces: the pre-guard unversioned-Python resolver and the post-gates Node ladder — its probe EXECUTES a candidate, so it runs only after the dispatch gates (§2; CLI / Headless Boundary below)
├── post_task_checkpoint.py ← Durable root post-task phase/final-cost checkpoint shared by task finalization and Project naming recovery
├── terminal_projection.py ← Cognition-free settled-result projection; durable Main delivery through the existing outbox and maintenance scan (§3 Main rows; §6 Post-task reflection)
├── presence_profile.py ← Strict reviewed `presence:` behavior-profile parser (instructions, context topics, runtime defaults, portable capability requests)
├── presence_runtime.py ← Symbolic `main`/`light` defaults; owner-local overrides clamped to the global round limit
├── presence_capabilities.py ← Host-owned installation selections for portable Presence capability requests, including an optional owner-selected working folder
@ -149,7 +151,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
├── skill_review_usage.py ← Read-only cached projection of final physical-attempt rows for one exact `(review_skill, review_wave_id)`; no second ledger or persisted totals
├── usage_ledger.py ← Durable append-only ledger substrate: cross-process locking, atomic append+fsync, row/transition validation, torn-tail quarantine; one-way seam — accounting imports it, never the reverse (§6 Usage ledger substrate vs. accounting policy)
├── usage_compaction.py, usage_legacy_import.py ← Seq-preserving compaction of that monetary ledger, BESIDE the substrate rather than inside it, and the one-time legacy usage-telemetry import (§6 Budget tracking)
├── cost_projection.py ← The ONE projection of task cost for every producer: `accounted_upper_bound_usd`, null as None (never $0.00), `COST_OPENNESS_FIELDS` beside every amount; the retired `cost_usd[_with_children]` spellings are read-only tolerance (a diverged stored pair resolves deprecated-wins) (§6 Budget tracking)
├── cost_projection.py ← The ONE projection of task cost for every producer: `accounted_upper_bound_usd`, null as None (never $0.00), `COST_OPENNESS_FIELDS` beside every amount (the optional scoped `cost_presentation` carrier among them); the retired `cost_usd[_with_children]` spellings are read-only tolerance (a diverged stored pair resolves deprecated-wins) (§6 Budget tracking)
├── delegate_custody.py ← Durable custody for delegated (Claudexor) runs: the `delegate_run_*` rows in the canonical event log plus the compact incident projection `<drive_root>/logs/containment_faults.jsonl`; OWNED/FOREIGN/UNKNOWN replay, the per-intention `Idempotency-Key` (`retry_of`), typed cancel vocabulary (confirmed | requested | failed | containment_fault_run_may_still_be_live), the one `daemon_says_absent` predicate, patch-apply intent rows (`delegate_run_patch_apply_started`/`_resolved`), `run_not_owned`/`run_ownership_unknown` refusals (§6 Delegated subagents)
├── delegate_custody_reconcile.py, delegate_state_sweep.py ← Reconciliation sweeps of delegated runs (a review-owned row is cancelled only behind an owner cancellation; a review invocation is never re-posted) and the terminal-plus-age sweep of leftover recovery/supervision state (§6 Delegated subagents)
├── delegate_custody_usage.py ← Usage and terminal-state projections over custody rows; the cross-process custody usage lock; the reviewer usage observers (one `llm_usage` row per ledger attempt)

View file

@ -115,7 +115,9 @@ The recent-window read is the chat's own request, so `createHistoryControls` car
DESIGN's role/type and `_emit_progress.narration` survive `send_with_budget` and replay; legacy absence stays assistant. Host Service accepts exact-origin terminal replies in either voice (`tests/test_host_message_voice.py`; computed aliases remain review-only). Task progress callbacks capture address, lineage and attempt at binding, so late reviews cannot inherit another turn. Unkeyed progress/summary rows never mutate a live card; Logs and existing incident toasts remain.
Main receives ordinary main-thread dialogue, Project question mirrors and the host-stamped Project lifecycle rows — `project_handoff`, `project_started` and the terminal `project_completion_summary` (a System row, or the mirrored answer below; both end with the Project reference); all other Project traffic stays in the Project thread. A lifecycle row's `text` is plain dashboard text: the producer strips markdown once before durable write and live send, history normalizes older rows on read (`chat.jsonl` is never rewritten), and the renderer escapes any system row without `markdown: true` (except `skill_review`'s dedicated renderer). A completion row whose root ended with a model-authored final answer also carries it whole in the typed `completion_answer` (`projects_registry.mirrored_answer`: `model_final`, non-empty, a settled phase, within `MIRRORED_ANSWER_MAX_CHARS`, else absent — never cut); `chat.js` then feeds that answer to the ordinary `addMessage` path as an assistant message and `project_answer.js::decorateProjectRow` adds the fold and the Project reference (DESIGN “Project completion mirror”), so keys, history, the session snapshot, Copy and disposal are the ordinary message's. An origin-addressed notice (`task_not_started`, `task_start_unconfirmed`, `steer_not_delivered`) stays in its issuing chat on replay even when the target is bound to another Project (`project_dialogue.room_membership` uses `ORIGIN_ADDRESSED_NOTICE_TYPES`). A steering refusal for an owner act with no owner-message receipt is one factual System row, never model-authored speech.
`terminal_projection.py` owes the Project row and deferred Main mirror together. `write_task_result` stamps `canonical_terminal_projection_origin=terminal_transition` only on an accepted root terminal transition, never from an incoming write/projector/replica. This preserves the terminal-write/readiness crash gap without replaying historical markerless rows. Readiness precedes effects; a projection lock serializes callbacks, not result IO. Publication and retirement require `headless.terminal_task_files_ready`, preserving cancellation and split-root semantics: synthesis alone does not prove artifact completion. Tokens heal append/receipt crashes; malformed history rows are skipped, unreadable segments defer. Task-done isolates publication errors; checkpoint/startup retry, then the existing 600s sweep. Children skip root history recovery. Retirement requires durable Main registration or proven ineligibility and matching token/attempt/checkpoint/artifact facts. Checkpoint returns the re-read row, or `None` if its write failed. Early finals carry status/axes beside finalizing without ending liveness; settled speech replay refreshes canonical axes. Bounded outbox and at-least-once transport preclude exactly-once.
Main receives its own dialogue, Project quiz mirrors and `project_handoff`/`project_started`/`project_completion_summary` rows ending in the Project reference; other Project traffic stays there. Lifecycle `text` is plain: producers strip markdown before persistence/send, history normalizes legacy rows without rewriting `chat.jsonl`, and System rendering escapes text unless `markdown: true` or the dedicated `skill_review` renderer applies. `projects_registry.mirrored_answer` carries whole `completion_answer` only for nonempty `model_final` in a settled phase within `MIRRORED_ANSWER_MAX_CHARS`; otherwise absent, never cut. `chat.js::addMessage` renders that as an ordinary assistant message; `project_answer.js::decorateProjectRow` adds the fold/reference, retaining ordinary keys, history, snapshots, Copy and disposal (DESIGN "Project completion mirror"). `ORIGIN_ADDRESSED_NOTICE_TYPES` in `project_dialogue.room_membership` keeps `task_not_started`, `task_start_unconfirmed` and `steer_not_delivered` in their issuing chat on replay despite target binding. Owner steering refused without an owner-message receipt gets one factual System row.
Every unanswered Project quiz, waited on or not, is mirrored into Main from one keyed pointer, without a second durable message. `chat_decision.buildQuizCard` renders the same Project form plus the Project reference (DESIGN “Project question mirror”); its own-answer and option controls use the existing decision ingress. The first confirmed `answered` observation from any source—answer response, 409 winner, `quiz_state`, history or census—starts one five-second `MIRROR_SETTLE_MS` countdown. Later observations do not restart it. Only Main's copy retires, via `releaseMessageNode` inside the stable-viewport transaction; Project history remains. Focus inside the retiring copy moves to the next Main question or the keyboard owner's composer. `releaseViews`/`destroy` clear timers and markdown. A partial copy upgraded to a complete answered form binds its card before settlement and gets the same countdown; an initially answered pointer never mounts.
@ -163,13 +165,17 @@ Direct registry rows read current attempt/model waits from their live `TaskModel
`web/modules/project_activity.js` groups census rows by `project_id`, minus unbound/child/duplicate rows. Working/thinking/finalizing reuse `chat-live-typing`; queue is static neutral, budget/model/owner waits static amber; wait rows pass the chat card's `mergeModelWaits` admission. A same-row wait suppresses motion; independent work keeps it and both descriptions. Partial positives stay current; omissions, errors, disconnect and an unreadable question detail (`required_question_unavailable`) stay static unknown. Only complete supervisor-ready snapshots clear absence. The shared sequencer drops stale replies; reconnect needs fresh state. Collapsed Projects shows an aggregate beside its label. Patches keep row order, marker/menu/focus nodes, unread state.
Owner-message continuity is journal-backed: each locally sent owner row is kept (bounded, with its routing annotation) until a fetched history response returns the same `client_message_id`, and a full rebuild re-renders unconfirmed journal rows after the server response, so a stale history snapshot cannot erase a message the owner just sent. Task finalization is honest: a root's early final answer carries `task_phase="finalizing"` (the same fact replay derives from the open post-task checkpoint), the card holds a sticky `Finalizing…` phase, and `task_cost_finalized` is bookkeeping that never resolves a card. If an observed managed root disappears from a queue-authoritative snapshot whose request began after the observation, the state-refresh fan-out removes activity/cancel authority and starts one single-flight durable task-detail read; request generations prevent an older response from undoing a newer projection.
Owner rows and routing annotations remain in the bounded local journal until history returns their `client_message_id`; rebuilds restore unconfirmed rows after server history. Early root answers carry `task_phase="finalizing"`, also derived on replay from the open checkpoint. `task_cost_finalized` never closes a card. `log_events.taskTerminalSummary` projects settled task-scope status as `observedOutcome`: the outcome owns the chip, `task_phase_chip.setLiveCardPhaseSecondary` adds quieter `Finalizing…`, and one accessible name states both. Titles keep their task identity; tool failures stay diagnostics; slot recycling clears outcome and hold. `harness_presentation.joinMetaParts` joins all metadata, including executor identity, with copyable/spoken ` · ` text. When a queue-authoritative request begun after observation omits a managed root, state-refresh removes activity/cancel authority and starts one single-flight durable-detail read; request generations reject stale replies.
Two invariants close the class of liveness the server cannot name — typing-fed header phantoms and cards shielded by a client-held live-set. First, the history window is lineage-closed: subagent lineage older than the window's progress recency floor is kept only while the child still runs or finalizes, or its parent is REPRESENTED by the response (a row proving its card: telemetry, its own message or summary, a folded review group or plan reference it owns — a typed photo/document/quiz delivery does NOT count), or the parent is alive; an anchored child represents its own children in turn, so a nested swarm is kept or dropped whole, and a FINAL row failing that test is emitted without lineage fields (`ouroboros/gateway/history.py`), so replay cannot mint an unfinishable parent card while a visible or live parent keeps its completed children and their executor receipts. Second, liveness reconciles over the CARD SET, not only the activity census: a card is shielded by an id the census actually lists and by nothing else; an unvouched connected root card is finished ONLY by proven durable terminal detail; a card without a current result stays unconfirmed until a complete fresh activity snapshot excludes it, then uses the retained typed terminal fact or a neutral `Outcome unavailable` anchor, without task phase or controls. A root proven terminal (its `task_done` or its terminal detail) settles the descendant cards a lost child terminal left open from each child's OWN durable result — one single-flight read per child through the ordinary child-terminal path, never a cascade from the parent's outcome, never from the card-set scan; until such a read settles it, a child whose terminal frames were all lost keeps a `Working` chip without live dots — a card-level residual the header no longer masks.
#### Task cards, errors and reason lines
Task activity collapses into a live task card per root instead of flooding the transcript; `log_events.js` keeps the live task card and grouped task cards on one reducer across Chat and Dashboard Logs. Non-terminal LLM/tool/checkpoint failures stay inspectable timeline facts but do not promote the card; only authoritative terminal task truth changes terminal status, and unknown Chat event names do not acquire severity from keyword substrings. Dashboard Logs files a row under Errors from the same typed projection that paints its phase pill (`categorizeLogEvent` reads the `summarizeLogEvent` phase — a failed task_done, an errored/killed/timed-out tool, an LLM call failure, a review lifecycle error): a replayed `tools.jsonl` row reads its `is_error`/signal facts exactly like the live frame, typed `ok`/`level` facts outrank a failure-shaped name, the name-substring test survives only as the disclosed non-expanding remainder for an unknown name carrying no typed fact, and a task group keeps Errors once earned. A delegated harness run's `swarm_fanout` row (host `role="delegated_run"`) is labelled a delegated run, never a subagent. A failed child keeps a local `Failed` chip while its root continues. A card keeps a plain-text latest-activity line and an expandable timeline; server-truncated rows fetch the complete typed task record on demand. The compact card is a navigation projection, not the result authority; urgent toast/unread behavior stays confined to explicit `task_incident` facts, never inferred from severity. The ONE explanatory line under a terminal headline is `taskReasonDetail`, and that ONE line states EVERY fact the record holds, one clause per fact separated by ` · `, never one fact chosen over another: an owner-requested soft stop contributes no cause, a hard failure or cancellation names its execution reason, a non-accepted host acceptance decision states its reason as the owner sentence from the same cause table (no status word, no stored rationale), a task held by a blocking plan review states the objective's own reason, and the standing limitations of the same answer — deferred child results (`outcome_axes.objective.deferred_count`), a plan review still open at delivery (the result's `terminal_plan_review_open` flag, worded by the outcome class on `outcome_axes.execution.plan_review` when one is recorded) and an unreconciled custody debt — are stated beside that cause rather than instead of it, each stated once, with an unknown code still raw; Logs meta additionally names `review <status>` and `acceptance <status>`.
`log_events.js` keeps the live task card and grouped task cards on one reducer across Chat and Dashboard Logs. Nonterminal LLM/tool/checkpoint failures stay in details, without promotion; only authoritative task truth changes terminal status. Unknown Chat names gain no severity from keywords. `task_checkpoints.js` owns the typed vocabulary (re-exported as `log_events.taskCheckpointLabel`): context fit/reclaim/view, prompt prefix, cost/time pacing, nanny nudges, budget scope, services and forced-candidate drift. Unknown kinds name themselves; only a kindless positive `checkpoint_number` proves a periodic self-check. Checkpoints and `context_reclaim` appear in the expanded card, never as titles, collapsed narration or toasts. `checkpointHasProgressRow` suppresses only companion rows already spoken by `emit_progress`: periodic checks and three manual-reclaim failures. Global workers stay in Logs; `worker_starting` exposes slot/pid/phase and `worker_ready_window_extended` exposes slots/attempt/window/ceiling.
`categorizeLogEvent` uses `summarizeLogEvent`'s phase for Errors: task failure, tool error/signal/timeout, LLM failure and review error. Replayed `tools.jsonl` uses the same typed facts; `ok`/`level` outrank names. Keyword matching remains only the non-expanding legacy fallback for untyped unknown Logs events; group Errors stays sticky. A `swarm_fanout` with `role="delegated_run"` names a delegated run, not a child. Failed children keep local `Failed` while their root continues. Cards show a plain latest-activity line and expandable timeline; truncated rows fetch full typed results. Cards navigate results, never own them. Only explicit `task_incident` facts raise urgent toasts/unread state.
`taskReasonDetail` joins every recorded terminal cause/limitation once with ` · `: soft stop contributes none; hard failure names its execution reason; cancellation names source and flattened short recorded cause, with an actor only from `request_origin.kind="agent_task"`. `requested_by` proves no initiator; matching the task's own parent/root lineage states parent/ancestor relation without IDs (`cancel_presentation.cancelCauseClauses`, host twin `supervisor.cancel_publication.cancel_cause_clauses`). Non-accepted acceptance uses the cause-table sentence, without status/rationale; blocking plan hold uses the objective reason. Deferred children (`outcome_axes.objective.deferred_count`), open plan review (`terminal_plan_review_open`, worded by `outcome_axes.execution.plan_review`) and custody debt remain beside the primary cause. Unknown codes stay raw. Logs additionally names `review <status>` and `acceptance <status>`.
#### Child cards and executor presentation
@ -185,11 +191,11 @@ The task card's `Reviews` section is a read-only projection over independent dom
#### Card cost
Card cost is sticky task-scope evidence. Only frames carrying task accounting status, finality, subtree, reservation or unknown-cost fields may update it; an unrelated per-call `cost_usd` delta is never relabelled as the task total. Compact cards render one amount — the accounted upper bound worded `up to` while the ledger is open, plain once final — preferring the complete subtree projection; a running root's heartbeat may carry a non-final aggregate from the physical-attempt ledger, so the amount advances without a second timer, endpoint or client-side sum, and a history window replays the same non-final projection (`live_root_cost_projection`, the one cost owner) on a running or finalizing root's latest in-window progress row so a reload between heartbeats shows the same ceiling — a subtree with no attributable rows stays absent, never zero. Precedence: unavailable, then pending, then final, newer evidence winning within a class; costless frames cannot erase a known value. Dashboard and task-detail accounting derive from the physical-attempt ledger, not the card; compact review rows copy or sum no money, and Skill wave attempt/slot money appears only inside the lazy exact-job detail, joined from the same ledger via `physical_attempt_v1`.
Card cost accepts task accounting/finality/subtree/reservation/unknown fields, never per-call `cost_usd` alone. `cost_presentation` binds one scope's amount/facts (producer: §6 "Budget tracking"; words: DESIGN "Conversation activity block"). Root-tree evidence outranks own scope; within scope unavailable < pending < final, newer within each class. Costless frames erase nothing. `live_root_cost_projection` feeds nonfinal root heartbeats and history's latest in-window progress for running/finalizing roots, without a new timer, endpoint or client sum; no attributable subtree rows means absent, not zero. Dashboard/detail read the ledger, not cards. Compact Reviews sum/copy no money; Skill wave attempt/slot money joins that ledger through `physical_attempt_v1` only in lazy exact-job detail.
#### Stop and cancel controls
A Cancel action appears only on unfinished, unconverted root cards carrying the host-attested `cancelable=true`; pooled tasks and addressable native direct turns use the existing custody owner (§5), and card shape alone grants no control. The stop control is the shared dropdown above, sent with `cascade:true` (root plus live descendant subtree): the hard path answers only after teardown or a typed refusal; the graceful "Wrap up" path records a durable finalize intent and answers immediately with a typed 202 pending acknowledgement. Natural completion wins a race, a missing live task reconciles from the durable record, and a process that cannot be proven dead remains a visible refusal rather than being painted Cancelled. Cancelled root and child cards use the same `log_events.taskReasonDetail` projection of durable `cancel_origin`: recorded HTTP sources name the app Stop now / Wrap up transport, cascade scope is stated, and the initiator comes from recorded agent-task identity or `requested_by`, otherwise “not recorded”. History carries the same fields after reload; no card infers personal owner intent from HTTP or assigns an actor to a legacy row without provenance.
A Cancel action appears only on unfinished, unconverted root cards carrying the host-attested `cancelable=true`; pooled tasks and addressable native direct turns use the existing custody owner (§5), and card shape alone grants no control. The stop control is the shared dropdown above, sent with `cascade:true` (root plus live descendant subtree): the hard path answers only after teardown or a typed refusal; the graceful "Wrap up" path records a durable finalize intent and answers immediately with a typed 202 pending acknowledgement. Natural completion wins a race, a missing live task reconciles from the durable record, and a process that cannot be proven dead remains a visible refusal rather than being painted Cancelled. Root/child cards share `log_events.taskReasonDetail` over durable `cancel_origin`: HTTP sources name the app Stop now / Wrap up transport; cascade scope and proven parent relation remain distinct from the recorded agent-task initiator. `requested_by` never proves authorship. History preserves these facts; absent provenance stays absent.
#### Turning a card into a Project

View file

@ -529,7 +529,7 @@ The Pattern Register writer (`reflection._update_patterns`) REPLACES the whole d
The advisory rows a reflection or summary reads are ATTRIBUTED. Advisory runs are scoped by repository and several tasks legitimately review one checkout, so `collect_review_evidence` renders as `recent_advisory_runs` only the rows this task owns plus legacy rows carrying no owner at all (which stay unknown, never re-attributed); another task's rows reach the prompt under their own heading naming the owning task ids, and every row carries its owning task, attempt, phase and complete snapshot hash. Repository readiness (`current_repo`, open obligations, commit-readiness debt, the exact-snapshot match) stays repository-scoped, because that is what "can this checkout be committed" means. The split is keyed on ROW IDENTITY, never on a scope key: an empty repository key widens the candidate list to every advisory run on the drive, and the installation's history is not one task's record.
Only the root runs full post-task synthesis, once (`root_phase_checkpoint` makes the paid phase at-most-once across restart); children contribute evidence, never a second global synthesis. The owner's final answer does not wait for it: after the durable result is stored, the final `send_message` is delivered immediately while the buffered-return copy is RETAINED; both copies carry one `delivery_id` and the supervisor suppresses the duplicate through the durable registry in `supervisor/terminal_delivery.py`, whose bounded PENDING outbox (§5; one seam for the normal, cancel and reap terminal paths) records every root's answer as owed under the canonical `final:<tid>:<digest>` id at durable-result persistence time, before it is sent. Replays back off and are bounded; an exhausted or capacity-evicted row is dropped LOUDLY (full text preserved on disk, typed `terminal_delivery_exhausted` event, chat notice) — external transports stay at-least-once and that residual is disclosed. `task_done` still goes last through the buffered return, because an early `task_done` would release the queue slot and start child-drive cleanup while post-task still runs; a worker reaped during a hung synthesis has therefore already delivered the answer.
Only roots synthesize; `root_phase_checkpoint` makes paid synthesis at-most-once across restart, while children contribute evidence. Durable-result persistence owes the answer as `final:<tid>:<digest>` in `supervisor/terminal_delivery.py`'s bounded outbox (§5; normal/cancel/reap). `send_message` delivers immediately; the retained buffered copy shares its ID for durable dedupe. Replays use bounded backoff. Exhaustion/eviction preserves full text on disk, emits `terminal_delivery_exhausted` and a chat notice; external delivery remains at-least-once. Buffered `task_done` stays last to retain the slot/child drive during synthesis; a hung-synthesis reap need not lose the delivered answer. Project roots keep early answers in Project. Their canonical row and deferred Main mirror use `terminal_projection.settle_terminal_projection` via task-done/checkpoint/startup/maintenance; §3 "Main rows and host-stamped card rows" owns readiness, retirement and limits.
Synthesis receives a sealed final package from the durable result — the submitted final text, its artifact manifest and completion_observations. Full redacted action observations live in the canonical artifact store (`task.budget_drive_root or drive_root`), in the write-once `source_handles/context_checkpoints` store with verified `task_source` refs, before compact publication and outside deliverables and inferred readiness; their native reader `get_task_result(include_completion_source=true)` returns complete length/hash first, then explicit `source_start_char`/`source_end_char` ranges (`artifacts.text_source_range_projection`, the shared work-order range contract), with bytes, kind, path containment and SHA checked before any excerpt. Packet-only summary/reflection receive per-send-tool counts, each family's latest recorded return, and task-related skill readiness with coverage; full-source references are for later readers, not evidence the synthesizer has read. Positive observed facts correct error-trace impressions, while tool success does not prove owner receipt, empty material does not prove absence, and skill readiness does not attribute an owner's action to the task. Before context cleanup, `agent_task_pipeline.emit_task_results` also freezes `review_evidence.task_inputs` through `post_task_synthesis.capture_task_inputs`: the existing task-local owner corpus, intact question/answer provenance and the canonical split-root verification-receipt union. Summary and reflection receive the same complete redacted content through `reflection.task_inputs_prompt_section`, separate from bounded trace/review excerpts. A zero return code is positive evidence; an unrelated later pass cannot resolve another check's failure. Peer proposals stay attributed, and unavailable input is not evidence that approval or verification never existed. Recovery uses these stored observations and inputs rather than a later conversation. Summary trace pointers name the task and existing archive-aware reader (`ouroboros tasks watch <task_id> --jsonl`), not guessed flat log paths.
@ -603,7 +603,9 @@ Browser target policy is `browser_policy.py`: ordinary owner-control admission u
The time/cost/intrinsic pacing checkpoints carry `resource_facts`: incremental per-tool call/error counts and producer-reported duration intervals (which may overlap; unmeasured calls stay unknown), plus own-task, tree and delegated-tree ledger buckets (own and delegated explicitly overlap the tree total) and the shared unreserved global remainder. No argv, stdout, sleep/poll classification or new stop rule selects behavior, and prompt and checkpoint show the same facts.
`usage_accounting.py` is the single monetary policy authority for core-mediated model work, over the physical-attempt ledger. Every provider send has a unique attempt id and the durable lifecycle `reserved → dispatched → settled | unresolved`, or `reserved → released` before dispatch; a `dispatched` row enters `released` only through the typed pre-dispatch transport seam (connection/pool failures proving no request bytes were sent), while ordinary timeouts and unknown errors remain `unresolved`; each retry is a new attempt (SDK/stream boundaries are labelled opaque). The wrapper covers main and direct calls, children, scouts, all review surfaces, safety, synthesis, reflection, consciousness, retries and opaque SDK calls; root scopes include their task tree and post-task/review work exactly once. A reviewed external script or extension with model credentials is unknown/unmetered at each host-observed opaque boundary unless authoritative settlement exists. `cost_projection.py` is the ONE projection of task cost for every producer surface: `accounted_upper_bound_usd` names the settled+reserved+unresolved upper bound honestly, null projects as None (never `$0.00`), finality is never fabricated, the `COST_OPENNESS_FIELDS` markers ride beside every amount, and the retired `cost_usd[_with_children]` spellings are read-only tolerance for stored legacy records, never emitted.
`usage_accounting.py` owns monetary policy over the physical-attempt ledger. Each send gets an ID and `reserved → dispatched → settled | unresolved`, or pre-dispatch `reserved → released`. Only typed proof of no sent bytes (connection/pool failure) permits `dispatched → released`; timeouts/unknown errors remain unresolved. Each retry is a new attempt; SDK/stream boundaries are labelled opaque. Main/direct/child/scout/review/safety/synthesis/reflection/consciousness work, retries and opaque SDK calls are covered; root scopes count tree and post-task/review work once. External scripts/extensions with model credentials stay unknown/unmetered at host-observed opaque boundaries absent authoritative settlement.
`cost_projection.py` owns producer amounts/openness: `accounted_upper_bound_usd` = settled+reserved+unresolved, null is unknown, finality is never invented, and `COST_OPENNESS_FIELDS` accompanies amounts. Retired `cost_usd[_with_children]` stays read-only. `cost_presentation` binds amount/facts to one bucket (§11.1): `reconstruct_task_cost` owns own scope; root terminal/heartbeat/checkpoint/synthesis paths use root-tree, never falling from null subtree to own zero. `_usage_rows._summary`'s weighted `priced_rows`, `tracked_nonfinal_rows`, `accounting_open_rows` survive compaction. A price or retained bound evidences zero; empty/unpriced rows do not. Settled unpriced rows leave the known subtotal exact; estimates (zero included), retained bounds, open attempts and integrity gaps stay nonfinal. Terminal refresh/copyback, synthesis, heartbeat and history preserve scope; unknown never becomes sticky-final. Sums and `cost_final` are unchanged.
A proven abandoned send closes administratively as `settled` with `settle_reason="abandoned"`, `cost_usd=None` and `cost_final=false`: its reservation bound stays accounted, not confirmed spend, and the price remains non-final. A real late receipt may correct the same attempt once (`late_receipt`); a positive never-started receipt may instead release it through the existing `before_dispatch_failed:` seam. Ordinary terminal settlements and releases remain immutable. Full and incremental ledger validation preserve the same correction eligibility; compaction retains unresolved and abandoned chains for that exact-id join, while existing historical baseline groups remain aggregates (the full storage contract is [Usage-ledger compaction](../USAGE_COMPACTION.md)).

View file

@ -24,6 +24,7 @@ This chapter owns the ABI promise: which typed shapes and their parsing, normali
| `StateResponse.active_direct_turns`/`active_chat_activities` (phases queued/working/finalizing/budget_paused — one predicate `budget_pause_fact` decides budget pause; a consciousness wake-up is one of those `direct_chat` turns and introduces no third `kind`); `TypingOutbound` activity fields; `ChatOutbound.task_phase`/`task_terminal_status` | `ouroboros/gateway/contracts.py`, `supervisor/active_activity.py`, `web/modules/chat_activity.js` | `tests/test_gateway_parity.py` + the activity test files |
| `ChatOutbound.card_row`/`card_row_id` — additive host-stamped placement fact on a task-keyed System row (`"timeline"` = a timeline item of the task's card; `"reviews"` = the card's Reviews group carries the fact and the row is still attached to the card; absent = an ordinary row) plus the row's stable identity across live delivery, outbox replay and history; stamped by the producer's `progress_meta`, persisted by `log_chat`, replayed by history | `ouroboros/gateway/contracts.py`, `supervisor/message_bus.py`, `ouroboros/gateway/history.py`, `supervisor/events_chat_delivery.py`, `ouroboros/acceptance_settlement.py`, `web/modules/api_types.js` | `tests/test_gateway_parity.py`, `tests/test_card_row_plumbing.py`, `tests/test_terminal_host_notice.py`, `web/tests/chat_card_row_placement.test.js` |
| `ChatOutbound.completion_answer` — additive: a Project root's model-authored final answer, whole, on Main's `project_completion_summary` row (absent for any other ending or row); stamped by the producer's `progress_meta`, persisted by `log_chat`, replayed by history unnormalized | `ouroboros/gateway/contracts.py`, `ouroboros/projects_registry.py`, `ouroboros/project_dialogue.py`, `supervisor/message_bus.py`, `ouroboros/gateway/history.py`, `web/modules/api_types.js` | `tests/test_gateway_parity.py`, `tests/test_project_completion_mirror.py`, `web/tests/chat_plain_system_rows.test.js` |
| `ChatOutbound.cost_presentation` — additive: ONE optional scoped carrier of the facts that explain the amount beside it, `{scope: "own"\|"root_tree", tracked_amount (nullable), has_unpriced, tracked_final, accounting_open, has_rows}`, built by the producer that summed the ledger rows (`cost_projection.build_cost_presentation` over `_usage_rows._summary`'s `priced_rows`) so a consumer can never pair one scope's number with another scope's openness; `tracked_amount` stays null unless a priced or bounded row evidenced it, and a root's null subtree amount never falls back to the root's own zero; null when the ledger is unreadable. Monetary names, sums and `cost_final` are unchanged | `ouroboros/cost_projection.py`, `ouroboros/_usage_rows.py`, `supervisor/state.py`, `ouroboros/post_task_checkpoint.py`, `supervisor/events_task_done.py`, `ouroboros/gateway/contracts.py`, `web/modules/api_types.js`, `web/modules/chat_activity.js` | `tests/test_cost_presentation_scope.py`, `tests/test_gateway_parity.py`, `web/tests/cost_presentation.test.js` |
| `ChatOutbound.initiator` — additive origin label of a self-initiated turn (`"consciousness"` on every frame and chat/progress/summary row of a consciousness wake-up and of the roots it starts; absent on an owner's turn); stamped by the turn's own event queue and the agent's frame meta, persisted by `log_chat`/the authored summary row/the task result, replayed by history on each row | `ouroboros/gateway/contracts.py`, `supervisor/log_addressing.py`, `ouroboros/subagent_messages.py`, `supervisor/message_bus.py`, `ouroboros/gateway/history.py`, `web/modules/api_types.js` | `tests/test_consciousness_initiator_label.py`, `tests/test_consciousness_wake_lane.py`, `web/tests/consciousness_label.test.js` |
| `project_thread` stamp on all seven outbound frame types, stamped at the message-bus broadcast choke; a stamped frame is never adopted by Main (`chat_activity.mainThreadAccepts`) | `supervisor/message_bus.py`, `ouroboros/projects_registry.py` | `tests/test_message_bus.py`, `web/tests/chat_thread_routing.test.js` |
| Media/link envelopes — media `task_id`/`size_bytes`/`download_url`; `LinkAction {label,url}` with at most twelve absolute HTTP(S) actions; `links` in `WS_MESSAGE_TYPES`; `chat.links` host topic | `ouroboros/gateway/contracts.py`, `ouroboros/tools/core.py`, `ouroboros/event_bus.py` | `tests/test_contracts.py` |

View file

@ -2,7 +2,7 @@
Machine extraction of the `docs/ARCHITECTURE.md` "Data layout (`~/Ouroboros/`)" tree — the durable-file orientation carrier (this tree's counterpart of the reference PERSISTENCE_OWNERS derivation checklist) — regenerated by `python scripts/regenerate_inventories.py`. Do not edit. Every entry is probed against reality: repo entries must exist as tracked paths; data-plane entries must appear as a literal in the runtime sources that construct them. A durable file renamed or removed in code while its tree row survives = red (`tests/test_generated_inventories.py`).
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 597-686; UTF-8 SHA-256 `87781e6db841d1d036d6c6f09911e7ce88a393cc8923714e49230ae7f9cad4fc`.
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 599-688; UTF-8 SHA-256 `152589b1f6ac43d258abd54a13cbd10c25186cf967e9bb1dcb4d97b91d729e94`.
- entries: **79** (code-ref: 72, repo-dir: 6, repo-path: 1)

View file

@ -2,7 +2,7 @@
AST-derived inventory of compatibility facades, regenerated by `python scripts/regenerate_inventories.py`. Do not edit. A facade row is any runtime module whose top-level `from <population module> import ...` statements carry the `noqa: F401` re-export marker — the codebase's declared "this binding exists for its binding, not for this module's own use" convention (reference FACADE_CONSUMERS method). Leaf domains come from `ouroboros/domains.toml`; a leaf outside the facade's domain is marked ✗ (that edge also appears in the manifest's pinned direction matrix). `tests/test_generated_inventories.py` pins byte-identity, so any re-export surface change must regenerate this file.
- facade modules: **61**; marked re-export bindings: **2321**; cross-domain facade→leaf pairs: **132**
- facade modules: **62**; marked re-export bindings: **2326**; cross-domain facade→leaf pairs: **133**
| facade | domain | bindings | leaves |
|---|---|---:|---|
@ -26,6 +26,7 @@ AST-derived inventory of compatibility facades, regenerated by `python scripts/r
| `ouroboros/loop_nudges.py` | D01 | 1 | `ouroboros/skill_readiness.py` (1 ✗D14) |
| `ouroboros/loop_round_limits.py` | D01 | 5 | `supervisor/owner_stop.py` (5 ✗D09) |
| `ouroboros/outcomes.py` | D01 | 7 | `ouroboros/outcome_receipt_store.py` (6)<br>`ouroboros/review_projection.py` (1 ✗D06) |
| `ouroboros/post_task_checkpoint.py` | D01 | 5 | `ouroboros/terminal_projection.py` (5 ✗D17) |
| `ouroboros/provider_models.py` | D02 | 3 | `ouroboros/settings_defaults.py` (3 ✗D12) |
| `ouroboros/review.py` | D06 | 1 | `ouroboros/tools/review_helpers.py` (1) |
| `ouroboros/review_cycles.py` | D06 | 1 | `ouroboros/outcomes.py` (1 ✗D01) |

View file

@ -2,9 +2,9 @@
Machine extraction of `docs/ARCHITECTURE.md` §11.1 (the frozen-ABI SSOT), regenerated by `python scripts/regenerate_inventories.py`. Do not edit — edit the owning chapter named in the Source line and regenerate; `tests/test_generated_inventories.py` pins byte-identity and the resolution invariants (a §11.1 row whose owner or anchor file disappeared from the tree = red).
Source: `docs/architecture/11-frozen-contracts-v1.md`, physical LF lines 7-39; UTF-8 SHA-256 `47aac4a4fb325ab5fe057456a4637cf910c4964a014be62dd0332bbfec368810`.
Source: `docs/architecture/11-frozen-contracts-v1.md`, physical LF lines 7-40; UTF-8 SHA-256 `18291245e49d5ff9de9c10b12bb635069104fdae46e47df0e4fb02c7523b5082`.
- table rows: **28**
- table rows: **29**
- browser-envelope prose owners:
| # | contract | owner files | anchored by |
@ -25,18 +25,19 @@ Source: `docs/architecture/11-frozen-contracts-v1.md`, physical LF lines 7-39; U
| 14 | `StateResponse.active_direct_turns` | `ouroboros/gateway/contracts.py` (ok)<br>`supervisor/active_activity.py` (ok)<br>`web/modules/chat_activity.js` (ok) | `tests/test_gateway_parity.py` (ok) |
| 15 | `ChatOutbound.card_row` | `ouroboros/gateway/contracts.py` (ok)<br>`supervisor/message_bus.py` (ok)<br>`ouroboros/gateway/history.py` (ok)<br>`supervisor/events_chat_delivery.py` (ok)<br>`ouroboros/acceptance_settlement.py` (ok)<br>`web/modules/api_types.js` (ok) | `tests/test_gateway_parity.py` (ok)<br>`tests/test_card_row_plumbing.py` (ok)<br>`tests/test_terminal_host_notice.py` (ok)<br>`web/tests/chat_card_row_placement.test.js` (ok) |
| 16 | `ChatOutbound.completion_answer` | `ouroboros/gateway/contracts.py` (ok)<br>`ouroboros/projects_registry.py` (ok)<br>`ouroboros/project_dialogue.py` (ok)<br>`supervisor/message_bus.py` (ok)<br>`ouroboros/gateway/history.py` (ok)<br>`web/modules/api_types.js` (ok) | `tests/test_gateway_parity.py` (ok)<br>`tests/test_project_completion_mirror.py` (ok)<br>`web/tests/chat_plain_system_rows.test.js` (ok) |
| 17 | `ChatOutbound.initiator` | `ouroboros/gateway/contracts.py` (ok)<br>`supervisor/log_addressing.py` (ok)<br>`ouroboros/subagent_messages.py` (ok)<br>`supervisor/message_bus.py` (ok)<br>`ouroboros/gateway/history.py` (ok)<br>`web/modules/api_types.js` (ok) | `tests/test_consciousness_initiator_label.py` (ok)<br>`tests/test_consciousness_wake_lane.py` (ok)<br>`web/tests/consciousness_label.test.js` (ok) |
| 18 | `project_thread` | `supervisor/message_bus.py` (ok)<br>`ouroboros/projects_registry.py` (ok) | `tests/test_message_bus.py` (ok)<br>`web/tests/chat_thread_routing.test.js` (ok) |
| 19 | `task_id` | `ouroboros/gateway/contracts.py` (ok)<br>`ouroboros/tools/core.py` (ok)<br>`ouroboros/event_bus.py` (ok) | `tests/test_contracts.py` (ok) |
| 20 | `QuizOption {label, detail?, recommended?}` | `ouroboros/gateway/contracts.py` (ok)<br>`ouroboros/gateway/task_decision.py` (ok)<br>`ouroboros/owner_quiz.py` (ok)<br>`ouroboros/tools/core.py` (ok) | `tests/test_gateway_parity.py` (ok)<br>`tests/test_quiz_display.py` (ok)<br>`tests/test_quiz_answer.py` (ok)<br>`web/tests/chat_decision.test.js` (ok) |
| 21 | `UpdateMergePlan` | `ouroboros/gateway/contracts.py` (ok) | `tests/test_update_apply_routing.py` (ok) |
| 22 | `ChatOutbound.review_projection` | `ouroboros/gateway/contracts.py` (ok) | `tests/test_review_substrate_v2.py` (ok)<br>`web/tests/review_truth.test.js` (ok) |
| 23 | `preflight_failed` | `ouroboros/skill_review_status.py` (ok) | `tests/test_skill_preflight_repair.py` (ok)<br>`web/tests/skill_preflight_repair.test.js` (ok) |
| 24 | `chat_id_policy` | `ouroboros/contracts/chat_id_policy.py` (ok) | `tests/test_chat_id_policy.py` (ok) |
| 25 | `task_contract` | `ouroboros/contracts/task_contract.py` (ok) | `tests/test_contracts.py` (ok) |
| 26 | `PluginAPI` | `ouroboros/contracts/plugin_api.py` (ok) | `tests/test_contracts.py` (ok)<br>`tests/test_extension_loader.py` (ok) |
| 27 | `SkillManifest` | `ouroboros/contracts/skill_manifest.py` (ok) | `tests/test_contracts.py` (ok) |
| 28 | `schema_versions` | `ouroboros/contracts/schema_versions.py` (ok) | `tests/test_contracts.py` (ok) |
| 17 | `ChatOutbound.cost_presentation` | `ouroboros/cost_projection.py` (ok)<br>`ouroboros/_usage_rows.py` (ok)<br>`supervisor/state.py` (ok)<br>`ouroboros/post_task_checkpoint.py` (ok)<br>`supervisor/events_task_done.py` (ok)<br>`ouroboros/gateway/contracts.py` (ok)<br>`web/modules/api_types.js` (ok)<br>`web/modules/chat_activity.js` (ok) | `tests/test_cost_presentation_scope.py` (ok)<br>`tests/test_gateway_parity.py` (ok)<br>`web/tests/cost_presentation.test.js` (ok) |
| 18 | `ChatOutbound.initiator` | `ouroboros/gateway/contracts.py` (ok)<br>`supervisor/log_addressing.py` (ok)<br>`ouroboros/subagent_messages.py` (ok)<br>`supervisor/message_bus.py` (ok)<br>`ouroboros/gateway/history.py` (ok)<br>`web/modules/api_types.js` (ok) | `tests/test_consciousness_initiator_label.py` (ok)<br>`tests/test_consciousness_wake_lane.py` (ok)<br>`web/tests/consciousness_label.test.js` (ok) |
| 19 | `project_thread` | `supervisor/message_bus.py` (ok)<br>`ouroboros/projects_registry.py` (ok) | `tests/test_message_bus.py` (ok)<br>`web/tests/chat_thread_routing.test.js` (ok) |
| 20 | `task_id` | `ouroboros/gateway/contracts.py` (ok)<br>`ouroboros/tools/core.py` (ok)<br>`ouroboros/event_bus.py` (ok) | `tests/test_contracts.py` (ok) |
| 21 | `QuizOption {label, detail?, recommended?}` | `ouroboros/gateway/contracts.py` (ok)<br>`ouroboros/gateway/task_decision.py` (ok)<br>`ouroboros/owner_quiz.py` (ok)<br>`ouroboros/tools/core.py` (ok) | `tests/test_gateway_parity.py` (ok)<br>`tests/test_quiz_display.py` (ok)<br>`tests/test_quiz_answer.py` (ok)<br>`web/tests/chat_decision.test.js` (ok) |
| 22 | `UpdateMergePlan` | `ouroboros/gateway/contracts.py` (ok) | `tests/test_update_apply_routing.py` (ok) |
| 23 | `ChatOutbound.review_projection` | `ouroboros/gateway/contracts.py` (ok) | `tests/test_review_substrate_v2.py` (ok)<br>`web/tests/review_truth.test.js` (ok) |
| 24 | `preflight_failed` | `ouroboros/skill_review_status.py` (ok) | `tests/test_skill_preflight_repair.py` (ok)<br>`web/tests/skill_preflight_repair.test.js` (ok) |
| 25 | `chat_id_policy` | `ouroboros/contracts/chat_id_policy.py` (ok) | `tests/test_chat_id_policy.py` (ok) |
| 26 | `task_contract` | `ouroboros/contracts/task_contract.py` (ok) | `tests/test_contracts.py` (ok) |
| 27 | `PluginAPI` | `ouroboros/contracts/plugin_api.py` (ok) | `tests/test_contracts.py` (ok)<br>`tests/test_extension_loader.py` (ok) |
| 28 | `SkillManifest` | `ouroboros/contracts/skill_manifest.py` (ok) | `tests/test_contracts.py` (ok) |
| 29 | `schema_versions` | `ouroboros/contracts/schema_versions.py` (ok) | `tests/test_contracts.py` (ok) |
## Package coverage

View file

@ -116,6 +116,16 @@ def _summary(rows: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
# reporting `cost_final: false` with every dollar bucket at zero and `unknown` at zero
# is a flag no reader can reconstruct.
non_final_rows = 0
# Rows whose money is KNOWN: a settled price, or a reservation upper bound a
# still-open row is carrying. It is the evidence behind a ZERO — an empty
# ledger and a ledger of exclusively unpriced rows both sum to 0.0, and only
# this count separates "nothing was spent" from "nothing is known" (#498).
# Weighted like every other axis, so a compacted baseline group answers the
# same as the final attempt rows it folded.
priced_rows = 0
# Presentation facts are independent of cost_final: a settled unpriced
# attempt leaves the total unknown without making a known subtotal inexact.
tracked_nonfinal_rows = accounting_open_rows = 0
counts: Dict[str, int] = {}
# Session count/quota and incremental cash remain separate observed axes.
sessions = 0
@ -156,27 +166,39 @@ def _summary(rows: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
bound = _number(row.get("reservation_upper_bound_usd"))
if bound is not None:
unresolved += bound
priced_rows += weight
tracked_nonfinal_rows += weight
accounting_open_rows += weight
else:
priced_rows += weight
settled += cost
if bool(row.get("cost_final")):
confirmed += cost
else:
estimated += cost
non_final_rows += weight
tracked_nonfinal_rows += weight
accounting_open_rows += weight
elif state == "reserved":
non_final_rows += weight
accounting_open_rows += weight
bound = _number(row.get("reservation_upper_bound_usd"))
if bound is None or pricing_unknown:
unknown += weight
if bound is not None:
reserved += bound
priced_rows += weight
tracked_nonfinal_rows += weight
elif state in {"dispatched", "unresolved"}:
non_final_rows += weight
accounting_open_rows += weight
bound = _number(row.get("reservation_upper_bound_usd"))
if bound is None or pricing_unknown:
unknown += weight
if bound is not None:
unresolved += bound
priced_rows += weight
tracked_nonfinal_rows += weight
settled, confirmed, estimated, reserved, unresolved = (
round(value, 6) for value in (settled, confirmed, estimated, reserved, unresolved)
)
@ -188,6 +210,9 @@ def _summary(rows: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
"unresolved_upper_bound_usd": unresolved,
"accounted_usd": round(settled + reserved + unresolved, 6),
"unknown_unmetered": unknown,
"priced_rows": priced_rows,
"tracked_nonfinal_rows": tracked_nonfinal_rows,
"accounting_open_rows": accounting_open_rows,
# Every row that increments `unknown` is open, so the old `not unknown` term is
# subsumed here rather than dropped.
"non_final_rows": non_final_rows,

View file

@ -52,6 +52,7 @@ from ouroboros.post_task_checkpoint import (
root_checkpoint_roots as _root_checkpoint_roots,
root_post_task_already_completed as _root_post_task_already_completed,
set_root_post_task_checkpoint as _set_root_post_task_checkpoint,
settle_terminal_projection as _settle_terminal_projection,
)
from ouroboros.skill_publish_result import apply_skill_publish_receipt_veto
from ouroboros.task_finalization import (
@ -306,7 +307,16 @@ def recover_pending_root_post_task_synthesis(
continue
checkpoint = stored.get("root_phase_checkpoint")
phase = str(checkpoint.get("post_task_synthesis") or "") if isinstance(checkpoint, dict) else ""
if not task_id or not _post_task_synthesis_is_open(phase):
if not task_id:
continue
if not _post_task_synthesis_is_open(phase):
# #1154: a root whose synthesis already settled can still OWE its
# terminal projection — the process died between the obligation and
# the Project/Main effects, or Main's eligibility could not be
# established last pass. Replaying it here is pure bookkeeping on this
# existing startup scan: no model call, no new timer, no new store. It is not
# counted as a recovered synthesis, which is what this number means.
_settle_terminal_projection(root, task_id, task={**stored, "id": task_id})
continue
task = {**stored, "id": task_id, "root_task_id": str(stored.get("root_task_id") or task_id)}
task.setdefault("budget_drive_root", str(root))
@ -572,6 +582,9 @@ def emit_task_results(
str(task.get("id") or ""), fields=True,
drive_root=pathlib.Path(task.get("budget_drive_root") or env.drive_root),
)
from ouroboros.cost_projection import with_task_cost_presentation
task_cost_fields = with_task_cost_presentation(task_cost_fields, task, env.drive_root)
except Exception:
log.error("Task cost authority unavailable at finalization", exc_info=True)
task_cost_fields = {
@ -581,6 +594,7 @@ def emit_task_results(
"prompt_tokens": None, "completion_tokens": None,
"reserved_usd": None, "unresolved_upper_bound_usd": None,
"unknown_unmetered": None,
"cost_presentation": None,
}
# SSOT cost naming (C2/ABI-3): the honest names on every terminal frame
# this pipeline emits; the seam also strips any legacy alias spelling.
@ -646,6 +660,7 @@ def emit_task_results(
# used to buffer the send with no delivery_id and no owed registration
# at all. Seam + dedup: ouroboros/task_finalization.py.
if _root_outbox and not _presence:
send_event.setdefault("progress_meta", {}).update(outcome_axes=outcome_axes, reason_code=reason_code)
stamp_root_final_phase( # the stamp names the SAME word the durable row below settles to
send_event, task, terminal_status=_durable_terminal_status(env, task, execution_status),
post_task_open=not task.get("_skip_post_task_synthesis") and not _root_post_task_already_completed(env, task),
@ -903,6 +918,7 @@ def _store_task_result(env: Any, task: Dict[str, Any], text: str,
cost_fields = with_cost_aliases(cost_fields or {
"cost_accounting_status": "unavailable", "cost_final": False,
"cost_accounting_error": "ledger_projection_missing",
"cost_presentation": None,
"accounted_upper_bound_usd": None, "total_rounds": None,
"prompt_tokens": None, "completion_tokens": None,
"reserved_usd": None, "unresolved_upper_bound_usd": None,

View file

@ -23,7 +23,7 @@ from __future__ import annotations
import logging
import pathlib
from typing import Any, Dict, Mapping, Optional
from typing import Any, Dict, Literal, Mapping, Optional, TypedDict
log = logging.getLogger(__name__)
@ -42,6 +42,10 @@ COST_ALIAS_PAIRS = (
# "not final" with nothing on it explaining what was still open). Adding a new
# marker means adding it here, once.
COST_OPENNESS_FIELDS = (
# #498: the optional scoped carrier below — the facts that EXPLAIN an amount,
# bound to the scope whose rows produced it, so a card never pairs a root-tree
# number with own-scope openness (or a parent's zero with a child's unknown).
"cost_presentation",
"cost_accounting_status",
"cost_accounting_error",
"cost_final",
@ -90,6 +94,80 @@ def honest_accounted_amount(source: Optional[Mapping[str, Any]]) -> Optional[flo
return amount
# The two scopes an amount can describe. A carrier states its own, because the
# same record holds both and only its producer knows which rows it summed.
COST_SCOPE_OWN = "own"
COST_SCOPE_ROOT_TREE = "root_tree"
class CostPresentation(TypedDict):
"""The optional wire carrier and its producer share this closed shape.
Amount and row-state facts belong to one bucket. Null at the enclosing
field means that ledger evidence was unavailable, not measured zero.
"""
scope: Literal["own", "root_tree"]
tracked_amount: Optional[float]
has_unpriced: bool
tracked_final: bool
accounting_open: bool
has_rows: bool
def build_cost_presentation(
summary: Optional[Mapping[str, Any]], *, scope: str,
) -> Optional[CostPresentation]:
"""The money facts a card needs, from ONE ledger summary bucket (#498).
The amount and the facts that explain it have to travel together. A card that
read the tracked subtotal from the subtree and the unknown-price markers from
the task showed a parent's honest ``$0.00`` over a child whose price nobody
knew. So this carrier is built once, by the producer that actually summed the
rows, and states which scope those rows belong to.
* ``tracked_amount`` — the accounted subtotal, or ``None``. A zero survives
only on PRICED evidence (``priced_rows``): an empty ledger and a ledger of
exclusively unpriced rows both add up to 0.0 and neither is a measured zero.
The count is what makes that exact, so the subtotal is read straight from
the bucket here rather than through :func:`honest_accounted_amount`, whose
bound-and-unknown heuristic is the count-free approximation of this same
question (it discards a real priced zero that shares a scope with an
unpriced row — the MIXED zero this carrier can now state honestly).
* ``has_unpriced`` — some row in this scope has no price at all, so the
tracked amount is only a subtotal; it may itself include open bounds.
* ``tracked_final`` — the tracked amount is exact. Estimates, retained
reservation bounds and ledger-integrity gaps all leave it False.
* ``accounting_open`` — more can still arrive for this scope.
* ``has_rows`` — this scope has any accountable row at all; without one there
is nothing to say, not a free result.
``None`` when the summary is missing entirely (an unreadable ledger already
says so through ``cost_accounting_status``).
"""
if not isinstance(summary, Mapping) or scope not in {COST_SCOPE_OWN, COST_SCOPE_ROOT_TREE}:
return None
try:
priced = max(0, int(summary.get("priced_rows") or 0))
unpriced = max(0, int(summary.get("unknown_unmetered") or 0))
# Absence on a legacy bucket proves neither closed nor exact.
tracked_nonfinal = int(summary["tracked_nonfinal_rows"])
open_rows = int(summary["accounting_open_rows"])
except (KeyError, TypeError, ValueError):
return None
amount = _as_amount(summary.get("accounted_usd")) if priced else None
integrity_gap = bool(summary.get("integrity_degraded"))
open_accounting = open_rows > 0 or integrity_gap
return {
"scope": str(scope),
"tracked_amount": round(amount, 6) if amount is not None else None,
"has_unpriced": unpriced > 0,
"tracked_final": amount is not None and tracked_nonfinal == 0 and not integrity_gap,
"accounting_open": open_accounting,
"has_rows": bool(priced or unpriced),
}
def resolve_cost_pair(source: Optional[Mapping[str, Any]], new: str, old: str) -> tuple[bool, Any]:
"""``(present, raw_value)`` for one alias pair under the ONE precedence rule.
@ -110,6 +188,36 @@ def resolve_cost_pair(source: Optional[Mapping[str, Any]], new: str, old: str) -
return False, None
def with_task_cost_presentation(projection: Mapping[str, Any], task: Mapping[str, Any],
drive_root: Any) -> Dict[str, Any]:
"""Attach a root bucket without altering any existing monetary field.
A root's first terminal frame can precede the supervisor's subtree refresh.
It must already explain the tree, including roots that skip synthesis. A
failed tree read is explicit null, never the previously computed own cost.
"""
from ouroboros.task_results import resolve_task_lineage
tid = str(task.get("id") or task.get("task_id") or "")
lineage = resolve_task_lineage(tid, metadata=task.get("metadata"), **{
key: task.get(key) for key in ("root_task_id", "parent_task_id", "delegation_role",
"original_task_id", "timeout_retry_from")
})
if not lineage["is_root_task"]:
return dict(projection)
carrier = None
if projection.get("cost_accounting_status") == "available":
try:
from ouroboros.usage_accounting import usage_breakdown
bucket = usage_breakdown(pathlib.Path(task.get("budget_drive_root") or drive_root),
root_task_id=str(lineage["root_task_id"] or tid))
carrier = build_cost_presentation(bucket, scope=COST_SCOPE_ROOT_TREE)
except Exception:
log.warning("Root cost presentation unavailable for %s", tid, exc_info=True)
return {**projection, "cost_presentation": carrier}
def honest_cost_pair_amount(
source: Optional[Mapping[str, Any]], new: str, old: str,
) -> tuple[bool, Optional[float]]:
@ -247,6 +355,8 @@ def live_root_cost_projection(
"unknown_unmetered": unknown,
"non_final_rows": int(usage.get("non_final_rows") or 0),
"ledger_integrity_degraded": bool(usage.get("integrity_degraded")),
# Built from THIS bucket, so the amount and its explanation are one fact.
"cost_presentation": build_cost_presentation(usage, scope=COST_SCOPE_ROOT_TREE),
}
except Exception:
log.debug("Root heartbeat cost unavailable for %s", task_id, exc_info=True)
@ -305,6 +415,10 @@ def cost_display(source: Optional[Mapping[str, Any]], *, decimals: int = 2) -> s
__all__ = [
"COST_ALIAS_PAIRS",
"COST_OPENNESS_FIELDS",
"COST_SCOPE_OWN",
"COST_SCOPE_ROOT_TREE",
"CostPresentation",
"build_cost_presentation",
"carry_cost_meta",
"cost_display",
"cost_projection",
@ -314,4 +428,5 @@ __all__ = [
"normalize_task_result_cost_planes",
"resolve_cost_pair",
"with_cost_aliases",
"with_task_cost_presentation",
]

View file

@ -419,6 +419,7 @@ D20 = "Presence"
"ouroboros/task_results.py" = "D17"
"ouroboros/task_result_schema.py" = "D17"
"ouroboros/task_status.py" = "D17"
"ouroboros/terminal_projection.py" = "D17"
"ouroboros/task_tree_ledger.py" = "D07"
"ouroboros/tool_access.py" = "D04"
"ouroboros/tool_access_paths.py" = "D04"

View file

@ -7,6 +7,7 @@ TypedDicts document payloads, not runtime validation. Keep discriminating
from __future__ import annotations
from typing import Any, Dict, List, Optional
from ouroboros.cost_projection import CostPresentation
from ouroboros.gateway.history_contracts import ChatHistoryResponse # noqa: F401 -- public re-export
from ouroboros.gateway.widgets import ExtensionLiveSnapshot, WidgetTab, WidgetsResponse
@ -227,6 +228,8 @@ class ChatOutbound(TypedDict):
# amount computed over a degraded ledger reached every surface looking exactly
# like one computed over a sound ledger.
ledger_integrity_degraded: NotRequired[Optional[bool]]
# Closed shape owned by the cost producer; null means ledger unavailable.
cost_presentation: NotRequired[Optional[CostPresentation]]
result: NotRequired[str]
result_truncated: NotRequired[bool] # P3: WS preview was capped; fetch full via task id
trace_summary: NotRequired[str]
@ -1480,6 +1483,7 @@ WS_MESSAGE_TYPES: tuple[str, ...] = (
__all__ = [
"CostPresentation",
"ChatInbound",
"TaskConstraintInbound",
"CommandInbound",

View file

@ -581,7 +581,7 @@ def _annotate_terminal_task_truth(
# window still holds, so its harness chip needs no "Load older".
message.update(terminal_receipt_by_task.get(task_id) or {})
is_summary = str(message.get("system_type") or "") == "task_summary"
if is_summary or (
if is_summary or (not message.get("is_progress") and message.get("task_terminal_status")) or (
task_id not in summary_task_ids
and latest_progress_by_task.get(task_id) is message
):
@ -896,7 +896,7 @@ def _collect_chat_rows(
_copy_task_summary_metadata(rec, entry)
# Lineage, the origin label, and the host's card placement (card_row /
# card_row_id) — a stored key is replayed verbatim, an absent one is omitted.
for field in (*SUBAGENT_MESSAGE_FIELDS, "initiator", "card_row", "card_row_id"):
for field in (*SUBAGENT_MESSAGE_FIELDS, "initiator", "card_row", "card_row_id", "narration"):
if field in entry:
rec[field] = entry[field]
combined.append(rec)

View file

@ -264,6 +264,15 @@ def _provider_unavailable_result(
# round record (a granted transport-death repeat, no usable response since) leaves an attempt
# unresolved and outranks the wait terminal, which in turn outranks the overflow salvage.
record = isinstance(ctx.accumulated_usage.get(TRANSPORT_DEATHS_KEY), dict)
# The UNKNOWN-OUTCOME predicate, spelled exactly as the two seams that already
# own it: `provider_no_call_source` (the no-resend decision) and
# `provider_terminal_fallback_text` (the owner sentence). The durable source
# asked only for the round record, so an episode whose attempt was interrupted
# in flight — no record, sticky kind `provider_outcome_unknown` — told the owner
# its outcome was unknown while stamping `transport_unavailable_no_resend` on the
# trace (#869). One question, one answer, on all three surfaces.
unknown_outcome = record or str(
ctx.accumulated_usage.get("_last_llm_error_kind") or "") == "provider_outcome_unknown"
is_transport_wait = wait_cause == "transport_unavailable"
is_context_overflow = kind == "context_overflow" and not (record or is_transport_wait)
is_deadline_exhausted = kind == "deadline_exhausted" or str(ctx.accumulated_usage.get("_last_llm_error_kind") or "") == "deadline_exhausted"
@ -315,7 +324,7 @@ def _provider_unavailable_result(
ctx.accumulated_usage.update(execution_status=RESULT_INFRA_FAILED, reason_code="provider_unavailable")
text, usage, llm_trace = _forced_fallback_result(
ctx, llm_trace, fallback, reason_code="provider_unavailable",
source="provider_outcome_unknown_no_resend" if record else "transport_unavailable_no_resend",
source="provider_outcome_unknown_no_resend" if unknown_outcome else "transport_unavailable_no_resend",
)
if usage.get("reason_code") == "provider_unavailable":
usage["execution_status"] = RESULT_INFRA_FAILED

View file

@ -714,6 +714,37 @@ def last_assistant_text(messages: List[Dict[str, Any]]) -> str:
return ""
# Unknown outcome alone establishes neither a receipt nor a numeric bound.
UNKNOWN_ATTEMPT_COST_NOTE = (
" This does not establish the attempt's cost; any recorded estimate or "
"upper bound is not a settled receipt."
)
def _unknown_wait_note(unknown: bool, waited_sec: float, interactive: bool) -> str:
"""The REAL wait this unknown terminal spent, on the rails that never said it.
The transport-wait branch below states its own wait in the sentence. The
no-call rails (``provider_no_call_source`` -> ``provider_outcome_unknown_no_resend``)
reach the generic terminal instead, which named neither the wait nor the
fence, so an owner whose turn had waited minutes read a bare "no usable
response". Zero stays silent rather than claiming a wait that never ran.
"""
if not unknown or waited_sec <= 0:
return ""
subject = "This turn" if interactive else "The task"
return f" {subject} spent {waited_sec / 60.0:.1f} min in the provider wait; that wait did not confirm the attempt's outcome."
def _unknown_terminal_recovery_hint(usage: Dict[str, Any]) -> str:
# Wording only, confined to unknown terminals. Stop/Wrap up keep their
# existing byte-for-byte control sentence and recovery hint.
return provider_recovery_hint(usage).replace(
"the dead ones stay unresolved at their upper bound",
"the dead ones stay unresolved; any recorded bound is retained",
)
def provider_terminal_fallback_text(
accumulated_usage: Dict[str, Any],
*,
@ -754,6 +785,14 @@ def provider_terminal_fallback_text(
"Any files written so far are preserved in the workspace."
)
if is_transport_wait:
if unknown:
# A wait duration does not prove a redial, nor a numeric price bound.
return (
"⚠️ The dispatched attempt has no confirmed provider outcome."
f"{_unknown_wait_note(True, waited_sec, interactive)}"
" Inspect the preserved facts before starting another run."
f"{_unknown_terminal_recovery_hint(accumulated_usage)}{UNKNOWN_ATTEMPT_COST_NOTE}"
)
advice = ("Inspect the preserved facts before starting another run." if unknown
else "Retry when connectivity returns.")
if interactive and waited_sec > 0:
@ -779,27 +818,23 @@ def provider_terminal_fallback_text(
"time to wait; the task ended as a provider outage, not completed. Any files "
f"written so far are preserved in the workspace. {advice}"
)
if unknown:
# The episode redialed a granted transport-death repeat that never left the
# host: an earlier attempt of the round is still unresolved at its upper
# bound, and the owner text says both facts (the wait and the fence). The
# class the repeat was released with is on the record, and the hint reads it
# there — never the sticky kind, which by the time the window closes names a
# LATER free redial's refusal (``deadline_exhausted``).
text += provider_recovery_hint(accumulated_usage)
return text
if is_deadline_exhausted:
text = "⚠️ The owner deadline ended primary model work; any files written so far are preserved."
if unknown:
text += provider_recovery_hint(accumulated_usage)
text += _unknown_terminal_recovery_hint(accumulated_usage) + UNKNOWN_ATTEMPT_COST_NOTE
return text
return (
"⚠️ The model provider returned no usable response."
f"{provider_failure_hint(accumulated_usage)}{provider_recovery_hint(accumulated_usage)} "
f"{_unknown_wait_note(unknown, waited_sec, interactive)}"
f"{provider_failure_hint(accumulated_usage)}"
f"{_unknown_terminal_recovery_hint(accumulated_usage) if unknown else provider_recovery_hint(accumulated_usage)}"
f"{UNKNOWN_ATTEMPT_COST_NOTE if unknown else ''} "
"Any files written so far are preserved in the workspace."
)
def provider_failure_hint(accumulated_usage: Dict[str, Any]) -> str:
from ouroboros.utils import sanitize_tool_result_for_log

View file

@ -9,6 +9,8 @@ from typing import Any, Dict
from ouroboros.cost_projection import (
COST_ALIAS_PAIRS,
COST_SCOPE_ROOT_TREE,
build_cost_presentation,
carry_cost_meta,
honest_accounted_amount,
with_cost_aliases,
@ -116,6 +118,16 @@ def project_replica_task_result_fields(
``updated_at`` is monotonic metadata only; it never selects field authority.
"""
overlay = dict(replica_fields)
# The receiving drive's first accepted terminal transition owns provenance,
# including its absence on historical rows; replicas cannot originate it.
overlay.pop("canonical_terminal_projection_origin", None)
canonical_cost = canonical_fields.get("cost_presentation")
replica_cost = overlay.get("cost_presentation")
if (isinstance(canonical_cost, dict) and canonical_cost.get("scope") == COST_SCOPE_ROOT_TREE
and (not isinstance(replica_cost, dict) or replica_cost.get("scope") != COST_SCOPE_ROOT_TREE)):
# A worker's own bucket cannot replace an already published tree bucket.
# Its own monetary fields remain own; the scoped amount/facts stay paired.
overlay.pop("cost_presentation", None)
if "review_projection" in overlay:
overlay["review_projection"] = merge_review_projection(
canonical_fields.get("review_projection"), overlay["review_projection"],
@ -159,6 +171,12 @@ def project_replica_task_result_fields(
# update_focus writes the canonical result only; a split root's worker
# replica carries the stale (often null) execution-local copy.
"focus",
# The terminal-projection obligation and its receipt are canonical
# bookkeeping (#1154): a replica that still carried the readiness row
# would resurrect an obligation this drive had already settled, and a
# replica marker would claim a Project row nobody appended here.
"canonical_terminal_projection",
"canonical_terminal_projection_ready",
):
if field in canonical_fields:
overlay.pop(field, None)
@ -374,6 +392,13 @@ def set_root_post_task_checkpoint(
),
"cost_with_children_partial": not subtree_final,
"cost_final": bool(cost_fields.get("cost_final") and subtree_final),
# #498: a ROOT's terminal record speaks for its whole tree, so
# the carrier is rebuilt from the SUBTREE bucket, replacing the
# own-scope one `reconstruct_task_cost` just attached. Presence
# is what selects it — a null subtree amount stays null and never
# falls back to the root's own (often zero) number.
"cost_presentation": build_cost_presentation(
subtree, scope=COST_SCOPE_ROOT_TREE),
})
except Exception:
log.error("Failed to refresh final root cost projection for %s", task_id, exc_info=True)
@ -383,6 +408,7 @@ def set_root_post_task_checkpoint(
"cost_accounting_error": "ledger_unavailable",
"accounted_upper_bound_usd": None,
"accounted_upper_bound_usd_with_children": None,
"cost_presentation": None,
})
# SSOT cost naming (C2/F12/ABI-3): every branch above writes the honest
# names directly onto the honest-named `reconstruct_task_cost` fields
@ -464,37 +490,27 @@ def set_root_post_task_checkpoint(
bridge.push_log(address_handler_push(authority_root, dict(finalized_event)))
except Exception:
log.debug("Live push of finalized task cost skipped for %s", task_id, exc_info=True)
pending_projection = (
stored.get("canonical_terminal_projection_ready")
if isinstance(stored, dict) else None
)
if (
isinstance(pending_projection, dict)
and post_task_synthesis_is_terminal(stored_post_task)
and not isinstance((stored or {}).get("canonical_terminal_projection"), dict)
):
try:
from ouroboros.project_dialogue import append_terminal_task_projection
settle_terminal_projection(authority_root, task_id, task=task)
if stored is None:
# The write failed: nothing was stored, and the contract is "the record
# actually stored, if any" — a pre-existing row must not impersonate a
# persisted checkpoint (callers treat None as "not persisted").
return None
# Settlement writes receipts/retirement and can race another enrichment.
# Never hand a caller the pre-settlement obligation as current authority.
try:
return load_task_result(authority_root, task_id, strict=True)
except Exception:
log.warning("Failed to read settled root post-task checkpoint for %s", task_id, exc_info=True)
return None
projection_task = {**task, **(stored or {}), "id": task_id}
append_terminal_task_projection(
authority_root,
task_id,
projection_task,
stored or {},
{
"ts": str(pending_projection.get("task_done_ts") or utc_now_iso()),
"chat_id": int(pending_projection.get("chat_id") or 0),
"status": str((stored or {}).get("status") or STATUS_COMPLETED),
},
)
except Exception:
log.warning(
"Failed to settle canonical terminal projection for %s",
task_id,
exc_info=True,
)
return stored
# Compatibility exports: the continuation owns no synthesis or result lock.
from ouroboros.terminal_projection import ( # noqa: E402, F401
SETTLEMENT_NONE, SETTLEMENT_DEFERRED, SETTLEMENT_SETTLED,
clear_terminal_projection_obligation as _clear_terminal_projection_obligation,
settle_terminal_projection,
)
def root_post_task_already_completed(env: Any, task: Dict[str, Any]) -> bool:

View file

@ -444,11 +444,13 @@ def _pre_synthesis_usage_snapshot(
})
try:
from ouroboros.usage_accounting import usage_breakdown
from ouroboros.cost_projection import COST_SCOPE_ROOT_TREE, build_cost_presentation
logical_root_id = str(task.get("root_task_id") or task_id)
subtree = usage_breakdown(budget_root, root_task_id=logical_root_id)
snapshot.update({
"accounted_upper_bound_usd_with_children": round(float(subtree["accounted_usd"]), 6),
"cost_presentation": build_cost_presentation(subtree, scope=COST_SCOPE_ROOT_TREE),
"reserved_usd": round(float(subtree["reserved_usd"]), 6),
"unresolved_upper_bound_usd": round(
float(subtree["unresolved_upper_bound_usd"]), 6
@ -472,6 +474,7 @@ def _pre_synthesis_usage_snapshot(
"unknown_unmetered": None,
"ledger_integrity": "unavailable",
"cost_accounting_status": "unavailable",
"cost_presentation": None,
})
return snapshot

View file

@ -16,7 +16,7 @@ import logging
import os
import pathlib
import uuid
from typing import Any, Dict, Iterable, List, Optional
from typing import Any, Callable, Dict, Iterable, List, Optional
from ouroboros.acceptance_preparation import incident_cause_clauses
from ouroboros.platform_layer import acquire_exclusive_file_lock, release_exclusive_file_lock
@ -779,6 +779,9 @@ TASK_CAUSE_PHRASES = {
"host_child_status_suffix": "A child task had not settled when the answer was delivered",
"invalid_delivery_control_after_repair": "Ouroboros's final delivery instruction could not be read even after repair, so the answer stands as delivered.",
"budget_exhausted": "The task ran out of budget before it could finish cleanly",
# #869: the provider-death rail's terminal words; the amount of retained text is
# said by the notice, this clause only names why the task ended.
"provider_unavailable": "The model provider stopped answering, so the task could not finish",
"delivery_control_degraded": "Ouroboros's final delivery instruction could not be applied, so the answer stands as delivered.",
"authoring_handover_incomplete": "The replacement model stopped before resuming tool work.",
"delegated_custody_unreconciled": "Some delegated work was never reconciled.",
@ -847,8 +850,8 @@ def append_canonical_task_summary(drive_root: Any, row: Dict[str, Any]) -> bool:
def canonical_task_summary_receipt(result: Dict[str, Any]) -> Dict[str, Any]:
"""The receipt proving this task's own terminal row reached the canonical chat.
``_append_terminal_task_projection`` stamps it on the task result in the same
write that appends the row, so another composer can tell that a task already
``terminal_projection`` stamps it after the chat append (deduped by the
readiness token on retry), so another composer can tell that a task already
spoke for itself without scanning chat text (BIBLE P5). Empty when no row was
appended for that task.
"""
@ -1065,117 +1068,9 @@ def _append_terminal_task_projection(
drive_root: Any, task_id: str, task: Dict[str, Any], result: Dict[str, Any],
task_done_event: Dict[str, Any],
) -> bool:
"""Project one terminal child result into canonical cognition, without an LLM."""
from ouroboros.task_results import resolve_task_lineage, write_task_result
from ouroboros.task_status import SETTLED_STATUSES
from ouroboros.terminal_projection import append_terminal_projection
tid = str(task_id or "").strip()
task = task if isinstance(task, dict) else {}
result = result if isinstance(result, dict) else {}
event = task_done_event if isinstance(task_done_event, dict) else {}
if not tid:
return False
lineage = resolve_task_lineage(
tid,
metadata=task.get("metadata") if isinstance(task.get("metadata"), dict) else {},
root_task_id=result.get("root_task_id") or task.get("root_task_id"),
parent_task_id=result.get("parent_task_id") or task.get("parent_task_id"),
delegation_role=result.get("delegation_role") or task.get("delegation_role"),
original_task_id=result.get("original_task_id") or task.get("original_task_id"),
timeout_retry_from=result.get("timeout_retry_from") or task.get("timeout_retry_from"),
)
status = str(result.get("status") or event.get("status") or "").strip().lower()
if status not in SETTLED_STATUSES:
return False
is_root = bool(lineage["is_root_task"])
summary_id = f"task-terminal:{tid}"
summary_kind = "terminal_root_projection" if is_root else "terminal_result_projection"
parent_id = str(lineage.get("parent_task_id") or "")
root_id = str(lineage.get("root_task_id") or tid)
from ouroboros.project_facts import resolve_project_id
appended = False
def _append_once(current: Dict[str, Any], _patch: Dict[str, Any]) -> Dict[str, Any]:
nonlocal appended
existing_marker = current.get("canonical_terminal_projection")
if isinstance(existing_marker, dict) and str(existing_marker.get("summary_id") or "") == summary_id:
return {"status": str(current.get("status") or status)}
checkpoint = current.get("root_phase_checkpoint")
post_task_phase = (
str(checkpoint.get("post_task_synthesis") or "")
if isinstance(checkpoint, dict) else ""
)
if is_root and post_task_phase in {"pending_once", "running"}:
ready = current.get("canonical_terminal_projection_ready")
if isinstance(ready, dict) and str(ready.get("summary_id") or "") == summary_id:
return {"status": str(current.get("status") or status)}
return {
"status": str(current.get("status") or status),
"canonical_terminal_projection_ready": {
"summary_id": summary_id,
"task_done_ts": str(event.get("ts") or utc_now_iso()),
"chat_id": int(event.get("chat_id") or task.get("chat_id") or 0),
},
}
effective = {**result, **current}
project_id = resolve_project_id({**task, **effective})
role = str(effective.get("role") or task.get("role") or ("root" if is_root else "child"))
reason = str(effective.get("reason_code") or event.get("reason_code") or "")
phase = outcome_phase(effective, event)
outcome = OUTCOME_PHASE_HEADLINE[phase]
row_chat_id = int(event.get("chat_id") or task.get("chat_id") or 0)
# The room IS the project and ``result_ref`` IS the pointer, so the row
# says in words only what the model cannot read off the typed fields:
# ``memory._format_chat_line`` renders the text and drops every other
# key, leaving lineage as the one fact that must stay prose.
text = (f"{outcome}. Root task {tid}." if is_root
else f"{outcome}. {role} (child {tid} of {parent_id or 'unknown'}).")
verdict = _completion_verdict(effective, event)
if verdict:
text += f" {verdict}"
excerpt = _completion_excerpt(effective, chat_id=row_chat_id, salvage_only=True)
if excerpt:
text += f" {excerpt}"
result_ref = {"kind": "task_result", "task_id": tid, "reader": "get_task_result"}
row = {
"ts": str(event.get("ts") or effective.get("ts") or utc_now_iso()),
"direction": "system", "type": "task_summary", "summary_kind": summary_kind,
"summary_id": summary_id, "task_id": tid,
"parent_task_id": parent_id, "root_task_id": root_id,
"project_id": project_id,
"chat_id": row_chat_id,
"delegation_role": str(effective.get("delegation_role") or task.get("delegation_role") or ""),
"role": role, "status": str(effective.get("status") or status),
"outcome": outcome, "outcome_phase": phase, "outcome_final": True,
"outcome_authority": "canonical_task_result_after_finalization",
"outcome_axes": effective.get("outcome_axes") or event.get("outcome_axes") or {},
"reason_code": reason, "result_ref": result_ref,
"text": text,
# The rendered cause, beside the raw code: the transports with no task card
# (Telegram's text bridge today) state the same sentence the card states,
# without a second rendering of TASK_CAUSE_PHRASES. Absent when there is no
# cause — an always-written empty key is a claim of its own.
**({"reason_detail": verdict} if verdict else {}),
}
if isinstance(effective.get("model_execution"), dict):
row["model_execution"] = dict(effective["model_execution"])
appended = append_canonical_task_summary(drive_root, row)
if not appended:
return {"status": str(current.get("status") or status)}
return {
"status": str(current.get("status") or status),
"canonical_terminal_projection": {
"summary_id": summary_id, "summary_kind": summary_kind,
"written_at": row["ts"], "chat_id": row_chat_id,
},
"canonical_terminal_projection_ready": None,
}
write_task_result(
drive_root, tid, status, _field_projector=_append_once,
)
return appended
return append_terminal_projection(drive_root, task_id, task, task_done_event, result=result)
def historical_terminal_projection(entry: Dict[str, Any]) -> Optional[Dict[str, Any]]:
@ -1381,6 +1276,7 @@ def _completion_verdict(result: Dict[str, Any], event: Dict[str, Any]) -> str:
status = str(decision.get("status") or "").strip()
cause = str(decision.get("reason") or "")
raw_reason = str(result.get("reason_code") or event.get("reason_code") or "")
origin = result.get("cancel_origin") or event.get("cancel_origin")
phase = outcome_phase(result, event)
# Resolved once for every branch: the custody debt is a standing limitation
# of the same answer, not a property of the branch that happened to fire.
@ -1391,6 +1287,11 @@ def _completion_verdict(result: Dict[str, Any], event: Dict[str, Any]) -> str:
elif (status and (status != ACCEPTANCE_ACCEPTED or cause in TASK_CAUSE_PHRASES)
and phase in {"done", "warn"}):
clause = TASK_CAUSE_PHRASES.get(cause, cause)
elif phase == "cancelled" and isinstance(origin, dict) and origin:
# The recorded cause and the relation the record PROVES (#1061).
from supervisor.cancel_publication import cancel_cause_clauses
clause = _join_cause_clauses(cancel_cause_clauses(origin, result, event))
elif held:
# A task HELD by a blocking plan review states the objective's own reason.
clause = TASK_CAUSE_PHRASES.get(str(objective.get("reason") or ""), str(objective.get("reason") or ""))
@ -1413,7 +1314,7 @@ def _completion_verdict(result: Dict[str, Any], event: Dict[str, Any]) -> str:
def _run_lives_in_its_project(
drive_root: Any, task_id: str, project_id: str, task: Dict[str, Any], result: Dict[str, Any],
) -> bool:
) -> Optional[bool]:
"""Did this run's work actually go into that project's room?
Two facts answer yes, and only these two. The run was ADDRESSED there —
@ -1433,43 +1334,57 @@ def _run_lives_in_its_project(
chat_id = result.get("chat_id")
if chat_id is None:
chat_id = task.get("chat_id")
project_chat = (get_reserved_project(drive_root, project_id) or {}).get("chat_id")
project_chat = (get_reserved_project(drive_root, project_id, strict=True) or {}).get("chat_id")
if chat_id is not None and project_chat is not None and int(chat_id) == int(project_chat):
return True
binding = project_binding_for_task(drive_root, task_id) or {}
binding = project_binding_for_task(drive_root, task_id, strict=True) or {}
return str(binding.get("project_id") or "") == str(project_id)
except Exception:
log.debug("project-room membership check failed for %s", task_id, exc_info=True)
return False
return None
def enqueue_project_completion_summary(
# Typed answers of the Main-mirror seam (#1154): INELIGIBLE is a POSITIVE finding
# (this run owes Main nothing); UNKNOWN leaves the obligation owed for a retry.
MAIN_MIRROR_OWED, MAIN_MIRROR_INELIGIBLE, MAIN_MIRROR_UNKNOWN = "owed", "ineligible", "unknown"
def project_completion_delivery_outcome(
drive_root: Any, evt: Dict[str, Any], task_id: str, task: Dict[str, Any],
result: Dict[str, Any], task_done_event: Dict[str, Any],
) -> bool:
*, _on_owed: Optional[Callable[[], bool]] = None,
) -> tuple:
"""Owe Main's answer for Project roots, including conversations moved from Main.
A direct conversation born inside a Project stays there. Only its durable,
ingress-bound source can prove that a direct turn was transferred from Main;
current chat addressing and project existence cannot establish that origin.
Answers ``(durability, live_send_queued)``: one of ``MAIN_MIRROR_OWED`` /
``MAIN_MIRROR_INELIGIBLE`` / ``MAIN_MIRROR_UNKNOWN``, resting on the OWED
registration and never on the live queue (a registered row the queue refused
is replayed from the outbox; a send the registry could not record is not
durable at all), plus the long-standing boolean below, which a duplicate
answers False without the answer ceasing to be owed. The bounded outbox and
at-least-once external delivery stay exactly as disclosed.
"""
tid = str(task_id or "").strip()
task = task if isinstance(task, dict) else {}
result = result if isinstance(result, dict) else {}
if not tid:
return False
return MAIN_MIRROR_INELIGIBLE, False
try:
from ouroboros.projects_registry import mirrored_answer, project_binding_for_task, task_presentation_snapshot
if any(bool(row.get("_is_direct_chat")) for row in (evt, task, result, task_done_event)
if isinstance(row, dict)):
binding = project_binding_for_task(drive_root, tid) or {}
binding = project_binding_for_task(drive_root, tid, strict=True) or {}
source = binding.get("source_ref")
if not owner_message_ref_is_valid(source) or source["chat_id"] != 1:
return False
return MAIN_MIRROR_INELIGIBLE, False
from ouroboros.task_results import resolve_task_lineage
from ouroboros.task_status import SETTLED_STATUSES
from supervisor.terminal_delivery import enqueue_terminal_delivery
from supervisor.terminal_delivery import enqueue_terminal_delivery, register_pending_delivery
metadata = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
lineage = resolve_task_lineage(
@ -1482,23 +1397,27 @@ def enqueue_project_completion_summary(
)
status = str(result.get("status") or task_done_event.get("status") or "").lower()
if not lineage["is_root_task"] or status not in SETTLED_STATUSES:
return False
return MAIN_MIRROR_INELIGIBLE, False
snapshot = task_presentation_snapshot(
drive_root, tid, task=task, result=result,
project_id=str(result.get("project_id") or task.get("project_id") or ""),
strict=True,
)
if not snapshot["project_id"] or not snapshot["project_routable"]:
# Owner decision 3A: a run whose project id was DERIVED from a
# workspace has no room, so Main stays silent instead of offering a
# Project reference that lands in an empty duplicate of itself. The same
# holds once a project is deleting or tombstoned.
return False
if not _run_lives_in_its_project(drive_root, tid, snapshot["project_id"], task, result):
return MAIN_MIRROR_INELIGIBLE, False
membership = _run_lives_in_its_project(drive_root, tid, snapshot["project_id"], task, result)
if membership is None:
return MAIN_MIRROR_UNKNOWN, False
if not membership:
# The room exists but holds none of this run's work: its id was only
# registered AFTER admission, or a mid-flight bind failed fail-soft.
# Offering "Open the Project" would reproduce the reported defect —
# a Main row leading into an empty room.
return False
return MAIN_MIRROR_INELIGIBLE, False
# Only the salvage excerpt survives in the TEXT: the model's own answer is
# never cut into it (it rides whole in the typed key below, or not at all),
# while salvaged bytes exist nowhere else. The text's only pointer is the
@ -1523,10 +1442,37 @@ def enqueue_project_completion_summary(
**mirrored_answer(result, outcome_phase(result, task_done_event)),
},
}
return bool(enqueue_terminal_delivery(drive_root, event))
# Owed BEFORE the live send: the enqueue below is idempotent on the same
# delivery_id, so a queue refusal after registration recovers from the outbox.
owed = register_pending_delivery(pathlib.Path(drive_root), dict(event))
if not owed:
return MAIN_MIRROR_UNKNOWN, False
# The settlement owner retires readiness to a durable result disposition
# before the live send. Published history then outlives registry eviction.
if _on_owed is not None and not _on_owed():
return MAIN_MIRROR_UNKNOWN, False
# A live queue exception cannot undo an already durable outbox row.
try:
queued = bool(enqueue_terminal_delivery(drive_root, event))
except Exception:
log.warning("Main mirror remains owed after queue failure for %s", tid, exc_info=True)
queued = False
return MAIN_MIRROR_OWED, queued
except Exception:
log.warning("Failed to enqueue Project completion summary for %s", tid, exc_info=True)
return False
return MAIN_MIRROR_UNKNOWN, False
def enqueue_project_completion_summary(
drive_root: Any, evt: Dict[str, Any], task_id: str, task: Dict[str, Any],
result: Dict[str, Any], task_done_event: Dict[str, Any],
) -> bool:
"""Whether THIS call queued a live Main copy — the long-standing answer;
durability is the typed pair's other half, for callers that must tell a
duplicate from a lost one."""
return project_completion_delivery_outcome(
drive_root, evt, task_id, task, result, task_done_event,
)[1]
def announce_project_started(
@ -1572,26 +1518,13 @@ def announce_project_started(
return False
__all__ = [
"AGENT_RECEIPT_ID_PREFIX",
"announce_project_started",
"append_authored_task_summary",
"append_chat_annotation",
"append_canonical_task_summary",
"append_terminal_task_projection",
"build_owner_message_ref",
"chat_annotation_receipt",
"entry_matches_source_ref",
"latest_chat_annotations",
"enqueue_project_completion_summary",
"completion_status_label",
"outcome_phase",
"owner_message_ref_is_valid",
"project_origin_rows",
"project_question_pointer",
"project_recent_dialogue",
"routing_options_with_labels",
"routing_target_label",
"resolve_owner_message_source",
__all__ = ["AGENT_RECEIPT_ID_PREFIX", "announce_project_started",
"append_authored_task_summary", "append_chat_annotation", "append_canonical_task_summary",
"append_terminal_task_projection", "build_owner_message_ref", "chat_annotation_receipt",
"entry_matches_source_ref", "latest_chat_annotations",
"enqueue_project_completion_summary", "project_completion_delivery_outcome",
"completion_status_label", "outcome_phase", "owner_message_ref_is_valid",
"project_origin_rows", "project_question_pointer", "project_recent_dialogue",
"routing_options_with_labels", "routing_target_label", "resolve_owner_message_source",
"source_refs_for_project",
]

View file

@ -83,8 +83,18 @@ def _bindings_path(drive_root: Any) -> pathlib.Path:
return pathlib.Path(drive_root) / "state" / _BINDINGS_NAME
def _load(drive_root: Any) -> Dict[str, Any]:
data = read_json_dict(_registry_path(drive_root))
def _load(drive_root: Any, *, strict: bool = False) -> Dict[str, Any]:
if strict:
import json
try:
data = json.loads(_registry_path(drive_root).read_text(encoding="utf-8"))
except FileNotFoundError:
return {"projects": []}
if (not isinstance(data, dict) or not isinstance(data.get("projects"), list)
or any(not isinstance(row, dict) or not row.get("id") for row in data["projects"])):
raise ValueError("Project registry is unavailable")
else:
data = read_json_dict(_registry_path(drive_root))
if not isinstance(data, dict) or not isinstance(data.get("projects"), list):
return {"projects": []}
data["projects"] = [
@ -343,13 +353,16 @@ def all_task_project_bindings(drive_root: Any, *, strict: bool = False) -> Dict[
return out
def project_binding_for_task(drive_root: Any, task_id: str) -> Optional[Dict[str, Any]]:
def project_binding_for_task(drive_root: Any, task_id: str, *, strict: bool = False) -> Optional[Dict[str, Any]]:
tid = str(task_id or "").strip()
if not tid:
return None
# Read needs no lock: atomic_write_json renames into place, so a reader
# always sees a complete (old or new) bindings file, never a torn one.
row = _load_bindings(drive_root)["bindings"].get(tid)
bindings = _load_bindings(drive_root, strict=strict)["bindings"]
row = bindings.get(tid)
if strict and tid in bindings and (not isinstance(row, dict) or not row.get("project_id")):
raise ValueError("Project binding is unavailable")
return dict(row) if isinstance(row, dict) else None
@ -630,10 +643,10 @@ def project_chat_for_task_tree(
return 0
def list_reserved_projects(drive_root: Any) -> List[Dict[str, Any]]:
def list_reserved_projects(drive_root: Any, *, strict: bool = False) -> List[Dict[str, Any]]:
"""All Project ids, including deleting/tombstoned history reservations."""
with _LOCK:
projects = _load(drive_root)["projects"]
projects = _load(drive_root, strict=strict)["projects"]
return sorted(
projects,
key=lambda p: str(p.get("last_active_at") or p.get("updated_at") or p.get("created_at") or ""),
@ -758,12 +771,12 @@ def get_project(drive_root: Any, project_id: str) -> Optional[Dict[str, Any]]:
return None
def get_reserved_project(drive_root: Any, project_id: str) -> Optional[Dict[str, Any]]:
def get_reserved_project(drive_root: Any, project_id: str, *, strict: bool = False) -> Optional[Dict[str, Any]]:
"""Lookup irrespective of lifecycle (history/recovery only)."""
pid = sanitize_project_id(project_id)
if not pid:
return None
for project in list_reserved_projects(drive_root):
for project in list_reserved_projects(drive_root, strict=strict):
if project.get("id") == pid:
return dict(project)
return None
@ -775,10 +788,10 @@ def _bounded_presentation_name(value: Any, *, fallback: str = "") -> str:
def task_presentation_snapshot(drive_root: Any, task_id: str, *, task: Any = None,
result: Any = None, project_id: str = "") -> Dict[str, Any]:
result: Any = None, project_id: str = "", strict: bool = False) -> Dict[str, Any]:
tid = str(task_id or "").strip()
sources = [row for row in (task, result) if isinstance(row, dict)]
if tid:
if tid and not strict:
try:
from ouroboros.task_status import load_effective_task_result
stored = load_effective_task_result(
@ -800,7 +813,7 @@ def task_presentation_snapshot(drive_root: Any, task_id: str, *, task: Any = Non
if pid:
break
if not pid and tid:
binding = project_binding_for_task(drive_root, tid) or {}
binding = project_binding_for_task(drive_root, tid, strict=strict) or {}
pid = str(binding.get("project_id") or "").strip()
pname = ""
registered = False
@ -809,7 +822,7 @@ def task_presentation_snapshot(drive_root: Any, task_id: str, *, task: Any = Non
# ``project_routable`` fact: a workspace-derived proj_<hash> is
# project-SCOPED without having a room, and a producer that announces it
# would point the owner at a project that does not exist.
project = get_reserved_project(drive_root, pid) or {}
project = get_reserved_project(drive_root, pid, strict=strict) or {}
# ROUTABLE, not merely reserved: list_reserved_projects deliberately
# includes deleting/tombstoned history reservations, and those have no
# room left to open.

View file

@ -266,6 +266,13 @@ def _run_periodic_custody_sweep(stop_event: Any = None, latch: Any = None) -> No
set, and the generation is re-read before every mutation.
"""
try:
try:
if _stop_requested(stop_event):
return
from ouroboros.terminal_projection import reconcile_terminal_projections
reconcile_terminal_projections(DATA_DIR)
except Exception:
log.warning("Terminal projection reconciliation deferred", exc_info=True)
try:
# Issue #844: release the owned-daemon start latch in ITS OWN try, ahead of
# the reap, so a raising reap can never pin it; retry once — only when THIS

View file

@ -131,6 +131,7 @@ BAND_PATHS = {
"ouroboros/loop_acceptance.py": "the local preparation incident rides the acceptance decision: incident ride-along, the exposed-stance finish intent and the forced-rail cause split live beside the decision authority they qualify (#1223-#1224)",
"ouroboros/loop_delivery.py": "F6 upstream sync: the delivery-protocol upstream deltas (hold-control literals, trailing-object/fence-aware protocol parsers) folded into the campaign delivery leaf (upstream leaf delivery_protocol.py retired)",
"ouroboros/loop_forced_finalization.py": "Forced-finalization rail of the v7 L-B loop split: one cohesive owner for the forced/orphan/absorption path, moved byte-preserving from loop.py (D01 lane).",
"ouroboros/loop_transport.py": "Entered the band from 981 lines with the #869 unknown-outcome terminal wording: an attempt with no provider outcome now states the real wait it spent and that its cost may be unknown or bounded, beside the existing no-resend fence. The sentence belongs with the episode facts it reads; no new terminal owner, no predicate change. Shrink-only from here.",
"ouroboros/marketplace/ouroboroshub.py": "Entered the band from 373 lines: the hubflow sprint added the adopt transaction (eligibility prelude, CAS re-verification, move-aside + state-quintet snapshot, verified rollback with per-step error collection, retention finalize) beside the existing install/update flows (hubflow sprint, adopt-in-ouroboroshub owner decision D4).",
"ouroboros/mcp_client.py": "F3.1 typed-organ producer cutover (D05 entry 7b) plus E5+s2r2 (#447): the MCP transport keeps the SDK-owned error bit as a typed ToolResult, follows nextCursor pagination with injective 12-hex slugs, and discloses collision/pagination omissions; grew into the band from 984 lines, shrink-only otherwise.",
"ouroboros/memory.py": "ibl-2b09abdadd25: scratchpad content-size cap added alongside the existing block-count cap in append_scratchpad_block's eviction loop",
@ -169,6 +170,7 @@ BAND_PATHS = {
"skills/telegram/plugin.py": None,
"skills/telegram/scripts/companion.py": None,
"skills/telegram/scripts/sidecar.py": None,
"supervisor/events_task_done.py": "Entered the band from exactly 1000 lines with the #1154 terminal-projection continuation and the #1087 shared warning fold: the terminal dispatch now hands both halves of a root's owed projection to one settlement owner and reads child warnings from the shared normalized projection instead of a second execution-axis copy. Shrink-only from here.",
"supervisor/message_bus.py": "push_log gained the A2A frame suppression and explicit-addressing contract comments in the live log routing fix (fix/main-chat-leak sprint); the module sat at exactly 1000 lines before it",
"supervisor/queue_transitions.py": "F2.2 cancel/custody organ: the owner-stop campaign closure (_close_campaign_after_owner_stop, reference row 970) moved in from the hot events monolith to live beside stop_evolution_tasks - one honesty rule, one owner; 1016 lines, shrink-only from here",
"supervisor/state.py": "Entered the band from 967 lines with the STATE_LOCK read-inversion fix (issue #1102): the ledger snapshot is now read OUTSIDE the lock, so the freshness-marker comparison deciding whether a snapshot may overwrite money must sit with the atomic state write it guards. One decision owner - lock, marker comparison, projection write - instead of a second budget authority beside it; its rationale comments are why this regression class stays visible. Shrink-only from here.",

View file

@ -161,7 +161,9 @@ def stamp_root_final_phase(
managed roots keep their task_done conclusion untouched.
"""
if post_task_open:
send_event.setdefault("progress_meta", {})["task_phase"] = "finalizing"
send_event.setdefault("progress_meta", {}).update(
task_phase="finalizing", task_terminal_status=terminal_status,
)
elif task.get("_is_direct_chat"):
send_event.setdefault("progress_meta", {})["task_terminal_status"] = terminal_status

View file

@ -627,22 +627,11 @@ def resolve_task_lineage(
resolved_role = _field(delegation_role, "delegation_role").lower()
resolved_original_id = _field(original_task_id, "original_task_id")
resolved_retry_from = _field(timeout_retry_from, "timeout_retry_from")
is_regular_root = bool(
resolved_task_id
and resolved_root_id == resolved_task_id
and not resolved_parent_id
and resolved_role != "subagent"
)
is_retry_root = bool(
resolved_task_id
and resolved_root_id
and resolved_root_id != resolved_task_id
and not resolved_parent_id
and resolved_role == "root"
and resolved_original_id
and resolved_original_id == resolved_retry_from
and resolved_original_id != resolved_task_id
)
is_regular_root = bool(resolved_task_id and resolved_root_id == resolved_task_id
and not resolved_parent_id and resolved_role != "subagent")
is_retry_root = bool(resolved_task_id and resolved_root_id and resolved_root_id != resolved_task_id
and not resolved_parent_id and resolved_role == "root" and resolved_original_id
and resolved_original_id == resolved_retry_from and resolved_original_id != resolved_task_id)
return {
"task_id": resolved_task_id,
"root_task_id": resolved_root_id,
@ -890,6 +879,16 @@ def write_task_result(
log.debug("Blocked status regression %s -> %s for task %s",
existing.get("status"), projected_status, task_id)
return None
# Only this accepted transition can originate terminal delivery debt:
# enrichment/replica fields cannot adopt historical terminal rows or
# erase provenance persisted before the separate readiness write.
projected_fields.pop("canonical_terminal_projection_origin", None)
if projected_status in _TRULY_TERMINAL_STATUSES and existing_status not in _TRULY_TERMINAL_STATUSES:
merged = {**existing, **projected_fields}
lineage_keys = ("root_task_id", "parent_task_id", "delegation_role", "original_task_id", "timeout_retry_from")
if resolve_task_lineage(task_id, metadata=merged.get("metadata"),
**{key: merged.get(key) for key in lineage_keys})["is_root_task"]:
projected_fields["canonical_terminal_projection_origin"] = "terminal_transition"
now = utc_now_iso()
# ABI-3 write seam: the merge BASE is the existing row normalized onto
# the honest cost names (its own legacy spelling wins its own pair,

View file

@ -0,0 +1,326 @@
"""Retryable Project/Main terminal publication, with no cognition or timer.
The first root terminal transition records provenance; readiness precedes all
effects and the Project receipt retains Main disposition after readiness retires.
A separate short-lived projection
lock serializes callbacks, never holding the result lock across reentrant IO.
The chat row's token heals append/receipt-write crashes. Main uses the existing
bounded outbox; its external send/register crash gap remains at-least-once.
"""
from __future__ import annotations
import json
import logging
import pathlib
import uuid
from contextlib import contextmanager
from typing import Any
from ouroboros.platform_layer import acquire_exclusive_file_lock, release_exclusive_file_lock
from ouroboros.task_results import load_task_result, resolve_task_lineage, task_result_path, write_task_result
from ouroboros.utils import jsonl_chain_handles, utc_now_iso
log = logging.getLogger(__name__)
SETTLEMENT_NONE, SETTLEMENT_DEFERRED, SETTLEMENT_SETTLED = "none", "deferred", "settled"
def _settled(row: dict) -> bool:
from ouroboros.task_status import SETTLED_STATUSES
return row.get("status") in SETTLED_STATUSES
def _lineage(tid: str, row: dict) -> dict:
return resolve_task_lineage(tid, metadata=row.get("metadata"), **{
key: row.get(key) for key in (
"root_task_id", "parent_task_id", "delegation_role", "original_task_id", "timeout_retry_from",
)
})
def _attempt(row: dict) -> dict:
# An obligation token distinguishes two publications even when old records
# lack attempt metadata. These facts additionally fence same-id retries.
metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
return {**{key: row.get(key) for key in ("task_attempt", "_attempt", "started_at")},
"metadata_attempt": metadata.get("attempt"), "metadata_task_attempt": metadata.get("task_attempt")}
def _witness(row: dict) -> dict:
return {"attempt": _attempt(row), "status": row.get("status"),
"checkpoint": row.get("root_phase_checkpoint"),
"artifact_status": row.get("artifact_status"),
"artifact_bundle": row.get("artifact_bundle")}
def _files_ready(root: Any, tid: str, row: dict) -> bool:
from ouroboros.headless import terminal_task_files_ready
return terminal_task_files_ready(pathlib.Path(root), {**row, "id": tid}, row)
def _open(row: dict) -> bool:
checkpoint = row.get("root_phase_checkpoint") or {}
return checkpoint.get("post_task_synthesis") in {"pending_once", "running"}
@contextmanager
def _publication_lock(root: Any, tid: str):
path = task_result_path(root, tid, create=False).with_suffix(".projection.lock")
fd = acquire_exclusive_file_lock(path, timeout_sec=0.01, poll_sec=0.005)
try:
yield fd is not None
finally:
if fd is not None:
release_exclusive_file_lock(path, fd)
def _prepare(root: Any, tid: str, task: dict, event: dict) -> dict:
"""Durably record readiness from canonical terminal authority, before IO."""
def prepare(current: dict, _patch: dict):
if not _settled(current):
return None
effective = {**task, **current}
if not _lineage(tid, effective)["is_root_task"]:
return None
ready = current.get("canonical_terminal_projection_ready")
if (not isinstance(ready, dict)
and current.get("canonical_terminal_projection_origin") != "terminal_transition"):
# Terminal status alone is historical, not proof of new debt.
# Existing readiness remains eligible across protocol upgrades.
return None
marker = current.get("canonical_terminal_projection")
if isinstance(ready, dict) and ready.get("attempt") == _attempt(current):
return None
if isinstance(marker, dict) and not isinstance(ready, dict):
# Pre-protocol Project receipts belonged to the legacy Main sender.
# Do not replay those historical rows after delivered IDs age out.
if "attempt" not in marker or marker["attempt"] == _attempt(current):
return None
chat_id = effective.get("chat_id")
if chat_id is None:
chat_id = event.get("chat_id", 0)
return {"status": current["status"], "canonical_terminal_projection_ready": {
"summary_id": f"task-terminal:{tid}", "token": uuid.uuid4().hex,
"attempt": _attempt(current), "task_done_ts": str(event.get("ts") or current.get("ts") or utc_now_iso()),
"chat_id": int(chat_id or 0),
}}
# Absence is not a terminal authority, and must never create a completed row.
stored = load_task_result(root, tid, strict=True)
if not stored or not _settled(stored):
return stored or {}
return write_task_result(root, tid, stored["status"], strict_existing_dict=True,
_field_projector=prepare)
def _project_row(tid: str, row: dict, event: dict, ready: dict) -> dict:
from ouroboros import project_dialogue as dialogue
from ouroboros.project_facts import resolve_project_id
lineage = _lineage(tid, row)
is_root = lineage["is_root_task"]
role = str(row.get("role") or ("root" if is_root else "child"))
parent = str(lineage["parent_task_id"] or "")
phase = dialogue.outcome_phase(row, event)
outcome = dialogue.OUTCOME_PHASE_HEADLINE[phase]
chat_id = int(ready.get("chat_id", row.get("chat_id", event.get("chat_id", 0))) or 0)
text = (f"{outcome}. Root task {tid}." if is_root
else f"{outcome}. {role} (child {tid} of {parent or 'unknown'}).")
verdict = dialogue._completion_verdict(row, event)
excerpt = dialogue._completion_excerpt(row, chat_id=chat_id, salvage_only=True)
text += "".join(f" {part}" for part in (verdict, excerpt) if part)
result = {
"ts": str(ready.get("task_done_ts") or event.get("ts") or row.get("ts") or utc_now_iso()),
"direction": "system", "type": "task_summary",
"summary_id": f"task-terminal:{tid}",
"summary_kind": "terminal_root_projection" if is_root else "terminal_result_projection",
"task_id": tid, "parent_task_id": parent, "root_task_id": lineage["root_task_id"],
"project_id": resolve_project_id(row), "chat_id": chat_id,
"delegation_role": str(row.get("delegation_role") or ""), "role": role,
"status": row["status"], "outcome": outcome, "outcome_phase": phase, "outcome_final": True,
"outcome_authority": "canonical_task_result_after_finalization",
"outcome_axes": row.get("outcome_axes") or {}, "reason_code": str(row.get("reason_code") or ""),
"result_ref": {"kind": "task_result", "task_id": tid, "reader": "get_task_result"}, "text": text,
**({"reason_detail": verdict} if verdict else {}),
**({"terminal_projection_token": ready["token"]} if ready.get("token") else {}),
}
if isinstance(row.get("model_execution"), dict):
result["model_execution"] = dict(row["model_execution"])
return result
def _already_in_chat(root: Any, row: dict) -> bool:
path = pathlib.Path(root) / "logs" / "chat.jsonl"
# Pin the live inode before enumerating archives: rotation between append
# and receipt persistence must not turn an existing row into an absence.
with jsonl_chain_handles(path, strict=True) as handles:
for segment, stream in handles:
for line_number, line in enumerate(stream, 1):
if not line.strip():
continue
try:
entry = json.loads(line)
except (json.JSONDecodeError, UnicodeDecodeError):
log.warning("Skipping malformed terminal projection history row at %s:%s", segment, line_number)
continue
if not isinstance(entry, dict):
log.warning("Skipping non-object terminal projection history row at %s:%s", segment, line_number)
continue
if (entry.get("summary_id") == row["summary_id"]
and entry.get("terminal_projection_token") == row.get("terminal_projection_token")):
return True
return False
def _append_project(root: Any, tid: str, task: dict, event: dict) -> bool:
from ouroboros import project_dialogue as dialogue
stored = _prepare(root, tid, task, event)
if not stored or not _settled(stored):
return False
effective = {**task, **stored}
is_root = _lineage(tid, effective)["is_root_task"]
ready = stored.get("canonical_terminal_projection_ready") or {}
marker = stored.get("canonical_terminal_projection")
if isinstance(marker, dict) and (not ready or marker.get("token") == ready.get("token")):
return False
if is_root and (not ready or _open(stored) or not _files_ready(root, tid, effective)):
return False
row = _project_row(tid, effective, {key: event[key] for key in ("ts", "chat_id") if key in event}, ready)
appended = False
if not is_root or not _already_in_chat(root, row):
appended = dialogue.append_canonical_task_summary(root, row)
if not appended:
return False
def receipt(current: dict, _patch: dict):
if (_witness(current) != _witness(stored)
or current.get("canonical_terminal_projection_ready") != stored.get("canonical_terminal_projection_ready")):
return None
return {"status": current["status"], "canonical_terminal_projection": {
"summary_id": row["summary_id"], "summary_kind": row["summary_kind"],
"written_at": row["ts"], "chat_id": row["chat_id"],
"attempt": _attempt(stored), **({"token": ready["token"]} if ready.get("token") else {}),
}}
write_task_result(root, tid, stored["status"], strict_existing_dict=True, _field_projector=receipt)
return appended
def append_terminal_projection(root: Any, tid: str, task: dict, event: dict, *, result: dict | None = None) -> bool:
with _publication_lock(root, tid) as acquired:
if not acquired:
return False
# Compatibility callers may hand over the first terminal observation.
# Persist it before readiness/effects; never overwrite existing authority.
if result and _settled(result) and load_task_result(root, tid, strict=True) is None:
fields = {**(task or {}), **result}
status = fields.pop("status")
fields.pop("task_id", None)
write_task_result(root, tid, status, create_only=True, strict_existing_dict=True, **fields)
return _append_project(root, tid, task or {}, event or {})
def clear_terminal_projection_obligation(root: Any, tid: str, expected: dict, disposition: str) -> bool:
"""CAS the complete readiness token and canonical attempt/checkpoint witness."""
if disposition not in {"owed", "ineligible"}:
return False
cleared = False
def retire(current: dict, _patch: dict):
nonlocal cleared
ready = expected.get("canonical_terminal_projection_ready")
marker = current.get("canonical_terminal_projection")
if (not isinstance(ready, dict) or not isinstance(marker, dict)
or current.get("canonical_terminal_projection_ready") != ready
or marker != expected.get("canonical_terminal_projection")
or _witness(current) != _witness(expected) or _open(current)
or not _files_ready(root, tid, current)
or marker.get("token") != ready.get("token")):
return None
cleared = True
return {"status": current["status"], "canonical_terminal_projection_ready": None,
"canonical_terminal_projection": {**marker, "main_disposition": disposition}}
write_task_result(root, tid, str(expected.get("status") or "completed"),
strict_existing_dict=True, _field_projector=retire)
return cleared
def settle_terminal_projection(drive_root: Any, task_id: str, *, task: dict | None = None,
event: dict | None = None) -> str:
from ouroboros import project_dialogue as dialogue
tid = str(task_id or "").strip()
if not tid:
return SETTLEMENT_NONE
try:
with _publication_lock(drive_root, tid) as acquired:
if not acquired:
return SETTLEMENT_DEFERRED
stored = _prepare(drive_root, tid, task or {}, event or {})
if not isinstance(stored.get("canonical_terminal_projection_ready"), dict):
return SETTLEMENT_NONE
if (_open(stored) or not _settled(stored)
or not _files_ready(drive_root, tid, {**(task or {}), **stored})):
return SETTLEMENT_DEFERRED
_append_project(drive_root, tid, task or {}, event or {})
# IO may have advanced canonical state. Render Main from the NEW
# result, never the task_done snapshot passed to this continuation.
stored = load_task_result(drive_root, tid, strict=True) or {}
ready = stored.get("canonical_terminal_projection_ready")
marker = stored.get("canonical_terminal_projection")
if (not isinstance(ready, dict) or not isinstance(marker, dict)
or marker.get("token") != ready.get("token") or _open(stored)
or not _settled(stored)
or not _files_ready(drive_root, tid, {**(task or {}), **stored})):
return SETTLEMENT_DEFERRED
effective = {**(task or {}), **stored, "id": tid}
done = {**(event or {}), **stored, "ts": ready["task_done_ts"], "chat_id": ready["chat_id"]}
retired = False
def retire_owed() -> bool:
nonlocal retired
retired = clear_terminal_projection_obligation(drive_root, tid, stored, "owed")
return retired
disposition, _queued = dialogue.project_completion_delivery_outcome(
drive_root, done, tid, effective, stored, done, _on_owed=retire_owed)
if retired or clear_terminal_projection_obligation(drive_root, tid, stored, disposition):
return SETTLEMENT_SETTLED
return SETTLEMENT_DEFERRED
except Exception:
log.warning("Terminal projection deferred for %s", tid, exc_info=True)
return SETTLEMENT_DEFERRED
def reconcile_terminal_projections(drive_root: Any) -> int:
"""Discover the terminal-write/readiness-write crash gap on the existing pass.
Never re-enter synthesis or infer debt from historical terminal status. Read
each authority strictly: one bad sibling must not stall others or quarantine
unknown bytes via a tolerant scan.
"""
from ouroboros.task_results import task_results_dir
settled = 0
for path in sorted(task_results_dir(drive_root, create=False).glob("*.json")):
try:
row = load_task_result(drive_root, path.stem, strict=True)
if not row or not _settled(row):
continue
ready = row.get("canonical_terminal_projection_ready")
if (not isinstance(ready, dict)
and row.get("canonical_terminal_projection_origin") != "terminal_transition"):
continue
marker = row.get("canonical_terminal_projection")
if isinstance(marker, dict) and not isinstance(ready, dict):
if "attempt" not in marker or marker["attempt"] == _attempt(row):
continue
tid = row["task_id"]
if _lineage(tid, row)["is_root_task"] and not _open(row):
settled += settle_terminal_projection(drive_root, tid) == SETTLEMENT_SETTLED
except Exception:
log.warning("Terminal projection reconciliation deferred for %s", path, exc_info=True)
return settled

View file

@ -36,6 +36,69 @@ CANCEL_FAILED = "failed"
_CANCEL_TERMINALIZED = frozenset({CANCEL_CANCELLED, CANCEL_ALREADY_SETTLED, CANCEL_NOT_FOUND})
# Transport is recorded fact, never proof that an HTTP caller was the owner.
# The browser twin is ``web/modules/cancel_presentation.js``; both
# tables must name the same sources, because one stored ``cancel_origin`` is
# rendered by the task card AND by this host's durable terminal rows.
CANCEL_SOURCE_PHRASES = {
"http_single": "Stopped from the app (Stop now)",
"http_cascade": "Stopped from the app (Stop now)",
"http_graceful": "Stopped from the app (Wrap up)",
# ``_cancel_subtree_sweep`` mints this for every captured descendant.
"cascade_descendant": "Stopped with the task tree it belongs to",
}
# A recorded cause is producer free text; bound it before it joins an owner line.
CANCEL_REASON_MAX_CHARS = 160
def cancel_cause_clauses(
origin: Dict[str, Any], result: Dict[str, Any], event: Dict[str, Any],
) -> List[str]:
"""The clauses a recorded ``cancel_origin`` PROVES, for one owner line.
Written here, beside ``_intent_outcome_fields`` which records the origin, so
the producer and the sentence share a home. The caller joins them with its
own separator; the browser twin is the cancelled branch of
``log_events.js::taskReasonDetail`` and must stay word for word identical.
``requested_by`` says which run ASKED — a cascade stamps the SWEPT ROOT
there — so reading it as the initiator named a task that somebody else had
stopped (#1061). When this record's OWN lineage proves that asker is its
parent or ancestor, the line states that relation. Only a typed
``request_origin`` proves an actor; ``requested_by`` alone never does.
"""
from ouroboros.utils import strip_markdown
request_origin = origin.get("request_origin")
actor = (
str(request_origin.get("task_id") or "")
if isinstance(request_origin, dict) and request_origin.get("kind") == "agent_task"
else ""
)
asked = str(origin.get("requested_by") or "")
record = {**event, **result}
self_id = str(record.get("task_id") or record.get("id") or record.get("subagent_task_id") or "")
parent = str(record.get("parent_task_id") or "")
root = str(record.get("root_task_id") or "")
relation = ""
if asked and asked != self_id:
if asked == parent:
relation = "Stopped with its parent task"
elif parent and asked == root:
relation = "Stopped with an ancestor task"
source = str(origin.get("source") or "")
reason = " ".join(strip_markdown(str(origin.get("reason") or "")).split())
if len(reason) > CANCEL_REASON_MAX_CHARS:
reason = reason[:CANCEL_REASON_MAX_CHARS - 1].rstrip() + "\u2026"
return [
CANCEL_SOURCE_PHRASES.get(source, source),
reason,
"this task and its sub-tasks" if origin.get("scope") == "cascade" else "",
relation,
"Requested by a task" if actor else "",
]
def _load_result_row(q: Any, task_id: str) -> Dict[str, Any]:
"""The durable result row, or ``{}`` — fail-soft."""
try:

View file

@ -57,17 +57,34 @@ log = logging.getLogger(__name__)
_DEGRADED_TERMINAL_REASONS = frozenset({"configured_actor_incomplete", "configured_actor_unknown"})
def _finished_with_warnings(task_done_event: Dict[str, Any]) -> bool:
"""True when a `completed` lifecycle carries a degraded execution axis.
def _finished_with_warnings(
task_done_event: Dict[str, Any], result: Dict[str, Any] | None = None,
) -> bool:
"""True when a `completed` lifecycle actually ended with warnings.
The chat line used to take its icon and verb from the lifecycle alone, so a
child that never ran its leaf still read as "✅ … completed" while the web
card, computing severity from these same axes, showed a warning. One terminal,
one story — for the EXECUTION axis: this mirrors only that axis of
`web/modules/log_events.js` `taskOutcomeSeverity`, whose objective and review
axes are not read here, so a child degraded on those axes alone still reads
as a clean completion in chat.
card showed a warning. Mirroring only the EXECUTION axis fixed half of that
(#1087): a child degraded on its objective, its review or its artifacts — the
other axes the card folds — still read as a clean completion. The shared
normalized projection (`project_dialogue.outcome_phase`, the host twin of
`taskOutcomeSeverity` that web/tests/fixtures/outcome_phase_parity.json pins)
answers the whole question instead, over the result AND the event.
PRECEDENCE IS THE CALLER'S: this only says "warnings", never which lifecycle
word the row uses — a cancelled or failed child keeps its own status display.
The axis/reason fallbacks below stay for a frame the shared projection cannot
settle (an open post-task checkpoint reads as still working, not as an
outcome), so a degraded frame never silently loses its warning.
"""
from ouroboros.project_dialogue import outcome_phase
record = result if isinstance(result, dict) else {}
phase = outcome_phase(record, task_done_event)
if phase == "warn":
return True
if phase in {"error", "cancelled"}:
return False
axes = task_done_event.get("outcome_axes")
execution = axes.get("execution") if isinstance(axes, dict) else None
if isinstance(execution, dict) and str(execution.get("status") or "") == EXECUTION_DEGRADED:
@ -75,12 +92,38 @@ def _finished_with_warnings(task_done_event: Dict[str, Any]) -> bool:
return str(task_done_event.get("reason_code") or "") in _DEGRADED_TERMINAL_REASONS
def _completed_lifecycle_display(
task_done_event: Dict[str, Any], result: Dict[str, Any] | None = None,
) -> tuple[str, str] | None:
"""Icon and verb for a `completed` lifecycle whose OUTCOME is not clean.
#1087: the card and Telegram fold the axes into one phase; the chat line
must speak the same word. A completed lifecycle can still end `error` (a
failed review, objective or artifacts), and `_finished_with_warnings`
deliberately answers False for that phase — so answering only "warnings"
let such a child read "✅ … completed". Returns None for a clean outcome.
Lifecycle-keyed fields (`subagent_event`, progress_meta `status`) are never
touched: only what the human line SAYS follows the phase.
"""
from ouroboros.project_dialogue import outcome_phase
record = result if isinstance(result, dict) else {}
phase = outcome_phase(record, task_done_event)
if phase == "error":
return "❌", "finished with a failed outcome"
if _finished_with_warnings(task_done_event, record):
return "⚠️", "finished with warnings"
return None
def _authoritative_terminal_cost(
task_id: str, task: Dict[str, Any], result: Dict[str, Any], evt: Dict[str, Any], drive_root: pathlib.Path,
*, breakdown: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""Project terminal cost; the optional breakdown belongs to drive_root only."""
from ouroboros.cost_projection import honest_accounted_amount
from ouroboros.cost_projection import (
COST_SCOPE_ROOT_TREE, build_cost_presentation, honest_accounted_amount,
)
from supervisor.state import reconstruct_task_cost
authority_root = pathlib.Path(task.get("budget_drive_root") or drive_root)
@ -143,6 +186,7 @@ def _authoritative_terminal_cost(
# non-final purely by a child's open row reported a cause of 0, a flag
# no reader could reconstruct.
"non_final_rows": int(subtree.get("non_final_rows") or 0),
"cost_presentation": build_cost_presentation(subtree, scope=COST_SCOPE_ROOT_TREE),
})
except Exception:
log.error("Root subtree cost authority unavailable for %s", task_id, exc_info=True)
@ -152,6 +196,7 @@ def _authoritative_terminal_cost(
"accounted_upper_bound_usd": None,
"accounted_upper_bound_usd_with_children": None,
"cost_with_children_partial": True,
"cost_presentation": None,
})
elif not is_root:
from ouroboros.cost_projection import resolve_cost_pair
@ -159,9 +204,20 @@ def _authoritative_terminal_cost(
present, rollup = resolve_cost_pair(
result, "accounted_upper_bound_usd_with_children", "cost_usd_with_children")
if not present:
_, rollup = resolve_cost_pair(
present, rollup = resolve_cost_pair(
evt, "accounted_upper_bound_usd_with_children", "cost_usd_with_children")
projection["accounted_upper_bound_usd_with_children"] = rollup
if present:
projection["accounted_upper_bound_usd_with_children"] = rollup
# A child pipeline mirrors its OWN bound into the rollup key (it never
# walks descendants), so for a leaf that mirror carries no second
# scope and the own facts stand. A child that ran descendants, or a
# rollup that differs from the own bound, is a subtree without
# same-scope row facts: an own carrier must not replace it.
swarm = result.get("swarm_efficiency")
fanned_out = isinstance(swarm, dict) and int(swarm.get("subagent_count") or 0) > 0
own_bound = projection.get("accounted_upper_bound_usd")
if fanned_out or (rollup is not None and rollup != own_bound):
projection["cost_presentation"] = None
projection["cost_with_children_partial"] = bool(
result.get("cost_with_children_partial", evt.get("cost_with_children_partial", True))
)
@ -290,10 +346,8 @@ def _finish_task_done_dispatch(
) -> None:
"""Notify lineage, release queue state, and preserve terminal compatibility."""
from ouroboros.project_dialogue import (
append_terminal_task_projection,
enqueue_project_completion_summary,
)
from ouroboros.post_task_checkpoint import settle_terminal_projection
from ouroboros.project_dialogue import append_terminal_task_projection
# This seam is shared by the normal task_done path AND the lifecycle-fault
# resolver, so open owner-quiz/hurry projections settle on EVERY dispatched
@ -310,8 +364,15 @@ def _finish_task_done_dispatch(
ctx.DRIVE_ROOT, str(task_id or ""), task, final_task_result, task_done_event,
)
enqueue_project_completion_summary(
ctx.DRIVE_ROOT, evt, str(task_id or ""), task, final_task_result, task_done_event,
# #1154: ONE continuation owns both halves of a root's owed terminal
# projection — the canonical Project row and Main's single mirror. It defers
# while post-task synthesis is still open, so the early answer stays in the
# Project thread and Main hears once, after the run has really finished; the
# post-task callback re-enters the same continuation. A run that owes nothing
# (a child, or a root already settled) returns immediately.
settle_terminal_projection(
ctx.DRIVE_ROOT, str(task_id or ""), task=task,
event={**(evt if isinstance(evt, dict) else {}), **task_done_event},
)
if task_id and str(task.get("delegation_role") or "") == "subagent":
@ -344,10 +405,12 @@ def _finish_task_done_dispatch(
STATUS_INTERRUPTED: ("⏹️", STATUS_INTERRUPTED, STATUS_INTERRUPTED),
}.get(status, ("ℹ️", status or "done", status or "finished"))
icon, subagent_event, verb = status_display
if status == STATUS_COMPLETED and _finished_with_warnings(task_done_event):
if status == STATUS_COMPLETED:
# Icon and verb only: `subagent_event` and progress_meta `status`
# stay the lifecycle values every card and Telegram consumer keys on.
icon, verb = "⚠️", "finished with warnings"
display = _completed_lifecycle_display(task_done_event, effective_result)
if display:
icon, verb = display
result_text = str(effective_result.get("result") or "")
trace_text = str(effective_result.get("trace_summary") or "")
constraint = effective_result.get("task_constraint")
@ -380,6 +443,8 @@ def _finish_task_done_dispatch(
"reserved_usd": _cost_meta.get("reserved_usd"),
"unresolved_upper_bound_usd": _cost_meta.get("unresolved_upper_bound_usd"),
"ledger_integrity_degraded": _cost_meta.get("ledger_integrity_degraded"),
# #498: the scoped carrier travels with the amount it explains.
"cost_presentation": _cost_meta.get("cost_presentation"),
"cost_accounting_error": _cost_meta.get("cost_accounting_error"),
"cost_accounting_status": str(
task_done_event.get("cost_accounting_status") or "unavailable"

View file

@ -1322,6 +1322,8 @@ def log_chat(
for key in SUBAGENT_MESSAGE_FIELDS:
if key in meta:
record[key] = meta[key]
if "narration" in meta:
record["narration"] = bool(meta["narration"])
if record_type in ("project_started", "project_handoff", "project_completion_summary"):
for key in ("project_id", "project_name", "target_label", "status", "completion_answer", "handoff_id"):
if key in meta:
@ -1378,11 +1380,31 @@ def send_with_budget(chat_id: int, text: str, log_text: Optional[str] = None,
is_progress: bool = False, task_id: str = "",
progress_meta: Optional[Dict[str, Any]] = None,
ts: Optional[str] = None,
role: str = "", system_type: str = "") -> None:
role: str = "", system_type: str = "",
narration: Optional[bool] = None) -> None:
"""Send one owner-visible message through the shared host seam.
``narration`` is the note's VOICE, the same typed fact the worker stamps on
every progress frame (``agent._emit_progress``): ``True`` only for the model's
own speech, ``False`` for the host talking about a turn. The browser reads an
ABSENT key as legacy narration, so every send that went through this seam
without one — startup, restart, evolution and command-reply notices, the
provider-death incident — was promoted to the card's title and collapsed
activity line as if Ouroboros had said it (#1011). New sends are therefore
normalized here to the host voice, once, into the ONE meta dict both the
durable record and the live bridge receive; an explicit value from the caller
(or one already on ``progress_meta``) is preserved, and stored rows written
before the fact keep their legacy reading.
"""
st = load_state()
owner_id = int(st.get("owner_id") or 0)
_text = str(text or "")
msg_ts = ts or utc_now_iso()
meta = dict(progress_meta or {})
if narration is not None:
meta["narration"] = bool(narration)
meta.setdefault("narration", False)
progress_meta = meta
if is_progress and DATA_DIR:
progress_record = {

View file

@ -786,10 +786,15 @@ def reconstruct_task_cost(
"cost_final": True, "reserved_usd": 0.0,
"unresolved_upper_bound_usd": 0.0, "unknown_unmetered": 0,
"non_final_rows": 0,
# No task was named, so no ledger bucket was summed: there is nothing
# for a carrier to explain (#498), and None says exactly that.
"cost_presentation": None,
}
else:
try:
from ouroboros.cost_projection import honest_accounted_amount
from ouroboros.cost_projection import (
COST_SCOPE_OWN, build_cost_presentation, honest_accounted_amount,
)
from ouroboros.usage_accounting import ensure_legacy_imported, usage_breakdown
authority_root = pathlib.Path(drive_root) if drive_root is not None else DRIVE_ROOT
@ -821,12 +826,15 @@ def reconstruct_task_cost(
# The disclosed CAUSE of cost_final=false, carried with the flag.
"non_final_rows": int(bucket.get("non_final_rows") or 0),
"ledger_integrity_degraded": bool(bucket.get("integrity_degraded")),
# #498: the same bucket's own explanation of its own amount.
"cost_presentation": build_cost_presentation(bucket, scope=COST_SCOPE_OWN),
}
except Exception:
log.error("Failed to reconstruct ledger task cost for %s", task_id, exc_info=True)
projection = {
"cost_accounting_status": "unavailable", "cost_final": False,
"cost_accounting_error": "ledger_unavailable",
"cost_presentation": None,
"accounted_upper_bound_usd": None, "total_rounds": None,
"prompt_tokens": None, "completion_tokens": None,
"reserved_usd": None, "unresolved_upper_bound_usd": None,

File diff suppressed because one or more lines are too long

View file

@ -76,7 +76,14 @@ TERMINAL_WRITERS = {
('ouroboros/mutation_attribution.py::capture_mutation_baseline', 'status'): 'dynamic',
('ouroboros/mutation_attribution.py::record_terminal_mutation_candidates', 'status'): 'dynamic',
('ouroboros/post_task_checkpoint.py::set_root_post_task_checkpoint', 'str(existing.get("status") or task.get("status") or STATUS_COMPLETED)'): 'terminal',
('ouroboros/project_dialogue.py::_append_terminal_task_projection', 'status'): 'dynamic',
# #1154: the compare-and-clear of a settled terminal-projection obligation.
# It preserves the record's CURRENT status inside the projector and publishes
# no lifecycle transition of its own; the status argument is only the
# primitive's required placeholder.
('ouroboros/terminal_projection.py::_prepare', 'stored["status"]'): 'dynamic',
('ouroboros/terminal_projection.py::_append_project', 'stored["status"]'): 'dynamic',
('ouroboros/terminal_projection.py::append_terminal_projection', 'status'): 'dynamic',
('ouroboros/terminal_projection.py::clear_terminal_projection_obligation', 'str(expected.get("status") or "completed")'): 'terminal',
('ouroboros/project_naming.py::spawn_turn_namer._work', 'status'): 'dynamic',
('ouroboros/project_dialogue.py::persist_continuation_narrative', 'requested_status'): 'dynamic',
# The locked field projector preserves the existing status, including a

View file

@ -0,0 +1,330 @@
"""#498 — the scoped cost carrier: one amount and the facts that explain it.
The defect this pins: a card could take its number from the task subtree and its
unknown-price markers from the task itself, so a parent that had spent nothing
showed an honest ``$0.00`` over a child whose price nobody knew. The carrier is
built by the producer that summed the rows and states which scope it describes,
so a consumer can never pair the two halves of different questions.
Monetary sums and ``cost_final`` are deliberately unchanged here — the carrier
only adds facts beside them.
"""
from __future__ import annotations
import pytest
from ouroboros._usage_rows import _summary
from ouroboros.cost_projection import (
COST_OPENNESS_FIELDS,
COST_SCOPE_OWN,
COST_SCOPE_ROOT_TREE,
build_cost_presentation,
carry_cost_meta,
cost_projection,
)
def _row(**over):
return {"kind": "attempt", "state": "settled", "cost_usd": 1.0, "cost_final": True, **over}
class TestPricedEvidence:
"""A zero survives only where a priced or bounded row actually evidenced it."""
def test_proven_zero_keeps_its_amount(self):
summary = _summary([_row(cost_usd=0.0)])
assert summary["priced_rows"] == 1
carrier = build_cost_presentation(summary, scope=COST_SCOPE_OWN)
assert carrier == {
"scope": "own", "tracked_amount": 0.0, "has_unpriced": False,
"tracked_final": True, "accounting_open": False, "has_rows": True,
}
def test_estimated_zero_is_open_not_a_settled_free_result(self):
summary = _summary([_row(cost_usd=0.0, cost_final=False)])
carrier = build_cost_presentation(summary, scope=COST_SCOPE_OWN)
assert carrier["tracked_amount"] == 0.0
assert carrier["tracked_final"] is False and carrier["accounting_open"] is True
def test_empty_ledger_is_unknown_not_zero(self):
carrier = build_cost_presentation(_summary([]), scope=COST_SCOPE_OWN)
assert carrier["tracked_amount"] is None
assert carrier["has_rows"] is False and carrier["has_unpriced"] is False
def test_all_unpriced_without_a_bound_is_unknown(self):
summary = _summary([_row(cost_usd=None), _row(cost_usd=None)])
assert summary["priced_rows"] == 0 and summary["unknown_unmetered"] == 2
carrier = build_cost_presentation(summary, scope=COST_SCOPE_OWN)
assert carrier["tracked_amount"] is None
assert carrier["has_rows"] is True and carrier["has_unpriced"] is True
def test_mixed_zero_keeps_the_tracked_zero_and_discloses_the_unpriced_rows(self):
# A priced $0 sharing a scope with an unpriced row: the tracked zero is
# real evidence and survives, with the unknown stated beside it. The
# count-free heuristic used everywhere else discards exactly this case.
from ouroboros.cost_projection import honest_accounted_amount
summary = _summary([_row(cost_usd=0.0), _row(cost_usd=None)])
assert honest_accounted_amount(summary) is None
carrier = build_cost_presentation(summary, scope=COST_SCOPE_OWN)
assert carrier["tracked_amount"] == 0.0 and carrier["has_unpriced"] is True
assert carrier["tracked_final"] is True
assert carrier["accounting_open"] is False
def test_a_retained_bound_is_priced_evidence_and_keeps_the_scope_open(self):
summary = _summary([{"kind": "attempt", "state": "dispatched",
"reservation_upper_bound_usd": 2.0}])
carrier = build_cost_presentation(summary, scope=COST_SCOPE_OWN)
assert summary["priced_rows"] == 1
assert carrier["tracked_amount"] == 2.0
assert carrier["tracked_final"] is False and carrier["accounting_open"] is True
def test_an_integrity_gap_is_never_exact(self):
from ouroboros._usage_rows import _with_integrity
summary = _with_integrity(_summary([_row()]), True)
carrier = build_cost_presentation(summary, scope=COST_SCOPE_OWN)
assert carrier["tracked_amount"] == 1.0
assert carrier["tracked_final"] is False and carrier["accounting_open"] is True
def test_unavailable_accounting_carries_no_carrier_at_all(self):
assert build_cost_presentation(None, scope=COST_SCOPE_OWN) is None
class TestLatePriceAndCompaction:
def test_a_late_receipt_moves_the_carrier_without_stickiness(self):
open_summary = _summary([{"kind": "attempt", "state": "dispatched",
"reservation_upper_bound_usd": 2.0}])
assert build_cost_presentation(open_summary, scope=COST_SCOPE_OWN)["tracked_final"] is False
settled = _summary([_row(cost_usd=1.4)])
carrier = build_cost_presentation(settled, scope=COST_SCOPE_OWN)
assert carrier["tracked_amount"] == 1.4 and carrier["tracked_final"] is True
def test_a_compacted_group_answers_exactly_like_the_rows_it_folded(self):
rows = [_row(cost_usd=0.5), _row(cost_usd=0.5), _row(cost_usd=None)]
folded = [
{"kind": "usage_baseline_group", "state": "settled", "cost_usd": 1.0,
"cost_final": True, "folded_attempt_count": 2},
{"kind": "usage_baseline_group", "state": "settled", "cost_usd": None,
"folded_attempt_count": 1},
]
raw_summary, folded_summary = _summary(rows), _summary(folded)
assert raw_summary["priced_rows"] == folded_summary["priced_rows"] == 2
assert (build_cost_presentation(raw_summary, scope=COST_SCOPE_OWN)
== build_cost_presentation(folded_summary, scope=COST_SCOPE_OWN))
class TestScopeSelection:
"""The root/child reversal: presence selects the subtree, never a null fallback."""
def test_a_root_tree_carrier_states_its_own_scope(self):
carrier = build_cost_presentation(_summary([_row()]), scope=COST_SCOPE_ROOT_TREE)
assert carrier["scope"] == "root_tree"
def test_a_null_subtree_amount_never_falls_back_to_the_parents_own_zero(self):
# The parent itself spent a proven zero; the child's price is unknown.
own = build_cost_presentation(_summary([_row(cost_usd=0.0)]), scope=COST_SCOPE_OWN)
subtree = build_cost_presentation(
_summary([_row(cost_usd=None)]), scope=COST_SCOPE_ROOT_TREE)
assert own["tracked_amount"] == 0.0
assert subtree["tracked_amount"] is None and subtree["has_unpriced"] is True
# A reader that takes the root_tree carrier gets the honest unknown; the
# two carriers never merge, because each names the scope it describes.
assert own["scope"] != subtree["scope"]
class TestCarryThrough:
def test_the_carrier_is_an_openness_marker_and_rides_every_shared_seam(self):
from ouroboros.task_results import TASK_COST_META_FIELDS
assert "cost_presentation" in COST_OPENNESS_FIELDS
assert "cost_presentation" in TASK_COST_META_FIELDS
carrier = build_cost_presentation(_summary([_row()]), scope=COST_SCOPE_OWN)
source = {"accounted_upper_bound_usd": 1.0, "cost_final": True,
"cost_presentation": carrier}
assert carry_cost_meta(source)["cost_presentation"] == carrier
assert cost_projection(source)["cost_presentation"] == carrier
# Absent stays absent: normalization never invents the carrier.
assert "cost_presentation" not in carry_cost_meta({"accounted_upper_bound_usd": 1.0})
def test_monetary_sums_and_finality_are_unchanged_by_the_new_fact(self):
summary = _summary([_row(cost_usd=0.5), _row(cost_usd=None)])
assert summary["settled_usd"] == 0.5
assert summary["accounted_usd"] == 0.5
assert summary["cost_final"] is False
assert summary["unknown_unmetered"] == 1
def test_the_task_result_fields_carry_the_own_scope_carrier(self, tmp_path):
from supervisor.state import reconstruct_task_cost
fields = reconstruct_task_cost("nothing-here", fields=True, drive_root=tmp_path)
assert "cost_presentation" in fields
if fields.get("cost_accounting_status") == "available":
# An empty ledger is not a measured zero, even though the sum is 0.0.
assert fields["cost_presentation"] == {
"scope": "own", "tracked_amount": None, "has_unpriced": False,
"tracked_final": False, "accounting_open": False, "has_rows": False,
}
else:
# An unreadable ledger says so through its own status, with no carrier.
assert fields["cost_presentation"] is None
def test_an_unreadable_count_degrades_to_no_carrier_instead_of_a_wrong_one():
# A count that cannot be read at all yields NO carrier — never a confident
# amount over evidence this function could not parse.
assert build_cost_presentation(
{**_summary([_row()]), "priced_rows": "x"}, scope=COST_SCOPE_OWN) is None
# An absent count reads as "nothing priced", which is the conservative answer.
assert build_cost_presentation(
{**_summary([_row()]), "priced_rows": None},
scope=COST_SCOPE_OWN)["tracked_amount"] is None
def test_settled_unknown_does_not_make_known_subtotal_open():
summary = _summary([_row(cost_usd=1.2), _row(cost_usd=None)])
carrier = build_cost_presentation(summary, scope=COST_SCOPE_ROOT_TREE)
assert carrier['tracked_amount'] == 1.2 and carrier['tracked_final'] is True
assert carrier['accounting_open'] is False and carrier['has_unpriced'] is True
assert summary['cost_final'] is False # unchanged monetary contract
def test_zero_bounds_and_estimates_are_positive_nonfinal_row_facts():
for row in [
_row(cost_usd=0, cost_final=False),
_row(cost_usd=None, reservation_upper_bound_usd=0),
{'state': 'reserved', 'reservation_upper_bound_usd': 0},
{'state': 'dispatched', 'reservation_upper_bound_usd': 0},
{'state': 'unresolved', 'reservation_upper_bound_usd': 0},
]:
carrier = build_cost_presentation(_summary([row]), scope=COST_SCOPE_OWN)
assert carrier['tracked_amount'] == 0
assert carrier['tracked_final'] is False and carrier['accounting_open'] is True
def test_legacy_summary_without_positive_row_facts_cannot_claim_exactness():
assert build_cost_presentation({'accounted_usd': 0, 'cost_final': True}, scope=COST_SCOPE_OWN) is None
# Production ledger/compaction fixtures, executed only by the parent's isolated suite.
from tests import fixtures_usage_compaction as compaction_fixtures
data_root_any_tier = compaction_fixtures.data_root_any_tier
data_root = compaction_fixtures.data_root
def test_real_compaction_preserves_all_presentation_facts(data_root):
from ouroboros import usage_accounting as usage
compaction_fixtures._seed_mixed_ledger(data_root)
compaction_fixtures._settle(data_root, cost=0, cost_final=True, task_id='zero')
compaction_fixtures._settle(data_root, cost=0, cost_final=False, task_id='estimated-zero')
before = usage.usage_breakdown(data_root, root_task_id='root')
receipt = compaction_fixtures._compact(data_root)
assert receipt is not None
assert any(row['kind'] == 'usage_baseline_group' for row in compaction_fixtures._ledger_rows(data_root))
after = usage.usage_breakdown(data_root, root_task_id='root')
for field in ['priced_rows', 'tracked_nonfinal_rows', 'accounting_open_rows', 'unknown_unmetered',
'accounted_usd', 'settled_usd', 'reserved_usd', 'unresolved_upper_bound_usd', 'cost_final']:
assert after[field] == before[field], field
assert build_cost_presentation(after, scope=COST_SCOPE_ROOT_TREE) == build_cost_presentation(before, scope=COST_SCOPE_ROOT_TREE)
@pytest.mark.serial
def test_producer_to_wire_consumers_keep_tree_scope_and_late_unknown_price(data_root):
import asyncio
import json
import subprocess
from pathlib import Path
from types import SimpleNamespace
from ouroboros import usage_accounting as usage
from ouroboros.cost_projection import live_root_cost_projection
from ouroboros.gateway.history import make_chat_history_endpoint
from ouroboros.post_task_checkpoint import project_replica_task_result_fields
from ouroboros.post_task_synthesis import _pre_synthesis_usage_snapshot
from ouroboros.task_results import write_task_result
from ouroboros.utils import append_jsonl
from supervisor.events_task_done import _authoritative_terminal_cost
from supervisor.state import reconstruct_task_cost
# Real priced parent zero plus real child's retained unresolved bound.
compaction_fixtures._settle(data_root, cost=0, cost_final=True, task_id='root')
child = usage.reserve_attempt(compaction_fixtures._request(data_root, task_id='child', reservation_usd=2.0))
usage.mark_dispatched(child)
usage.mark_unresolved(child, 'receipt unavailable')
usage.terminalize_abandoned_attempt(child, reason='child exited without receipt')
task = {'id': 'root', 'root_task_id': 'root', 'chat_id': 1}
terminal = _authoritative_terminal_cost('root', task, {}, {}, data_root)
heartbeat = live_root_cost_projection('root', task, {}, data_root)
own = reconstruct_task_cost('root', fields=True, drive_root=data_root)
assert own['cost_presentation']['tracked_amount'] == 0
assert terminal['cost_presentation']['scope'] == heartbeat['cost_presentation']['scope'] == 'root_tree'
assert terminal['cost_presentation']['tracked_amount'] == 2
assert terminal['cost_presentation']['tracked_final'] is False
overlay = project_replica_task_result_fields(terminal, own)
assert 'cost_presentation' not in overlay
synthesis = _pre_synthesis_usage_snapshot(SimpleNamespace(drive_root=data_root), task, own)
assert synthesis['cost_presentation'] == terminal['cost_presentation']
write_task_result(data_root, 'root', 'completed', root_task_id='root', chat_id=1, **terminal)
append_jsonl(data_root / 'logs/progress.jsonl', {
'type': 'send_message', 'is_progress': True, 'task_id': 'root', 'chat_id': 1,
'content': 'Reading source', 'text': 'Reading source', 'ts': '2026-09-20T00:00:00Z', **terminal,
})
response = asyncio.run(make_chat_history_endpoint(data_root)(SimpleNamespace(query_params={'chat_id': '1'})))
rows = json.loads(response.body)['messages']
history = next(row for row in rows if row.get('task_id') == 'root' and row.get('cost_presentation'))
assert history['cost_presentation'] == terminal['cost_presentation']
# A real late receipt replaces the abandoned unknown row. The previous
# mixed subtotal cannot be sticky-final, even when lifecycle was completed.
usage.settle_attempt(child, cost_usd=0.35, cost_final=True)
assert compaction_fixtures._ledger_rows(data_root)[-1]['settle_reason'] == 'late_receipt'
late = _authoritative_terminal_cost('root', task, {}, {}, data_root)
assert late['cost_presentation']['has_unpriced'] is False
assert late['cost_presentation']['tracked_final'] is True
# Feed production payloads to the real JS consumer, including the child
# literal/card-meta reader and its sticky reducer. No handcrafted carrier.
script = """
import fs from 'node:fs';
import {taskCostMeta, taskCostProjection, mergeStickyCostMeta, cardMetaKeys} from './web/modules/chat_activity.js';
const rows = JSON.parse(fs.readFileSync(0, 'utf8'));
const projections = rows.map((row, i) => taskCostProjection(cardMetaKeys(row), `2026-09-20T00:0${i}:00Z`));
const sticky = mergeStickyCostMeta(projections[0], projections[3]);
console.log(JSON.stringify({meta: rows.map(taskCostMeta), sticky: sticky.meta,
late: mergeStickyCostMeta(sticky, projections[4]).meta}));
"""
result = subprocess.run(['node', '--input-type=module', '-e', script],
cwd=Path(__file__).parents[1], input=json.dumps([terminal, heartbeat, history, own, late]),
capture_output=True, text=True, check=True)
browser = json.loads(result.stdout)
assert browser['meta'] == [['Tracked: up to $2.00', 'some steps have no price']] * 3 + [['$0.00'], ['$0.35']]
assert browser['sticky'] == ['Tracked: up to $2.00', 'some steps have no price']
assert browser['late'] == ['$0.35']
def test_first_terminal_root_frame_preserves_money_on_tree_failure(monkeypatch):
from ouroboros.cost_projection import with_task_cost_presentation
original = {'cost_accounting_status': 'available', 'accounted_upper_bound_usd': 0,
'cost_final': True, 'cost_presentation': {'scope': 'own', 'tracked_amount': 0}}
monkeypatch.setattr('ouroboros.usage_accounting.usage_breakdown',
lambda *_a, **_kw: (_ for _ in ()).throw(OSError('unreadable tree')))
output = with_task_cost_presentation(original, {'id': 'root', '_skip_post_task_synthesis': True}, '.')
assert output == {**original, 'cost_presentation': None}
@pytest.mark.parametrize('rollup, swarm, keeps_own', [
(4.25, {}, False), # a legacy foreign subtree total: own zero may not stand for it
(0, {'subagent_count': 2}, False), # the child fanned out; its own mirror is not the subtree
(0, {}, True), # a leaf's pipeline mirror of its own bound carries no second scope
(None, {}, True),
])
def test_nested_rollup_scope_rule(tmp_path, monkeypatch, rollup, swarm, keeps_own):
from supervisor.events_task_done import _authoritative_terminal_cost
own = {'cost_accounting_status': 'available', 'accounted_upper_bound_usd': 0,
'cost_final': True, 'cost_presentation': {'scope': 'own', 'tracked_amount': 0}}
monkeypatch.setattr('supervisor.state.reconstruct_task_cost', lambda *_a, **_kw: dict(own))
result = _authoritative_terminal_cost('child', {'parent_task_id': 'root', 'root_task_id': 'root'},
{'accounted_upper_bound_usd_with_children': rollup, 'swarm_efficiency': swarm}, {}, tmp_path)
assert result['accounted_upper_bound_usd_with_children'] == rollup
assert result['cost_presentation'] == (own['cost_presentation'] if keeps_own else None)
assert result['accounted_upper_bound_usd'] == 0

View file

@ -187,12 +187,18 @@ def test_lost_admission_reply_does_not_prove_no_model_work(agent_episode):
@pytest.mark.serial
def test_completion_waits_for_artifacts_and_cost_then_keeps_full_answer(agent_episode):
@pytest.mark.parametrize("carrier", [None, {
"scope": "root_tree", "tracked_amount": 2.5, "has_unpriced": False,
"tracked_final": True, "accounting_open": False, "has_rows": True,
}])
def test_completion_waits_for_artifacts_and_cost_then_keeps_full_answer(agent_episode, carrier):
answer = "Полный ответ " * 3000
final = {"status": "completed", "artifact_bundle": {"status": "ready"}, "cost_final": True,
"cost_with_children_partial": False, "accounted_upper_bound_usd": 1.25,
"accounted_upper_bound_usd_with_children": 2.5,
"cost_known": True, "reserved_usd": 0.0, "unresolved_upper_bound_usd": 0.0,
"unknown_unmetered": 0, "prompt_tokens": 200, "final_answer": answer}
"unknown_unmetered": 0, "prompt_tokens": 200, "final_answer": answer,
"cost_presentation": carrier}
agent_episode.results[:] = [
{**final, "artifact_bundle": {"status": "finalizing"}, "cost_final": False},
{**final, "cost_final": False, "cost_with_children_partial": True}, final,
@ -201,6 +207,9 @@ def test_completion_waits_for_artifacts_and_cost_then_keeps_full_answer(agent_ep
assert agent_episode.calls == ["POST", "GET", "GET", "GET", "terminate", "terminate"]
assert agent_episode.clock.now == 4
assert summary["artifact_bundle"]["status"] == "ready"
# The scoped presentation is passed intact, including explicit unknown.
# Do not rebuild it from the own-scope amount or finality beside it.
assert summary["cost_presentation"] == carrier
for key in adapter.COST_RESULT_FIELDS:
if key in final:
assert summary[key] == final[key]

View file

@ -683,3 +683,14 @@ def test_quiz_option_recommendation_is_an_additive_optional_field_in_both_langua
)
option_decl = re.search(r"@typedef \{Object\} QuizOption\b([\s\S]*?)\*/", text)
assert option_decl and "@property {boolean=} recommended" in option_decl.group(1)
def test_cost_presentation_has_a_closed_nullable_wire_shape():
from typing import get_args, get_type_hints
from ouroboros.cost_projection import CostPresentation
hints = get_type_hints(CostPresentation)
assert set(hints) == {'scope', 'tracked_amount', 'has_unpriced', 'tracked_final', 'accounting_open', 'has_rows'}
assert set(get_args(hints['scope'])) == {'own', 'root_tree'}
assert _contains_none(hints['tracked_amount'])
assert _contains_none(get_type_hints(ChatOutbound, include_extras=True)['cost_presentation'])

View file

@ -11,7 +11,7 @@ class TestStampRootFinalPhase:
evt = {"type": "send_message"}
stamp_root_final_phase(evt, {"_is_direct_chat": True}, post_task_open=True, terminal_status="completed")
assert evt["progress_meta"] == {"task_phase": "finalizing"}
assert evt["progress_meta"] == {"task_phase": "finalizing", "task_terminal_status": "completed"}
def test_settled_direct_bare_final_is_typed_terminal(self):
from ouroboros.task_finalization import stamp_root_final_phase

View file

@ -36,9 +36,9 @@ def _terminal(tmp_path, *, current, task_id="parent1"):
text, usage, trace = loop._handle_provider_unavailable(ctx, error_kind="provider_outcome_unknown",
wait_cause="transport_unavailable", waited_sec=125.0)
assert text == RAW
assert "waited and redialed for 2.1 min" in usage["terminal_provider_notice"]
assert "no terminal provider outcome" in usage["terminal_provider_notice"]
assert "no further retry or paid fallback was sent" in usage["terminal_provider_notice"]
assert "spent 2.1 min in the provider wait" in usage["terminal_provider_notice"]
assert "no confirmed provider outcome" in usage["terminal_provider_notice"]
assert "waited and redialed" not in usage["terminal_provider_notice"]
return text, usage, trace

View file

@ -0,0 +1,214 @@
"""#869 — what the owner is told when a dispatched attempt has NO outcome.
The rail already refuses to resend (``provider_no_call_source``), and it already
records ``infra_failed`` / ``provider_unavailable``. Two things were still wrong
in what the OWNER read:
* the durable source asked only for the round's repeat RECORD, while the owner
sentence asked the wider question (record OR the sticky
``provider_outcome_unknown`` kind) — so an attempt interrupted in flight was
described as unknown and stamped ``transport_unavailable_no_resend``;
* the terminal never said what the unresolved attempt COST, and on the no-call
rails it never said how long the turn had actually waited.
This is presentation and provenance only. The differential guard below is the
contract: no additional model call, unchanged metadata, unchanged salvage.
"""
from __future__ import annotations
import pytest
import ouroboros.loop as loop_mod
import ouroboros.loop_transport as loop_transport
from ouroboros.loop_llm_call import TRANSPORT_DEATHS_KEY
class _NoCallLLM:
"""Any touch is a model call the unknown-outcome terminal must not make."""
def __getattr__(self, name): # pragma: no cover - the failure IS the point
pytest.fail(f"the unknown-outcome terminal reached the provider: .{name}")
def _ctx(tmp_path, accumulated, messages=None):
return loop_mod._RoundLimitContext(
messages=messages or [{"role": "user", "content": "go"}],
llm=_NoCallLLM(), active_model="test-model", active_effort="low", max_retries=3,
drive_logs=tmp_path, task_id="t-unknown", round_idx=1, event_queue=None,
accumulated_usage=accumulated, task_type="task", active_use_local=False,
max_rounds=200, drive_root=tmp_path,
)
def _text(accumulated, **over):
kwargs = {
"is_context_overflow": False, "is_transport_wait": True, "waited_sec": 0.0,
"interactive": False, "is_deadline_exhausted": False,
}
kwargs.update(over)
return loop_transport.provider_terminal_fallback_text(accumulated, **kwargs)
class TestTheSourcePredicate:
def test_an_unknown_outcome_without_a_repeat_record_keeps_the_unknown_source(self, tmp_path):
accumulated = {"_last_llm_error_kind": "provider_outcome_unknown",
"execution_status": "infra_failed", "reason_code": "provider_unavailable"}
text, usage, trace = loop_mod._handle_provider_unavailable(
_ctx(tmp_path, accumulated), error_kind="provider_outcome_unknown",
wait_cause="transport_unavailable", waited_sec=300.0,
)
assert TRANSPORT_DEATHS_KEY not in usage
assert trace["forced_finalization"]["source"] == "provider_outcome_unknown_no_resend"
# Metadata is the rail's, unchanged by the wording fix.
assert usage["execution_status"] == "infra_failed"
assert usage["reason_code"] == "provider_unavailable"
# The owner reads the no-resend fence AND the money fact that follows it.
assert "no retry or paid fallback was sent" in text
assert text.endswith(loop_transport.UNKNOWN_ATTEMPT_COST_NOTE)
def test_a_plain_connection_failure_still_takes_the_transport_source(self, tmp_path):
accumulated = {"_last_llm_error_kind": "transport_unavailable",
"execution_status": "infra_failed", "reason_code": "provider_unavailable"}
text, usage, trace = loop_mod._handle_provider_unavailable(
_ctx(tmp_path, accumulated), error_kind="provider_unavailable",
wait_cause="transport_unavailable", waited_sec=300.0,
)
assert trace["forced_finalization"]["source"] == "transport_unavailable_no_resend"
assert loop_transport.UNKNOWN_ATTEMPT_COST_NOTE not in text
assert "Retry when connectivity returns" in text
def test_the_repeat_record_path_is_unchanged(self, tmp_path):
accumulated = {
TRANSPORT_DEATHS_KEY: {"round_id": "r", "count": 1, "backoff_sec": 4.0,
"error_kind": "transport_unavailable"},
"execution_status": "infra_failed", "reason_code": "llm_api_error",
}
text, usage, trace = loop_mod._handle_provider_unavailable(
_ctx(tmp_path, accumulated), error_kind="context_overflow",
wait_cause="transport_unavailable", waited_sec=610.0,
)
assert trace["forced_finalization"]["source"] == "provider_outcome_unknown_no_resend"
assert usage["reason_code"] == "provider_unavailable"
assert "1 earlier physical attempt(s) of the last dispatched round" in text
assert "context exceeded" not in text
class TestWhatTheOwnerReads:
def test_the_unknown_wait_states_the_fence_the_outcome_and_the_cost(self):
unknown = {"_last_llm_error_kind": "provider_outcome_unknown"}
text = _text(unknown, waited_sec=600.0)
assert "provider wait" in text and "10.0 min" in text
assert "no terminal provider outcome" in text
assert "no retry or paid fallback was sent" in text
assert "does not establish the attempt's cost" in text and "not a settled receipt" in text
assert "redialed" not in text
def test_the_no_call_rails_say_the_real_wait_instead_of_implying_none(self):
unknown = {"_last_llm_error_kind": "provider_outcome_unknown"}
# The generic terminal (the `provider_outcome_unknown_no_resend` no-call
# rail) used to name neither the wait nor the price.
generic = _text(unknown, is_transport_wait=False, waited_sec=600.0)
assert "The task spent 10.0 min in the provider wait" in generic
assert generic.endswith("Any files written so far are preserved in the workspace.")
assert loop_transport.UNKNOWN_ATTEMPT_COST_NOTE.strip() in generic
# An interactive turn speaks for the turn, not the task.
assert "This turn spent 10.0 min in the provider wait" in _text(
unknown, is_transport_wait=False, waited_sec=600.0, interactive=True)
# A zero wait claims none.
assert "waited" not in _text(unknown, is_transport_wait=False, waited_sec=0.0)
def test_a_known_outcome_gains_no_cost_or_wait_clause(self):
known = {"_last_llm_error_kind": "provider_incomplete_response"}
text = _text(known, is_transport_wait=False, waited_sec=600.0)
assert "waited" not in text
assert loop_transport.UNKNOWN_ATTEMPT_COST_NOTE not in text
def test_the_deadline_terminal_carries_the_same_two_facts_when_unknown(self):
unknown = {"_last_llm_error_kind": "provider_outcome_unknown"}
text = _text(unknown, is_transport_wait=False, is_deadline_exhausted=True)
assert "owner deadline ended primary model work" in text
assert text.endswith(loop_transport.UNKNOWN_ATTEMPT_COST_NOTE)
class TestSalvageAndBoundaries:
def test_a_salvaged_answer_still_wins_over_the_host_sentence(self, tmp_path):
accumulated = {"_last_llm_error_kind": "provider_outcome_unknown",
"execution_status": "infra_failed", "reason_code": "provider_unavailable"}
messages = [{"role": "user", "content": "go"},
{"role": "assistant", "content": "Partial work is on disk."}]
text, _usage, trace = loop_mod._handle_provider_unavailable(
_ctx(tmp_path, accumulated, messages), error_kind="provider_outcome_unknown",
wait_cause="transport_unavailable", waited_sec=300.0,
)
assert "Partial work is on disk." in text
assert trace["forced_finalization"]["source"] == "provider_outcome_unknown_no_resend"
def test_the_no_call_and_wait_predicates_themselves_are_untouched(self):
from ouroboros.loop_llm_call import provider_no_call_source
assert provider_no_call_source(
{"_last_llm_error_kind": "provider_outcome_unknown"}, False,
) == ("provider_outcome_unknown_no_resend", False)
assert provider_no_call_source({}, False) == ("", False)
# The rail's durable source now asks the SAME question this predicate and
# the owner sentence ask: a record OR the sticky unknown kind. One
# question, one answer, on all three surfaces.
for usage in ({"_last_llm_error_kind": "provider_outcome_unknown"},
{TRANSPORT_DEATHS_KEY: {"count": 1}},
{TRANSPORT_DEATHS_KEY: {"count": 1},
"_last_llm_error_kind": "context_overflow"}):
assert provider_no_call_source(usage, False)[0] == "provider_outcome_unknown_no_resend"
@pytest.mark.parametrize('control,action', [
('owner_requested_finalization', 'Wrap up'), ('owner_stopped_direct_turn', 'Stop'),
])
def test_stop_and_wrapup_text_are_byte_preserved(control, action):
from ouroboros.outcomes import REASON_OWNER_REQUESTED_FINALIZATION
from supervisor.owner_stop import REASON_OWNER_STOPPED_DIRECT_TURN
reason = REASON_OWNER_REQUESTED_FINALIZATION if action == 'Wrap up' else REASON_OWNER_STOPPED_DIRECT_TURN
usage = {'_last_llm_error_kind': 'provider_outcome_unknown'}
expected = (
f'⚠️ The owner requested {action} while the provider connection was unavailable.'
' The wait ended after 5.0 min; No new summary request was sent. Any files written so far are preserved.'
' The dispatched request has no terminal provider outcome, so no '
'retry or paid fallback was sent; either could duplicate live work.'
)
assert _text(usage, control_reason=reason, waited_sec=300).encode() == expected.encode()
@pytest.mark.parametrize('wait', [False, True])
@pytest.mark.parametrize('salvage', ['', 'Saved **exact** bytes.\n\n```text\nα < β\n```'])
def test_terminal_wording_is_differentially_call_free_and_preserves_salvage(tmp_path, monkeypatch, wait, salvage):
import copy
original = loop_mod._provider_terminal_fallback_text
accumulated = {'_last_llm_error_kind': 'provider_outcome_unknown',
'execution_status': 'infra_failed', 'reason_code': 'provider_unavailable'}
messages = [{'role': 'user', 'content': 'go'}]
if salvage:
messages.append({'role': 'assistant', 'content': salvage})
def run():
return loop_mod._handle_provider_unavailable(
_ctx(tmp_path, copy.deepcopy(accumulated), copy.deepcopy(messages)),
error_kind='provider_outcome_unknown',
wait_cause='transport_unavailable' if wait else '', waited_sec=300,
)
# Replace ONLY the presentation seam for the reference run. Both exercise
# the actual rail and the fail-on-any-provider-access sentinel above.
monkeypatch.setattr(loop_mod, '_provider_terminal_fallback_text', lambda *_a, **_kw: 'reference terminal notice')
before_text, before_usage, before_trace = run()
monkeypatch.setattr(loop_mod, '_provider_terminal_fallback_text', original)
after_text, after_usage, after_trace = run()
before_usage.pop('terminal_provider_notice', None)
after_usage.pop('terminal_provider_notice', None)
assert before_usage == after_usage
assert before_trace['forced_finalization']['source'] == after_trace['forced_finalization']['source']
if salvage:
assert before_text.encode() == after_text.encode() == salvage.encode()
else:
assert before_text == 'reference terminal notice'
assert after_text != before_text and 'redialed' not in after_text

View file

@ -64,7 +64,12 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
# 106400 -> 106600 (#1195 merge of 32d8dfc6): the base's settings_catalog.js
# paragraph (#1214, +319 bytes) landed in the same window; both additions stand,
# neither displaces the other's text.
"docs/architecture/03-web-ui-pages-and-buttons.md": 106600,
# 106600 -> 107000 (truthful-cards batch): the chapter gains one new owner
# paragraph (`terminal_projection.py`, #1154) and the checkpoint/cancellation
# vocabulary (#931/#1061); the touched descriptions were REPLACED and
# compressed (net chapter growth is under the added owner's paragraph size),
# and the merged #1236 base already sat 5 bytes under the previous budget.
"docs/architecture/03-web-ui-pages-and-buttons.md": 107000,
"docs/architecture/04-server-api-endpoints.md": 26833,
# 27137 -> 30400: the schedule table gains a documented write contract the
# chapter had no text for — one transaction owning the lock ORDER, the strict

View file

@ -160,17 +160,30 @@ def test_host_terminal_without_child_terminal_does_not_disable_retention(
write_task_result(child, task_id, child_status, result="unfinished work")
stored = write_task_result(root, task_id, status, result="Host ended this execution",
child_drive_root=str(child))
# Repeated boots do not turn confirmed absence of a child terminal into
# a permanent save obligation that blocks unrelated startup retention.
for _ in range(2):
report = _recovery(root, repo)
assert report["unresolved"] == report["errors"] == report["protected"] == []
assert load_task_result(root, task_id, strict=True) == stored
# First boot may owe/publish the new host terminal's presentation, but
# must preserve every execution fact and never acquire a retention hold.
report = _recovery(root, repo)
assert report["unresolved"] == report["errors"] == report["protected"] == []
actual = load_task_result(root, task_id, strict=True)
bookkeeping = {"canonical_terminal_projection", "canonical_terminal_projection_ready", "updated_at"}
assert {k: v for k, v in actual.items() if k not in bookkeeping} == {
k: v for k, v in stored.items() if k not in bookkeeping}
if status == "cancelled":
assert actual["canonical_terminal_projection"]["summary_id"] == f"task-terminal:{task_id}"
assert actual["canonical_terminal_projection_ready"] is None
else:
assert actual["canonical_terminal_projection_ready"]["token"]
assert not actual.get("canonical_terminal_projection")
result_path = root / "task_results" / f"{task_id}.json"
settled_bytes = result_path.read_bytes()
report = _recovery(root, repo)
assert report["unresolved"] == report["errors"] == report["protected"] == []
assert result_path.read_bytes() == settled_bytes
monkeypatch.setattr("ouroboros.retention.age_cutoff", lambda *a, **k: 4_000_000_000)
maintenance._startup_prune_sweeps(preserve_task_sources=bool(
report["unresolved"] or report["protected"] or report["errors"]))
assert not child.exists()
assert load_task_result(root, task_id, strict=True) == stored
assert result_path.read_bytes() == settled_bytes
def test_no_provider_unrestored_wait_is_preserved_but_other_saved_work_recovers(roots, monkeypatch):
@ -221,7 +234,7 @@ def test_failed_first_save_preserves_child_and_followup_attachment_through_prune
assert recovered["recovered"] == ["saved"]
assert load_task_result(root, "saved")["child_ref_promotion"]["pending_refs"] == []
for row in manifest:
assert (task_artifact_dir_path(root, "saved") / row["relpath"]).read_text() == "accepted follow-up file"
assert (task_artifact_dir_path(root, "saved") / row["relpath"]).read_text(encoding="utf-8") == "accepted follow-up file"
@pytest.mark.parametrize("pids", [None, {777}])

View file

@ -11,6 +11,8 @@ Pins the "ghost subagent" / status-corruption protections:
- normal forward progress and same-status enrichment are unaffected
"""
import json
import pytest
from ouroboros import task_results as tr
@ -57,6 +59,71 @@ def test_same_terminal_status_enrichment_allowed(drive):
assert data["trace_summary"] == "trace"
@pytest.mark.parametrize("previous", [None, tr.STATUS_RUNNING])
@pytest.mark.parametrize("terminal", [
tr.STATUS_COMPLETED, tr.STATUS_FAILED, tr.STATUS_CANCELLED, tr.STATUS_REJECTED_DUPLICATE,
])
def test_first_accepted_root_terminal_write_originates_projection_debt(drive, previous, terminal):
if previous is not None:
tr.write_task_result(drive, "root", previous)
stored = tr.write_task_result(drive, "root", terminal)
assert stored["canonical_terminal_projection_origin"] == "terminal_transition"
assert "canonical_terminal_projection_ready" not in stored
assert tr.load_task_result(drive, "root", strict=True) == stored
enriched = tr.write_task_result(drive, "root", terminal,
canonical_terminal_projection_origin=None, result="enriched")
assert enriched["canonical_terminal_projection_origin"] == "terminal_transition"
@pytest.mark.parametrize("lineage", [
{"parent_task_id": "parent", "root_task_id": "parent", "delegation_role": "subagent"},
{"metadata": {"parent_task_id": "parent", "root_task_id": "parent", "delegation_role": "subagent"}},
{"root_task_id": "other"},
])
def test_child_or_unproven_lineage_cannot_originate_projection_debt(drive, lineage):
tr.write_task_result(drive, "child", tr.STATUS_RUNNING, **lineage)
stored = tr.write_task_result(drive, "child", tr.STATUS_COMPLETED,
canonical_terminal_projection_origin="terminal_transition")
assert "canonical_terminal_projection_origin" not in stored
def test_root_retry_transition_uses_merged_lineage(drive):
tr.write_task_result(drive, "retry", tr.STATUS_RUNNING, root_task_id="logical-root",
parent_task_id="", delegation_role="root", original_task_id="previous",
timeout_retry_from="previous")
stored = tr.write_task_result(drive, "retry", tr.STATUS_COMPLETED)
assert stored["canonical_terminal_projection_origin"] == "terminal_transition"
@pytest.mark.parametrize("projector", ["plain", "replica", "enrichment"])
def test_terminal_enrichment_cannot_adopt_a_historical_result(drive, projector):
from ouroboros.post_task_checkpoint import project_replica_task_result_fields
path = tr.task_result_path(drive, "historical")
path.write_text(json.dumps({"_schema_version": tr.TASK_RESULT_SCHEMA_VERSION,
"task_id": "historical", "status": tr.STATUS_COMPLETED}), encoding="utf-8")
def enrich(_current, fields):
return {**fields, "canonical_terminal_projection_origin": "terminal_transition"}
reducer = {"plain": None, "replica": project_replica_task_result_fields, "enrichment": enrich}[projector]
stored = tr.write_task_result(drive, "historical", tr.STATUS_COMPLETED, result="refreshed",
canonical_terminal_projection_origin="terminal_transition",
_field_projector=reducer)
assert stored["result"] == "refreshed"
assert "canonical_terminal_projection_origin" not in stored
# A rejected lifecycle write cannot create provenance either.
assert tr.write_task_result(drive, "historical", tr.STATUS_FAILED,
canonical_terminal_projection_origin="terminal_transition") == stored
def test_replica_cannot_supply_terminal_origin_to_an_effective_read():
from ouroboros.post_task_checkpoint import project_replica_task_result_fields
replica = {"canonical_terminal_projection_origin": "terminal_transition", "result": "worker"}
assert project_replica_task_result_fields({}, replica) == {"result": "worker"}
def test_replica_projector_keeps_terminal_lifecycle_guard(drive):
from ouroboros.post_task_checkpoint import project_replica_task_result_fields

View file

@ -0,0 +1,553 @@
"""#1154 — a root's Project row and Main's single mirror are ONE owed unit.
Before this, ``task_done`` appended the canonical Project row (or deferred it
behind an open post-task synthesis) and, INDEPENDENTLY, enqueued Main's mirror
right away. So Main heard about a run that had not finished finalizing, and a
crash between the two effects lost whichever half had not happened yet.
The obligation is now a durable row (``canonical_terminal_projection_ready``)
persisted before the effects and cleared only when Main is durably OWED or
positively INELIGIBLE. One continuation runs both halves outside the result
lock, ``task_done`` and the post-task callback share it, and existing startup
and off-loop maintenance passes retry it without cognition or a new timer/store.
The disclosed limits stay: the outbox is bounded and external delivery is
at-least-once — this is not exactly-once.
"""
from __future__ import annotations
import json
import pathlib
from types import SimpleNamespace
import pytest
from ouroboros.post_task_checkpoint import (
SETTLEMENT_DEFERRED,
SETTLEMENT_NONE,
SETTLEMENT_SETTLED,
settle_terminal_projection,
)
from ouroboros.project_dialogue import append_terminal_task_projection
from ouroboros.task_results import STATUS_COMPLETED, load_task_result, write_task_result
DONE = {"chat_id": 1, "status": "completed", "outcome_axes": {"execution": {"status": "ok"}}}
def _chat_rows(root: pathlib.Path):
path = root / "logs" / "chat.jsonl"
if not path.is_file():
return []
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
def _project_rows(root: pathlib.Path, task_id: str):
return [row for row in _chat_rows(root)
if row.get("task_id") == task_id and row.get("type") == "task_summary"]
@pytest.fixture()
def project_root(tmp_path, monkeypatch):
"""A registered project whose room actually holds the run's work."""
from ouroboros.projects_registry import bind_task_to_project, create_project
project = create_project(tmp_path, "launch", name="Launch")
bind_task_to_project(tmp_path, "root-1", project["id"], project["chat_id"],
origin={"absent": "system"})
queued: list[dict] = []
monkeypatch.setattr("supervisor.terminal_delivery.enqueue_terminal_delivery",
lambda _root, event, **_kw: queued.append(dict(event)) or True)
task = {"id": "root-1", "project_id": project["id"], "title": "Ship release",
"chat_id": project["chat_id"], "root_task_id": "root-1"}
return SimpleNamespace(root=tmp_path, project=project, task=task, queued=queued)
def _store(root, task_id="root-1", *, project_id="launch", phase=None, **over):
fields = {"root_task_id": task_id, "project_id": project_id,
"result": "Owner already has the answer", **over}
if phase is not None:
fields["root_phase_checkpoint"] = {"post_task_synthesis": phase}
return write_task_result(root, task_id, STATUS_COMPLETED, **fields)
class TestOwedBeforeEffects:
def test_an_open_post_task_defers_both_halves_and_keeps_the_obligation(self, project_root):
stored = _store(project_root.root, phase="running")
assert not append_terminal_task_projection(
project_root.root, "root-1", project_root.task, stored, DONE)
owed = load_task_result(project_root.root, "root-1")["canonical_terminal_projection_ready"]
assert owed["summary_id"] == "task-terminal:root-1"
assert _project_rows(project_root.root, "root-1") == []
# The continuation refuses to run either half while the synthesis is open:
# the early answer stays in the Project thread, and Main hears nothing.
assert settle_terminal_projection(
project_root.root, "root-1", task=project_root.task) == SETTLEMENT_DEFERRED
assert project_root.queued == []
assert _project_rows(project_root.root, "root-1") == []
def test_a_settled_root_appends_the_row_and_still_owes_main(self, project_root):
stored = _store(project_root.root)
assert append_terminal_task_projection(
project_root.root, "root-1", project_root.task, stored, DONE)
record = load_task_result(project_root.root, "root-1")
# BOTH: the Project receipt AND the obligation the mirror still stands on.
assert record["canonical_terminal_projection"]["summary_id"] == "task-terminal:root-1"
assert record["canonical_terminal_projection_ready"]["summary_id"] == "task-terminal:root-1"
assert len(_project_rows(project_root.root, "root-1")) == 1
assert project_root.queued == []
def test_a_child_owes_main_nothing_and_carries_no_obligation(self, tmp_path):
child = {"id": "child-1", "parent_task_id": "root-1", "root_task_id": "root-1",
"delegation_role": "subagent", "chat_id": 41}
stored = write_task_result(tmp_path, "child-1", STATUS_COMPLETED,
parent_task_id="root-1", root_task_id="root-1",
delegation_role="subagent")
assert append_terminal_task_projection(tmp_path, "child-1", child, stored,
{"chat_id": 41, "status": "completed"})
record = load_task_result(tmp_path, "child-1")
assert "canonical_terminal_projection_ready" not in record
assert settle_terminal_projection(tmp_path, "child-1") == SETTLEMENT_NONE
class TestSettlementClosesTheUnit:
def test_the_post_task_callback_runs_both_halves_exactly_once(self, project_root):
_store(project_root.root, phase="running")
stored = load_task_result(project_root.root, "root-1")
append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
_store(project_root.root, phase="completed")
assert settle_terminal_projection(
project_root.root, "root-1", task=project_root.task) == SETTLEMENT_SETTLED
assert len(_project_rows(project_root.root, "root-1")) == 1
assert len(project_root.queued) == 1
assert project_root.queued[0]["chat_id"] == 1
assert project_root.queued[0]["delivery_id"] == "project-completion:root-1"
record = load_task_result(project_root.root, "root-1")
assert record["canonical_terminal_projection_ready"] is None
# Repeating the continuation is a no-op: nothing owed, nothing appended.
assert settle_terminal_projection(
project_root.root, "root-1", task=project_root.task) == SETTLEMENT_NONE
assert len(_project_rows(project_root.root, "root-1")) == 1
assert len(project_root.queued) == 1
def test_a_duplicate_settlement_reuses_the_one_delivery_id(self, project_root):
_store(project_root.root)
stored = load_task_result(project_root.root, "root-1")
append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
for _ in range(3):
settle_terminal_projection(project_root.root, "root-1", task=project_root.task)
assert len(_project_rows(project_root.root, "root-1")) == 1
assert {row["delivery_id"] for row in project_root.queued} == {"project-completion:root-1"}
def test_a_positively_ineligible_root_clears_the_obligation_without_sending(self, tmp_path, monkeypatch):
queued: list[dict] = []
monkeypatch.setattr("supervisor.terminal_delivery.enqueue_terminal_delivery",
lambda _root, event, **_kw: queued.append(dict(event)) or True)
# A workspace-derived project id has no registry row, so there is no room
# to open and Main is owed NOTHING — a positive answer, not an unknown.
stored = write_task_result(tmp_path, "root-2", STATUS_COMPLETED,
root_task_id="root-2", project_id="proj_deadbeef1234")
task = {"id": "root-2", "project_id": "proj_deadbeef1234", "chat_id": 7}
append_terminal_task_projection(tmp_path, "root-2", task, stored,
{"chat_id": 7, "status": "completed"})
assert settle_terminal_projection(tmp_path, "root-2", task=task) == SETTLEMENT_SETTLED
assert queued == []
assert load_task_result(tmp_path, "root-2")["canonical_terminal_projection_ready"] is None
class TestPartialWritesAndRetry:
def test_a_registration_that_is_not_durable_keeps_the_obligation_owed(self, project_root, monkeypatch):
_store(project_root.root)
stored = load_task_result(project_root.root, "root-1")
append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
# The live send may still go out; what is NOT durable is the owed row, so
# a crash before delivery would lose it. UNKNOWN, never a cleared debt.
monkeypatch.setattr("supervisor.terminal_delivery.register_pending_delivery",
lambda *_a, **_kw: False)
assert settle_terminal_projection(
project_root.root, "root-1", task=project_root.task) == SETTLEMENT_DEFERRED
record = load_task_result(project_root.root, "root-1")
assert record["canonical_terminal_projection_ready"]["summary_id"] == "task-terminal:root-1"
# The next pass heals: one Project row, one delivery id, obligation closed.
monkeypatch.undo()
monkeypatch.setattr("supervisor.terminal_delivery.enqueue_terminal_delivery",
lambda _root, event, **_kw: project_root.queued.append(dict(event)) or True)
assert settle_terminal_projection(
project_root.root, "root-1", task=project_root.task) == SETTLEMENT_SETTLED
assert len(_project_rows(project_root.root, "root-1")) == 1
assert load_task_result(project_root.root, "root-1")["canonical_terminal_projection_ready"] is None
def test_an_unreadable_eligibility_answer_is_unknown_not_ineligible(self, project_root, monkeypatch):
_store(project_root.root)
stored = load_task_result(project_root.root, "root-1")
append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
def _boom(*_a, **_kw):
raise OSError("registry unreadable")
monkeypatch.setattr("ouroboros.projects_registry.task_presentation_snapshot", _boom)
assert settle_terminal_projection(
project_root.root, "root-1", task=project_root.task) == SETTLEMENT_DEFERRED
assert load_task_result(
project_root.root, "root-1")["canonical_terminal_projection_ready"] is not None
def test_clearing_is_a_compare_and_swap_against_the_current_marker_and_phase(self, project_root):
from ouroboros.post_task_checkpoint import _clear_terminal_projection_obligation
_store(project_root.root)
stored = load_task_result(project_root.root, "root-1")
append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
# A stale summary id belongs to a different terminal and clears nothing.
expected = load_task_result(project_root.root, "root-1")
stale = {**expected, "canonical_terminal_projection_ready": {"summary_id": "task-terminal:other"}}
_clear_terminal_projection_obligation(project_root.root, "root-1", stale, "owed")
assert load_task_result(
project_root.root, "root-1")["canonical_terminal_projection_ready"] is not None
# A post-task pass that re-opened also blocks the clear.
_store(project_root.root, phase="running")
_clear_terminal_projection_obligation(project_root.root, "root-1", expected, "owed")
assert load_task_result(
project_root.root, "root-1")["canonical_terminal_projection_ready"] is not None
# Matching marker and settled phase: the obligation closes.
_store(project_root.root, phase="completed")
expected = load_task_result(project_root.root, "root-1")
_clear_terminal_projection_obligation(project_root.root, "root-1", expected, "owed")
assert load_task_result(
project_root.root, "root-1")["canonical_terminal_projection_ready"] is None
def test_readiness_is_on_disk_before_project_append_and_no_result_lock_is_held(self, project_root, monkeypatch):
from ouroboros import project_dialogue as dialogue
stored = _store(project_root.root)
original = dialogue.append_canonical_task_summary
def append(root, row):
current = load_task_result(root, "root-1")
assert current["canonical_terminal_projection_ready"]["token"] == row["terminal_projection_token"]
# A real reentrant result write would time out if append held its lock.
write_task_result(root, "root-1", "completed", reentrant_probe=True)
return original(root, row)
monkeypatch.setattr(dialogue, "append_canonical_task_summary", append)
assert append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
def test_initial_marker_write_crash_is_discovered_without_restart(self, project_root, monkeypatch):
from ouroboros import terminal_projection as projection
_store(project_root.root, chat_id=project_root.project["chat_id"])
original = projection.write_task_result
monkeypatch.setattr(projection, "write_task_result", lambda *_a, **_kw: (_ for _ in ()).throw(OSError("crash")))
assert settle_terminal_projection(project_root.root, "root-1") == SETTLEMENT_DEFERRED
stored = load_task_result(project_root.root, "root-1", strict=True)
assert "canonical_terminal_projection_ready" not in stored
assert stored["canonical_terminal_projection_origin"] == "terminal_transition"
monkeypatch.setattr(projection, "write_task_result", original)
assert projection.reconcile_terminal_projections(project_root.root) == 1
assert len(_project_rows(project_root.root, "root-1")) == len(project_root.queued) == 1
def test_append_receipt_crash_dedupes_project_by_the_durable_token(self, project_root, monkeypatch):
from ouroboros import terminal_projection as projection
from ouroboros import project_dialogue as dialogue
_store(project_root.root)
original = dialogue.append_canonical_task_summary
def append_then_crash(root, row):
assert original(root, row)
raise OSError("process ended after append, before result receipt")
monkeypatch.setattr(dialogue, "append_canonical_task_summary", append_then_crash)
assert settle_terminal_projection(project_root.root, "root-1", task=project_root.task) == SETTLEMENT_DEFERRED
monkeypatch.setattr(dialogue, "append_canonical_task_summary", original)
assert projection.reconcile_terminal_projections(project_root.root) == 1
assert len(_project_rows(project_root.root, "root-1")) == 1
def test_queue_failure_after_registration_retires_readiness_to_durable_disposition(self, project_root, monkeypatch):
from supervisor.terminal_delivery import pending_deliveries
from ouroboros.terminal_projection import reconcile_terminal_projections
_store(project_root.root)
monkeypatch.setattr("supervisor.terminal_delivery.enqueue_terminal_delivery",
lambda *_a, **_kw: (_ for _ in ()).throw(OSError("queue unavailable")))
assert settle_terminal_projection(project_root.root, "root-1", task=project_root.task) == SETTLEMENT_SETTLED
assert [r["delivery_id"] for r in pending_deliveries(project_root.root)] == ["project-completion:root-1"]
current = load_task_result(project_root.root, "root-1")
assert current["canonical_terminal_projection"]["main_disposition"] == "owed"
assert current["canonical_terminal_projection_ready"] is None
# Even once the bounded registry has forgotten it, historical scanning
# must never register another mirror for this terminal result.
(project_root.root / "state" / "terminal_deliveries.json").write_text('{"delivered": [], "pending": {}}')
assert reconcile_terminal_projections(project_root.root) == 0
def test_stale_retirement_cannot_clear_new_token_or_attempt(self, project_root):
from ouroboros.terminal_projection import clear_terminal_projection_obligation
_store(project_root.root)
append_terminal_task_projection(project_root.root, "root-1", project_root.task,
load_task_result(project_root.root, "root-1"), DONE)
expected = load_task_result(project_root.root, "root-1")
newer = {**expected["canonical_terminal_projection_ready"], "token": "new-attempt"}
write_task_result(project_root.root, "root-1", "completed", task_attempt=2,
canonical_terminal_projection_ready=newer)
assert not clear_terminal_projection_obligation(project_root.root, "root-1", expected, "owed")
assert load_task_result(project_root.root, "root-1")["canonical_terminal_projection_ready"] == newer
@pytest.mark.parametrize("filename", ["projects.json", "task_project_bindings.json"])
def test_corrupt_membership_is_unknown_not_ineligible(self, project_root, filename):
from ouroboros import projects_registry as registry
_store(project_root.root, _is_direct_chat=True)
path = registry._registry_path(project_root.root) if filename == "projects.json" else registry._bindings_path(project_root.root)
# For the registry case test ordinary task membership; direct turns read
# their binding first and are positively native in this fixture.
if filename == "projects.json":
_store(project_root.root, _is_direct_chat=False)
path.write_text("{unreadable", encoding="utf-8")
assert settle_terminal_projection(project_root.root, "root-1", task=project_root.task) == SETTLEMENT_DEFERRED
assert load_task_result(project_root.root, "root-1")["canonical_terminal_projection_ready"]
def test_concurrent_callbacks_append_and_register_one_projection(self, project_root):
from concurrent.futures import ThreadPoolExecutor
_store(project_root.root)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(lambda _: settle_terminal_projection(
project_root.root, "root-1", task=project_root.task), range(2)))
assert SETTLEMENT_SETTLED in results
assert len(_project_rows(project_root.root, "root-1")) == len(project_root.queued) == 1
class TestRestartRecovery:
def test_the_existing_scan_retries_an_owed_terminal_row_without_paid_cognition(
self, project_root, monkeypatch,
):
from ouroboros import agent_task_pipeline as pipeline
_store(project_root.root, phase="completed")
stored = load_task_result(project_root.root, "root-1")
append_terminal_task_projection(project_root.root, "root-1", project_root.task, stored, DONE)
# Simulate a crash between the Project row and Main: the obligation is
# still on disk, the mirror never went out.
assert load_task_result(
project_root.root, "root-1")["canonical_terminal_projection_ready"] is not None
assert project_root.queued == []
monkeypatch.setattr(
pipeline, "_run_post_task_processing_async",
lambda *_a, **_kw: pytest.fail("restart settlement must not buy cognition"))
# The scan counts RECOVERED SYNTHESES; a settlement is bookkeeping, not one.
assert pipeline.recover_pending_root_post_task_synthesis(
project_root.root, repo_dir=project_root.root / "repo") == 0
assert len(project_root.queued) == 1
assert len(_project_rows(project_root.root, "root-1")) == 1
assert load_task_result(
project_root.root, "root-1")["canonical_terminal_projection_ready"] is None
# Idempotent across restarts.
assert pipeline.recover_pending_root_post_task_synthesis(
project_root.root, repo_dir=project_root.root / "repo") == 0
assert len(project_root.queued) == 1
def test_a_split_replica_can_neither_resurrect_nor_replace_the_canonical_facts(self):
from ouroboros.post_task_checkpoint import project_replica_task_result_fields
canonical = {
"canonical_terminal_projection": {"summary_id": "task-terminal:root-1"},
"canonical_terminal_projection_ready": None,
}
replica = {
"canonical_terminal_projection": {"summary_id": "task-terminal:imposter"},
"canonical_terminal_projection_ready": {"summary_id": "task-terminal:root-1"},
"result": "worker copy",
}
overlay = project_replica_task_result_fields(canonical, replica)
assert "canonical_terminal_projection" not in overlay
assert "canonical_terminal_projection_ready" not in overlay
assert overlay["result"] == "worker copy"
class TestNoLeakage:
def test_a_project_native_direct_turn_is_never_mirrored_into_main(self, project_root):
from ouroboros.project_dialogue import (
MAIN_MIRROR_INELIGIBLE, project_completion_delivery_outcome,
)
stored = _store(project_root.root, _is_direct_chat=True)
assert project_completion_delivery_outcome(
project_root.root, {"_is_direct_chat": True}, "root-1",
project_root.task, stored, DONE) == (MAIN_MIRROR_INELIGIBLE, False)
assert project_root.queued == []
def test_an_unbound_run_whose_room_was_registered_late_is_ineligible(self, tmp_path, monkeypatch):
from ouroboros.project_dialogue import (
MAIN_MIRROR_INELIGIBLE, project_completion_delivery_outcome,
)
from ouroboros.projects_registry import create_project
queued: list[dict] = []
monkeypatch.setattr("supervisor.terminal_delivery.enqueue_terminal_delivery",
lambda _root, event, **_kw: queued.append(dict(event)) or True)
create_project(tmp_path, "late", name="Late")
task = {"id": "root-3", "project_id": "late", "chat_id": 99}
stored = write_task_result(tmp_path, "root-3", STATUS_COMPLETED,
root_task_id="root-3", project_id="late")
assert project_completion_delivery_outcome(
tmp_path, {}, "root-3", task, stored,
{"status": "completed"}) == (MAIN_MIRROR_INELIGIBLE, False)
assert queued == []
def test_a_child_never_reaches_the_main_mirror_seam(self, project_root):
from ouroboros.project_dialogue import (
MAIN_MIRROR_INELIGIBLE, project_completion_delivery_outcome,
)
child = {**project_root.task, "id": "child-1", "parent_task_id": "root-1",
"delegation_role": "subagent"}
stored = write_task_result(project_root.root, "child-1", STATUS_COMPLETED,
parent_task_id="root-1", root_task_id="root-1",
delegation_role="subagent", project_id="launch")
assert project_completion_delivery_outcome(
project_root.root, {}, "child-1", child, stored, DONE) == (MAIN_MIRROR_INELIGIBLE, False)
assert project_root.queued == []
def test_periodic_maintenance_retries_terminal_only_without_synthesis(project_root, monkeypatch):
from ouroboros import server_maintenance as maintenance
from ouroboros import agent_task_pipeline as pipeline
_store(project_root.root, phase='completed', chat_id=project_root.project['chat_id'])
monkeypatch.setattr(pipeline, '_run_post_task_processing_async',
lambda *_a, **_kw: pytest.fail('maintenance must not run synthesis'))
monkeypatch.setattr('supervisor.task_lifecycle.sweep_cancel_intents', lambda: [])
monkeypatch.setattr('supervisor.terminal_delivery.replay_pending_deliveries', lambda *_a: [])
monkeypatch.setattr('ouroboros.observability.retry_pending_child_ref_promotions', lambda *_a: None)
monkeypatch.setattr(maintenance, '_reconcile_abandoned_usage', lambda *_a: None)
real_register = __import__('supervisor.terminal_delivery', fromlist=['register_pending_delivery']).register_pending_delivery
monkeypatch.setattr('supervisor.terminal_delivery.register_pending_delivery', lambda *_a, **_kw: False)
assert settle_terminal_projection(project_root.root, 'root-1') == SETTLEMENT_DEFERRED
assert not project_root.queued
monkeypatch.setattr('supervisor.terminal_delivery.register_pending_delivery', real_register)
monkeypatch.setattr(maintenance, 'DATA_DIR', project_root.root)
stop_checks = iter([False, True])
monkeypatch.setattr(maintenance, '_stop_requested', lambda *_a: next(stop_checks))
assert maintenance._CUSTODY_SWEEP_LOCK.acquire(blocking=False)
maintenance._run_periodic_custody_sweep()
assert len(project_root.queued) == len(_project_rows(project_root.root, 'root-1')) == 1
assert load_task_result(project_root.root, 'root-1')['canonical_terminal_projection']['main_disposition'] == 'owed'
def test_main_uses_current_canonical_result_after_project_append(project_root, monkeypatch):
from ouroboros import project_dialogue as dialogue
_store(project_root.root, phase='completed', terminal_origin='model_final', result='old answer')
original = dialogue.append_canonical_task_summary
def append(root, row):
success = original(root, row)
write_task_result(root, 'root-1', 'completed', result='fresh canonical answer')
return success
monkeypatch.setattr(dialogue, 'append_canonical_task_summary', append)
assert settle_terminal_projection(project_root.root, 'root-1', task=project_root.task,
event={'result': 'stale event answer'}) == SETTLEMENT_SETTLED
assert project_root.queued[0]['progress_meta']['completion_answer'] == 'fresh canonical answer'
def test_main_disposition_is_durable_before_live_queue(project_root, monkeypatch):
_store(project_root.root)
def queue(root, event):
stored = load_task_result(root, 'root-1')
assert stored['canonical_terminal_projection_ready'] is None
assert stored['canonical_terminal_projection']['main_disposition'] == 'owed'
project_root.queued.append(event)
return True
monkeypatch.setattr('supervisor.terminal_delivery.enqueue_terminal_delivery', queue)
assert settle_terminal_projection(project_root.root, 'root-1', task=project_root.task) == SETTLEMENT_SETTLED
def test_append_receipt_crash_dedupes_across_chat_rotation(project_root, monkeypatch):
from ouroboros import project_dialogue as dialogue
from ouroboros.terminal_projection import reconcile_terminal_projections
_store(project_root.root)
original = dialogue.append_canonical_task_summary
def append_then_rotate(root, row):
assert original(root, row)
archive = root / 'archive'
archive.mkdir(exist_ok=True)
(root / 'logs/chat.jsonl').rename(archive / 'chat_20260923.jsonl')
raise OSError('append succeeded; result receipt was not written')
monkeypatch.setattr(dialogue, 'append_canonical_task_summary', append_then_rotate)
assert settle_terminal_projection(project_root.root, 'root-1', task=project_root.task) == SETTLEMENT_DEFERRED
monkeypatch.setattr(dialogue, 'append_canonical_task_summary', original)
assert reconcile_terminal_projections(project_root.root) == 1
assert _project_rows(project_root.root, 'root-1') == []
archived = [json.loads(line) for line in (project_root.root / 'archive/chat_20260923.jsonl').read_text(encoding="utf-8").splitlines()]
assert len([row for row in archived if row.get('summary_id') == 'task-terminal:root-1']) == 1
assert len(project_root.queued) == 1
@pytest.mark.parametrize('artifact_status', ['pending', 'finalizing'])
def test_artifact_finalization_holds_project_and_main(project_root, artifact_status):
task = {**project_root.task, 'workspace_root': str(project_root.root / 'workspace')}
_store(project_root.root, workspace_root=task['workspace_root'], artifact_status=artifact_status)
assert settle_terminal_projection(project_root.root, 'root-1', task=task) == SETTLEMENT_DEFERRED
assert not _project_rows(project_root.root, 'root-1')
assert not project_root.queued
_store(project_root.root, workspace_root=task['workspace_root'], artifact_status='failed',
artifact_bundle={'status': 'failed'}, outcome_axes={'artifacts': {'status': 'failed'}})
assert settle_terminal_projection(project_root.root, 'root-1', task=task) == SETTLEMENT_SETTLED
assert len(_project_rows(project_root.root, 'root-1')) == 1
assert _project_rows(project_root.root, 'root-1')[0]['outcome'] == 'Failed'
assert len(project_root.queued) == 1
assert settle_terminal_projection(project_root.root, 'root-1', task=task) == SETTLEMENT_NONE
def test_terminal_timestamp_enrichment_does_not_remint(project_root):
_store(project_root.root)
assert settle_terminal_projection(project_root.root, 'root-1', task=project_root.task) == SETTLEMENT_SETTLED
before = load_task_result(project_root.root, 'root-1')['canonical_terminal_projection']
_store(project_root.root, ts='2099-01-01T00:00:00Z')
assert settle_terminal_projection(project_root.root, 'root-1', task=project_root.task) == SETTLEMENT_NONE
assert load_task_result(project_root.root, 'root-1')['canonical_terminal_projection'] == before
assert len(_project_rows(project_root.root, 'root-1')) == 1
assert len(project_root.queued) == 1
def test_canonical_cancel_keeps_existing_artifact_readiness_exception(project_root):
write_task_result(project_root.root, 'root-1', 'cancelled', root_task_id='root-1',
project_id='launch', workspace_root=str(project_root.root/'workspace'),
artifact_status='pending', result='Cancelled by custody')
assert settle_terminal_projection(project_root.root, 'root-1') == SETTLEMENT_SETTLED
assert _project_rows(project_root.root, 'root-1')[0]['outcome'] == 'Cancelled'
def test_split_root_uses_stored_adoption_facts_without_task_argument(project_root):
child = project_root.root/'child-drive'
_store(project_root.root, child_drive_root=str(child),
workspace_root=str(project_root.root/'workspace'), artifact_status='ready_with_changes')
assert settle_terminal_projection(project_root.root, 'root-1') == SETTLEMENT_DEFERRED
assert not _project_rows(project_root.root, 'root-1')
_store(project_root.root, headless_child_drive_root=str(child),
child_ref_promotion={'schema_version': 1, 'status': 'complete'})
assert settle_terminal_projection(project_root.root, 'root-1') == SETTLEMENT_SETTLED
assert len(_project_rows(project_root.root, 'root-1')) == 1
def test_child_append_does_not_scan_root_recovery_history(tmp_path, monkeypatch):
def forbidden(*_a):
raise AssertionError('child entered root history recovery')
monkeypatch.setattr('ouroboros.terminal_projection._already_in_chat', forbidden)
child = {'id': 'child', 'parent_task_id': 'root', 'root_task_id': 'root',
'delegation_role': 'subagent', 'chat_id': 1}
stored = write_task_result(tmp_path, 'child', 'completed', parent_task_id='root',
root_task_id='root', delegation_role='subagent')
assert append_terminal_task_projection(tmp_path, 'child', child, stored, DONE)
assert not append_terminal_task_projection(tmp_path, 'child', child, stored, DONE)
assert len(_project_rows(tmp_path, 'child')) == 1

View file

@ -235,7 +235,10 @@ def test_project_completion_host_salvage_labels_its_bytes_and_points(tmp_path, m
assert len(queued) == 1
text = queued[0]["text"]
assert raw not in text
assert f"provider_unavailable. {SALVAGE_EXCERPT_LABEL}: RAW PATCH" in text
# The cause speaks its human sentence (TASK_CAUSE_PHRASES twin), never the raw code.
assert (f"The model provider stopped answering, so the task could not finish. "
f"{SALVAGE_EXCERPT_LABEL}: RAW PATCH") in text
assert "provider_unavailable." not in text
# The excerpt form keeps the pointer too: this writer has no other one.
assert text.endswith(" Open the Project for details.")
labelled = text.split(f"{SALVAGE_EXCERPT_LABEL}: ", 1)[1]

View file

@ -52,7 +52,7 @@ def test_both_real_are_stated_in_one_line_separated_by_a_middle_dot() -> None:
delegated_runs_unreconciled=["run-a1", "run-b2"],
outcome_axes={"execution": {"status": "failed", "reason_code": "provider_unavailable"}},
)
assert _completion_verdict(both, {}) == f"provider_unavailable · {CUSTODY_SENTENCE}"
assert _completion_verdict(both, {}) == f"The model provider stopped answering, so the task could not finish · {CUSTODY_SENTENCE}"
def test_a_healed_debt_with_no_execution_cause_states_nothing() -> None:
@ -74,8 +74,8 @@ def test_the_debt_may_arrive_on_the_event_instead_of_the_result() -> None:
def test_every_other_reason_code_passes_through_untouched() -> None:
plain = {"status": "failed", "reason_code": "provider_unavailable"}
assert _completion_verdict(plain, {}) == "provider_unavailable."
plain = {"status": "failed", "reason_code": "mystery_rail_code"}
assert _completion_verdict(plain, {}) == "mystery_rail_code."
assert _completion_verdict({"status": "completed"}, {}) == ""
@ -116,7 +116,7 @@ def test_a_healed_debt_is_never_resurrected_by_its_own_frozen_warning() -> None:
railed = _row(outcome_axes=custody_debt_axes(
{"execution": {"status": "failed", "reason_code": "provider_unavailable"}}),
delegated_runs_unreconciled=[])
assert _completion_verdict(railed, {}) == "provider_unavailable."
assert _completion_verdict(railed, {}) == "The model provider stopped answering, so the task could not finish."
_DEFERRED_BESIDE_PLAN = {
@ -207,7 +207,7 @@ def test_a_held_task_never_says_the_work_went_on(source, reason, sentence) -> No
other = {"status": "failed", "reason_code": "provider_unavailable",
"outcome_axes": {"execution": {"status": "failed", "reason_code": "provider_unavailable"},
"objective": {"status": "fail", "source": "task_acceptance_review"}}}
assert _completion_verdict(other, {}) == "provider_unavailable."
assert _completion_verdict(other, {}) == "The model provider stopped answering, so the task could not finish."
def test_the_event_and_the_row_render_one_reason_line_over_the_shared_fixture() -> None:

View file

@ -144,10 +144,13 @@ def test_wait_episode_exhausted_on_a_round_holding_a_repeat_record_takes_the_unk
assert trace["forced_finalization"]["source"] == "provider_outcome_unknown_no_resend"
assert usage[TRANSPORT_DEATHS_KEY]["count"] == 1
assert "1 earlier physical attempt(s) of the last dispatched round" in result
assert "unresolved at their upper bound" in result
assert "any recorded bound is retained" in result
assert "Retry when connectivity returns" not in result
assert "Inspect the preserved facts before starting another run." in result
assert result.endswith(loop_transport.provider_recovery_hint(usage))
# #869: the unknown terminal ends with the no-resend fence AND the money
# uncertainty: no bound is invented when the attempt has none.
assert result.endswith(
loop_transport._unknown_terminal_recovery_hint(usage) + loop_transport.UNKNOWN_ATTEMPT_COST_NOTE)
else:
assert trace["forced_finalization"]["source"] == "transport_unavailable_no_resend"
assert TRANSPORT_DEATHS_KEY not in usage
@ -198,7 +201,7 @@ def test_deadline_refused_redial_still_names_the_class_the_repeat_was_released_w
assert usage[TRANSPORT_DEATHS_KEY]["count"] == 1
assert trace["forced_finalization"]["source"] == "provider_outcome_unknown_no_resend"
assert usage["execution_status"] == "infra_failed" and usage["reason_code"] == "provider_unavailable"
assert "waited and redialed" in result # the wait wording is still there
assert "provider wait" in result and "redialed" not in result
assert "the repeat failed as transport_unavailable" in result
assert "deadline_exhausted" not in result
@ -391,7 +394,7 @@ def test_round_record_outranks_a_wait_cause_that_holds_an_overflow_kind(tmp_path
assert trace["forced_finalization"]["source"] == "provider_outcome_unknown_no_resend"
assert usage["execution_status"] == "infra_failed"
assert usage["reason_code"] == "provider_unavailable"
assert "the task waited and redialed" in text
assert "The task spent" in text and "provider wait" in text
assert "1 earlier physical attempt(s) of the last dispatched round" in text
assert "the repeat failed as transport_unavailable" in text
assert "context exceeded" not in text

View file

@ -0,0 +1,195 @@
"""Presentation truth on the host side: voice, child warnings, cancel cause.
Three small contracts that the browser already keeps and the host did not:
* #1011 a host note sent through the shared seam is typed as the HOST's voice,
so a reader cannot promote it to the card's title the way an absent key
(legacy) still means narration;
* #1087 a child that finished with warnings reads that way in chat whichever
axis carries the degradation, through the SHARED normalized projection;
* #1061 a cancelled row states the RECORDED cause and only the relation the
record proves, word for word with the browser twin.
"""
from __future__ import annotations
import json
import pytest
# ---------------------------------------------------------------------------
# #1011 — the voice of a host note
# ---------------------------------------------------------------------------
class TestHostNoteVoice:
def _bus(self, tmp_path, monkeypatch):
import supervisor.message_bus as mb
monkeypatch.setattr(mb, "DATA_DIR", tmp_path)
(tmp_path / "logs").mkdir(parents=True, exist_ok=True)
monkeypatch.setattr(mb, "load_state", lambda: {"owner_id": 1})
sent: list[dict] = []
monkeypatch.setattr(mb, "get_bridge", lambda: type("B", (), {
"send_message": staticmethod(lambda *a, **kw: sent.append(dict(kw))),
})())
return mb, sent
def test_an_omitted_voice_is_normalized_to_the_host_on_every_new_send(self, tmp_path, monkeypatch):
mb, sent = self._bus(tmp_path, monkeypatch)
mb.send_with_budget(1, "startup notice", is_progress=True, task_id="t1",
role="system", system_type="startup_notice")
rows = [json.loads(line) for line in
(tmp_path / "logs" / "progress.jsonl").read_text(encoding="utf-8").splitlines()]
# ONE meta dict reaches the durable record and the live bridge.
assert rows[-1]["narration"] is False
assert sent[-1]["progress_meta"]["narration"] is False
def test_an_explicit_voice_is_preserved_in_both_directions(self, tmp_path, monkeypatch):
mb, sent = self._bus(tmp_path, monkeypatch)
mb.send_with_budget(1, "the model speaking", is_progress=True, task_id="t1",
narration=True)
assert sent[-1]["progress_meta"]["narration"] is True
mb.send_with_budget(1, "host note", is_progress=True, task_id="t1", narration=False)
assert sent[-1]["progress_meta"]["narration"] is False
# A caller that already put the fact on its own meta keeps it.
mb.send_with_budget(1, "carried", is_progress=True, task_id="t1",
progress_meta={"narration": True, "card_row": "timeline"})
assert sent[-1]["progress_meta"] == {"narration": True, "card_row": "timeline"}
def test_markdown_and_the_callers_other_meta_are_untouched(self, tmp_path, monkeypatch):
mb, sent = self._bus(tmp_path, monkeypatch)
captured: list[tuple] = []
monkeypatch.setattr(mb, "_send_markdown",
lambda *a, **kw: captured.append((a, kw)) or (True, ""))
mb.send_with_budget(1, "**bold**", fmt="markdown", is_progress=True, task_id="t1",
progress_meta={"card_row": "timeline"})
assert captured[-1][1]["progress_meta"] == {"card_row": "timeline", "narration": False}
def test_the_legacy_read_is_unchanged(self):
# ABSENT still means narration for every row written before the fact —
# the normalization is a write-side default, never a rewrite of history.
from ouroboros.gateway.contracts import ChatOutbound
assert "narration" in ChatOutbound.__annotations__
def test_nonprogress_host_voice_survives_real_history_replay(self, tmp_path, monkeypatch):
import asyncio
from types import SimpleNamespace
from ouroboros.gateway.history import make_chat_history_endpoint
mb, sent = self._bus(tmp_path, monkeypatch)
mb.send_with_budget(1, "Host note", task_id="t1", role="system", system_type="host_progress")
rows = [json.loads(line) for line in (tmp_path / "logs/chat.jsonl").read_text(encoding="utf-8").splitlines()]
assert rows[-1]["narration"] is sent[-1]["progress_meta"]["narration"] is False
response = asyncio.run(make_chat_history_endpoint(tmp_path)(SimpleNamespace(query_params={"chat_id": "1"})))
replay = next(row for row in json.loads(response.body)["messages"] if row.get("text") == "Host note")
assert replay["role"] == "system" and replay["narration"] is False
# ---------------------------------------------------------------------------
# #1087 — a child's warnings, from the shared projection
# ---------------------------------------------------------------------------
class TestChildWarnings:
@pytest.mark.parametrize("axes", [
{"execution": {"status": "degraded"}},
{"objective": {"status": "degraded", "source": "task_acceptance_review"}},
{"review": {"status": "degraded"}},
{"objective": {"status": "best_effort", "source": "task_acceptance_review"}},
{"objective": {"status": "pass", "warning": True}},
])
def test_every_axis_the_card_folds_also_warns_in_chat(self, axes):
from supervisor.events_task_done import _finished_with_warnings
event = {"status": "completed", "outcome_axes": axes}
assert _finished_with_warnings(event, {"status": "completed", "outcome_axes": axes})
# The result alone is enough: the frame need not repeat the axes.
assert _finished_with_warnings({"status": "completed"},
{"status": "completed", "outcome_axes": axes})
def test_a_clean_child_stays_clean_and_precedence_is_the_callers(self):
from supervisor.events_task_done import _finished_with_warnings
clean = {"status": "completed", "outcome_axes": {"execution": {"status": "ok"}}}
assert not _finished_with_warnings(clean, clean)
# A FAILED or CANCELLED child is not "warnings": this predicate answers
# only the warning question, and the caller asks it for completed rows.
failed = {"status": "failed", "outcome_axes": {"execution": {"status": "failed"}}}
assert not _finished_with_warnings(failed, failed)
def test_a_reason_code_without_axes_still_warns(self):
from supervisor.events_task_done import _finished_with_warnings
event = {"status": "completed", "reason_code": "configured_actor_incomplete"}
assert _finished_with_warnings(event, {})
@pytest.mark.parametrize('status', ['failed', 'cancelled'])
def test_terminal_failure_or_cancel_outranks_a_degraded_event(self, status):
from supervisor.events_task_done import _finished_with_warnings
event = {'status': 'completed', 'outcome_axes': {'execution': {'status': 'degraded'}}}
assert not _finished_with_warnings(event, {'status': status})
def test_the_host_and_browser_folds_agree_on_the_same_record(self):
from ouroboros.project_dialogue import outcome_phase
record = {"status": "completed", "outcome_axes": {"review": {"status": "degraded"}}}
assert outcome_phase(record, {}) == "warn"
def test_a_completed_lifecycle_with_a_failed_outcome_never_reads_as_a_clean_completion(self):
# R2 (#1087): `_finished_with_warnings` answers only the warning question
# (False for `error`), so the chat line took ✅ from the lifecycle while the
# card folded the same record to Failed. The display helper speaks the phase.
from ouroboros.project_dialogue import outcome_phase
from supervisor.events_task_done import _completed_lifecycle_display
failed_review = {"status": "completed", "outcome_axes": {"review": {"status": "fail"}}}
assert outcome_phase(failed_review, {}) == "error"
assert _completed_lifecycle_display({"status": "completed"}, failed_review) == ("❌", "finished with a failed outcome")
warned = {"status": "completed", "outcome_axes": {"review": {"status": "degraded"}}}
assert _completed_lifecycle_display({"status": "completed"}, warned) == ("⚠️", "finished with warnings")
clean = {"status": "completed", "outcome_axes": {"execution": {"status": "ok"}}}
assert _completed_lifecycle_display(clean, clean) is None
def test_cancel_cause_python_browser_fixture_parity():
from pathlib import Path
from ouroboros.project_dialogue import _completion_verdict
cases = json.loads((Path(__file__).parents[1] / "web/tests/fixtures/cancel_cause_parity.json").read_text(encoding="utf-8"))
for case in cases:
assert _completion_verdict(case["record"], {}) == case["text"], case
def test_cancel_cause_is_bounded_by_unicode_characters():
from ouroboros.project_dialogue import _completion_verdict
assert _completion_verdict({"status": "cancelled", "cancel_origin": {
"reason": "🙂" * 200,
}}, {}) == "🙂" * 159 + "…"
def test_settled_early_speech_keeps_canonical_warning_axes(tmp_path):
from ouroboros.gateway.history import _assemble_history_response
from ouroboros.task_finalization import stamp_root_final_phase
from ouroboros.task_results import write_task_result
from ouroboros.utils import append_jsonl
from supervisor.message_bus import log_chat
axes = {'execution': {'status': 'degraded'}}
event = {'progress_meta': {'outcome_axes': axes}}
stamp_root_final_phase(event, {}, post_task_open=True, terminal_status='completed')
log_chat('out', 1, 1, 'Retained early answer', task_id='early',
message_meta=event['progress_meta'], drive_root=tmp_path)
write_task_result(tmp_path, 'early', 'completed', outcome_axes=axes,
root_phase_checkpoint={'post_task_synthesis': 'completed'})
append_jsonl(tmp_path/'logs/progress.jsonl', {'task_id': 'early', 'chat_id': 1,
'content': 'Working', 'ts': '2026-09-23T10:00:00Z'})
append_jsonl(tmp_path/'logs/chat.jsonl', {'task_id': 'early', 'chat_id': 1,
'direction': 'system', 'type': 'task_summary', 'text': 'Summary',
'ts': '2026-09-23T10:00:01Z'})
messages = json.loads(_assemble_history_response(tmp_path, 1, 50, 200))['messages']
speech = next(row for row in messages if row.get('text') == 'Retained early answer')
assert speech['task_terminal_status'] == 'completed'
assert speech['outcome_axes']['execution']['status'] == 'degraded'
assert speech['outcome_phase'] == 'warn'

View file

@ -0,0 +1,270 @@
"""A REAL root actor fails on a provider refusal while its post-task synthesis
is still open (#1110/#1011): the Main card reads the outcome first ("Failed")
and "Finalizing…" only as the secondary phase — live, after a reload, after a
real WebSocket reconnect and on a narrow light page — then settles once.
Only model judgment is a fixture. The provider refusal is a real HTTP 401 on
the model wire, the host's own provider-unavailable rail salvages the answer,
and the one event-held call is the post-task summary (identified by the first
line of the production prompt), so synthesis is provably open while the
browser looks. Nothing is timed: every wait is an event or a bounded DOM poll.
"""
import json
import pytest
from devtools.benchmarks.common.server_runner import _api
from ouroboros.contracts.chat_id_policy import WEB_UI_CHAT_ID
from ouroboros.post_task_synthesis import _TASK_SUMMARY_PROMPT
from tests.test_owner_wait_integration import wait_clone as clone_fixture
from tests.system_e2e.harness import (
ArtifactOracle, KeylessIsolatedServer, ModelGate, ScriptedStubModel, body_text,
keyless_settings, wait_until, write_settings_file,
)
wait_clone = clone_fixture
pytestmark = [pytest.mark.serial, pytest.mark.browser]
MARKER = "FAILED_FINALIZING_REAL_ACTOR"
SUMMARY_MARKER = _TASK_SUMMARY_PROMPT.splitlines()[0]
SALVAGE_MARKER = "[PROVIDER_UNAVAILABLE]" # ouroboros/loop.py::_provider_unavailable_result
SALVAGE = "The provider refused the next request; the VERSION read before the outage is retained."
SUMMARY = "Episodic summary: read VERSION, then the provider refused the next round with HTTP 401."
NEUTRAL = "Nothing further to record for this fixture."
TITLE = "Provider outage proof"
CARD = '#chat-messages .chat-live-card[data-task-id="{}"]'
STATE_JS = """tid => {
const cards = document.querySelectorAll(`#chat-messages .chat-live-card[data-task-id="${tid}"]`);
const card = cards[0];
if (!card) return null;
const chip = card.querySelector('[data-live-phase]');
const second = card.querySelector('[data-live-phase-secondary]');
const style = el => el ? getComputedStyle(el) : null;
return {count: cards.length, finished: card.dataset.finished, phase: chip.dataset.phase,
chip: chip.textContent, chipHidden: chip.hidden, label: chip.getAttribute('aria-label'),
secondary: second.textContent, secondaryHidden: second.hidden,
title: card.querySelector('[data-live-title]')?.textContent || '',
theme: document.documentElement.dataset.theme || '',
chipColor: style(chip).color, chipBackground: style(chip).backgroundColor,
secondaryColor: style(second).color,
overflow: card.scrollWidth > card.clientWidth + 1
|| second.getBoundingClientRect().right > innerWidth + 1};
}"""
HOLDING_JS = """tid => { const c = document.querySelector(`#chat-messages .chat-live-card[data-task-id="${tid}"]`);
const chip = c?.querySelector('[data-live-phase]'), s = c?.querySelector('[data-live-phase-secondary]');
return Boolean(c && chip.textContent === 'Failed' && !s.hidden && s.textContent === 'Finalizing…'); }"""
SETTLED_JS = """tid => { const c = document.querySelector(`#chat-messages .chat-live-card[data-task-id="${tid}"]`);
const s = c?.querySelector('[data-live-phase-secondary]');
return Boolean(c && c.dataset.finished === '1' && s.hidden); }"""
OBSERVE_JS = """tid => { const card = document.querySelector(`#chat-messages .chat-live-card[data-task-id="${tid}"]`);
const chip = card.querySelector('[data-live-phase]'), second = card.querySelector('[data-live-phase-secondary]');
window.__proofCard = card; window.__phaseLog = [];
const note = () => window.__phaseLog.push([chip.textContent, second.hidden ? '' : second.textContent, card.dataset.finished]);
window.__phaseObserver = new MutationObserver(note);
window.__phaseObserver.observe(card, {subtree: true, childList: true, characterData: true,
attributes: true, attributeFilter: ['hidden', 'data-finished', 'data-phase']}); }"""
class _OutageModel(ScriptedStubModel):
"""One real tool round, then a provider refusal (HTTP 401, a permanent class,
so no backoff retries) on every later tool round of the marked task. The host's
salvage call and the post-task summary get their own answers; the summary is
the one call the gate holds."""
def __init__(self, gate):
super().__init__([{"tool": "read_file", "arguments": {"path": "VERSION"}}],
final_answer=NEUTRAL, gate=gate)
self.refused = 0
outer, base = self, self._server.RequestHandlerClass
class Handler(base):
def do_POST(self): # noqa: N802 - stdlib callback name
raw = self.rfile.read(int(self.headers.get("Content-Length") or 0))
try:
body = json.loads((raw or b"{}").decode("utf-8"))
except ValueError:
body = {}
body = body if isinstance(body, dict) else {}
if outer._refuse(body):
return self._send({"error": {"message": "Invalid API key (fixture provider outage)",
"type": "invalid_request_error", "code": "invalid_api_key"}},
status=401)
outer.gate(body)
return self._send(outer._completion(body), stream=bool(body.get("stream")))
self._server.RequestHandlerClass = Handler
def _refuse(self, body):
text = body_text(body)
if not body.get("tools") or MARKER not in text or SALVAGE_MARKER in text:
return False
if not any(isinstance(m, dict) and m.get("role") == "tool" for m in body.get("messages") or []):
return False
with self._lock:
self.refused += 1
self.calls.append(("refused_401", body))
return True
def _answer(self, body, seq):
text = body_text(body)
if SALVAGE_MARKER in text:
return "salvage", {"role": "assistant", "content": SALVAGE}
if SUMMARY_MARKER in text:
return "summary", {"role": "assistant", "content": SUMMARY}
return super()._answer(body, seq)
def _open(browser, url, *, theme, viewport):
context = browser.new_context(viewport=viewport, has_touch=viewport["width"] < 500,
reduced_motion="reduce")
context.add_init_script(f"try {{ localStorage.setItem('ouroboros.theme', '{theme}'); }} catch {{}}")
page = context.new_page()
page.goto(url, wait_until="domcontentloaded")
return context, page
def _synthesis(row):
return str((row.get("root_phase_checkpoint") or {}).get("post_task_synthesis") or "")
def _rows(oracle, task_id):
return oracle.task_result(task_id), oracle.task_drive(task_id).task_result(task_id)
def _brief(row):
return {key: row.get(key) for key in ("status", "reason_code", "root_phase_checkpoint", "outcome_axes")}
def _holding(page, task_id, stage):
page.wait_for_function(HOLDING_JS, arg=task_id, timeout=60000)
state = page.evaluate(STATE_JS, task_id)
# Outcome owns the chip; the running finalization is only the secondary fact.
assert state["count"] == 1 and state["finished"] == "0", (stage, state)
assert state["chip"] == "Failed" and not state["chipHidden"], (stage, state)
assert state["label"] == "Task status: Failed, Finalizing…", (stage, state)
# The name stays the task's own; the outcome is never smuggled into it.
assert state["title"] == TITLE, (stage, state)
assert page.locator(CARD.format(task_id) + " [data-live-phase-secondary]").is_visible(), stage
return state
def test_failed_root_reads_failed_then_finalizing(wait_clone, tmp_path, monkeypatch):
from playwright.sync_api import sync_playwright
root = tmp_path / "instance" / "data"
root.mkdir(parents=True)
fake_home = tmp_path / "home"
fake_home.mkdir()
original_env = KeylessIsolatedServer._env
monkeypatch.setattr(KeylessIsolatedServer, "_env", lambda server: {
**original_env(server), "HOME": str(fake_home), "USERPROFILE": str(fake_home),
"XDG_CONFIG_HOME": str(fake_home / ".config")})
# Evidence stays inside the test's own temp root; an explicit out-dir is opt-in
# only and must itself be a temp-root path, so the test never writes elsewhere.
shots = tmp_path / "screenshots"
shots.mkdir(parents=True, exist_ok=True)
gate = ModelGate(lambda body: SUMMARY_MARKER in body_text(body) and MARKER in body_text(body), timeout=300)
facts = {}
with _OutageModel(gate) as model:
settings_path = root / "settings.json"
write_settings_file(settings_path, keyless_settings(model, OUROBOROS_MAX_WORKERS=2))
server = KeylessIsolatedServer(wait_clone, root, settings_path)
server.start(ready_timeout=120)
oracle = ArtifactOracle(root)
try:
with sync_playwright() as pw:
browser = pw.chromium.launch()
try:
desk_ctx, desk = _open(browser, server.base_url, theme="dark",
viewport={"width": 1440, "height": 1000})
# An owner's open Main: the admission name frame must reach a live socket.
desk.wait_for_function("() => window.__ouroWs?.ws?.readyState === 1", timeout=60000)
created = _api(server.base_url, "POST", "/api/tasks", {
"description": f"{MARKER}: read VERSION, then report what it says.",
"title": TITLE, "chat_id": WEB_UI_CHAT_ID, "source": "web",
"memory_mode": "forked", "metadata": {"delegation_role": "root"}})
task_id = str(created.get("task_id") or "")
assert task_id, created
# The held summary IS the open synthesis: the early final already left.
assert gate.arrived.wait(180), (model.kinds(), oracle.task_result(task_id))
# The worker's own (forked) row already settled Failed; the canonical
# row stays live until task_done, and carries the open checkpoint.
canonical, forked = _rows(oracle, task_id)
facts["held_rows"] = {"canonical": _brief(canonical), "forked": _brief(forked)}
assert forked.get("status") == "failed", facts["held_rows"]
assert "running" in (_synthesis(canonical), _synthesis(forked)), facts["held_rows"]
assert model.refused >= 1
card = desk.locator(CARD.format(task_id))
facts["live"] = _holding(desk, task_id, "live")
card.screenshot(animations="disabled", path=str(shots / "chromium-failed-finalizing-live.png"))
desk.evaluate("window.__beforeReload = true")
desk.reload(wait_until="domcontentloaded")
facts["reload"] = _holding(desk, task_id, "reload")
assert desk.evaluate("window.__beforeReload === undefined"), "reload kept the old document"
card.screenshot(animations="disabled", path=str(shots / "chromium-failed-finalizing-reload.png"))
# A real socket close through the production client: its own reconnect
# re-reads history in place (same document, new socket).
desk.wait_for_function("() => window.__ouroWs?.ws?.readyState === 1", timeout=30000)
desk.evaluate("window.__sameDocument = true; window.__oldSocket = window.__ouroWs.ws")
with desk.expect_response(lambda r: "/api/chat/history" in r.url, timeout=60000):
desk.evaluate("window.__oldSocket.close()")
desk.wait_for_function("() => window.__ouroWs.ws && window.__ouroWs.ws !== window.__oldSocket"
" && window.__ouroWs.ws.readyState === 1", timeout=30000)
facts["reconnect"] = _holding(desk, task_id, "reconnect")
assert desk.evaluate("window.__sameDocument === true"), "reconnect reloaded the window"
card.screenshot(animations="disabled", path=str(shots / "chromium-failed-finalizing-reconnect.png"))
narrow_ctx, narrow = _open(browser, server.base_url, theme="light",
viewport={"width": 390, "height": 844})
facts["narrow_light"] = light = _holding(narrow, task_id, "narrow-light")
dark = facts["reconnect"]
assert (dark["theme"], light["theme"]) == ("dark", "light"), (dark, light)
assert (light["chipColor"], light["chipBackground"]) != (dark["chipColor"], dark["chipBackground"])
assert light["secondaryColor"] != dark["secondaryColor"], (dark, light)
assert not light["overflow"], light
narrow.locator(CARD.format(task_id)).scroll_into_view_if_needed()
narrow.screenshot(animations="disabled", path=str(shots / "chromium-390-failed-finalizing.png"))
narrow_ctx.close()
# Only now may synthesis close; the live card must settle exactly once.
assert "running" in (_synthesis(r) for r in _rows(oracle, task_id)), _rows(oracle, task_id)
desk.evaluate(OBSERVE_JS, task_id)
gate.release.set()
settled = wait_until(lambda: (row if _synthesis(row := oracle.task_result(task_id)) == "completed"
else None), 120)
assert settled and settled["status"] == "failed", oracle.task_result(task_id)
desk.wait_for_function(SETTLED_JS, arg=task_id, timeout=60000)
facts["settled"] = state = desk.evaluate(STATE_JS, task_id)
assert state["count"] == 1 and state["chip"] == "Failed" and state["secondary"] == "", state
assert state["title"] == TITLE, state
assert state["label"] == "Task status: Failed", state
facts["phase_log"] = log = desk.evaluate("window.__phaseLog")
assert desk.evaluate("window.__proofCard.isConnected"), "the card was replaced, not settled"
assert {chip for chip, _second, _finished in log} <= {"Failed"}, log
finished = [f for _c, _s, f in log]
assert "1" in finished and "0" not in finished[finished.index("1"):], log
assert desk.locator("#chat-messages").get_by_text(SALVAGE).count() == 1
card.screenshot(animations="disabled", path=str(shots / "chromium-failed-settled.png"))
# Replay after synthesis closed settles too: no stale Finalizing….
desk.reload(wait_until="domcontentloaded")
desk.wait_for_function(SETTLED_JS, arg=task_id, timeout=60000)
facts["settled_reload"] = again = desk.evaluate(STATE_JS, task_id)
assert again["chip"] == "Failed" and again["secondary"] == "" and again["count"] == 1, again
assert again["title"] == TITLE, again
desk_ctx.close()
finally:
browser.close()
facts.update(task_id=task_id, refused=model.refused, gate={"matched": gate.matched, "held": gate.held,
"timed_out": gate.timed_out}, kinds=model.kinds(), server_pid=server.proc.pid,
execution=settled.get("outcome_axes", {}).get("execution"),
reason_code=settled.get("reason_code"), status=settled.get("status"))
(shots / "journey.json").write_text(json.dumps(facts, indent=2, default=str))
assert gate.held == 1 and not gate.timed_out
finally:
gate.release.set()
server.stop()
assert server.proc.poll() is not None

View file

@ -81,6 +81,24 @@ def _seed_history(root):
return {"preserved_path": str(preserved), "preserved_bytes": preserved.read_bytes()}
def _seed_early_failure(root):
from ouroboros.task_results import write_task_result
from ouroboros.utils import append_jsonl
from supervisor.message_bus import log_chat
logs = root / "logs"
from ouroboros.task_finalization import stamp_root_final_phase
early = {"type": "send_message", "progress_meta": {"outcome_axes": {"execution": {"status": "infra_failed"}}}}
stamp_root_final_phase(early, {}, post_task_open=True, terminal_status="failed")
write_task_result(root, "known-failure", "failed", title="Known failed work",
root_phase_checkpoint={"post_task_synthesis": "running"},
outcome_axes={"execution": {"status": "infra_failed"}})
append_jsonl(logs / "progress.jsonl", {"task_id": "known-failure", "chat_id": 1,
"content": "Checking the requested result", "ts": "2026-09-08T10:00:00Z"})
log_chat("out", 1, 1, "The provider failed; retained work remains available.",
task_id="known-failure", message_meta=early["progress_meta"], drive_root=root)
def test_ui_results_and_required_question_journey(wait_clone, tmp_path, monkeypatch):
from playwright.sync_api import sync_playwright
@ -111,11 +129,18 @@ def test_ui_results_and_required_question_journey(wait_clone, tmp_path, monkeypa
oracle = ArtifactOracle(root)
try:
project = _api(server.base_url, "POST", "/api/projects", {"name": "Evidence review"})["project"]
_seed_early_failure(root)
with sync_playwright() as pw:
browser = pw.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 1000}, reduced_motion="reduce")
try:
page.goto(server.base_url, wait_until="domcontentloaded")
early_card = page.locator('.chat-live-card[data-task-id="known-failure"]')
early_card.locator('[data-live-phase]').filter(has_text="Failed").wait_for(timeout=30000)
early_card.get_by_text("Finalizing…", exact=True).wait_for()
# This seeded row has no live actor. The real census can
# correctly retire it; it is a replay metadata assertion,
# not a screenshot claim of a live finalizing process.
cancelled = page.locator('.chat-live-card[data-task-id="old-cancelled"]')
cancelled.locator('[data-live-phase]').filter(has_text="Cancelled").wait_for(timeout=30000)
assert page.get_by_text("Retained ordinary answer before cancellation", exact=True).count() == 1

View file

@ -155,6 +155,7 @@ def test_watchdog_alerts_on_chat_turn_wedge(monkeypatch):
assert wedge[2]["progress_meta"] == {
"task_incident": "chat_turn_wedge",
"toast_once": "wedged1:chat_turn_wedge",
"narration": False,
}

View file

@ -1,5 +1,15 @@
/** Dependency-free JSDoc mirror of `ouroboros.gateway.contracts`. */
/**
* @typedef {Object} CostPresentation
* @property {'own'|'root_tree'} scope
* @property {?number} tracked_amount
* @property {boolean} has_unpriced
* @property {boolean} tracked_final
* @property {boolean} accounting_open
* @property {boolean} has_rows
*/
/**
* @typedef {Object} StateResponse
* @property {number} uptime
@ -402,6 +412,11 @@
* v6.87.48: the count of OPEN ledger rows — the disclosed cause of `cost_final: false`,
* which can hold with every dollar bucket at zero (an estimated $0.00, or a dispatched
* row whose reservation is exactly zero).
* @property {?CostPresentation=} cost_presentation
* #498: the facts that EXPLAIN the amount beside it, bound to the scope whose ledger
* rows produced them (`own` or `root_tree`). `tracked_amount` is null unless a priced
* or bounded row actually evidenced it, so an empty ledger and an all-unpriced one
* never read as a measured zero. Null when the ledger could not be read.
* @property {?boolean=} ledger_integrity_degraded
* C12: the ledger's INTEGRITY marker, produced by the cost authority all along but
* named in no carry list — an amount computed over a degraded ledger used to reach the

View file

@ -0,0 +1,28 @@
import { plainCauseText } from './utils.js';
// Twin of supervisor.cancel_publication.cancel_cause_clauses. IDs stay in
// cancel_origin/lineage details; a compact sentence never guesses an actor.
export const CANCEL_SOURCE_PHRASES = {
http_single: 'Stopped from the app (Stop now)',
http_cascade: 'Stopped from the app (Stop now)',
http_graceful: 'Stopped from the app (Wrap up)',
cascade_descendant: 'Stopped with the task tree it belongs to',
};
export function cancelCauseClauses(origin, record = {}) {
const source = String(origin.source || '');
const asked = String(origin.requested_by || '');
const self = String(record.task_id || record.id || record.subagent_task_id || '');
const parent = String(record.parent_task_id || '');
const root = String(record.root_task_id || '');
const actor = origin.request_origin?.kind === 'agent_task' && origin.request_origin.task_id;
const relation = asked && asked !== self && asked === parent ? 'Stopped with its parent task'
: asked && asked !== self && parent && asked === root ? 'Stopped with an ancestor task' : '';
return [
Object.hasOwn(CANCEL_SOURCE_PHRASES, source) ? CANCEL_SOURCE_PHRASES[source] : source,
plainCauseText(origin.reason),
origin.scope === 'cascade' ? 'this task and its sub-tasks' : '',
relation,
actor ? 'Requested by a task' : '',
];
}

View file

@ -993,16 +993,18 @@ export function createChatInstance({
);
}
// Early final stays live while post-task synthesis runs.
function markLiveCardFinalizing(taskId = '') {
// Early outcome is not completion.
function markLiveCardFinalizing(taskId = '', fact = {}) {
return withStableViewport(() => {
const record = liveCardRecords.get(taskKey(taskId));
if (!record || record.finished || !record.phaseEl) return false;
const anchored = markReviewAnchor(record);
if (record.cancelPendingPolicy) return anchored;
record.observedOutcome = taskTerminalSummary({ ...fact, task_phase: 'finalizing' }).observedOutcome || record.observedOutcome;
record.finalizingHold = true;
const desired = desiredLiveCardPhase(record);
const phased = setLiveCardPhase(
record, 'working', 'Finalizing…', 'chat-live-phase working finalizing',
record, desired.phase, desired.text, desired.className, desired.secondary,
);
return Boolean(anchored || phased);
});
@ -1073,6 +1075,7 @@ export function createChatInstance({
if (restoredPhase) {
changed = setLiveCardPhase(
record, restoredPhase.phase, restoredPhase.text, restoredPhase.className,
restoredPhase.secondary,
) || changed;
}
return changed;
@ -1297,6 +1300,7 @@ export function createChatInstance({
<div class="chat-live-summary">
<div class="chat-live-summary-main">
<span class="chat-live-phase working" data-live-phase role="status" aria-live="polite" aria-atomic="true" aria-label="${options.isSubagent ? 'Subagent' : 'Task'} status: Working">Working</span>
<span class="chat-live-phase-secondary" data-live-phase-secondary hidden></span>
<div class="chat-live-typing" data-live-typing aria-hidden="true">
<span></span><span></span><span></span>
</div>
@ -1725,23 +1729,19 @@ export function createChatInstance({
: (record.lastHumanHeadline
|| (record.updates > 1 ? record.titleEl.textContent : '')
|| 'Working...');
const activePhase = record.finished
? (summary.phase || 'done')
: (shouldPromote ? (summary.phase || 'working') : (record.phaseEl.dataset.phase || 'working'));
const desiredPhase = desiredLiveCardPhase(record, activePhase);
setLiveCardPhase(record, desiredPhase.phase, desiredPhase.text, desiredPhase.className);
// #1110: a task-scope frame's observed outcome is the chip under the hold; a failed tool call is diagnostics, never the task's outcome.
if (summary.observedOutcome && !record.finished) record.observedOutcome = summary.observedOutcome;
const desiredPhase = desiredLiveCardPhase(record, record.finished ? summary.phase || 'done' : '');
setLiveCardPhase(record, desiredPhase.phase, desiredPhase.text, desiredPhase.className,
desiredPhase.secondary);
// A coined project name takes the title slot (the activity headline stays in the
// timeline); a child's title is its lineage identity; a block without work
// (open attention, a bare non-Done ending) carries no title; otherwise the
// activity headline.
// A Failed outcome reported during the finalizing hold keeps the title slot: the chip says
// only "Finalizing…" then, so narration must not be the one thing that hides the failure.
if (shouldPromote && !record.finished && taskPresentation(summary.phase).headline === 'Failed') record.failedHeadline = headline;
const title = record.suggestedName || (record.isSubagent ? childTitle(record)
: !blockHasWork(record) ? ''
: (record.finished ? record.lastHumanHeadline || 'Task activity'
: record.failedHeadline || record.lastHumanHeadline || activeHeadline));
: record.lastHumanHeadline || activeHeadline));
if (record.titleEl.textContent !== title) record.titleEl.textContent = title;
// The collapsed line is a compact projection; the full activity stays in the
// expanded timeline. Every card, a child's included, takes activity only from
@ -2529,7 +2529,7 @@ export function createChatInstance({
// Progress-only/failed tasks still anchor at their first event.
insertCardIfNeeded(taskId);
// Open post-task checkpoint replays as "Finalizing…".
if (msg.task_phase === 'finalizing') markLiveCardFinalizing(taskId);
if (msg.task_phase === 'finalizing') markLiveCardFinalizing(taskId, msg);
continue;
}
if (msg.system_type === 'task_summary') continue;
@ -2569,7 +2569,7 @@ export function createChatInstance({
insertCardIfNeeded(taskId);
// A replayed early final must not finalize the card.
if (msg.task_phase === 'finalizing') {
markLiveCardFinalizing(taskId);
markLiveCardFinalizing(taskId, msg);
} else if (!msg.historical_terminal || positiveTaskTerminalFact(msg)) {
const record = liveCardRecords.get(taskId);
finishLiveCard(taskId, msg.task_terminal_status ? taskTerminalPhase(msg) : replayTerminalPhase(record));
@ -3785,7 +3785,7 @@ export function createChatInstance({
return Boolean(changed);
}
let changed = false;
if (finalizing) changed = markLiveCardFinalizing(explicitTaskId) || changed;
if (finalizing) changed = markLiveCardFinalizing(explicitTaskId, msg) || changed;
else if (explicitTaskId && typedTerminal) {
changed = appendTaskSummaryToLiveCard(msg) || changed;
}

View file

@ -1,7 +1,7 @@
// Pure chat-activity helpers shared by chat.js and dependency-free node tests:
// live-card presentation projections (moved verbatim from chat.js) plus the
// in-flight direct/ephemeral turn status reducer and snapshot hydration.
import { executorIdentityMarkup } from './harness_presentation.js';
import { executorIdentityMarkup, joinMetaParts } from './harness_presentation.js';
import { compactModel, formatLogDuration, modelExecutionLabel } from './log_events.js';
import { createSystemMessageActions } from './ui_helpers.js';
import { projectReference } from './project_reference.js';
@ -502,6 +502,34 @@ export function headerBudgetPresentation(data) {
* Render task money without conflating unknown/non-final values with a final
* zero. The returned strings are card metadata, not another cost authority.
*/
/**
* Project the producer's scoped carrier (#498) into card meta. The carrier
* answers the whole question in one fact — which scope the number describes,
* whether a zero is EVIDENCED, whether anything in that scope is unpriced — so
* a frame that has one never re-derives it from two half-matching fields.
*
* `null` means "no carrier here, use the legacy derivation below".
*/
export function costPresentationMeta(presentation) {
if (!presentation || typeof presentation !== 'object') return null;
if (!presentation.has_rows) return presentation.accounting_open ? ['Cost unknown'] : [];
const amount = presentation.tracked_amount;
if (amount === null || amount === undefined || !Number.isFinite(Number(amount))) {
// No priced or bounded row evidenced anything: an empty ledger and a
// ledger of exclusively unpriced calls both sum to 0.0, and neither is a
// measured zero. The card says so instead of inventing a free result.
return ['Cost unknown'];
}
const money = `$${Number(amount).toFixed(2)}`;
if (presentation.has_unpriced) {
// Mixed: this is only the tracked subtotal. The reason is stated in WORDS
// beside it — a title attribute is invisible on touch, to assistive
// technology, and in a copied line.
return [`Tracked: ${presentation.tracked_final ? money : `up to ${money}`}`, 'some steps have no price'];
}
return [presentation.tracked_final ? money : `up to ${money}`];
}
export function taskCostMeta(payload = {}) {
// Presence means a VALUE, exactly as in `resolveCostPair`: a browser
// producer literal (chat.js `costMetaKeys`) materializes every cost name it
@ -517,13 +545,27 @@ export function taskCostMeta(payload = {}) {
// task_done/task_cost_finalized frames carry cost_accounting_status /
// cost_final alongside cost_usd, so honest task-scope frames still qualify.
const hasAccountingEvidence = [
'cost_accounting_status', 'cost_final',
'cost_accounting_status', 'cost_final', 'cost_presentation',
'cost_usd_with_children', 'cost_with_children_partial',
'accounted_upper_bound_usd', 'accounted_upper_bound_usd_with_children',
'reserved_usd', 'unresolved_upper_bound_usd', 'unknown_unmetered',
].some(has);
if (!hasAccountingEvidence) return [];
if (payload.cost_accounting_status === 'unavailable') return ['cost unavailable'];
// #498: a producer that had a readable ledger but NO same-scope facts for
// this frame (a nested child's foreign subtree rollup) sends an explicit
// null carrier. That amount is unknown to this card, not unreadable: the
// owner vocabulary for an unknown amount is "Cost unknown" (DESIGN), while
// "cost unavailable" stays reserved for a ledger that could not be read.
// The projection below still ranks it `unavailable` so a narrower own zero
// cannot outrank it (mergeStickyCostMeta).
if (has('cost_presentation') && payload.cost_presentation === null) return ['Cost unknown'];
// #498: the producer's own carrier wins, because it was built from the exact
// ledger bucket it describes. The derivation below stays for legacy frames
// and is deliberately conservative: it may not know a zero is unevidenced.
const presented = costPresentationMeta(payload.cost_presentation);
if (presented) return presented;
// C2/F12: ONE precedence resolver, shared with the Python seams and with
// log_events — the deprecated alias wins a diverged pair, so the read side
@ -531,7 +573,8 @@ export function taskCostMeta(payload = {}) {
const own = accountedUpperBound(payload);
// Compact cards show one complete amount. Prefer the subtree projection
// when the producer has one; leaf/legacy frames still fall back to own.
const total = accountedUpperBoundWithChildren(payload) ?? own;
const hasSubtree = has('accounted_upper_bound_usd_with_children') || has('cost_usd_with_children');
const total = hasSubtree ? accountedUpperBoundWithChildren(payload) : own;
const finalKnown = payload.cost_final === true
&& payload.cost_with_children_partial !== true;
const pendingKnown = payload.cost_final === false
@ -543,7 +586,7 @@ export function taskCostMeta(payload = {}) {
// (`up to`) while the ledger is open, a plain amount once final. Calls with no
// known price are not named here (owner: no separate counter); component
// breakdowns and unmetered counts stay on Costs, Logs and task detail.
if (total === null) return ['cost pending'];
if (total === null) return ['Cost unknown'];
if (!(finalKnown || pendingKnown || total !== 0)) return [];
const amount = `$${total.toFixed(2)}`;
return [finalKnown ? amount : `up to ${amount}`];
@ -558,15 +601,25 @@ export function taskCostMeta(payload = {}) {
export function taskCostProjection(payload = {}, rawTs = '') {
const meta = taskCostMeta(payload);
if (!meta.length) return null;
const unavailable = payload.cost_accounting_status === 'unavailable';
const unavailable = payload.cost_accounting_status === 'unavailable' || payload.cost_presentation === null;
const presentation = payload.cost_presentation;
const legacyRollup = payload.cost_presentation === null && (
payload.accounted_upper_bound_usd_with_children !== undefined || payload.cost_usd_with_children !== undefined);
return {
meta,
ts: rawTimestampEpoch(rawTs),
...(presentation?.scope ? { scope: presentation.scope } : legacyRollup ? { scope: 'rollup' } : {}),
// Only a SETTLED ledger value is final. "unavailable" is an honest
// unknown, not a settled truth: marking it final let one transient
// ledger-read failure outrank every later real reading.
final: payload.cost_final === true
&& payload.cost_with_children_partial !== true,
// ledger-read failure outrank every later real reading. The scoped
// carrier answers this for its own scope when the frame has one (#498).
final: presentation && typeof presentation === 'object'
? !unavailable && presentation.tracked_final === true
&& presentation.has_unpriced === false && presentation.accounting_open === false
&& payload.cost_final !== false && payload.cost_with_children_partial !== true
: !unavailable && !meta.includes('Cost unknown') && payload.cost_final === true
&& payload.cost_with_children_partial !== true && !(Number(payload.unknown_unmetered) > 0)
&& !(Number(payload.non_final_rows) > 0) && !payload.ledger_integrity_degraded,
unavailable,
};
}
@ -582,6 +635,10 @@ export function taskCostProjection(payload = {}, rawTs = '') {
export function mergeStickyCostMeta(previous, next) {
if (!next || !Array.isArray(next.meta) || !next.meta.length) return previous || null;
if (!previous || !Array.isArray(previous.meta) || !previous.meta.length) return next;
if (previous.scope === 'root_tree' && next.scope !== 'root_tree') return previous;
if (next.scope === 'root_tree' && previous.scope !== 'root_tree') return next;
if (previous.scope === 'rollup' && next.scope === 'own') return previous;
if (next.scope === 'rollup' && previous.scope === 'own') return next;
// Rank: unavailable < pending < final. An `unavailable` snapshot is sticky (a
// costless frame must not erase it) but must NOT outrank a later HONEST reading:
// one transient ledger-read failure would otherwise pin the card to "cost
@ -611,8 +668,11 @@ export function clearStickyCardState(record) {
// The executor chip is cycle state like the cost projection: a recycled
// slot must not claim the previous cycle's delegated route as its own.
record.executorChip = null;
// A recycled slot must not inherit the previous cycle's finalizing hold.
// A recycled slot must not inherit the previous cycle's finalizing hold —
// nor the outcome observed under it (#1110), which would otherwise paint the
// new cycle's chip with the old cycle's Failed.
record.finalizingHold = false;
record.observedOutcome = '';
// The activity clock is cycle state too: a recycled slot ('active') would
// otherwise open showing the previous cycle's "updated" time.
record.latestActivityTs = '';
@ -1198,13 +1258,13 @@ export function renderCollapsedActivity(record, text) {
return Boolean(changed && record.activityEl.isConnected);
}
// The 12 cost-meta keys shared by both subagent whitelists (the delegation
// The 13 cost-meta keys shared by both subagent whitelists (the delegation
// trio stays inline in each literal — the wire test scans those literals).
const COST_META_KEYS = [
'cost_usd', 'accounted_upper_bound_usd', 'accounted_upper_bound_usd_with_children',
'cost_accounting_status', 'cost_accounting_error', 'cost_final', 'cost_usd_with_children',
'cost_with_children_partial', 'reserved_usd', 'unresolved_upper_bound_usd',
'unknown_unmetered', 'non_final_rows',
'unknown_unmetered', 'non_final_rows', 'cost_presentation',
];
export function costMetaKeys(src) {
return Object.fromEntries(COST_META_KEYS.map((key) => [key, src?.[key]]));
@ -1222,17 +1282,24 @@ export function cardMetaKeys(src) {
// so a replay batch renders it exactly once per card.
export function renderLiveCardMeta(record, { agentModel = record?.agentModel || '' } = {}) {
if (!record?.metaEl) return false;
const html = executorIdentityMarkup(record.executorChip, { agentModel: compactModel(agentModel) }) + [
record.initiator === 'consciousness' ? 'Consciousness' : '',
record.historicalUnavailable ? 'Outcome unavailable' : (record.historicalUnconfirmed ? 'Activity unconfirmed' : ''),
modelExecutionLabel(record.modelExecution),
Number.isInteger(record.toolCalls) ? `${record.toolCalls} tool ${record.toolCalls === 1 ? "call" : "calls"}` : '',
record.toolErrors > 0 ? `${record.toolErrors} error${record.toolErrors === 1 ? '' : 's'}` : '',
Number.isFinite(record.durationSec) ? formatLogDuration(record.durationSec) : '',
...(Array.isArray(record._lastFrameMeta) ? record._lastFrameMeta : []),
...((record.costMeta && Array.isArray(record.costMeta.meta)) ? record.costMeta.meta : []),
record.latestActivityTs ? `updated ${record.latestActivityTs}` : '',
].filter(Boolean).map((item) => `<span class="chat-live-meta-text">${escapeHtml(item)}</span>`).join(' · ');
// The executor block is ONE part of this line, joined by the same text
// separator as the rest: concatenating it left the chip and the first fact
// touching in a copied line and running together for a screen reader,
// because only the flex gap separated them.
const html = joinMetaParts([
executorIdentityMarkup(record.executorChip, { agentModel: compactModel(agentModel) }),
...[
record.initiator === 'consciousness' ? 'Consciousness' : '',
record.historicalUnavailable ? 'Outcome unavailable' : (record.historicalUnconfirmed ? 'Activity unconfirmed' : ''),
modelExecutionLabel(record.modelExecution),
Number.isInteger(record.toolCalls) ? `${record.toolCalls} tool ${record.toolCalls === 1 ? "call" : "calls"}` : '',
record.toolErrors > 0 ? `${record.toolErrors} error${record.toolErrors === 1 ? '' : 's'}` : '',
Number.isFinite(record.durationSec) ? formatLogDuration(record.durationSec) : '',
...(Array.isArray(record._lastFrameMeta) ? record._lastFrameMeta : []),
...((record.costMeta && Array.isArray(record.costMeta.meta)) ? record.costMeta.meta : []),
record.latestActivityTs ? `updated ${record.latestActivityTs}` : '',
].filter(Boolean).map((item) => `<span class="chat-live-meta-text">${escapeHtml(item)}</span>`),
]);
if (record.metaEl.innerHTML === html) return false;
record.metaEl.innerHTML = html;
return Boolean(record.metaEl.isConnected);

View file

@ -91,6 +91,16 @@ export function harnessAccountIdentityMarkup(harnessId, {
return identity + (profile ? ` (${escapeHtml(profile)})` : '');
}
// The meta line's separator is TEXT, not a CSS gap: it has to survive a copy
// out of the page and be read aloud, and a flex gap does neither. One rule for
// every part boundary, here and in `renderLiveCardMeta`, which joins its own
// parts with the same string and receives this block as one of them.
export const META_PART_SEPARATOR = ' \u00b7 ';
export function joinMetaParts(parts) {
return parts.filter(Boolean).join(META_PART_SEPARATOR);
}
/** Card identity consumes projected facts; it does not infer execution from marks. */
export function executorIdentityMarkup(chip, { agentModel = '' } = {}) {
const identity = chip
@ -101,5 +111,8 @@ export function executorIdentityMarkup(chip, { agentModel = '' } = {}) {
agentModel ? `${chip ? 'Coordinator' : 'Agent model'}: ${agentModel}` : '',
chip?.observedModels?.length ? `Observed: ${chip.observedModels.join(', ')}` : '',
];
return identity + facts.filter(Boolean).map(text => `<span class="chat-live-meta-text">${escapeHtml(text)}</span>`).join('');
return joinMetaParts([
identity,
...facts.filter(Boolean).map(text => `<span class="chat-live-meta-text">${escapeHtml(text)}</span>`),
]);
}

View file

@ -1,4 +1,7 @@
import { accountedUpperBound, accountedUpperBoundWithChildren, formatUsd4, joinMarkdownHeadings } from './utils.js';
import { cancelCauseClauses } from './cancel_presentation.js';
import { taskCheckpointLabel, checkpointHasProgressRow } from './task_checkpoints.js';
export { taskCheckpointLabel } from './task_checkpoints.js';
import { harnessPresentation } from './harness_presentation.js';
import { acceptanceIncidentClauses } from './acceptance_incident_presentation.js';
import {
@ -455,6 +458,8 @@ const TASK_CAUSE_PHRASES = {
host_child_status_suffix: "A child task had not settled when the answer was delivered",
invalid_delivery_control_after_repair: "Ouroboros's final delivery instruction could not be read even after repair, so the answer stands as delivered.",
budget_exhausted: "The task ran out of budget before it could finish cleanly",
// #869: the provider-death rail's terminal words (twin of project_dialogue.TASK_CAUSE_PHRASES).
provider_unavailable: "The model provider stopped answering, so the task could not finish",
delivery_control_degraded: "Ouroboros's final delivery instruction could not be applied, so the answer stands as delivered.",
authoring_handover_incomplete: "The replacement model stopped before resuming tool work.",
delegated_custody_unreconciled: "Some delegated work was never reconciled.",
@ -530,13 +535,6 @@ function terminalLimitations(record, reason, held = false) {
return [deferred ? taskReasonPhrase('child_results_deferred') : '', open ? taskReasonPhrase(planReviewKey(record, planKey)) : ''];
}
// Transport is recorded fact, never proof that an HTTP caller was the owner.
const CANCEL_SOURCE_PHRASES = {
http_single: 'Stopped from the app (Stop now)',
http_cascade: 'Stopped from the app (Stop now)',
http_graceful: 'Stopped from the app (Wrap up)',
};
// Every simultaneous cause the record holds, as ONE line: the primary cause is what
// ENDED the task; a deferred child, a plan review still open at delivery and an
// unreconciled custody debt are standing limitations of the SAME answer, stated
@ -564,14 +562,9 @@ export function taskReasonDetail(evt) {
// own typed reason (an accepted decision only when it has a sentence); the stored
// reviewer rationale stays in the card body, the task result and Logs.
clause = taskReasonPhrase(decisionCause);
} else if (severity === 'cancelled' && origin && typeof origin === 'object' && !Array.isArray(origin)) {
const source = String(origin.source || '');
const actor = origin.request_origin?.kind === 'agent_task' ? origin.request_origin.task_id : '';
clause = [
Object.hasOwn(CANCEL_SOURCE_PHRASES, source) ? CANCEL_SOURCE_PHRASES[source] : source,
origin.scope === 'cascade' ? 'this task and its sub-tasks' : '',
`initiator: ${String(actor || origin.requested_by || '') || 'not recorded'}`,
].filter(Boolean).join(' · ');
} else if (severity === "cancelled" && origin && typeof origin === "object"
&& !Array.isArray(origin) && Object.keys(origin).length) {
clause = joinCauseClauses(cancelCauseClauses(origin, record));
} else if (held) {
// A task HELD by a blocking plan review states the objective's own reason, so it
// never reads as work that went on.
@ -589,8 +582,11 @@ export function taskReasonDetail(evt) {
? String(receiptVeto.detail).split(/\s+/).filter(Boolean).join(' ')
: taskReasonPhrase(reason === 'plan_review_advisory' ? planReviewKey(record, reason) : reason);
}
return joinCauseClauses([clause, ...acceptanceIncidentClauses(decision, reason, TASK_CAUSE_PHRASES),
const line = joinCauseClauses([clause, ...acceptanceIncidentClauses(decision, reason, TASK_CAUSE_PHRASES),
...terminalLimitations(record, reason, held), custody ? taskReasonPhrase(custody) : '']);
// Cancellation's host/browser sentence has identical punctuation. Other
// cause policy stays with the runtime owner of this shared seam.
return severity === 'cancelled' && line && !/[.!?…)\u2026]$/.test(line) ? `${line}.` : line;
}
// S3 (HQ1): the ONE shared projection of a typed owner_hurry event for the
@ -679,6 +675,8 @@ export function taskTerminalPhase(evt) {
const TERMINAL_TASK_DETAIL_STATUSES = new Set([
'completed', 'failed', 'cancelled', 'rejected_duplicate',
]);
// Statuses that make the OUTCOME known, whether or not the card may close yet.
const OBSERVED_OUTCOME_STATUSES = new Set([...TERMINAL_TASK_DETAIL_STATUSES, 'done', 'cancel_requested']);
const OPEN_POST_TASK_SYNTHESIS_STATUSES = new Set(['pending_once', 'running']);
export function isTerminalTaskDetail(record) {
@ -995,9 +993,16 @@ export function summarizeLogEvent(evt) {
});
}
if (t === 'worker_spawn_start') {
return view('start', `Spawning ${evt.count || '?'} workers`, { meta: [evt.start_method || ''] });
}
// One row per worker-lifecycle frame, naming the fields it carries: the two
// readiness frames fell through to the unknown-name remainder and rendered as a
// bare event name with no fields, so a stalled spawn wave was unreadable (#1073).
if (t === 'worker_spawn_start') return view('start', `Spawning ${evt.count || '?'} workers`, { meta: [evt.start_method || ''] });
if (t === 'worker_starting') return view('start', 'Worker starting', { meta: [
evt.worker_id != null ? `worker ${evt.worker_id}` : '', evt.pid ? `pid ${evt.pid}` : '', evt.phase ? `phase ${evt.phase}` : ''] });
if (t === 'worker_ready_window_extended') return view('warn', 'Worker readiness window extended', { meta: [
Array.isArray(evt.worker_ids) && evt.worker_ids.length ? `workers ${evt.worker_ids.join(', ')}` : '',
evt.attempt != null ? `attempt ${evt.attempt}` : '', evt.window_sec != null ? `window ${evt.window_sec}s` : '',
evt.ceiling_sec != null ? `ceiling ${evt.ceiling_sec}s` : ''] });
if (t === 'worker_sha_verify') {
return view(evt.ok ? 'ok' : 'warn', evt.ok ? 'Worker SHA verified' : 'Worker SHA mismatch', {
@ -1009,11 +1014,8 @@ export function summarizeLogEvent(evt) {
});
}
if (t === 'worker_boot') {
return view('boot', 'Worker booted', {
meta: [evt.pid ? `pid ${evt.pid}` : '', evt.git_sha ? String(evt.git_sha).slice(0, 8) : ''],
});
}
if (t === 'worker_boot') return view('boot', 'Worker booted', {
meta: [evt.pid ? `pid ${evt.pid}` : '', evt.git_sha ? String(evt.git_sha).slice(0, 8) : ''] });
if (t === 'deps_sync_ok') {
return view('ok', 'Dependencies in sync', { meta: [evt.reason || '', shortText(evt.source, 60)] });
@ -1029,23 +1031,12 @@ export function summarizeLogEvent(evt) {
});
}
if (t === 'task_checkpoint') {
if (evt.checkpoint_kind === 'context_fit_low_retry') {
return view('warn', 'Context rebuilt in Low mode', {
meta: taskMeta(
evt.model ? compactModel(evt.model) : '',
evt.round ? `r${evt.round}` : '',
'same-model retry',
),
});
}
const cpNum = evt.checkpoint_number || Math.floor((evt.round || 0) / 15);
return view('thinking', `Checkpoint ${cpNum}`, {
meta: taskMeta(
evt.round ? `r${evt.round}` : '',
evt.context_tokens ? `~${evt.context_tokens} tok` : '',
formatLogMoney(evt.task_cost),
),
if (t === 'task_checkpoint' || t === 'context_reclaim') {
const lowRetry = evt.checkpoint_kind === 'context_fit_low_retry';
return view(lowRetry ? 'warn' : 'thinking', taskCheckpointLabel(evt), {
meta: taskMeta(evt.model ? compactModel(evt.model) : '', evt.round ? `r${evt.round}` : '',
lowRetry ? 'same-model retry' : '', evt.context_tokens ? `~${evt.context_tokens} tok` : '',
formatLogMoney(evt.task_cost)),
});
}
@ -1118,16 +1109,7 @@ function chatView({
receipt = false,
toolCall = null,
} = {}) {
const out = {
phase,
headline,
body,
visible,
promote,
terminal,
human,
dedupeKey,
};
const out = { phase, headline, body, visible, promote, terminal, human, dedupeKey };
// A receipt row renders inside a block but is not content the block can
// stand on: the fact it reports lives elsewhere (the owner message's
// routing annotation for an addressing call).
@ -1157,7 +1139,18 @@ export function taskTerminalSummary(evt = {}) {
&& (evt.outcome_final === true || evt.system_type === 'task_summary'
|| taskDoneIsTerminal(evt));
const outcome = taskTerminalPhase(evt);
const presentation = taskPresentation(terminal || outcome === 'error' ? outcome : 'working');
// Terminality and outcome knowledge are two facts (#1110): the lifecycle status
// can settle while post-task finalization still runs. Painting the whole frame
// 'error' put Failed where only a terminal frame may, so the title carried it.
// The stamp (`task_terminal_status`/`status`) is the producer's word; a row
// that carries only the canonical axes still knows its lifecycle, so a
// settled `outcome_axes.lifecycle.status` counts too (as taskOutcomeSeverity
// already reads it) — an unstamped finalizing replay of a Failed root must
// not hide the Failed behind "Finalizing…".
const record = normalizeTaskTerminalRecord(evt);
const knownStatus = String(record?.status || record?.outcome_axes?.lifecycle?.status || '').toLowerCase();
const observedOutcome = OBSERVED_OUTCOME_STATUSES.has(knownStatus) ? outcome : '';
const presentation = taskPresentation(terminal ? outcome : 'working');
const body = [taskStoppedWithSummary(evt) ? OWNER_STOP_DETAIL_MARKER : '', taskReasonDetail(evt)]
.filter(Boolean).join('\n');
const cause = terminal ? taskReasonDetail(evt) : '';
@ -1169,6 +1162,7 @@ export function taskTerminalSummary(evt = {}) {
// A task that ends with a cause states it on the collapsed line (DESIGN, activity block).
...(cause ? { activityPreview: cause } : {}),
}),
...(observedOutcome ? { observedOutcome } : {}),
...(evt.model_execution && typeof evt.model_execution === 'object'
? { modelExecution: evt.model_execution } : {}),
...(Number.isInteger(evt.tool_calls) ? { toolCalls: evt.tool_calls } : {}),
@ -1258,7 +1252,11 @@ function summarizeChatLiveEventView(evt) {
const errorText = describeText(evt.error || '', 220);
const cancelDetail = evt.cancel_origin && (rawEvent === 'cancelled'
|| (rawEvent === 'completed' && taskOutcomeSeverity(evt) === 'cancelled'))
? taskReasonDetail({ status: 'cancelled', cancel_origin: evt.cancel_origin }) : '';
// The child's OWN lineage rides along, so the cancel clause can prove the
// parent relation instead of naming the swept root as an initiator (#1061).
? taskReasonDetail({ status: 'cancelled', cancel_origin: evt.cancel_origin,
task_id: evt.subagent_task_id || evt.task_id,
parent_task_id: evt.parent_task_id, root_task_id: evt.root_task_id }) : '';
const reasonDetail = cancelDetail || (evt.reason_code ? taskReasonPhrase(evt.reason_code) : '');
const detailParts = [
progressText.full,
@ -1409,23 +1407,15 @@ function summarizeChatLiveEventView(evt) {
});
}
if (t === 'task_checkpoint') {
if (evt.checkpoint_kind === 'context_fit_low_retry') {
return chatView({
phase: 'warn',
headline: 'Context rebuilt in Low mode — retrying the same model once',
visible: true,
dedupeKey: key(evt.checkpoint_kind, evt.round || ''),
});
}
// Not visible in chat live card — the emit_progress message is the visible source
// for the chat timeline (avoids duplicate timeline entries). This event remains
// visible in the Logs tab via summarizeLogEvent.
const cpNum = evt.checkpoint_number || Math.floor((evt.round || 0) / 15);
if (t === 'task_checkpoint' || t === 'context_reclaim') {
// Visible in the expanded timeline, with no narration/title promotion.
const lowRetry = evt.checkpoint_kind === 'context_fit_low_retry';
return chatView({
phase: 'thinking',
headline: `Checkpoint ${cpNum} — periodic self-check`,
dedupeKey: key(cpNum),
phase: lowRetry ? 'warn' : 'thinking',
headline: lowRetry ? 'Context rebuilt in Low mode — retrying the same model once'
: taskCheckpointLabel(evt),
visible: !checkpointHasProgressRow(evt),
dedupeKey: key(evt.checkpoint_kind || '', evt.checkpoint_number || evt.round || ''),
});
}

View file

@ -102,7 +102,7 @@ export function createModelWaitController({ getRecord, onDomWrite = (fn) => fn()
// A block without work carries no title placeholder while it waits: the chrome
// follows the work it stands on, never the lane (and no lane is always shown).
const phase = desiredLiveCardPhase(record);
setLiveCardPhase(record, phase.phase, phase.text, phase.className);
setLiveCardPhase(record, phase.phase, phase.text, phase.className, phase.secondary);
}
onChange(taskId, waiting);
}

View file

@ -0,0 +1,36 @@
// Producer census: loop_model_call/round_limits/nudges/budget, task_pacing,
// transcript_prefix, loop_forced_finalization and agent budget-pause events.
export const CHECKPOINT_LABELS = {
context_fit_low_retry: 'Context rebuilt in Low mode',
context_fit_route_rebound: 'Context fit rebound the route',
context_view: 'Context inspected',
context_reclaim_manual: 'Context reclaimed on request',
context_reclaim_automatic: 'Context reclaimed automatically',
prompt_prefix_break: 'Prompt prefix rebuilt',
cost_budget_milestone: 'Cost budget milestone',
cost_budget_wrapup: 'Cost budget wrap-up',
time_budget_milestone: 'Time budget milestone',
intrinsic_pacing: 'Pacing check',
nanny_economics_reminder: 'Economics reminder',
nanny_finalization_nudge: 'Finalization nudge',
budget_scope_paused: 'Budget scope paused',
services_stopped: 'Background services stopped',
services_kept: 'Background services kept',
forced_candidate_drift: 'Forced candidate drifted',
};
export function taskCheckpointLabel(evt = {}) {
const kind = String(evt.checkpoint_kind || '');
if (!kind && Number(evt.checkpoint_number) > 0) {
return `Checkpoint ${evt.checkpoint_number} — periodic self-check`;
}
return Object.hasOwn(CHECKPOINT_LABELS, kind) ? CHECKPOINT_LABELS[kind]
: kind ? `Checkpoint · ${kind}` : 'Task checkpoint';
}
export function checkpointHasProgressRow(evt = {}) {
// These exact producer paths already emit a visible host progress note.
return (!evt.checkpoint_kind && Number(evt.checkpoint_number) > 0)
|| (evt.checkpoint_kind === 'context_reclaim_manual'
&& ['checkpoint_failed', 'summarizer_failed', 'binding_mismatch'].includes(evt.status));
}

View file

@ -19,6 +19,19 @@ export function desiredLiveCardPhase(record = {}, terminalPhase = 'done') {
};
}
if (record.finalizingHold) {
// #1110: when the outcome is already observed, it OWNS the chip and the
// hold states itself beside it. A card whose task had failed used to read
// only "Finalizing…", so the failure had to be smuggled into the title.
const observed = String(record.observedOutcome || '');
if (observed) {
const presentation = taskPresentation(observed);
return {
phase: presentation.phase,
text: presentation.headline,
className: `chat-live-phase ${presentation.phase}`,
secondary: 'Finalizing…',
};
}
return {
phase: 'working',
text: 'Finalizing…',
@ -44,6 +57,7 @@ export function captureLiveCardPhaseState(record = {}) {
return {
phase: String(record?.phaseEl?.dataset?.phase || 'working'),
finalizingHold: Boolean(record?.finalizingHold),
observedOutcome: String(record?.observedOutcome || ''),
};
}
@ -51,17 +65,21 @@ export function restoreLiveCardPhaseState(record, snapshot) {
if (!record || !snapshot || record.finished) return null;
record.cancelPendingPolicy = '';
record.finalizingHold = Boolean(snapshot.finalizingHold);
record.observedOutcome = String(snapshot.observedOutcome || '');
return desiredLiveCardPhase(record, snapshot.phase || 'working');
}
// One writer for the stable factual task/subagent phase chip. Technical
// nonterminal diagnostics stay in the card timeline/details.
export function setLiveCardPhase(record, phase = 'working', text = '', className = '') {
export function setLiveCardPhase(record, phase = 'working', text = '', className = '', secondary = '') {
if (!record?.phaseEl) return false;
const activePhase = String(phase || 'working');
const activeText = String(text || taskPresentation(activePhase).headline);
const activeClassName = className || `chat-live-phase ${activePhase}`;
const activeLabel = `${record.isSubagent ? 'Subagent' : 'Task'} status: ${activeText}`;
const secondaryText = String(secondary || '');
const activeLabel = `${record.isSubagent ? 'Subagent' : 'Task'} status: ${activeText}`
+ (secondaryText ? `, ${secondaryText}` : '');
const secondaryChanged = setLiveCardPhaseSecondary(record, secondaryText);
const phaseEl = record.phaseEl;
const changed = phaseEl.dataset.phase !== activePhase
|| phaseEl.className !== activeClassName
@ -74,7 +92,25 @@ export function setLiveCardPhase(record, phase = 'working', text = '', className
if (phaseEl.getAttribute('aria-live') !== 'polite') phaseEl.setAttribute('aria-live', 'polite');
if (phaseEl.getAttribute('aria-atomic') !== 'true') phaseEl.setAttribute('aria-atomic', 'true');
if (phaseEl.getAttribute('aria-label') !== activeLabel) phaseEl.setAttribute('aria-label', activeLabel);
return setLiveCardTypingVisible(record, !record.finished) || changed;
return setLiveCardTypingVisible(record, !record.finished) || changed || secondaryChanged;
}
// The secondary chip is a SEPARATE fact beside the outcome, never a second
// status word: only the finalization hold writes it, and the primary chip's
// accessible name states both so the pair is read as one status.
export function setLiveCardPhaseSecondary(record, text = '') {
if (!record) return false;
if (record.phaseSecondaryEl === undefined) {
record.phaseSecondaryEl = record.root?.querySelector?.('[data-live-phase-secondary]') || null;
}
const el = record.phaseSecondaryEl;
if (!el) return false;
const next = String(text || '');
const hidden = !next || Boolean(record.phaseEl?.hidden);
if (el.textContent === next && el.hidden === hidden) return false;
el.textContent = next;
el.hidden = hidden;
return Boolean(el.isConnected);
}
// Phase and activity share this one animation writer. A subscription wait
@ -92,6 +128,7 @@ export function setLiveCardTypingVisible(record, visible) {
export function setInertCardPresentation(record, enabled) {
if (!record?.phaseEl) return;
record.phaseEl.hidden = enabled;
setLiveCardPhaseSecondary(record, enabled ? '' : desiredLiveCardPhase(record).secondary);
if (record.root?.dataset) record.root.dataset.inert = enabled ? '1' : '0';
setLiveCardTypingVisible(record, !enabled && !record.finished);
}
@ -103,7 +140,7 @@ export function setHistoricalUnavailable(record, enabled) {
setInertCardPresentation(record, enabled || Boolean(record.reviewAnchor));
if (!enabled && !record.reviewAnchor) {
const desired = desiredLiveCardPhase(record);
setLiveCardPhase(record, desired.phase, desired.text, desired.className);
setLiveCardPhase(record, desired.phase, desired.text, desired.className, desired.secondary);
}
return true;
}

View file

@ -464,6 +464,37 @@ export function joinMarkdownHeadings(text) {
}).join('\n');
}
/**
* One plain-text projection of RECORDED free text before it joins a host cause
* cancellation clause. The Python twin is
* `ouroboros.utils.strip_markdown` followed by a whitespace split/join, and the
* two strip the SAME marker inventory so one stored cause reads the same in the
* browser card and in the host's durable chat row. Line-anchored patterns
* (headings, bullets) only match while the newlines are still there, so
* stripping precedes flattening — exactly the order the Python docstring names.
* The common fixture pins Markdown, empty provenance and sentence punctuation
* through both consumers. `max` of 0 keeps the whole text; any other value
* bounds it by Unicode characters with an ellipsis.
*/
export function plainCauseText(value, max = 160) {
const plain = String(value || '')
.replace(/```[^\n]*\n([\s\S]*?)```/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '$1')
.replace(/(?<![\p{L}\p{N}_])_(.+?)_(?![\p{L}\p{N}_])/gu, '$1')
.replace(/~~(.+?)~~/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/^#{1,6}\s+/gm, '')
.replace(/^[*-]\s+/gm, '• ')
.replace(/\*\*|__|~~|`/g, '')
.replace(/\s+/g, ' ')
.trim();
const chars = Array.from(plain);
return max > 0 && chars.length > max ? `${chars.slice(0, max - 1).join('').trimEnd()}…` : plain;
}
export function renderMarkdown(text, { inlineHeadingBreaks = false } = {}) {
let html = escapeHtmlText(text);
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');

View file

@ -2014,6 +2014,16 @@ body.resizing-panels { user-select: none; }
min-height: 1.35em;
}
/* The secondary status fact beside the outcome chip (a running finalization).
Neutral by rule: the outcome owns the status colour, this only says the task
has not let go yet, so it stays a quiet sentence rather than a second chip. */
.chat-live-phase-secondary {
color: var(--text-meta);
font-size: var(--type-meta);
line-height: var(--line-meta);
white-space: nowrap;
}
.chat-live-meta-text {
display: inline-flex;
align-items: center;
@ -6861,7 +6871,9 @@ textarea.chat-input {
.chat-live-executor-chip {
font-size: var(--type-meta);
padding: 0 6px;
margin-right: 6px;
/* No trailing margin: the meta line now carries a real ' \00b7 ' text part
between the chip and the next fact, so this boundary is spaced by the
same flex gap as every other one. */
vertical-align: middle;
opacity: 0.85;
}

View file

@ -1,35 +1,20 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import { cardMetaKeys } from '../modules/chat_activity.js';
import { summarizeChatLiveEvent, taskReasonDetail, taskTerminalSummary } from '../modules/log_events.js';
test('cancelled root and replay name recorded transport without inventing an owner', () => {
for (const [origin, expected] of [
[{ source: 'http_single' }, 'Stopped from the app (Stop now) · initiator: not recorded'],
[{ source: 'http_cascade', scope: 'cascade' }, 'Stopped from the app (Stop now) · this task and its sub-tasks · initiator: not recorded'],
[{ source: 'http_graceful', requested_by: 'owner' }, 'Stopped from the app (Wrap up) · initiator: owner'],
[{ source: 'agent_tool', requested_by: 'parent' }, 'agent_tool · initiator: parent'],
[{ source: 'owner_restart', requested_by: 'owner' }, 'owner_restart · initiator: owner'],
[{ source: '__proto__' }, '__proto__ · initiator: not recorded'],
]) {
const record = { status: 'cancelled', cancel_origin: origin };
assert.equal(taskTerminalSummary(record).body, expected);
assert.equal(taskReasonDetail({ ...record, status: 'running', task_terminal_status: 'cancelled' }), expected);
test('recorded cancellation: Python/browser parity including absent cause and punctuation', () => {
const cases = JSON.parse(readFileSync(new URL('./fixtures/cancel_cause_parity.json', import.meta.url)));
for (const { record, text } of cases) {
assert.equal(taskReasonDetail(record), text, JSON.stringify(record));
assert.equal(taskTerminalSummary(record).body, text);
}
assert.equal(taskReasonDetail({ status: 'cancelled' }), '');
assert.equal(taskReasonDetail({ status: 'completed', cancel_origin: { source: 'http_single' } }), '');
assert.equal(taskReasonDetail({ status: 'cancelled', cancel_origin: { reason: '🙂'.repeat(200) } }),
'🙂'.repeat(159) + '…');
});
test('a recorded foreign task actor survives the empty parent-decision trigger', () => {
const origin = { source: 'agent_tool', request_origin: { kind: 'agent_task', task_id: 'foreign-root' } };
assert.equal(taskReasonDetail({ status: 'cancelled', cancel_origin: origin }), 'agent_tool · initiator: foreign-root');
assert.equal(taskReasonDetail({ status: 'cancelled', cancel_origin: {
source: 'http_single', request_origin: { kind: 'http_client', source: 'http_single' },
} }), 'Stopped from the app (Stop now) · initiator: not recorded');
});
test('the child wire carry and child summarizer keep cancellation visible and partial work inspectable', () => {
test('child live and replay use genuine lineage and keep saved work inspectable', () => {
const origin = { source: 'cascade_descendant', requested_by: 'root' };
const carried = cardMetaKeys({ cancel_origin: origin });
assert.deepEqual(carried.cancel_origin, origin);
@ -43,10 +28,10 @@ test('the child wire carry and child summarizer keep cancellation visible and pa
...carried, ...frame,
});
assert.equal(view.phase, 'cancelled');
assert.equal(view.body, 'cascade_descendant · initiator: root');
assert.equal(view.body, 'Stopped with the task tree it belongs to · Stopped with its parent task.');
assert.equal(view.activityPreview, view.body);
assert.match(view.fullBody, /Saved partial work/);
assert.match(view.fullBody, /cascade_descendant · initiator: root/);
assert.doesNotMatch(view.body, /initiator:|root/);
}
});
@ -58,7 +43,7 @@ test('retained origin does not replace a non-cancelled child frame', () => {
result: 'Saved partial work', error: subagent_event === 'failed' ? 'Worker failed' : '',
status: 'cancelled', cancel_origin: { source: 'http_single' },
});
assert.doesNotMatch(view.body, /initiator:/);
assert.doesNotMatch(view.body, /Stopped from/);
assert.equal(view.phase, subagent_event === 'failed' ? 'error' : 'working');
}
});

View file

@ -766,3 +766,25 @@ test('a child card folds its own tool calls live and takes no at-rest evidence r
assert.doesNotMatch(folded()[0].innerHTML, /5 tool calls/);
} finally { f.close(); }
});
test('#931 a typed checkpoint is a real expanded row without stealing narration', () => {
const f = fixture();
try {
f.census(managed());
f.emit('chat', { task_id: TASK, role: 'assistant', is_progress: true,
narration: true, content: 'Reading the source' });
const card = f.card();
const title = card.querySelector('[data-live-title]').textContent;
f.log({ type: 'task_checkpoint', checkpoint_kind: 'context_view', round: 3 });
if (card.dataset.expanded !== '1') {
card.querySelector('[data-live-summary-button]').listeners.get('click')[0]({ detail: 0 });
}
assert.ok(f.rows().some((row) => row.innerHTML.includes('Context inspected')));
assert.equal(card.querySelector('[data-live-title]').textContent, title);
f.log({ type: 'task_checkpoint', checkpoint_kind: 'context_view', round: 3 });
assert.equal(f.rows().filter((row) => row.innerHTML.includes('Context inspected')).length, 1);
const count = f.rows().length;
f.log({ type: 'worker_starting', worker_id: 0 });
assert.equal(f.rows().length, count);
} finally { f.close(); }
});

View file

@ -165,9 +165,17 @@ test('durable detail terminality is narrow and outcome labels stay unchanged', (
assert.equal(isTerminalTaskDetail(record), false, `${status}/${post_task_synthesis}`);
assert.equal(taskDoneIsTerminal(record), false);
assert.equal(summarizeChatLiveEvent(record).terminal, false);
const headline = status === 'failed' ? 'Failed' : 'Working';
assert.equal(summarizeChatLiveEvent(record).headline, headline);
assert.equal(summarizeLogEvent(record).headline, headline);
// #1110: an unfinished frame paints Working, and the KNOWN outcome
// rides beside it as its own fact — the card states it in the chip
// with `Finalizing…` next to it, instead of the whole frame having
// to claim `Failed` before the run is terminal.
assert.equal(summarizeChatLiveEvent(record).headline, 'Working');
assert.equal(summarizeChatLiveEvent(record).observedOutcome,
status === 'failed' ? 'error' : 'done');
// Logs is an event ledger, not a card chip: its row keeps the
// outcome word this frame carries.
assert.equal(summarizeLogEvent(record).headline,
status === 'failed' ? 'Failed' : 'Working');
}
}
assert.equal(isTerminalTaskDetail({

View file

@ -8,7 +8,7 @@ import {
taskCostMeta,
taskCostProjection,
} from '../modules/chat.js';
import { renderLiveCardMeta } from '../modules/chat_activity.js';
import { costPresentationMeta, renderLiveCardMeta } from '../modules/chat_activity.js';
import { costBucketPresentation, costDashboardPresentation } from '../modules/costs.js';
import { summarizeLogEvent } from '../modules/log_events.js';
import {
@ -109,13 +109,13 @@ test('unknown zero-dollar accounting stays pending instead of becoming free', ()
cost_final: false,
cost_with_children_partial: true,
unknown_unmetered: 1,
}), ['cost pending']);
}), ['Cost unknown']);
assert.deepEqual(taskCostMeta({
cost_usd: 0,
cost_accounting_status: 'available',
cost_final: false,
unknown_unmetered: 1,
}), ['cost pending']);
}), ['Cost unknown']);
});
test('a bare per-round cost_usd delta is NOT task cost (v6.82 P1)', () => {
@ -386,3 +386,143 @@ test('llm round rows show money from BOTH the honest backfill name and the live
});
assert.ok(diverged.meta.includes('$0.9000'), JSON.stringify(diverged.meta));
});
test('#498 the scoped carrier answers the whole money question from one bucket', () => {
const carrier = (over = {}) => ({
scope: 'own', tracked_amount: 1.2, has_unpriced: false,
tracked_final: true, accounting_open: false, has_rows: true, ...over,
});
// A proven zero: priced rows evidenced it and the accounting is closed.
assert.deepEqual(costPresentationMeta(carrier({ tracked_amount: 0 })), ['$0.00']);
// An open subtotal is a ceiling, not a receipt.
assert.deepEqual(costPresentationMeta(carrier({ tracked_final: false, accounting_open: true })),
['up to $1.20']);
// An estimated $0.00 is open, so it never reads as a settled free result.
assert.deepEqual(costPresentationMeta(carrier({ tracked_amount: 0, tracked_final: false, accounting_open: true })),
['up to $0.00']);
// Nothing priced anything: an empty ledger and an all-unpriced one both say unknown.
assert.deepEqual(costPresentationMeta(carrier({ tracked_amount: null, tracked_final: false, accounting_open: true })),
['Cost unknown']);
assert.deepEqual(costPresentationMeta(carrier({
tracked_amount: null, has_unpriced: true, tracked_final: false, accounting_open: true,
})), ['Cost unknown']);
// Mixed: the tracked number is a subtotal, and the reason is stated in WORDS
// beside it rather than hidden in a tooltip.
assert.deepEqual(costPresentationMeta(carrier({ has_unpriced: true })),
['Tracked: $1.20', 'some steps have no price']);
assert.deepEqual(costPresentationMeta(carrier({ tracked_amount: 0, has_unpriced: true })),
['Tracked: $0.00', 'some steps have no price']);
// No accountable row at all is not a free result and not a money line.
assert.deepEqual(costPresentationMeta(carrier({ has_rows: false, tracked_amount: null })), []);
// No carrier at all defers to the legacy derivation.
assert.equal(costPresentationMeta(undefined), null);
assert.equal(costPresentationMeta(null), null);
});
test('#498 a card with a carrier uses it; legacy frames keep the old derivation', () => {
// The root's carrier describes the SUBTREE, so a null subtree amount stays
// unknown instead of falling back to the root's own (often zero) number.
assert.deepEqual(taskCostMeta({
cost_accounting_status: 'available',
accounted_upper_bound_usd: 0,
cost_final: true,
cost_presentation: {
scope: 'root_tree', tracked_amount: null, has_unpriced: true,
tracked_final: false, accounting_open: true, has_rows: true,
},
}), ['Cost unknown']);
// An unreadable ledger still outranks the carrier.
assert.deepEqual(taskCostMeta({
cost_accounting_status: 'unavailable', cost_presentation: null,
}), ['cost unavailable']);
// A carrier alone is task-scope accounting evidence.
assert.deepEqual(taskCostMeta({
cost_presentation: {
scope: 'own', tracked_amount: 4.25, has_unpriced: false,
tracked_final: true, accounting_open: false, has_rows: true,
},
}), ['$4.25']);
// Legacy frames (no carrier) keep their existing, deliberately conservative reading.
assert.deepEqual(taskCostMeta({
cost_usd: 4.25, cost_accounting_status: 'available', cost_final: true,
}), ['$4.25']);
// A late receipt just changes the carrier — nothing is sticky in the projection.
const open = taskCostProjection({
cost_presentation: {
scope: 'own', tracked_amount: 1.2, has_unpriced: false,
tracked_final: false, accounting_open: true, has_rows: true,
},
}, '2026-07-29T00:00:00Z');
const settled = taskCostProjection({
cost_presentation: {
scope: 'own', tracked_amount: 1.35, has_unpriced: false,
tracked_final: true, accounting_open: false, has_rows: true,
},
}, '2026-07-29T00:01:00Z');
assert.equal(open.final, false);
assert.equal(settled.final, true);
assert.deepEqual(mergeStickyCostMeta(open, settled).meta, ['$1.35']);
assert.deepEqual(mergeStickyCostMeta(settled, open).meta, ['$1.35']);
});
test('#498 mixed exact subtotal is not sticky final while prices are unknown', () => {
const frame = (amount, final, open = false) => ({ cost_presentation: {
scope: 'root_tree', tracked_amount: amount, tracked_final: final,
accounting_open: open, has_unpriced: true, has_rows: true,
} });
const exactSubtotal = taskCostProjection(frame(0, true), '2026-09-23T00:00:00Z');
const lateEstimatedPrice = taskCostProjection(frame(1.2, false, true), '2026-09-23T00:01:00Z');
assert.equal(exactSubtotal.final, false);
assert.deepEqual(mergeStickyCostMeta(exactSubtotal, lateEstimatedPrice).meta,
['Tracked: up to $1.20', 'some steps have no price']);
assert.deepEqual(mergeStickyCostMeta(lateEstimatedPrice, exactSubtotal).meta,
['Tracked: up to $1.20', 'some steps have no price']);
const ownZero = taskCostProjection({ cost_presentation: {
scope: 'own', tracked_amount: 0, tracked_final: true,
accounting_open: false, has_unpriced: false, has_rows: true,
} }, '2026-09-23T00:02:00Z');
assert.deepEqual(mergeStickyCostMeta(lateEstimatedPrice, ownZero).meta, lateEstimatedPrice.meta);
});
test('#498 legacy null tree is never replaced by own zero; empty damaged ledger stays unknown', () => {
assert.deepEqual(taskCostMeta({ accounted_upper_bound_usd_with_children: null,
accounted_upper_bound_usd: 0, cost_final: true }), ['Cost unknown']);
assert.deepEqual(costPresentationMeta({ scope: 'root_tree', tracked_amount: null,
has_rows: false, has_unpriced: false, tracked_final: false, accounting_open: true }), ['Cost unknown']);
});
test('#498 exact live subtotal cannot freeze a still-running root heartbeat', () => {
const exact = taskCostProjection({ cost_final: false, cost_with_children_partial: true,
cost_presentation: { scope: 'root_tree', tracked_amount: 0, tracked_final: true,
accounting_open: false, has_unpriced: false, has_rows: true } }, '2026-09-23T00:00:00Z');
const next = taskCostProjection({ cost_final: false, cost_presentation: {
scope: 'root_tree', tracked_amount: 0.3, tracked_final: false,
accounting_open: true, has_unpriced: true, has_rows: true } }, '2026-09-23T00:01:00Z');
assert.equal(exact.final, false);
assert.deepEqual(mergeStickyCostMeta(exact, next).meta, ['Tracked: up to $0.30', 'some steps have no price']);
// A null carrier beside a readable ledger is an UNKNOWN amount (owner
// vocabulary), never the unreadable-ledger phrase.
assert.deepEqual(taskCostMeta({ accounted_upper_bound_usd: 0, cost_final: true,
cost_presentation: null }), ['Cost unknown']);
});
test('#498 legacy unknown price never becomes sticky-final', () => {
const previous = taskCostProjection({ cost_final: true, accounted_upper_bound_usd: null,
unknown_unmetered: 1 }, '2026-09-23T00:00:00Z');
assert.equal(previous.final, false);
const receipt = taskCostProjection({ cost_final: false, accounted_upper_bound_usd: 0.35 },
'2026-09-23T00:01:00Z');
assert.deepEqual(mergeStickyCostMeta(previous, receipt).meta, ['up to $0.35']);
});
test('unknown legacy descendant rollup cannot retain a narrower own zero', () => {
const own = taskCostProjection({cost_final:true, cost_presentation:{scope:'own', tracked_amount:0,
tracked_final:true, accounting_open:false, has_rows:true, has_unpriced:false}}, '2026-09-24T00:00:00Z');
const rollup = taskCostProjection({cost_presentation:null, accounted_upper_bound_usd_with_children:null}, '2026-09-24T00:01:00Z');
assert.deepEqual(mergeStickyCostMeta(own, rollup).meta, ['Cost unknown']);
assert.deepEqual(mergeStickyCostMeta(rollup, own).meta, ['Cost unknown']);
// The unreadable-ledger phrase is reserved for the status flag.
assert.deepEqual(taskCostMeta({ cost_accounting_status: 'unavailable', cost_presentation: null }),
['cost unavailable']);
});

View file

@ -0,0 +1,16 @@
[
{"record":{"status":"cancelled","cancel_origin":{}},"text":""},
{"record":{"status":"cancelled"},"text":""},
{"record":{"status":"completed","cancel_origin":{"source":"http_single"}},"text":""},
{"record":{"status":"cancelled","cancel_origin":{"source":"http_single"}},"text":"Stopped from the app (Stop now)"},
{"record":{"status":"cancelled","cancel_origin":{"source":"http_cascade","scope":"cascade"}},"text":"Stopped from the app (Stop now) · this task and its sub-tasks."},
{"record":{"status":"cancelled","cancel_origin":{"source":"http_graceful","requested_by":"owner"}},"text":"Stopped from the app (Wrap up)"},
{"record":{"status":"cancelled","cancel_origin":{"source":"agent_tool","reason":"**Stop**\n_now_."}},"text":"agent_tool · Stop now."},
{"record":{"status":"cancelled","cancel_origin":{"source":"__proto__"}},"text":"__proto__."},
{"record":{"task_id":"child","parent_task_id":"root","root_task_id":"root","status":"cancelled","cancel_origin":{"source":"cascade_descendant","requested_by":"root"}},"text":"Stopped with the task tree it belongs to · Stopped with its parent task."},
{"record":{"task_id":"grandchild","parent_task_id":"child","root_task_id":"root","status":"cancelled","cancel_origin":{"source":"cascade_descendant","requested_by":"root"}},"text":"Stopped with the task tree it belongs to · Stopped with an ancestor task."},
{"record":{"task_id":"root","root_task_id":"root","status":"cancelled","cancel_origin":{"source":"cascade_descendant","requested_by":"root"}},"text":"Stopped with the task tree it belongs to."},
{"record":{"task_id":"unrelated","parent_task_id":"other","status":"cancelled","cancel_origin":{"source":"cascade_descendant","requested_by":"root"}},"text":"Stopped with the task tree it belongs to."},
{"record":{"status":"cancelled","cancel_origin":{"source":"agent_tool","request_origin":{"kind":"agent_task","task_id":"foreign-root"}}},"text":"agent_tool · Requested by a task."},
{"record":{"status":"cancelled","cancel_origin":{"source":"agent_tool","reason":"Keep saved work!"}},"text":"agent_tool · Keep saved work!"}
]

View file

@ -183,7 +183,7 @@
},
"phase": "error",
"headline": "Failed",
"acceptance_clause": "provider_unavailable · Some delegated work was never reconciled."
"acceptance_clause": "The model provider stopped answering, so the task could not finish · Some delegated work was never reconciled."
},
{
"name": "a healed custody debt names the current execution cause alone",
@ -195,7 +195,7 @@
},
"phase": "error",
"headline": "Failed",
"acceptance_clause": "provider_unavailable."
"acceptance_clause": "The model provider stopped answering, so the task could not finish."
},
{
"name": "a record carrying no debt list states nothing about the debt",
@ -206,7 +206,7 @@
},
"phase": "error",
"headline": "Failed",
"acceptance_clause": "provider_unavailable."
"acceptance_clause": "The model provider stopped answering, so the task could not finish."
},
{
"name": "acceptance reason author_finish says its own sentence",

View file

@ -9,9 +9,12 @@ import { createLoginCardController, loginCardHtml } from '../modules/harness_log
import {
GENERIC_HARNESS_MARK,
HARNESS_MARKS,
META_PART_SEPARATOR,
executorIdentityMarkup,
harnessAccountIdentityMarkup,
harnessIdentityMarkup,
harnessPresentation,
joinMetaParts,
} from '../modules/harness_presentation.js';
import {
ROUTE_KIND_AGENT_SESSION,
@ -295,3 +298,33 @@ test('Chat, Logs, onboarding, and reviewer lanes consume the same mark owner', (
assert.doesNotMatch(events, /HARNESS_CHIP_(?:ICON|NAME)/);
assert.doesNotMatch(events, /['"](?:◇|✳|▸|◆)['"]/);
});
test('#516 the card meta line separates its parts with real text, not a CSS gap', () => {
const chip = { harness: 'claudexor', label: 'Claudexor', title: 'route', observedModels: ['opus'] };
const markup = executorIdentityMarkup(chip, { agentModel: 'sonnet' });
// Chip, coordinator model and observed models are three parts of one line,
// and every boundary between them carries the separator itself — a copied
// line and a screen reader get it, which a flex gap never provided.
assert.equal([...markup.matchAll(/ · /g)].length, 2);
assert.match(markup, /<\/span> · <span class="chat-live-meta-text">Coordinator: sonnet<\/span> · /);
// A missing part takes its separator with it: no leading or doubled dots.
assert.equal(executorIdentityMarkup(null, { agentModel: 'sonnet' }),
'<span class="chat-live-meta-text">Agent model: sonnet</span>');
assert.equal(executorIdentityMarkup(null, {}), '');
assert.equal(joinMetaParts(['a', '', 'b']), `a${META_PART_SEPARATOR}b`);
assert.equal(META_PART_SEPARATOR, ' · ');
// The renderer joins the executor block with the rest through that same rule,
// instead of concatenating it onto the first fact.
const activitySource = readFileSync(new URL('../modules/chat_activity.js', import.meta.url), 'utf8');
const renderer = activitySource.slice(
activitySource.indexOf('export function renderLiveCardMeta'),
activitySource.indexOf('export function ownLiveActionsEl'),
);
assert.match(renderer, /joinMetaParts\(\[\s*executorIdentityMarkup\(/);
assert.doesNotMatch(renderer, /\.join\(' · '\)/);
// The visual-only spacing that stood in for the separator is gone.
const css = readFileSync(new URL('../style.css', import.meta.url), 'utf8');
const chipRule = css.slice(css.indexOf('.chat-live-executor-chip {'), css.indexOf('.chat-live-executor-chip {') + 400);
assert.doesNotMatch(chipRule, /margin-right/);
});

View file

@ -0,0 +1,99 @@
// Typed host-lifecycle rows: a checkpoint states WHICH checkpoint it is (#931)
// and the worker readiness frames state the fields they carry (#1073). Both are
// projections of already-typed producer facts — no name substrings, no taxonomy.
import assert from 'node:assert/strict';
import test from 'node:test';
import {
summarizeChatLiveEvent,
summarizeLogEvent,
taskCheckpointLabel,
} from '../modules/log_events.js';
test('#931 every typed checkpoint kind names itself; only a kindless one is periodic', () => {
// One family per producer module, so a kind that loses its label shows up here.
for (const [kind, label] of [
['context_fit_low_retry', 'Context rebuilt in Low mode'],
['context_fit_route_rebound', 'Context fit rebound the route'],
['context_reclaim_manual', 'Context reclaimed on request'],
['context_reclaim_automatic', 'Context reclaimed automatically'],
['context_view', 'Context inspected'],
['prompt_prefix_break', 'Prompt prefix rebuilt'],
['cost_budget_milestone', 'Cost budget milestone'],
['cost_budget_wrapup', 'Cost budget wrap-up'],
['time_budget_milestone', 'Time budget milestone'],
['intrinsic_pacing', 'Pacing check'],
['nanny_economics_reminder', 'Economics reminder'],
['nanny_finalization_nudge', 'Finalization nudge'],
['budget_scope_paused', 'Budget scope paused'],
['services_stopped', 'Background services stopped'],
['services_kept', 'Background services kept'],
['forced_candidate_drift', 'Forced candidate drifted'],
]) {
assert.equal(taskCheckpointLabel({ checkpoint_kind: kind, round: 30 }), label);
assert.equal(summarizeLogEvent({ type: 'task_checkpoint', checkpoint_kind: kind, round: 30 }).headline, label);
}
// An unknown kind names ITSELF rather than borrowing the periodic sentence.
assert.equal(taskCheckpointLabel({ checkpoint_kind: 'brand_new_kind', round: 30 }),
'Checkpoint · brand_new_kind');
// Only a checkpoint with no kind at all is the periodic round self-check.
assert.equal(taskCheckpointLabel({ round: 30 }), 'Task checkpoint');
assert.equal(taskCheckpointLabel({ checkpoint_number: 7 }), 'Checkpoint 7 — periodic self-check');
});
test('#931 typed checkpoints are task facts in the expanded card, never narration', () => {
for (const kind of ['context_view', 'budget_scope_paused', 'brand_new_kind', '']) {
const view = summarizeChatLiveEvent({ type: 'task_checkpoint', task_id: 't1', checkpoint_kind: kind, round: 30 });
// Not promoted: the title and the collapsed activity line stay the turn's.
assert.equal(view.promote, false);
assert.equal(view.human, false);
assert.equal(view.terminal, false);
// No toast: the typed incident seam is the only one that fires one.
assert.equal('receipt' in view, false);
assert.equal(view.visible, true);
}
// The one owner-visible exception keeps its full sentence and its warning.
const lowRetry = summarizeChatLiveEvent({
type: 'task_checkpoint', task_id: 't1', checkpoint_kind: 'context_fit_low_retry', round: 4,
});
assert.equal(lowRetry.visible, true);
assert.equal(lowRetry.phase, 'warn');
assert.equal(lowRetry.headline, 'Context rebuilt in Low mode — retrying the same model once');
// Distinct kinds on the same round are distinct rows, so one never hides another.
const keyOf = (kind) => summarizeChatLiveEvent({
type: 'task_checkpoint', task_id: 't1', checkpoint_kind: kind, round: 4,
}).dedupeKey;
assert.notEqual(keyOf('context_view'), keyOf('budget_scope_paused'));
assert.equal(keyOf('context_view'), keyOf('context_view'));
assert.equal(summarizeChatLiveEvent({ type: 'task_checkpoint', checkpoint_number: 2, round: 30 }).visible, false);
assert.equal(summarizeChatLiveEvent({ type: 'task_checkpoint', checkpoint_kind: 'context_reclaim_manual', status: 'summarizer_failed' }).visible, false);
assert.equal(summarizeChatLiveEvent({ type: 'context_reclaim', checkpoint_kind: 'context_reclaim_automatic' }).visible, true);
});
test('#1073 the worker readiness frames render their own fields in Logs', () => {
const starting = summarizeLogEvent({
type: 'worker_starting', worker_id: 0, pid: 5001, phase: 'entry',
});
assert.equal(starting.headline, 'Worker starting');
assert.deepEqual(starting.meta, ['worker 0', 'pid 5001', 'phase entry']);
// worker_id 0 is a real slot, not an absent one.
assert.match(String(starting.meta[0]), /worker 0/);
const extended = summarizeLogEvent({
type: 'worker_ready_window_extended', attempt: 2, worker_ids: [0, 3],
window_sec: 45.0, ceiling_sec: 120.0,
});
assert.equal(extended.phase, 'warn');
assert.equal(extended.headline, 'Worker readiness window extended');
assert.deepEqual(extended.meta, ['workers 0, 3', 'attempt 2', 'window 45s', 'ceiling 120s']);
// A frame missing its optional fields drops them instead of printing blanks.
assert.deepEqual(summarizeLogEvent({ type: 'worker_starting' }).meta, []);
assert.deepEqual(summarizeLogEvent({ type: 'worker_ready_window_extended', worker_ids: [] }).meta, []);
// The frames that already had rows keep them.
assert.equal(summarizeLogEvent({ type: 'worker_boot', pid: 12 }).headline, 'Worker booted');
assert.equal(summarizeLogEvent({ type: 'worker_spawn_start', count: 4 }).headline, 'Spawning 4 workers');
for (const type of ['worker_starting', 'worker_ready_window_extended', 'worker_boot', 'worker_spawn_start']) {
assert.equal(summarizeChatLiveEvent({ type }).visible, false);
}
});

View file

@ -265,7 +265,7 @@ test('a real debt beside a real execution cause is one card line', () => {
reason_code: 'delegated_custody_unreconciled',
delegated_runs_unreconciled: ['run-a1', 'run-b2'],
outcome_axes: { execution: { status: 'failed', reason_code: 'provider_unavailable' } },
}), 'provider_unavailable · Some delegated work was never reconciled.');
}), 'The model provider stopped answering, so the task could not finish · Some delegated work was never reconciled.');
});
// A LIVE task_done event carries the row's own debt list too
@ -288,7 +288,7 @@ test('a record carrying no debt list states nothing about the debt', () => {
status: 'failed',
reason_code: 'delegated_custody_unreconciled',
outcome_axes: { execution: { status: 'failed', reason_code: 'provider_unavailable' } },
}), 'provider_unavailable');
}), 'The model provider stopped answering, so the task could not finish');
assert.equal(taskReasonDetail({
status: 'completed',
reason_code: 'delegated_custody_unreconciled',

View file

@ -16,6 +16,7 @@ import {
desiredLiveCardPhase,
replayTerminalPhase,
restoreLiveCardPhaseState,
setInertCardPresentation,
setLiveCardPhase,
} from '../modules/task_phase_chip.js';
@ -187,9 +188,13 @@ test('interrupted child stays retryable with a Working chip and inspectable deta
chatSource.indexOf('function applyLiveCardStateMutation'),
chatSource.indexOf('function finishLiveCard'),
);
assert.match(applyState, /const desiredPhase = desiredLiveCardPhase\(record, activePhase\);/);
assert.match(applyState, /setLiveCardPhase\(record, desiredPhase\.phase, desiredPhase\.text, desiredPhase\.className\);/);
// #1110: an unfinished card's chip comes from the record's own state, so the
// terminal phase is passed only when the card is finished.
assert.match(applyState, /desiredLiveCardPhase\(record, record\.finished \? summary\.phase \|\| 'done' : ''\)/);
assert.match(applyState, /setLiveCardPhase\(record, desiredPhase\.phase, desiredPhase\.text, desiredPhase\.className,\s*desiredPhase\.secondary\)/);
assert.equal([...applyState.matchAll(/setLiveCardPhase\(/g)].length, 1);
// The Failed-into-the-title workaround is gone: the name stays stable.
assert.doesNotMatch(chatSource, /failedHeadline/);
});
test('desired phase-chip precedence keeps stop/finalizing state sticky', () => {
@ -214,6 +219,81 @@ test('desired phase-chip precedence keeps stop/finalizing state sticky', () => {
});
});
test('#1110 an observed outcome owns the chip while Finalizing states itself beside it', () => {
// The outcome is known (the lifecycle status settled) while post-task work
// still runs: the card says Failed and adds the hold as a SECOND fact, so
// the title never has to carry the failure.
assert.deepEqual(desiredLiveCardPhase({
finished: false, finalizingHold: true, observedOutcome: 'error',
}, 'working'), {
phase: 'error', text: 'Failed', className: 'chat-live-phase error',
secondary: 'Finalizing…',
});
// An owner stop outranks the hold entirely — no secondary, no outcome chip.
assert.deepEqual(desiredLiveCardPhase({
finished: false, cancelPendingPolicy: 'immediate', finalizingHold: true,
observedOutcome: 'error',
}, 'working'), {
phase: 'working', text: 'Cancelling…', className: 'chat-live-phase working cancelling',
});
// A hold with no observed outcome keeps the plain Finalizing chip.
assert.equal('secondary' in desiredLiveCardPhase({
finished: false, finalizingHold: true,
}, 'working'), false);
// The observed outcome comes from a task-scope frame whose own lifecycle
// status settled — NEVER from a failed tool call, which is diagnostics.
assert.equal(taskTerminalSummary({
status: 'failed', task_phase: 'finalizing',
}).observedOutcome, 'error');
assert.equal(taskTerminalSummary({ status: 'failed', task_phase: 'finalizing' }).terminal, false);
// R1 (#1110): a finalizing REPLAY row often carries no stamp — history merges
// the result's canonical axes, not `task_terminal_status`. The settled
// lifecycle inside those axes is the same knowledge: the Failed is painted,
// the hold stays secondary. A still-running lifecycle stays unobserved.
const unstampedFailed = taskTerminalSummary({
task_phase: 'finalizing', outcome_final: false, outcome_phase: 'error',
outcome_axes: { lifecycle: { status: 'failed' }, execution: { status: 'infra_failed', reason_code: 'provider_unavailable' } },
});
assert.equal(unstampedFailed.observedOutcome, 'error');
assert.equal(unstampedFailed.terminal, false);
assert.equal(taskTerminalSummary({
task_phase: 'finalizing', outcome_axes: { lifecycle: { status: 'running' } },
}).observedOutcome, undefined);
// While unfinished the frame itself paints Working: the chip is the record's.
assert.equal(taskTerminalSummary({ status: 'failed', task_phase: 'finalizing' }).phase, 'working');
assert.equal(taskTerminalSummary({ status: 'running' }).observedOutcome, undefined);
assert.equal(summarizeChatLiveEvent({
type: 'tool_call_finished', task_id: 't1', is_error: true, tool: 'bash',
}).observedOutcome, undefined);
// Recycling a card slot drops the outcome with the rest of the cycle state.
assert.match(activitySource, /record\.observedOutcome = '';/);
assert.match(chatSource, /record\.observedOutcome = summary\.observedOutcome;/);
});
test('#1110 the secondary chip is a separate element with its own accessible name', () => {
const record = {
isSubagent: false,
phaseEl: {
dataset: {}, className: '', textContent: '',
attrs: {},
getAttribute(name) { return this.attrs[name] ?? null; },
setAttribute(name, value) { this.attrs[name] = value; },
},
phaseSecondaryEl: { textContent: '', hidden: true, isConnected: true },
};
setLiveCardPhase(record, 'error', 'Failed', 'chat-live-phase error', 'Finalizing…');
assert.equal(record.phaseEl.textContent, 'Failed');
assert.equal(record.phaseSecondaryEl.textContent, 'Finalizing…');
assert.equal(record.phaseSecondaryEl.hidden, false);
assert.equal(record.phaseEl.getAttribute('aria-label'), 'Task status: Failed, Finalizing…');
setLiveCardPhase(record, 'error', 'Failed', 'chat-live-phase error');
assert.equal(record.phaseSecondaryEl.textContent, '');
assert.equal(record.phaseSecondaryEl.hidden, true);
assert.equal(record.phaseEl.getAttribute('aria-label'), 'Task status: Failed');
});
test('failed optimistic stop restores the finalizing fact, not only its DOM text', () => {
const record = {
finished: false,
@ -426,3 +506,43 @@ test('a review-caused warning names the acceptance decision on the card and in L
assert.ok(replay.meta.includes('review degraded'));
assert.ok(replay.meta.includes('acceptance finalized_unaccepted'));
});
test('#1110 inert history cannot retain a floating Finalizing chip', () => {
const record = {
finalizingHold: true, observedOutcome: 'error', finished: false,
phaseEl: { hidden: false, dataset: {}, attrs: {}, textContent: '', className: '',
getAttribute(key) { return this.attrs[key]; }, setAttribute(key, value) { this.attrs[key] = value; } },
phaseSecondaryEl: { hidden: true, textContent: '', isConnected: true },
};
setLiveCardPhase(record, 'error', 'Failed', '', 'Finalizing…');
assert.equal(record.phaseSecondaryEl.hidden, false);
setInertCardPresentation(record, true);
setLiveCardPhase(record, 'error', 'Failed', '', 'Finalizing…');
assert.equal(record.phaseEl.hidden, true);
assert.equal(record.phaseSecondaryEl.hidden, true);
setInertCardPresentation(record, false);
assert.equal(record.phaseSecondaryEl.hidden, false);
});
test('ordinary early-final handler consumes producer status without ending lifecycle', () => {
const body = chatSource.slice(chatSource.indexOf(' function markLiveCardFinalizing('),
chatSource.indexOf(' // Durable cancel state wins', chatSource.indexOf(' function markLiveCardFinalizing(')));
for (const [fact, expected] of [
[{ task_terminal_status: 'failed' }, 'error'],
[{ task_terminal_status: 'completed', outcome_axes: { execution: { status: 'degraded' } } }, 'warn'],
[{}, 'working'],
[{ type: 'tool_error', status: 'running' }, 'working'],
]) {
const record = { phaseEl: {}, title: 'Stable task name', finished: false };
const handler = new Function('liveCardRecords', 'taskKey', 'withStableViewport',
'markReviewAnchor', 'taskTerminalSummary', 'desiredLiveCardPhase', 'setLiveCardPhase',
`${body}; return markLiveCardFinalizing;`)(new Map([['t', record]]), x => x, fn => fn(),
() => false, taskTerminalSummary, desiredLiveCardPhase, () => true);
handler('t', { ...fact, task_phase: 'finalizing' });
assert.equal(desiredLiveCardPhase(record).phase, expected);
assert.equal(record.finalizingHold, true);
assert.equal(record.finished, false);
assert.equal(record.title, 'Stable task name');
}
});