dsh-engram 0.3.6 → 0.3.7

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/README.md CHANGED
@@ -238,6 +238,76 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
238
238
  -d "{\"payload\": $(cat engram-backup.json), \"mode\": \"merge\"}"
239
239
  ```
240
240
 
241
+ ## DSH compatibility
242
+
243
+ > **Short version.** Every **published** release (`0.2.0` … `0.3.6`) supports
244
+ > the **old** DSH API graph (`0.1.0-rc.7` / `0.1.1`, peers `^0.1.0-rc.7`)
245
+ > **only**. DSH `0.1.2-alpha.2` and newer are **not** compatible with those
246
+ > releases — the settings/client API family broke there. New-DSH support
247
+ > **starts with `0.3.7`**, the first release of the new API generation
248
+ > (described below).
249
+
250
+ | dsh-engram release | Status | DSH support | Declared via |
251
+ | --- | --- | --- | --- |
252
+ | `0.2.0` … `0.3.6` (npm, GitHub v0.3.3–v0.3.6) | published | **old DSH only** — `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2` | peers `@deepseek-ai/dsh-*@^0.1.0-rc.7` |
253
+ | `0.3.7` (npm, GitHub v0.3.7) | published | **new DSH only** — `>=0.1.2-alpha.2 <0.2.0-0` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.2.0-0` |
254
+
255
+ ### Where the API family broke — `0.1.2-alpha.2`
256
+
257
+ `0.1.2-alpha.2` is the first version of the new API family and the first that
258
+ **breaks** the released plugin versions:
259
+
260
+ - `@deepseek-ai/dsh-settings` **removed** `settingsNamespace()` and
261
+ `installSettingsSection()` — the exact imports every release ≤ `0.3.6` uses —
262
+ and replaced them with `settings.installSection`. The old host half fails to
263
+ load on `0.1.2-alpha.2+`.
264
+ - The web client half swapped its inject set: `@deepseek-ai/dsh-client-runtime`
265
+ no longer exists in the new family (replaced by `dsh-api-remotes` plus
266
+ `dsh-client-locale` / `-conversation` / `-renderer` / `-ui-settings-plugins`),
267
+ and the settings card used to talk through `ctx.remote.settings`.
268
+ - Supporting bumps at the same boundary: `@deepseek-ai/cordis` `^4.0.1 → ^4.0.2`,
269
+ `@deepseek-ai/schemastery` `^3.18.1 → ^3.18.2`, and `@deepseek-ai/dsh` stopped
270
+ shipping `config/agent-presets` in the package (moved to `dsh.configTrees`).
271
+
272
+ The web config card no longer depends on any of this: since `0.3.7` it reads
273
+ and writes the `dsh-engram` settings namespace through the plugin's **own**
274
+ HTTP route (`/api/dsh-engram/settings`, same-origin fetch like the memory
275
+ viewer), so the card is generation-agnostic on both sides of the
276
+ `0.1.2-alpha.2` boundary.
277
+
278
+ So: keep DSH on `0.1.1-rc.2` or older while running a release ≤ `0.3.6`, and
279
+ upgrade the plugin to `>=0.3.7` before moving DSH to `0.1.2-alpha.2`.
280
+
281
+ ### Current release — `0.3.7`
282
+
283
+ This release declares support for DSH `>=0.1.2-alpha.2 <0.2.0-0` and was fully
284
+ validated against the `dsh-v0.1.5-alpha.1` source and web runtime from the local
285
+ `deepseek-harness` checkout.
286
+
287
+ | Component | Supported range |
288
+ | --- | --- |
289
+ | DSH runtime / host / client package family | `>=0.1.2-alpha.2 <0.2.0-0` |
290
+ | `@deepseek-ai/cordis` | `^4.0.2` |
291
+ | `@deepseek-ai/schemastery` | `^3.18.2` |
292
+ | Node.js | `>=22.19.0` |
293
+
294
+ `0.1.2-alpha.2` is the minimum version for this API family: host settings use
295
+ `settings.installSection`, and the browser settings card reads and writes
296
+ through the plugin's own HTTP route (`/api/dsh-engram/settings`, see Route B
297
+ above). Host import and settings-registration smoke checks pass on
298
+ `0.1.2-alpha.2` and `0.1.3-alpha.2`; the full regression suite is validated
299
+ on `0.1.5-alpha.1` (real-machine GET/PUT/conflict/reset/persist-across-restart
300
+ all verified on `dsh 0.1.5-rc.1` with an isolated profile). The older
301
+ `0.1.0-rc.7` and `0.1.1` API graphs are outside this release's compatibility
302
+ range.
303
+
304
+ To install the DSH version used for validation:
305
+
306
+ ```sh
307
+ npm install --global @deepseek-ai/dsh@0.1.5-alpha.1
308
+ dsh --version
309
+ ```
310
+
241
311
  ## Install
242
312
 
243
313
  ```sh
package/README.zh.md CHANGED
@@ -149,6 +149,66 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
149
149
  -d "{\"payload\": $(cat engram-backup.json), \"mode\": \"merge\"}"
150
150
  ```
151
151
 
152
+ ## 兼容性
153
+
154
+ > **一句话结论**:所有**已真正发布**的版本(`0.2.0` … `0.3.6`)**只支持旧版
155
+ > DSH**(`0.1.0-rc.7` / `0.1.1` API 图,peers `^0.1.0-rc.7`)。DSH
156
+ > `0.1.2-alpha.2` 及更新版本与它们**不兼容**——settings/client API 族在该版本
157
+ > 断裂。新版 DSH 支持**自 `0.3.7` 起**提供——它是新 API 代际的第一个发布(见下文)。
158
+
159
+ | dsh-engram 发布版本 | 状态 | 支持的 DSH | 声明方式 |
160
+ | --- | --- | --- | --- |
161
+ | `0.2.0` … `0.3.6`(npm 全量 + GitHub v0.3.3–v0.3.6) | 已发布 | **仅旧版** —— `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2` | peers `@deepseek-ai/dsh-*@^0.1.0-rc.7` |
162
+ | `0.3.7`(npm + GitHub v0.3.7) | 已发布 | **仅新版** —— `>=0.1.2-alpha.2 <0.2.0-0` | `engines.dsh` + peers 同范围 |
163
+
164
+ ### API 族断裂点:`0.1.2-alpha.2`
165
+
166
+ `0.1.2-alpha.2` 是新 API 族的第一个版本,也是第一个**让已发布插件失效**的版本:
167
+
168
+ - `@deepseek-ai/dsh-settings` **移除了** `settingsNamespace()` 与
169
+ `installSettingsSection()`(≤ `0.3.6` 的每个发布都直接 import 这两个符号),
170
+ 改为 `settings.installSection`——旧宿主端在 `0.1.2-alpha.2+` 上直接加载失败。
171
+ - Web 客户端注入集整体更换:`@deepseek-ai/dsh-client-runtime` 在新代际**已不存在**
172
+ (被 `dsh-api-remotes` + `dsh-client-locale` / `-conversation` / `-renderer` /
173
+ `-ui-settings-plugins` 取代),设置卡此前改走 `ctx.remote.settings`。
174
+ - 同一边界的伴随升级:`@deepseek-ai/cordis` `^4.0.1 → ^4.0.2`、
175
+ `@deepseek-ai/schemastery` `^3.18.1 → ^3.18.2`、`@deepseek-ai/dsh` 不再随包
176
+ 发布 `config/agent-presets`(改为 `dsh.configTrees` 机制)。
177
+
178
+ **web 设置卡已不依赖上面任何一项**:自 `0.3.7` 起,配置卡通过插件**自有**
179
+ HTTP 路由(`/api/dsh-engram/settings`,与记忆 viewer 同样的同源 fetch)读写
180
+ `dsh-engram` settings namespace,因此在 `0.1.2-alpha.2` 分界线两侧都是代际无关的。
181
+
182
+ 所以:跑 ≤ `0.3.6` 的已发布版本时请把 DSH 保持在 `0.1.1-rc.2` 或更旧;升级
183
+ DSH 到 `0.1.2-alpha.2` 之前,先升级插件到 `>=0.3.7`。
184
+
185
+ ### 当前发布版本 —— `0.3.7`
186
+
187
+ 当前发布版本声明适配 DSH `>=0.1.2-alpha.2 <0.2.0-0`,并以本地
188
+ `deepseek-harness` 的 `dsh-v0.1.5-alpha.1` 代码和 Web 运行时完成完整验证。
189
+
190
+ | 组件 | 适配版本 |
191
+ | --- | --- |
192
+ | DSH runtime / host / client 包族 | `>=0.1.2-alpha.2 <0.2.0-0` |
193
+ | `@deepseek-ai/cordis` | `^4.0.2` |
194
+ | `@deepseek-ai/schemastery` | `^3.18.2` |
195
+ | Node.js | `>=22.19.0` |
196
+
197
+ `0.1.2-alpha.2` 是这组 API 的最低版本:宿主设置注册使用
198
+ `settings.installSection`,浏览器端设置卡通过插件自有 HTTP 路由
199
+ (`/api/dsh-engram/settings`,见上文 Route B)读写。
200
+ `0.1.2-alpha.2` 和 `0.1.3-alpha.2` 已完成宿主导入与设置注册 smoke 验证;
201
+ `0.1.5-alpha.1` 已完成完整回归(并在 `dsh 0.1.5-rc.1` 隔离 profile 上真机验证
202
+ 设置 GET/PUT/冲突/复位/跨重启持久化全通)。旧的 `0.1.0-rc.7` 与 `0.1.1` API 图不在
203
+ 本发布版本的兼容范围内。
204
+
205
+ 如需按验证版本安装 DSH:
206
+
207
+ ```sh
208
+ npm install --global @deepseek-ai/dsh@0.1.5-alpha.1
209
+ dsh --version
210
+ ```
211
+
152
212
  ## 安装
153
213
 
154
214
  ```sh
@@ -0,0 +1,68 @@
1
+ # Release Notes 模板(每次发布必用)
2
+
3
+ 每个 GitHub Release 正文都应包含以下区块。**「DSH 版本支持」一节为必填**,
4
+ 其余按发布内容填写。版本支持信息从 `package.json` 读取,避免手写漂移:
5
+
6
+ ```sh
7
+ # 直接生成「DSH 版本支持」markdown 段(读 package.json 的 engines.dsh + peers)
8
+ npm run dsh-engram:release-notes
9
+ # 或
10
+ node scripts/gen-release-notes.mjs
11
+ ```
12
+
13
+ ---
14
+
15
+ ## 模板正文(复制后填空)
16
+
17
+ ```
18
+ ## dsh-engram v<版本号>
19
+
20
+ <一句话:自上一个版本以来的主题>
21
+
22
+ ### 功能
23
+ - ...
24
+
25
+ ### 修复
26
+ - ...
27
+
28
+ ### 其他
29
+ - ...
30
+
31
+ ## DSH 版本支持(必填)
32
+
33
+ - **支持的 DSH 范围**:`<复制自 package.json engines.dsh,如 >=0.1.2-alpha.2 <0.2.0-0>`
34
+ - **验证版本**:`<如 dsh-v0.1.5-alpha.1>`(本地 deepseek-harness checkout)
35
+ - **最后验证日期**:`<YYYY-MM-DD>`
36
+ - **安装方式已验证**:`<如 dsh plugin --profile web add dsh-engram>`
37
+ - **peer 声明**:`<复制自 package.json peerDependencies 中的 @deepseek-ai/dsh-* 范围>`
38
+
39
+ > ⚠️ 新旧分界:DSH `0.1.2-alpha.2` 起 settings/client API 族大改
40
+ > (`@deepseek-ai/dsh-settings` 移除 `settingsNamespace`/`installSettingsSection`,
41
+ > 改 `settings.installSection`;client 注入集从 `dsh-client-runtime` 换到
42
+ > `dsh-api-remotes` 等;cordis `^4.0.2`、schemastery `^3.18.2`)。
43
+ > 若本版本仍声明 peers `^0.1.0-rc.7`,说明它**只支持旧版 DSH**
44
+ > (≤ `0.1.1-rc.2`),正文必须写明「仅支持旧版 DSH」并用上面第一段替换当前示例。
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 历史版本口径(补注用,legacy releases 说明)
50
+
51
+ 已发布版本 `0.2.0` … `0.3.6` 的 Release/README 如果缺失 DSH 兼容说明,统一补这
52
+ 一段:
53
+
54
+ ```
55
+ ## DSH 版本支持
56
+
57
+ - **支持的 DSH 范围**:仅旧版 —— `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2`
58
+ - **peer 声明**:`@deepseek-ai/dsh-*@^0.1.0-rc.7`
59
+ - ⚠️ DSH `>=0.1.2-alpha.2` 与本版本**不兼容**(settings/client API 族在该版本断裂)。
60
+ 如需新版 DSH,请升级插件到 `master` 的下一个发布。
61
+ ```
62
+
63
+ ## dsh-market 兼容信息格式参考
64
+
65
+ - 包清单的版本兼容字段是 **semver 范围**:`package.json` / `dsh.plugin.json` 的
66
+ `engines.dsh`(如 `">=0.1.5-alpha.2"` 或 `"0.1.0-rc.6 || 0.1.2-rc.1"`)。
67
+ - 市场侧 README 公约(`## Compatibility` 段):
68
+ `DSH 版本` · `最后验证日期` · `安装方式已验证`。
package/lib/api.js CHANGED
@@ -5,12 +5,14 @@
5
5
  * wears the loopback-only trust fence (copied from the SSH family): these
6
6
  * routes expose the on-disk memory store, so a LAN-exposed dsh web must not
7
7
  * serve them. All reads go straight at the opened storage-domain handle
8
- * (synchronous after the first open); the two mutations (archive / delete)
9
- * go through the same store methods the agent tools use.
8
+ * (synchronous after the first open); the mutations (archive / delete, and
9
+ * the settings card's PUT /settings) go through the same store methods the
10
+ * agent tools use (settings writes through the `service.settings` seam).
10
11
  */
11
12
 
12
13
  import { uuid, shortId } from "./util.js";
13
14
  import { collectToolCounts, collectUsageStats } from "./usage.js";
15
+ import { SETTINGS_NS } from "./settings.js";
14
16
 
15
17
  /** Route family prefix (kept in sync with the client half). */
16
18
  export const API_PREFIX = "/api/dsh-engram";
@@ -541,6 +543,71 @@ export function makeEngramRoutes(service) {
541
543
  writeJson(res, 200, { items: list });
542
544
  };
543
545
 
546
+ /** Settings card transport — read the `dsh-engram` namespace through the
547
+ * host settings service (see settings.js for the normalized `service.settings`
548
+ * seam). Same-origin fetch, like every other route in this family; this is
549
+ * what lets the web config card be DSH-generation-agnostic (no
550
+ * connection/remote RPC), at the cost of the same loopback/trustedHosts
551
+ * fence the memory viewer already wears. */
552
+ const getSettings = (_req, res) => {
553
+ const settings = service.settings;
554
+ if (!settings) {
555
+ writeError(res, 503, "settings service not available");
556
+ return;
557
+ }
558
+ try {
559
+ // dsh-settings `describe()` returns a BARE array of namespace descriptors
560
+ // (in registration order); older adapters may wrap it as {namespaces:[...]}.
561
+ // Normalize both onto the HTTP contract {namespaces:[...]} the client uses.
562
+ const payload = settings.describe();
563
+ const namespaces = Array.isArray(payload) ? payload : (payload?.namespaces ?? []);
564
+ const view = namespaces.find((n) => n && n.ns === SETTINGS_NS);
565
+ writeJson(res, 200, view ? { namespaces: [view] } : { namespaces: [] });
566
+ } catch (error) {
567
+ writeError(res, 500, String(error?.message ?? error));
568
+ }
569
+ };
570
+
571
+ /** Settings card transport — mutate the `dsh-engram` namespace. Accepts
572
+ * either path ops (`ops: [{op:"set"|"unset", path:[...], value?}]`, the
573
+ * exact shape the card used through the RPC) or a plain partial patch
574
+ * (`patch: object`). The provider validates and persists through the same
575
+ * document the GUI card always wrote to. */
576
+ const putSettings = async (req, res) => {
577
+ const settings = service.settings;
578
+ if (!settings) {
579
+ writeError(res, 503, "settings service not available");
580
+ return;
581
+ }
582
+ const body = await readJsonBody(req);
583
+ if (body.error) return writeError(res, 400, body.error);
584
+ const { ops, patch, expectedRevision } = body.value ?? {};
585
+ if (!Array.isArray(ops) && !(patch !== null && typeof patch === "object")) {
586
+ return writeError(res, 400, "body must provide ops (array of {op,path[,value]}) or patch (object)");
587
+ }
588
+ try {
589
+ if (Array.isArray(ops)) {
590
+ await settings.mutate(SETTINGS_NS, ops, expectedRevision);
591
+ } else {
592
+ await settings.update(SETTINGS_NS, patch, expectedRevision);
593
+ }
594
+ writeJson(res, 200, { ok: true });
595
+ } catch (error) {
596
+ if (error?.code === "SETTINGS_CONFLICT" || error?.name === "SettingsConflictError") {
597
+ writeJson(res, 409, {
598
+ error: {
599
+ code: "SETTINGS_CONFLICT",
600
+ message: String(error?.message ?? "settings moved since read"),
601
+ expected: error?.expected,
602
+ actual: error?.actual,
603
+ },
604
+ });
605
+ return;
606
+ }
607
+ writeError(res, 400, String(error?.message ?? error));
608
+ }
609
+ };
610
+
544
611
  const make = (method, path, handler) => ({
545
612
  kind: "exact",
546
613
  path: `${API_PREFIX}${path}`,
@@ -585,6 +652,7 @@ export function makeEngramRoutes(service) {
585
652
  make("GET", "/model", model),
586
653
  make("GET", "/preview", preview),
587
654
  make("GET", "/config", readConfig),
655
+ makeUnion("/settings", { GET: getSettings, PUT: putSettings }),
588
656
  make("POST", "/memories/archive", archive),
589
657
  make("POST", "/memories/delete", remove),
590
658
  make("POST", "/tasks/close", closeTask),
package/lib/client.js CHANGED
@@ -153,6 +153,34 @@ var EngramApi = class {
153
153
  })
154
154
  );
155
155
  }
156
+ /** Settings card transport — read the `dsh-engram` namespace through our
157
+ * own HTTP API instead of the host connection/remote RPC. */
158
+ async getSettings() {
159
+ return readJson(await fetch(`${API_PREFIX}/settings`));
160
+ }
161
+ /** Settings card transport — persist one or more path ops into the
162
+ * `dsh-engram` namespace. Non-2xx surfaces the server's message. */
163
+ async updateSettings(ops, expectedRevision) {
164
+ const response = await fetch(`${API_PREFIX}/settings`, {
165
+ method: "PUT",
166
+ headers: { "content-type": "application/json" },
167
+ body: JSON.stringify({
168
+ ops,
169
+ ...expectedRevision !== void 0 ? { expectedRevision } : {}
170
+ })
171
+ });
172
+ let body = null;
173
+ try {
174
+ body = await response.json();
175
+ } catch {
176
+ }
177
+ if (!response.ok) {
178
+ const err = body;
179
+ const message = typeof err?.error === "string" ? err.error : typeof err?.error === "object" && err.error !== null && typeof err.error.message === "string" ? err.error.message : `HTTP ${response.status}`;
180
+ throw new EngramApiError(message);
181
+ }
182
+ return body ?? { ok: true };
183
+ }
156
184
  };
157
185
 
158
186
  // client/src/EngramTaskDock.tsx
@@ -3824,14 +3852,7 @@ var EngramScopeImpl = class {
3824
3852
  this.snapshot = LOADING;
3825
3853
  this.notify();
3826
3854
  try {
3827
- const response = await this.api.settings.describe({});
3828
- const result = response?.result;
3829
- if (!result || result.ok !== true) {
3830
- const err = result && "error" in result ? result.error : { code: "settings.describe", message: "unreachable" };
3831
- this.fail(`settings.describe failed: ${String(err?.code ?? err)}: ${String(err?.message ?? "")}`);
3832
- return;
3833
- }
3834
- const payload = result.value;
3855
+ const payload = await this.api.getSettings();
3835
3856
  const view = payload.namespaces?.find((n) => n.ns === this.ns);
3836
3857
  if (!view) {
3837
3858
  this.fail(`the '${this.ns}' settings namespace is not served by this host.`);
@@ -3845,27 +3866,16 @@ var EngramScopeImpl = class {
3845
3866
  base: view.base ?? view.value,
3846
3867
  user: view.user,
3847
3868
  revision: view.revision ?? 0,
3848
- writable: this.opts.writable !== false && payload.writable !== false && applied
3869
+ writable: this.opts.writable !== false && applied
3849
3870
  };
3850
3871
  this.notify();
3851
3872
  } catch (error) {
3852
- this.fail(`settings.describe threw: ${String(error instanceof Error ? error.message : error)}`);
3873
+ this.fail(`settings fetch failed: ${String(error instanceof Error ? error.message : error)}`);
3853
3874
  }
3854
3875
  }
3855
3876
  async mutate(op) {
3856
3877
  const revision = this.snapshot.revision;
3857
- const response = await this.api.settings.mutate({
3858
- ns: this.ns,
3859
- ops: [op],
3860
- ...revision ? { expectedRevision: revision } : {}
3861
- });
3862
- const result = response?.result;
3863
- if (!result || result.ok !== true) {
3864
- const err = result && "error" in result ? result.error : { code: "settings.mutate", message: "unreachable" };
3865
- throw new Error(`settings.mutate failed: ${String(err?.code ?? err)}: ${String(err?.message ?? "")}`);
3866
- }
3867
- const view = result.value;
3868
- if (view) this.snapshot = { ...this.snapshot, revision: view.revision ?? revision };
3878
+ await this.api.updateSettings([op], revision > 0 ? revision : void 0);
3869
3879
  }
3870
3880
  async set(key, value) {
3871
3881
  await this.mutate({ op: "set", path: [String(key)], value });
@@ -4924,7 +4934,7 @@ var en = {
4924
4934
  refresh: "Refresh",
4925
4935
  error: "Load failed"
4926
4936
  };
4927
- var inject = ["slots", "locale", "connection", "sessions", "workspaces"];
4937
+ var inject = ["slots", "locale", "sessions", "workspaces"];
4928
4938
  function apply(ctx) {
4929
4939
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-engram: dictionaries");
4930
4940
  const api = new EngramApi();
@@ -4965,8 +4975,7 @@ function apply(ctx) {
4965
4975
  } catch (error) {
4966
4976
  console.warn("[dsh-engram] settings.section registration failed:", error);
4967
4977
  }
4968
- const connection = ctx.get("connection");
4969
- const scope = new EngramScopeImpl(connection.api, "dsh-engram");
4978
+ const scope = new EngramScopeImpl(api, "dsh-engram");
4970
4979
  const cardInjected = () => ({ scope });
4971
4980
  try {
4972
4981
  ctx.slots.inject(
package/lib/index.js CHANGED
@@ -258,7 +258,7 @@ export function apply(ctx, config) {
258
258
 
259
259
  // User settings (web GUI card): merges stored values onto the live config.
260
260
  // Non-blocking — when the settings service is absent this never runs.
261
- installEngramSettings(ctx, resolved, resolved);
261
+ installEngramSettings(ctx, resolved, resolved, service);
262
262
 
263
263
  // Web API for the GUI: deferred on the webServer service, so headless
264
264
  // profiles keep running the plugin without it. `register` returns the
package/lib/settings.js CHANGED
@@ -12,11 +12,12 @@
12
12
  */
13
13
 
14
14
  import z from "@deepseek-ai/schemastery";
15
- import { settingsNamespace, installSettingsSection } from "@deepseek-ai/dsh-settings";
16
15
 
17
16
  /** Namespace join key — must match the client card's `key`. */
18
17
  export const SETTINGS_NS_NAME = "dsh-engram";
19
- export const SETTINGS_NS = settingsNamespace(SETTINGS_NS_NAME);
18
+ // dsh-settings 0.1.5 exposes the namespace as a validated string at the
19
+ // provider boundary; the legacy branding helper was removed.
20
+ export const SETTINGS_NS = SETTINGS_NS_NAME;
20
21
 
21
22
  /** Fields the GUI exposes (a curated subset of the plugin DEFAULTS). */
22
23
  export const SETTINGS_KEYS = [
@@ -86,30 +87,49 @@ export function makeSettingsSchema(defaults) {
86
87
  /**
87
88
  * Wire the settings section into `ctx` and keep `live` (the plugin's mutable
88
89
  * config object) merged with what the user stored. Called from `apply()`.
90
+ *
91
+ * When a `service` object is supplied, a normalized `service.settings` seam
92
+ * ({ describe, mutate, update }) is exposed for the web API — this is the
93
+ * single host-side adapter point: both the current and the legacy DSH
94
+ * settings APIs reduce onto the same three calls, so `/api/dsh-engram/settings`
95
+ * never needs to know which generation it is talking to.
89
96
  */
90
- export function installEngramSettings(ctx, defaults, live) {
97
+ export function installEngramSettings(ctx, defaults, live, service) {
91
98
  const base = settingsBaseFrom(defaults);
92
99
  const schema = makeSettingsSchema(defaults);
93
100
  let source = () => base;
94
- installSettingsSection(ctx, SETTINGS_NS, schema, base, {
95
- setSource(current) {
96
- source = current;
97
- },
98
- onChange() {
99
- let value;
100
- try {
101
- value = source();
102
- } catch {
103
- return;
104
- }
105
- if (value === null || typeof value !== "object") return;
106
- for (const key of SETTINGS_KEYS) {
107
- if (value[key] !== void 0 && live[key] !== value[key]) live[key] = value[key];
108
- }
109
- },
110
- validate(value) {
111
- // The schema already constrains every exposed field; refuse nothing here.
112
- void value;
113
- },
101
+ ctx.inject(["settings"], (settingsCtx) => {
102
+ // dsh-settings 0.1.5 moved the optional-consumer wiring onto the provider
103
+ // instance. Keeping the registration on the scoped context preserves the
104
+ // provider attach/detach lifecycle while avoiding a removed helper export.
105
+ const provider = settingsCtx.settings;
106
+ if (service) {
107
+ service.settings = {
108
+ describe: () => provider.describe({ redactSecrets: true }),
109
+ mutate: (ns, ops, expectedRevision) => provider.mutate(ns, ops, expectedRevision),
110
+ update: (ns, patch, expectedRevision) => provider.update(ns, patch, expectedRevision),
111
+ };
112
+ }
113
+ provider.installSection(ctx, SETTINGS_NS, schema, base, {
114
+ setSource(current) {
115
+ source = current;
116
+ },
117
+ onChange() {
118
+ let value;
119
+ try {
120
+ value = source();
121
+ } catch {
122
+ return;
123
+ }
124
+ if (value === null || typeof value !== "object") return;
125
+ for (const key of SETTINGS_KEYS) {
126
+ if (value[key] !== void 0 && live[key] !== value[key]) live[key] = value[key];
127
+ }
128
+ },
129
+ validate(value) {
130
+ // The schema already constrains every exposed field; refuse nothing here.
131
+ void value;
132
+ },
133
+ });
114
134
  });
115
135
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-engram",
3
3
  "description": "Minimalist long-term memory for DeepSeek Harness, distilling symbolic-index + pi-esr ideas: zero-LLM auto-capture, a symbolic [ENGRAM] index with progressive disclosure, and an ESR-lite evidence-closure protocol (esr_task / esr_node / esr_close / esr_link). Real agent-behaviour telemetry (usage rollup + /stats) and a deterministic offline recall benchmark (npm run eval). Hot path is model-free; storage on ctx.storageDomain; web memory viewer, observability panel and config card through DSH's native settings slots.",
4
- "version": "0.3.6",
4
+ "version": "0.3.7",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -33,7 +33,8 @@
33
33
  "LICENSE"
34
34
  ],
35
35
  "engines": {
36
- "node": ">=22.19.0"
36
+ "node": ">=22.19.0",
37
+ "dsh": ">=0.1.2-alpha.2 <0.2.0-0"
37
38
  },
38
39
  "scripts": {
39
40
  "setup": "node scripts/setup-links.mjs",
@@ -47,7 +48,8 @@
47
48
  "web-compaction:enable": "node scripts/dsh-engram.mjs enable",
48
49
  "web-compaction:revert": "node scripts/dsh-engram.mjs revert",
49
50
  "dsh-engram": "node scripts/dsh-engram.mjs",
50
- "dsh-engram:doctor": "node scripts/dsh-engram.mjs doctor"
51
+ "dsh-engram:doctor": "node scripts/dsh-engram.mjs doctor",
52
+ "release-notes": "node scripts/gen-release-notes.mjs"
51
53
  },
52
54
  "publishConfig": {
53
55
  "access": "public"
@@ -59,9 +61,11 @@
59
61
  "client": {
60
62
  "platform": "web",
61
63
  "inject": [
62
- "@deepseek-ai/dsh-client-runtime",
63
- "@deepseek-ai/dsh-client-connection",
64
- "@deepseek-ai/dsh-client-ui-settings"
64
+ "@deepseek-ai/dsh-client-locale",
65
+ "@deepseek-ai/dsh-client-ui-conversation",
66
+ "@deepseek-ai/dsh-client-ui-renderer",
67
+ "@deepseek-ai/dsh-client-ui-settings",
68
+ "@deepseek-ai/dsh-client-ui-settings-plugins"
65
69
  ]
66
70
  }
67
71
  },
@@ -69,13 +73,13 @@
69
73
  "zod": "^4.4.3"
70
74
  },
71
75
  "peerDependencies": {
72
- "@deepseek-ai/cordis": "^4.0.1",
73
- "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.7",
74
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
75
- "@deepseek-ai/schemastery": "^3.18.1",
76
- "@deepseek-ai/dsh-compaction": "^0.1.0-rc.7",
77
- "@deepseek-ai/dsh-compaction-basic": "^0.1.0-rc.7",
78
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7"
76
+ "@deepseek-ai/cordis": "^4.0.2",
77
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.2-alpha.2 <0.2.0-0",
78
+ "@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2 <0.2.0-0",
79
+ "@deepseek-ai/schemastery": "^3.18.2",
80
+ "@deepseek-ai/dsh-compaction": ">=0.1.2-alpha.2 <0.2.0-0",
81
+ "@deepseek-ai/dsh-compaction-basic": ">=0.1.2-alpha.2 <0.2.0-0",
82
+ "@deepseek-ai/dsh-llm": ">=0.1.2-alpha.2 <0.2.0-0"
79
83
  },
80
84
  "peerDependenciesMeta": {
81
85
  "@deepseek-ai/cordis": {
File without changes
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-engram release-note helper.
4
+ *
5
+ * Prints the required "DSH 版本支持" markdown stanza for a release note by
6
+ * reading the single source of truth in package.json (engines.dsh + the
7
+ * @deepseek-ai/dsh-* peers), so the declared compatibility can never drift
8
+ * from what the package.json actually claims.
9
+ *
10
+ * Usage:
11
+ * node scripts/gen-release-notes.mjs # print the stanza
12
+ * node scripts/gen-release-notes.mjs --watch # also echo engines + peers
13
+ */
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, resolve } from "node:path";
18
+
19
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
20
+ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
21
+
22
+ const engines = pkg.engines ?? {};
23
+ const peers = pkg.peerDependencies ?? {};
24
+
25
+ const DSH_PEER_PREFIX = "@deepseek-ai/dsh-";
26
+ const dshPeers = Object.entries(peers)
27
+ .filter(([name]) => name.startsWith(DSH_PEER_PREFIX))
28
+ .sort(([a], [b]) => a.localeCompare(b));
29
+
30
+ const range = engines.dsh;
31
+ const legacyOld =
32
+ !range && dshPeers.every(([, r]) => r.includes("0.1.0-rc.7"));
33
+ const supportLine = legacyOld
34
+ ? "仅旧版 —— `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2`"
35
+ : range ?? "(package.json 未声明 engines.dsh —— 补注历史版本时按上一行规则填写)";
36
+
37
+ const peerLine = dshPeers.length
38
+ ? dshPeers.map(([n, r]) => ` - \`${n}\`: \`${r}\``).join("\n")
39
+ : " - (无 @deepseek-ai/dsh-* peer 声明)";
40
+
41
+ const body = `## DSH 版本支持
42
+
43
+ - **支持的 DSH 范围**:${supportLine}
44
+ - **peer 声明**:
45
+ ${peerLine}
46
+ ${
47
+ legacyOld
48
+ ? "- ⚠️ DSH `>=0.1.2-alpha.2` 与本版本**不兼容**(settings/client API 族在该版本断裂:`@deepseek-ai/dsh-settings` 移除 `settingsNamespace`/`installSettingsSection`,改 `settings.installSection`;client 注入集换到 `dsh-api-remotes` 等;cordis `^4.0.2`、schemastery `^3.18.2`)。如需新版 DSH,请升级插件到 `master` 的下一个发布。"
49
+ : range
50
+ ? `- **验证版本**:<如 dsh-v0.1.5-alpha.1>(本地 deepseek-harness checkout)
51
+ - **最后验证日期**:<YYYY-MM-DD>
52
+ - **安装方式已验证**:<如 dsh plugin --profile web add dsh-engram>`
53
+ : ""
54
+ }
55
+ `;
56
+
57
+ if (process.argv.includes("--watch")) {
58
+ console.log(`engines.dsh = ${range ?? "<未声明>"}`);
59
+ console.log(`engines.node = ${engines.node ?? "<未声明>"}`);
60
+ console.log(`@deepseek-ai/cordis peer = ${peers["@deepseek-ai/cordis"] ?? "<未声明>"}`);
61
+ console.log(`@deepseek-ai/schemastery peer = ${peers["@deepseek-ai/schemastery"] ?? "<未声明>"}`);
62
+ console.log("---");
63
+ }
64
+ process.stdout.write(body);
@@ -23,10 +23,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
23
23
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
24
24
  const tmp = mkdtempSync(join(tmpdir(), "dsh-engram-pack-"));
25
25
  const PEERS = [
26
- "@deepseek-ai/cordis@^4.0.1",
27
- "@deepseek-ai/dsh-settings@^0.1.0-rc.7",
28
- "@deepseek-ai/dsh-storage-domain@^0.1.0-rc.7",
29
- "@deepseek-ai/schemastery@^3.18.1",
26
+ "@deepseek-ai/cordis@^4.0.2",
27
+ "@deepseek-ai/dsh-settings@>=0.1.2-alpha.2 <0.2.0-0",
28
+ "@deepseek-ai/dsh-storage-domain@>=0.1.2-alpha.2 <0.2.0-0",
29
+ "@deepseek-ai/schemastery@^3.18.2",
30
30
  ];
31
31
 
32
32
  function sh(cmd, args, cwd) {
@@ -93,6 +93,11 @@ function findStoreZod(harness) {
93
93
  function buildPlan(harness) {
94
94
  const zodDir = findStoreZod(harness);
95
95
  const plan = [
96
+ {
97
+ label: "@deepseek-ai/cordis",
98
+ mount: join(SCOPE, "cordis"),
99
+ target: join(harness, "vendor", "cordis"),
100
+ },
96
101
  {
97
102
  label: "zod",
98
103
  mount: join(NODE_MODULES, "zod"),
@@ -141,7 +146,9 @@ function buildPlan(harness) {
141
146
  function isInstalled(mount, expected) {
142
147
  if (existsSync(mount)) {
143
148
  try {
144
- if (resolve(readlinkSync(mount)) === resolve(expected)) return true;
149
+ // readlinkSync() returns a relative target for pnpm-created links; resolve
150
+ // it from the mount's parent, rather than from the process cwd.
151
+ return resolve(dirname(mount), readlinkSync(mount)) === resolve(expected);
145
152
  } catch {
146
153
  /* not a symlink — fall through */
147
154
  }
@@ -185,24 +192,27 @@ function ensureLink(item) {
185
192
  return;
186
193
  }
187
194
  // 允许替换:符号链接直接 unlink;pnpm 预建的空目录用 rmdir 清掉。
195
+ let removed = false;
188
196
  try {
189
197
  unlinkSync(mount);
190
198
  console.log(` ~ ${label}: 替换旧链接`);
191
- return;
199
+ removed = true;
192
200
  } catch {
193
201
  /* not a symlink */
194
202
  }
195
- try {
196
- if (readdirSync(mount).length === 0) {
197
- rmdirSync(mount);
198
- console.log(` ~ ${label}: 移除空目录后重建`);
199
- } else {
200
- console.log(` ! ${label}: ${mount} 是含文件的真实目录,无法自动替换,请手动清理后重试`);
203
+ if (!removed) {
204
+ try {
205
+ if (readdirSync(mount).length === 0) {
206
+ rmdirSync(mount);
207
+ console.log(` ~ ${label}: 移除空目录后重建`);
208
+ } else {
209
+ console.log(` ! ${label}: ${mount} 是含文件的真实目录,无法自动替换,请手动清理后重试`);
210
+ return;
211
+ }
212
+ } catch {
213
+ console.log(` ! ${label}: ${mount} 无法自动替换,请手动清理后重试`);
201
214
  return;
202
215
  }
203
- } catch {
204
- console.log(` ! ${label}: ${mount} 无法自动替换,请手动清理后重试`);
205
- return;
206
216
  }
207
217
  }
208
218
  if (CHECK_ONLY) {