@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.
- package/LICENSE +14 -0
- package/README.md +76 -2
- package/cordis.patch.yml +363 -0
- package/docs/architecture.md +692 -0
- package/docs/autorun-guide.md +248 -0
- package/docs/backlog.md +2022 -0
- package/docs/contract.md +938 -0
- package/docs/development.md +990 -0
- package/docs/host-compaction-seam.md +225 -0
- package/docs/host-protected-head-ask.md +80 -0
- package/docs/knowledge-repo.md +1332 -0
- package/docs/plans-carriage.md +571 -0
- package/docs/project-state.md +642 -0
- package/docs/provider.md +98 -0
- package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
- package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
- package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
- package/docs/verify.md +561 -0
- package/lib/archive.d.ts +161 -0
- package/lib/archive.js +229 -0
- package/lib/archive.js.map +1 -0
- package/lib/arrival.d.ts +23 -0
- package/lib/arrival.js +125 -0
- package/lib/arrival.js.map +1 -0
- package/lib/artifact-query.d.ts +62 -0
- package/lib/artifact-query.js +187 -0
- package/lib/artifact-query.js.map +1 -0
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +145 -0
- package/lib/auto-save.js +334 -0
- package/lib/auto-save.js.map +1 -0
- package/lib/aux-message.d.ts +17 -0
- package/lib/aux-message.js +20 -0
- package/lib/aux-message.js.map +1 -0
- package/lib/client.js +1419 -0
- package/lib/client.js.map +1 -0
- package/lib/codegraph.d.ts +76 -0
- package/lib/codegraph.js +190 -0
- package/lib/codegraph.js.map +1 -0
- package/lib/commands.d.ts +52 -0
- package/lib/commands.js +946 -0
- package/lib/commands.js.map +1 -0
- package/lib/compose.d.ts +30 -0
- package/lib/compose.js +185 -0
- package/lib/compose.js.map +1 -0
- package/lib/continue-core.d.ts +245 -0
- package/lib/continue-core.js +405 -0
- package/lib/continue-core.js.map +1 -0
- package/lib/diag.d.ts +55 -0
- package/lib/diag.js +186 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +301 -0
- package/lib/engine-core.js +531 -0
- package/lib/engine-core.js.map +1 -0
- package/lib/engine-note.d.ts +35 -0
- package/lib/engine-note.js +32 -0
- package/lib/engine-note.js.map +1 -0
- package/lib/engine.d.ts +329 -0
- package/lib/engine.js +1179 -0
- package/lib/engine.js.map +1 -0
- package/lib/enrich-queue.d.ts +66 -0
- package/lib/enrich-queue.js +126 -0
- package/lib/enrich-queue.js.map +1 -0
- package/lib/enrich-store.d.ts +31 -0
- package/lib/enrich-store.js +103 -0
- package/lib/enrich-store.js.map +1 -0
- package/lib/enrich-wire.d.ts +163 -0
- package/lib/enrich-wire.js +353 -0
- package/lib/enrich-wire.js.map +1 -0
- package/lib/enrich.d.ts +102 -0
- package/lib/enrich.js +267 -0
- package/lib/enrich.js.map +1 -0
- package/lib/gitops.d.ts +107 -0
- package/lib/gitops.js +539 -0
- package/lib/gitops.js.map +1 -0
- package/lib/index.d.ts +241 -0
- package/lib/index.js +1162 -0
- package/lib/index.js.map +1 -0
- package/lib/indexing.d.ts +98 -0
- package/lib/indexing.js +223 -0
- package/lib/indexing.js.map +1 -0
- package/lib/injections.d.ts +90 -0
- package/lib/injections.js +165 -0
- package/lib/injections.js.map +1 -0
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/log-shape.d.ts +38 -0
- package/lib/log-shape.js +93 -0
- package/lib/log-shape.js.map +1 -0
- package/lib/models-command.d.ts +74 -0
- package/lib/models-command.js +100 -0
- package/lib/models-command.js.map +1 -0
- package/lib/notice.d.ts +74 -0
- package/lib/notice.js +92 -0
- package/lib/notice.js.map +1 -0
- package/lib/plans.d.ts +154 -0
- package/lib/plans.js +315 -0
- package/lib/plans.js.map +1 -0
- package/lib/preset.d.ts +186 -0
- package/lib/preset.js +212 -0
- package/lib/preset.js.map +1 -0
- package/lib/prompt-keys.d.ts +7 -0
- package/lib/prompt-keys.js +8 -0
- package/lib/prompt-keys.js.map +1 -0
- package/lib/prompt-loop.d.ts +73 -0
- package/lib/prompt-loop.js +147 -0
- package/lib/prompt-loop.js.map +1 -0
- package/lib/prompt-push.d.ts +47 -0
- package/lib/prompt-push.js +115 -0
- package/lib/prompt-push.js.map +1 -0
- package/lib/prompt-runner.d.ts +48 -0
- package/lib/prompt-runner.js +90 -0
- package/lib/prompt-runner.js.map +1 -0
- package/lib/prompt-state.d.ts +142 -0
- package/lib/prompt-state.js +101 -0
- package/lib/prompt-state.js.map +1 -0
- package/lib/prompt.d.ts +270 -0
- package/lib/prompt.js +408 -0
- package/lib/prompt.js.map +1 -0
- package/lib/provider.d.ts +44 -0
- package/lib/provider.js +66 -0
- package/lib/provider.js.map +1 -0
- package/lib/redact.d.ts +24 -0
- package/lib/redact.js +85 -0
- package/lib/redact.js.map +1 -0
- package/lib/registry.d.ts +187 -0
- package/lib/registry.js +235 -0
- package/lib/registry.js.map +1 -0
- package/lib/removal.d.ts +159 -0
- package/lib/removal.js +556 -0
- package/lib/removal.js.map +1 -0
- package/lib/render.d.ts +97 -0
- package/lib/render.js +530 -0
- package/lib/render.js.map +1 -0
- package/lib/repair-run.d.ts +76 -0
- package/lib/repair-run.js +506 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +208 -0
- package/lib/repair.js +357 -0
- package/lib/repair.js.map +1 -0
- package/lib/repo.d.ts +78 -0
- package/lib/repo.js +184 -0
- package/lib/repo.js.map +1 -0
- package/lib/rules-commands.d.ts +46 -0
- package/lib/rules-commands.js +379 -0
- package/lib/rules-commands.js.map +1 -0
- package/lib/rules.d.ts +267 -0
- package/lib/rules.js +478 -0
- package/lib/rules.js.map +1 -0
- package/lib/save-core.d.ts +72 -0
- package/lib/save-core.js +220 -0
- package/lib/save-core.js.map +1 -0
- package/lib/search.d.ts +112 -0
- package/lib/search.js +282 -0
- package/lib/search.js.map +1 -0
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/signature.d.ts +45 -0
- package/lib/signature.js +199 -0
- package/lib/signature.js.map +1 -0
- package/lib/store.d.ts +156 -0
- package/lib/store.js +368 -0
- package/lib/store.js.map +1 -0
- package/lib/sync.d.ts +220 -0
- package/lib/sync.js +810 -0
- package/lib/sync.js.map +1 -0
- package/lib/tools.d.ts +113 -0
- package/lib/tools.js +1090 -0
- package/lib/tools.js.map +1 -0
- package/lib/treedx/client.d.ts +57 -0
- package/lib/treedx/client.js +82 -0
- package/lib/treedx/client.js.map +1 -0
- package/lib/treedx/provider.d.ts +39 -0
- package/lib/treedx/provider.js +479 -0
- package/lib/treedx/provider.js.map +1 -0
- package/lib/treedx/state.d.ts +27 -0
- package/lib/treedx/state.js +65 -0
- package/lib/treedx/state.js.map +1 -0
- package/lib/types.d.ts +210 -0
- package/lib/types.js +31 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/vocabulary.d.ts +41 -0
- package/lib/vocabulary.js +118 -0
- package/lib/vocabulary.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- 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.
|