@try-works/dsh-memory 0.1.26 → 0.1.27
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 +67 -36
- package/package.json +1 -1
- package/scripts/inspect-one-session.ts +28 -0
- package/scripts/inspect-search-db.ts +24 -0
- package/scripts/orphan-for-recovery.ts +15 -0
- package/scripts/probe-search.ts +44 -0
- package/scripts/scan-unreadable.ts +52 -0
- package/scripts/skip-subagent-held.ts +24 -0
- package/src/consolidate-run.js +10 -9
package/STATUS.md
CHANGED
|
@@ -3,7 +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-
|
|
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`).
|
|
7
8
|
|
|
8
9
|
## How to read this
|
|
9
10
|
|
|
@@ -64,6 +65,8 @@ Observed in the running host. Each row names the effect that was seen, not a cla
|
|
|
64
65
|
| `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
66
|
| 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
67
|
| 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 |
|
|
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
|
+
| 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 |
|
|
67
70
|
|
|
68
71
|
## WRITTEN but NOT LOADED
|
|
69
72
|
|
|
@@ -76,6 +79,8 @@ Implemented and tested on disk. The running host predates them, so none of this
|
|
|
76
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` |
|
|
77
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 |
|
|
78
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 |
|
|
79
84
|
|
|
80
85
|
How to tell loaded from not: ask the host. `memory_recover` used to reply *"unavailable in this
|
|
81
86
|
context"* and now replies *"returned invalid output: value is not lossless JSON"*. The change of
|
|
@@ -121,15 +126,31 @@ reporting success.
|
|
|
121
126
|
Known limitation, by design: the host deletes a task when its owning session stops, so a restored
|
|
122
127
|
task lives as long as the session that adopted it. The boot restore is what re-arms them.
|
|
123
128
|
|
|
124
|
-
##
|
|
129
|
+
## OUT OF SCOPE — not dsh-memory's problem
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
Closed 2026-10-12 by operator decision. Recorded once so it is not re-investigated.
|
|
132
|
+
|
|
133
|
+
The sidebar's full-text session search does not work on this machine, and **that is not
|
|
134
|
+
dsh-memory's concern**. The session logs under `~/.dsh/sessions/` are DSH's own files: they are
|
|
135
|
+
written by `session-persistence-jsonl`, they exist whether or not this plugin is installed, and
|
|
136
|
+
they are not memory data. This store holds no session logs at all.
|
|
137
|
+
|
|
138
|
+
What was found while investigating (kept only so nobody repeats the work): 91 of 630 stored logs
|
|
139
|
+
are legacy v0 artifacts DSH cannot read (`subagent/descriptor ... unsupported descriptor version
|
|
140
|
+
2`); one such log aborts the whole corpus reconcile, so the search index stays empty and the
|
|
141
|
+
sidebar falls back to matching titles. Verified by isolation — two valid v4 logs return content
|
|
142
|
+
hits, and adding one bad v0 log fails the same query.
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
Why this is not a dsh-memory bug: the plugin **never calls session search**. It reads individual
|
|
145
|
+
sessions directly through `sessionQuery.readSession` in `capture.js` and `recover.js`. The only
|
|
146
|
+
link is one config line — this plugin's patch layer sets `openAt: first-search` on the
|
|
147
|
+
`session-query-sqlite` row, which is what turns the harness's opt-in search on. Nothing in the
|
|
148
|
+
plugin's code is involved.
|
|
149
|
+
|
|
150
|
+
Per instruction: do not modify the harness, do not touch the session logs, do not work around
|
|
151
|
+
this from the plugin. Sidebar search stays title-only; dsh-memory is unaffected.
|
|
152
|
+
|
|
153
|
+
### Where the sidebar search comes from
|
|
133
154
|
|
|
134
155
|
Recorded because it was got wrong once. `client/ui-workspace/src/client/tree.ts`
|
|
135
156
|
`deriveSearchResults` has two sources:
|
|
@@ -139,22 +160,21 @@ Recorded because it was got wrong once. `client/ui-workspace/src/client/tree.ts`
|
|
|
139
160
|
sessionTitle(summary).toLowerCase().includes(q)
|
|
140
161
|
|| labelOf(summary).toLowerCase().includes(q)
|
|
141
162
|
|
|
142
|
-
// CONTENT - server-side FTS hits, empty
|
|
163
|
+
// CONTENT - server-side FTS hits, empty because the index cannot fill
|
|
143
164
|
for (const item of content.items) include(summary)
|
|
144
165
|
```
|
|
145
166
|
|
|
146
|
-
So title search works and content search is wired but starved. The distinction matters: turning
|
|
147
|
-
the flag on did not change what the user sees.
|
|
148
|
-
|
|
149
167
|
## TODO — ordered
|
|
150
168
|
|
|
151
169
|
### Blocked on a restart
|
|
152
170
|
|
|
153
|
-
- [
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
- [x] **Restart to load 0.1.26 — DONE 2026-10-12 05:18+08.** After the boot, `memory_recover({limit:1})`
|
|
172
|
+
rebuilt one candidate (log: `recover count:1 detail.id 9521e4c9...`), and the rebuilt digest
|
|
173
|
+
was committed by the sync cadence right after. The recover run path is live.
|
|
174
|
+
- [x] **Restart to load 0.1.22 — DONE 2026-10-11.** The lazy git-runner fix is live: real `sync`
|
|
175
|
+
events in the log, `state/sync.json` ok:true against the configured remote, `memory(store)`
|
|
176
|
+
commits in the store's git history, and `history-check ok=true` entries including an ingested
|
|
177
|
+
commit. The hourly `null.run` era is over.
|
|
158
178
|
- [x] **Restart to load 0.1.21 — DONE 2026-10-11 06:01+08.** The host booted the installed 0.1.21;
|
|
159
179
|
`map-publish ok:true` events follow boot and the boot tasks backup/restore ran live (see
|
|
160
180
|
LIVE table).
|
|
@@ -173,23 +193,33 @@ the flag on did not change what the user sees.
|
|
|
173
193
|
|
|
174
194
|
- [x] `memory_recover({})` — DONE 2026-10-11: report-only call returned a clean report (75
|
|
175
195
|
recoverable, 0 oversized), no error.
|
|
176
|
-
- [
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
- [x] `memory_recover({ limit: 1 })` — DONE 2026-10-12: rebuilt 1, declined 0 (the 0.1.26 run
|
|
197
|
+
path). Attempted earlier on 0.1.25 it rebuilt 0 and declined 1 — the live proof that the
|
|
198
|
+
undefined-`services` ReferenceError was real.
|
|
199
|
+
- [x] The rest in batches of ~10 — DONE 2026-10-12: all 75 candidates recovered (1 proof + 10/10
|
|
200
|
+
batches through the queue, `logs/memory.log` shows `recover` count=75), 0 remain. Each
|
|
201
|
+
rebuild was followed by a sync commit; `state/sync.json` stayed ok:true. Recovered digests
|
|
202
|
+
now flow into the normal consolidation cadence.
|
|
180
203
|
### Open work, not blocked
|
|
181
204
|
|
|
182
|
-
- [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
- [x] **The held backlog — CLEARED 2026-10-12.** Both figures quoted here for weeks were WRONG,
|
|
206
|
+
which is why this sat untouched: the real store had **one** record marked `unclassified` (not
|
|
207
|
+
79 — the append-only journal is the authority, and it contained exactly one), and
|
|
208
|
+
`state/backfill-hold.json` is read by **no production code at all** (an orphaned artefact of the
|
|
209
|
+
retired `sessions.json` migration; 27 of its 28 ids are not even in the queue). The genuine
|
|
210
|
+
backlog was 80 pending digests, consolidated in batches into **~215 entries**. Two real defects
|
|
211
|
+
were found while doing it:
|
|
212
|
+
- **Header-only digests were sent to the model.** `runConsolidation` batched
|
|
213
|
+
`withContent.concat(headerOnly)`, so a digest with no summary section went to the LLM as a bare
|
|
214
|
+
header. The reply could only be "nothing durable", which never advanced the entry, so the same
|
|
215
|
+
empty shells were re-sent every cadence — one entry reached `consolidateAttempts: 39`. Fixed:
|
|
216
|
+
consolidation now filters to digests that actually contain a summary section (capture writes
|
|
217
|
+
summaries; consolidation only reads them), with a test asserting the model is never called.
|
|
218
|
+
- **A stuck `pending` candidate had no operator path.** Recovery accepts orphaned/abandoned/failed
|
|
219
|
+
but not pending, and capture never re-summarizes a historical session. One genuine main-agent
|
|
220
|
+
session (1317 events) was recovered by marking it orphaned, then `memory_recover`.
|
|
221
|
+
The other 8 stragglers were `origin: subagent` sessions — never memory material by
|
|
222
|
+
`isSubagentSession` — and were skipped with a truthful reason.
|
|
193
223
|
- [ ] **Agent-facing session tools** (`session_search` and friends). NOT a config change. In a
|
|
194
224
|
Web profile the tool plane belongs to a preset, and a patch REPLACES a preset row config
|
|
195
225
|
wholesale — so adding one tool means restating the whole ~20-row `config.plugins` list and
|
|
@@ -217,10 +247,11 @@ Kept because the pattern matters more than the list: every one of these was foun
|
|
|
217
247
|
| A test edit silently discarded | my own script re-read and overwrote the file before writing |
|
|
218
248
|
## Published
|
|
219
249
|
|
|
220
|
-
**`@try-works/dsh-memory@0.1.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
**`@try-works/dsh-memory@0.1.26`** on npm, installed in the profile byte-identical to source.
|
|
251
|
+
(0.1.0 was the first release; the Memory tab, the tasks layer, the ignorable/log-poisoning fix, the
|
|
252
|
+
git-sync fixes and the recovery run-path fix each added versions in between.) The unscoped name
|
|
253
|
+
`dsh-memory` was already taken by an unrelated DeepSeek Harness memory plugin, so the package
|
|
254
|
+
publishes under the existing `@try-works` scope.
|
|
224
255
|
|
|
225
256
|
What did NOT change, deliberately: the Cordis plugin name (`export const name = 'dsh-memory'`) and
|
|
226
257
|
the bundle row id (`id: dsh-memory`). Config is addressed by ROW ID, so an existing profile's
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// One-session inspection via the harness's real session-query reader.
|
|
3
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import { SessionStore } from '@deepseek-ai/dsh-session'
|
|
5
|
+
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
|
6
|
+
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
|
|
7
|
+
|
|
8
|
+
const root = process.argv[2] ?? 'C:/Users/erikb/.dsh/sessions'
|
|
9
|
+
const id = process.argv[3]
|
|
10
|
+
if (!id) throw new Error('usage: inspect-one-session.ts <sessions-root> <id>')
|
|
11
|
+
const ctx = new Context()
|
|
12
|
+
await ctx.plugin(SessionStore)
|
|
13
|
+
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'zstd' })
|
|
14
|
+
await ctx.plugin(SqliteSessionQueryEngine, { path: ':memory:', openAt: 'first-search' })
|
|
15
|
+
const snapshot = await ctx.sessionQuery.readSession(id)
|
|
16
|
+
console.log(JSON.stringify({ id: snapshot.session.id, cwd: snapshot.session.cwd, createdAt: snapshot.session.createdAt, count: snapshot.events.length, origin: snapshot.session.origin, delegationDepth: snapshot.session.delegationDepth, parentSession: snapshot.session.parentSession }))
|
|
17
|
+
for (const event of snapshot.events) {
|
|
18
|
+
if (event.type === 'user/message') {
|
|
19
|
+
const text = event.data.message?.content?.filter?.(x => x.type === 'text').map(x => x.text).join('\n') ?? ''
|
|
20
|
+
if (text) console.log(`USER[${event.seq}]: ${text.slice(0, 500)}`)
|
|
21
|
+
} else if (event.type === 'assistant/message') {
|
|
22
|
+
const text = event.data.message?.content?.filter?.(x => x.type === 'text').map(x => x.text).join('\n') ?? ''
|
|
23
|
+
if (text) console.log(`ASSISTANT[${event.seq}]: ${text.slice(0, 350)}`)
|
|
24
|
+
} else if (event.type === 'tool/call') {
|
|
25
|
+
console.log(`TOOL[${event.seq}]: ${event.data.name} ${String(event.data.arguments).slice(0, 250)}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
process.exit(0)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// Diagnostic: read the session-search index the way a search would.
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
4
|
+
|
|
5
|
+
const db = new DatabaseSync(process.argv[2] ?? 'C:/Users/erikb/.dsh/storages/session-search.sqlite', { readOnly: true })
|
|
6
|
+
const tables = db.prepare("SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name").all()
|
|
7
|
+
console.log('objects:', tables.map((t) => `${t.type}:${t.name}`).join(', ') || '(none)')
|
|
8
|
+
|
|
9
|
+
for (const { name, type } of tables) {
|
|
10
|
+
if (type !== 'table' || name.startsWith('sqlite_')) continue
|
|
11
|
+
try {
|
|
12
|
+
const n = db.prepare(`SELECT COUNT(*) AS n FROM "${name}"`).get()
|
|
13
|
+
console.log(` ${name}: ${n.n} rows`)
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.log(` ${name}: unreadable (${String(err).slice(0, 80)})`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Show the schema so the columns a search would read are visible.
|
|
20
|
+
for (const { name, type } of tables) {
|
|
21
|
+
if (type !== 'table' || name.startsWith('sqlite_')) continue
|
|
22
|
+
const sql = db.prepare('SELECT sql FROM sqlite_master WHERE name = ?').get(name)
|
|
23
|
+
console.log(`\n--- ${name} ---\n${sql.sql}`)
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// One-shot: mark a legacy header-only candidate orphaned so memory_recover can rebuild it.
|
|
3
|
+
// A `pending` entry with a header-only digest (no summary) cannot be consolidated, and capture
|
|
4
|
+
// will never re-summarize a historical session on its own. Recovery is the path that re-reads
|
|
5
|
+
// the session log and re-captures it.
|
|
6
|
+
import { readQueue, transition, STATES, REASONS } from '../src/queue.js'
|
|
7
|
+
|
|
8
|
+
const ROOT = process.argv[2] ?? 'E:/dsh-memory'
|
|
9
|
+
const id = process.argv[3]
|
|
10
|
+
if (!id) throw new Error('usage: orphan-for-recovery.ts <root> <id>')
|
|
11
|
+
|
|
12
|
+
const entry = readQueue(ROOT).get(id)
|
|
13
|
+
if (!entry) throw new Error(id + ' is not in the queue')
|
|
14
|
+
transition(ROOT, id, STATES.ORPHANED, { reason: REASONS.DIGEST_GONE })
|
|
15
|
+
console.log(`orphaned ${id} (was ${entry.state}); now run memory_recover({ id: '${id}' })`)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// Probe: does the session-search index actually index the REAL session logs?
|
|
3
|
+
//
|
|
4
|
+
// The live index at ~/.dsh/storages/session-search.sqlite holds 0 rows and has never been
|
|
5
|
+
// written since it was created. Because `openAt: first-search` means reconciliation runs on the
|
|
6
|
+
// first real search, "empty" could equally mean "never searched" or "searches index nothing".
|
|
7
|
+
// This assembles the real persistence + query services over a COPY of a real sessions root, so a
|
|
8
|
+
// search can be observed without touching the live index.
|
|
9
|
+
//
|
|
10
|
+
// Usage: node --import tsx/esm scripts/probe-search.ts <sessions-root-copy> <out.sqlite>
|
|
11
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import { SessionStore } from '@deepseek-ai/dsh-session'
|
|
13
|
+
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
|
14
|
+
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
|
|
15
|
+
|
|
16
|
+
const root = process.argv[2]
|
|
17
|
+
const out = process.argv[3]
|
|
18
|
+
if (!root || !out) throw new Error('usage: probe-search.ts <sessions-root> <out.sqlite>')
|
|
19
|
+
|
|
20
|
+
const ctx = new Context()
|
|
21
|
+
await ctx.plugin(SessionStore)
|
|
22
|
+
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'zstd' })
|
|
23
|
+
await ctx.plugin(SqliteSessionQueryEngine, { path: out, openAt: 'first-search' })
|
|
24
|
+
|
|
25
|
+
const persistence = ctx.get('sessionPersistence')
|
|
26
|
+
console.log('persistence service:', persistence ? 'present' : 'MISSING')
|
|
27
|
+
const listed = await persistence.list()
|
|
28
|
+
console.log('persistence.list():', listed.length, 'stored session(s)')
|
|
29
|
+
for (const s of listed.slice(0, 3)) console.log(' -', s.header.id, 'cwd=' + s.header.cwd)
|
|
30
|
+
|
|
31
|
+
const query = ctx.get('sessionQuery')
|
|
32
|
+
console.log('sessionQuery service:', query ? 'present' : 'MISSING')
|
|
33
|
+
|
|
34
|
+
// A word that certainly appears inside some session body but in no title.
|
|
35
|
+
const needle = process.argv[4] ?? 'the'
|
|
36
|
+
const page = await query.searchSessions({ query: needle, limit: 5 })
|
|
37
|
+
console.log(`\nsearchSessions(${JSON.stringify(needle)}) -> ${page.items.length} hit(s)`)
|
|
38
|
+
for (const hit of page.items.slice(0, 5)) {
|
|
39
|
+
console.log(' hit', hit.header.id, 'live=' + hit.live, 'persisted=' + hit.persisted)
|
|
40
|
+
console.log(' ', String(hit.bestMatch.snippet).replace(/\s+/g, ' ').slice(0, 140))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await ctx.stop?.()
|
|
44
|
+
process.exit(0)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// Diagnostic: which stored session logs can the harness NOT read?
|
|
3
|
+
//
|
|
4
|
+
// The SQLite search index reconciles by listing persistence and cold-reading each log. A single
|
|
5
|
+
// unreadable artifact aborts the whole observation (`session-search persistence observation
|
|
6
|
+
// failed`), so the index stays empty and every search returns an error instead of results. This
|
|
7
|
+
// walks the real sessions root and reports each artifact that fails to read, with its reason.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node --import tsx/esm scripts/scan-unreadable.ts [sessions-root]
|
|
10
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
11
|
+
import { SessionStore } from '@deepseek-ai/dsh-session'
|
|
12
|
+
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
|
13
|
+
import { readColdSessionLog } from '@deepseek-ai/dsh-session-query'
|
|
14
|
+
|
|
15
|
+
const root = process.argv[2] ?? 'C:/Users/erikb/.dsh/sessions'
|
|
16
|
+
|
|
17
|
+
const ctx = new Context()
|
|
18
|
+
await ctx.plugin(SessionStore)
|
|
19
|
+
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'zstd' })
|
|
20
|
+
const persistence = ctx.get('sessionPersistence')
|
|
21
|
+
|
|
22
|
+
const listed = await persistence.list()
|
|
23
|
+
console.log(`stored sessions: ${listed.length}\n`)
|
|
24
|
+
|
|
25
|
+
let ok = 0
|
|
26
|
+
const failures = []
|
|
27
|
+
for (const snapshot of listed) {
|
|
28
|
+
try {
|
|
29
|
+
await readColdSessionLog(persistence, snapshot.header.id, undefined)
|
|
30
|
+
ok += 1
|
|
31
|
+
} catch (error) {
|
|
32
|
+
failures.push({ id: snapshot.header.id, cwd: snapshot.header.cwd, error: String((error && error.message) || error) })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`readable: ${ok}`)
|
|
37
|
+
console.log(`UNREADABLE: ${failures.length}\n`)
|
|
38
|
+
for (const f of failures) {
|
|
39
|
+
console.log(`- ${f.id} cwd=${f.cwd}`)
|
|
40
|
+
console.log(` ${f.error.slice(0, 220)}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Group the failure reasons so the shape of the problem is visible at a glance.
|
|
44
|
+
console.log('\n--- reasons, grouped ---')
|
|
45
|
+
const groups = new Map()
|
|
46
|
+
for (const f of failures) {
|
|
47
|
+
const key = f.error.replace(/\(raw log:.*\)/, '').replace(/session-[0-9a-f-]+|[0-9a-f]{8}-[0-9a-f-]{27,}/g, '<id>').trim().slice(0, 120)
|
|
48
|
+
groups.set(key, (groups.get(key) ?? 0) + 1)
|
|
49
|
+
}
|
|
50
|
+
for (const [reason, count] of [...groups].sort((a, b) => b[1] - a[1])) console.log(`${count} ${reason}`)
|
|
51
|
+
|
|
52
|
+
process.exit(0)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// One-shot: settle subagent candidates stuck in `pending` with header-only digests.
|
|
3
|
+
// These sessions are `origin: subagent` / `delegationDepth > 0`, so by the plugin's own rule
|
|
4
|
+
// they are never memory material; their parent transcript holds the story. They reached `pending`
|
|
5
|
+
// through the legacy migration and now have no summary to consolidate, so they would otherwise
|
|
6
|
+
// sit in the backlog forever. This marks them skipped with a truthful reason.
|
|
7
|
+
import { readQueue, transition, STATES, REASONS } from '../src/queue.js'
|
|
8
|
+
|
|
9
|
+
const ROOT = process.argv[2] ?? 'E:/dsh-memory'
|
|
10
|
+
const ids = process.argv.slice(3)
|
|
11
|
+
if (ids.length === 0) throw new Error('usage: skip-subagent-held.ts <root> <id...>')
|
|
12
|
+
|
|
13
|
+
// The subagent determination is NOT made here: the caller lists ids it has already verified
|
|
14
|
+
// (by reading each session's header) as subagent sessions. This script only performs the queue
|
|
15
|
+
// transition with a truthful reason, so it cannot skip a candidate on a guess.
|
|
16
|
+
let skipped = 0
|
|
17
|
+
for (const id of ids) {
|
|
18
|
+
const entry = readQueue(ROOT).get(id)
|
|
19
|
+
if (!entry) { console.log(`not in queue: ${id}`); continue }
|
|
20
|
+
transition(ROOT, id, STATES.SKIPPED, { reason: REASONS.TRIVIAL })
|
|
21
|
+
console.log(`skipped subagent: ${id}`)
|
|
22
|
+
skipped += 1
|
|
23
|
+
}
|
|
24
|
+
console.log(`\nskipped ${skipped} subagent candidate(s)`)
|
package/src/consolidate-run.js
CHANGED
|
@@ -10,18 +10,19 @@ import { readAssociations } from './workspace.js'
|
|
|
10
10
|
export async function runConsolidation(store, {
|
|
11
11
|
llm, digests, indexHeads, guidance, batchSize = 3, maxPromptBytes = 65536, maxDigestBytes = 20000, maxAttempts = 3,
|
|
12
12
|
} = {}) {
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// A header-only digest carries NOTHING to consolidate: capture has not summarized it yet.
|
|
14
|
+
// Sending it to the model spends a call on a header with no body, and the reply can only be
|
|
15
|
+
// "nothing durable" — which never advances the entry, so the same empty shells get re-sent
|
|
16
|
+
// forever (one real entry accumulated 39 attempts). Consolidation works on summaries; capture
|
|
17
|
+
// is what fills summaries. Header-only candidates are therefore left untouched here.
|
|
15
18
|
const all = digests ?? pendingDigests(store)
|
|
16
19
|
const withContent = []
|
|
17
|
-
const headerOnly = []
|
|
18
20
|
for (const item of all) {
|
|
19
21
|
const raw = readDigestText(store.root, item.id)
|
|
20
22
|
if (raw.includes('summary (')) withContent.push(item)
|
|
21
|
-
else headerOnly.push({ ...item, empty: true })
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if (withContent.length === 0) return { consolidated: 0, written: 0 }
|
|
25
|
+
const pending = withContent.slice(0, batchSize)
|
|
25
26
|
// Bound the prompt: a digest is clipped to its own ceiling, and the whole payload
|
|
26
27
|
// to the prompt budget. Each digest carries its header plus the most recent bullets.
|
|
27
28
|
// Hand the model the RIGHT workspace key instead of letting it invent one from a path.
|
|
@@ -41,9 +42,9 @@ export async function runConsolidation(store, {
|
|
|
41
42
|
const full = readDigestText(store.root, item.id)
|
|
42
43
|
const text = clipBlock(full, Math.min(maxDigestBytes, budget))
|
|
43
44
|
if (text === '') continue
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
45
|
+
// Every item in `pending` now carries a summary section (see the filter above), so `empty`
|
|
46
|
+
// is never true here. Kept explicit so a future change to the filter cannot silently revive
|
|
47
|
+
// the empty-digest branch that used to send a bare header to the model.
|
|
47
48
|
const empty = !full.includes('summary (')
|
|
48
49
|
// A backfilled digest carries material reconstructed from an old session rather than a live
|
|
49
50
|
// summary. If ANY digest in the batch came from a backfill, the pass takes the stricter bar,
|