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 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` : '—'),
@@ -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.1.0",
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) => runDoctor({ store, paths, config: effectiveConfig({ home }) }));
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;
@@ -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.1.0';
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,
@@ -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
  /**
@@ -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:
@@ -36,7 +36,7 @@ export function openFacadeStore(options = {}) {
36
36
  memoryRoot: root,
37
37
  paths,
38
38
  config,
39
- pluginVersion: '2.1.0',
39
+ pluginVersion: '2.2.0',
40
40
  readOnly: options.readOnly === true,
41
41
  });
42
42
  return { store, paths };