| 1 | # Compaction survival contract |
| 2 | |
| 3 | Schema version 2. |
| 4 | |
| 5 | The protected last round means the current user instructions and the two |
| 6 | most recent complete tool exchanges (including their assistant output). |
| 7 | Short rounds remain intact. Older completed steps within a long uninterrupted |
| 8 | task may be summarized; otherwise a single user request could grow forever. |
| 9 | Tool-call batches and their results are never split, and unresolved calls |
| 10 | prevent a split across them. Original history is saved before any rewrite. |
| 11 | |
| 12 | Language-invariant schema for session-tree journal entry types. Compaction |
| 13 | may summarize older turns; it must not drop the fields marked **survive** |
| 14 | below. The B1 strategy (Rust `codewhale-tui` compaction path) enforces this |
| 15 | over the live journal / API transcript. Later TS/Go strategies validate |
| 16 | against the same field names. |
| 17 | |
| 18 | Failed compact must not replace live history. A second compact must replace |
| 19 | the prior summary instead of stacking it, and must not grow the cacheable |
| 20 | prefix by duplicating checkpoints. Internal protocol placeholders |
| 21 | (`(no summary available)`, refusal-only text) are never durable transcript |
| 22 | content. |
| 23 | |
| 24 | Live repository, tool, and GitHub state is distinguished from summarized |
| 25 | facts: background-work handles and the exact branch/worktree identity |
| 26 | survive here or are re-read from live state before consequential work. |
| 27 | |
| 28 | ## Entry types |
| 29 | |
| 30 | Journal kinds are the protocol `kind` strings (`header`, `user`, |
| 31 | `assistant`, `tool_result`, `compaction`, `branch_summary`). Session-tree |
| 32 | `message` / `system` entries carry the same payload on `SessionEntryKind`. |
| 33 | |
| 34 | | kind | payload location | survive | summarized | pruned | not retained | |
| 35 | | --- | --- | --- | --- | --- | --- | |
| 36 | | `header` | `payload` identity, `id`, `created_at` | always | — | — | secrets | |
| 37 | | `user` | `payload` text / `SessionEntryKind::User.text` / `Message` text blocks | last-round verbatim (bounded token budget for older user text) | older user turns | obsolete narration | secrets, raw dumps | |
| 38 | | `assistant` | `payload` text / `SessionEntryKind::Assistant.text` / `Message` text + `tool_use` | protected last-round verbatim | older assistant text and reasoning | — | internal placeholders | |
| 39 | | `tool_result` | `payload.tool_use_id`, `content`, `is_error`, `content_blocks` | last-round bounded (8KiB) | older results | nested images, oversized blocks | secrets, raw large outputs | |
| 40 | | `compaction` | `payload.summary`, `tokens_before`, `tokens_after`, `model`, coverage receipt | latest checkpoint only | prior summaries replaced | stacked prior summaries | placeholder-only summaries | |
| 41 | | `branch_summary` | `payload.branch_id`, `summary`, `parent_branch_id` | always (not rewritten by compact) | — | — | — | |
| 42 | | `message` | flattened `Message.role` + `Message.content[]` | see content-block table | older turns | see prune rules | see never-durable | |
| 43 | | `system` | `SessionEntryKind::System.content` | constitution / repo-law identity when present | duplicated policy | — | unsupported completion claims | |
| 44 | |
| 45 | ## Content blocks on `message` entries |
| 46 | |
| 47 | | block `type` | fields that must survive | bound / notes | |
| 48 | | --- | --- | --- | |
| 49 | | `text` | `text` on the last user round | older user text may truncate to the retained-user token budget | |
| 50 | | `tool_use` | `id`, `name`, `input`, `thought_signature` | last-round only; `id` is the join key for `tool_result` | |
| 51 | | `tool_result` | `tool_use_id`, `content`, `is_error` | last-round; `content` truncated at 8KiB, `content_blocks` dropped if truncated | |
| 52 | | `thinking` | `thinking`, `signature` (byte-for-byte) | all retained reasoning is replay protocol; older exchanges may be summarized as complete units | |
| 53 | | `image_url` | last-round images | older images may be pruned with tool results | |
| 54 | | `server_tool_use` / `*_tool_result` | last-round ids | older blocks may be summarized | |
| 55 | |
| 56 | ## Fields restated from live state (not the summary) |
| 57 | |
| 58 | These must remain usable after compact. They live outside free-form summary |
| 59 | text; if the journal copy is missing they are re-read before consequential |
| 60 | work. |
| 61 | |
| 62 | | field | where it lives | after compact | |
| 63 | | --- | --- | --- | |
| 64 | | `/anchor` pin | `.codewhale/anchors.md` (legacy `.deepseek/anchors.md`) | restated verbatim on the checkpoint user message | |
| 65 | | compaction path + coverage | `CompactionCoverage` on the result / `/context` inspector | named as `summary` or `prune-only` plus last-round counts | |
| 66 | | compaction receipt | checkpoint `user` message whose text contains the summary marker; TUI `HistoryCell::System` | latest receipt kept; failed compact does not write a replacement | |
| 67 | | billed parent prompt tokens | `TurnContext.latest_parent_input_tokens` / session-carried copy | survive the turn boundary; cleared on history rewrite | |
| 68 | | branch / worktree identity | live git / workspace | re-read from live state | |
| 69 | | background-work / WorkRef handles | session + worker registry | survive or re-read from live state | |
| 70 | | child usage / cumulative tool steps | turn billing totals | must not create false compaction pressure | |
| 71 | |
| 72 | ## Enforcement |
| 73 | |
| 74 | Rust now: |
| 75 | |
| 76 | - `last_round::build_replacement_history` keeps the bounded protected last round |
| 77 | (assistant + tool results) and appends one checkpoint receipt. A trailing |
| 78 | toolless user/assistant tail still walks back to the last tool-bearing |
| 79 | round; chat-only sessions keep only the latest user turn. |
| 80 | - `validate_last_round_coverage` refuses the rewrite if any of that round's |
| 81 | user texts, protected tool-call ids, tool-result ids, or assistant texts would vanish. |
| 82 | Every user turn in the round is checked, not just the first: the round spans |
| 83 | a tool-bearing turn plus the toolless tail after it, so checking one would |
| 84 | let the latest turn -- the one this contract exists for -- be dropped. The |
| 85 | assistant check compares text, because "some assistant message survived" is |
| 86 | satisfied by the summary the rewrite itself just wrote. |
| 87 | - `validate_survival_contract` also refuses a missing checkpoint receipt, |
| 88 | duplicated prior summaries, dropped `/anchor` text, or a placeholder-only |
| 89 | checkpoint. |
| 90 | - `compact_messages_safe` never mutates the caller's live history; the host |
| 91 | commits only after `Ok`. |
| 92 | |
| 93 | The language-invariant fixture matrix is |
| 94 | `crates/tui/src/compaction/fixtures/matrix.json`. Rust loads it in |
| 95 | `last_round` tests. `validate_survival_contract.mjs` is the same coverage |
| 96 | floor for a later TypeScript strategy (`node validate_survival_contract.mjs`). |
| 97 | There is no Go runtime in this repository yet; a Go validator remains a |
| 98 | follow-up when a Go strategy exists. |
| 99 | |
| 100 | `/context` names the compaction path and `/anchor` survival. |
| 101 |