@treeseed/dsh-chapters 0.1.24 → 0.2.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/README.md +2 -1
- package/cordis.patch.yml +49 -1
- package/docs/architecture.md +1 -496
- package/docs/backlog.md +234 -4
- package/docs/contract.md +127 -1
- package/docs/development.md +248 -12
- package/docs/host-compaction-seam.md +108 -0
- package/docs/knowledge-repo.md +532 -16
- package/docs/project-state.md +323 -0
- package/docs/verify.md +217 -0
- package/lib/archive.d.ts +49 -1
- package/lib/archive.js +56 -2
- package/lib/archive.js.map +1 -1
- package/lib/arrival.js +24 -10
- package/lib/arrival.js.map +1 -1
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +74 -0
- package/lib/auto-save.js +162 -26
- package/lib/auto-save.js.map +1 -1
- package/lib/client.js +413 -11
- package/lib/client.js.map +1 -1
- package/lib/commands.d.ts +8 -1
- package/lib/commands.js +135 -7
- package/lib/commands.js.map +1 -1
- package/lib/compose.js +25 -6
- package/lib/compose.js.map +1 -1
- package/lib/continue-core.d.ts +24 -1
- package/lib/continue-core.js +41 -9
- package/lib/continue-core.js.map +1 -1
- package/lib/diag.d.ts +47 -0
- package/lib/diag.js +141 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +51 -1
- package/lib/engine-core.js +63 -7
- package/lib/engine-core.js.map +1 -1
- package/lib/engine.d.ts +93 -0
- package/lib/engine.js +477 -36
- package/lib/engine.js.map +1 -1
- package/lib/enrich-queue.js +28 -2
- package/lib/enrich-queue.js.map +1 -1
- package/lib/enrich-wire.d.ts +92 -2
- package/lib/enrich-wire.js +135 -17
- package/lib/enrich-wire.js.map +1 -1
- package/lib/enrich.d.ts +27 -2
- package/lib/enrich.js +114 -18
- package/lib/enrich.js.map +1 -1
- package/lib/index.d.ts +51 -0
- package/lib/index.js +180 -2
- package/lib/index.js.map +1 -1
- package/lib/indexing.d.ts +7 -0
- package/lib/indexing.js.map +1 -1
- package/lib/injections.d.ts +1 -0
- package/lib/injections.js +38 -0
- package/lib/injections.js.map +1 -1
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/prompt.d.ts +130 -0
- package/lib/prompt.js +255 -0
- package/lib/prompt.js.map +1 -0
- package/lib/registry.d.ts +39 -1
- package/lib/registry.js +76 -9
- package/lib/registry.js.map +1 -1
- package/lib/render.d.ts +31 -3
- package/lib/render.js +75 -3
- package/lib/render.js.map +1 -1
- package/lib/repair-run.d.ts +49 -0
- package/lib/repair-run.js +423 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +200 -0
- package/lib/repair.js +354 -0
- package/lib/repair.js.map +1 -0
- package/lib/rules-commands.d.ts +22 -0
- package/lib/rules-commands.js +177 -20
- package/lib/rules-commands.js.map +1 -1
- package/lib/rules.d.ts +184 -7
- package/lib/rules.js +312 -27
- package/lib/rules.js.map +1 -1
- package/lib/save-core.d.ts +31 -1
- package/lib/save-core.js +153 -17
- package/lib/save-core.js.map +1 -1
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/store.d.ts +22 -1
- package/lib/store.js +94 -17
- package/lib/store.js.map +1 -1
- package/lib/sync.js +13 -1
- package/lib/sync.js.map +1 -1
- package/lib/tools.d.ts +14 -0
- package/lib/tools.js +311 -32
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +12 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- package/package.json +9 -3
package/docs/backlog.md
CHANGED
|
@@ -1,4 +1,78 @@
|
|
|
1
|
-
# Open items — 2026-10-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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.
|