dsh-loci 2.1.0 → 2.2.0
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/CHANGELOG.md +32 -0
- package/client.js +1 -0
- package/docs/COMPATIBILITY.md +31 -0
- package/package.json +10 -1
- package/scripts/cli.mjs +6 -1
- package/src/host/doctor.js +14 -1
- package/src/host/host-version.js +89 -0
- package/src/host/index.js +17 -1
- package/src/host/store/schema.js +2 -0
- package/src/host/store/store.js +5 -0
- package/src/mcp/store-facade.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) from 1.0.0.
|
|
6
6
|
|
|
7
|
+
## [2.2.0] — 2026-11-15
|
|
8
|
+
|
|
9
|
+
Four reviewers of an independent blind review named the same risk: `engines.dsh`
|
|
10
|
+
admits the whole `>=0.2.2-rc.1 <0.3.0-0` range, but the only version ever booted was
|
|
11
|
+
`0.2.2-rc.1`. A range is a promise nobody tested.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`scripts/compat-probe.mjs`** — reads the *installed* Host out of its `app.asar`
|
|
16
|
+
and asserts that every API this plugin touches still exists in the expected shape:
|
|
17
|
+
the section contract (`interpolate: false`, `order`), the `tools`, `commands` and
|
|
18
|
+
`connection` services with their registration calls, and the `turn/end` event.
|
|
19
|
+
Run against the installed **0.2.2-rc.3**: **5/5 passed**. It is not a boot test,
|
|
20
|
+
and the docs now say exactly which question it answers and which it does not.
|
|
21
|
+
|
|
22
|
+
- **The store records the Host that opened it.** `DSH_INSTALL_DIR` is read at
|
|
23
|
+
activation, the version lands in `meta.host_version`, `stats` exposes it, the panel
|
|
24
|
+
shows it, and `loci doctor` prints
|
|
25
|
+
`[ok] host: last opened by DeepSeek Harness 0.2.2-rc.3`. Every user install now
|
|
26
|
+
produces the evidence the next compatibility table needs, instead of leaving the
|
|
27
|
+
answer to a range in a manifest.
|
|
28
|
+
|
|
29
|
+
- CI gained a conditional probe step: it runs where a Host is installed and prints a
|
|
30
|
+
skip notice otherwise (the Host is not published to npm, so a stock runner has
|
|
31
|
+
nothing to read).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `dshReleases` gained a `0.2.2-rc.3` row with status `api-surface-verified` — a
|
|
36
|
+
deliberate third status between "verified" (booted) and "untested", so a range
|
|
37
|
+
entry can never again imply more than was actually checked.
|
|
38
|
+
|
|
7
39
|
## [2.1.0] — 2026-11-15
|
|
8
40
|
|
|
9
41
|
Two findings from an independent blind review, where seven reviewers compared this
|
package/client.js
CHANGED
|
@@ -1076,6 +1076,7 @@ window.__ModuleLoader__.load({
|
|
|
1076
1076
|
{ className: 'loci-kv' },
|
|
1077
1077
|
h('dt', null, 'path'), h('dd', null, data.memoryRoot ?? '—'),
|
|
1078
1078
|
h('dt', null, 'schema'), h('dd', null, `v${data.schemaVersion}`),
|
|
1079
|
+
h('dt', null, 'host'), h('dd', null, data.hostVersion ?? 'unknown'),
|
|
1079
1080
|
h('dt', null, 'vector'), h('dd', null, `${vs.mode ?? 'off'}${vs.healthy ? ' · healthy' : ''}${vs.lastError ? ` · ${vs.lastError}` : ''}`),
|
|
1080
1081
|
h('dt', null, 'last injection'),
|
|
1081
1082
|
h('dd', null, data.lastInjection ? `${data.lastInjection.bytes} B · ${data.lastInjection.listed} listed · ${data.lastInjection.hidden} hidden` : '—'),
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -8,8 +8,39 @@
|
|
|
8
8
|
| 0.2.2-rc.1 (host) | **24.22.3** | Ubuntu 24.04 (WSL2) | **verified** — manifest gate, 38/38 tests, coverage 82.93%, benchmark p95 5.28 ms at 2k | `scripts/run-linux-ci.sh` (fetches Node 24, runs the same commands as CI) |
|
|
9
9
|
| 0.2.2-rc.1 (host) | 22.23.0 | Ubuntu 24.04 (WSL2) | **verified** — same suite × 3 consecutive runs | recorded in `docs/ACCEPTANCE.md` §6 |
|
|
10
10
|
| 0.2.2-rc.1 | 24 | Ubuntu (GitHub Actions) | the identical step list runs in `.github/workflows/ci.yml`; the host is not published, so Actions has not run it | `.github/workflows/ci.yml` |
|
|
11
|
+
| **0.2.2-rc.3** | 24.21.0 | Windows 11 | **API surface verified** — the source-signature probe passes 5/5 against the installed `app.asar`; not a boot test | `node scripts/compat-probe.mjs --asar <install>/resources/app.asar` |
|
|
11
12
|
| other 0.2.x | — | — | not verified | — |
|
|
12
13
|
|
|
14
|
+
### What "API surface verified" does and does not mean
|
|
15
|
+
|
|
16
|
+
A range in `engines.dsh` is a promise nobody tested. `scripts/compat-probe.mjs`
|
|
17
|
+
reads the **installed** host package out of the shipped `app.asar` and asserts that
|
|
18
|
+
every Host API this plugin touches is still present in the shape it expects:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
host : 0.2.2-rc.3
|
|
22
|
+
[ok ] systemPrompt sections interpolate: false, order, section(
|
|
23
|
+
[ok ] tools service + register super(ctx, "tools"), register(
|
|
24
|
+
[ok ] session turn/end event "turn/end"
|
|
25
|
+
[ok ] commands service + register super(ctx, "commands"), register(
|
|
26
|
+
[ok ] connection service super(ctx, "connection")
|
|
27
|
+
5/5 checks passed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
It catches a renamed service, a changed section contract, or a moved event — the
|
|
31
|
+
failures that would otherwise show up as "the plugin silently stopped injecting" on
|
|
32
|
+
a version we never booted. It does **not** prove activation, tool registration at
|
|
33
|
+
runtime, or injection on a live session; those still require a real host, and rc.3
|
|
34
|
+
has not had one in this repository yet.
|
|
35
|
+
|
|
36
|
+
The plugin also records the host version it meets: `DSH_INSTALL_DIR` is read at
|
|
37
|
+
activation, the value lands in `meta.host_version`, and `loci doctor` prints
|
|
38
|
+
`[ok] host: last opened by DeepSeek Harness 0.2.2-rc.3`. That turns every user
|
|
39
|
+
install into evidence for the next compatibility table.
|
|
40
|
+
|
|
41
|
+
CI runs the probe **when a host is present** and prints a skip notice otherwise
|
|
42
|
+
(the host is not published to npm, so a stock runner has nothing to read).
|
|
43
|
+
|
|
13
44
|
`scripts/run-linux-ci.sh` exists so the Linux claim is reproducible on any machine
|
|
14
45
|
with WSL: it downloads the newest Node 24 tarball to `/tmp` (nothing is installed
|
|
15
46
|
system-wide), then runs `check-manifest`, the unit+integration suite, the coverage
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loci",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Layered long-term memory for DeepSeek Harness: the prompt sees only a stable index, full text is one lookup away. Bi-temporal facts, strict scope isolation, verified-only injection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,6 +64,15 @@
|
|
|
64
64
|
],
|
|
65
65
|
"status": "verified",
|
|
66
66
|
"evidence": "docs/COMPATIBILITY.md#022-rc1"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"dsh": "0.2.2-rc.3",
|
|
70
|
+
"node": "24.21.0",
|
|
71
|
+
"os": [
|
|
72
|
+
"win32"
|
|
73
|
+
],
|
|
74
|
+
"status": "api-surface-verified",
|
|
75
|
+
"evidence": "docs/COMPATIBILITY.md#what-api-surface-verified-does-and-does-not-mean"
|
|
67
76
|
}
|
|
68
77
|
],
|
|
69
78
|
"dependencies": {
|
package/scripts/cli.mjs
CHANGED
|
@@ -39,6 +39,9 @@ const PLUGIN_VERSION = JSON.parse(
|
|
|
39
39
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
40
40
|
).version;
|
|
41
41
|
|
|
42
|
+
/** The shipped manifest, read once: version for reports, engines for the host check. */
|
|
43
|
+
const MANIFEST = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
44
|
+
|
|
42
45
|
const rawArgv = process.argv.slice(2);
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -129,7 +132,9 @@ const diagnosticScope = () => {
|
|
|
129
132
|
|
|
130
133
|
switch (command) {
|
|
131
134
|
case 'doctor': {
|
|
132
|
-
const result = await withStore((store) =>
|
|
135
|
+
const result = await withStore((store) =>
|
|
136
|
+
runDoctor({ store, paths, config: effectiveConfig({ home }), engines: MANIFEST.engines }),
|
|
137
|
+
);
|
|
133
138
|
if (json) console.log(JSON.stringify(result, null, 2));
|
|
134
139
|
else console.log(formatDoctor(result));
|
|
135
140
|
process.exitCode = result.status === 'fail' ? 1 : 0;
|
package/src/host/doctor.js
CHANGED
|
@@ -21,7 +21,7 @@ function check(name, status, detail) {
|
|
|
21
21
|
* @returns `{status, checks, summary}` where status is the worst check status.
|
|
22
22
|
*/
|
|
23
23
|
export function runDoctor(options) {
|
|
24
|
-
const { store, paths, config } = options;
|
|
24
|
+
const { store, paths, config, engines } = options;
|
|
25
25
|
const checks = [];
|
|
26
26
|
|
|
27
27
|
const health = (() => {
|
|
@@ -63,6 +63,19 @@ export function runDoctor(options) {
|
|
|
63
63
|
const lastMaintenance = Number(health.migration?.to === undefined ? 0 : 0);
|
|
64
64
|
checks.push(check('rows', 'ok', `${health.counts.memory} revisions · ${health.counts.memory_head} entries · ${health.counts.entity} entities`));
|
|
65
65
|
|
|
66
|
+
// Which Harness opened this store. A range in engines.dsh is a promise; this row
|
|
67
|
+
// is what actually happened, and it is the first thing to ask for in a bug report.
|
|
68
|
+
try {
|
|
69
|
+
const hostVersion = store.hostVersion ?? null;
|
|
70
|
+
checks.push(
|
|
71
|
+
hostVersion === null
|
|
72
|
+
? check('host', 'warn', `not recorded yet; this build declares engines.dsh ${engines?.dsh ?? 'unknown'}`)
|
|
73
|
+
: check('host', 'ok', `last opened by DeepSeek Harness ${hostVersion}`),
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
checks.push(check('host', 'warn', `could not read: ${String(error?.message ?? error)}`));
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
try {
|
|
67
80
|
accessSync(paths.root, constants.W_OK);
|
|
68
81
|
checks.push(check('data directory', 'ok', paths.root));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Harness version are we actually running on?
|
|
3
|
+
*
|
|
4
|
+
* `engines.dsh` is a range; a range is a promise nobody tested. This module resolves
|
|
5
|
+
* the *installed* host package at activation time and records it, so the panel and
|
|
6
|
+
* `doctor` can say "running on 0.2.2-rc.3" instead of leaving the user to guess
|
|
7
|
+
* which point release the plugin met. It is best-effort by design: a plugin that
|
|
8
|
+
* refuses to boot because it cannot read someone else's package.json is worse than
|
|
9
|
+
* one that says "unknown".
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-loci/host-version
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const CANDIDATE_PATHS = [
|
|
18
|
+
// The Host normally resolves as a peer dependency of the profile.
|
|
19
|
+
'@deepseek-ai/dsh/package.json',
|
|
20
|
+
// Desktop bundles keep it one level down.
|
|
21
|
+
'dsh/package.json',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the installed host version.
|
|
26
|
+
*
|
|
27
|
+
* @param from - module URL to resolve relative to (defaults to this module).
|
|
28
|
+
* @returns `{version, path}` when found, `undefined` otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function detectHostVersion(from = import.meta.url) {
|
|
31
|
+
// The Host sets this to its own package directory, and the plugin runs inside the
|
|
32
|
+
// Host process, where reading an `app.asar/...` path works.
|
|
33
|
+
const installDir = process.env.DSH_INSTALL_DIR;
|
|
34
|
+
if (typeof installDir === 'string' && installDir.length > 0) {
|
|
35
|
+
try {
|
|
36
|
+
const manifest = JSON.parse(readFileSync(join(installDir, 'package.json'), 'utf8'));
|
|
37
|
+
if (typeof manifest?.version === 'string' && manifest.version.length > 0) {
|
|
38
|
+
return { version: manifest.version, path: join(installDir, 'package.json') };
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* fall through to resolution */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let require;
|
|
46
|
+
try {
|
|
47
|
+
require = createRequire(from);
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
for (const candidate of CANDIDATE_PATHS) {
|
|
52
|
+
try {
|
|
53
|
+
const resolved = require.resolve(candidate);
|
|
54
|
+
const manifest = JSON.parse(readFileSync(resolved, 'utf8'));
|
|
55
|
+
if (typeof manifest?.version === 'string' && manifest.version.length > 0) {
|
|
56
|
+
return { version: manifest.version, path: resolved };
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
/* try the next candidate */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Last resort: a sibling directory named `dsh` next to a cordis install.
|
|
63
|
+
try {
|
|
64
|
+
const resolved = require.resolve('@deepseek-ai/cordis/package.json');
|
|
65
|
+
const sibling = join(dirname(dirname(resolved)), 'dsh', 'package.json');
|
|
66
|
+
const manifest = JSON.parse(readFileSync(sibling, 'utf8'));
|
|
67
|
+
if (typeof manifest?.version === 'string') return { version: manifest.version, path: sibling };
|
|
68
|
+
} catch {
|
|
69
|
+
/* nothing else to try */
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compare a version against the `engines.dsh` range in one coarse way: does the
|
|
76
|
+
* major/minor line match what this build declares it supports?
|
|
77
|
+
*
|
|
78
|
+
* Deliberately not a semver implementation — the plugin has no dependency on one.
|
|
79
|
+
* It answers the only question that matters for a support message: "is this an
|
|
80
|
+
* untested point release inside our declared range, or a different line entirely?"
|
|
81
|
+
*
|
|
82
|
+
* @param version - the detected host version.
|
|
83
|
+
* @returns 'same-line' | 'other-line' | 'unknown'.
|
|
84
|
+
*/
|
|
85
|
+
export function hostLineOf(version) {
|
|
86
|
+
const match = /^(\d+)\.(\d+)\./.exec(String(version ?? ''));
|
|
87
|
+
if (match === null) return 'unknown';
|
|
88
|
+
return `${match[1]}.${match[2]}` === '0.2' ? 'same-line' : 'other-line';
|
|
89
|
+
}
|
package/src/host/index.js
CHANGED
|
@@ -18,11 +18,12 @@ import { registerTools } from './tools/tools.js';
|
|
|
18
18
|
import { registerApiRoutes } from './api/routes.js';
|
|
19
19
|
import { UpdateChecker } from './update.js';
|
|
20
20
|
import { Extractor, completeViaHost } from './extract/extractor.js';
|
|
21
|
+
import { detectHostVersion, hostLineOf } from './host-version.js';
|
|
21
22
|
import { registerCommands } from './commands.js';
|
|
22
23
|
import { runDoctor } from './doctor.js';
|
|
23
24
|
|
|
24
25
|
/** Version stamped into the database and diagnostics. */
|
|
25
|
-
export const VERSION = '2.
|
|
26
|
+
export const VERSION = '2.2.0';
|
|
26
27
|
|
|
27
28
|
/** Optional collaborators; without them the plugin still serves tools. */
|
|
28
29
|
export const inject = ['systemPrompt', 'tools'];
|
|
@@ -98,15 +99,30 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
98
99
|
log.info(`loci: using ${memoryRoot}`);
|
|
99
100
|
|
|
100
101
|
let capture;
|
|
102
|
+
// Record which Harness this store is actually running on. `engines.dsh` is a
|
|
103
|
+
// range; this field is the only one that says which point release met the file,
|
|
104
|
+
// so a support report can be checked instead of guessed at.
|
|
105
|
+
const detectedHost = detectHostVersion();
|
|
101
106
|
const store = new Store({
|
|
102
107
|
memoryRoot,
|
|
103
108
|
paths,
|
|
104
109
|
config,
|
|
105
110
|
log,
|
|
106
111
|
pluginVersion: VERSION,
|
|
112
|
+
hostVersion: detectedHost?.version,
|
|
107
113
|
live: read,
|
|
108
114
|
quoteVerifier: (quote, options) => capture?.verifyQuote(quote, options) ?? false,
|
|
109
115
|
});
|
|
116
|
+
if (detectedHost !== undefined) {
|
|
117
|
+
try {
|
|
118
|
+
store.db
|
|
119
|
+
.prepare("INSERT INTO meta (key, value) VALUES ('host_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
|
120
|
+
.run(detectedHost.version);
|
|
121
|
+
} catch {
|
|
122
|
+
/* recording is best effort */
|
|
123
|
+
}
|
|
124
|
+
log?.info?.(`loci: host ${detectedHost.version} (${hostLineOf(detectedHost.version)})`);
|
|
125
|
+
}
|
|
110
126
|
|
|
111
127
|
const updates = new UpdateChecker({
|
|
112
128
|
currentVersion: VERSION,
|
package/src/host/store/schema.js
CHANGED
|
@@ -160,6 +160,8 @@ export const META_KEYS = Object.freeze({
|
|
|
160
160
|
createdAt: 'created_at',
|
|
161
161
|
indexRev: 'index_rev',
|
|
162
162
|
lastMaintenance: 'last_maintenance',
|
|
163
|
+
/** The Harness version this store has been used with, recorded at activation. */
|
|
164
|
+
hostVersion: 'host_version',
|
|
163
165
|
});
|
|
164
166
|
|
|
165
167
|
/**
|
package/src/host/store/store.js
CHANGED
|
@@ -123,6 +123,8 @@ export class Store {
|
|
|
123
123
|
this.now = options.now ?? (() => Date.now());
|
|
124
124
|
this.quoteVerifier = options.quoteVerifier ?? (() => false);
|
|
125
125
|
this.pluginVersion = options.pluginVersion ?? '0.0.0';
|
|
126
|
+
/** Recorded host version, so support can see which point release met this store. */
|
|
127
|
+
this.hostVersion = options.hostVersion ?? undefined;
|
|
126
128
|
this.readOnly = options.readOnly === true;
|
|
127
129
|
this.db = openDatabase({ file: this.paths.db, readOnly: this.readOnly });
|
|
128
130
|
this.audit = this.readOnly
|
|
@@ -1059,6 +1061,9 @@ export class Store {
|
|
|
1059
1061
|
byStatus,
|
|
1060
1062
|
byKind,
|
|
1061
1063
|
byScope,
|
|
1064
|
+
// Which Harness point release this store has met. A range in engines.dsh is a
|
|
1065
|
+
// promise; this is the record of what actually opened the file.
|
|
1066
|
+
hostVersion: this.hostVersion ?? readMeta(this.db, META_KEYS.hostVersion) ?? null,
|
|
1062
1067
|
pendingCandidates: pending,
|
|
1063
1068
|
disputed,
|
|
1064
1069
|
lastInjection:
|