@try-works/dsh-memory 0.1.19 → 0.1.21
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/LICENSE +21 -21
- package/README.md +585 -585
- package/STATUS.md +294 -287
- package/cordis.patch.yml +69 -69
- package/package.json +1 -1
- package/scripts/check-package.mjs +76 -76
- package/scripts/inspect-log.ts +40 -0
- package/scripts/repair-ignorable.ts +130 -0
- package/scripts/scan-sessions.ts +43 -0
- package/scripts/verify-log.ts +26 -0
- package/src/SYSTEM.template.md +311 -311
- package/src/capture.js +242 -242
- package/src/client/index.js +886 -886
- package/src/config.js +57 -57
- package/src/consolidate-run.js +132 -132
- package/src/consolidate.js +330 -330
- package/src/context.js +88 -88
- package/src/decisions.js +106 -106
- package/src/defaults.js +93 -93
- package/src/digest.js +255 -255
- package/src/entry.js +179 -179
- package/src/eval.js +72 -72
- package/src/files.js +60 -60
- package/src/fixture.js +56 -56
- package/src/forget.js +71 -71
- package/src/freshness.js +65 -65
- package/src/fts.js +166 -166
- package/src/git.js +53 -53
- package/src/history.js +226 -226
- package/src/host.js +884 -866
- package/src/index.js +115 -94
- package/src/inject.js +108 -108
- package/src/layers.js +178 -178
- package/src/ledger.js +19 -19
- package/src/map.js +184 -184
- package/src/models.js +136 -136
- package/src/queue.js +602 -602
- package/src/rank.js +31 -31
- package/src/recover.js +91 -91
- package/src/refresh.js +80 -80
- package/src/remember.js +58 -58
- package/src/rerank.js +90 -90
- package/src/search.js +75 -75
- package/src/select.js +57 -57
- package/src/session.js +60 -60
- package/src/state.js +240 -240
- package/src/store.js +128 -128
- package/src/summarize.js +195 -195
- package/src/system-doc.js +171 -171
- package/src/tasks.js +617 -617
- package/src/telemetry.js +122 -122
- package/src/tools.js +828 -818
- package/src/working.js +288 -288
- package/src/workspace-key.js +68 -68
- package/src/workspace.js +87 -87
package/cordis.patch.yml
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
# dsh-memory bundle patch — the patch layer this package exports.
|
|
2
|
-
#
|
|
3
|
-
# Declared by `dsh.bundle.patch` in package.json. When the package is added to a profile, dsh
|
|
4
|
-
# reconciles it into `dsh.profile.bundles` and composes this layer over the profile, so the
|
|
5
|
-
# plugin mounts from the installed package rather than by absolute path.
|
|
6
|
-
#
|
|
7
|
-
# This layer does two things: it mounts dsh-memory, and it switches on the session history
|
|
8
|
-
# search that a memory plugin is useless without. Both are described where they appear.
|
|
9
|
-
#
|
|
10
|
-
# Patch semantics, which the rest of this file depends on: a patch REPLACES the targeted row's
|
|
11
|
-
# whole `config`, it does not merge into it. Every override below therefore restates EVERY key
|
|
12
|
-
# it owns, or the omitted ones silently become undefined.
|
|
13
|
-
- insert:
|
|
14
|
-
# dsh-memory itself.
|
|
15
|
-
#
|
|
16
|
-
# A single row naming the published package mounts the plugin on the profile root. The row id
|
|
17
|
-
# stays dsh-memory, which is what the PROFILE patch addresses for config. It publishes the `memory`
|
|
18
|
-
# service, registers the memory_* tools into the host-owned tools registry, and keeps its
|
|
19
|
-
# browser half module-scoped — so, like the other memory-like plugins, it needs no `isolate`
|
|
20
|
-
# realm.
|
|
21
|
-
#
|
|
22
|
-
# The row carries NO config: the store root, the memory model and the git origin are set on
|
|
23
|
-
# Settings -> Memory, which writes a top-level `config:` row for this id into the PROFILE
|
|
24
|
-
# patch. That is why the mounted row must be addressable by id — the config editor finds a
|
|
25
|
-
# row by id at the top level, so a profile that configures memory ends up with both this
|
|
26
|
-
# insert and a sibling `- id: dsh-memory` config row after it.
|
|
27
|
-
- id: dsh-memory
|
|
28
|
-
# A scoped name MUST be quoted: YAML treats a leading @ as a reserved indicator.
|
|
29
|
-
name: '@try-works/dsh-memory'
|
|
30
|
-
|
|
31
|
-
# NOTE: the model-facing session history TOOLS are deliberately NOT mounted here.
|
|
32
|
-
#
|
|
33
|
-
# Not because they are unwanted, but because this is the wrong plane. A Web profile
|
|
34
|
-
# disables every base tool row and hands the tool plane to PRESETS, and its gate reads
|
|
35
|
-
# declared row ids WITHOUT regard to `disabled` — the web-app bundle keeps `tool-ralph`
|
|
36
|
-
# declared-but-disabled purely to hold it off the host plane and out of collision with
|
|
37
|
-
# every preset's rows. Inserting a live tool row here would put it on the host plane and
|
|
38
|
-
# in collision with that arrangement, which is a large side effect for a memory plugin to
|
|
39
|
-
# have on somebody else's tool surface. Enabling the index below is a config override, the
|
|
40
|
-
# kind the web-app bundle explicitly invites; owning a tool row is not.
|
|
41
|
-
|
|
42
|
-
# ---------------------------------------------------------------------------------------------
|
|
43
|
-
# Session search: switched on by installing this plugin.
|
|
44
|
-
# ---------------------------------------------------------------------------------------------
|
|
45
|
-
#
|
|
46
|
-
# The base bundle mounts `session-query-sqlite` with `openAt: never`, and the web-app bundle
|
|
47
|
-
# restates that. It is deliberate: ctx.sessionQuery stays available for exact reads and lineage
|
|
48
|
-
# while SEARCH calls fail with SESSION_QUERY_SEARCH_DISABLED, so no deployment indexes gigabytes
|
|
49
|
-
# of session logs without asking. The cost of that default is a system that cannot look back at
|
|
50
|
-
# what happened — and memory that cannot be searched is half a memory. Installing a memory plugin
|
|
51
|
-
# is the request that flag was waiting for, so this layer asks. The web-app bundle says so in as
|
|
52
|
-
# many words: 'a deployment enabling content search overrides openAt to first-search in a later
|
|
53
|
-
# patch layer'. This is that layer.
|
|
54
|
-
#
|
|
55
|
-
# `first-search` rather than `startup`: nothing is indexed until a search is actually made, so
|
|
56
|
-
# the capability is present from install while the work still happens lazily, on a query.
|
|
57
|
-
#
|
|
58
|
-
# `path` is restated even though both bundles already set one, because a patch replaces the
|
|
59
|
-
# whole config rather than merging: omitting it would drop the index to undefined and rebuild
|
|
60
|
-
# it in memory on every process. A durable file under storages/ is what makes a several-gigabyte
|
|
61
|
-
# corpus worth indexing once.
|
|
62
|
-
#
|
|
63
|
-
# Precedence is why this is safe to set here. Bundle layers compose FIRST and the profile's own
|
|
64
|
-
# cordis.patch.yml applies AFTER, so a profile wanting `startup`, a different path, or `never`
|
|
65
|
-
# simply says so there and wins. This layer supplies a default, not a mandate.
|
|
66
|
-
- id: session-query-sqlite
|
|
67
|
-
config:
|
|
68
|
-
path: !!js dshHomePath('storages', 'session-search.sqlite')
|
|
69
|
-
openAt: first-search
|
|
1
|
+
# dsh-memory bundle patch — the patch layer this package exports.
|
|
2
|
+
#
|
|
3
|
+
# Declared by `dsh.bundle.patch` in package.json. When the package is added to a profile, dsh
|
|
4
|
+
# reconciles it into `dsh.profile.bundles` and composes this layer over the profile, so the
|
|
5
|
+
# plugin mounts from the installed package rather than by absolute path.
|
|
6
|
+
#
|
|
7
|
+
# This layer does two things: it mounts dsh-memory, and it switches on the session history
|
|
8
|
+
# search that a memory plugin is useless without. Both are described where they appear.
|
|
9
|
+
#
|
|
10
|
+
# Patch semantics, which the rest of this file depends on: a patch REPLACES the targeted row's
|
|
11
|
+
# whole `config`, it does not merge into it. Every override below therefore restates EVERY key
|
|
12
|
+
# it owns, or the omitted ones silently become undefined.
|
|
13
|
+
- insert:
|
|
14
|
+
# dsh-memory itself.
|
|
15
|
+
#
|
|
16
|
+
# A single row naming the published package mounts the plugin on the profile root. The row id
|
|
17
|
+
# stays dsh-memory, which is what the PROFILE patch addresses for config. It publishes the `memory`
|
|
18
|
+
# service, registers the memory_* tools into the host-owned tools registry, and keeps its
|
|
19
|
+
# browser half module-scoped — so, like the other memory-like plugins, it needs no `isolate`
|
|
20
|
+
# realm.
|
|
21
|
+
#
|
|
22
|
+
# The row carries NO config: the store root, the memory model and the git origin are set on
|
|
23
|
+
# Settings -> Memory, which writes a top-level `config:` row for this id into the PROFILE
|
|
24
|
+
# patch. That is why the mounted row must be addressable by id — the config editor finds a
|
|
25
|
+
# row by id at the top level, so a profile that configures memory ends up with both this
|
|
26
|
+
# insert and a sibling `- id: dsh-memory` config row after it.
|
|
27
|
+
- id: dsh-memory
|
|
28
|
+
# A scoped name MUST be quoted: YAML treats a leading @ as a reserved indicator.
|
|
29
|
+
name: '@try-works/dsh-memory'
|
|
30
|
+
|
|
31
|
+
# NOTE: the model-facing session history TOOLS are deliberately NOT mounted here.
|
|
32
|
+
#
|
|
33
|
+
# Not because they are unwanted, but because this is the wrong plane. A Web profile
|
|
34
|
+
# disables every base tool row and hands the tool plane to PRESETS, and its gate reads
|
|
35
|
+
# declared row ids WITHOUT regard to `disabled` — the web-app bundle keeps `tool-ralph`
|
|
36
|
+
# declared-but-disabled purely to hold it off the host plane and out of collision with
|
|
37
|
+
# every preset's rows. Inserting a live tool row here would put it on the host plane and
|
|
38
|
+
# in collision with that arrangement, which is a large side effect for a memory plugin to
|
|
39
|
+
# have on somebody else's tool surface. Enabling the index below is a config override, the
|
|
40
|
+
# kind the web-app bundle explicitly invites; owning a tool row is not.
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------------------------
|
|
43
|
+
# Session search: switched on by installing this plugin.
|
|
44
|
+
# ---------------------------------------------------------------------------------------------
|
|
45
|
+
#
|
|
46
|
+
# The base bundle mounts `session-query-sqlite` with `openAt: never`, and the web-app bundle
|
|
47
|
+
# restates that. It is deliberate: ctx.sessionQuery stays available for exact reads and lineage
|
|
48
|
+
# while SEARCH calls fail with SESSION_QUERY_SEARCH_DISABLED, so no deployment indexes gigabytes
|
|
49
|
+
# of session logs without asking. The cost of that default is a system that cannot look back at
|
|
50
|
+
# what happened — and memory that cannot be searched is half a memory. Installing a memory plugin
|
|
51
|
+
# is the request that flag was waiting for, so this layer asks. The web-app bundle says so in as
|
|
52
|
+
# many words: 'a deployment enabling content search overrides openAt to first-search in a later
|
|
53
|
+
# patch layer'. This is that layer.
|
|
54
|
+
#
|
|
55
|
+
# `first-search` rather than `startup`: nothing is indexed until a search is actually made, so
|
|
56
|
+
# the capability is present from install while the work still happens lazily, on a query.
|
|
57
|
+
#
|
|
58
|
+
# `path` is restated even though both bundles already set one, because a patch replaces the
|
|
59
|
+
# whole config rather than merging: omitting it would drop the index to undefined and rebuild
|
|
60
|
+
# it in memory on every process. A durable file under storages/ is what makes a several-gigabyte
|
|
61
|
+
# corpus worth indexing once.
|
|
62
|
+
#
|
|
63
|
+
# Precedence is why this is safe to set here. Bundle layers compose FIRST and the profile's own
|
|
64
|
+
# cordis.patch.yml applies AFTER, so a profile wanting `startup`, a different path, or `never`
|
|
65
|
+
# simply says so there and wins. This layer supplies a default, not a mandate.
|
|
66
|
+
- id: session-query-sqlite
|
|
67
|
+
config:
|
|
68
|
+
path: !!js dshHomePath('storages', 'session-search.sqlite')
|
|
69
|
+
openAt: first-search
|
package/package.json
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
// Package invariants for dsh-memory. Run with: node scripts/check-package.mjs
|
|
2
|
-
//
|
|
3
|
-
// A plugin that is installed by name must declare everything the loader needs; when one of these
|
|
4
|
-
// drifts the failure appears at MOUNT time in someone else's profile, which is the worst place to
|
|
5
|
-
// discover it. This is a fast pre-flight, and test/package.test.js asserts the same invariants.
|
|
6
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
7
|
-
import { dirname, join } from 'node:path'
|
|
8
|
-
import { fileURLToPath } from 'node:url'
|
|
9
|
-
|
|
10
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
-
const problems = []
|
|
12
|
-
const note = (ok, message) => { if (!ok) problems.push(message) }
|
|
13
|
-
|
|
14
|
-
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
15
|
-
|
|
16
|
-
note(manifest.name === '@try-works/dsh-memory', 'package name must be "@try-works/dsh-memory", found ' + manifest.name)
|
|
17
|
-
note(manifest.type === 'module', 'package must be ESM ("type": "module")')
|
|
18
|
-
note(typeof manifest.main === 'string', 'package must declare "main"')
|
|
19
|
-
note(existsSync(join(root, manifest.main)), 'main does not exist: ' + manifest.main)
|
|
20
|
-
|
|
21
|
-
// The loader must be able to import the entry.
|
|
22
|
-
const entry = await import(new URL('../' + manifest.main, import.meta.url))
|
|
23
|
-
note(typeof entry.name === 'string' && entry.name === 'dsh-memory', 'entry must export name = "dsh-memory"')
|
|
24
|
-
note(Array.isArray(entry.inject), 'entry must export inject')
|
|
25
|
-
note(typeof entry.apply === 'function', 'entry must export apply')
|
|
26
|
-
note(entry.Config !== undefined, 'entry must export Config')
|
|
27
|
-
note(entry.default === undefined, 'entry must NOT export default (the loader does exports.default ?? exports, which would discard name/inject/Config)')
|
|
28
|
-
|
|
29
|
-
// Bundle patch: what makes an install-by-name mount the plugin.
|
|
30
|
-
const patchPath = manifest.dsh?.bundle?.patch
|
|
31
|
-
note(typeof patchPath === 'string', 'dsh.bundle.patch must be declared for install-by-name')
|
|
32
|
-
if (typeof patchPath === 'string') {
|
|
33
|
-
const full = join(root, patchPath.replace(/^\.\//, ''))
|
|
34
|
-
note(existsSync(full), 'bundle patch does not exist: ' + patchPath)
|
|
35
|
-
if (existsSync(full)) {
|
|
36
|
-
const text = readFileSync(full, 'utf8')
|
|
37
|
-
note(text.includes('id: dsh-memory'), 'bundle patch must insert an entry with id: dsh-memory')
|
|
38
|
-
note(text.includes("name: '@try-works/dsh-memory'"), 'bundle patch must mount the package by its published name')
|
|
39
|
-
}
|
|
40
|
-
note((manifest.files ?? []).some((f) => f.includes('cordis.patch.yml')), 'files[] must ship the bundle patch')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Client half.
|
|
44
|
-
const client = manifest.dsh?.client
|
|
45
|
-
note(client !== undefined, 'dsh.client must be declared for the Settings page')
|
|
46
|
-
if (client) {
|
|
47
|
-
note(client.platform === 'web', 'dsh.client.platform must be "web"')
|
|
48
|
-
const clientExport = manifest.exports?.['./client']
|
|
49
|
-
const target = typeof clientExport === 'string' ? clientExport : clientExport?.default
|
|
50
|
-
note(typeof target === 'string', 'exports["./client"] must resolve to a file')
|
|
51
|
-
if (typeof target === 'string') note(existsSync(join(root, target)), 'client half missing: ' + target)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Everything the entry imports at load time must ship.
|
|
55
|
-
note((manifest.files ?? []).includes('src'), 'files[] must ship src/')
|
|
56
|
-
// The system document template is read at runtime from beside its module.
|
|
57
|
-
note(existsSync(join(root, 'src', 'SYSTEM.template.md')), 'src/SYSTEM.template.md must exist (rendered into every store)')
|
|
58
|
-
|
|
59
|
-
// Peers: a missing declaration means an unresolvable import in a clean profile.
|
|
60
|
-
for (const peer of ['@deepseek-ai/cordis', '@deepseek-ai/dsh-tools', '@deepseek-ai/dsh-llm']) {
|
|
61
|
-
note(Object.hasOwn(manifest.peerDependencies ?? {}, peer), 'peerDependencies must declare ' + peer)
|
|
62
|
-
}
|
|
63
|
-
// schemastery is the exception, and it is deliberate: .volatile() exists only from 3.18.4, and a
|
|
64
|
-
// PEER lets the profile supply 3.18.1, which throws at module load. As a dependency the version
|
|
65
|
-
// is ours to pin. Declaring it as a peer is what broke the first registry install.
|
|
66
|
-
note(!Object.hasOwn(manifest.peerDependencies ?? {}, '@deepseek-ai/schemastery'), 'schemastery must NOT be a peer dependency')
|
|
67
|
-
note(/^\^3\.18\.[4-9]/.test((manifest.dependencies ?? {})['@deepseek-ai/schemastery'] ?? ''), 'schemastery must be a dependency pinned at 3.18.4 or newer, for .volatile()')
|
|
68
|
-
{
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (problems.length > 0) {
|
|
72
|
-
console.error('dsh-memory package check FAILED:')
|
|
73
|
-
for (const problem of problems) console.error(' - ' + problem)
|
|
74
|
-
process.exit(1)
|
|
75
|
-
}
|
|
76
|
-
console.log('dsh-memory package check OK (' + manifest.name + '@' + manifest.version + ')')
|
|
1
|
+
// Package invariants for dsh-memory. Run with: node scripts/check-package.mjs
|
|
2
|
+
//
|
|
3
|
+
// A plugin that is installed by name must declare everything the loader needs; when one of these
|
|
4
|
+
// drifts the failure appears at MOUNT time in someone else's profile, which is the worst place to
|
|
5
|
+
// discover it. This is a fast pre-flight, and test/package.test.js asserts the same invariants.
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const problems = []
|
|
12
|
+
const note = (ok, message) => { if (!ok) problems.push(message) }
|
|
13
|
+
|
|
14
|
+
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
15
|
+
|
|
16
|
+
note(manifest.name === '@try-works/dsh-memory', 'package name must be "@try-works/dsh-memory", found ' + manifest.name)
|
|
17
|
+
note(manifest.type === 'module', 'package must be ESM ("type": "module")')
|
|
18
|
+
note(typeof manifest.main === 'string', 'package must declare "main"')
|
|
19
|
+
note(existsSync(join(root, manifest.main)), 'main does not exist: ' + manifest.main)
|
|
20
|
+
|
|
21
|
+
// The loader must be able to import the entry.
|
|
22
|
+
const entry = await import(new URL('../' + manifest.main, import.meta.url))
|
|
23
|
+
note(typeof entry.name === 'string' && entry.name === 'dsh-memory', 'entry must export name = "dsh-memory"')
|
|
24
|
+
note(Array.isArray(entry.inject), 'entry must export inject')
|
|
25
|
+
note(typeof entry.apply === 'function', 'entry must export apply')
|
|
26
|
+
note(entry.Config !== undefined, 'entry must export Config')
|
|
27
|
+
note(entry.default === undefined, 'entry must NOT export default (the loader does exports.default ?? exports, which would discard name/inject/Config)')
|
|
28
|
+
|
|
29
|
+
// Bundle patch: what makes an install-by-name mount the plugin.
|
|
30
|
+
const patchPath = manifest.dsh?.bundle?.patch
|
|
31
|
+
note(typeof patchPath === 'string', 'dsh.bundle.patch must be declared for install-by-name')
|
|
32
|
+
if (typeof patchPath === 'string') {
|
|
33
|
+
const full = join(root, patchPath.replace(/^\.\//, ''))
|
|
34
|
+
note(existsSync(full), 'bundle patch does not exist: ' + patchPath)
|
|
35
|
+
if (existsSync(full)) {
|
|
36
|
+
const text = readFileSync(full, 'utf8')
|
|
37
|
+
note(text.includes('id: dsh-memory'), 'bundle patch must insert an entry with id: dsh-memory')
|
|
38
|
+
note(text.includes("name: '@try-works/dsh-memory'"), 'bundle patch must mount the package by its published name')
|
|
39
|
+
}
|
|
40
|
+
note((manifest.files ?? []).some((f) => f.includes('cordis.patch.yml')), 'files[] must ship the bundle patch')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Client half.
|
|
44
|
+
const client = manifest.dsh?.client
|
|
45
|
+
note(client !== undefined, 'dsh.client must be declared for the Settings page')
|
|
46
|
+
if (client) {
|
|
47
|
+
note(client.platform === 'web', 'dsh.client.platform must be "web"')
|
|
48
|
+
const clientExport = manifest.exports?.['./client']
|
|
49
|
+
const target = typeof clientExport === 'string' ? clientExport : clientExport?.default
|
|
50
|
+
note(typeof target === 'string', 'exports["./client"] must resolve to a file')
|
|
51
|
+
if (typeof target === 'string') note(existsSync(join(root, target)), 'client half missing: ' + target)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Everything the entry imports at load time must ship.
|
|
55
|
+
note((manifest.files ?? []).includes('src'), 'files[] must ship src/')
|
|
56
|
+
// The system document template is read at runtime from beside its module.
|
|
57
|
+
note(existsSync(join(root, 'src', 'SYSTEM.template.md')), 'src/SYSTEM.template.md must exist (rendered into every store)')
|
|
58
|
+
|
|
59
|
+
// Peers: a missing declaration means an unresolvable import in a clean profile.
|
|
60
|
+
for (const peer of ['@deepseek-ai/cordis', '@deepseek-ai/dsh-tools', '@deepseek-ai/dsh-llm']) {
|
|
61
|
+
note(Object.hasOwn(manifest.peerDependencies ?? {}, peer), 'peerDependencies must declare ' + peer)
|
|
62
|
+
}
|
|
63
|
+
// schemastery is the exception, and it is deliberate: .volatile() exists only from 3.18.4, and a
|
|
64
|
+
// PEER lets the profile supply 3.18.1, which throws at module load. As a dependency the version
|
|
65
|
+
// is ours to pin. Declaring it as a peer is what broke the first registry install.
|
|
66
|
+
note(!Object.hasOwn(manifest.peerDependencies ?? {}, '@deepseek-ai/schemastery'), 'schemastery must NOT be a peer dependency')
|
|
67
|
+
note(/^\^3\.18\.[4-9]/.test((manifest.dependencies ?? {})['@deepseek-ai/schemastery'] ?? ''), 'schemastery must be a dependency pinned at 3.18.4 or newer, for .volatile()')
|
|
68
|
+
{
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (problems.length > 0) {
|
|
72
|
+
console.error('dsh-memory package check FAILED:')
|
|
73
|
+
for (const problem of problems) console.error(' - ' + problem)
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
console.log('dsh-memory package check OK (' + manifest.name + '@' + manifest.version + ')')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// One-shot diagnostic: decompress a session.v4.jsonl.zstd artifact with the
|
|
3
|
+
// harness's own frame primitives and report memory/map rows and their envelope.
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { scanZstdFrames, decompressZstdFrame } from 'file:///D:/deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts'
|
|
6
|
+
|
|
7
|
+
const file = process.argv[2]
|
|
8
|
+
if (!file) throw new Error('usage: inspect-log.ts <session.v4.jsonl.zstd>')
|
|
9
|
+
|
|
10
|
+
const buf = readFileSync(file)
|
|
11
|
+
const scan = scanZstdFrames(buf)
|
|
12
|
+
const lines: string[] = []
|
|
13
|
+
for (const frame of scan.frames) {
|
|
14
|
+
const plain = await decompressZstdFrame(buf.subarray(frame.start, frame.end))
|
|
15
|
+
lines.push(...plain.toString('utf8').split('\n').filter((l) => l.trim() !== ''))
|
|
16
|
+
}
|
|
17
|
+
console.log(`frames=${scan.frames.length} torn=${scan.tornStart ?? '-'} rows=${lines.length}`)
|
|
18
|
+
if (scan.tornStart !== undefined) console.log(`tornStart=${scan.tornStart}`)
|
|
19
|
+
|
|
20
|
+
const header = lines[0]
|
|
21
|
+
console.log(`header: ${header?.slice(0, 300) ?? '(none)'}`)
|
|
22
|
+
|
|
23
|
+
const hits = lines.filter((l) => l.startsWith('{"type":"memory/map"'))
|
|
24
|
+
console.log(`\nmemory/map EVENT rows: ${hits.length}`)
|
|
25
|
+
for (const h of hits) {
|
|
26
|
+
let env: { seq?: number; time?: number; ignorable?: unknown } = {}
|
|
27
|
+
try { env = JSON.parse(h) } catch { /* not json */ }
|
|
28
|
+
console.log(`seq=${env.seq} time=${env.time} ignorable=${JSON.stringify(env.ignorable)} keys=${Object.keys(env).join(',')}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const unmarked = hits.filter((l) => !l.includes('"ignorable":true'))
|
|
32
|
+
console.log(`\nUNMARKED memory/map events: ${unmarked.length}`)
|
|
33
|
+
for (const h of unmarked.slice(0, 10)) console.log(h.slice(0, 300))
|
|
34
|
+
|
|
35
|
+
console.log('\nlast 4 rows:')
|
|
36
|
+
for (const l of lines.slice(-4)) {
|
|
37
|
+
let env: { type?: string; seq?: number; time?: number } = {}
|
|
38
|
+
try { env = JSON.parse(l) } catch { /* not json */ }
|
|
39
|
+
console.log(`type=${env.type} seq=${env.seq} time=${env.time}`)
|
|
40
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// One-shot repair for the dsh-memory "unknown event type" incident.
|
|
3
|
+
//
|
|
4
|
+
// Plugin <=0.1.19 appended `{"type":"memory/map",...}` events WITHOUT the
|
|
5
|
+
// `ignorable: true` envelope marker. The harness's fail-closed storage contract
|
|
6
|
+
// (session-persistence/src/storage-contract.ts) refuses such logs on every
|
|
7
|
+
// load: "unknown to this harness and not marked ignorable". This rewrites each
|
|
8
|
+
// poisoned log with the marker inserted into the memory/map rows, preserving
|
|
9
|
+
// every other byte, the header-only first frame, frame checksums and seq
|
|
10
|
+
// contiguity, then verifies the result through the harness's own validator.
|
|
11
|
+
//
|
|
12
|
+
// Idempotent: files with zero unmarked memory/map events are left untouched.
|
|
13
|
+
// Usage: node --import tsx/esm scripts/repair-ignorable.ts [session-dir ...]
|
|
14
|
+
// (no args = scan the default sessions root and repair every broken log)
|
|
15
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { join, basename } from 'node:path'
|
|
17
|
+
import { scanZstdFrames, decompressZstdFrame, decompressZstdPrefix, compressZstdFrame } from 'file:///D:/deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts'
|
|
18
|
+
import { validateStoredEvents } from 'file:///D:/deepseek-harness/packages/session/session-persistence/src/storage-contract.ts'
|
|
19
|
+
|
|
20
|
+
const ROOT = 'C:/Users/erikb/.dsh/sessions'
|
|
21
|
+
const CUT_MS = Date.parse('2026-10-09T20:53:00Z')
|
|
22
|
+
|
|
23
|
+
/** Insert the marker into one memory/map row, preserving every other byte of the line. */
|
|
24
|
+
function markLine(line) {
|
|
25
|
+
if (!line.startsWith('{"type":"memory/map"') || line.includes('"ignorable":true')) return { line, marked: false }
|
|
26
|
+
const edited = line.replace(/^(\{"type":"memory\/map","seq":\d+,"time":\d+,)/, '$1"ignorable":true,')
|
|
27
|
+
if (edited === line) throw new Error(`memory/map row did not match the envelope pattern: ${line.slice(0, 80)}`)
|
|
28
|
+
return { line: edited, marked: true }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function markText(text) {
|
|
32
|
+
let marked = 0
|
|
33
|
+
const out = text.split('\n').map((line) => {
|
|
34
|
+
const r = markLine(line)
|
|
35
|
+
if (r.marked) marked += 1
|
|
36
|
+
return r.line
|
|
37
|
+
}).join('\n')
|
|
38
|
+
return { text: out, marked }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Repair one file in place; returns a report or throws. */
|
|
42
|
+
async function repairFile(file) {
|
|
43
|
+
const buf = readFileSync(file)
|
|
44
|
+
const scan = scanZstdFrames(buf)
|
|
45
|
+
const frames = []
|
|
46
|
+
for (const frame of scan.frames) {
|
|
47
|
+
const plain = await decompressZstdFrame(buf.subarray(frame.start, frame.end))
|
|
48
|
+
frames.push(plain.toString('utf8'))
|
|
49
|
+
}
|
|
50
|
+
if (scan.tornStart !== undefined) {
|
|
51
|
+
const prefix = await decompressZstdPrefix(buf.subarray(scan.tornStart))
|
|
52
|
+
frames.push(prefix.toString('utf8'))
|
|
53
|
+
}
|
|
54
|
+
let marked = 0
|
|
55
|
+
const edited = []
|
|
56
|
+
for (const text of frames) {
|
|
57
|
+
const r = markText(text)
|
|
58
|
+
marked += r.marked
|
|
59
|
+
edited.push(r.text)
|
|
60
|
+
}
|
|
61
|
+
if (marked === 0) return { file, repaired: false, reason: 'no unmarked memory/map events' }
|
|
62
|
+
|
|
63
|
+
// Compress each frame back as its own checksummed frame: frame 1 stays the
|
|
64
|
+
// header-only frame, batch boundaries are preserved, and a recovered torn
|
|
65
|
+
// prefix becomes a complete final frame.
|
|
66
|
+
const parts = []
|
|
67
|
+
for (const text of edited) parts.push(await compressZstdFrame(Buffer.from(text, 'utf8')))
|
|
68
|
+
const out = Buffer.concat(parts)
|
|
69
|
+
|
|
70
|
+
const backup = `${file}.bak-pre-ignorable`
|
|
71
|
+
if (!existsSync(backup)) copyFileSync(file, backup)
|
|
72
|
+
const tmp = `${file}.repair.tmp`
|
|
73
|
+
writeFileSync(tmp, out)
|
|
74
|
+
renameSync(tmp, file)
|
|
75
|
+
return { file, repaired: true, marked }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Verify a file the way the gateway will read it: frames decode, frame 1 is
|
|
79
|
+
* header-only, every row parses, seq is contiguous, and the storage contract
|
|
80
|
+
* (the exact validator that refused these logs) accepts every event. */
|
|
81
|
+
async function verifyFile(file) {
|
|
82
|
+
const buf = readFileSync(file)
|
|
83
|
+
const scan = scanZstdFrames(buf)
|
|
84
|
+
if (scan.frames.length === 0) throw new Error('no frames')
|
|
85
|
+
const header = (await decompressZstdFrame(buf.subarray(scan.frames[0].start, scan.frames[0].end))).toString('utf8')
|
|
86
|
+
if (header.indexOf('\n') !== header.length - 1) throw new Error('first frame is not exactly one header line')
|
|
87
|
+
const lines = [header.trimEnd()]
|
|
88
|
+
for (const frame of scan.frames.slice(1)) {
|
|
89
|
+
lines.push(...(await decompressZstdFrame(buf.subarray(frame.start, frame.end))).toString('utf8').split('\n').filter((l) => l.trim() !== ''))
|
|
90
|
+
}
|
|
91
|
+
if (scan.tornStart !== undefined) {
|
|
92
|
+
lines.push(...(await decompressZstdPrefix(buf.subarray(scan.tornStart))).toString('utf8').split('\n').filter((l) => l.trim() !== ''))
|
|
93
|
+
}
|
|
94
|
+
const rows = lines.map((l) => JSON.parse(l))
|
|
95
|
+
const meta = rows[0]
|
|
96
|
+
const events = rows.slice(1)
|
|
97
|
+
// Relative contiguity: a fork inherits a prefix, so the log may start at a nonzero seq.
|
|
98
|
+
for (let i = 1; i < events.length; i += 1) {
|
|
99
|
+
if (events[i].seq !== events[0].seq + i) throw new Error(`seq gap at index ${i}: ${events[i].seq}`)
|
|
100
|
+
}
|
|
101
|
+
const unmarked = events.filter((e) => e.type === 'memory/map' && e.ignorable !== true)
|
|
102
|
+
if (unmarked.length > 0) throw new Error(`${unmarked.length} memory/map events still unmarked`)
|
|
103
|
+
validateStoredEvents(meta, events, { kind: 'jsonl', path: file })
|
|
104
|
+
return { rows: rows.length, events: events.length, memoryMaps: events.filter((e) => e.type === 'memory/map').length }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function collectFiles() {
|
|
108
|
+
const dirs = process.argv.slice(2).filter((a) => !a.startsWith('-'))
|
|
109
|
+
if (dirs.length > 0) return dirs.map((d) => join(d, 'session.v4.jsonl.zstd'))
|
|
110
|
+
const out = []
|
|
111
|
+
const walk = (dir) => {
|
|
112
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
113
|
+
const p = join(dir, e.name)
|
|
114
|
+
if (e.isDirectory()) walk(p)
|
|
115
|
+
else if (e.name.endsWith('.zstd') && !e.name.includes('.bak') && statSync(p).mtimeMs >= CUT_MS) out.push(p)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
walk(ROOT)
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let repaired = 0
|
|
123
|
+
for (const file of collectFiles()) {
|
|
124
|
+
const r = await repairFile(file)
|
|
125
|
+
if (!r.repaired) continue
|
|
126
|
+
repaired += 1
|
|
127
|
+
const v = await verifyFile(file)
|
|
128
|
+
console.log(`repaired ${basename(file)}: marked ${r.marked} events; verified ${v.events} events, ${v.memoryMaps} memory/map rows, storage contract OK`)
|
|
129
|
+
}
|
|
130
|
+
console.log(`\nrepaired ${repaired} file(s); backups kept as <file>.bak-pre-ignorable`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// One-shot diagnostic: scan every session log modified since the first
|
|
3
|
+
// memory/map publish and report unmarked memory/map events per file.
|
|
4
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
5
|
+
import { join, basename } from 'node:path'
|
|
6
|
+
import { scanZstdFrames, decompressZstdFrame, decompressZstdPrefix } from 'file:///D:/deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts'
|
|
7
|
+
|
|
8
|
+
const root = process.argv[2] ?? 'C:/Users/erikb/.dsh/sessions'
|
|
9
|
+
const cut = Date.parse(process.argv[3] ?? '2026-10-09T20:53:00Z')
|
|
10
|
+
|
|
11
|
+
function walk(dir: string): string[] {
|
|
12
|
+
const out: string[] = []
|
|
13
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const p = join(dir, e.name)
|
|
15
|
+
if (e.isDirectory()) out.push(...walk(p))
|
|
16
|
+
else if (e.name.endsWith('.zstd') && !e.name.includes('.bak') && statSync(p).mtimeMs >= cut) out.push(p)
|
|
17
|
+
}
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let scanned = 0
|
|
22
|
+
for (const file of walk(root)) {
|
|
23
|
+
scanned += 1
|
|
24
|
+
let text = ''
|
|
25
|
+
try {
|
|
26
|
+
const buf = readFileSync(file)
|
|
27
|
+
const scan = scanZstdFrames(buf)
|
|
28
|
+
for (const f of scan.frames) text += (await decompressZstdFrame(buf.subarray(f.start, f.end))).toString('utf8')
|
|
29
|
+
if (scan.tornStart !== undefined) text += (await decompressZstdPrefix(buf.subarray(scan.tornStart))).toString('utf8')
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.log(`${basename(file)}: DECOMPRESS ERROR ${String(err).slice(0, 120)}`)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
let events = 0
|
|
35
|
+
let unmarked = 0
|
|
36
|
+
for (const line of text.split('\n')) {
|
|
37
|
+
if (!line.startsWith('{"type":"memory/map"')) continue
|
|
38
|
+
events += 1
|
|
39
|
+
if (!line.includes('"ignorable":true')) unmarked += 1
|
|
40
|
+
}
|
|
41
|
+
if (events > 0) console.log(`${unmarked > 0 ? 'BROKEN ' : 'ok '} events=${events} unmarked=${unmarked} ${file}`)
|
|
42
|
+
}
|
|
43
|
+
console.log(`scanned ${scanned} files`)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// Read-only: run the harness's own storage-contract validator over a live log.
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { scanZstdFrames, decompressZstdFrame, decompressZstdPrefix } from 'file:///D:/deepseek-harness/packages/session/session-persistence-jsonl/src/zstd.ts'
|
|
5
|
+
import { validateStoredEvents } from 'file:///D:/deepseek-harness/packages/session/session-persistence/src/storage-contract.ts'
|
|
6
|
+
|
|
7
|
+
for (const file of process.argv.slice(2)) {
|
|
8
|
+
try {
|
|
9
|
+
const buf = readFileSync(file)
|
|
10
|
+
const scan = scanZstdFrames(buf)
|
|
11
|
+
const header = (await decompressZstdFrame(buf.subarray(scan.frames[0].start, scan.frames[0].end))).toString('utf8')
|
|
12
|
+
if (header.indexOf('\n') !== header.length - 1) throw new Error('first frame is not exactly one header line')
|
|
13
|
+
const lines = [header.trimEnd()]
|
|
14
|
+
for (const frame of scan.frames.slice(1)) {
|
|
15
|
+
lines.push(...(await decompressZstdFrame(buf.subarray(frame.start, frame.end))).toString('utf8').split('\n').filter((l) => l.trim() !== ''))
|
|
16
|
+
}
|
|
17
|
+
if (scan.tornStart !== undefined) {
|
|
18
|
+
lines.push(...(await decompressZstdPrefix(buf.subarray(scan.tornStart))).toString('utf8').split('\n').filter((l) => l.trim() !== ''))
|
|
19
|
+
}
|
|
20
|
+
const rows = lines.map((l) => JSON.parse(l))
|
|
21
|
+
validateStoredEvents(rows[0], rows.slice(1), { kind: 'jsonl', path: file })
|
|
22
|
+
console.log(`OK ${rows.length} rows, ${rows.slice(1).filter((e) => e.type === 'memory/map').length} memory/map: ${file}`)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.log(`FAIL ${file}: ${String(err).slice(0, 300)}`)
|
|
25
|
+
}
|
|
26
|
+
}
|