dsh-engram 0.9.0 → 0.9.2

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
@@ -1266,12 +1266,51 @@ Defaults are token-conscious; override any key via the profile patch
1266
1266
  esrOrder: 41
1267
1267
  ```
1268
1268
 
1269
+ ## Localization
1270
+
1271
+ The client UI (composer dock, ESR board, memory viewer, telemetry, config card)
1272
+ renders through a message catalog keyed by the **Chinese source text**
1273
+ (gettext-style msgid):
1274
+
1275
+ - `client/src/i18n/index.ts` — `tr(key, params)` + `installTranslator`; an
1276
+ unknown key renders the Chinese source, so untranslated copy degrades to the
1277
+ original instead of a `missing.key` placeholder.
1278
+ - `client/src/i18n/ru.ts` — Russian catalog (the language layer this fork adds).
1279
+ - `client/src/entry.tsx` registers `{ zh, en, ru }` with the DSH locale service;
1280
+ at runtime `tr()` follows the active locale's fallback chain, and the shared
1281
+ `esrModel` date formatting uses the active locale tag.
1282
+
1283
+ `zh`/`en` behaviour is unchanged: the English dictionary still carries only the
1284
+ three shell-level keys (`nav` / `refresh` / `error`), and msgid copy falls back
1285
+ to Chinese exactly as before this layer existed.
1286
+
1287
+ Maintaining copy:
1288
+
1289
+ ```sh
1290
+ npm run i18n:wrap # wrap new Chinese strings in tr(...) and refresh ru.ts
1291
+ npm run i18n:check # fail on unwrapped copy or empty catalog entries
1292
+ ```
1293
+
1294
+ `i18n:wrap` needs the TypeScript 5 compiler API (TypeScript 7 — what the client
1295
+ typecheck installs — has no JS AST API); install it locally with
1296
+ `cd tools/.toolchain && npm install`. Template literals become
1297
+ `tr("已沉淀 {0} 项", { "0": n })`, so translations may reorder placeholders.
1298
+ A second language is one dictionary file plus an entry in `ctx.locale.register`.
1299
+
1300
+ Host-rendered GUI text uses the same idea at the API boundary: the standing
1301
+ summary (`/api/dsh-engram/model`) accepts `?lang=` and renders on demand, while
1302
+ the cached row and the `esr_model` tool keep their default copy — the
1303
+ model-facing output never changes with the browser language.
1304
+
1269
1305
  ## Development
1270
1306
 
1271
1307
  ```sh
1272
- npm test # 381 tests: core + web API + GC + usage-observability + Context GC + ESR triggers + BoundaryPrune + preference learning + Context Ledger + JIT push (node:test)
1308
+ npm test # 532 tests: core + web API + GC + usage-observability + Context GC + ESR triggers + BoundaryPrune + preference learning + Context Ledger + JIT push (node:test)
1273
1309
  npm run eval # offline recall + structure benchmark (deterministic)
1274
1310
  npm run build:client
1311
+ npm run host:probe # boot a throwaway real dsh host and assert the compaction engine's
1312
+ # pressure path end to end (no model call; --gc for GC mode, --tgz <old>
1313
+ # for the regression control) — issues #12/#13
1275
1314
  ```
1276
1315
 
1277
1316
  ### Testing & evaluation (two layers of real testing)
package/README.zh.md CHANGED
@@ -886,6 +886,36 @@ promote: 2 pending todo(s) vs 1 ESR task(s) — esr_task(name="…") #suggest-pr
886
886
  esrOrder: 41
887
887
  ```
888
888
 
889
+ ## 本地化
890
+
891
+ 客户端界面(输入框上方任务条、ESR 看板、记忆查看器、遥测、配置卡)通过按
892
+ **中文源文本**(gettext 式 msgid)索引的消息目录渲染:
893
+
894
+ - `client/src/i18n/index.ts` —— `tr(key, params)` 与 `installTranslator`;缺少条目时
895
+ 直接回退到中文源文本,而不是 `missing.key`。
896
+ - `client/src/i18n/ru.ts` —— 俄语目录。
897
+ - `client/src/entry.tsx` 通过 DSH locale 服务注册 `{ zh, en, ru }`;运行时 `tr()`
898
+ 沿当前语言的 fallback 链查找,`esrModel` 的日期格式也使用当前语言。
899
+
900
+ `zh`/`en` 行为不变:英文词典仍只有 `nav` / `refresh` / `error` 三个键,msgid
901
+ 文案与之前的回退行为完全一致。
902
+
903
+ 维护文案:
904
+
905
+ ```sh
906
+ npm run i18n:wrap # 把新增中文串包进 tr(...),并刷新 ru.ts
907
+ npm run i18n:check # 未包裹的文案或空翻译会失败
908
+ ```
909
+
910
+ `i18n:wrap` 需要 TypeScript 5 编译器 API(客户端 typecheck 安装的 TypeScript 7
911
+ 已无 JS AST API):`cd tools/.toolchain && npm install`。模板字符串会转成
912
+ `tr("已沉淀 {0} 项", { "0": n })`,翻译可以调整占位符顺序。新增一种语言 = 一个
913
+ 词典文件 + `ctx.locale.register` 中的一项。
914
+
915
+ 宿主端渲染的 GUI 文本采用同样的思路:常驻摘要接口
916
+ (`/api/dsh-engram/model`)接受 `?lang=` 按需渲染,而缓存行与 `esr_model`
917
+ 工具保持默认文案——模型侧输出不会随浏览器语言变化。
918
+
889
919
  ## 开发
890
920
 
891
921
  ```sh
@@ -0,0 +1,112 @@
1
+ # dsh-engram v0.9.1
2
+
3
+ **两个「自己说自己活着」的部件:一个在 Windows 上从第一次启动起就没亮过,一个接管了
4
+ 本机 compaction 却一次也没压缩过。**
5
+
6
+ 从 `v0.9.0` 到这里只有两个 commit,修的是同一类故障——**状态面显示正常,功能面从未发生**。
7
+ 这类故障单测抓不到,因为它不是逻辑错了,而是两个面用的键、或者两个面用的上下文不一致:
8
+ 写的时候用了一套,读/挂的时候用了另一套。两次都是先有人在真实环境里看见「那个东西没出现」,
9
+ 才回头找到这条缝。
10
+
11
+ ## 修复
12
+
13
+ ### 1. `[ENGRAM]` 块在 Windows 上恒为空(issue #10)
14
+
15
+ `blockFor` / `esrBlockFor` 用裸 `path.resolve(cwd)` 推会话工作区,而所有写入路径——以及
16
+ 渲染调用下面两行的账本——用的是 `workspaceKey()`。win32 上写入端会把整个 key 小写
17
+ (盘符默认大写),于是原始 key 匹配到 0 行:`[ENGRAM]` 渲染成空串,`sessionCache` 再把
18
+ 空串冻住整场会话,看起来像「记忆库是空的」。
19
+
20
+ `[RESUME]` 与 `[ESR]` 快照/冻结块是同一个第二 key 派生,一起修。三处调用点现在共用
21
+ `wk(cwd)`,`lib/index.js` 不再 import `node:path`——没有第二条派生路可走。
22
+ `test/workspace-key.test.mjs` 不依赖 Windows 主机也能复现这对键,另外用源码不变量把调用点
23
+ 锁住。
24
+
25
+ ### 2. 宿主面 compaction 引擎从未真正挂上(issues #12 / #13)
26
+
27
+ `BasicCompactionEngine` 声明 `static inject = ["llm", "tokenMeter", "sessions"]`,但
28
+ `static inject` 只有被 loader **当插件加载**时才转成供给好的依赖图。`mountCompactionEngine`
29
+ 用的是 `new Engine(ctx)`,而且这个 `ctx` 来自 engram 自己那条 `inject = ["tools"]` 的 fiber,
30
+ 于是 `compactIfNeeded()` 的第一句 `this.ctx.tokenMeter` 直接从 ctx 代理里抛
31
+ `cannot get property "tokenMeter" without inject`——可选链救不了,getter 本身在抛。
32
+
33
+ 两个后端把同一条错误吞成了两种表情:
34
+
35
+ - 注册 `agent/request(prepend)` 的后端 → 错误冒到模型切换上,界面提示
36
+ "select a model with a larger context window"(误导,跟模型大小无关);
37
+ - 在 `agent/pre-step` 里读的后端 → 被 catch 成 `step compaction failed … continuing the turn`,
38
+ 引擎挂着、`context-gc.status.json` 一直写着 `host: "context-gc"`、却从不压缩。
39
+
40
+ 现在 `mountCompactionEngine` 改为 `ctx.plugin(Engine)`,两个子类都接
41
+ `constructor(engineCtx = ctx)`,loader 在引擎自己的 fiber 上供给三个服务;engram 自身仍只
42
+ inject `["tools"]`,minimal/headless profile 不因此多背依赖。
43
+
44
+ 顺手处理的三个坑(都有测试钉住):
45
+
46
+ - **`fiber.await()` 不是就绪信号**:inject 缺失时 fiber 从不启动,`await()` 立即 resolve
47
+ 且 resolve 的是 undefined。改成等引擎自身构造的回调;宿主永不供给服务时挂载保持 pending,
48
+ `lib/index.js` 用 15s 上界把 `host: "unavailable"` 写进 status 并 warn 一行,不再静默。
49
+ - **重复 provider 必须在挂载前拦**(`ctx.get("compaction") !== undefined` → 抛)。否则 cordis
50
+ 从 fiber 内部拒绝并 `logger.error` 打栈,而「base 的 `compaction-basic` 行还启用着」本是
51
+ 设计内的情况,不是事故。
52
+ - **错误路径**:`MountedEngine` 里 `try { super() } catch (e) { settle.reject(e); throw e }`,
53
+ 否则构造失败会让挂载静默挂死(内部 `catch {}` 把异常吃了)。
54
+
55
+ ## 这次怎么验的
56
+
57
+ 单测跑的是真 cordis + 真 backend(`test/compaction-mount.test.mjs`,含两个反向对照:
58
+ 一个断言 `compactIfNeeded()` 确实经注入的 meter 实测,一个保留 `new` 的做法仍复现原报错)。
59
+ 但「真 backend」还不是「真宿主」,所以补了一次真机探针:
60
+
61
+ **做法** —— 把本仓库 `npm pack` 成 tarball,在 `/tmp` 里起一个一次性 `DSH_HOME`,用
62
+ `dsh --from-default-profile web` 造 profile,`dsh plugin --profile probe add <tgz>` 装上,
63
+ 再用一份 overlay patch 插一个只做一件事的探针插件:启动 8 秒后调
64
+ `ctx.compaction.compactIfNeeded(agent, "pressure")`,把结果写进文件;`dsh --port 3199 web`
65
+ 起真宿主,读文件。这套流程已固化成 `npm run host:probe`(`scripts/host-mount-probe.mjs`,
66
+ `--gc` 切 GC 模式、`--tgz <old.tgz>` 当对照;不联网、不调模型,退出码 0 = 通 / 1 = 引擎报错)——
67
+ 下面这张表的每一行都能用一条命令重跑。
68
+
69
+ **读数**(`dsh 0.2.1-rc.2`,本机):
70
+
71
+ | 装上的是 | `status.host` | `compactIfNeeded("pressure")` |
72
+ |---|---|---|
73
+ | 本版(fixed,默认配置) | `default` | `ok: true`,`null`(空会话无需压缩) |
74
+ | 本版(fixed,`gcReplacesCompaction: true`) | `context-gc` | `ok: true`,`null` |
75
+ | 父提交 `42a9552`(对照) | `default` | `ok: false` —— `cannot get property "tokenMeter" without inject` |
76
+
77
+ 第三行是关键:同一套探针、同一个宿主,把插件换回父提交的 tarball,原报错原样复现。也就是说
78
+ 前两行的绿不是「没测到那条路」,是那条路真的通了。
79
+
80
+ ## 其他
81
+
82
+ - 贡献者 PR #11(客户端 i18n + 俄语目录,457 条)已在本机全量验证(合并无冲突、`build:client`
83
+ 与提交产物逐字节一致、527/525 只有本机 fence 失败),但**不在本版**:它改的是 `client/`,
84
+ 等 review 合并后再发。
85
+ - 配置键与数据格式与 `0.9.0` / `0.8.8` 完全一致,**无需迁移**。
86
+
87
+ ## DSH 版本支持
88
+
89
+ - **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
90
+ - **peer 声明**:
91
+ - `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
92
+ - `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
93
+ - `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
94
+ - `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
95
+ - `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
96
+ - **验证版本**:`dsh 0.2.1-rc.2`(本机 CLI + web;宿主面 compaction 挂载另用一次性 profile 实测,
97
+ 见上)
98
+ - **最后验证日期**:2026-11-09
99
+ - **安装方式已验证**:`dsh plugin --profile <name> add <tgz|dsh-engram>`
100
+
101
+ ## 验证
102
+
103
+ ```
104
+ npm test → 532 tests, 530 pass
105
+ (2 个失败仅 test/g6-sandbox.test.mjs:需要 OS 级 fence,本机没有,
106
+ 父提交上同样失败)
107
+ npm run typecheck → ok
108
+ npm run build:client → 无 diff(bundle 新鲜)
109
+ npm run prepublishOnly → packed tarball 里 host + client bundle 双入口 import OK
110
+ npm pack → 476.8 kB / 96 files(client bundle 298476 bytes)
111
+ npm run host:probe → exit 0(`--tgz <父提交 tarball>` 复跑:exit 1,原报错)
112
+ ```
@@ -0,0 +1,83 @@
1
+ # dsh-engram v0.9.2
2
+
3
+ **客户端第一次有了语言层。代价是 bundle 从 291KB 涨到 415KB——这笔账记在减法债上,不假装没发生。**
4
+
5
+ 0.9.1 之后合并的贡献者 PR #11(@maxadrom)把浏览器侧所有文案从「各组件各写各的中文字符串」
6
+ 收进一张 msgid 目录,并带上第一份完整译文(俄语,457 条)。宿主侧只多了一条读路径,
7
+ 服务端行为、配置键、数据格式都没动。
8
+
9
+ ## 功能
10
+
11
+ - **客户端文案走 msgid 目录**(gettext 风格,以中文源文为 key):`tr(key, params)`,
12
+ 未知 key 渲染中文源文而不是 `missing.key` —— 没翻译到的地方退化成原文,不会在界面上
13
+ 露出占位符。
14
+ - **俄语目录 457 条**(`client/src/i18n/ru.ts`);`entry.tsx` 向 DSH locale 服务注册
15
+ `{ zh, en, ru }`,日期格式化跟随 active locale,切语言时 DOM 挂载面通过
16
+ `locale.subscribe` 重渲染。
17
+ - **常驻摘要按需渲染**:`/api/dsh-engram/model` 接受 `?lang=`,GUI 拿到自己语言的那份;
18
+ 缓存行和 `esr_model` 工具保持默认文案——**模型看到的输出不随浏览器语言变化**。
19
+ - **模板字符串可重排占位符**:`tr("已沉淀 {0} 项", { "0": n })`,译文不必迁就中文语序。
20
+ - **维护工具**:`npm run i18n:wrap` / `npm run i18n:check`(需要 `tools/.toolchain` 里的
21
+ TS 5 编译器 API,README 写了 bootstrap 命令)。
22
+
23
+ ## 这次改了、但没有改的
24
+
25
+ 三条边界,都是**有意**的:
26
+
27
+ - `en` 仍然只有 3 个壳级 key(`nav` / `refresh` / `error`),msgid 文案回落中文——与这一层
28
+ 存在之前完全一致,不是回归。
29
+ - 服务端另有一张 zh/ru 文案表在 `lib/mental.js`:常驻摘要的正文不在目录里,将来 `en`
30
+ 目录落地时它会是唯一不跟随的面。
31
+ - 少数字面量留在 JSX 注释和壳字典里,`i18n:check` 之外我用启发式扫过一遍 `client/src`,
32
+ 没有漏掉的用户可见文案。
33
+
34
+ ## 代价
35
+
36
+ | | v0.9.1 | v0.9.2 |
37
+ |---|---|---|
38
+ | `lib/client.js` raw | 298 476 B(291 KiB) | 425 128 B(415 KiB) |
39
+ | `lib/client.js` gzip | 61 056 B(59.6 KiB) | 84 572 B(82.6 KiB) |
40
+
41
+ gzip 增量 23.5KB(+38.5%),其中约八成是译文数据本身:编译后的 `ru.ts` 单独就占
42
+ 18 508 B gzip。0.9.0 的发布说明把「client bundle <150KB」列在减法债里,这一版把它推远了。
43
+
44
+ 出路是按 active locale 动态 `import()` 目录——机制已经支持(目录就是一张表 + 一个
45
+ `register` 条目),中文用户不必为俄语文本付费。这条记进待办。
46
+
47
+ ## 其他
48
+
49
+ - 合并时 `package.json` 与 master 的 `host:probe` 脚本冲突(双方各追加 script 行),
50
+ 保留双方三行,没丢任何一方;合并树全量门重跑过,见下。
51
+ - **本版无服务端行为变化**:`lib/` 只新增了那条 `?lang=` 读路径。配置键与数据格式与
52
+ `0.9.1` 一致,无需迁移。
53
+ - 0.9.1 修的 #10 / #12 / #13 已在 npm 上,三个 issue 已按各自的验证口径关闭。
54
+ - `host:probe` 里那个假 session 被发现太假:真 tokenMeter 要经 session-projection 折
55
+ `snapshotEvents()`,缺这些方法时探针会先死在宿主侧、根本没走到引擎读 ctx。补全 session
56
+ 面之后连跑两次稳定复现,`--tgz` 对照也仍按预期失败——探针自己先得过自证。
57
+
58
+ ## DSH 版本支持
59
+
60
+ - **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
61
+ - **peer 声明**:
62
+ - `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
63
+ - `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
64
+ - `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
65
+ - `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
66
+ - `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
67
+ - **验证版本**:`dsh 0.2.1-rc.2`(本机 CLI + web)
68
+ - **最后验证日期**:2026-11-09
69
+ - **安装方式已验证**:`dsh plugin --profile <name> add <tgz|dsh-engram>`
70
+
71
+ ## 验证
72
+
73
+ ```
74
+ npm test → 536 tests, 534 pass
75
+ (2 个失败仅 test/g6-sandbox.test.mjs:需要 OS 级 fence,本机没有,
76
+ 父提交上同样失败)
77
+ npm run typecheck → ok
78
+ npm run build:client → 无 diff(bundle 与提交产物逐字节一致,425 128 B)
79
+ npm run prepublishOnly → packed tarball 里 host + client bundle 双入口 import OK
80
+ npm run host:probe → exit 0(真 dsh 宿主上 compactIfNeeded("pressure") 跑通;
81
+ `--tgz` 换回 0.9.0 的 tarball 仍复现 `cannot get property "tokenMeter" without inject`)
82
+ npm pack → 504.0 kB / 97 files(client bundle 425128 bytes)
83
+ ```
package/lib/api.js CHANGED
@@ -537,10 +537,16 @@ export function makeEngramRoutes(service) {
537
537
  const model = async (req, res) => {
538
538
  const domain = await withDomain(res);
539
539
  if (domain === void 0) return;
540
- const ws = readQuery(req).get("workspace") ?? "";
540
+ const q = readQuery(req);
541
+ const ws = q.get("workspace") ?? "";
542
+ // Render language for the GUI: the cached row (and the esr_model tool) keeps
543
+ // the default copy; only this read is localized, so the model-facing output
544
+ // never changes with the browser language.
545
+ const lang = q.get("lang") ?? "";
541
546
  try {
542
- const { getModel } = await import("./mental.js");
547
+ const { getModel, compileSummary } = await import("./mental.js");
543
548
  const m = await getModel(domain, ws);
549
+ if (lang !== "" && lang !== "zh") m.content = compileSummary(domain, ws, { lang });
544
550
  writeJson(res, 200, { model: m });
545
551
  } catch (err) {
546
552
  writeJson(res, 500, { error: err instanceof Error ? err.message : String(err) });