@treeseed/dsh-chapters 0.1.0

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 (119) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +644 -0
  3. package/cordis.patch.yml +88 -0
  4. package/docs/architecture.md +460 -0
  5. package/docs/contract.md +375 -0
  6. package/docs/development.md +437 -0
  7. package/docs/host-compaction-seam.md +117 -0
  8. package/docs/knowledge-repo.md +730 -0
  9. package/docs/provider.md +98 -0
  10. package/docs/verify.md +200 -0
  11. package/lib/archive.d.ts +88 -0
  12. package/lib/archive.js +114 -0
  13. package/lib/archive.js.map +1 -0
  14. package/lib/arrival.d.ts +23 -0
  15. package/lib/arrival.js +86 -0
  16. package/lib/arrival.js.map +1 -0
  17. package/lib/artifact-query.d.ts +53 -0
  18. package/lib/artifact-query.js +130 -0
  19. package/lib/artifact-query.js.map +1 -0
  20. package/lib/client.js +234 -0
  21. package/lib/client.js.map +1 -0
  22. package/lib/commands.d.ts +28 -0
  23. package/lib/commands.js +305 -0
  24. package/lib/commands.js.map +1 -0
  25. package/lib/compose.d.ts +30 -0
  26. package/lib/compose.js +131 -0
  27. package/lib/compose.js.map +1 -0
  28. package/lib/continue-core.d.ts +153 -0
  29. package/lib/continue-core.js +252 -0
  30. package/lib/continue-core.js.map +1 -0
  31. package/lib/engine-core.d.ts +219 -0
  32. package/lib/engine-core.js +405 -0
  33. package/lib/engine-core.js.map +1 -0
  34. package/lib/engine.d.ts +211 -0
  35. package/lib/engine.js +571 -0
  36. package/lib/engine.js.map +1 -0
  37. package/lib/enrich-queue.d.ts +66 -0
  38. package/lib/enrich-queue.js +100 -0
  39. package/lib/enrich-queue.js.map +1 -0
  40. package/lib/enrich-store.d.ts +31 -0
  41. package/lib/enrich-store.js +103 -0
  42. package/lib/enrich-store.js.map +1 -0
  43. package/lib/enrich-wire.d.ts +73 -0
  44. package/lib/enrich-wire.js +209 -0
  45. package/lib/enrich-wire.js.map +1 -0
  46. package/lib/enrich.d.ts +73 -0
  47. package/lib/enrich.js +150 -0
  48. package/lib/enrich.js.map +1 -0
  49. package/lib/gitops.d.ts +107 -0
  50. package/lib/gitops.js +539 -0
  51. package/lib/gitops.js.map +1 -0
  52. package/lib/index.d.ts +81 -0
  53. package/lib/index.js +305 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/indexing.d.ts +85 -0
  56. package/lib/indexing.js +212 -0
  57. package/lib/indexing.js.map +1 -0
  58. package/lib/injections.d.ts +66 -0
  59. package/lib/injections.js +83 -0
  60. package/lib/injections.js.map +1 -0
  61. package/lib/notice.d.ts +60 -0
  62. package/lib/notice.js +84 -0
  63. package/lib/notice.js.map +1 -0
  64. package/lib/provider.d.ts +44 -0
  65. package/lib/provider.js +66 -0
  66. package/lib/provider.js.map +1 -0
  67. package/lib/redact.d.ts +24 -0
  68. package/lib/redact.js +85 -0
  69. package/lib/redact.js.map +1 -0
  70. package/lib/registry.d.ts +120 -0
  71. package/lib/registry.js +130 -0
  72. package/lib/registry.js.map +1 -0
  73. package/lib/render.d.ts +40 -0
  74. package/lib/render.js +280 -0
  75. package/lib/render.js.map +1 -0
  76. package/lib/repo.d.ts +78 -0
  77. package/lib/repo.js +184 -0
  78. package/lib/repo.js.map +1 -0
  79. package/lib/rules-commands.d.ts +24 -0
  80. package/lib/rules-commands.js +138 -0
  81. package/lib/rules-commands.js.map +1 -0
  82. package/lib/rules.d.ts +90 -0
  83. package/lib/rules.js +185 -0
  84. package/lib/rules.js.map +1 -0
  85. package/lib/search.d.ts +48 -0
  86. package/lib/search.js +152 -0
  87. package/lib/search.js.map +1 -0
  88. package/lib/signature.d.ts +28 -0
  89. package/lib/signature.js +149 -0
  90. package/lib/signature.js.map +1 -0
  91. package/lib/store.d.ts +130 -0
  92. package/lib/store.js +235 -0
  93. package/lib/store.js.map +1 -0
  94. package/lib/sync.d.ts +183 -0
  95. package/lib/sync.js +719 -0
  96. package/lib/sync.js.map +1 -0
  97. package/lib/tools.d.ts +84 -0
  98. package/lib/tools.js +581 -0
  99. package/lib/tools.js.map +1 -0
  100. package/lib/treedx/client.d.ts +57 -0
  101. package/lib/treedx/client.js +82 -0
  102. package/lib/treedx/client.js.map +1 -0
  103. package/lib/treedx/provider.d.ts +39 -0
  104. package/lib/treedx/provider.js +479 -0
  105. package/lib/treedx/provider.js.map +1 -0
  106. package/lib/treedx/state.d.ts +27 -0
  107. package/lib/treedx/state.js +65 -0
  108. package/lib/treedx/state.js.map +1 -0
  109. package/lib/types.d.ts +100 -0
  110. package/lib/types.js +19 -0
  111. package/lib/types.js.map +1 -0
  112. package/lib/vocabulary.d.ts +41 -0
  113. package/lib/vocabulary.js +118 -0
  114. package/lib/vocabulary.js.map +1 -0
  115. package/package.json +132 -0
  116. package/presets/chapters/agent.cordis.yml +272 -0
  117. package/presets/chapters/preset.yml +3 -0
  118. package/presets/probe-roots/agent.cordis.yml +11 -0
  119. package/presets/probe-roots/preset.yml +3 -0
@@ -0,0 +1,375 @@
1
+ # DSH Plugin Contract
2
+
3
+ Verified against `examples/deepseek-harness` and the reference plugins. Every claim here cites a file
4
+ so it can be re-checked after an upgrade. **Nothing in this file is inferred.**
5
+
6
+ ## Three Schema Systems
7
+
8
+ Conflating these is the top time-waster in this project.
9
+
10
+ | Declaring | Format | Import |
11
+ |---|---|---|
12
+ | Plugin `Config` | `Schema.object({...})` | `import Schema from '@deepseek-ai/schemastery'` — **default** |
13
+ | Tool `parameters` | plain declarative spec (`ParameterSchemaSpec`) | none; `defineTool` from `@deepseek-ai/dsh-tools` |
14
+ | Storage-domain records | zod | `import { z } from 'zod'` |
15
+
16
+ Plain objects are *required* for tool parameters and *rejected* for plugin config.
17
+
18
+ `import { z } from '@deepseek-ai/schemastery'` **does not exist** — 0 occurrences upstream versus 128
19
+ default imports. Copying the named form fails to build.
20
+
21
+ ## Config
22
+
23
+ Shape copied from `examples/dsh-compact/src/index.ts:15,26-37`.
24
+
25
+ ```ts
26
+ import Schema from '@deepseek-ai/schemastery'
27
+
28
+ export interface Config {
29
+ continuationBudgetRatio: number
30
+ chapterTokenTarget: number
31
+ toolResultDeferFloorTokens: number
32
+ artifactStoreRoot: string
33
+ tocBulletBudgetTokens: number
34
+ pressureSuggestionRatio: number
35
+ }
36
+
37
+ // Per-model, mirroring the host's `modelPolicies[]` — do NOT invent a flat project-wide ratio.
38
+ export const Config: Schema<Config> = Schema.object({
39
+ compactAtRatio: Schema.number().default(0.90), // host thresholdRatio, ours defaults higher
40
+ continuationBudgetRatio: Schema.number().default(0.25), // of the window REMAINING after the header
41
+ chapterTokenTarget: Schema.number().default(8000),
42
+ toolResultDeferFloorTokens: Schema.number().default(200), // a floor, not the policy — see below
43
+ artifactStoreRoot: Schema.string().default('.dsh-chapters'),
44
+ tocBulletBudgetTokens: Schema.number().default(2000),
45
+ pressureSuggestionRatio: Schema.number().default(0.75),
46
+ })
47
+ ```
48
+
49
+ These are **defaults overridable per model**, matching the host's own `modelPolicies: z.array(modelPolicy)`
50
+ (`compaction-basic/src/index.ts:113`) — some models have huge windows, some have 32K, and one flat number
51
+ serves neither. Resolve the window with `ctx.llm.resolveModelInfo()` and keep our own keys alongside the
52
+ host's rather than shadowing them. Three notes on the defaults, so nobody trusts them as settled:
53
+
54
+ - `continuationBudgetRatio` is a share of **(window − system prompt tokens)**, never of the whole window.
55
+ See `docs/architecture.md § Budgets`. Measure the header once per process generation and cache it.
56
+ - `toolResultDeferFloorTokens` is **a floor, not the policy.** The keep/defer decision belongs to the model
57
+ (`toolResultOverrides` below); the floor exists only so trivial results stay inline without being
58
+ itemized, keeping the chapter's narrative readable and the exception list sparse. The number is a guess
59
+ to calibrate in Phase 1 — too high and meaningful short outputs get deferred, too low and chapters bloat.
60
+ - **One exception since 2026-09-19:** `toolResultArtifactTokens` IS a policy — a hard band for results too
61
+ large to be reasoning context at all (see architecture.md's arrival-time amendment). It intercepts at
62
+ the tail node before the next request composes (the pruner's mid-history rewrite is NOT this); the
63
+ model still judges everything below the floor, at archive time, exactly as before. Queries go through
64
+ `chapters_artifact` (toc | search | read), which is read-only and store-contained by construction.
65
+ - There is deliberately **no maximum.** Deferral is a judgement with a size floor; over-sized output is
66
+ caught by the continuation budget and by the chapter split signal, both of which refuse or report rather
67
+ than silently clip.
68
+
69
+ Note `forkThresholdRatio` from earlier drafts is renamed to `pressureSuggestionRatio`: with the reordered
70
+ plan it gates a *suggestion*, not an action.
71
+
72
+ ## Entry Point
73
+
74
+ ```ts
75
+ export const name = 'dsh-chapters' // must match package.json and cordis.patch.yml
76
+ export const inject = [/* service names */]
77
+ export const Config: Schema<Config> = …
78
+ export function apply(ctx: Context, config: Config) { … }
79
+ ```
80
+
81
+ ## Services (`inject`)
82
+
83
+ `dsh-session-fork/src/index.ts:161` shows the full menu: `commands`, `storageDomain`, `sessions`,
84
+ `sessionPersistence`, `sessionQuery`, `agents`, `tokenMeter`, `llm`, `sessionController`, `connection`,
85
+ `tools`, `systemPrompt`.
86
+
87
+ Ours, minimal and honest:
88
+
89
+ ```ts
90
+ export const inject = [
91
+ 'sessions', 'sessionPersistence', 'sessionQuery', 'storageDomain',
92
+ 'agents', 'tokenMeter', 'llm', 'tools',
93
+ ]
94
+ ```
95
+
96
+ - `agents` — `ctx.agents.create`, the continuation primitive. **The load-bearing one.**
97
+ - `storageDomain` — chain registry.
98
+ - `sessionQuery` — `observeSession` for the calling agent's own log and preset projection.
99
+ - `tokenMeter` — pressure math and the continuation budget preflight.
100
+ - **`systemPrompt` is excluded on purpose** (invariant 3).
101
+ - Add `commands` at the UI/CLI trigger phase; `connection` / `sessionController` only if a create path
102
+ turns out to need the client RPC round-trip.
103
+
104
+ Declare only what you use — `dsh-compact` gets by on `inject = ["commands"]`.
105
+
106
+ ## Tools
107
+
108
+ `defineTool` at `packages/core/tools/src/schema.ts:545`:
109
+ `defineTool<const S extends ParameterSchemaSpec, const O extends ValueSchemaSpec>(...)`.
110
+
111
+ **`output` is mandatory** on every `ToolDefinition` — the `O` — and needs `{ schema, render }`. Idiom
112
+ from `dsh-session-fork/src/tools.ts:110-121`:
113
+
114
+ ```ts
115
+ function jsonOutput<T>(schema: ValueSchemaSpec) {
116
+ return { schema, render: (_args: unknown, value: T) => [{ type: 'text', text: JSON.stringify(value) }] }
117
+ }
118
+ ```
119
+
120
+ Definitions (names pending the rename decision — see `docs/architecture.md § Naming`):
121
+
122
+ ```ts
123
+ const chaptersContinue = defineTool({
124
+ name: 'chapters_continue',
125
+ description:
126
+ 'Archive this conversation as Markdown chapters and open a new session that starts from their '
127
+ + 'table of contents. This session is left untouched. Supply event ranges, not text.',
128
+ parameters: {
129
+ title: { type: 'string', required: true, description: 'Name for the continued session.' },
130
+ handoffNote: { type: 'string', required: true, description: 'State of play: what was in progress, decisions held, next step. As long as it needs to be — it is budget-checked, not truncated.' },
131
+ chapters: {
132
+ type: 'array', required: true,
133
+ description: 'Ranges ascending by startSeq, non-overlapping, all <= archiveCeiling, covering every event below the ceiling.',
134
+ },
135
+ },
136
+ output: jsonOutput(CHAPTERS_CONTINUE_RESULT),
137
+ async execute(args, exec) {
138
+ // caller agent from `exec`; refuse cleanly when absent
139
+ },
140
+ })
141
+ ```
142
+
143
+ Register host-side: `ctx.tools.register(tool)`. Call-site precedent:
144
+ `dsh-session-fork/src/index.ts:786`.
145
+
146
+ ## LLM Calls
147
+
148
+ Segmentation and summaries go through **`ctx.llm.stream()`** — the concrete method, not `ctx.llm`
149
+ generically. Window sizes via `ctx.llm.resolveModelInfo(provider, model, signal)`.
150
+
151
+ The cache-friendly pattern — a stream call reusing the conversation's own system prompt as its prefix —
152
+ is at `packages/compaction/compaction-basic/src/index.ts:225,290`. Copy that for segmentation so the
153
+ pass is served from cache.
154
+
155
+ ## Session Creation
156
+
157
+ **Never** `ctx.sessions.fork(...)`. *(Corrected by Phase 0 measurement — an earlier draft of this file
158
+ claimed the method was client-only. It is not.)* `ctx.sessions` does expose `fork`, `_forkSeed` and
159
+ `_resolveForkSource` host-side; calling `fork` from a plugin is refused:
160
+
161
+ > `session fork is owned by session-controller; ambient plugin access is denied`
162
+
163
+ So the prohibition holds for a better reason than we stated: the copy-the-log path is capability-gated
164
+ away from plugins, and the kernel handler seeds with
165
+ `commands.ts:260` → `seed: source.events.slice(0, cut)` (invariant 2). Do not go looking for a way
166
+ through the gate — `agents.create` is the sanctioned seam.
167
+
168
+ Ours — `seed` is optional per `packages/core/agent/src/index.ts:95`
169
+ (`readonly seed?: readonly SessionEvent[]`); the multi-event and one-event seed contracts are measured
170
+ in [spikes/probe/FINDINGS.md](../spikes/probe/FINDINGS.md). **Phase 0b rewrote this call**: `agents.create`
171
+ composes NO preset — `meta.agentPreset` is a durable label only (r15: child first request 974 tokens,
172
+ `tools: 0`, a tool-less agent that cannot `read` its own chapters). Composition goes through the `setup`
173
+ callback exactly as session-controller's `composeAgent` does it
174
+ (`api/session-controller/src/agent.ts:377-390`; the service itself says so at
175
+ `preset/agent-presets/src/index.ts:221`):
176
+
177
+ ```ts
178
+ ctx.agents.create({
179
+ sessionId: newId,
180
+ seed: [tocNoticeEvent], // exactly one event at seq 0 (invariant 2)
181
+ inheritedEventCount: 0,
182
+ meta: { cwd: parentCwd, agentPreset: presetId },
183
+ agentOptions: ctx.get('agentDefaultModel').currentSelection(),
184
+ setup: async (agentCtx) => { await ctx.get('agentPresets').mount(agentCtx, presetId) },
185
+ })
186
+ // then attach to the parent's workspace (r6 lesson: unattached sessions list nowhere)
187
+ ```
188
+
189
+ Without `agentOptions` every turn dies pre-provider (`{{model}}` no value, r10); without `setup` the
190
+ child has no tools (r15); with both, r16 measured the real composed head: 13,315 tokens / 27 tools.
191
+ Carry over from `dsh-session-fork`: `src/vendor/fork.ts` (`forkWorkspace`, `composeAgent`,
192
+ `readSessionState`) already follows this shape. `branch.ts:10-14` warns that kernel `SessionStore.fork`
193
+ yields fiber-scoped sessions evicted from the sidebar — `agents.create` is the only durable, listed,
194
+ resumable route. *(Phase 0 confirmed: durable, listed, resumable, byte-identical across a kill.)*
195
+
196
+ Keep `anchoredBoundaryOf` (`vendor/fork.ts:213`) with its role changed: it yields the **archive ceiling**
197
+ (`boundarySeq`, `cut`) for *what to render*, never for seeding.
198
+
199
+ Notice event shape — built and vocabulary-pinned by `src/notice.ts` (+ `test/notice.test.ts`): one
200
+ `user/message`, `surfaceOp: 'append'`, at `seq = 0`, `source { kind: 'plugin', plugin: 'dsh-chapters',
201
+ form: 'snapshot', sections: [{ name: 'chapters:toc', text }] }`. Continuity validation in `Session.create`
202
+ requires the seq to be the next contiguous seed position (rejection message measured in r5).
203
+
204
+ ## Compaction Engine Integration (Phase 0b)
205
+
206
+ The subclass seam, with everything measured by rounds 12-17 (sources: the installed
207
+ `@deepseek-ai/dsh-compaction-basic@0.1.5-rc.2`; same tree in `examples/deepseek-harness`):
208
+
209
+ - `class X extends BasicCompactionEngine` (`export default` it; the loader mounts class plugins directly)
210
+ and override `protected summarize(input, agent, signal?)` — the **sole** customization hook; `compactRegion`,
211
+ `compactIfNeeded`, `compactNow` stay inherited. Subclassing inherits `static inject
212
+ = ['llm','tokenMeter','sessions']` — extend it or `ctx.agents` throws *cannot get property "agents" without
213
+ inject* (r12 crash).
214
+ - Deterministic summaries use the unmarked `SummaryResult` branch: `{ summary, provider, model }` with
215
+ **no `llmStreamCall`** — the union explicitly admits "template, remote, or other summarizer"
216
+ (`compaction-basic/src/summarizer.ts:100-106`); the durable `compaction/summary` schema matches
217
+ (`compaction/src/types.ts`). Omit `usage` — its absence is the zero-token record (r12 verified).
218
+ - `summarize()` gets content, not seqs: `SummarizationInput = { tools?, messages }` where messages are the
219
+ system head (when present) + the region's derived messages (`region.ts:545`). Correlate chapter files
220
+ to ranges **post-commit** from `CompactionResult.shadowedSeqs` / the `compaction/summary` event — and
221
+ remember the manual path calls `compactSurfaceRegion` directly (`index.ts:382`), so overriding
222
+ `compactRegion` sees only the automatic paths.
223
+ - The kernel enforces the transaction: `compaction/start` lock, `SurfaceChangedError` → `changed`,
224
+ shrink floor `framed ≥ priced` → refuse (`code='summary'`, r12), replacement `user/message` carries
225
+ `source { kind: 'plugin', plugin: 'compact', compactionId }` — a NEW vocabulary member to pin against
226
+ (our notices use `plugin: 'dsh-chapters'`; the checkpoint's is the host's own).
227
+ - Config: `retainRatio ∈ (0, 1]` (rc.2 validates; boot dies), `retainTokens ≥ 0`, `retainRatio <
228
+ thresholdRatio`, `modelPolicies[]` per exact target, `auto` default true. Mounting shape per
229
+ `docs/host-compaction-seam.md`: OUR row goes inside a preset's `cordis:group` realm
230
+ (`isolate: { compaction: true }`), where `ctx.get('toolResultPruner')` also resolves in-realm —
231
+ the standard preset deliberately mounts basic + command-compact + pruner as one group.
232
+ - Cross-copy identity rules (we ship our own `0.1.5-rc.2` deps; the host runs its copies):
233
+ `ctx.get('compaction')` returns the host's receiver proxy — property reads work, `=== this` never
234
+ (r12); duplicate provision of `compaction` on one plane fails boot loudly (r14); a host-plane
235
+ `command-compact` does not classify OUR `ManualCompactionError` (different class objects) — put the
236
+ command in our own realm rows or ship our command (r12 §J).
237
+
238
+ ### Mounting shape, measured (stages 0-3)
239
+
240
+ - Realm rows resolve **subpath exports**: `name: dsh-chapters/engine` →
241
+ `exports["./engine"]` (r18). Host-plane mounting is the wrong shape (double-fire
242
+ with preset realms; the kernel's duplicate guard is same-plane only).
243
+ - The loader passes row config to constructors **unstripped**, and the base's
244
+ `resolveConfig` **rejects unknown keys at runtime** (`BasicCompactionConfig:
245
+ unknown key "artifactStoreRoot"`, r18) — peel engine-specific keys in the
246
+ subclass constructor before `super(ctx, baseConfig)`.
247
+ - Preset delivery: profile `!!js` evaluates `new Function("ctx", …) with (ctx)` —
248
+ `dshHomePath`/`process` reachable, **`require` is not** (r20 boot-killer) — so a
249
+ patch cannot name a package-relative `agentPresets` roots path. The plugin
250
+ copies its `presets/chapters/` into `$DSH_HOME/.agent-presets/` at boot,
251
+ write-if-missing (`installChaptersPreset`, schemastery default import —
252
+ version line `@deepseek-ai/schemastery@3.x`, NOT host rc.x).
253
+ - `ctx.logger` output is invisible to headless `dsh web` (r19); engine failures
254
+ mirror to `$DSH_CHAPTERS_ENGINE_ERRORS` when set. Engine finalization runs on
255
+ BOTH the success and rejection paths of `compactIfNeeded`/`compactNow` — the
256
+ retry loop can commit then throw, and success-path-only finalization strands
257
+ committed records (measured: 3 plans / 1 finalized / no errors).
258
+ - `@deepseek-ai/dsh-home-paths` exports `dshHomePath(...segments)` — the sanctioned
259
+ way to reach `$DSH_HOME` from plugin code (host rc.2 line).
260
+
261
+ ## Lineage: Kernel Fields We Must Not Lean On
262
+
263
+ `SessionHeader` (`packages/core/session/src/types.ts:93-122`) carries `version`, `id`, `createdAt`,
264
+ `cwd`, `parentSession`, `isSeeded`, `origin`, `delegationDepth` — and **no system prompt or tool
265
+ schemas**.
266
+
267
+ - `isSeeded` means "contains a fork-inherited event prefix"; our one-event synthetic seed is not that.
268
+ - `parentSession` is documented as *seed lineage*. Setting it to fake a relationship would be a misuse.
269
+ - Therefore **our storage-domain registry is the sole authority on ancestry**, and the web UI's branch
270
+ graph will show our child as an unrelated root. A chain/manifest view is this plugin's job.
271
+ - The header being composed per request from the preset plus plugin contributions is *why* invariant 3
272
+ holds, and why cache validity is per-process-generation: a restart or config edit recomposes it for
273
+ everyone.
274
+
275
+ ## Storage Domain Registry
276
+
277
+ Precedent verbatim: `examples/dsh-session-fork/src/store.ts:36-43`.
278
+
279
+ ```ts
280
+ import { z } from 'zod'
281
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
282
+
283
+ const chapterRecord = z.object({
284
+ id: z.string(), // `${authorSession}/${n}`
285
+ authorSession: z.string(), // keying by creator, not by a linear chain
286
+ path: z.string(),
287
+ startSeq: z.number().int().nonnegative(),
288
+ endSeq: z.number().int().nonnegative(),
289
+ sha256: z.string(), // integrity: verified before citing
290
+ tokens: z.number().int().nonnegative(),
291
+ summary: z.string(),
292
+ createdAt: z.string(),
293
+ })
294
+
295
+ const sessionState = z.object({
296
+ parentSession: z.string().nullable(),
297
+ rootSession: z.string(),
298
+ nextChapterNumber: z.number().int().nonnegative(),
299
+ chapters: z.array(chapterRecord),
300
+ })
301
+
302
+ export const chapterDomainSpec = defineDomain({
303
+ name: 'dsh_chapters',
304
+ version: 0,
305
+ tables: { sessions: domainTable<string, SessionState>(sessionState) },
306
+ })
307
+ ```
308
+
309
+ Open with `ctx.storageDomain.open(spec)` → `table(name).get/put/entries/size`. `put` reaches backend
310
+ durability before resolving, so a resolved save survives restart. Backend is JSON under
311
+ `~/.dsh/storages`.
312
+
313
+ ## Chapter File Format: Markdown + Frontmatter, Not XML
314
+
315
+ Decision with reasoning in `docs/architecture.md § File Format`. Short form: `.md` with YAML frontmatter.
316
+
317
+ ```markdown
318
+ ---
319
+ chapter: "003-database-schema"
320
+ title: "Database Schema"
321
+ session: 01HTZ6Q8K3
322
+ root: 01HTAB12CD7
323
+ seqRange: [184, 231]
324
+ sha256: 6d59…
325
+ tokens: 7420
326
+ generatedAt: 2026-09-15T17:22:04Z
327
+ artifacts: 2
328
+ ---
329
+
330
+ # Database Schema
331
+
332
+ **User:** Let's design the user table…
333
+ ```
334
+
335
+ Frontmatter carries every machine-queriable fact a parser would want. The body stays token-dense prose.
336
+ The `read` tool returns both; the harness's own skill loader uses this same frontmatter-plus-body idiom,
337
+ so it is an established convention here. **Do not convert chapters to XML** — see
338
+ `docs/architecture.md § File Format` for the full rationale and the one case that would reopen it.
339
+
340
+ ## Packaging
341
+
342
+ `cordis.patch.yml` is required for the bundle to mount — a one-entry list that `insert`s the package by
343
+ name (compare `examples/dsh-session-fork/cordis.patch.yml`). Its `name` must match `package.json`. In
344
+ `package.json` the patch appears in **three** places:
345
+
346
+ ```jsonc
347
+ "main": "lib/index.js",
348
+ "exports": { ".": {…}, "./cordis.patch.yml": "./cordis.patch.yml", "./package.json": "./package.json" },
349
+ "files": ["lib", "cordis.patch.yml"],
350
+ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
351
+ ```
352
+
353
+ Entry `apply(ctx, config)`. Build with `tsc -p tsconfig.json` and **build before installing**. A client
354
+ UI (later phase) additionally needs `dsh.client` and a `tsdown.client.config.ts` — see
355
+ `dsh-session-fork/package.json` for the shape.
356
+
357
+ ## Trigger Constraints
358
+
359
+ Two different actions, two different timings — Phase 0b measured the seam so this split is now factual,
360
+ not stylistic:
361
+
362
+ - **In-place compaction** (the engine's job) fires at the base's own timings, which we inherit:
363
+ `agent/pre-step` pressure and `agent/request-error` overflow recovery
364
+ (`compaction-basic/src/index.ts:144,176`). Pre-step is BETWEEN steps, never mid-request; the pressure
365
+ loop is retry-bounded and its failures are caught, warned, and the turn continues (r13 watched it).
366
+ An earlier draft called `agent/pre-step` "mid-turn, breaks the in-flight request" — the shipped design
367
+ contradicts that reading; the harness itself compacts there, replay-safely. One structural limit worth
368
+ remembering: `compactIfNeeded` needs a durable routed request, so the FIRST step of a fresh session can
369
+ never compact (index.ts:260, measured r13).
370
+ - **Session creation** (`chapters_continue`, `chapters_fork`) fires at `turn/end` boundaries only — an
371
+ archive ceiling is a completed `turn/end` by construction, and an in-flight exchange belongs to the
372
+ handoff note, never a mid-turn cut.
373
+ - Manual is MVP. The button path cannot supply model-authored titles or ranges, so segmentation must be
374
+ a plain function callable **without an agent turn** — build that seam in Phase 1, not Phase 5, or the
375
+ UI and automatic paths both force a rewrite.