@try-works/dsh-memory 0.1.21 → 0.1.23

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 (3) hide show
  1. package/STATUS.md +22 -15
  2. package/package.json +1 -1
  3. package/src/host.js +41 -6
package/STATUS.md CHANGED
@@ -62,6 +62,8 @@ Observed in the running host. Each row names the effect that was seen, not a cla
62
62
  | `memory_recover` is wired | calling it no longer answers "unavailable in this context"; it reaches output validation |
63
63
  | Session logs are searchable in principle | zstd, not encrypted; 368 logs, 740MB compressed, ~2.96GB plaintext |
64
64
  | `memory/map` events carry `ignorable: true` | the live session log's own event is marked, and both dsh-memory logs pass the harness's `validateStoredEvents` — the exact check that refused session-cc4795a7 with "unknown to this harness and not marked ignorable" |
65
+ | 0.1.21 guard is LOADED | the 2026-10-11 06:01+08 restart booted the installed 0.1.21; `map-publish ok:true` events follow boot, so the guarded publisher runs. The suppression branch itself cannot be observed on this harness — it only fires on one that does not stamp the marker |
66
+ | Boot tasks backup + restore ran live | at boot (2026-10-10T22:03:14Z) the log records `tasks-backup ok:true written:0 refreshed:5` and `tasks-restore ok:true owner:session-6e8d9a10 present:5 added:0 paused:0` — the restore reached the schedule service, found everything present, and duplicated nothing |
65
67
 
66
68
  ## WRITTEN but NOT LOADED
67
69
 
@@ -73,6 +75,7 @@ Implemented and tested on disk. The running host predates them, so none of this
73
75
  | `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 |
74
76
  | `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` |
75
77
  | 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 |
78
+ | 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 |
76
79
 
77
80
  How to tell loaded from not: ask the host. `memory_recover` used to reply *"unavailable in this
78
81
  context"* and now replies *"returned invalid output: value is not lossless JSON"*. The change of
@@ -102,12 +105,12 @@ byte-identical.
102
105
  - No task file leaks into memory search, checked by building the index over a store holding the layer.
103
106
  - `SYSTEM.md` names the layer, its path, `memory_tasks`, and all eight states.
104
107
 
105
- **NOT verified, and the log proves it.** `E:\dsh-memory\logs\memory.log` holds 3976 entries and
106
- ZERO `tasks-backup` or `tasks-restore` ones, while the running host is 3h24m old and predates every
107
- install from 0.1.10 on. The plugin is alive — it is injecting, capturing and consolidating — it has
108
- simply never executed this code. The cadence and the boot restore have only ever run inside the
109
- wiring tests, where the schedule service is a stub and the store is temporary. **Calling the module
110
- directly is not the same claim as the plugin calling it.**
108
+ **VERIFIED live, 2026-10-11 boot.** `logs/memory.log` at 2026-10-10T22:03:14Z records a real
109
+ `tasks-backup ok:true written:0 refreshed:5` and a real `tasks-restore ok:true present:5 added:0
110
+ paused:0` from the running host — the cadence and the boot restore executed outside the test
111
+ harness for the first time, reached the schedule service, found all five tasks present, and
112
+ duplicated nothing (`added: 0`, which is the de-duplication property the drill cared about). The
113
+ paragraph below is retained as the record of what "unverified" looked like before this boot.
111
114
 
112
115
  Seven defects were found by asking what the REAL path does, and not one by an existing test: the
113
116
  empty-string config fallback, the wrong directory, a fixture-coincidence assertion, `orphaned` with
@@ -147,11 +150,14 @@ the flag on did not change what the user sees.
147
150
 
148
151
  ### Blocked on a restart
149
152
 
150
- - [ ] **Publish 0.1.21, install, restart.** The guard against unmarked `memory/map` events is
151
- written and tested (403 pass) but the publish needs npm credentials that were not available
152
- in this session (`npm publish` → ENEEDAUTH). Installed 0.1.20 already appends WITH the
153
- marker, so nothing is at risk while this waits; the guard only protects against a harness
154
- older than the `append(type, data, { ignorable: true })` option.
153
+ - [ ] **Restart to load 0.1.22.** The lazy git-runner fix is published; install is pending the
154
+ registry finishing its staged publish (E409 "previously staged version" on retry). Once the
155
+ profile install lands and the host restarts, the hourly `history-check ok:false null.run`
156
+ should become either a working ingest or an honest `reason` — and git sync gets its first
157
+ real chance to run ever.
158
+ - [x] **Restart to load 0.1.21 — DONE 2026-10-11 06:01+08.** The host booted the installed 0.1.21;
159
+ `map-publish ok:true` events follow boot and the boot tasks backup/restore ran live (see
160
+ LIVE table).
155
161
  - [x] **Restart, then check the tasks layer — DONE 2026-10-09.** The restart confirmed it. See the
156
162
  tasks-layer section above for the two log entries and the counts. It writes to ANOTHER plugin's storage through
157
163
  that plugin's service, so it deserves a real observation rather than a passing suite. After
@@ -159,13 +165,14 @@ the flag on did not change what the user sees.
159
165
  `pending.present: 2`. Then check `logs/memory.log` for a `tasks-backup` entry and a
160
166
  `tasks-restore` entry: the first should say `refreshed: 2`, the second `present: 2, added: 0`.
161
167
  An `added` above zero would mean de-duplication failed and it duplicated a task.
162
- - [ ] **Restart again.** The previous restart worked: `memory_recover` went from "unavailable in
163
- this context" to executing. It now fails one layer further on, at output validation, which
164
- is fixed on disk. Confirm by calling `memory_recover({})` and getting a report, not an error.
168
+ - [x] **Restart again — DONE 2026-10-11.** After the 06:01+08 restart, `memory_recover({})` answers
169
+ with a report: 75 candidates stopped with missing material, 75 recoverable now, 0 need
170
+ approval. The output-validation fix is live.
165
171
 
166
172
  ### Recovery (needs the restart)
167
173
 
168
- - [ ] `memory_recover({})` — report only, no cost.
174
+ - [x] `memory_recover({})` — DONE 2026-10-11: report-only call returned a clean report (75
175
+ recoverable, 0 oversized), no error.
169
176
  - [ ] `memory_recover({ limit: 1 })` — **prove it end to end on one** before spending on 76.
170
177
  - [ ] Then the rest in batches of ~10. Cost is roughly 76-150 model calls plus consolidation,
171
178
  against **52 MB** of logs. All 76 are under the 8 MB approval threshold, so nothing will
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@try-works/dsh-memory",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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",
package/src/host.js CHANGED
@@ -29,15 +29,28 @@ const TASKS_BACKUP_MS = 30 * 60 * 1000
29
29
  const IDLE_TICK_MS = 60_000
30
30
 
31
31
  function makeGitRunner(ctx) {
32
- const subprocess = ctx.get('subprocess')
33
- if (!subprocess || typeof subprocess.resolveExecutable !== 'function' || typeof subprocess.spawn !== 'function') return null
32
+ // The subprocess service is NOT resolvable during apply. cordis ctx.get() only returns an
33
+ // implementation whose PROVIDING FIBER IS ACTIVE (vendor/cordis reflect.ts _getImpl), and no
34
+ // fiber is active while the composition is still mounting. The first version probed here,
35
+ // once, at wire time - it cached undefined, returned null, and every git path was dead
36
+ // forever: zero sync events ever recorded, and the history hook threw "Cannot read
37
+ // properties of null (reading 'run')" every hour. The service exists by first USE (an agent
38
+ // pre-step, minutes after boot), so resolve lazily and keep re-probing until it appears.
39
+ let subprocess
40
+ const probe = () => {
41
+ const svc = ctx.get('subprocess')
42
+ if (svc && typeof svc.resolveExecutable === 'function' && typeof svc.spawn === 'function') subprocess = svc
43
+ return subprocess
44
+ }
34
45
  let executablePromise
35
46
  const run = async (argv, opts = {}) => {
36
- if (!executablePromise) executablePromise = subprocess.resolveExecutable('git', undefined, undefined).catch(() => null)
47
+ const svc = probe()
48
+ if (!svc) return { exitCode: 127, stdout: '', stderr: 'subprocess service unavailable' }
49
+ if (!executablePromise) executablePromise = svc.resolveExecutable('git', undefined, undefined).catch(() => null)
37
50
  const executable = await executablePromise
38
51
  if (!executable) return { exitCode: 127, stdout: '', stderr: 'git unavailable' }
39
52
  try {
40
- const handle = subprocess.spawn({
53
+ const handle = svc.spawn({
41
54
  argv: [executable, ...argv],
42
55
  cwd: opts.cwd,
43
56
  stdio: { stdin: 'ignore', stdout: { maxBytes: 4 * 1024 * 1024 }, stderr: { maxBytes: 16 * 1024 } },
@@ -52,7 +65,9 @@ function makeGitRunner(ctx) {
52
65
  return { exitCode: 1, stdout: '', stderr: String(error) }
53
66
  }
54
67
  }
55
- return new GitRunner(run)
68
+ const runner = new GitRunner(run)
69
+ runner.available = () => Boolean(probe())
70
+ return runner
56
71
  }
57
72
 
58
73
  function resolveTarget(agent) {
@@ -235,6 +250,9 @@ export function wireHost(ctx, store, cfg) {
235
250
  if (!workspace) return { ok: false, reason: 'no workspace for this session', added: 0 }
236
251
  const path = readAssociations(store.root)[workspace]?.path
237
252
  if (!path) return { ok: false, reason: 'no local folder associated with ' + workspace, added: 0 }
253
+ // Report the honest reason instead of throwing: a hook that throws hourly because a service
254
+ // was probed too early must never look like a repository with no commits.
255
+ if (!git.available()) return { ok: false, reason: 'subprocess service unavailable', added: 0 }
238
256
  return ingestHistory(store.root, workspace, path, (argv, opts) => git.run(argv, opts), { limit })
239
257
  }
240
258
 
@@ -570,6 +588,9 @@ export function wireHost(ctx, store, cfg) {
570
588
  }
571
589
 
572
590
  async function runSync(force = false) {
591
+ // Not a git failure: no CONFLICT marker, no status record - the capability simply is not
592
+ // there yet. available() re-probes, so this is true only while the host is still mounting.
593
+ if (!git.available()) return { initialized: false, committed: false, synced: false, error: 'subprocess service unavailable' }
573
594
  const branch = cfg.gitBranch || 'main'
574
595
  const hasGit = existsSync(join(store.root, '.git'))
575
596
  if (!hasGit) {
@@ -590,6 +611,20 @@ export function wireHost(ctx, store, cfg) {
590
611
  catch (error) { ctx.logger.warn('dsh-memory: commit failed: ' + (error && error.message)) }
591
612
  }
592
613
  if (cfg.gitRemote && (force || scheduler.shouldSync())) {
614
+ // The origin is CONFIGURED, not necessarily INSTALLED. remoteAdd ran only on the fresh-repo
615
+ // path above, so a store that already had a .git - which every store that has ever committed
616
+ // does - went straight to `pull origin` with no origin at all and failed on 'no such remote',
617
+ // leaving memory.json and the profile config both claiming a remote git had never heard of.
618
+ const configured = await git.remoteGet(store.root).catch(() => null)
619
+ const installed = configured && configured.exitCode === 0 ? String(configured.stdout ?? '').trim() : ''
620
+ if (installed !== cfg.gitRemote) {
621
+ const applied = installed === ''
622
+ ? await git.remoteAdd(store.root, cfg.gitRemote).catch(() => null)
623
+ : await git.remoteSet(store.root, cfg.gitRemote).catch(() => null)
624
+ if (!applied || applied.exitCode !== 0) {
625
+ return failSync('remote', { ok: false, reason: (applied && (applied.stderr || applied.stdout)) || 'could not set origin to ' + cfg.gitRemote }, committed)
626
+ }
627
+ }
593
628
  const pulled = await git.pull(store.root, branch).catch((error) => ({ code: -1, stderr: String((error && error.message) || error) }))
594
629
  const pullVerdict = classifyGitResult(pulled)
595
630
  if (!pullVerdict.ok) return failSync('pull', pullVerdict, committed)
@@ -764,7 +799,7 @@ export function wireHost(ctx, store, cfg) {
764
799
  }
765
800
 
766
801
  function maybeSync() {
767
- if (syncInFlight || !git) return
802
+ if (syncInFlight || !git.available()) return
768
803
  if (!scheduler.shouldCommit() && !scheduler.shouldSync()) return
769
804
  syncInFlight = true
770
805
  // Sync wrote its outcome only to state/sync.json, so the log could not answer whether the