@treeseed/dsh-chapters 0.1.22 → 0.1.24
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/cordis.patch.yml +5 -0
- package/docs/architecture.md +30 -0
- package/docs/contract.md +10 -3
- package/docs/development.md +14 -0
- package/docs/plans-carriage.md +1 -1
- package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
- package/lib/arrival.js +2 -1
- package/lib/arrival.js.map +1 -1
- package/lib/auto-save.d.ts +71 -0
- package/lib/auto-save.js +182 -0
- package/lib/auto-save.js.map +1 -0
- package/lib/commands.d.ts +3 -0
- package/lib/commands.js +6 -2
- package/lib/commands.js.map +1 -1
- package/lib/continue-core.js +14 -0
- package/lib/continue-core.js.map +1 -1
- package/lib/engine-core.js +7 -0
- package/lib/engine-core.js.map +1 -1
- package/lib/engine.js +2 -2
- package/lib/engine.js.map +1 -1
- package/lib/index.d.ts +8 -0
- package/lib/index.js +36 -1
- package/lib/index.js.map +1 -1
- package/lib/injections.js +6 -0
- package/lib/injections.js.map +1 -1
- package/lib/notice.d.ts +41 -27
- package/lib/notice.js +24 -16
- package/lib/notice.js.map +1 -1
- package/lib/render.js +7 -0
- package/lib/render.js.map +1 -1
- package/lib/rules-commands.js +4 -3
- package/lib/rules-commands.js.map +1 -1
- package/lib/save-core.d.ts +42 -0
- package/lib/save-core.js +84 -0
- package/lib/save-core.js.map +1 -0
- package/lib/store.d.ts +5 -0
- package/lib/store.js +11 -2
- package/lib/store.js.map +1 -1
- package/lib/sync.js +5 -4
- package/lib/sync.js.map +1 -1
- package/lib/tools.js +36 -63
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +19 -0
- package/lib/types.js +12 -0
- package/lib/types.js.map +1 -1
- package/package.json +2 -1
package/cordis.patch.yml
CHANGED
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
# mergeThreshold: 0.3 # τ for merging turns into one chapter at
|
|
60
60
|
# # archive time. Composition is archive-time
|
|
61
61
|
# # only: tune for future archives.
|
|
62
|
+
# subagentAutoSave: true # archive a subagent's completed turns into
|
|
63
|
+
# # searchable chapters when its run settles
|
|
64
|
+
# # (save-only: no session, no compaction;
|
|
65
|
+
# # enrichment queued, never per-child).
|
|
66
|
+
# # false = listener never registers.
|
|
62
67
|
#
|
|
63
68
|
# # ---- context economy -------------------------------------------------
|
|
64
69
|
# toolResultDeferFloorTokens: 200 # archive-time inline floor
|
package/docs/architecture.md
CHANGED
|
@@ -49,6 +49,11 @@ What we *do* take from the XML instinct, because it was a real concern:
|
|
|
49
49
|
|
|
50
50
|
- **Frontmatter carries the parseable structure** — `chapter`, `session`, `root`, `seqRange`, `sha256`,
|
|
51
51
|
`tokens`, `artifacts`. Queryable with a YAML parser, invisible to the reading experience.
|
|
52
|
+
Chapters authored by a **subagent** carry two more lines — `subagent: true` and
|
|
53
|
+
`parentSession: <id>` — copied from the child's durable session header, which the host itself
|
|
54
|
+
stamps (`childSessionMeta`: `parentSession`, `origin: subagent`, `delegationDepth`, `agentPreset`;
|
|
55
|
+
measured, `spikes/subagent-end/FINDINGS.md`). Emitted ONLY when the header has `parentSession`,
|
|
56
|
+
so existing chapter bytes never change — which is also why no tape needs re-recording.
|
|
52
57
|
- **There is one index, not three.** The TOC lists the chapters for reading; the storage domain already
|
|
53
58
|
holds the machine-readable enumeration. A separate `MANIFEST.md` was proposed and **rejected as
|
|
54
59
|
redundant** — a regenerated duplicate that can drift, whose growth becomes a context cost when read
|
|
@@ -213,6 +218,31 @@ is **going on**, authored while the model still holds the thread:
|
|
|
213
218
|
- Invariant check: the model authors *state*, never archive text (invariant 4 intact); the note rides
|
|
214
219
|
the replacement event append-only; every byte of the conversation still lives in chapters.
|
|
215
220
|
|
|
221
|
+
## The archive moments
|
|
222
|
+
|
|
223
|
+
Chapters are written by exactly five doors, and knowledge that trips none of them is knowledge
|
|
224
|
+
that never enters the index. Four are session-facing, all idempotent over a per-session watermark
|
|
225
|
+
(`fromSeq = max(chapter.endSeq) + 1`) and anchored on the last **completed** turn (`turn/end`):
|
|
226
|
+
|
|
227
|
+
1. `chapters_continue` (tool) and `/chapters-continue` (deterministic door) — archive + child session.
|
|
228
|
+
2. `chapters_fork` (tool) and `/chapters-fork` (door) — archive + branch session (no new bytes).
|
|
229
|
+
3. `/chapters-save` — archive only, parent untouched.
|
|
230
|
+
4. Compaction finalize — the engine's `summarize()` commits a plan pre-durable, and the post-commit
|
|
231
|
+
finalizer writes the chapter (issue #47 note: the fanout field study shows this is the ONLY one
|
|
232
|
+
of these that subagents historically reached — 20 child compactions, or nothing).
|
|
233
|
+
5. **Subagent settlement (issue #47, this round)** — a host-plane listener on the published
|
|
234
|
+
`subagent/end` lifecycle event runs the SAME save pipeline (one implementation, shared with
|
|
235
|
+
door 3 by extraction — never a second renderer) for children whose header carries
|
|
236
|
+
`origin: subagent` and `agentPreset === chapters`. The emitter fires `end` synchronously before
|
|
237
|
+
the holder's dispose continuation (measured, spike), so the fast path is a synchronous
|
|
238
|
+
`agents.get(info.id).session.snapshotEvents()`; the fallback arm is `sessionQuery.listEvents`
|
|
239
|
+
over the durable log (ambient-readable — measured); when neither reads, the child is skipped
|
|
240
|
+
with a loud diagnostic, never guessed. Save-only, so the "no continuation-creating actions
|
|
241
|
+
mid-step" rule is not engaged: no session is created, no surface is compacted. Enrichment is
|
|
242
|
+
**queued** per settlement — a 10-child fanout must not `runNow` ten annotator passes. A
|
|
243
|
+
continuable child settles repeatedly; the watermark makes every repeat after the first a
|
|
244
|
+
save of only-new-turns. Config: `subagentAutoSave` (default on).
|
|
245
|
+
|
|
216
246
|
## Budgets and the Reserve Margin
|
|
217
247
|
|
|
218
248
|
Two distinct ceilings. Conflating them caused the original design's silent-failure mode.
|
package/docs/contract.md
CHANGED
|
@@ -196,9 +196,16 @@ resumable route. *(Phase 0 confirmed: durable, listed, resumable, byte-identical
|
|
|
196
196
|
Keep `anchoredBoundaryOf` (`vendor/fork.ts:213`) with its role changed: it yields the **archive ceiling**
|
|
197
197
|
(`boundarySeq`, `cut`) for *what to render*, never for seeding.
|
|
198
198
|
|
|
199
|
-
Notice event shape — built and vocabulary-pinned by `src/notice.ts` (+ `test/notice.test.ts
|
|
200
|
-
`user/message`, `surfaceOp: 'append'`,
|
|
201
|
-
form: 'snapshot', sections: [{ name: 'chapters:toc', text }] }
|
|
199
|
+
Notice event shape — built and vocabulary-pinned by `src/notice.ts` (+ `test/notice.test.ts`, + the
|
|
200
|
+
v4 admission rows in `tests/unit/host-seed-contract.test.ts`): one `user/message`, `surfaceOp: 'append'`,
|
|
201
|
+
at `seq = 0`, `source { kind: 'plugin:dsh-chapters', form: 'snapshot', sections: [{ name: 'chapters:toc', text }] }`
|
|
202
|
+
— **producer-owned kind, no `plugin` field**. Session format v4 (host 0.2.x, measured 2026-10-18 on
|
|
203
|
+
`0.2.0-rc.2`) retired the bare `kind: 'plugin'` wrapper: persistence-row admission refuses it with
|
|
204
|
+
`format v4 message requires a producer-owned source kind`
|
|
205
|
+
(`@deepseek-ai/dsh-session-format-v3-to-v4` `lib/index.js:126`; also
|
|
206
|
+
`dsh-session-persistence-jsonl/lib/worker.cjs:10902`). The v3→v4 migration's own
|
|
207
|
+
`rewritePluginSource` does exactly what we now emit: "kind is the producer's own kind and whose
|
|
208
|
+
`plugin` field is dropped". Continuity validation in `Session.create`
|
|
202
209
|
requires the seq to be the next contiguous seed position (rejection message measured in r5).
|
|
203
210
|
|
|
204
211
|
## Compaction Engine Integration (Phase 0b)
|
package/docs/development.md
CHANGED
|
@@ -94,6 +94,20 @@ and `frontload: true` (structural context injected into prompts). After editing
|
|
|
94
94
|
`codegraph_sync` so subsequent queries reflect the new code. The index is gitignored and persists
|
|
95
95
|
across sessions.
|
|
96
96
|
|
|
97
|
+
### The `docs/superpowers/` plan directory
|
|
98
|
+
|
|
99
|
+
The upstream `writing-plans` / `brainstorming` skills (`.dsh/skills/`, from obra/superpowers — see
|
|
100
|
+
dev/install README § Skills) default their artifacts to `docs/superpowers/plans/YYYY-MM-DD-<feature>.md`
|
|
101
|
+
and `docs/superpowers/specs/…`. Those files are **tracked**: they are per-feature design records, and a
|
|
102
|
+
later session may re-reference them for the *why* behind an implementation. Two rules keep them honest:
|
|
103
|
+
|
|
104
|
+
1. A plan is stamped with a `**Status:**` header when its work lands (implemented → commit refs +
|
|
105
|
+
pointers to the shipped code/tests/site docs), so nobody executes a spent to-do list.
|
|
106
|
+
2. The durable cross-session record remains the chapters store and `docs/` proper; a superpowers plan
|
|
107
|
+
is a working artifact of one pre-implementation session, not a second index. If it rots into
|
|
108
|
+
contradiction with the shipped code, update or supersede it in place via git history — never rewrite
|
|
109
|
+
silently.
|
|
110
|
+
|
|
97
111
|
### Three test layers (and why the third exists)
|
|
98
112
|
|
|
99
113
|
| Layer | Command | Runs | Covers |
|
package/docs/plans-carriage.md
CHANGED
|
@@ -20,7 +20,7 @@ something nobody approved (worse).
|
|
|
20
20
|
| Rejection = failed call carrying review feedback; dismissal = failed call naming the takeover | `index.js` execute arm + package README § Model Experience | Three distinct non-approval shapes to classify, plus a fourth we must not invent. |
|
|
21
21
|
| State is one log-only whole-value event `plan/mode { active }`, appended at the next **accepted in-turn pre-step** | `index.js:374, 389` | An approved exit may not have its mode event yet at archive time → the *result* is the primary signal, the mode event secondary. |
|
|
22
22
|
| Mode state is recovered by **folding the log** (projection `apply`, `stateVersion: 3`) | `index.js:83-94` | Last logged value wins; we mirror that rule, we do not cache a verdict. |
|
|
23
|
-
| The mode-switch notice rides
|
|
23
|
+
| The mode-switch notice rides with `form: 'notice'` — v3 `source { kind: 'plugin', plugin: 'plan-mode' }`, v4 `{ kind: 'plan-mode' }` (measured dsh-plan-mode/lib/index.js:395-410 on 0.2.0-rc.2; issue #43) | `index.js:392-410` | The screen treats injected context as NOT conversation **except** this one sentence, which is archived verbatim (the named exception, `src/injections.ts:55-68`; both arms narrow by producer AND form). Amended 2026-10-10 — this row originally said "omits". |
|
|
24
24
|
| `/plan <message>` steers a real user message, `source.kind 'user'`; bare `/plan`, `/plan off` and their terminal results stay outside model history | `index.js:220` + README § Human Command | The instruction a human typed under plan guidance **is** conversation and is archived verbatim today. Nothing to change. |
|
|
25
25
|
| Freshly created agents begin plan mode **inactive**; `plan/mode` is per-session and not inheritable | README § Known Limitations | A `chapters_continue` / `chapters_fork` child cannot know it was mid-plan unless we carry that text. |
|
|
26
26
|
| `tool/call` data shape is `data.{callId, name, arguments}`, `arguments` an object **or** a JSON string | `src/types.ts:8`, precedent `src/render.ts:253` | The extractor must accept both, exactly as the renderer already does. |
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Implementation Plan: `/chapters-save` Command
|
|
2
|
+
|
|
3
|
+
**Issue:** #11
|
|
4
|
+
**Date:** 2026-10-17
|
|
5
|
+
**Author:** Agent (subagent)
|
|
6
|
+
**Status:** ✅ IMPLEMENTED — shipped as a6c840d (#11 → PR #12), hardened by 75ca0ff (#16 → PR #17,
|
|
7
|
+
fire-and-forget enrichment after the timeout bug). `runSave` lives in `src/tools.ts` (`saveCommand`,
|
|
8
|
+
registered beside fork/continue); tests in `tests/unit/save-command.test.ts`; behavior documented on
|
|
9
|
+
the site at `site/src/content/docs/reference/commands.md` § `/chapters-save`. Kept as the design
|
|
10
|
+
record for that command — read it with the shipped code, not as an open to-do.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
Implement `/chapters-save` — a host command that archives the current session into
|
|
15
|
+
searchable chapters **without** creating a child session or triggering compaction.
|
|
16
|
+
The parent session remains fully functional; archived content becomes searchable
|
|
17
|
+
via `chapters_search` from any session in the workspace.
|
|
18
|
+
|
|
19
|
+
## Success Criteria
|
|
20
|
+
|
|
21
|
+
- `/chapters-save` archives completed turns into verbatim chapter files on disk
|
|
22
|
+
- Session state is updated with new chapter records (search index picks them up)
|
|
23
|
+
- Enrichment is triggered after archiving (if configured)
|
|
24
|
+
- Idempotent: respects the watermark, re-running saves only new content
|
|
25
|
+
- No child session is created
|
|
26
|
+
- No compaction is triggered
|
|
27
|
+
- Returns clear success/error messages
|
|
28
|
+
|
|
29
|
+
## Architecture Decision
|
|
30
|
+
|
|
31
|
+
`/chapters-save` is the **side-effect-only half** of the deterministic door. It reuses
|
|
32
|
+
the same archive pipeline as `/chapters-fork` and `/chapters-continue`:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
composeChapters → renderChapter → writeArchive → commitChapters → scheduler.schedule
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
But stops before `agents.create`. The key insight: `runContinue` in `src/continue-core.ts`
|
|
39
|
+
bundles rendering + writing + child creation into one function. We need to extract the
|
|
40
|
+
render+write portion into a reusable function, then call it from both `runContinue` and
|
|
41
|
+
the new `runSave`.
|
|
42
|
+
|
|
43
|
+
### Why NOT reuse `runContinue` directly
|
|
44
|
+
|
|
45
|
+
`runContinue` calls `finishChild` which does budget preflight, notice assembly, and
|
|
46
|
+
`ports.createChild` — all of which are irrelevant for save. Extracting the archive-only
|
|
47
|
+
portion avoids coupling save to child-creation logic and keeps the atomicity contract
|
|
48
|
+
clean.
|
|
49
|
+
|
|
50
|
+
## File Structure
|
|
51
|
+
|
|
52
|
+
| File | Change | Responsibility |
|
|
53
|
+
|------|--------|---------------|
|
|
54
|
+
| `src/continue-core.ts` | Add `runSave()` function | Pure archive-without-child orchestration |
|
|
55
|
+
| `src/tools.ts` | Add `saveCommand` to `buildChaptersTools` return | Host command definition + handler |
|
|
56
|
+
| `src/tools.ts` | Register `saveCommand` in `registerChaptersTools` | Wire into host command service |
|
|
57
|
+
| `tests/unit/save-command.test.ts` | New test file | Unit tests for the save command handler |
|
|
58
|
+
| `site/src/content/docs/reference/commands.md` | Already documented | No change needed |
|
|
59
|
+
|
|
60
|
+
## Detailed Tasks
|
|
61
|
+
|
|
62
|
+
### Task 1: Extract `runSave` in `src/continue-core.ts`
|
|
63
|
+
|
|
64
|
+
Add a new exported function that performs steps 1-3 of `runContinue` (validate, render,
|
|
65
|
+
write, commit) but skips step 4-5 (notice, budget, create child).
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// src/continue-core.ts — new export
|
|
69
|
+
|
|
70
|
+
export interface SaveArgs {
|
|
71
|
+
callerSessionId: string
|
|
72
|
+
chapters: ChapterRange[]
|
|
73
|
+
toolResultOverrides: ToolResultOverride[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SaveConfig extends RenderConfig {
|
|
77
|
+
artifactStoreRoot: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SaveResult {
|
|
81
|
+
ok: boolean
|
|
82
|
+
chapters?: Array<{ number: number; path: string; startSeq: number; endSeq: number }>
|
|
83
|
+
warnings?: string[]
|
|
84
|
+
reason?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function runSave(
|
|
88
|
+
ports: Pick<ContinuePorts, 'readCallerEvents' | 'getState' | 'putState' | 'fs'>,
|
|
89
|
+
args: SaveArgs,
|
|
90
|
+
config: SaveConfig,
|
|
91
|
+
): Promise<SaveResult>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Implementation notes:**
|
|
95
|
+
- Reuse `validateRanges`, `renderChapter`, `writeArchive`, `appendChapters` from existing imports
|
|
96
|
+
- Use `attemptKey(args.callerSessionId, ceiling)` for idempotent number allocation
|
|
97
|
+
- Call `ports.putState` with `appendChapters(state, wrote.records)` to commit records
|
|
98
|
+
- Do NOT call `ports.createChild`, `assembleNotice`, `preflight`, or `linkChild`
|
|
99
|
+
- Return `{ ok: true, chapters, warnings }` on success
|
|
100
|
+
|
|
101
|
+
### Task 2: Add `saveCommand` in `src/tools.ts`
|
|
102
|
+
|
|
103
|
+
Inside `buildChaptersTools`, after the `continueCommand` definition (~line 529), add:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const saveCommand: HostCommand = {
|
|
107
|
+
name: 'chapters-save',
|
|
108
|
+
description: 'Archive completed turns into searchable chapters without creating a child session. The parent stays fully functional.',
|
|
109
|
+
input: { hint: '[title]' },
|
|
110
|
+
async handler(invocation): Promise<CommandResult> {
|
|
111
|
+
// 1. Get ports (same as deterministicCommand)
|
|
112
|
+
// 2. Read events, find anchor (last turn/end)
|
|
113
|
+
// 3. Check watermark — if nothing new, return success with "nothing to save"
|
|
114
|
+
// 4. Compose/derive ranges (same logic as deterministicCommand lines 485-494)
|
|
115
|
+
// 5. Call runSave(ports, { callerSessionId, chapters, toolResultOverrides }, config)
|
|
116
|
+
// 6. On success: trigger scheduler.schedule(cwd, 'archive:save')
|
|
117
|
+
// 7. Return success text with chapter count and seq range
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Key differences from `deterministicCommand`:**
|
|
123
|
+
- No `intent`, no `handoffNote`, no `callerPreset`, no `rulesSection`, no `projectLine`
|
|
124
|
+
- No `runContinue` / `runFork` call — uses `runSave` instead
|
|
125
|
+
- No child session ID in the return message
|
|
126
|
+
- Simpler error handling (no Refusal/budget concerns)
|
|
127
|
+
|
|
128
|
+
Update the return value of `buildChaptersTools` (line 734):
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
return {
|
|
132
|
+
segment: chaptersSegment,
|
|
133
|
+
chaptersContinue, chaptersFork, chaptersSearch, chaptersArtifact, chaptersRulePropose,
|
|
134
|
+
forkCommand, continueCommand, saveCommand // ← add saveCommand
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Task 3: Register `saveCommand` in `registerChaptersTools`
|
|
139
|
+
|
|
140
|
+
In `registerChaptersTools` (line 737), add `saveCommand` to the command registration loop:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
for (const def of [built.forkCommand, built.continueCommand, built.saveCommand]) {
|
|
144
|
+
const disposer = commands.register(def)
|
|
145
|
+
if (typeof disposer === 'function') commandDisposers.push(disposer as () => void)
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This is a one-line change to the existing loop — the disposal mechanism already handles N commands.
|
|
150
|
+
|
|
151
|
+
### Task 4: Write tests in `tests/unit/save-command.test.ts`
|
|
152
|
+
|
|
153
|
+
Follow the pattern from `tests/unit/fork-command.test.ts`:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Test cases:
|
|
157
|
+
// 1. Saves completed turns into chapter files on disk
|
|
158
|
+
// 2. Updates session state with chapter records
|
|
159
|
+
// 3. Respects watermark — second call with no new turns returns "nothing to save"
|
|
160
|
+
// 4. Returns error when no completed turn exists
|
|
161
|
+
// 5. Triggers scheduler.schedule after successful save
|
|
162
|
+
// 6. Does NOT create a child session (verify agents.create is never called)
|
|
163
|
+
// 7. Handles title argument (optional prefix)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Test infrastructure:** Same `world()` helper pattern as fork-command tests. Stub `ctx`
|
|
167
|
+
with `agents.create` that throws if called (proving no child is created). Real temp-dir
|
|
168
|
+
fs so chapter files materialize.
|
|
169
|
+
|
|
170
|
+
### Task 5: Verify documentation
|
|
171
|
+
|
|
172
|
+
`site/src/content/docs/reference/commands.md` lines 77-94 already document `/chapters-save`.
|
|
173
|
+
Verify the documented behavior matches the implementation. No doc changes expected.
|
|
174
|
+
|
|
175
|
+
## Dependencies Between Tasks
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Task 1 (runSave) → Task 2 (saveCommand) → Task 3 (registration) → Task 4 (tests)
|
|
179
|
+
Task 5 (docs verification) can run in parallel with Task 4
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Risk Assessment
|
|
183
|
+
|
|
184
|
+
| Risk | Mitigation |
|
|
185
|
+
|------|-----------|
|
|
186
|
+
| `runSave` duplicates logic from `runContinue` | Extract shared render+write into a private helper if duplication exceeds ~20 lines |
|
|
187
|
+
| Scheduler not triggered | Mirror the exact `config.scheduler?.schedule(cwd, 'archive:save')` pattern from fork/continue |
|
|
188
|
+
| Search index doesn't pick up new chapters | `commitChapters` via `appendChapters` updates the registry; search reads from registry |
|
|
189
|
+
| Watermark check wrong | Use identical logic to `deterministicCommand` lines 473-480 |
|
|
190
|
+
|
|
191
|
+
## Verification
|
|
192
|
+
|
|
193
|
+
After implementation:
|
|
194
|
+
1. `npm test` — all existing tests still pass (no regressions)
|
|
195
|
+
2. `npm run build` — compiles cleanly
|
|
196
|
+
3. Manual: run `/chapters-save` in a live session, verify chapter files appear in `.dsh-chapters/`
|
|
197
|
+
4. Manual: run `chapters_search` from another session, verify archived content is findable
|
|
198
|
+
5. Manual: run `/chapters-save` again immediately, verify "nothing to save" response
|
package/lib/arrival.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { artifactPath, sha256 } from "./render.js";
|
|
15
15
|
import fs from 'node:fs/promises';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
+
import { resolveStoreRoot } from "./store.js";
|
|
17
18
|
export const ARRIVAL_MARKER = '[dsh:artifact';
|
|
18
19
|
const kb = (n) => n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
|
|
19
20
|
export async function applyArrivalStubs(session, opts) {
|
|
@@ -45,7 +46,7 @@ export async function applyArrivalStubs(session, opts) {
|
|
|
45
46
|
continue;
|
|
46
47
|
const hash = sha256(text);
|
|
47
48
|
const rel = artifactPath(hash);
|
|
48
|
-
const abs = path.join(opts.cwd, opts.storeRoot, rel);
|
|
49
|
+
const abs = path.join(resolveStoreRoot(opts.cwd, opts.storeRoot), rel);
|
|
49
50
|
try {
|
|
50
51
|
await fs.mkdir(path.dirname(abs), { recursive: true, mode: 0o700 });
|
|
51
52
|
// content-addressed: rewriting identical bytes is a no-op by definition
|
package/lib/arrival.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrival.js","sourceRoot":"","sources":["../src/arrival.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"arrival.js","sourceRoot":"","sources":["../src/arrival.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type SaveSessionConfig } from './save-core.ts';
|
|
2
|
+
import { type SessionEventLike } from './types.ts';
|
|
3
|
+
import type { RegistryStore } from './store.ts';
|
|
4
|
+
/** What `subagent/end` carries — the published `SubagentRunEndInfo` subset we read. */
|
|
5
|
+
export interface SubagentEndInfoLike {
|
|
6
|
+
/** The child agent's id — a durable session id for local (in-process) runs. */
|
|
7
|
+
id?: string;
|
|
8
|
+
runId?: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
stopReason?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AutoSaveChildHeader {
|
|
13
|
+
id?: string;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
parentSession?: string;
|
|
16
|
+
origin?: string;
|
|
17
|
+
agentPreset?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface AutoSaveChildView {
|
|
20
|
+
events: SessionEventLike[];
|
|
21
|
+
header: AutoSaveChildHeader;
|
|
22
|
+
}
|
|
23
|
+
export interface AutoSaveDeps {
|
|
24
|
+
/** The preset id a child must carry to be archived (CHAPTERS_PRESET_ID in production). */
|
|
25
|
+
presetId: string;
|
|
26
|
+
/** Synchronous live read at emit time (host `agents.get(id)`); undefined when gone. */
|
|
27
|
+
liveChild: (sessionId: string) => AutoSaveChildView | undefined;
|
|
28
|
+
/** Durable fallback when the live handle is gone; undefined when unreadable. */
|
|
29
|
+
durableChild?: ((sessionId: string) => Promise<AutoSaveChildView | undefined>) | undefined;
|
|
30
|
+
/** The archive itself — saveSession in production, injected for containment tests. */
|
|
31
|
+
save: (child: AutoSaveChildView) => Promise<{
|
|
32
|
+
archived: number;
|
|
33
|
+
skipped: string | null;
|
|
34
|
+
}>;
|
|
35
|
+
warn: (message: string) => void;
|
|
36
|
+
/** Observability seam: what each settlement decided. Outcome vocabulary pinned by tests. */
|
|
37
|
+
onSettled?: ((sessionId: string, outcome: string) => void) | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The listener factory. Pure against injected reads — the real pipeline is wired
|
|
41
|
+
* by `wireSubagentAutoSave` below, and both are covered by
|
|
42
|
+
* tests/{unit,integration} rows. The callback itself is SYNCHRONOUS on the
|
|
43
|
+
* snapshot (that is the race-avoidance), chained asynchronously per child.
|
|
44
|
+
*/
|
|
45
|
+
export declare function makeSubagentEndListener(deps: AutoSaveDeps): (info: SubagentEndInfoLike) => void;
|
|
46
|
+
/** A minimal slice of the host ctx the wiring reads (structural, cordis-free). */
|
|
47
|
+
export interface AutoSaveCtx {
|
|
48
|
+
get?(name: string): unknown;
|
|
49
|
+
logger?: {
|
|
50
|
+
info?(m: string): void;
|
|
51
|
+
warn?(m: string): void;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface AutoSaveWiring extends SaveSessionConfig {
|
|
55
|
+
presetId: string;
|
|
56
|
+
/** §5 sync scheduler: every archive is a push point. */
|
|
57
|
+
scheduler?: {
|
|
58
|
+
schedule(cwd: string, reason: string): void;
|
|
59
|
+
} | undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Bind the listener to the host-plane services: `agents` for the live read,
|
|
63
|
+
* `sessionQuery` for the durable fallback, `saveSession` for the archive.
|
|
64
|
+
* Returns the callback to register with `ctx.on('subagent/end', …)`.
|
|
65
|
+
*
|
|
66
|
+
* ENRICHMENT DELIBERATELY ABSENT: settlement never runs the annotator — a
|
|
67
|
+
* ten-child fanout must not fire ten LLM drains; new chapters are pending work
|
|
68
|
+
* for the idle/afterPush ladders exactly like a `/chapters-save` that skipped
|
|
69
|
+
* the manual nudge. A test pins that nothing here calls runNow.
|
|
70
|
+
*/
|
|
71
|
+
export declare function wireSubagentAutoSave(ctx: AutoSaveCtx, store: RegistryStore, wiring: AutoSaveWiring): (info: SubagentEndInfoLike) => void;
|
package/lib/auto-save.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Door 5 — subagent settlement auto-save (issue #47).
|
|
3
|
+
*
|
|
4
|
+
* The host publishes a `subagent/end` lifecycle event for every settled child
|
|
5
|
+
* run (one-shot AND each residency epoch of a continuable child). This module
|
|
6
|
+
* turns each such settlement of a chapters-preset child into an archive through
|
|
7
|
+
* the SHARED save core (src/save-core.ts) — the same pipeline `/chapters-save`
|
|
8
|
+
* runs, never a second implementation.
|
|
9
|
+
*
|
|
10
|
+
* The seam facts this leans on are MEASURED, not assumed
|
|
11
|
+
* (spikes/subagent-end/FINDINGS.md, host-subagent-seam drift rows):
|
|
12
|
+
* - `end` fires synchronously from a microtask attached at spawn, strictly
|
|
13
|
+
* BEFORE the run holder's dispose continuation → a SYNCHRONOUS snapshot
|
|
14
|
+
* inside the listener sees the live child's complete log and header.
|
|
15
|
+
* - the child header is durable kernel state (`childSessionMeta`):
|
|
16
|
+
* `origin: 'subagent'`, `parentSession`, `agentPreset`, `delegationDepth`.
|
|
17
|
+
* The gate reads the HEADER, never an event scan (a synthetic child can
|
|
18
|
+
* complete turns without ever carrying `agent-preset/selected` — measured).
|
|
19
|
+
* - fallback arm: `sessionQuery.listEvents` + `listSessions` headers are
|
|
20
|
+
* ambient-readable after disposal (not capability-gated) — used only when
|
|
21
|
+
* the live handle is gone at emit.
|
|
22
|
+
*
|
|
23
|
+
* Containment is the signature-listener discipline: the emitter catches listener
|
|
24
|
+
* throws by contract, but we ALSO never let one lose knowledge silently — every
|
|
25
|
+
* non-archive is either a quiet, expected skip (wrong preset, no completed turn)
|
|
26
|
+
* or a LOUD warn (unreadable, save failed).
|
|
27
|
+
*/
|
|
28
|
+
import { appendFileSync } from 'node:fs';
|
|
29
|
+
import { saveSession } from "./save-core.js";
|
|
30
|
+
import { lineageOf } from "./types.js";
|
|
31
|
+
const hasCompletedTurn = (events) => events.some((e) => e.type === 'turn/end');
|
|
32
|
+
/**
|
|
33
|
+
* The listener factory. Pure against injected reads — the real pipeline is wired
|
|
34
|
+
* by `wireSubagentAutoSave` below, and both are covered by
|
|
35
|
+
* tests/{unit,integration} rows. The callback itself is SYNCHRONOUS on the
|
|
36
|
+
* snapshot (that is the race-avoidance), chained asynchronously per child.
|
|
37
|
+
*/
|
|
38
|
+
export function makeSubagentEndListener(deps) {
|
|
39
|
+
// Per-child promise chain: repeated settlements of the SAME child serialize
|
|
40
|
+
// (the watermark makes the second save only see the first's records); distinct
|
|
41
|
+
// children never queue against each other. Same pattern as the signature listener.
|
|
42
|
+
const chains = new Map();
|
|
43
|
+
return (info) => {
|
|
44
|
+
try {
|
|
45
|
+
const id = info?.id;
|
|
46
|
+
if (typeof id !== 'string' || id === '') {
|
|
47
|
+
deps.warn('subagent auto-save: settlement event without a child session id — nothing archived');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// --- synchronous snapshot NOW: `end` strictly precedes disposal (measured).
|
|
51
|
+
let view;
|
|
52
|
+
try {
|
|
53
|
+
view = deps.liveChild(id);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
deps.warn(`subagent auto-save: live read failed for ${id}: ${String(error?.message ?? error)}`);
|
|
57
|
+
view = undefined;
|
|
58
|
+
}
|
|
59
|
+
const hadLiveHandle = view !== undefined;
|
|
60
|
+
const job = async () => {
|
|
61
|
+
let arm = 'live';
|
|
62
|
+
if (view === undefined && deps.durableChild !== undefined) {
|
|
63
|
+
try {
|
|
64
|
+
view = await deps.durableChild(id);
|
|
65
|
+
arm = 'durable';
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
deps.warn(`subagent auto-save: durable read failed for ${id}: ${String(error?.message ?? error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (view === undefined) {
|
|
72
|
+
deps.warn(`subagent auto-save: child ${id} unreadable at settlement (live handle ${hadLiveHandle ? 'read failed' : 'gone'}${deps.durableChild === undefined ? ', no durable arm available' : ', durable read empty'}) — its knowledge was NOT archived`);
|
|
73
|
+
deps.onSettled?.(id, 'skipped:unreadable');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const suffix = arm === 'durable' ? '@durable' : '';
|
|
77
|
+
// Gates, in the order the docs state. Expected skips are quiet: this fires
|
|
78
|
+
// for EVERY subagent in the profile; a warn per non-chapters child would
|
|
79
|
+
// bury the anomalies that need a human.
|
|
80
|
+
if (view.header.origin !== 'subagent') {
|
|
81
|
+
deps.onSettled?.(id, `skipped:origin${suffix}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (view.header.agentPreset !== deps.presetId) {
|
|
85
|
+
deps.onSettled?.(id, `skipped:preset${suffix}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!hasCompletedTurn(view.events)) {
|
|
89
|
+
deps.onSettled?.(id, `skipped:no-completed-turn${suffix}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const out = await deps.save(view);
|
|
94
|
+
deps.onSettled?.(id, out.skipped === null ? `saved${suffix}` : `skipped:${out.skipped}${suffix}`);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
deps.warn(`subagent auto-save: archiving ${id} failed: ${String(error?.message ?? error)} — its knowledge was NOT archived`);
|
|
98
|
+
deps.onSettled?.(id, `failed${suffix}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const prev = chains.get(id);
|
|
102
|
+
const next = prev === undefined ? job() : prev.then(job);
|
|
103
|
+
chains.set(id, next);
|
|
104
|
+
void next.catch(() => undefined).finally(() => { if (chains.get(id) === next)
|
|
105
|
+
chains.delete(id); });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// never-break containment, twice: the emitter catches us, we catch ourselves.
|
|
109
|
+
deps.warn(`subagent auto-save listener threw (contained): ${String(error?.message ?? error)}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Bind the listener to the host-plane services: `agents` for the live read,
|
|
115
|
+
* `sessionQuery` for the durable fallback, `saveSession` for the archive.
|
|
116
|
+
* Returns the callback to register with `ctx.on('subagent/end', …)`.
|
|
117
|
+
*
|
|
118
|
+
* ENRICHMENT DELIBERATELY ABSENT: settlement never runs the annotator — a
|
|
119
|
+
* ten-child fanout must not fire ten LLM drains; new chapters are pending work
|
|
120
|
+
* for the idle/afterPush ladders exactly like a `/chapters-save` that skipped
|
|
121
|
+
* the manual nudge. A test pins that nothing here calls runNow.
|
|
122
|
+
*/
|
|
123
|
+
export function wireSubagentAutoSave(ctx, store, wiring) {
|
|
124
|
+
const warn = (message) => {
|
|
125
|
+
ctx.logger?.warn?.(message);
|
|
126
|
+
// the engine's precedent (r19): a headless profile routes ctx.logger where
|
|
127
|
+
// nobody can see it; the sink env makes the loss readable from outside.
|
|
128
|
+
try {
|
|
129
|
+
const sink = process.env.DSH_CHAPTERS_ENGINE_ERRORS;
|
|
130
|
+
if (sink !== undefined)
|
|
131
|
+
appendFileSync(sink, `[${new Date().toISOString()}] ${message}\n`);
|
|
132
|
+
}
|
|
133
|
+
catch { /* diagnostics must never mask the real failure */ }
|
|
134
|
+
};
|
|
135
|
+
const liveChild = (sessionId) => {
|
|
136
|
+
const agents = ctx.get?.('agents');
|
|
137
|
+
const handle = agents?.get?.(sessionId);
|
|
138
|
+
const session = handle?.agent?.session ?? handle?.session;
|
|
139
|
+
if (session === undefined || typeof session.snapshotEvents !== 'function')
|
|
140
|
+
return undefined;
|
|
141
|
+
return { events: [...session.snapshotEvents()], header: (session.header ?? {}) };
|
|
142
|
+
};
|
|
143
|
+
const durableChild = async (sessionId) => {
|
|
144
|
+
const sq = ctx.get?.('sessionQuery');
|
|
145
|
+
if (sq?.listEvents === undefined || sq.listSessions === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
const events = await sq.listEvents(sessionId);
|
|
148
|
+
if (!Array.isArray(events) || events.length === 0)
|
|
149
|
+
return undefined;
|
|
150
|
+
const records = await sq.listSessions();
|
|
151
|
+
const header = (records ?? []).find((r) => r?.header?.id === sessionId)?.header;
|
|
152
|
+
if (header === undefined)
|
|
153
|
+
return undefined;
|
|
154
|
+
return { events, header };
|
|
155
|
+
};
|
|
156
|
+
const save = async (child) => {
|
|
157
|
+
const cwd = child.header.cwd;
|
|
158
|
+
if (cwd === undefined || cwd === '')
|
|
159
|
+
throw new Error('subagent child session has no workspace cwd — nowhere reachable to write');
|
|
160
|
+
const out = await saveSession({
|
|
161
|
+
events: child.events,
|
|
162
|
+
sessionId: child.header.id ?? '',
|
|
163
|
+
cwd,
|
|
164
|
+
store,
|
|
165
|
+
config: wiring,
|
|
166
|
+
subagentOf: lineageOf(child.header),
|
|
167
|
+
});
|
|
168
|
+
if (out.skipped === null && out.records.length > 0 && wiring.scheduler !== undefined && cwd !== '') {
|
|
169
|
+
wiring.scheduler.schedule(cwd, 'archive:autoSave');
|
|
170
|
+
}
|
|
171
|
+
return { archived: out.records.length, skipped: out.skipped };
|
|
172
|
+
};
|
|
173
|
+
return makeSubagentEndListener({
|
|
174
|
+
presetId: wiring.presetId,
|
|
175
|
+
liveChild,
|
|
176
|
+
durableChild,
|
|
177
|
+
save,
|
|
178
|
+
warn,
|
|
179
|
+
onSettled: (id, outcome) => { ctx.logger?.info?.(`dsh-chapters: subagent ${id} settled → ${outcome}`); },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=auto-save.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-save.js","sourceRoot":"","sources":["../src/auto-save.ts"],"names":[],"mappings":"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"}
|
package/lib/commands.d.ts
CHANGED
|
@@ -24,5 +24,8 @@ export interface HostCommandsConfig {
|
|
|
24
24
|
kind: 'success' | 'error';
|
|
25
25
|
text: string;
|
|
26
26
|
}>;
|
|
27
|
+
/** Door 5 (issue #47): whether subagent settlements auto-archive. Reported in
|
|
28
|
+
* /chapters-status because it runs WITHOUT a human seeing it happen. */
|
|
29
|
+
subagentAutoSave?: boolean;
|
|
27
30
|
}
|
|
28
31
|
export declare function registerHostCommands(ctx: ToolsCtx, domain: DomainLike, config: HostCommandsConfig): () => void;
|