# Negotiated local-agent tool cards
## Status and scope
`tool.cards` enables schema-1 live snapshots and immutable saved cards after explicit initialization negotiation. The contract originated in issue [#862](https://github.com/magimetal/magi-code/issues/862); implementation and bounded process observations accompany [#864](https://github.com/magimetal/magi-code/issues/864). Delivery ownership and evidence limits: [tool-card handoff](local-agent-tool-cards-handoff.md).
The extension enhances display of existing activities, never provider execution, tool permissions or approval routing. It uses a shared safe backend projection, independent of Mission Control's private Rust types. No raw `ToolActivityDetail`, `TranscriptCard`, tool-result endpoint or arbitrary metadata passthrough. [Version-1 envelopes, admission and transport limits](local-agent-protocol.md) remain authoritative.
Backend meaning and source selection use [shared tool-card projection](tool-card-projection.md). `src/local_agent/tool_cards.rs` converts selected facts into schema-1 objects; `turns/cards.rs` owns bounded live identities, hierarchy, revisions and drain finalization; `tool_cards/history.rs` selects saved facts and remaps relationships into the immutable history revision. Negotiation admits both projections together.
## Negotiation and advertisement
`initialize` params remain exactly `{ "required_capabilities": [string, ...] }`, at most **16 request names**. A client supporting this entire schema requests `tool.cards` alongside needed existing names. There is no speculative event subscription or second initialization after admission.
An implementing backend must:
1. Validate parameters and all required capabilities before configuration, authentication, filesystem creation, MCP startup or any other startup effect. Unknown `tool.cards` support returns the existing `unsupported_capability` response and leaves initialization unadmitted.
2. Without a `tool.cards` request, return the existing ordered sixteen-capability list, unchanged. Do not include `tool.cards`, its descriptor, rich events or rich history fields. This rule applies even when other extensions were requested.
3. With a supported `tool.cards` request, return that same list followed by `tool.cards` (seventeen **response names**) and `extensions["tool.cards"]` below. Only this client has opted into the larger advertised list and schema. Existing `limits` values do not change.
4. Admit initialization once. An admitted startup, including one that later fails, cannot be retried in the same child; `already_initialized` and normal restart rules apply.
Descriptor fields are required; values are fixed for schema 1:
```json
{"schema_version":1,"event":"tool.card","history_field":"tool_card","snapshot_parts":1,"card_bytes":16384,"core_bytes":2048,"cards_per_run":4096,"snapshots_per_run":16384,"run_snapshot_bytes":33554432,"retained_card_bytes":8388608,"blocks_per_card":16,"rows_per_block":32,"diff_lines_per_block":128,"children_per_block":32,"children_depth":8,"string_bytes":1024,"title_bytes":256,"metadata_bytes":512,"preview_bytes":4096}
```
Request-name count and advertised-response count are separate. A rich client must accept exactly the negotiated semantics and a bounded response list of at most seventeen names, not raise the request limit. Future capabilities beyond this contract need a separately agreed advertisement policy. Duplicate required names do not gain additional capacity; clients send unique names.
After successful rich initialization, the client validates all required capability names and the schema-1 descriptor before admitting turns or history display. Missing/mismatched descriptor or bounds fail closed; successful admission is not an effect-free refusal and cannot trigger same-child base fallback.
On a correlated `unsupported_capability` response to the first, unadmitted initialization only, a client may retry once with a fresh request ID and a base-only required list it already supports. Do not retry on timeout, disconnect, startup failure, `already_initialized` or an uncertain response. All request IDs, including rejected ones, count toward the existing 4096-ID lifetime. Fallback cannot replay `turn.start`, prompts, tools or accepted mutations. No events from the refused extension may precede fallback.
Protocol version 1 is retained only under these opt-in conditions. An unconditional seventeenth advertised capability is not backward compatible. Changing base activity meanings, required history fields or existing bounds requires a protocol-version agreement rather than this extension.
## Card schema
Each card is a complete replacement snapshot. All fields below are required, including nullable ones; no arbitrary object fields carry display data. Integers are nonnegative JSON integers bounded to the JavaScript safe-integer range unless a smaller bound is specified. Strings are sanitized UTF-8, never terminal markup. IDs use the existing 1–64-byte ASCII ID grammar.
| Field | Type and meaning |
| --- | --- |
| `schema_version` | Integer `1`. |
| `card_id` | Same opaque ID as `activity_id`; not a second card identity. |
| `activity_id` | ID of the enhanced base activity. Live: child/session/run scope. Saved: session/history-revision scope, equal to entry `message_id`. |
| `parent_activity_id` | ID in the same scope or null; never a raw internal task ID. |
| `parent_state` | `root`, `available`, `missing`, `omitted`. Root requires null parent; available requires a projected parent. Missing means source ancestry unavailable; omitted means known ancestry excluded by projection bounds/privacy. Missing/omitted may have null ID. |
| `task_id` | Opaque mapped delegated-task identity in the same scope, or null when absent/unavailable. No cross-session lookup meaning. |
| `tool_kind` | `read`, `search`, `write`, `edit`, `command`, `web`, `mcp`, `code_mode`, `subagent`, `other`. Unrecognized backend tool names use `other`, not a new wire enum. |
| `state` | `running` or `finished`. This is display lifecycle, not transport/persistence state. |
| `outcome` | Null while running; otherwise `success`, `failure`, `cancelled`, `interrupted` or `unknown`. |
| `changes` | `not_applicable`, `none`, `applied`, `partial`, `unknown`. Not-applicable means a non-mutating operation by contract; none requires evidence no changes occurred. Applied means observed changes completed; partial means observed changes with incomplete application. Missing evidence is unknown. Independent of outcome; cancellation never implies rollback. |
| `revision` | Integer starting at 0, strictly increasing for each emitted replacement of this card in its scope. Immutable once emitted. |
| `origin` | `live` or `saved`; saved means projection from durable records, not a live-card durability claim. |
| `detail` | Exactly `{ "availability": "complete"\|"truncated"\|"unavailable"\|"none", "reason": reason\|null }`, as defined below. |
| `title` | Safe selected title, at most 256 UTF-8 bytes; fixed safe tool-kind label when source title cannot be shown. Never parse it to recover outcome. |
| `metadata` | Fixed object described below. |
| `blocks` | Ordered array of typed display blocks, at most 16. |
| `omissions` | Array of explicit omission objects, at most one per subject listed below. Empty only when nothing was omitted. |
`success` requires authoritative tool completion, not an assistant claim, streamed preview, shell output or run completion. `failure` requires an observed failed result. `cancelled` requires observed tool cancellation; merely requesting cancellation does not establish it. `interrupted` means the backend has evidence execution ended without a tool terminal record (for example, durable interrupted-turn evidence). `unknown` means outcome evidence is insufficient. On lost transport, the client marks unfinished live cards locally unknown; it must not invent a backend revision or claim cancellation. Partial/applied changes survive failure or cancellation and remain explicit. Missing change evidence uses `unknown`, not `none`.
`detail.availability=complete` means all safely displayable detail from this source fits, not unrestricted access to raw results. `truncated` means selected detail was shortened or excluded; `unavailable` means no safe detail can be provided; `none` means this activity has no display detail by design. Complete/none require null reason; truncated/unavailable require a reason. Reasons: `size_limit`, `budget_limit`, `redacted`, `not_recorded`, `unsupported_source`, `missing_source`. No free-form failure explanation. Summaries cannot substitute for these typed facts.
`metadata` is exactly:
```json
{"tool_name":null,"resource_path":null,"working_directory":null,"public_protection":{"decision":"not_reported","summary":null}}
```
First three values are nullable selected display strings, each at most 512 UTF-8 bytes. `tool_name` is a sanitized name, never executable routing information. `public_protection.decision` is `not_reported`, `allowed`, `blocked`, `approval_required` or `denied`; it records the latest authoritative public decision, not a private assessment or complete audit trail. An observed approval may advance approval-required to allowed/denied; detail-only updates cannot erase a known decision to not-reported. Nullable summary at most 256 UTF-8 bytes contains only the backend's public protection summary. Preserve known blocking/denial/approval decisions even when summary text is unavailable, with a metadata omission for excluded summary text. No private scores, assessments, evidence, policy prompts or reasoning. This object cannot authorize execution; only existing `approval.requested`/`approval.answer` exchanges do that.
An omission object is exactly `{ "subject": subject, "reason": reason, "omitted_count": integer|null }`. Subjects: `title`, `metadata`, `blocks`, `rows`, `text`, `diff_lines`, `children`, `progress`, `ancestry`, `updates`. Reasons use the detail reason enum. Count is the actual omitted item count when known, otherwise null; text counts Unicode scalar values, not bytes. Never invent totals. Merge same-subject omissions without double counting; any unknown count makes the merged count null. When multiple reasons apply, use the first in this priority order: redacted, missing-source, not-recorded, unsupported-source, budget-limit, size-limit (wire values retain underscores). Apply the same order to `detail.reason`. These indicators describe detail loss, not loss of outcome or public safety facts.
## Typed display blocks
Each block has a `type` discriminator and exactly the fields specified here. No HTML, styles, arbitrary JSON, executable actions, embedded images or recursively nested blocks. All nullable fields must be present. General display strings are at most 1024 UTF-8 bytes unless an explicit field bound overrides it; byte and serialized-card limits also apply to aggregate content. Line numbers are positive integers when known.
| `type` | Remaining fields |
| --- | --- |
| `text` | `text:string` (at most 4096 UTF-8 bytes); `format:"plain"\|"code"`; `language:string\|null` (at most 64 bytes, display hint only). |
| `resources` | `rows:[resource]`, at most 32. Resource exactly `kind:"file"\|"url"\|"resource"`, `label:string`, `location:string\|null`, `availability:"available"\|"unavailable"\|"unknown"`. Availability concerns source evidence, not permission to open/fetch. |
| `results` | `rows:[result]`, at most 32. Result exactly `label:string`, `value:string\|null`, `outcome:"success"\|"failure"\|"partial"\|"unknown"`. Row outcomes do not replace card outcome. |
| `command` | `command:string\|null` (at most 4096 UTF-8 bytes), `working_directory:string\|null` (at most 512 bytes), `exit_code:integer\|null` (signed 32-bit), `signal:string\|null` (at most 64 bytes). Unknown/redacted command is null with a text omission; no raw environment, stdin or secret arguments. A known exit code is a result fact, not the sole overall success rule. |
| `diff` | `old_path:string\|null`, `new_path:string\|null` (each at most 512 bytes), `application:"proposed"\|"applied"\|"partial"\|"unknown"`, `lines:[diff_line]` (at most 128). Paths/lines describe a selected preview, not a patch to apply. |
| `children` | `children:[child]` (at most 32), `total:integer\|null`. Child exactly `activity_id:string`, `task_id:string\|null`, `title:string` (at most 256 bytes), `state:"running"\|"finished"`, `outcome:card_outcome\|null`, `changes:card_changes`. State/outcome rules match card rules. Children reference separately projected activities, never embed cards. Total is known source count or null. |
| `progress` | `label:string`, `completed:integer\|null`, `total:integer\|null`, `unit:"items"\|"bytes"\|"steps"\|"unknown"`. Null means not known. When both counts known, completed ≤ total. Counts do not establish success. |
Diff line exactly `{ "kind": "file_header"|"hunk_header"|"context"|"added"|"removed"|"no_newline", "text":string, "old_line":integer|null, "new_line":integer|null }`. Text excludes an added/removed/context prefix; kind is authoritative. Context uses both known line numbers, added uses only new, removed only old. Header and no-newline rows use null numbers. Unknown source numbers stay null. Hunk/file headers are data, not something clients parse to infer line kinds. Truncated content uses omissions, never fake context or invented lines. `application=applied` requires evidence of the actual applied operation; proposed input or rereading today's file cannot establish historical application.
Hierarchy must be acyclic, at most eight parent edges deep, with references in the same identity scope. Missing parents render as explicitly unresolved, never attach to a guessed task. Excluded child identities receive a children omission; unknown totals remain null. Each child is enhanced at its existing activity position, not duplicated as another transcript card under every parent. Parent child rows are navigation/display summaries, not additional authoritative copies: never overwrite a separately received child card with a parent row. Pending saved references may resolve on later pages of the same revision; do not infer missing ancestry merely from page order.
## Live event and ordering
Negotiated event `tool.card` uses the existing event envelope and exactly these required data fields: `session_id`, `run_id`, `activity_id`, `part=0`, `last_part=true`, `card`. Data activity ID must equal both card IDs; origin must be live. Schema 1 deliberately has **one atomic part per snapshot**, not arbitrarily chunked JSON. Clients replace the snapshot, never concatenate cards.
Ordering requirements:
- `turn.start` admission and `input.persisted` precede any card. Flush/reconcile the preceding assistant segment at the tool boundary before the base `activity(state=started)`; emit initial card after that base activity. Cards enhance that position, not the latest assistant message.
- Keep base `activity` lifecycle/name/summary unchanged for non-rich consumers. Multiple snapshots with the same ID update one activity. Do not create a duplicate card for the base summary.
- Interim progress/preview updates may coalesce **before** assigning a revision or enqueueing. Once emitted, a revision is immutable; later snapshots replace all its detail, including removed blocks. Revision gaps are permitted, backwards revisions are invalid, equal revisions must have identical content. A gap does not require append reconstruction.
- An observed tool finish emits the unchanged base completed/failed activity first, then its final rich snapshot. Rich cancelled/partial/unknown distinctions refine the coarse base failure state; clients must not infer them from base summaries. Existing base completion is not rewritten into a different protocol enum.
- No running snapshot may overwrite a finished card. Retain authoritative outcome, changes and public protection decisions across later detail-only updates. Omissions always describe the current replacement's content; they may clear only when that content is supplied, not merely after a tool finishes. A finished card may receive a higher revision with additional safe detail before run terminal, but cannot reverse its outcome without a new protocol agreement.
- Before `run.finished`, resolve every admitted unfinished card with observed cancelled/interrupted/unknown facts and deliver its final snapshot, after required assistant reconciliation. Do not fabricate a base tool finish without evidence. A successful run cannot turn an unknown tool into a success.
- `run.finished` remains once, after worker/cleanup accounting; no card event follows it. Run outcome/persistence and tool outcomes are independent. A complete displayed card proves neither durable append nor successful run completion.
Every enqueued card snapshot is critical, using existing bounded delivery; no best-effort authoritative card drops. On output failure, normal `output_unavailable`, cancellation and process failure policy applies. Final delivery after disconnect is not guaranteed; clients retain explicit uncertain state and never replay tools to fill it.
## Bounds and oversized detail
All descriptor bounds are maxima, not allocations or permission to read extra source content. Bound source work before projection; reuse existing guarded file/tool-output readers and cancellation. Never read unrestricted raw output to fill a small card. Projection, sanitization and persistence stay on existing workers, not the protocol/UI thread.
Measure compact UTF-8 JSON **after sanitization and JSON escaping**, with actual keys, nulls, numbers and arrays. `card_bytes=16384` applies to the serialized card object. `core_bytes=2048` applies to the same object with empty blocks, fixed fallback title, nullable display metadata and required omission indicators; known outcome/change/protection/identity facts remain. Raw string maxima alone do not prove fit: quotes, backslashes and escaped controls can expand JSON. Reduce strings only at Unicode boundaries, and report each reduction. If safe metadata/title cannot fit core, use nullable metadata/fixed title with explicit omissions; do not shorten IDs or safety enum values.
Additional rules:
- Full live envelope including LF must remain ≤65,536 bytes. A history **result**, including session/revision/cursor/page fields and all entries/cards, must remain ≤32,768 serialized bytes; its enclosing response including LF must also fit output frame. Size the first activity text part together with its atomic card; smaller summary chunks continue in subsequent numbered parts without losing text. Emit fewer history entries rather than exceeding bytes.
- At most 4096 card identities/run, sharing the existing activity identity budget, not an additional 4096 tools. Backend retained serialized card state totals ≤8 MiB/run. Retain required cores; optional safe detail may be released after delivery and explicitly unavailable in a later replacement. This bounds backend retention, not a promise of a full revision cache. Bound mapping/collection counts as well as content bytes; no retained raw details or unbounded revision log. Clients bound their own caches within identity/card/cumulative run limits.
- At most 16,384 emitted snapshots/run and 32 MiB cumulative serialized card bytes/run, counting every revision. At run admission reserve 4096 initial cores and 4096 terminal cores, each ≤2048 bytes, plus their 8192 snapshot slots. This 16 MiB reserve cannot be spent on previews; it covers every identity admitted under the existing activity bound. Initial/final detail above core size and all interim updates use only remaining capacity. Release unused reservations after run terminal, not to speculative updates during the run.
- Intermediate limits coalesce progress and omit preview/update detail with `budget_limit`, without suppressing changed public safety facts or authoritative outcome. If required facts cannot be represented under bounds, fail visibly with existing `content_limit`/diagnostic and normal terminal accounting, never silently declare cards complete. The client marks any undelivered final cards unknown.
- No card chunking or detail-fetch endpoint in schema 1. Oversized selected blocks first shorten/exclude optional preview content with explicit counts/reasons. If no useful detail fits, send the core with `detail.availability=unavailable` and `reason=size_limit` or `budget_limit`. Preserve identity, outcome, change state and public protection decision. Omitted content is not a hidden invitation to fetch raw results.
- History retains the existing 8 MiB **entire serialized entry projection** and 10,000-part budgets, including rich cards, not a separate allowance. Reduce optional card detail deterministically before freezing the revision; if required history facts still exceed bounds, return `history_limit`/`content_limit`, never a falsely complete page. Stored reads remain 64 MiB/100,000 lines.
- Nesting remains 32 containers; hierarchy uses references, not nested objects. Existing 32-event channel, 32-output-frame queue, 2000 ms critical delivery/output/cleanup bounds, cooperative cancellation and 4096-request lifetime are unchanged. Cards have no additional unbounded queue or deadline reset.
## Negotiated saved history
Without negotiation, `history.page` entries are exactly the base shape and summary policy. With negotiation, an `activity` entry adds `tool_card:card|null` **only on `part=0`**. All base fields remain required, with the same kinds, text-part concatenation, `partial` and `last_part` semantics. No new entry kind. Non-activity entries and later text parts do not carry this field. A null card means the source activity cannot be safely mapped; retain its base summary and mark rich detail unavailable locally, without inventing typed outcome facts.
For a nonnull saved card, both card IDs equal the entry message ID, origin is saved, state is finished, revision is 0 within this immutable history revision, and live-only instance/run IDs are not required or persisted. Parent/task IDs are remapped into that snapshot scope. Full cards use the same schema and safe projection as live output, not an independent frontend interpretation. A saved tool terminal result establishes outcome; absent evidence uses interrupted or unknown under the rules above. Legacy missing previews use `not_recorded`; inaccessible safe source uses `missing_source`; unsupported source uses `unsupported_source`; redacted detail uses `redacted`. Missing detail never means successful execution or no applied changes.
Text parts retain their existing immutable contiguous numbering. Schema 1's card is atomic, attached once to the first text part, even if summary text spans subsequent pages. Its `part=0` is not a second card stream. Repeated page responses for the same revision/entry must have identical card content. Budget selection/redaction occurs before freezing the snapshot; do not lazily add blocks on later pages. Mutation, appends, compaction or display-record changes invalidate the whole revision stickily; reopen explicitly. Cursor rules, ordering, stale errors and history worker ownership stay unchanged.
Saved IDs are not promised to equal live IDs or IDs after reopening/compaction. After `run.finished`, replace the live transcript projection with pages of the authoritative returned history revision (or explicitly reopen if null); do not append saved cards to live copies or match by human title. Keep drafts separate. `complete=true` means the existing history completeness contract, not that every card has detailed previews. Per-card unavailable/truncated fields remain visible even on a complete page.
Applied-diff persistence is display-only. Required execution records retain outcome, committed-file and mutation facts; separate preview records retain bounded sanitized applied evidence. Include their bytes in existing storage/append/read budgets. No retroactive reconstruction from current files, attempted arguments or arbitrary raw output. Display append failure preserves execution truth, surfaces persistence degradation and omits the unpersisted preview from final live/saved detail. Never imply rollback or resend accepted tools.
### Display-only session records
The owned session log stores `tool_applied_preview` after its required `tool_result` or `code_mode_tool_result`. A result with an applied-preview source carries a unique sibling `display_id`; raw `ToolResult.display` remains excluded from serialization and deserialization. Result status and structured execution evidence, not the preview, establish success, cancellation and partial changes.
Preview payload:
```json
{"display_id":"unique-result-display-id","preview":{"schema_version":1,"diff":"-before\n+after\n","truncated":false}}
```
Code Mode records also carry the same `parent_call_id` and `sequence` as their execution result. Hydration joins unique display IDs within the recorded session, requires matching scope and later record order, and refuses duplicate result IDs, duplicate previews, malformed/newer preview schemas or mismatched parents/sequences. Such detail stays unavailable; results retain independent execution facts. Unknown session event kinds remain readable without card projection; malformed required call/result data rejects exact open before replacing attachment.
Schema 1 preview bounds: 8192 serialized UTF-8 bytes including JSON escaping, 128 lines, 1024 UTF-8 bytes per line. Redact multiline secrets and sanitize controls before selection. Never split a UTF-8 scalar; mark truncation explicitly. Live final and Mission Control/local-agent history hydration consume this same selected source. Ordinary preview appends consume an 8 MiB/run allowance under the existing 64 MiB session ceiling; nested Code Mode uses its existing owned history budget. JSONL-durable metadata failure does not erase a stored preview. Failed or uncertain preview append does not establish saved availability; preserve execution and require explicit reconciliation rather than replay.
Partial Hash Edit retains previews from completed operations, including confirmed effects followed by durability/reporting errors. An incomplete move cannot label its attempted patch as applied; missing portion remains omitted. Deletes use recorded pre-edit content against an empty after-state. Reopen never reads current file contents to reconstruct those changes.
Saved attachment uses turn, top-level/Code Mode scope, recorded parent and real call ID. Duplicate identities within that scope, including reuse after completion, produce null rich attachments. Top-level and nested IDs do not collide. A Code Mode parent must be an unambiguous earlier recorded Code Mode call in the same turn; other ancestry remains unavailable. Preserve assistant boundaries, snapshot parts, public decisions and authoritative outcomes.
Display events stay local-only and never enter provider tool-result replay/context. Existing tolerant unknown-event handling preserves readable histories without schema migration or personal-history rewriting. Reopen performs no provider generation or opportunistic filesystem scan.
## Privacy and decoder policy
Allowlist only selected sanitized tool names/titles, commands without secrets, paths, resource/result rows, bounded output previews, semantic diff content, mapped task hierarchy/progress and public protection summaries. Run every string through shared redaction and control sanitization before sizing/storage/delivery. Redaction may produce null metadata or explicit preview omissions. Resource URLs/paths are inert display data; consumers must not automatically open, fetch, execute or apply them. No credential paths/files, tokens, environment dumps, headers, raw provider/tool responses, private protection assessments, raw reasoning or unrestricted metadata objects. Read-only display does not relax protections on obtaining source data.
Clients fail closed on unnegotiated `tool.card`, unknown schema versions, block types, enums, missing required fields, duplicate JSON fields, mismatched identities, invalid part/revision semantics or exceeded bounds. Do not render unsupported safety/outcome semantics as generic text. New enum/block/schema semantics require a new negotiated capability agreement; never silently send schema 2 to a schema-1 client.
Bounded future unknown object fields may be ignored only as inert optional display annotations: still count their bytes/depth toward existing bounds, never execute them, and never use them to override required facts. Producers may add such fields only when ignoring them preserves every safety/outcome/identity meaning. This does not allow arbitrary metadata/result passthrough or unknown typed blocks. The frozen schema's documented fields remain required.
## Synthetic sanitized examples
These are invented schema examples, not process captures or native/provider acceptance. First initialization requests rich semantics; an unsupported backend returns the correlated effect-free error, then the client retries base-only with a different ID:
```json
{"protocol":"magi-code.local-agent","version":1,"id":"init-rich","method":"initialize","params":{"required_capabilities":["status","shutdown","session.open","history.page","turn.start","turn.cancel","tool.cards"]}}
{"protocol":"magi-code.local-agent","version":1,"type":"response","instance_id":"child-1","seq":1,"id":"init-rich","error":{"code":"unsupported_capability"}}
{"protocol":"magi-code.local-agent","version":1,"id":"init-base","method":"initialize","params":{"required_capabilities":["status","shutdown","session.open","history.page","turn.start","turn.cancel"]}}
```
On a different, rich-admitted connection, after base activity `a1` started and then failed, this final snapshot records observed cancellation **with partial applied changes**, not rollback:
```json
{"protocol":"magi-code.local-agent","version":1,"type":"event","instance_id":"child-2","seq":9,"event":"tool.card","data":{"session_id":"example-session","run_id":"run-1","activity_id":"a1","part":0,"last_part":true,"card":{"schema_version":1,"card_id":"a1","activity_id":"a1","parent_activity_id":null,"parent_state":"root","task_id":null,"tool_kind":"edit","state":"finished","outcome":"cancelled","changes":"partial","revision":2,"origin":"live","detail":{"availability":"truncated","reason":"size_limit"},"title":"Edit example.rs","metadata":{"tool_name":"hash_edit","resource_path":"example.rs","working_directory":null,"public_protection":{"decision":"allowed","summary":null}},"blocks":[{"type":"diff","old_path":"example.rs","new_path":"example.rs","application":"partial","lines":[{"kind":"removed","text":"let count = 1;","old_line":4,"new_line":null},{"kind":"added","text":"let count = 2;","old_line":null,"new_line":4}]}],"omissions":[{"subject":"diff_lines","reason":"size_limit","omitted_count":null}]}}}
```
A saved activity entry from a legacy interrupted turn, with no recorded preview (history result/envelope omitted):
```json
{"message_id":"saved-a1","kind":"activity","text":"Command interrupted","partial":true,"part":0,"last_part":true,"tool_card":{"schema_version":1,"card_id":"saved-a1","activity_id":"saved-a1","parent_activity_id":null,"parent_state":"missing","task_id":null,"tool_kind":"command","state":"finished","outcome":"interrupted","changes":"unknown","revision":0,"origin":"saved","detail":{"availability":"unavailable","reason":"not_recorded"},"title":"Command","metadata":{"tool_name":null,"resource_path":null,"working_directory":null,"public_protection":{"decision":"not_reported","summary":null}},"blocks":[],"omissions":[{"subject":"blocks","reason":"not_recorded","omitted_count":null},{"subject":"ancestry","reason":"not_recorded","omitted_count":null}]}}
```
## Compatibility matrix
| Client / backend | Required behavior |
| --- | --- |
| Published non-rich client / implementing rich backend | Original sixteen advertised names, original history shape, no rich events/descriptors. No client update required for base initialization. |
| Rich client / current or older backend without extension | Effect-free `unsupported_capability`; optional one fresh-ID base-only retry before startup. No rich decoding or replay. |
| Rich client / implementing rich backend, no rich request | Base-only behavior, even though client knows schema. |
| Rich client / implementing rich backend, explicit request | Seventeen advertised names plus schema-1 descriptor; same base facts enhanced by live snapshots and saved field. |
| Any client / admitted startup failure or uncertain disconnect | No same-child initialization fallback or uncertain work replay; explicit recovery/restart. |
| Schema-1 client / unsupported schema, enum or block | Fail closed; implementation must not advertise/send those semantics under `tool.cards`. |