Durable mutation outcomes
Durable mutation outcomes
Section titled “Durable mutation outcomes”mutation_outcome/v1 publishes what happened to each protected write
independently of assistant narration. Records are derived on read from the
session’s mutation ledger; there is no second store. Replay
refusal, identity and settlement come only from the ledger, so history, live
events and replay can never disagree with what dispatch enforces.
Record
Section titled “Record”{ "schema": "mutation_outcome/v1", "logical_turn_id": "…", "mutation_id": "…", "tool": "mcp__dashboards__create", "status": "completed", "resource_id": "server-confirmed-id", "url": "https://app.example.com/d/server-confirmed-id", "title": "Service overview"}| Ledger status | Published status |
|---|---|
completed |
completed |
failed |
failed |
outcome_unknown, execution_started |
outcome_unknown |
approved after recorded intent (cancelled before the router, or reconciled with completed=False) |
not_executed |
approved, never started |
not published: the router was never entered |
resource_id, url and title come only from host metadata attached with
Engine.attach_mutation_metadata (id, url, title), never from tool output.
They are null until the host attaches them.
resource_idis published unchanged or not at all: values over 256 UTF-8 bytes, with control characters, or resembling credentials are withheld.urlkeeps onlyhttp(s)scheme, host and path; credentials, query and fragment are dropped. URLs over 2048 bytes are withheld.titledrops control characters, redacts credential patterns, is truncated to 512 UTF-8 bytes, and is withheld if it is (or truncates to) structured JSON.- A serialized record never exceeds 16 KiB.
docs/contracts/mutation_outcome_v1.json holds the limits and ledger-entry →
record examples. Consumers should validate against it.
Starting in 0.23.30, a settled timeout may include an optional reason:
pre_dispatch_deadline with not_executed, or execution_timeout with
outcome_unknown. This is projected from the ledger, never inferred from error
text. A fresh attempt or authoritative reconciliation clears the old reason.
Older records and other failures omit it. Approval expiry precedes mutation
intent and remains an approval/tool event (approval_outcome: timed_out).
Session history
Section titled “Session history”SessionManager.mutation_outcomes(session_key) returns every retained outcome,
oldest logical turn first. The UI API exposes the same list:
GET /api/mutation-outcomes?session_id=<key>→ {"mutation_outcomes": [ … ]}History follows ledger retention: it survives restart, is hidden when the conversation expires, and is deleted with the session.
Events
Section titled “Events”After an outcome is durably settled, the engine calls
Adapter.on_mutation_outcome(outcome, mutation_revision=n). The API adapter emits
it as a mutation_outcome run event. Refusals of a recorded mutation emit nothing:
a redelivered request yields no new outcome. Delivery is best effort; adapter
errors never change settlement.
mutation_revision is the replay cursor. Each logical turn keeps a durable
revision counter in the ledger; every change a consumer can see (recorded intent,
settlement, reconciliation, host metadata) stores the next revision on its entry.
An entry is replayed under the revision of its latest change, so a client that
has seen revision N has seen every change up to N, even when mutations settle out
of creation order or change after settling. Live events on one stream are emitted
in revision order; in-flight intent is not emitted live, only its settlement.
GET /api/mutation-events?session_id=<key>&logical_turn_id=<id>[&after=<n>]returns a finite text/event-stream snapshot of the outcomes changed after the
cursor (also accepted as Last-Event-ID), oldest revision first, each framed with
id: <n>. Every outcome_unknown outcome is also replayed after any cursor
until it is settled or reconciled: a live stream can deliver revision N while an
earlier write is still in flight, and if that write’s settlement fails or the
process stops, no later revision would announce it. Clients should upsert by
mutation_id. Each event carries the entry’s current projection, so a reconciled
outcome_unknown reappears as completed or not_executed, and late host
metadata reappears with its resource_id. Entries recorded before revisions
existed appear only in a full replay. Errors: 400 for an invalid cursor, 404
for an unknown session or logical turn, 409 for a cursor past the turn’s latest
revision.