@try-works/dsh-memory 0.1.27 → 0.1.28

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/STATUS.md CHANGED
@@ -3,8 +3,8 @@
3
3
  A tracker and planner: what exists, what is verified working, what is written but not
4
4
  loaded, and what is left. Kept as a record of MEASURED state, not intention.
5
5
 
6
- **Last measured: 2026-10-12** (408 tests passing; plugin at 0.1.26, published and installed
7
- byte-identical to source; store committed and pushed to `origin`).
6
+ **Last measured: 2026-10-17** (408 tests passing; plugin at 0.1.27, published and installed
7
+ byte-identical and confirmed LOADED by the running host; queue terminal-clean).
8
8
 
9
9
  ## How to read this
10
10
 
@@ -68,24 +68,28 @@ Observed in the running host. Each row names the effect that was seen, not a cla
68
68
  | Git sync works live | after the 0.1.22+ boot the log shows real `sync` events, `state/sync.json` records `ok:true remote:https://github.com/try-works/dsh-memory-store` (2026-10-11T10:34:54Z), and `git log` in the store holds `memory(store): update [dsh-memory]` commits — the first syncs this plugin has ever performed |
69
69
  | History ingest works live | `history-check ok=true` entries after the boot, including `count=1` at 09:37Z — a commit was actually ingested; the hourly `null.run` error era is over |
70
70
 
71
- ## WRITTEN but NOT LOADED
71
+ ## Shipped and LOADED
72
72
 
73
- Implemented and tested on disk. The running host predates them, so none of this is in effect.
73
+ Formerly "WRITTEN but NOT LOADED". Every row below was verified present in the **installed**
74
+ 0.1.27 package (grepped in `profiles/web/node_modules/@try-works/dsh-memory`), not merely in the
75
+ source tree — that distinction is the whole point of this document. One row used to sit here as
76
+ "what remains pending"; nothing does now.
74
77
 
75
- | change | why it matters | how it was verified on disk |
78
+ | change | why it matters | how it was verified |
76
79
  |---|---|---|
77
80
  | `due()` priority ordering | retry → abandon → work → recover, so a bounded batch reaches a due retry | measured: the retry sat at position 101 of 111 and is now position 1 |
78
- | `hostExtras(host)` as one source | `index.js` and the wiring test shared a copied list that drifted | the test now derives extras from the host object |
79
- | `memory_recover` report omits its `entries` field rather than blanking it | the old line added `entries: undefined`, which is NOT valid JSON, so the harness rejected the whole result | the new guard round-trips every tool output; reintroducing it fails with `value.entries is undefined` |
80
- | Publisher stops when the harness does not stamp `ignorable` (0.1.21) | ≤0.1.19 appended `memory/map` WITHOUT the marker into ten real session logs, so every later load of those sessions failed ("unknown to this harness and not marked ignorable"). The guard checks the RETURN VALUE of `session.append` and suppresses publishing for that session at the first refusal, instead of poisoning the log | 403 tests pass; the new test drives a harness-shaped stub that returns an unstamped event and asserts the next turn appends nothing, even with a changed store and a passed throttle |
81
- | Git runner resolves the subprocess service lazily (0.1.22) | `makeGitRunner` probed `ctx.get('subprocess')` ONCE at wire time; cordis `get()` only returns services whose providing fiber is ACTIVE (vendor/cordis reflect.ts), so during mount it returns undefined, git stayed null forever, and every git path was dead: zero sync events ever recorded, and `history-check` threw `Cannot read properties of null (reading 'run')` hourly since 2026-10-06. The runner now re-probes at use and reports an honest reason while absent | 405 tests pass; the new test pins no-probe-at-mount, an honest refusal while the service is absent, and working ingest after it appears |
82
- | `classifyGitResult` reads `exitCode`, the name the runner actually returns (0.1.23-0.1.25) | the doc promised `code` and the code read `code`, but the injected runner returns `exitCode` — every successful git command was classified as a failure, so pull/push always reported failed and sync "had never once succeeded". `memory_sync` also stopped counting local commits as `synced: true` | sync tests drive the runner through the real classifier; an empty remote is asserted as the first push, not a failed pull |
83
- | `memory_recover` run path gets real services (0.1.26) | `recoverNow.run` called `recoverOne(store, services, ...)` where `services` was NEVER DEFINED — a ReferenceError the catch quietly turned into "1 declined or refused", so 75 recoverable candidates sat behind a tool that could only refuse them (observed live: limit:1 rebuilt 0). Services are now resolved at call time (`sessionQuery`), and the summary uses the plugin's own memory-model ladder | 408 tests pass; the new test executes the REAL run path through the wiring: plan → requeue → readSession → capture → digest file written, queue leaves ORPHANED |
84
-
85
- How to tell loaded from not: ask the host. `memory_recover` used to reply *"unavailable in this
86
- context"* and now replies *"returned invalid output: value is not lossless JSON"*. The change of
87
- ERROR is the proof the first fix is loaded and the tool now executes — a different failure, one
88
- layer further along. The report fix in the last row is what remains pending.
81
+ | `hostExtras(host)` as one source | `index.js` and the wiring test shared a copied list that drifted | the test derives extras from the host object |
82
+ | `memory_recover` report omits its `entries` field rather than blanking it | the old line added `entries: undefined`, which is NOT valid JSON, so the harness rejected the whole result | the guard round-trips every tool output; reintroducing it fails with `value.entries is undefined` |
83
+ | Publisher stops when the harness does not stamp `ignorable` (0.1.21) | ≤0.1.19 appended `memory/map` WITHOUT the marker into ten real session logs, so every later load of those sessions failed ("unknown to this harness and not marked ignorable"). The guard checks the RETURN VALUE of `session.append` and suppresses publishing for that session at the first refusal, instead of poisoning the log | the test drives a harness-shaped stub that returns an unstamped event and asserts the next turn appends nothing, even with a changed store and a passed throttle |
84
+ | Git runner resolves the subprocess service lazily (0.1.22) | `makeGitRunner` probed `ctx.get('subprocess')` ONCE at wire time; cordis `get()` only returns services whose providing fiber is ACTIVE, so during mount it returns undefined, git stayed null forever, and every git path was dead: zero sync events ever recorded, and `history-check` threw `Cannot read properties of null (reading 'run')` hourly from 2026-10-06. The runner now re-probes at use and reports an honest reason while absent | the test pins no-probe-at-mount, an honest refusal while the service is absent, and working ingest after it appears; live proof: real `sync` events and `state/sync.json` ok:true |
85
+ | `classifyGitResult` reads `exitCode`, the name the runner actually returns (0.1.23-0.1.25) | the doc promised `code` and the code read `code`, but the injected runner returns `exitCode` — every successful git command was classified as a failure, so pull/push always reported failed and sync "had never once succeeded". `memory_sync` also stopped counting local commits as `synced: true` | sync tests drive the runner through the real classifier; an empty remote is asserted as the first push, not a failed pull; live proof: the store pushed to GitHub |
86
+ | `memory_recover` run path gets real services (0.1.26) | `recoverNow.run` called `recoverOne(store, services, ...)` where `services` was NEVER DEFINED — a ReferenceError the catch quietly turned into "1 declined or refused", so 75 recoverable candidates sat behind a tool that could only refuse them | the test executes the REAL run path through the wiring: plan → requeue → readSession → capture → digest file written, queue leaves ORPHANED; live proof: all 75 recovered |
87
+ | Consolidation never sees a header-only digest (0.1.27) | `runConsolidation` batched `withContent.concat(headerOnly)`, so a digest with no summary section went to the model as a bare header. The reply could only be "nothing durable", which never advanced the entry, so the same empty shells were re-sent every cadence — one entry reached `consolidateAttempts: 39`. Capture writes summaries; consolidation now only reads them | the test asserts the model is never called for a header-only digest |
88
+
89
+ How to tell loaded from not: ask the host, and read the **installed** file rather than the source
90
+ tree. A version string and a successful `install_bundle` are both insufficient — on 2026-10-12 a
91
+ fix was committed but the version was not bumped, so the install was a no-op and the host kept
92
+ running the old code while the source tree looked correct.
89
93
  ### The tasks layer (new since the last restart)
90
94
 
91
95
  A fifth layer, `tasks`, global only: `global/tasks/` holds one JSON file per DSH scheduled task
@@ -94,8 +98,9 @@ your automated tasks and is in no git repository. The host DELETES a task when i
94
98
  stops, so a task can vanish with nothing to observe; the backup runs on the sync cadence and a
95
99
  restore re-arms what is missing at the first session after boot.
96
100
 
97
- Verified on disk: 396 tests pass, and `@try-works/dsh-memory@0.1.16` is published and installed
98
- byte-identical.
101
+ Verified on disk: 408 tests pass, and `@try-works/dsh-memory@0.1.27` is published and installed
102
+ byte-identical. (The layer landed at 0.1.16; that figure was left in place here long after it
103
+ stopped being the current release.)
99
104
 
100
105
  - `backupTasks` against `E:\dsh-memory`: two cron tasks captured, a second pass `written=0`.
101
106
  - The layer is at `global/tasks/`. It first shipped at `<root>/tasks`; every test used a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@try-works/dsh-memory",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Git-backed, workspace-scoped memory for the DeepSeek Harness (global layer + per-workspace layers)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.1",
41
+ "@deepseek-ai/dsh-session": "^0.2.1-rc.2",
41
42
  "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
42
43
  "@deepseek-ai/dsh-llm": "^0.2.0-rc.2"
43
44
  },
@@ -11,5 +11,14 @@ if (!id) throw new Error('usage: orphan-for-recovery.ts <root> <id>')
11
11
 
12
12
  const entry = readQueue(ROOT).get(id)
13
13
  if (!entry) throw new Error(id + ' is not in the queue')
14
+ const from = entry.state
15
+ // ORPHANED is only reachable from PENDING (and FAILED/ABANDONED). A candidate parked in HELD
16
+ // must be released back to PENDING first, which is legal, then orphaned. Do the shortest legal
17
+ // walk rather than guessing at the transition graph.
18
+ if (from === STATES.HELD) {
19
+ transition(ROOT, id, STATES.PENDING)
20
+ } else if (from !== STATES.PENDING && from !== STATES.FAILED && from !== STATES.ABANDONED) {
21
+ throw new Error(`${id} is ${from}, which has no legal route to orphaned`)
22
+ }
14
23
  transition(ROOT, id, STATES.ORPHANED, { reason: REASONS.DIGEST_GONE })
15
- console.log(`orphaned ${id} (was ${entry.state}); now run memory_recover({ id: '${id}' })`)
24
+ console.log(`orphaned ${id} (was ${from}); now run memory_recover({ id: '${id}' })`)
@@ -0,0 +1,13 @@
1
+ import { Context } from "@deepseek-ai/cordis"
2
+ import { SessionStore } from "@deepseek-ai/dsh-session"
3
+ import JsonlSessionPersistence from "@deepseek-ai/dsh-session-persistence-jsonl"
4
+ const ctx = new Context()
5
+ await ctx.plugin(SessionStore)
6
+ await ctx.plugin(JsonlSessionPersistence, { root: "C:/Users/erikb/.dsh/sessions", compression: "zstd" })
7
+ const p = ctx.get("sessionPersistence")
8
+ const id = "session-b21ecc7e-7743-4b06-b07f-453541dbcde5"
9
+ try {
10
+ const h = await p.open(id, "read")
11
+ const snap = await h.read()
12
+ console.log("REAL OPEN OK — events:", snap?.events?.length ?? snap?.log?.length ?? "(shape)")
13
+ } catch (e) { console.log("REAL OPEN FAILED:", String(e.message).slice(0, 300)) }
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { scanZstdFrames, decompressZstdFrame, decompressZstdPrefix } from "file:///D:/deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts"
3
+ import { validateStoredEvents } from "file:///D:/deepseek-harness/packages/session/session-persistence/src/storage-contract.ts"
4
+ const buf = readFileSync(process.argv[2])
5
+ const scan = scanZstdFrames(buf)
6
+ let text = ""
7
+ for (const f of scan.frames) text += (await decompressZstdFrame(buf.subarray(f.start, f.end))).toString("utf8")
8
+ if (scan.tornStart !== undefined) text += (await decompressZstdPrefix(buf.subarray(scan.tornStart))).toString("utf8")
9
+ const rows = text.split("\n").filter(Boolean).map(l => JSON.parse(l))
10
+ const meta = rows[0]; const events = rows.slice(1)
11
+ const dups = new Map(); for (const e of events) dups.set(e.seq, (dups.get(e.seq) ?? 0) + 1)
12
+ console.log("events:", events.length, "| duplicate seqs:", [...dups].filter(([,n]) => n > 1).slice(0, 5))
13
+ const bad = events.filter(e => e.type === "memory/map" && e.ignorable !== true)
14
+ console.log("memory/map without marker:", bad.length)
15
+ try { validateStoredEvents(meta, events, { kind: "jsonl", path: process.argv[2] }); console.log("VALIDATOR: OK") }
16
+ catch (e) { console.log("VALIDATOR THREW:", String(e.message).slice(0, 300)) }
@@ -400,7 +400,13 @@ window.__ModuleLoader__.load({
400
400
  // The host PUBLISHES the map into the log itself, so the tab fills in on its own and needs
401
401
  // nobody to call a tool first. This is the route that matters; the tool/result route below
402
402
  // is kept so a session whose snapshots predate the publisher still draws.
403
- if (event.type === 'memory/map') {
403
+ //
404
+ // BOTH TYPE NAMES. Harness 0.2.1 writes out-of-repo records through appendPluginRecord,
405
+ // which names them under the `plugin:` grammar; older builds wrote the bare `memory/map`
406
+ // type, and a V3 log migrated to V4 is renamed into `plugin:` but a V4 log written by an
407
+ // old build keeps the bare name. Accepting only one of them makes the tab silently empty
408
+ // for half the session history.
409
+ if (event.type === 'plugin:memory/map' || event.type === 'memory/map') {
404
410
  const published = event.data
405
411
  if (published === null || typeof published !== 'object') continue
406
412
  if (typeof published.generatedAt !== 'string' || typeof published.scope !== 'string') continue
package/src/host.js CHANGED
@@ -3,6 +3,10 @@
3
3
  import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { BlockAssembler, ReasoningEffortId, createUserMessage } from '@deepseek-ai/dsh-llm'
6
+ // Namespace import on purpose: an older harness does not export appendPluginRecord, and a named
7
+ // import of a missing ESM export fails at link time — which would break the whole plugin load
8
+ // instead of falling back to the append-with-ignorable path.
9
+ import * as dshSession from '@deepseek-ai/dsh-session'
6
10
  import { captureSession, pendingDigests, pruneDigests } from './capture.js'
7
11
  import { runConsolidation } from './consolidate-run.js'
8
12
  import { runRefresh } from './refresh.js'
@@ -807,14 +811,14 @@ export function wireHost(ctx, store, cfg) {
807
811
  if (!force && lastMapSignature.get(session.id) === signature) return
808
812
  lastMapSignature.set(session.id, signature)
809
813
  // Out-of-repo event types are absent from KNOWN_SESSION_EVENT_TYPES; without
810
- // ignorable:true the persistence read path refuses the whole session log.
811
- // The append returns the logged event: if the harness did NOT stamp
812
- // ignorable onto it (a harness older than the option), the very next load
813
- // of this session would fail with "unknown to this harness and not marked
814
- // ignorable". Detecting that from the return value lets us stop before a
815
- // log is poisoned, instead of discovering it when history fails to load.
816
- const logged = session.append('memory/map', payload, { ignorable: true })
817
- if (logged?.ignorable !== true) {
814
+ // ignorable:true the persistence read path refuses the whole session log. Harness 0.2.1
815
+ // moved that contract: `Session.append` no longer accepts an `ignorable` option at all
816
+ // (its third argument is surface metadata, valid only for surface event types), and an
817
+ // out-of-repo type is written through `appendPluginRecord`, which stamps `ignorable: true`
818
+ // by construction under the `plugin:` namespace. Both paths are handled by appendMapRecord;
819
+ // if the harness refuses to stamp the marker we suppress publishing rather than poison the
820
+ // log, so a later load cannot fail with "unknown to this harness and not marked ignorable".
821
+ if (!appendMapRecord(session, payload)) {
818
822
  if (!mapPublishSuppressed.has(session.id)) {
819
823
  mapPublishSuppressed.add(session.id)
820
824
  logRun(store.root, 'map-publish', { ok: false, error: 'harness did not stamp ignorable; publishing suppressed for this session' })
@@ -828,6 +832,28 @@ export function wireHost(ctx, store, cfg) {
828
832
  }
829
833
  }
830
834
 
835
+ /**
836
+ * Write the map snapshot into the session log as an ignorable record.
837
+ *
838
+ * Two harness contracts, chosen at call time rather than captured at wire time:
839
+ * - 0.2.1+: `appendPluginRecord(session, 'plugin:memory/map', data)`; the harness owns the
840
+ * envelope and always stamps `ignorable: true`, which is exactly what out-of-repo records
841
+ * require.
842
+ * - older: `session.append(type, data, { ignorable: true })`, whose RETURN VALUE must be
843
+ * checked — a harness that ignores the option returns an event without the marker, and
844
+ * every further append would poison the log.
845
+ * @returns whether the record was written with the ignorable marker.
846
+ */
847
+ function appendMapRecord(session, payload) {
848
+ const appendRecord = cfg.appendPluginRecord !== undefined ? cfg.appendPluginRecord : dshSession.appendPluginRecord
849
+ if (typeof appendRecord === 'function') {
850
+ appendRecord(session, 'plugin:memory/map', payload)
851
+ return true
852
+ }
853
+ const logged = session.append('memory/map', payload, { ignorable: true })
854
+ return logged?.ignorable === true
855
+ }
856
+
831
857
  function maybeSync() {
832
858
  if (syncInFlight || !git.available()) return
833
859
  if (!scheduler.shouldCommit() && !scheduler.shouldSync()) return