dsh-engram 0.8.6 → 0.8.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
@@ -264,7 +264,7 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
264
264
  <!-- dsh-market compatibility block — keep labels bilingual and in sync with
265
265
  docs/RELEASE-NOTES.TEMPLATE.md (market README convention) -->
266
266
 
267
- - **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.8.6`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
267
+ - **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.8.7`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
268
268
  - **最后验证日期 / Last verified**: 2026-10-27 — DSH `0.2.1-rc.2` (`dsh --version`), web profile live composition; npm test 483/483
269
269
  - **安装方式已验证 / Verified install**: `dsh plugin --profile web add dsh-engram`
270
270
 
@@ -293,7 +293,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
293
293
  | `0.8.0`–`0.8.2` | 本地 | 同 `0.5.0` — 证据化 procedure(v0.8-A)、导出为 DSH Skill(v0.8-B)、subagent 交接胶囊(v0.8-C) | 同 `0.5.0` |
294
294
  | `0.8.3`–`0.8.4` | 本地 | 同 `0.5.0` — 审批门(v0.8-D)+ 其 GUI;G6 门槛改单边(Δ ≥ −2pp)+ 隐藏规格语料 | 同 `0.5.0` |
295
295
  | `0.8.5` | 本地 | 同 `0.5.0` — 记忆行文件信号(`⚠file-changed` / `⚠file-missing`),G6 第三/四轮(撤回 +30pp,Δ=+58.3pp) | 同 `0.5.0` |
296
- | `0.8.6` (本期 / this release) | 本地 | 同 `0.5.0` — 修 issue #9:宿主没有 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
296
+ | `0.8.6` (npm) | published | 同 `0.5.0` — 修 issue #9:宿主没有 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
297
+ | `0.8.7` (本期 / this release) | 本地 | 同 `0.5.0` — 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
297
298
 
298
299
  ### Where the API family broke — `0.1.2-alpha.2`
299
300
 
@@ -1035,6 +1036,12 @@ and silence when there is nothing relevant to point at:
1035
1036
  > session object does offer — that line is the thing to paste into a bug report.
1036
1037
  > Known case: the **desktop runtime** (`@deepseek-ai/dsh-desktop-runtime`) on
1037
1038
  > Windows, tracked as issue #9.
1039
+ >
1040
+ > `dsh-engram status` also prints a **push-legs** line — `推送腿:事件日志读法
1041
+ > sessionQuery · 最近 …` — the read path the live process actually took, written
1042
+ > to `$DSH_HOME/engram/push-legs.jsonl`. A dormant host shows
1043
+ > `事件日志不可读(none)… 休眠中` there, so the four features can be checked
1044
+ > without hunting a startup log.
1038
1045
 
1039
1046
  ### Entity pages + entity cards (v0.7-B)
1040
1047
 
package/README.zh.md CHANGED
@@ -188,7 +188,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
188
188
  | `0.7.3`–`0.7.4`(npm `latest`) | 已发布 | 同 `0.5.0` —— 设置卡迁到「Engram 记忆 → 设置」tab(DSH `0.2.1` 删了旧 section);修 DSH `0.2.x` 上「推送腿」整条静默失效(`[RECALL]` 启动召回、JIT 推送) | 同 `0.5.0` |
189
189
  | `0.7.5`–`0.7.13` | 本地 | 同 `0.5.0` —— 账本按会话落行 + `gate-audit`;L1 从「自己剪」转成「记账 + 归因」;压缩入账读真实事件形状;会话计数重启不清零;CLI 尊重 `DSH_HOME` | 同 `0.5.0` |
190
190
  | `0.8.0`–`0.8.5` | 本地 | 同 `0.5.0` —— 证据化 procedure、导出 DSH Skill、subagent 交接胶囊、审批门 + GUI、记忆行文件信号(`⚠file-changed`/`⚠file-missing`)、G6 门改单边 + 隐藏规格语料(Δ=+58.3pp) | 同 `0.5.0` |
191
- | `0.8.6`(本期 / this release) | 本地 | 同 `0.5.0` —— 修 issue #9:宿主不暴露 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
191
+ | `0.8.6`(npm) | 已发布 | 同 `0.5.0` —— 修 issue #9:宿主不暴露 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
192
+ | `0.8.7`(本期 / this release) | 本地 | 同 `0.5.0` —— 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
192
193
 
193
194
  ### API 族断裂点:`0.1.2-alpha.2`
194
195
 
@@ -0,0 +1,69 @@
1
+ # dsh-engram v0.8.7
2
+
3
+ **「特性睡着了」这件事,现在有离线证据可查;顺手拆掉一句永远不会兑现的状态承诺。**
4
+
5
+ 0.8.6 修了 issue #9 的**能力**问题(宿主不暴露批量读接口时退回 `seq`+`eventAt`)。
6
+ 这一版补的是**可观测**问题:就算下一次再遇到同类宿主,也不该有人靠解 zstd 帧
7
+ 才能证明「不是还没触发,是根本不会触发」。
8
+
9
+ ## 新增:推送腿(push legs)有了落盘证据
10
+
11
+ `[RECALL]`、会话 episode、`[RESUME]` 简报、会话结束的 todo sink —— 这四件事共享
12
+ 同一个前提:会话事件日志可读。它们一起醒,也一起睡。
13
+
14
+ 以前这里只有一个 `warn`,打在一个没人留着的终端上。现在每个会话装配时,插件把
15
+ **实际走了哪条读法**追加进 `$DSH_HOME/engram/push-legs.jsonl`(有界:超 32 KB 只留尾 50 行):
16
+
17
+ ```json
18
+ {"at":1793086321920,"pid":59392,"sessionId":"session-…","source":"sessionQuery"}
19
+ ```
20
+
21
+ `source` 就是 `sessionEventsSource()` 的裁决:`legacy` / `sessionQuery` /
22
+ `snapshotEvents` / `eventAt` / `none`。
23
+
24
+ `dsh-engram status` 多一行:
25
+
26
+ ```
27
+ 推送腿:事件日志读法 sessionQuery · 最近 2026/10/27 15:32:01 · sessionQuery×3 · eventAt×1
28
+ 推送腿:⚠ 事件日志不可读(none)—— [RECALL]/episode/resume/todo-sink 休眠中 · 2026/10/27 15:32:01 · none×1
29
+ ```
30
+
31
+ 第二条就是 issue #9 那台机器的样子——不用开日志、不用解帧,一眼能看出是休眠还是没触发。
32
+
33
+ ## 修复:`minimal` 预设的状态行在撒谎
34
+
35
+ `dsh-engram status` 对未接管的预设一直打印「出厂布局,重启 dsh web 后自动接管」。
36
+ 但 DSH 0.2.1-rc.2 的 `minimal.patch.yml` 里 compaction 组**缺 `tool-result-pruner` 行**,
37
+ 换行不变量(要标准形状)不成立,`swapCompactionRow` 返回 `null`,boot 时记的是
38
+ `skipped / layout-mismatch`——那句承诺永远不会兑现,重启多少次都是 `stock`。
39
+
40
+ 现在对 `stock` 再做一次换行探测,换不动就直说:
41
+
42
+ ```
43
+ minimal: stock — 出厂布局但与标准形状不同(行不全),自动接管不会发生;要 Context GC 请手工接线或改用 standard
44
+ ```
45
+
46
+ ## 证据
47
+
48
+ - `npm test` **488/488**(+5:push-legs 落盘、最新裁决、休眠措辞、有界重写、坏文件不抛)
49
+ - `npm run typecheck` 通过;`prepublishOnly` 真实 tarball + peers 装上后 host/client 双入口导入通过
50
+ - 本机 0.8.6 实机取证:状态快照 `pid 59392`(15:31:47 写入)→ `recall-injections.json`
51
+ 的 `lastInjection` 为 **15:32:01**,即重启后 14 秒推送腿真的动了
52
+
53
+ ## 升级后请做的一件事(如果你在 issue #9 里报过障)
54
+
55
+ ```sh
56
+ npx dsh-engram status
57
+ ```
58
+
59
+ 把「推送腿」那一行贴回 issue。如果它写 `none`,说明桌面运行时的 Session 连
60
+ `eventAt` 都不给,那才需要走「直接读 `session.v4.jsonl.zstd`」这条重路子
61
+ (Node 22 无内置 zstd,要引依赖);如果写 `eventAt` 或 `sessionQuery`,说明已经修好。
62
+
63
+ ## DSH 版本支持(必填)
64
+
65
+ - **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
66
+ - **验证版本**:`dsh 0.2.1-rc.2`(本机 CLI + web)
67
+ - **最后验证日期**:2026-10-27
68
+ - **安装方式已验证**:`prepublishOnly` 用真实 tarball 装 peers 后导入 host/client 双入口
69
+ - **peer 声明**:`@deepseek-ai/dsh-{settings,storage-domain,compaction,compaction-basic,llm}@>=0.1.2-alpha.2 <0.3.0-0`、`@deepseek-ai/cordis@^4.0.2`、`@deepseek-ai/schemastery@^3.18.2`
package/lib/index.js CHANGED
@@ -33,6 +33,7 @@ import { buildEpisode } from "./episode.js";
33
33
  import { collectStaleAnchors } from "./anchor.js";
34
34
  import { renderIndex, renderStartRecall, renderResumeBrief, firstUserMessageText, renderEsr, esrMethodology, buildEsrSnapshot, buildFrozenEsrBlock } from "./index-block.js";
35
35
  import { describeSessionSurface, sessionEventsSource, sessionSeq } from "./session-events.js";
36
+ import { noteSessionEventsSource } from "./push-legs.js";
36
37
  import { makeTriggerRecorder } from "./trigger.js";
37
38
  import { pendingTodosFromSession, sinkPendingTodos } from "./todo-sink.js";
38
39
  import { installEngramSettings, makeEngineConfigSchema, SETTINGS_KEYS } from "./settings.js";
@@ -543,6 +544,8 @@ export function apply(ctx, config) {
543
544
  const sessionCache = new WeakMap();
544
545
  /** One-time-per-process notice: the host's session log is unreadable. */
545
546
  let warnedUnreadableLog = false;
547
+ /** Sessions whose event-log read path was already recorded (push-legs.jsonl). */
548
+ const notedSessionEvents = new WeakSet();
546
549
  const blockFor = (agent) => {
547
550
  if (agent === void 0) return "";
548
551
  const session = agent.session;
@@ -581,6 +584,17 @@ export function apply(ctx, config) {
581
584
  // next assembly retry, so a new conversation never permanently misses its
582
585
  // recalled memory.
583
586
  if (resolved.autoRecallOnStart !== false) {
587
+ // Push-legs telemetry (issue #9): record which read path this session
588
+ // took, once per session object, so `dsh-engram status` can answer
589
+ // "were [RECALL]/episode/resume/todo-sink awake?" without a log capture.
590
+ if (!notedSessionEvents.has(session)) {
591
+ notedSessionEvents.add(session);
592
+ noteSessionEventsSource({
593
+ home: engramHome(),
594
+ source: sessionEventsSource(session, sessionQuery),
595
+ sessionId: session.header?.id ?? session.id,
596
+ });
597
+ }
584
598
  const query = firstUserMessageText(session, sessionQuery);
585
599
  if (query.length === 0) {
586
600
  // Retry on the next assembly ONLY when the log is readable and the
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Observability for the four features that read the session event log.
3
+ *
4
+ * WHY THIS EXISTS (2026-10-27, issue #9): `[RECALL]`, the session episode, the
5
+ * resume brief and the session-end todo sink all depend on ONE host capability
6
+ * — a readable session event log. When a host stops exposing every read path,
7
+ * the four of them sleep together and the only trace is a single `warn` line on
8
+ * a terminal nobody kept. The reporter in issue #9 had to decompress
9
+ * `session.v4.jsonl.zstd` frames by hand to prove the features were not merely
10
+ * "not triggered yet".
11
+ *
12
+ * A warning is not an artifact. This module records which read path the live
13
+ * process actually took, once per session, into
14
+ * `$DSH_HOME/engram/push-legs.jsonl` so `dsh-engram status` can answer
15
+ * "were the push legs awake?" without a session, a log capture, or a report.
16
+ *
17
+ * The record is append-only and bounded (tail kept, like the other telemetry
18
+ * files): diagnostics must never grow without limit or throw into the
19
+ * assembly path.
20
+ * @module dsh-engram/push-legs
21
+ */
22
+
23
+ import { appendFileSync, existsSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
24
+ import { mkdirSync } from "node:fs";
25
+ import { dirname, join } from "node:path";
26
+
27
+ /** Rows kept before the file is rewritten (tail-first). */
28
+ export const PUSH_LEGS_KEEP = 50;
29
+ /** Size at which the file is rewritten to the kept tail. */
30
+ const PUSH_LEGS_MAX_BYTES = 32 * 1024;
31
+
32
+ /** `<home>/engram/push-legs.jsonl` */
33
+ export function pushLegsPath(home) {
34
+ return join(home, "engram", "push-legs.jsonl");
35
+ }
36
+
37
+ /**
38
+ * Record the read path taken for one session. Best effort: never throws.
39
+ *
40
+ * @param {object} options
41
+ * @param {string} options.home DSH home
42
+ * @param {string} options.source `sessionEventsSource()` verdict
43
+ * @param {string} [options.sessionId]
44
+ * @param {number} [options.pid]
45
+ * @param {number} [options.now]
46
+ * @returns {boolean} whether the row was written
47
+ */
48
+ export function noteSessionEventsSource({ home, source, sessionId, pid = process.pid, now = Date.now() } = {}) {
49
+ if (typeof home !== "string" || home.length === 0) return false;
50
+ if (typeof source !== "string" || source.length === 0) return false;
51
+ const path = pushLegsPath(home);
52
+ const row = `${JSON.stringify({ at: now, pid, sessionId: sessionId ?? null, source })}\n`;
53
+ try {
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ if (existsSync(path) && statSync(path).size > PUSH_LEGS_MAX_BYTES) {
56
+ const kept = readFileSync(path, "utf8").split("\n").filter((line) => line.length > 0).slice(-PUSH_LEGS_KEEP);
57
+ writeFileSync(`${path}.tmp`, `${kept.join("\n")}\n`, "utf8");
58
+ renameSync(`${path}.tmp`, path);
59
+ }
60
+ appendFileSync(path, row, "utf8");
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Newest recorded read path (and how many rows mention each verdict).
69
+ * Never throws; returns `undefined` when nothing was ever recorded.
70
+ *
71
+ * @param {string} home DSH home
72
+ * @returns {{at: number, source: string, sessionId: string|null, pid: number, counts: Record<string, number>} | undefined}
73
+ */
74
+ export function readPushLegsHealth(home) {
75
+ if (typeof home !== "string" || home.length === 0) return undefined;
76
+ let raw;
77
+ try {
78
+ raw = readFileSync(pushLegsPath(home), "utf8");
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ const counts = {};
83
+ let newest;
84
+ for (const line of raw.split("\n")) {
85
+ if (line.length === 0) continue;
86
+ let row;
87
+ try {
88
+ row = JSON.parse(line);
89
+ } catch {
90
+ continue;
91
+ }
92
+ if (typeof row?.source !== "string") continue;
93
+ counts[row.source] = (counts[row.source] ?? 0) + 1;
94
+ if (newest === undefined || (typeof row.at === "number" && row.at >= (newest.at ?? 0))) newest = row;
95
+ }
96
+ if (newest === undefined) return undefined;
97
+ return {
98
+ at: typeof newest.at === "number" ? newest.at : 0,
99
+ source: newest.source,
100
+ sessionId: typeof newest.sessionId === "string" ? newest.sessionId : null,
101
+ pid: typeof newest.pid === "number" ? newest.pid : 0,
102
+ counts,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * One-line human summary for `dsh-engram status`.
108
+ * @param {ReturnType<typeof readPushLegsHealth>} health
109
+ * @returns {string}
110
+ */
111
+ export function describePushLegs(health) {
112
+ if (health === undefined) return "推送腿:还没记录(本机还没有会话装配过)";
113
+ const when = new Date(health.at).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
114
+ const tally = Object.entries(health.counts)
115
+ .map(([source, count]) => `${source}×${count}`)
116
+ .join(" · ");
117
+ if (health.source === "none") {
118
+ return `推送腿:⚠ 事件日志不可读(none)—— [RECALL]/episode/resume/todo-sink 休眠中 · ${when} · ${tally}`;
119
+ }
120
+ return `推送腿:事件日志读法 ${health.source} · 最近 ${when} · ${tally}`;
121
+ }
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.8.6",
4
+ "version": "0.8.7",
5
5
  "packageManager": "pnpm@11.22.0",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -25,6 +25,7 @@ import { execFileSync } from "node:child_process";
25
25
  import { dirname, basename, resolve, join } from "node:path";
26
26
  import { homedir } from "node:os";
27
27
  import { fileURLToPath } from "node:url";
28
+ import { describePushLegs, readPushLegsHealth } from "../lib/push-legs.js";
28
29
  import {
29
30
  compactionWireStatus,
30
31
  discoverPresetsDir,
@@ -37,7 +38,7 @@ import { defaultSkillDir, exportSkills, skillDescription, skillName, planSkillEx
37
38
  import { approvedIds, isApproved, loadApprovals, recordApproval, revokeApproval } from "../lib/approvals.js";
38
39
  import { isProvenProcedure, invalidatedProcedure } from "../lib/status-insights.js";
39
40
  import { workspaceKey } from "../lib/util.js";
40
- import { ENGRAM_PRUNE_ROW_NAME } from "../lib/web-provision.js";
41
+ import { ENGRAM_PRUNE_ROW_NAME, swapCompactionRow } from "../lib/web-provision.js";
41
42
 
42
43
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
43
44
 
@@ -236,6 +237,15 @@ async function printStatus(presetsDirFlag) {
236
237
  }
237
238
  } catch {}
238
239
 
240
+ // Push legs (issue #9): which session-event read path the live process took.
241
+ // Printed from data, not from startup logs — a dormant leg must be visible
242
+ // without the terminal that printed the warning.
243
+ try {
244
+ const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
245
+ log(` ${describePushLegs(readPushLegsHealth(home))}`);
246
+ log("");
247
+ } catch {}
248
+
239
249
  if (status === undefined) {
240
250
  log(" 尚无状态文件(插件还没写过,或本机没跑过 dsh web)。");
241
251
  } else {
@@ -299,7 +309,14 @@ async function printStatus(presetsDirFlag) {
299
309
  const note = cs === "wired"
300
310
  ? `已接管(dsh-engram/compaction 行${hasPrune ? " + dsh-engram/prune 行" : " · 缺 prune 行:L1 在这个预设里看不见边界,重跑 enable 补齐"})`
301
311
  : cs === "stock"
302
- ? "出厂布局,重启 dsh web 后自动接管"
312
+ ? swapCompactionRow(text) === null
313
+ // "stock" only says the stock row is present; it does not promise
314
+ // the swap will take. `minimal` ships a compaction group without
315
+ // the result pruner, so the swap refuses to guess and the preset
316
+ // stays stock forever — saying "重启后自动接管" there would be a
317
+ // status line that lies.
318
+ ? "出厂布局但与标准形状不同(行不全),自动接管不会发生;要 Context GC 请手工接线或改用 standard"
319
+ : "出厂布局,重启 dsh web 后自动接管"
303
320
  : "自定义/无 compaction,插件不碰";
304
321
  log(` ${preset.id}: ${cs} — ${note}`);
305
322
  }