@try-works/dsh-memory 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,12 +150,14 @@ the flag on did not change what the user sees.
147
150
 
148
151
  ### Blocked on a restart
149
152
 
150
- - [ ] **Restart to load 0.1.21.** Published and installed: 0.1.21 is on npm (tarball byte-identical to
151
- source) and installed in the profile (host.js byte-identical, guard at host.js:727/753). The
152
- profile's minimumReleaseAge gate needed its dsh-memory exclusion extended to 0.1.21 in
153
- pnpm-workspace.yaml, the same convention as every earlier version. The host half loads at
154
- boot, so the guard only takes effect after a restart; installed 0.1.20 already appends WITH
155
- the marker, so nothing is at risk while this waits.
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).
156
161
  - [x] **Restart, then check the tasks layer — DONE 2026-10-09.** The restart confirmed it. See the
157
162
  tasks-layer section above for the two log entries and the counts. It writes to ANOTHER plugin's storage through
158
163
  that plugin's service, so it deserves a real observation rather than a passing suite. After
@@ -160,13 +165,14 @@ the flag on did not change what the user sees.
160
165
  `pending.present: 2`. Then check `logs/memory.log` for a `tasks-backup` entry and a
161
166
  `tasks-restore` entry: the first should say `refreshed: 2`, the second `present: 2, added: 0`.
162
167
  An `added` above zero would mean de-duplication failed and it duplicated a task.
163
- - [ ] **Restart again.** The previous restart worked: `memory_recover` went from "unavailable in
164
- this context" to executing. It now fails one layer further on, at output validation, which
165
- 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.
166
171
 
167
172
  ### Recovery (needs the restart)
168
173
 
169
- - [ ] `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.
170
176
  - [ ] `memory_recover({ limit: 1 })` — **prove it end to end on one** before spending on 76.
171
177
  - [ ] Then the rest in batches of ~10. Cost is roughly 76-150 model calls plus consolidation,
172
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.22",
3
+ "version": "0.1.24",
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/git.js CHANGED
@@ -26,12 +26,18 @@ export class GitRunner {
26
26
  */
27
27
  export function classifyGitResult(result) {
28
28
  if (!result) return { ok: false, reason: 'git produced no result' }
29
- if (result.code === 0) return { ok: true }
29
+ // BOTH names, because the two halves disagreed and nobody noticed: the doc comment above the
30
+ // GitRunner constructor promises a run() result carrying `code`, so this read `result.code` -
31
+ // while the runner the plugin actually injects returns `exitCode`. Every successful git command
32
+ // was therefore classified as a failure, every pull and push was reported as failed, and sync had
33
+ // never once succeeded. A classifier that rejects success is worse than no classifier.
34
+ const code = result.exitCode ?? result.code
35
+ if (code === 0) return { ok: true }
30
36
  const stdout = String(result.stdout ?? '')
31
37
  const stderr = String(result.stderr ?? '')
32
38
  const combined = stdout + ' ' + stderr
33
39
  const conflicted = /conflict|automatic merge failed|needs merge|could not apply/i.test(combined)
34
- const reason = (stderr.trim() || stdout.trim() || 'exit code ' + String(result.code)).slice(0, 400)
40
+ const reason = (stderr.trim() || stdout.trim() || 'exit code ' + String(code)).slice(0, 400)
35
41
  return { ok: false, conflicted, reason }
36
42
  }
37
43
 
package/src/host.js CHANGED
@@ -28,6 +28,19 @@ const MAP_PUBLISH_MS = 60 * 1000
28
28
  const TASKS_BACKUP_MS = 30 * 60 * 1000
29
29
  const IDLE_TICK_MS = 60_000
30
30
 
31
+ /**
32
+ * Did the pull fail only because the remote has no such branch yet?
33
+ *
34
+ * The first sync of a store that has been committing to itself - which is every store that has ever
35
+ * been used - meets a remote with no refs at all. git says so in a few ways depending on version and
36
+ * transport, and none of them is a real failure: there is simply nothing to pull and everything to
37
+ * push.
38
+ */
39
+ function isMissingRemoteBranch(result) {
40
+ const text = String((result && result.stdout) || '') + ' ' + String((result && result.stderr) || '')
41
+ return /couldn't find remote ref|could not find remote ref|no such ref|remote branch .* not found|unable to find remote ref/i.test(text)
42
+ }
43
+
31
44
  function makeGitRunner(ctx) {
32
45
  // The subprocess service is NOT resolvable during apply. cordis ctx.get() only returns an
33
46
  // implementation whose PROVIDING FIBER IS ACTIVE (vendor/cordis reflect.ts _getImpl), and no
@@ -611,9 +624,27 @@ export function wireHost(ctx, store, cfg) {
611
624
  catch (error) { ctx.logger.warn('dsh-memory: commit failed: ' + (error && error.message)) }
612
625
  }
613
626
  if (cfg.gitRemote && (force || scheduler.shouldSync())) {
627
+ // The origin is CONFIGURED, not necessarily INSTALLED. remoteAdd ran only on the fresh-repo
628
+ // path above, so a store that already had a .git - which every store that has ever committed
629
+ // does - went straight to `pull origin` with no origin at all and failed on 'no such remote',
630
+ // leaving memory.json and the profile config both claiming a remote git had never heard of.
631
+ const configured = await git.remoteGet(store.root).catch(() => null)
632
+ const installed = configured && configured.exitCode === 0 ? String(configured.stdout ?? '').trim() : ''
633
+ if (installed !== cfg.gitRemote) {
634
+ const applied = installed === ''
635
+ ? await git.remoteAdd(store.root, cfg.gitRemote).catch(() => null)
636
+ : await git.remoteSet(store.root, cfg.gitRemote).catch(() => null)
637
+ if (!applied || applied.exitCode !== 0) {
638
+ return failSync('remote', { ok: false, reason: (applied && (applied.stderr || applied.stdout)) || 'could not set origin to ' + cfg.gitRemote }, committed)
639
+ }
640
+ }
614
641
  const pulled = await git.pull(store.root, branch).catch((error) => ({ code: -1, stderr: String((error && error.message) || error) }))
615
642
  const pullVerdict = classifyGitResult(pulled)
616
- if (!pullVerdict.ok) return failSync('pull', pullVerdict, committed)
643
+ // An EMPTY remote has no branch to pull, and that is not a failure - it is the first push.
644
+ // The store has been committing to itself since it was created, so its very first sync meets a
645
+ // remote with no refs at all; failing there would mean a freshly configured origin could never
646
+ // be bootstrapped by the plugin, only by hand.
647
+ if (!pullVerdict.ok && !isMissingRemoteBranch(pulled)) return failSync('pull', pullVerdict, committed)
617
648
  const pushed = await git.push(store.root, branch).catch((error) => ({ code: -1, stderr: String((error && error.message) || error) }))
618
649
  const pushVerdict = classifyGitResult(pushed)
619
650
  if (!pushVerdict.ok) return failSync('push', pushVerdict, committed)