@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +76 -2
  3. package/cordis.patch.yml +363 -0
  4. package/docs/architecture.md +692 -0
  5. package/docs/autorun-guide.md +248 -0
  6. package/docs/backlog.md +2022 -0
  7. package/docs/contract.md +938 -0
  8. package/docs/development.md +990 -0
  9. package/docs/host-compaction-seam.md +225 -0
  10. package/docs/host-protected-head-ask.md +80 -0
  11. package/docs/knowledge-repo.md +1332 -0
  12. package/docs/plans-carriage.md +571 -0
  13. package/docs/project-state.md +642 -0
  14. package/docs/provider.md +98 -0
  15. package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
  16. package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
  17. package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
  18. package/docs/verify.md +561 -0
  19. package/lib/archive.d.ts +161 -0
  20. package/lib/archive.js +229 -0
  21. package/lib/archive.js.map +1 -0
  22. package/lib/arrival.d.ts +23 -0
  23. package/lib/arrival.js +125 -0
  24. package/lib/arrival.js.map +1 -0
  25. package/lib/artifact-query.d.ts +62 -0
  26. package/lib/artifact-query.js +187 -0
  27. package/lib/artifact-query.js.map +1 -0
  28. package/lib/attach.d.ts +46 -0
  29. package/lib/attach.js +120 -0
  30. package/lib/attach.js.map +1 -0
  31. package/lib/auto-save.d.ts +145 -0
  32. package/lib/auto-save.js +334 -0
  33. package/lib/auto-save.js.map +1 -0
  34. package/lib/aux-message.d.ts +17 -0
  35. package/lib/aux-message.js +20 -0
  36. package/lib/aux-message.js.map +1 -0
  37. package/lib/client.js +1419 -0
  38. package/lib/client.js.map +1 -0
  39. package/lib/codegraph.d.ts +76 -0
  40. package/lib/codegraph.js +190 -0
  41. package/lib/codegraph.js.map +1 -0
  42. package/lib/commands.d.ts +52 -0
  43. package/lib/commands.js +946 -0
  44. package/lib/commands.js.map +1 -0
  45. package/lib/compose.d.ts +30 -0
  46. package/lib/compose.js +185 -0
  47. package/lib/compose.js.map +1 -0
  48. package/lib/continue-core.d.ts +245 -0
  49. package/lib/continue-core.js +405 -0
  50. package/lib/continue-core.js.map +1 -0
  51. package/lib/diag.d.ts +55 -0
  52. package/lib/diag.js +186 -0
  53. package/lib/diag.js.map +1 -0
  54. package/lib/engine-core.d.ts +301 -0
  55. package/lib/engine-core.js +531 -0
  56. package/lib/engine-core.js.map +1 -0
  57. package/lib/engine-note.d.ts +35 -0
  58. package/lib/engine-note.js +32 -0
  59. package/lib/engine-note.js.map +1 -0
  60. package/lib/engine.d.ts +329 -0
  61. package/lib/engine.js +1179 -0
  62. package/lib/engine.js.map +1 -0
  63. package/lib/enrich-queue.d.ts +66 -0
  64. package/lib/enrich-queue.js +126 -0
  65. package/lib/enrich-queue.js.map +1 -0
  66. package/lib/enrich-store.d.ts +31 -0
  67. package/lib/enrich-store.js +103 -0
  68. package/lib/enrich-store.js.map +1 -0
  69. package/lib/enrich-wire.d.ts +163 -0
  70. package/lib/enrich-wire.js +353 -0
  71. package/lib/enrich-wire.js.map +1 -0
  72. package/lib/enrich.d.ts +102 -0
  73. package/lib/enrich.js +267 -0
  74. package/lib/enrich.js.map +1 -0
  75. package/lib/gitops.d.ts +107 -0
  76. package/lib/gitops.js +539 -0
  77. package/lib/gitops.js.map +1 -0
  78. package/lib/index.d.ts +241 -0
  79. package/lib/index.js +1162 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/indexing.d.ts +98 -0
  82. package/lib/indexing.js +223 -0
  83. package/lib/indexing.js.map +1 -0
  84. package/lib/injections.d.ts +90 -0
  85. package/lib/injections.js +165 -0
  86. package/lib/injections.js.map +1 -0
  87. package/lib/last-resort.d.ts +24 -0
  88. package/lib/last-resort.js +27 -0
  89. package/lib/last-resort.js.map +1 -0
  90. package/lib/log-shape.d.ts +38 -0
  91. package/lib/log-shape.js +93 -0
  92. package/lib/log-shape.js.map +1 -0
  93. package/lib/models-command.d.ts +74 -0
  94. package/lib/models-command.js +100 -0
  95. package/lib/models-command.js.map +1 -0
  96. package/lib/notice.d.ts +74 -0
  97. package/lib/notice.js +92 -0
  98. package/lib/notice.js.map +1 -0
  99. package/lib/plans.d.ts +154 -0
  100. package/lib/plans.js +315 -0
  101. package/lib/plans.js.map +1 -0
  102. package/lib/preset.d.ts +186 -0
  103. package/lib/preset.js +212 -0
  104. package/lib/preset.js.map +1 -0
  105. package/lib/prompt-keys.d.ts +7 -0
  106. package/lib/prompt-keys.js +8 -0
  107. package/lib/prompt-keys.js.map +1 -0
  108. package/lib/prompt-loop.d.ts +73 -0
  109. package/lib/prompt-loop.js +147 -0
  110. package/lib/prompt-loop.js.map +1 -0
  111. package/lib/prompt-push.d.ts +47 -0
  112. package/lib/prompt-push.js +115 -0
  113. package/lib/prompt-push.js.map +1 -0
  114. package/lib/prompt-runner.d.ts +48 -0
  115. package/lib/prompt-runner.js +90 -0
  116. package/lib/prompt-runner.js.map +1 -0
  117. package/lib/prompt-state.d.ts +142 -0
  118. package/lib/prompt-state.js +101 -0
  119. package/lib/prompt-state.js.map +1 -0
  120. package/lib/prompt.d.ts +270 -0
  121. package/lib/prompt.js +408 -0
  122. package/lib/prompt.js.map +1 -0
  123. package/lib/provider.d.ts +44 -0
  124. package/lib/provider.js +66 -0
  125. package/lib/provider.js.map +1 -0
  126. package/lib/redact.d.ts +24 -0
  127. package/lib/redact.js +85 -0
  128. package/lib/redact.js.map +1 -0
  129. package/lib/registry.d.ts +187 -0
  130. package/lib/registry.js +235 -0
  131. package/lib/registry.js.map +1 -0
  132. package/lib/removal.d.ts +159 -0
  133. package/lib/removal.js +556 -0
  134. package/lib/removal.js.map +1 -0
  135. package/lib/render.d.ts +97 -0
  136. package/lib/render.js +530 -0
  137. package/lib/render.js.map +1 -0
  138. package/lib/repair-run.d.ts +76 -0
  139. package/lib/repair-run.js +506 -0
  140. package/lib/repair-run.js.map +1 -0
  141. package/lib/repair.d.ts +208 -0
  142. package/lib/repair.js +357 -0
  143. package/lib/repair.js.map +1 -0
  144. package/lib/repo.d.ts +78 -0
  145. package/lib/repo.js +184 -0
  146. package/lib/repo.js.map +1 -0
  147. package/lib/rules-commands.d.ts +46 -0
  148. package/lib/rules-commands.js +379 -0
  149. package/lib/rules-commands.js.map +1 -0
  150. package/lib/rules.d.ts +267 -0
  151. package/lib/rules.js +478 -0
  152. package/lib/rules.js.map +1 -0
  153. package/lib/save-core.d.ts +72 -0
  154. package/lib/save-core.js +220 -0
  155. package/lib/save-core.js.map +1 -0
  156. package/lib/search.d.ts +112 -0
  157. package/lib/search.js +282 -0
  158. package/lib/search.js.map +1 -0
  159. package/lib/session-rules.d.ts +222 -0
  160. package/lib/session-rules.js +454 -0
  161. package/lib/session-rules.js.map +1 -0
  162. package/lib/signature.d.ts +45 -0
  163. package/lib/signature.js +199 -0
  164. package/lib/signature.js.map +1 -0
  165. package/lib/store.d.ts +156 -0
  166. package/lib/store.js +368 -0
  167. package/lib/store.js.map +1 -0
  168. package/lib/sync.d.ts +220 -0
  169. package/lib/sync.js +810 -0
  170. package/lib/sync.js.map +1 -0
  171. package/lib/tools.d.ts +113 -0
  172. package/lib/tools.js +1090 -0
  173. package/lib/tools.js.map +1 -0
  174. package/lib/treedx/client.d.ts +57 -0
  175. package/lib/treedx/client.js +82 -0
  176. package/lib/treedx/client.js.map +1 -0
  177. package/lib/treedx/provider.d.ts +39 -0
  178. package/lib/treedx/provider.js +479 -0
  179. package/lib/treedx/provider.js.map +1 -0
  180. package/lib/treedx/state.d.ts +27 -0
  181. package/lib/treedx/state.js +65 -0
  182. package/lib/treedx/state.js.map +1 -0
  183. package/lib/types.d.ts +210 -0
  184. package/lib/types.js +31 -0
  185. package/lib/types.js.map +1 -0
  186. package/lib/version.d.ts +9 -0
  187. package/lib/version.js +10 -0
  188. package/lib/version.js.map +1 -0
  189. package/lib/vocabulary.d.ts +41 -0
  190. package/lib/vocabulary.js +118 -0
  191. package/lib/vocabulary.js.map +1 -0
  192. package/lib/warnings.d.ts +10 -0
  193. package/lib/warnings.js +18 -0
  194. package/lib/warnings.js.map +1 -0
  195. package/package.json +151 -4
@@ -0,0 +1,225 @@
1
+ # The Host Compaction Seam — and Why It Shrinks the MVP
2
+
3
+ Found in Phase 0b by reading `examples/deepseek-harness/packages/compaction/`, before writing any plugin
4
+ code. The plan in `docs/architecture.md § Phases` assumed we would build pressure detection, surface
5
+ replacement, and a TOC-delivery mechanism. **The harness already has all three.** Cited so this can be
6
+ re-verified on upgrade.
7
+
8
+ ## What already exists
9
+
10
+ | Capability | Where | Consequence for us |
11
+ |---|---|---|
12
+ | Automatic pressure compaction with a configurable ratio | `compaction-basic/src/index.ts:126` `if (this.config.auto) this._registerAutomaticCompaction()`; `thresholdRatio` default **0.8**, `retainRatio` **0.16** | Do not build `pressure.ts`. **Delete it from the plan.** We set `thresholdRatio: 0.9` in config. |
13
+ | **Provider-confirmed context-overflow recovery** | `index.ts:220-221` (`context overflow recovery`), `maxOverflowRetries`, returns `{ kind: 'retry' }` | The user's "compact when it runs out in the current conversation" is already implemented, including retry. |
14
+ | Between-step trigger, replayed at step boundaries | `_registerAutomaticCompaction`, `compactIfNeeded` "dynamically dispatched so subclass overrides are honored" | Correct trigger timing is inherited, and safe: never mid-step. |
15
+ | Per-model policies | `modelPolicies: z.array(modelPolicy)`, `resolveTargetPolicy(config, target)` | 32K models get their own ratios without code. |
16
+ | **One sanctioned extension hook** | `index.ts:215-217`: "`summarize()` is the **sole subclass customization hook**; the replay and durable mutation strategy stays fixed" | Our entire integration surface: subclass `BasicCompactionEngine` (exported, `export default BasicCompactionEngine`, `:434`) and override `summarize()`. |
17
+ | Cache-aware auxiliary call shape | `:224-227`: the summary call's "prefix reuses the conversation's own system prompt, tools, and messages so the provider's KV cache is not invalidated" | The pattern to imitate if we ever need a model call — already solved for us. |
18
+ | Transactional result contract | `ManualCompactionError` codes `busy / cancelled / changed / summary / commit / persistence` (`summary` = shrink floor refused; observed live in r12); `compaction/summary` event records summary inputs, route, model, usage, `shadowedSeqs`, `shadowedRange` | Real failure semantics to render, not to invent. `changed` = history moved under us; `commit` = partial application is a documented state. |
19
+ | **Tool-result pruning** | `compaction-tool-result-pruner` — "replay-safe, model-free", `PRUNE_MARKER`, shadow-price event | Overlaps our deferral idea. Ours differs by writing a **retrievable file and a path reference**; its pruning replaces content with a marker. Verify they compose before assuming. |
20
+ | Image offload | `compaction-image-offload` (`agents`, `sessions`) | Precedent for exactly the "attachments leave the body" pattern. |
21
+
22
+ ## The correction this forces on our founding premise
23
+
24
+ `compaction/types.ts:3` describes `compaction/*` events as **"log-only, no surface events"**, and a
25
+ separate replacement `user/message` carries the summary while `result.shadowedSeqs` names the hidden
26
+ surface nodes.
27
+
28
+ So DSH's compaction **shadows surface nodes; it does not delete the durable log.** Every claim in
29
+ `README.md` and `AGENTS.md` that in-place compaction destroys the raw text is therefore **wrong about the
30
+ record** — the text stays in the log. What is actually lost is *model accessibility*: the content is
31
+ hidden from the projection and there is no addressable handle the agent can use to get it back.
32
+
33
+ That is a smaller and more honest claim, and it redefines our value:
34
+
35
+ > dsh-chapters does not prevent data loss the harness would otherwise cause. It makes compacted content
36
+ > **addressable and re-loadable by the model** — topic-named chapter files with stable paths, an index the
37
+ > model can consult, and a decision about which oversized tool results are worth carrying — plus branching.
38
+
39
+ Everything in both docs that says "the raw conversation is gone" must be rewritten to say "the model can no
40
+ longer see it." *(Done — README §the two costs and AGENTS.md invariant 1 now phrase it as loss of
41
+ accessibility, with the check-2 qualification from Phase 0b.)*
42
+
43
+ ## Revised MVP (much smaller than the approved plan)
44
+
45
+ 1. **`ChaptersCompactionEngine extends BasicCompactionEngine`**, overriding only `summarize()`. Instead of
46
+ returning an LLM summary it returns **deterministic TOC blocks** over chapters written to disk — **zero
47
+ summarization tokens spent**, which also means no `summarizationProvider`/`summarizationModel` config,
48
+ no extra call, no summary-accuracy risk.
49
+ 2. **Archive module**: `render` (ranges → Markdown + frontmatter), artifact deferral
50
+ (content-addressed, dedup), `budget` check. Mount (corrected by Phase 0b — see "closed" section):
51
+ a **shipped user-root preset** whose compaction group names `dsh-chapters` instead of
52
+ `compaction-basic` (the web profile already ships the host rows `disabled: true`; the live engine
53
+ sits in the agent-preset realm), plus `agents.create({ setup: ctx => agentPresets.mount(ctx, id) })`
54
+ for continuations. The kernel refuses double-provision of `compaction` on one plane at boot.
55
+ 3. **Registry** (storage domain `dsh_chapters`): chapter records, ranges, hashes, per-session index.
56
+ 4. **`chapters_fork`** tool for branching/related conversations (Phase 0 already proved the mechanics), plus
57
+ `chapters_segment` if model-assisted ranges prove better than deterministic chunking.
58
+ 5. **Config**: `thresholdRatio: 0.9`, `retainRatio` tuned so the retained tail plus TOC fits a 32K window
59
+ after the measured ~12K header.
60
+
61
+ Dropped from the plan: `pressure.ts`, `compact.ts`, custom surface mutation, custom trigger wiring,
62
+ `chapters_compact` as our own command (`/compact` already exists via `command-compact`), and the summarizer.
63
+
64
+ ## Phase 0b closed — all five items measured (rounds 12-17)
65
+
66
+ Full evidence in [spikes/probe/FINDINGS.md § Phase 0b](../spikes/probe/FINDINGS.md). One-liners:
67
+
68
+ 1. **Engine replacement: PROVEN.** Subclass + host-mounted service + `/compact` resolution + the
69
+ automatic `agent/pre-step` path all dispatch into our override, manual and automatic transactions
70
+ commit real durable records, zero provider calls (r12/r13). Double-provision on one plane fails boot
71
+ loudly (`service "compaction" has been registered at <BasicCompactionEngine>`, r14). The live mount
72
+ point is the **preset realm** — the web profile disables host rows and the `standard` preset mounts
73
+ the engine inside an `isolate: { compaction: true }` cordis group; a user-root preset naming our
74
+ package works end-to-end (r17, 6/6). New founding fact: **`agents.create` composes no preset** —
75
+ `meta.agentPreset` is a label; without `setup: agentCtx => agentPresets.mount(agentCtx, id)` a child
76
+ is a tool-less agent (974-token bare head, no `read`), with it the head is the real ~13.3K header
77
+ (27 tools; r16). This lands on invariant 2.
78
+ 2. **Shadowed content reachability:** no default model path exists (the web profile mounts no session
79
+ query tool; the sqlite index ships `openAt: never`), **but** an opt-in package
80
+ (`dsh-tool-session-query`, five tools incl. `session_event_read` = full unabridged event JSON)
81
+ already ships in rc.2 — mounting it costs five schemas + fixed guidance per request. Differentiation
82
+ survives as *readable topic-named files + zero-token index + forking*; the README wording was
83
+ tightened in the same change.
84
+ 3. **Prefix-cache sharing — CLOSED by r23:** cross-session sharing is ~one leading block (1,024 of
85
+ 13.7K on a cold start with siblings warm; r10/11 stand). In-place compaction across a head-position
86
+ replacement: `cacheReadTokens` HELD (7,424 on both post-compaction turns) while uncached refill
87
+ halved (13,658 → ~7,000, stable) — the header survives replacement, "refill halves" is the honest
88
+ phrase, "fully cached" is not. So the cache story favors the engine as designed: the engine is the
89
+ relief mechanism; `chapters_continue` is for branching and topic resets (it pays one full cold
90
+ ~13.3-15K head).
91
+ 4. **Pruner × deferral: compose**, given one rule — render chapter bodies from the **durable log**
92
+ following each replacement's `sourceEventSeqs` (the pruner only shadows surface nodes; originals are
93
+ untouched), never from `summarize()`'s input, which already shows `PRUNE_MARKER` truncations.
94
+ 5. **Vocabulary: pinned** — `src/notice.ts` + `test/notice.test.ts` (kernel-measured rejections
95
+ reproduced; published `MessageSourceMap`/`ContextForm` constants; installed-`.d.ts` drift guard).
96
+
97
+ ### Also true, learned late (fix on contact)
98
+
99
+ - `summarize()` receives no seqs; the manual path bypasses `compactRegion` dispatch; correlation is
100
+ post-commit via `CompactionResult.shadowedSeqs` / the `compaction/summary` event.
101
+ - `compactIfNeeded` no-ops until the session has a durable routed request — the first step can never
102
+ compact (index.ts:260).
103
+ - `retainRatio ∈ (0, 1]`; zero-tail tests use `retainTokens: 0`.
104
+ - Subclassing inherits `static inject` — extend it or `ctx.agents` throws `cannot get property "agents"
105
+ without inject`.
106
+ - Host consumers do `instanceof` against THEIR copy of `ManualCompactionError`; our engine's throws are
107
+ a different class when mounted from a different tree — pair `command-compact` with us in one tree or
108
+ ship our own command (r12 section J observed the raw-throw degradation).
109
+
110
+ ## Verification delta
111
+
112
+ Landed as `docs/verify.md § Engine-Path Checks` (E1-E6): after automatic compaction, assert (a) the
113
+ header prefix stays cached — **E3 still OPEN, never measured on a composed session**; (b) the
114
+ durable log length **never decreased** (shadowing, not deletion — r12 proved the host honors this);
115
+ (c) a compacted session can still answer a question whose answer lives only in a chapter it chooses to
116
+ `read`; (d) the summary call cost is **zero** tokens, since our `summarize()` is deterministic —
117
+ proved with the r12 stub (no `usage` field), must re-prove with the real TOC.
118
+
119
+ ## Measured on 0.2.0-rc.2 (2026-10-23, #78) — what the seam actually does now
120
+
121
+ Every line below came from an artifact in this repo's own run: the diagnostics sink
122
+ (`realm:engine:compact`), the tape proxy journal (`reqBytes=`, `OVERFLOW refused`), or a decoded
123
+ `session.v4.jsonl.zstd`. None of it is inferred from the 0.1.x notes above, which is the point —
124
+ those notes are what made the failure legible as "the hook is never called", and that was wrong.
125
+
126
+ 1. **The host does call our override.** Pre-step, with `trigger="pressure"`:
127
+ `compactIfNeeded entered (trigger="pressure", session=session-…)` — three entries in one probe
128
+ session, seven compactions in one `heavy` session. The claim "never invoked" was an inference
129
+ from absent `compaction/*` events, and the instrument replaced it with a fact in one run.
130
+ 2. **The base decision path** (`dsh-compaction-basic/lib/index.js`) is: `routedTarget(session)` →
131
+ `undefined` ⇒ return `null` immediately; else `tokenMeter.measure(session)`, then
132
+ `llm.resolveModelInfo(provider, model)` for `contextWindow`, then
133
+ `resolveCompactSpec(policy, window, reservedCompletionTokens(agent, defaultMaxTokens))`, then
134
+ `measurement.totalTokens < spec.thresholdTokens` ⇒ `null`; else `selectCompactableRange(...)`,
135
+ which can itself return `null` — that is the documented "no safe range" case, and it is the case
136
+ `heavy` reaches.
137
+ 3. **Two failure shapes for the same field symptom.** A decline can be a `null` return or a THROW
138
+ (`selectCompactableRange` threw on a harness session whose surface it could not read). The
139
+ engine now reports on both paths, because "the endpoint said 400" was the only user-visible
140
+ trace of a state the product knew about.
141
+ 4. **A single oversized `tool/result` is not relievable by pressure.** Measured: one 120,650-byte
142
+ result (~30K tokens) in a 32,000 window at ratio 0.5 — six compactions of *older* content, then
143
+ the hard `400 … exceeds the context window`. The knob that prevents it is the **arrival floor**
144
+ (`toolResultArtifactTokens`), and `heavy` deliberately sets it to 1,000,000 so pressure — not
145
+ arrival — is what gets exercised. The engine does NOT override that floor: AGENTS calls it "a
146
+ behavior switch, not a knob", and an engine quietly reinterpreting an operator's switch is the
147
+ same species of dishonesty as a pin that is collected and never applied. What it does now is name
148
+ the switch, the offending node size, and the count of nodes above the trigger, in the sink.
149
+ 5. **Bytes ↔ tokens, calibrated.** 78,140 serialized request bytes measured 27,306 / 27,952 /
150
+ 28,928 tokens by the host meter — **≈2.8 B/token**, not the conventional 4. The rig's window
151
+ enforcement uses whole-body `/3` (deliberately conservative: early refusal still exercises the
152
+ regime; late refusal exercises nothing) and the rows assert the estimator against the two field
153
+ values that matter, including the 120,129-byte / 35,428-token request that `4/` let through.
154
+ 6. **The regime must be identical in record and replay.** Enforcement applies in both modes now:
155
+ a distill whose request was refused, relieved, and re-asked recorded a sequence *shaped by* that
156
+ refusal, so a replay without it diverges and looks like prompt drift.
157
+ 7. **Realm config lives on the preset row, not the host row.** `thresholdRatio`,
158
+ `toolResultArtifactTokens`, `artifactStoreRoot`, `auto`, `elicitedPlot` are read from the
159
+ `chapters-engine` row inside `preset-chapters` (`cordis.patch.yml`); the host plugin row's
160
+ `compactionThresholdRatio` / `installChaptersPreset` are inert since 0.1.18. This is why an
161
+ apparently-configured rig ran every leg on realm defaults while its own header claimed 0.5.
162
+
163
+ ## Relief must not shadow the question (#87, measured 2026-10-23)
164
+
165
+ The seam's real failure mode is not a crash — it is a **surface with no request in it**. Field
166
+ evidence from one `heavy` session (299 events, decoded `session.v4.jsonl.zstd`):
167
+
168
+ ```
169
+ 13 compaction summaries, 108 distinct shadowed seqs
170
+ STILL-HERE instruction seqs: [109,116,138,154,172,188,216,236,254,268,291]
171
+ shadowed away by relief: [109,116,138,154,172,188,216,236,254,268] ← 10 of 11
172
+ final assistant text: ""
173
+ ```
174
+
175
+ Why: `retainRatio` reserves a *token fraction* as a verbatim tail. When an oversized `tool/result`
176
+ sits in that tail it eats the whole budget, and range selection shadows the newest `user/message`
177
+ anyway. Our summary is deterministic and plot-shaped, so it carries no request. The user sees an agent
178
+ that stopped responding, with nothing in the sink — a `#61a` violation twice over (undetected, and
179
+ therefore undelivered).
180
+
181
+ **The rule we ship.** We author the summary text, so the newest human turn travels *into* it — verbatim
182
+ (`carryOpenRequest`, `src/engine.ts`). Nothing recorded is rewritten: this honors invariant 1, appends
183
+ to our own output, and the input array is never mutated. Details that are the point:
184
+
185
+ - **Injected scaffolding is skipped.** `<system-reminder>`, the host's checkpoint preamble,
186
+ `Time sampled …`, artifact stubs: re-injecting machine state as if it were a request would be a
187
+ different lie, so the predicate that decides "is this a human turn" lives in ONE place — the same
188
+ one-predicate rule that bit us with #59's title chooser.
189
+ - **A cap is stated, never silent.** `openRequestCarryMaxChars` (realm config, default 4000) bounds the
190
+ block, and when it bites the text says `carried in part: N characters total, M included — the whole
191
+ turn stays in the durable log`. Never truncate to fit is a plugin rule, and it applies to our own
192
+ summaries.
193
+ - **Nothing is claimed when there is nothing to carry.** No human turn in the region ⇒ no OPEN REQUEST
194
+ block, no empty promise of one.
195
+
196
+ Rows: `tests/unit/engine-open-request.test.ts` (verbatim carry; injections excluded; stated truncation;
197
+ empty region), written before the wiring was trusted. **Process lapse, recorded honestly:** the mandated
198
+ cycle is Issue → Docs → Tests → Code, and here I wrote the implementation before this section. The
199
+ issue (#87) and the tests did come first, and the docs landed before the commit — but the order slipped,
200
+ and a rule that is only followed when convenient is the kind of drift this file exists to catch.
201
+
202
+ ## The last-resort deferral (#78 option A, decided 2026-10-23)
203
+
204
+ **The rule.** A single `tool/result` that cannot coexist with the mandatory verbatim tail makes pressure
205
+ relief *impossible*, not merely hard: `selectCompactableRange` finds no safe range, the engine declines,
206
+ the endpoint refuses the request, and the session is left spinning (#86) or answering something other
207
+ than what was asked (#78's original symptom). Survivability is therefore an invariant, not a tuning
208
+ outcome: such a node is artifact-deferred **as a last resort**, with a delivered warning naming the seq,
209
+ the node's tokens, and the ceiling it failed.
210
+
211
+ **Three bounds, because the obvious implementation is the wrong one.**
212
+ 1. **After a decline, never before.** The trigger is the measured fact that relief ran, found no safe
213
+ range, and the surface is over the trigger. Deferring big nodes eagerly makes compaction unnecessary
214
+ and turns `oversized-turn.spec.ts` into a tautology — the trap AGENTS names in the rig rules — so an
215
+ anti-vacuity row asserts the ceiling does NOT fire when relief is possible.
216
+ 2. **Derived, not invented.** The ceiling is `windowTokens − retainBudget`, both from the base's own
217
+ config (`resolveCompactSpec` inputs), so no new magic constant appears. The one new key is the
218
+ operator-visible boolean switch `lastResortArtifact` (default on), which `preset-declaration.test.ts`
219
+ must see declared or the loader strips it.
220
+ 3. **The archive keeps every byte.** The deferral writes the artifact and leaves a citation, so the worst
221
+ case is a context-economy miss and `chapters_artifact` still answers for it — which is the only thing
222
+ that makes overriding a configured economy knob defensible at all.
223
+
224
+ Rows: `tests/unit/last-resort.test.ts` (the ceiling math, `null` on incomplete inputs, and the anti-vacuity ordering row) plus the engine lifecycle suite; below the ceiling, untouched;
225
+ switch off, untouched even after a decline; and the warning carries the numbers.
@@ -0,0 +1,80 @@
1
+ # Ask of the host team: the protected system head and a plugin-seeded first message (#105)
2
+
3
+ **Filed 2026-10-24 against `dsh 0.2.1` / session format v4.** Reproduced on the maintainer's own data:
4
+ a `chapters_continue` child (`ch-dc3be3ab-c3d9-4afc-bc53-9994c06600d6`) refuses to open in the UI with
5
+ `SessionFormatError: system/message requires a protected first surface head`, while its 2,924-line log
6
+ decodes cleanly. Nothing about that file is damaged.
7
+
8
+ ## The two kernel rules that cannot both hold for our children
9
+
10
+ 1. **The seed contract**, established by measurement against the installed kernel (`spikes/probe/FINDINGS.md`)
11
+ and recorded in `AGENTS.md` invariant 2: a continuation is
12
+ `agents.create({ sessionId, seed: [noticeEvent] })` with **one synthetic `user/message` at `seq 0`**, a
13
+ producer-owned `source.kind` (`plugin:dsh-chapters`, `form: 'snapshot'`), no `plugin` field (format v4
14
+ retired it and refuses it at row admission), and the kernel's own `permission/preset` / `sandbox/mode` /
15
+ `approval/policy` events appended **after** the seed. Seeding from the parent's events is refused as
16
+ capability-gated, and is the failure mode this plugin exists to avoid.
17
+ 2. **The protected head**, as read out of `@deepseek-ai/dsh-session-format-v2-to-v3/lib/index.js`:
18
+
19
+ ```js
20
+ const SURFACE_TYPES = new Set(["system/message", "user/message", "assistant/message", "tool/result"])
21
+ // inside restoreReleasedV3Artifact's event walk:
22
+ if (system) {
23
+ …
24
+ if (hasSurface && head === void 0)
25
+ throw new SessionFormatError("system/message requires a protected first surface head")
26
+ }
27
+ …
28
+ if (SURFACE_TYPES.has(event.type)) hasSurface = true
29
+ ```
30
+
31
+ `user/message` **is** a surface type. So a kernel-sanctioned seed at `seq 0` sets `hasSurface` before the
32
+ host composes and writes its own `system/message` header at the first step — and at that moment the header
33
+ can no longer claim the protected head, because `head` is only assigned to a `system/message` that arrives
34
+ while `hasSurface` is still false.
35
+
36
+ ## The statistics that make this a rule collision, not a corrupt file
37
+
38
+ 53 durable logs on this machine, first surface event preceding the header:
39
+
40
+ | shape | sessions |
41
+ |---|---|
42
+ | header **is** the first surface event | 50 |
43
+ | a `user/message src=plugin:dsh-chapters` at `seq 0` precedes it | **2** |
44
+
45
+ Both of the 2 are children this plugin created (`ch-dc3be3ab…`, 2,924 events — refused; `ch-c2a7ee0f…`,
46
+ 105 events — same shape, not yet opened). Also observed: that child's header carries `isSeeded: false`.
47
+
48
+ ## What we are asking for
49
+
50
+ **Preferred:** for the protected-head rule, do not treat a `user/message` whose `source.kind` is
51
+ producer-owned plugin (`form: 'snapshot'`) at `seq 0` as a surface event that pre-empts the head. The rule
52
+ exists to protect the system head from being shadowed or replaced; a plugin snapshot that the kernel
53
+ itself invited to `seq 0` is not a competing user turn.
54
+
55
+ **Acceptable alternative:** admit the mid-run header by letting the first `system/message` claim the head
56
+ when the preceding surface events are all plugin-produced seeds, and record the head position as it is
57
+ written.
58
+
59
+ **What would NOT fix it:** telling us to seed nothing. Then a continuation starts with no TOC, which is the
60
+ whole feature. And we cannot write the header ourselves — invariant 3, and a per-session system contribution
61
+ changes every session's header in the profile.
62
+
63
+ ## Why we are not just changing our own shape unilaterally
64
+
65
+ We could seed a non-surface synthetic event instead (the `plan/mode` seed in `docs/plans-carriage.md` § 10
66
+ proves the kernel accepts plugin-authored STATE events in a seed) and deliver the TOC as an appended message
67
+ at the child's first step. We are **not** building that on an assumption: it needs a measurement first —
68
+ does a state-only seed get admitted, and does the header then land as the first surface event so the session
69
+ loads? The spike design is in `spikes/seed-protected-head/` (see `docs/backlog.md`); one boot, zero model
70
+ tokens, a fake provider supplying the turn. If the measurement says the kernel admits it, we ship it behind
71
+ a config flag and the ask above becomes belt-and-braces. If it says otherwise, the ask is the only path, and
72
+ we would rather know that before a user loses a child.
73
+
74
+ ## Side observation, offered as a fact rather than a claim
75
+
76
+ We tried to reproduce the throw by calling `restoreReleasedV3Artifact` directly with the real bytes. Every
77
+ variant — the refused child *and* healthy sessions — failed earlier at `assertV3EventAdmission` with
78
+ `"system message requires plugin source"`. So that module's admission gate is not the path that validated
79
+ this v4 log, even though its message text is what the UI shows. Whatever the actual v4 validator is, the
80
+ statistics above and the rule text are consistent; a pointer to the right entry point would help us pin it.