@treeseed/dsh-chapters 0.1.24 → 0.2.1

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 (103) hide show
  1. package/README.md +2 -1
  2. package/cordis.patch.yml +49 -1
  3. package/docs/architecture.md +1 -496
  4. package/docs/backlog.md +234 -4
  5. package/docs/contract.md +127 -1
  6. package/docs/development.md +248 -12
  7. package/docs/host-compaction-seam.md +108 -0
  8. package/docs/knowledge-repo.md +532 -16
  9. package/docs/project-state.md +323 -0
  10. package/docs/verify.md +266 -0
  11. package/lib/archive.d.ts +49 -1
  12. package/lib/archive.js +56 -2
  13. package/lib/archive.js.map +1 -1
  14. package/lib/arrival.js +24 -10
  15. package/lib/arrival.js.map +1 -1
  16. package/lib/attach.d.ts +46 -0
  17. package/lib/attach.js +120 -0
  18. package/lib/attach.js.map +1 -0
  19. package/lib/auto-save.d.ts +74 -0
  20. package/lib/auto-save.js +175 -26
  21. package/lib/auto-save.js.map +1 -1
  22. package/lib/client.js +413 -11
  23. package/lib/client.js.map +1 -1
  24. package/lib/commands.d.ts +8 -1
  25. package/lib/commands.js +135 -7
  26. package/lib/commands.js.map +1 -1
  27. package/lib/compose.js +25 -6
  28. package/lib/compose.js.map +1 -1
  29. package/lib/continue-core.d.ts +24 -1
  30. package/lib/continue-core.js +41 -9
  31. package/lib/continue-core.js.map +1 -1
  32. package/lib/diag.d.ts +55 -0
  33. package/lib/diag.js +186 -0
  34. package/lib/diag.js.map +1 -0
  35. package/lib/engine-core.d.ts +53 -1
  36. package/lib/engine-core.js +64 -7
  37. package/lib/engine-core.js.map +1 -1
  38. package/lib/engine.d.ts +99 -0
  39. package/lib/engine.js +488 -36
  40. package/lib/engine.js.map +1 -1
  41. package/lib/enrich-queue.js +28 -2
  42. package/lib/enrich-queue.js.map +1 -1
  43. package/lib/enrich-wire.d.ts +92 -2
  44. package/lib/enrich-wire.js +135 -17
  45. package/lib/enrich-wire.js.map +1 -1
  46. package/lib/enrich.d.ts +27 -2
  47. package/lib/enrich.js +114 -18
  48. package/lib/enrich.js.map +1 -1
  49. package/lib/index.d.ts +52 -0
  50. package/lib/index.js +182 -2
  51. package/lib/index.js.map +1 -1
  52. package/lib/indexing.d.ts +7 -0
  53. package/lib/indexing.js.map +1 -1
  54. package/lib/injections.d.ts +1 -0
  55. package/lib/injections.js +38 -0
  56. package/lib/injections.js.map +1 -1
  57. package/lib/last-resort.d.ts +24 -0
  58. package/lib/last-resort.js +27 -0
  59. package/lib/last-resort.js.map +1 -0
  60. package/lib/prompt.d.ts +130 -0
  61. package/lib/prompt.js +255 -0
  62. package/lib/prompt.js.map +1 -0
  63. package/lib/registry.d.ts +39 -1
  64. package/lib/registry.js +76 -9
  65. package/lib/registry.js.map +1 -1
  66. package/lib/render.d.ts +31 -3
  67. package/lib/render.js +75 -3
  68. package/lib/render.js.map +1 -1
  69. package/lib/repair-run.d.ts +49 -0
  70. package/lib/repair-run.js +423 -0
  71. package/lib/repair-run.js.map +1 -0
  72. package/lib/repair.d.ts +200 -0
  73. package/lib/repair.js +354 -0
  74. package/lib/repair.js.map +1 -0
  75. package/lib/rules-commands.d.ts +22 -0
  76. package/lib/rules-commands.js +177 -20
  77. package/lib/rules-commands.js.map +1 -1
  78. package/lib/rules.d.ts +184 -7
  79. package/lib/rules.js +312 -27
  80. package/lib/rules.js.map +1 -1
  81. package/lib/save-core.d.ts +31 -1
  82. package/lib/save-core.js +153 -17
  83. package/lib/save-core.js.map +1 -1
  84. package/lib/session-rules.d.ts +222 -0
  85. package/lib/session-rules.js +454 -0
  86. package/lib/session-rules.js.map +1 -0
  87. package/lib/store.d.ts +22 -1
  88. package/lib/store.js +94 -17
  89. package/lib/store.js.map +1 -1
  90. package/lib/sync.js +13 -1
  91. package/lib/sync.js.map +1 -1
  92. package/lib/tools.d.ts +14 -0
  93. package/lib/tools.js +321 -32
  94. package/lib/tools.js.map +1 -1
  95. package/lib/types.d.ts +12 -1
  96. package/lib/types.js.map +1 -1
  97. package/lib/version.d.ts +9 -0
  98. package/lib/version.js +10 -0
  99. package/lib/version.js.map +1 -0
  100. package/lib/warnings.d.ts +10 -0
  101. package/lib/warnings.js +18 -0
  102. package/lib/warnings.js.map +1 -0
  103. package/package.json +9 -3
package/docs/backlog.md CHANGED
@@ -1,4 +1,78 @@
1
- # Open items — 2026-10-11
1
+ # Open items — 2026-10-23 (host 0.2.0-rc.2)
2
+
3
+ Handoff while the nine-leg re-record and strict replay are in flight on
4
+ `feature/56-reanchor-tape-corpus`. Read this section first; the 2026-10-11 record below is history,
5
+ and item 1 there (live-model proof of the plans journey) is still open — tonight's `plans` leg was
6
+ again recorded through the scripted stub (`tests/e2e/stub-model.ts`), as its PROVENANCE says.
7
+
8
+ **What the last two days changed about our claims.** The preset pin had been written to a retired row
9
+ id (`agent-presets` → `agent-preset-registry`), so every browser boot since the 0.2.x move ran the
10
+ STANDARD realm: no `compaction/*`, no artifacting, no `chapters_rules` — the legs were measuring an
11
+ absence, not a failure (#56). The 0.2.x flattened `tool/result` shape silently disabled arrival
12
+ artifacting chain-wide. `uiWorkspace.openSession` never existed, so the continuation door reported a
13
+ switch it could not make (#77). All four are fixed with rows; the corpus is being re-anchored as I
14
+ write, and **no e2e claim is current until replay is green** — `CORPUS_HOST` was flipped in the same
15
+ change, so a red replay means that flip must be reverted, not the tape widened.
16
+
17
+ 0. **The acceptance chain is one rig bug from green — and it was #77's lesson, in my own tooling.**
18
+ A pristine home commits a draft under a **fresh session id** while `dsh.sessions.current` keeps
19
+ naming the draft, so `newSessionWithTurn` watched a log that received nothing (journey turn
20
+ completed at seq 42, 28 s after its question, in *another* directory). Warm homes hide it; CI is
21
+ always cold, which is why "green here, red there" survived the matcher fix (#89's path fold), the
22
+ host-binary fix (`resolveHostBinary`), the tracked profile seeds, and my own false
23
+ header-composition claim (withdrawn on #89). Fixed by `adoptSessionForTurn` — the durable log
24
+ decides, candidates are only where to look, `null` rather than an invented id — with 4 rows,
25
+ mutation-checked. **The pending step is mechanical but long**: re-distill the nine legs under it
26
+ (`var/run-decisive.sh`, heavy behind the #86 watchdog), strict-replay locally, then one push and
27
+ watch hosted acceptance. Only then: corpus commit + `CORPUS_HOST` re-lift, `coverage:e2e`,
28
+ merge #89, rewrite #73's verification from those numbers.
29
+
30
+ 0b. **One sequencer at a time (#82, live lesson 2026-10-23).** Two detached helper scripts
31
+ (`var/after-pass.sh`, `var/after-pass2.sh`) were both waiting on "runner idle"; the first fired,
32
+ relaunched a full eight-leg pass against a build taken *before* the newest commit, and the second
33
+ would have stacked a competing cycle on top. Killed the duplicate. Rules now written into the tooling:
34
+ `run-decisive.sh` honors `DISTILL_PROJECTS` so a follow-up re-records only the legs that need a new
35
+ build, and a cycle script must `pgrep` for any live `distill|playwright|run-decisive` before starting.
36
+ Careful with `pgrep -f` patterns from inside a shell whose own command line contains the pattern — my
37
+ cleanup killed its own tool call.
38
+
39
+ 1. **#83 — the e2e specs have never been type-checked.** `npm run typecheck:e2e` (new, deliberately not
40
+ in CI) reports **120 errors**: 39 wrong-arity calls, 12 cannot-find-name, 10 argument-type, 26
41
+ un-narrowed `unknown`, 22 missing-lib, 3 phantom properties. Two bugs this week (a missing import,
42
+ a dropped closing paren — both mine, both from the composer wiring) were invisible *only* because
43
+ this check did not exist. Close it: fix the arity class by giving `newSessionWithTurn` a named
44
+ options object (`{ turnMs, actionGrace }`) so a positional `false` is un-writable; triage the rest;
45
+ then wire the gate beside `typecheck` and `typecheck:client`. Do not exclude a file to shrink the
46
+ number — a dev-only tool moves out of `tests/e2e` instead.
47
+ 2. **#82 — a running distill and the deterministic suite share `tests/fixtures/model-tape`.** (Third live instance 2026-10-23: `tape-provenance.test.ts` correctly went red mid-re-record because the runner had cleared a project directory — that row is now the canary, don't soften it.) Tonight
48
+ the tape-coherence rows went red for exactly the projects being re-recorded, and a leg that dies
49
+ halfway can leave a partial project directory in the corpus. Recommended shape: record into
50
+ `tests/fixtures/.tape-staging/<proj>`, promote with `rename` on `rc == 0`. That makes "the corpus
51
+ is one coherent change" a mechanism instead of my self-control, which has already slipped once.
52
+ 3. **#80 — plugin command results produce nothing operator-visible.** Durable `command/done` yes, DOM
53
+ no (measured twice: this leg's own teardown aria snapshot; `/chapters-status` on a fresh boot).
54
+ Success paths are undelivered, which is #61a one layer up. Needs a host fact (`docs/contract.md`)
55
+ before code: is there a slot/toast/flow renderer for `command/done`, or must the plugin own the
56
+ affordance?
57
+ 4. **#81 criteria 3 and 4.** The delivery guard is now at every composer call site with a drift row,
58
+ but the browser assertion "one instruction ⇒ exactly one `user/message` carrying it" is not written
59
+ yet, and the predicate still decides from the durable log only — a synthetic turn whose record is
60
+ delayed is still a race we have not measured.
61
+ 5. **#78 — the design decision, still the maintainer's.** A single `tool/result` larger than what can
62
+ fit beside the mandatory retain tail is NOT relievable by pressure compaction (measured: 6
63
+ compactions + 6 `context-overflow` entries + the endpoint's `400`, with the arrival floor switched
64
+ off by design). Option A: treat survivability as an invariant (hard ceiling, artifact with a loud
65
+ warning — the archive keeps every byte, so the worst case is a context-economy miss). Option B:
66
+ accept "disable the floor, read 30K tokens, the session degrades" as correct. `heavy` is green, so
67
+ this no longer blocks a leg — it is a product statement we should make deliberately.
68
+ 6. **The remaining release steps for #56**, in order: green strict replay (no fallback) → fresh
69
+ `coverage:e2e` → PR to `staging` (reload the `release-workflow` skill first — the gate is
70
+ mandatory) → CI green → merge → rewrite PR #73's verification text so every claim carries the
71
+ artifact that proves it. `main` stays agent-forbidden.
72
+
73
+ ---
74
+
75
+ # Open items — 2026-10-11 (history)
2
76
 
3
77
  Handoff for a fresh session. Everything below is *scoped and reachable*; none of it is a unknown
4
78
  defect. Shipped state: **0.1.13** (tag = npm `latest` = the user's installed profile), hosted CI green
@@ -119,9 +193,15 @@ NEW host too (measured). Full record: `docs/contract.md` § Presets are declarat
119
193
 
120
194
  **Open, in order:**
121
195
 
122
- 0. **Re-anchor the e2e host, then re-distill the tapes.** `tests/e2e/boot.ts` now refuses by name
123
- while `dev/dsh-host-lock` pins a pre-0.2.0 host, and the refusal lifts itself when the lock moves —
124
- that is the upgrade path, not a TODO. `ci.yml`'s `host-capability` job reads that same lock and
196
+ 0. **Re-anchor the e2e host, then re-distill the tapes — IN PROGRESS (#54).** The lock now
197
+ resolves `@deepseek-ai/dsh` **0.2.0-rc.2** (chosen by witness: the host this box has been running
198
+ the plugin on, not npm's latest), `hostSupportsPresetRows()` answers true, and the boot's named
199
+ refusal has lifted itself exactly as designed. Three 0.2.x rig drifts were found by booting rather
200
+ than guessing and are fixed — the `session.v4` log format, the Preview Notice focus trap, and the
201
+ provider block that 0.2.x reads only from `cordis.patch.yml`. Distillation no longer needs the GPU
202
+ box (see `docs/development.md` § Distilling tapes WITHOUT the GPU box). REMAINING: re-distill all
203
+ eight projects on the new host, re-run `npm run test:e2e:replay` strict and `coverage:e2e`, and
204
+ re-measure the browser ledger before ANY browser-plane claim is made. `ci.yml`'s `host-capability` job reads that same lock and
125
205
  SKIPS the acceptance job with a step-summary reason plus a `::warning::` while the pinned host
126
206
  predates declaration rows: visibly skipped, never red-by-accident, never green-by-omission.
127
207
  Publishing is unaffected — the release gate is the deterministic suite. The per-boot row pins (`thresholdRatio`, `enrichmentEnabled`,
@@ -174,3 +254,153 @@ pin the pair deliberately in the profile patch (`disabled: true` on `directory-p
174
254
  `@deepseek-ai/dsh-host-directory-picker-browse` + `@deepseek-ai/dsh-client-ui-directory-picker-browse`).
175
255
  Upstream ask: compat aliases for the renamed icon exports, or a boot-time shell↔plugin version handshake
176
256
  that rejects a mismatched bundle instead of crashing at render.
257
+
258
+ ## Two-tier rules — specified, deliberately not built (2026-10-20, #50)
259
+
260
+ The design round that produced record §7.1 + §7.3–§7.9 and Amendment 2026-10-20 started from one
261
+ measured hole: `planSummarize` composes `leadSections = [planSection, plotSection]`, so a plan and a
262
+ plot survive an in-place compaction and a **rule does not**. Docs are committed ahead of code
263
+ (docs-first); `docs/verify.md § Two-Tier Rules — SPEC` rows R1–R10 are the checklist, and each one is
264
+ supposed to be provable-red before it is called a fact. A fresh session should start at the tests, not
265
+ at the implementation.
266
+
267
+ Two things the round decided **not** to do, so they don't get rediscovered as oversights:
268
+
269
+ **1. Team-identity curation facts (multi-human collective approval) — deferred, and it is a real
270
+ design question, not a gap.** Approval is per-machine by the 2026-09-20 amendment, and that boundary
271
+ was NOT relaxed: a rule a teammate approves is not `core` on your machine. For checkouts on *one*
272
+ machine that is invisible (`harnessId` = hostname, so they share one facts file) — which is exactly the
273
+ use case that asked for it. For **several humans on one project**, the cost is re-reviewing a growing
274
+ list. The clean way to buy it back without reopening the bug the amendment closed: keep facts namespaced
275
+ by *writer* and let a machine read **more** namespaces — my own `edits/<harness>/curation.jsonl` ∪ a
276
+ recognized team identity's `edits/<teamId>/curation.jsonl`, still one writer per file (§3.3 survives),
277
+ no file rewrite, and "primary architect" becomes whoever holds the team credentials rather than a role
278
+ people are trusted to respect. Local `revoke` must still win over a team fact (last-write-wins by
279
+ namespace precedence, stated explicitly). Needs a decision first: is the population one team, or can
280
+ unrelated projects share a TreeDX instance? The second turns this from a feature into governance.
281
+
282
+ **2. Per-request rule injection — rejected on measurement, not taste.** Invariant 3 keeps the global
283
+ system prompt out, and E3/r23-r28 show a head-position replacement reuses nothing on llama.cpp
284
+ (`cacheReuse 0`) — a ruleset that churns mid-session would be a cache-wipe machine on the target
285
+ hardware. Carriage happens at the two moments a head is actually rebuilt, plus `chapters_rules` on
286
+ demand. If someone revisits this, the honest argument is "small models drift mid-window", and the
287
+ answer is the pinned class + the withheld-count line, not more injection.
288
+
289
+ ## #50 two-tier rules — what the next session inherits (2026-10-20)
290
+
291
+ The round landed green on the deterministic plane (640 unit / 137 integration rows, coverage gate
292
+ held at 93.16 / 85.53 / 90.01). Three things are deliberately unfinished, and none is a surprise:
293
+
294
+ 1. **Mined-rule quality: first data in, sample tiny.** `npm run mining:probe` (2026-10-21) mined
295
+ three real chapters of THIS repo at `qwen3.8-flash` through the shipped path: 9 rules, 6 stated / 3
296
+ inferred, every inference confined to the one chapter that had no human text, none wrong or unsafe.
297
+ Full numbers and the caveats in `docs/verify.md` R5. Next: probe archives from a project that is not
298
+ this repo — our own history is the friendliest corpus available, and a rule like "treat pinned tests
299
+ as authoritative" is true here partly because the reader is already convinced.
300
+
301
+ 1b. **Settled 2026-10-21, and settled twice:** the first reading ("mining is being truncated by
302
+ `ENRICH_MAX_TOKENS = 900`") was wrong — the token-plan route does not enforce `max_tokens` at all
303
+ (a 64-token cap returned 113 tokens, `finish_reason: stop`) and it itemizes
304
+ `completion_tokens_details.reasoning_tokens`. What was real is that the budget was three
305
+ hardcoded literals sized for pre-mining answers, so it is now `enrichmentMaxTokens` (default
306
+ 4000). If a future provider DOES enforce the ceiling, this is the knob; if mining ever needs its
307
+ own budget, `rulesModel` already gives it its own call.
308
+ 1c. **Endpoint default is the regional token-plan door**
309
+ (`token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1`): QwenCloud gives no region
310
+ choice, and the `maas.qwencloudapi.com/compatible-mode/v1` alias answers `401 Invalid API-key
311
+ provided` for the keys on this box. `scripts/llm.mjs` defaults to the regional door, keeps the
312
+ alias as a probe candidate behind it, and takes `DSH_LLM_BASE` as the override — so a credential
313
+ that starts working at the alias is used with no edit.
314
+ 2a-1. **The knowledge-loop journey cannot reach the door after two commands run** — precise,
315
+ measured 2026-10-22. `suite` now dies at `forkDoor(page).click()` waiting for
316
+ `button[aria-label="Fork with chapters"]:visible, last` for 25 minutes, with the upstream
317
+ journal showing **44 completed answers, 0 throttles, 0 deadlines** — so it is not quota and
318
+ not the model. What the journey does between the last assistant message and the click:
319
+ `/chapters-link`, then `/chapters-status`. Those render as command **flow nodes**, so after
320
+ them the last assistant chat row is no longer the newest actionable node, and the door row
321
+ on it is not `:visible` (host action rows appear to be gated on the newest/hovered message —
322
+ the fork journey, which forks right after ONE turn, clicks the same helper without issue).
323
+ FIX (rig, not assertion-weakening): select the door **scoped to the last assistant message**,
324
+ scrolling/hovering that message first — the same affordance a human uses — rather than
325
+ "the last visible door in the document". Keep `forkDoor` as the single selector so all four
326
+ journeys move together, and keep the `toBeVisible` assertions: a door nobody can reach is a
327
+ real defect, whether the cause is the plugin or the client's hover gating.
328
+
329
+ 2a. **On 0.2.x the switch to a forked child is a silent no-op, and the child never
330
+ appears in the session list** — measured 2026-10-22 against the CURRENT build (host
331
+ 0.2.0-rc.2, plugin link-verified by the new boot guard). The door announces success
332
+ (`branch “…” is session ch-3865899e…`) and renders NO note element, which
333
+ `src/client/doors.ts` emits only when `sessions.open()` did not throw — yet
334
+ `localStorage['dsh.sessions.current']` still names the PARENT, focus stays on the parent's
335
+ composer, and after a full reload the sidebar lists one session. The child is real: its
336
+ directory holds `session.lock` + `session.v4.jsonl.zstd`, `session_projcache` has both
337
+ entries, and the log carries the whole notice (`Project: …`, `(N msgs)`, rule categories).
338
+ **Prime suspect is our own swallowed failure**: `src/tools.ts` attaches with
339
+ `workspaceRegistry.createCanonical(cwd)` → `workspace?.attachSession?.(sessionId)`, both
340
+ optional-chained — and AGENTS records that very attach as the fix for round 6's "an
341
+ unattached session shows as an unrelated root, so it is not listed". If 0.2.x reshaped that
342
+ return, the chain no-ops silently and orphans the child from the workspace, which matches
343
+ every symptom. NEXT: make the attach loud (diagnostic + door note when the service or method
344
+ is absent; never an optional chain through a required step), re-run, and let the message name
345
+ what is missing. `tests/e2e/fork-button.spec.ts` already asserts the switch — it is the gate,
346
+ and it is red for the right reason.
347
+ Superseded by the above, kept so nobody re-bisects it: an earlier version of this entry blamed
348
+ a `format v4 message requires a producer-owned source kind` refusal. That message was real but
349
+ came from a COPIED plugin tree dated Oct 16 in `.dshdev-local`; the current build produces no
350
+ such refusal, and `spikes/seed-admission/` shows host 0.2.0-rc.2 admitting our notice, the
351
+ notice + `plan/mode`, and the notice + `agentOptions` + preset mount (5–6 durable events each)
352
+ while correctly refusing the retired v3 wrapper.
353
+ 2. **The browser plane is blocked by CORPUS PROVENANCE, not by the host any more.** The
354
+ preset-row refusal is gone — `dev/dsh-host-lock` resolves 0.2.0-rc.2 and
355
+ `hostSupportsPresetRows()` answers true, so the rig boots the preset and CI's acceptance job
356
+ RUNS instead of skipping on capability. What stops replay now is that the eight tape projects
357
+ were distilled against 0.1.5-rc.1, and tape matching is normalized request-PREFIX matching over
358
+ prompt bytes the host composes. `tests/fixtures/model-tape/CORPUS_HOST` says so in one place,
359
+ `scripts/corpus-anchor.mjs` reads it, `tests/e2e/boot.ts` refuses REPLAY by name (RECORD stays
360
+ allowed — a gate that blocked the way out would strand itself), and CI skips the acceptance job
361
+ with a step summary and a `::warning::`. Lift it by re-distilling all eight
362
+ (`scripts/distill-tapes.sh`) and updating the record — **issue #56**, with **#57** (door
363
+ selector scoped to the last assistant message) as the blocker inside it. Extending
364
+ `rules.spec.ts` with the checkpoint-carriage and continue-vs-fork assertions is still owed, and
365
+ must be written after the corpus is anchored — spec assertions that cannot execute are a false
366
+ witness, which is why they are not written preemptively.
367
+ 3. **R10 is only partly proven.** Marker rules re-derive off the durable log and dedupe against
368
+ disk, but there is no end-to-end wipe-and-replay row yet.
369
+
370
+ Two local truths to save the next agent an hour: the suites need **Node ≥ 24** (on 22 every file
371
+ fails for a reason that looks like your diff), and `coverage:ci` intermittently reddens
372
+ `git-auth-classification`/`sync-remote` on this box under parallel load — **verified pre-existing** by
373
+ running it in a worktree at the last docs-only commit. Re-run before blaming the change.
374
+
375
+
376
+ ## Retrieval is untested in the field (#61, record only — do not "fix" it speculatively)
377
+
378
+ Measured over 26 SAIP sessions / 4,049 tool calls: `chapters_search`, `chapters_artifact` and
379
+ `chapters_rules` were called **0 times**, and no tool call anywhere referenced a `.dsh-chapters` path.
380
+ So the archive's retrieval promise is **unverified in the field**, not proven broken: the deterministic
381
+ and browser planes both exercise those doors, and a real long session may simply never have needed them.
382
+
383
+ Deliberately NOT acted on in #61: no persona rewrite, no "make the model search more" prompt change. The
384
+ persona is a product-prompt surface and every byte of it costs a tape re-distill (#56/#57), so a change
385
+ there needs a measured defect, not an inference from an unused-tool count. If a future project shows
386
+ knowledge that WAS archived and never retrievable when asked for, that is the evidence that opens this.
387
+
388
+ ## #71 prompt recommendations — push transport (follow-up, gated on the compile-deps move)
389
+
390
+ The feature ships on the PULL transport (`/chapters-prompt suggest` over `remote.commands`,
391
+ the door-proven wire) because our compile-time `@deepseek-ai/*` closure is still
392
+ 0.1.5-rc.2, where `appendPluginRecord` / `PluginRecordMap` do not exist (grep-verified;
393
+ facts in `docs/contract.md` § Composer Ghost Layer). When the workspace deps move to 0.2.x
394
+ (they must for `hostSupportsPresetRows`-era work anyway):
395
+
396
+ 1. Merge `SessionProjectionMap['chaptersPrompt']` + `PluginRecordMap['plugin:dsh-chapters/prompt']`
397
+ in `src/prompt.ts`; at generation, `appendPluginRecord` the whole-value record (durable,
398
+ append-only, never surface-eligible — invariant 1 honored), register a
399
+ `ProjectionDefinition` folding it (plan/goal pattern), and have the client read
400
+ `useProjection('chaptersPrompt')` instead of pulling.
401
+ 2. Screen the record type in `src/injections.ts` (host-injected, never archived) and bucket it
402
+ as un-rendered in the chapter renderer — one predicate, per the #59 rule.
403
+ 3. Add a kernel-drift row: plugin records must stay NOT surface-eligible
404
+ (`isSurfaceEligibleType`), read from the kernel's own exports.
405
+ 4. The pull command stays as a manual verb (`suggest` re-arms the ghost); the anti-dup client
406
+ row keeps the wire body singular either way.
package/docs/contract.md CHANGED
@@ -95,7 +95,8 @@ export const inject = [
95
95
 
96
96
  - `agents` — `ctx.agents.create`, the continuation primitive. **The load-bearing one.**
97
97
  - `storageDomain` — chain registry.
98
- - `sessionQuery` — `observeSession` for the calling agent's own log and preset projection.
98
+ - `sessionQuery` — `observeSession` for the calling agent's own log and preset projection, plus the
99
+ durable reads for **other** sessions (see § Durable reads — the verbs are not interchangeable).
99
100
  - `tokenMeter` — pressure math and the continuation budget preflight.
100
101
  - **`systemPrompt` is excluded on purpose** (invariant 3).
101
102
  - Add `commands` at the UI/CLI trigger phase; `connection` / `sessionController` only if a create path
@@ -103,6 +104,49 @@ export const inject = [
103
104
 
104
105
  Declare only what you use — `dsh-compact` gets by on `inject = ["commands"]`.
105
106
 
107
+ ## Durable reads — `sessionQuery` verbs are NOT interchangeable
108
+
109
+ Measured 2026-10-22 against host `0.2.0-rc.2` while root-causing #58. The rule this section exists to
110
+ enforce: **measure a host verb's payload *shape*, not its presence.** `spikes/subagent-end/FINDINGS.md`
111
+ finding 5 recorded `{count, types}` from `listEvents`, saw 10 plausible event types, wrote "returned the
112
+ full durable log", and the whole of door 5 was built on that. The types were real; the payloads were not
113
+ there at all.
114
+
115
+ | verb | returns | safe as an archive input? |
116
+ |---|---|---|
117
+ | `listEvents(sessionId)` | `SessionEventRecord[]` = `{sessionId, seq, type, time, surface}` — **no `data`** | **No.** Types and seqs only: a metadata index. |
118
+ | `readSession(sessionId)` | `{session: structuredClone(header), inheritedEventCount, events: snapshotSessionEvent(e)[]}` | **Yes**, and it carries the header too — one call, so no `listSessions()` scan. |
119
+ | `readSurface(sessionId)` | cloned header + `currentSurfaceEvents` (folded surface, snapshot-detached) | Yes for surface work; it is *folded*, so it is not the whole log. |
120
+ | `filterEvents(sessionId, filters)` | semantic event documents | No — search documents, not events. |
121
+
122
+ File:line (host install, `/home/adrian/node_modules/@deepseek-ai/dsh-session-query/`):
123
+ `lib/index.js:1141` `async listEvents(sessionId) { return eventRecords(sessionId, (await this._corpus.load(sessionId)).events) }`
124
+ → `:845-847` `eventRecords` → `analyzeEventLog(sessionId, events).records`, built at `:966-973` as
125
+ `records: events.map((event) => ({ sessionId, seq: event.seq, type: event.type, time: event.time, surface: … }))`.
126
+ Published type: `lib/types/types.d.ts:43-54` (`interface SessionEventRecord` — the doc comment says
127
+ *"Lightweight metadata for one event"*, which is the sentence the spike skipped).
128
+ `lib/index.js:1079-1087` `readSession` → `events: loaded.events.map(snapshotSessionEvent)`;
129
+ `snapshotSessionEvent = adoptSessionEvent(structuredClone(event))` at
130
+ `@deepseek-ai/dsh-session/lib/index.js:1106-1108`, i.e. a **validated, payload-bearing** clone.
131
+
132
+ Read the header, never infer it: `readSession().session` is the kernel's own `SessionHeader` clone, so
133
+ `origin` / `parentSession` / `agentPreset` / `cwd` come from the same observation as the bytes being
134
+ archived — which is what door 5's gate needs (#58). The header is *recorded* on the child's own row
135
+ (`subagent`/`parentSession` frontmatter), not used to re-link the tree — see docs/architecture.md
136
+ § The archive moments for why `linkChild` was measured and rejected.
137
+
138
+ And the host's own teardown ordering, which decides **which** arm runs:
139
+ `@deepseek-ai/dsh-subagent/lib/index.js:1240-1250` (continuable) — `flushFinalState` →
140
+ `await activation.handle.dispose()` → `resident.delete(childId)` → `observer.settle(failure)`, and
141
+ `settle` is what emits `subagent/end` (`:317-324`). `observeRun` (one-shot, `:296-306`) emits earlier,
142
+ before disposal. So a continuable child's live handle is **always** gone at settlement: the durable arm
143
+ is the only arm for it, and the fast path is the only arm for one-shot children. Both must be tested;
144
+ one measurement of one mode is what #58 is.
145
+
146
+ `SessionEventLike.data` is therefore **required** in `src/types.ts` (was optional). Optionality is the
147
+ reason a `listEvents` record passed as `SessionEventLike[]` type-checked, and the renderer's
148
+ `ev.data ?? {}` then made a hollow chapter legal instead of failing to compile.
149
+
106
150
  ## Tools
107
151
 
108
152
  `defineTool` at `packages/core/tools/src/schema.ts:545`:
@@ -467,3 +511,85 @@ not stylistic:
467
511
  - Manual is MVP. The button path cannot supply model-authored titles or ranges, so segmentation must be
468
512
  a plain function callable **without an agent turn** — build that seam in Phase 1, not Phase 5, or the
469
513
  UI and automatic paths both force a rewrite.
514
+
515
+ ## Composer Ghost Layer — measured host facts (#71, verified 2026-10-21 against `examples/dsh` @ 0.2.1-rc.2; runtime pin 0.2.0-rc.2, compile deps still 0.1.5-rc.2)
516
+
517
+ The prompt-recommendation feature renders a ghost layer in the composer. What the host does
518
+ and does not let a plugin do, with anchors:
519
+
520
+ - **The placeholder text is NOT plugin-writable.** `placeholderText` is computed inside the
521
+ host's `InputBar.tsx:363-372` (`packages/client/ui-conversation/src/client/skeleton/`): an
522
+ owner prop (passed only by the host's own `ConversationContent`) ?? offline/disabled/steer
523
+ states ?? `useProjection('plan')` swap ?? locale defaults. Every string is locale-owned
524
+ (`client/locales.ts`, enforced upstream by `verify-client-ui-i18n`); no config key, slot,
525
+ projection input, or service feeds a plugin value into that chain. Plan mode swaps it from
526
+ host code (`InputBar.tsx:67-68`) — mechanism precedent, not an extension point.
527
+ - **The composer overlay slot is the hole.** `'conversation.input.overlay'` — "floating
528
+ entries rendered inside the resident composer card", `{ kind: 'list'; scope: 'session' }`
529
+ (`contract/slots.ts:271-272`), rendered at `InputBar.tsx:404` inside
530
+ `[data-composer-overlay]`. The shipped slash-menu occupies it
531
+ (`ui-input-trigger/src/client/index.ts:65`) — registering there is a proven path, and
532
+ `slots.inject(name, …)` waits for the declaration, same as our assistant-actions door rows.
533
+ - **Standard session props give the composer machine.** Session-scope slot entries receive
534
+ `useConversation` / `useInput` / `inputActions` (`contract/slots.ts:329-336`); `InputActions`
535
+ carries `setDraft(text)` ("replace the whole draft with plain text"), `insertText`,
536
+ `persistDraft`, `submit` (`contract/input.ts:263-291`), and `InputState.draft` is the
537
+ published draft text (`contract/input.ts`, `InputState`). `useProjection` is on
538
+ `SessionStandardProps` (`ui-session/src/client/index.ts:166-173`) for the 0.2.x push upgrade.
539
+ - **ArrowRight is free.** The composer keymap registers `KEY_ARROW_UP/DOWN`, `KEY_TAB`,
540
+ `KEY_ESCAPE`, `KEY_SPACE`, `KEY_ENTER` at `COMMAND_PRIORITY_CRITICAL`
541
+ (`input/editor/keymap.ts:104-140`); `grep ArrowRight` over `ui-conversation/src/client` is
542
+ empty on both 0.1.5 and the 0.2.1 checkout. `ComposerKeyboard` itself is
543
+ "InputBar-exclusive … never across a plugin boundary" (`contract/draft-editor.ts:54-58`),
544
+ so the plugin's accept gesture attaches a capture-phase `keydown` listener at the DOM level
545
+ while its ghost is visible — fail-open: if the host ever claims the key or reshapes the DOM,
546
+ the click-to-accept path still works and nothing else breaks.
547
+ - **Host→client pull channel: `remote.commands.execute(sessionId, line, [])`**, the exact wire
548
+ the door buttons ride (`src/client/doors.ts`, Playwright-verified; result arm
549
+ `{value.result.kind/text}`). New **wire events are NOT available to plugins**: the forwarded
550
+ allowlist is app-owned ("the one home of this application's forwarded-Host-event allowlist",
551
+ `packages/api/remotes/src/remote-events.ts:24`).
552
+ - **Push transport is 0.2.x-only.** `appendPluginRecord(session, 'plugin:<name>', data)`
553
+ (`packages/core/session/src/index.ts:492`, grammar/types at `types.ts:525-543`, reader
554
+ `pluginRecordOf` at `index.ts:508`): durable, append-only, "never enters the model-visible
555
+ surface", retained by fork/resume, skipped by unknown readers. The `verify-plugin-record-callers`
556
+ gate is a **source-tree policy inside the dsh repo** ("rejects any other production caller
557
+ *in this repository*") — it does not and cannot gate an external plugin, and the `plugin:`
558
+ grammar is precisely the external-plugin carrier. `PluginRecordMap` is declaration-merged
559
+ (`export interface PluginRecordMap {}`, `types.ts:533`); a `ProjectionDefinition` registered
560
+ on `ctx.sessionProjections` (the plan/goal pattern; our host plane ALREADY injects
561
+ `sessionProjections`) folds committed events to client-pushed values. Both APIs are absent
562
+ from our installed `@deepseek-ai/dsh-session@0.1.5-rc.2` (grep-verified on `lib/types`),
563
+ which is why #71 ships the PULL transport; the push swap is the backlog item, and it needs
564
+ the compile-time `@deepseek-ai/*` closure moved to 0.2.x alongside the runtime pin.
565
+
566
+ ## Client Sessions Service: which verbs exist (measured 0.2.0-rc.2, #77)
567
+
568
+ The client service injected as `sessions` (`dsh-api-session-controller/lib/types/client/sessions/
569
+ service.js`) is a **session store and lifecycle**, not a navigator. Its verbs are
570
+ `create / fork / using / retain / retainInfo / release / refresh / projectList / binding /
571
+ handleSessionAdded / handleSessionRemoved / handleSessionActivity / …`. There is **no
572
+ `sessions.open`** — a guard written as `if (typeof sessions.open !== 'function') return false`
573
+ fails silently on every call, which is exactly how the chapters door spent three evenings looking
574
+ like a client that refused to catalogue a plugin-created child.
575
+
576
+ The navigation verb lives on the workspace service (`dsh-client-ui-workspace/lib/client.js`):
577
+
578
+ ```js
579
+ openSession(target) { this.replaceMain(target, this.lifetime.signal, "reveal"); }
580
+ ```
581
+
582
+ and the host's own fork affordance uses it — `dsh-client-ui-chat/lib/client.js`, right after its
583
+ fork RPC resolves: `.then((childId) => { ctx.uiWorkspace.openSession(childId); })`. So a plugin
584
+ that must move the visible session injects **`uiWorkspace`** and calls `openSession(sessionId)`;
585
+ `sessions.*` cannot do it. `sessions.using/retain` materialize a scope for a session the client
586
+ addresses; they do not change what is displayed.
587
+
588
+ Two corollaries, both bought once:
589
+ - A client capability you assume is a client capability you must assert at the decision point —
590
+ `src/client/doors.ts` now prints which verbs were actually present when it decided
591
+ (`+c/+u/+o/+w`), because a note that blames the user's client for the plugin's own missing port
592
+ sends the next reader down the wrong path.
593
+ - An injected port that is optional and stops being wired fails as a symptom, not as an error:
594
+ `tests/unit/client-doors.test.ts` has a row that fails if `src/client/index.ts` drops
595
+ `uiWorkspace` from `inject` or stops passing it.