@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.
- package/LICENSE +14 -0
- package/README.md +644 -0
- package/cordis.patch.yml +88 -0
- package/docs/architecture.md +460 -0
- package/docs/contract.md +375 -0
- package/docs/development.md +437 -0
- package/docs/host-compaction-seam.md +117 -0
- package/docs/knowledge-repo.md +730 -0
- package/docs/provider.md +98 -0
- package/docs/verify.md +200 -0
- package/lib/archive.d.ts +88 -0
- package/lib/archive.js +114 -0
- package/lib/archive.js.map +1 -0
- package/lib/arrival.d.ts +23 -0
- package/lib/arrival.js +86 -0
- package/lib/arrival.js.map +1 -0
- package/lib/artifact-query.d.ts +53 -0
- package/lib/artifact-query.js +130 -0
- package/lib/artifact-query.js.map +1 -0
- package/lib/client.js +234 -0
- package/lib/client.js.map +1 -0
- package/lib/commands.d.ts +28 -0
- package/lib/commands.js +305 -0
- package/lib/commands.js.map +1 -0
- package/lib/compose.d.ts +30 -0
- package/lib/compose.js +131 -0
- package/lib/compose.js.map +1 -0
- package/lib/continue-core.d.ts +153 -0
- package/lib/continue-core.js +252 -0
- package/lib/continue-core.js.map +1 -0
- package/lib/engine-core.d.ts +219 -0
- package/lib/engine-core.js +405 -0
- package/lib/engine-core.js.map +1 -0
- package/lib/engine.d.ts +211 -0
- package/lib/engine.js +571 -0
- package/lib/engine.js.map +1 -0
- package/lib/enrich-queue.d.ts +66 -0
- package/lib/enrich-queue.js +100 -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 +73 -0
- package/lib/enrich-wire.js +209 -0
- package/lib/enrich-wire.js.map +1 -0
- package/lib/enrich.d.ts +73 -0
- package/lib/enrich.js +150 -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 +81 -0
- package/lib/index.js +305 -0
- package/lib/index.js.map +1 -0
- package/lib/indexing.d.ts +85 -0
- package/lib/indexing.js +212 -0
- package/lib/indexing.js.map +1 -0
- package/lib/injections.d.ts +66 -0
- package/lib/injections.js +83 -0
- package/lib/injections.js.map +1 -0
- package/lib/notice.d.ts +60 -0
- package/lib/notice.js +84 -0
- package/lib/notice.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 +120 -0
- package/lib/registry.js +130 -0
- package/lib/registry.js.map +1 -0
- package/lib/render.d.ts +40 -0
- package/lib/render.js +280 -0
- package/lib/render.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 +24 -0
- package/lib/rules-commands.js +138 -0
- package/lib/rules-commands.js.map +1 -0
- package/lib/rules.d.ts +90 -0
- package/lib/rules.js +185 -0
- package/lib/rules.js.map +1 -0
- package/lib/search.d.ts +48 -0
- package/lib/search.js +152 -0
- package/lib/search.js.map +1 -0
- package/lib/signature.d.ts +28 -0
- package/lib/signature.js +149 -0
- package/lib/signature.js.map +1 -0
- package/lib/store.d.ts +130 -0
- package/lib/store.js +235 -0
- package/lib/store.js.map +1 -0
- package/lib/sync.d.ts +183 -0
- package/lib/sync.js +719 -0
- package/lib/sync.js.map +1 -0
- package/lib/tools.d.ts +84 -0
- package/lib/tools.js +581 -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 +100 -0
- package/lib/types.js +19 -0
- package/lib/types.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/package.json +132 -0
- package/presets/chapters/agent.cordis.yml +272 -0
- package/presets/chapters/preset.yml +3 -0
- package/presets/probe-roots/agent.cordis.yml +11 -0
- package/presets/probe-roots/preset.yml +3 -0
package/docs/contract.md
ADDED
|
@@ -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.
|