dsh-engram 0.8.6 → 0.8.8

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.8`; 已发布的 `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,9 @@ 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` (npm) | published | 同 `0.5.0` — 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
298
+ | `0.8.8` (本期 / this release) | 本地 | 同 `0.5.0` — 修推送腿遥测写错目录(`engram/engram/`)导致状态显示「还没记录」;`pushLegsPath` 契约写死在一处 | 同 `0.5.0` |
297
299
 
298
300
  ### Where the API family broke — `0.1.2-alpha.2`
299
301
 
@@ -1035,6 +1037,12 @@ and silence when there is nothing relevant to point at:
1035
1037
  > session object does offer — that line is the thing to paste into a bug report.
1036
1038
  > Known case: the **desktop runtime** (`@deepseek-ai/dsh-desktop-runtime`) on
1037
1039
  > Windows, tracked as issue #9.
1040
+ >
1041
+ > `dsh-engram status` also prints a **push-legs** line — `推送腿:事件日志读法
1042
+ > sessionQuery · 最近 …` — the read path the live process actually took, written
1043
+ > to `$DSH_HOME/engram/push-legs.jsonl`. A dormant host shows
1044
+ > `事件日志不可读(none)… 休眠中` there, so the four features can be checked
1045
+ > without hunting a startup log.
1038
1046
 
1039
1047
  ### Entity pages + entity cards (v0.7-B)
1040
1048
 
package/README.zh.md CHANGED
@@ -188,7 +188,9 @@ 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`(npm) | 已发布 | 同 `0.5.0` —— 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
193
+ | `0.8.8`(本期 / this release) | 本地 | 同 `0.5.0` —— 修推送腿遥测写错目录(`engram/engram/`)导致状态显示「还没记录」;`pushLegsPath` 契约写死在一处 | 同 `0.5.0` |
192
194
 
193
195
  ### API 族断裂点:`0.1.2-alpha.2`
194
196
 
@@ -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`
@@ -0,0 +1,44 @@
1
+ # dsh-engram v0.8.8
2
+
3
+ **0.8.7 的推送腿遥测写错了一层目录——装上真机、重启、看一眼状态,5 分钟内就露馅了。**
4
+
5
+ ## 修复:`push-legs.jsonl` 写到了 `engram/engram/`
6
+
7
+ 0.8.7 引入的推送腿遥测,`engramHome()` 已经以 `engram/` 结尾,写入端又拼了一次
8
+ `engram`,于是文件落在 `~/.dsh/engram/engram/push-legs.jsonl`;而 `dsh-engram status`
9
+ 读的是 `~/.dsh/engram/push-legs.jsonl`。
10
+
11
+ 结果是**一个工作正常的遥测,在状态里显示成「还没记录」**——正好是这一版想消灭的那种
12
+ 「看起来像没发生」。单测没抓到,因为测试自己造目录、两边用的是同一个临时路径。
13
+
14
+ 现在把契约写死在一处:`pushLegsPath(engramDir)` 的参数就是 engram 目录本身,
15
+ 写入端传 `engramHome()`、读取端传 `<dsh home>/engram`,注释里写明为什么不能拼第二次。
16
+ 新增一条回归测试直接断言 `pushLegsPath(dir) === join(dir, "push-legs.jsonl")`。
17
+
18
+ ## 顺带得到的一个真数据点
19
+
20
+ 修好路径、把线上那条记录归位之后,本机(`dsh 0.2.1-rc.2` CLI + web)的答案是:
21
+
22
+ ```
23
+ 推送腿:事件日志读法 snapshotEvents · 最近 2026/10/27 15:44:35 · snapshotEvents×1
24
+ ```
25
+
26
+ 也就是说**我们自己的宿主走的是 `snapshotEvents`**——`ctx.sessionQuery` 在这套组合里
27
+ 没挂上(宿主已把它标为 deprecated,但仍在)。这解释了为什么 0.8.6 补的
28
+ `seq`+`eventAt` 第四手值得留着:桌面运行时那类宿主一旦连 `snapshotEvents` 也去掉,
29
+ `eventAt` 就是最后的退路。
30
+
31
+ ## 证据
32
+
33
+ - `npm test` **489/489**(+1 路径契约回归)
34
+ - `npm run typecheck` 通过;`prepublishOnly` 真实 tarball + peers 后 host/client 双入口导入通过
35
+ - 真机验证:`dsh-engram status` 打印出上面那行读法;`~/.dsh/engram/engram/` 这个错误
36
+ 目录已清理,记录已归位到 `~/.dsh/engram/push-legs.jsonl`
37
+
38
+ ## DSH 版本支持(必填)
39
+
40
+ - **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
41
+ - **验证版本**:`dsh 0.2.1-rc.2`(本机 CLI + web)
42
+ - **最后验证日期**:2026-10-27
43
+ - **安装方式已验证**:`prepublishOnly` 用真实 tarball 装 peers 后导入 host/client 双入口
44
+ - **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
+ engramDir: 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,129 @@
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
+ /**
33
+ * `<engramDir>/push-legs.jsonl`.
34
+ *
35
+ * The argument is the ENGRAM directory (`~/.dsh/engram`), not the DSH home:
36
+ * `engramHome()` already ends in `engram/`, and appending it a second time is
37
+ * how 0.8.7 shipped a telemetry file at `~/.dsh/engram/engram/push-legs.jsonl`
38
+ * that `dsh-engram status` then reported as never written. Kept explicit so the
39
+ * two call sites cannot disagree again.
40
+ */
41
+ export function pushLegsPath(engramDir) {
42
+ return join(engramDir, "push-legs.jsonl");
43
+ }
44
+
45
+ /**
46
+ * Record the read path taken for one session. Best effort: never throws.
47
+ *
48
+ * @param {object} options
49
+ * @param {string} options.engramDir engram directory (`~/.dsh/engram`)
50
+ * @param {string} options.source `sessionEventsSource()` verdict
51
+ * @param {string} [options.sessionId]
52
+ * @param {number} [options.pid]
53
+ * @param {number} [options.now]
54
+ * @returns {boolean} whether the row was written
55
+ */
56
+ export function noteSessionEventsSource({ engramDir, source, sessionId, pid = process.pid, now = Date.now() } = {}) {
57
+ if (typeof engramDir !== "string" || engramDir.length === 0) return false;
58
+ if (typeof source !== "string" || source.length === 0) return false;
59
+ const path = pushLegsPath(engramDir);
60
+ const row = `${JSON.stringify({ at: now, pid, sessionId: sessionId ?? null, source })}\n`;
61
+ try {
62
+ mkdirSync(dirname(path), { recursive: true });
63
+ if (existsSync(path) && statSync(path).size > PUSH_LEGS_MAX_BYTES) {
64
+ const kept = readFileSync(path, "utf8").split("\n").filter((line) => line.length > 0).slice(-PUSH_LEGS_KEEP);
65
+ writeFileSync(`${path}.tmp`, `${kept.join("\n")}\n`, "utf8");
66
+ renameSync(`${path}.tmp`, path);
67
+ }
68
+ appendFileSync(path, row, "utf8");
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Newest recorded read path (and how many rows mention each verdict).
77
+ * Never throws; returns `undefined` when nothing was ever recorded.
78
+ *
79
+ * @param {string} engramDir engram directory (`~/.dsh/engram`)
80
+ * @returns {{at: number, source: string, sessionId: string|null, pid: number, counts: Record<string, number>} | undefined}
81
+ */
82
+ export function readPushLegsHealth(engramDir) {
83
+ if (typeof engramDir !== "string" || engramDir.length === 0) return undefined;
84
+ let raw;
85
+ try {
86
+ raw = readFileSync(pushLegsPath(engramDir), "utf8");
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ const counts = {};
91
+ let newest;
92
+ for (const line of raw.split("\n")) {
93
+ if (line.length === 0) continue;
94
+ let row;
95
+ try {
96
+ row = JSON.parse(line);
97
+ } catch {
98
+ continue;
99
+ }
100
+ if (typeof row?.source !== "string") continue;
101
+ counts[row.source] = (counts[row.source] ?? 0) + 1;
102
+ if (newest === undefined || (typeof row.at === "number" && row.at >= (newest.at ?? 0))) newest = row;
103
+ }
104
+ if (newest === undefined) return undefined;
105
+ return {
106
+ at: typeof newest.at === "number" ? newest.at : 0,
107
+ source: newest.source,
108
+ sessionId: typeof newest.sessionId === "string" ? newest.sessionId : null,
109
+ pid: typeof newest.pid === "number" ? newest.pid : 0,
110
+ counts,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * One-line human summary for `dsh-engram status`.
116
+ * @param {ReturnType<typeof readPushLegsHealth>} health
117
+ * @returns {string}
118
+ */
119
+ export function describePushLegs(health) {
120
+ if (health === undefined) return "推送腿:还没记录(本机还没有会话装配过)";
121
+ const when = new Date(health.at).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
122
+ const tally = Object.entries(health.counts)
123
+ .map(([source, count]) => `${source}×${count}`)
124
+ .join(" · ");
125
+ if (health.source === "none") {
126
+ return `推送腿:⚠ 事件日志不可读(none)—— [RECALL]/episode/resume/todo-sink 休眠中 · ${when} · ${tally}`;
127
+ }
128
+ return `推送腿:事件日志读法 ${health.source} · 最近 ${when} · ${tally}`;
129
+ }
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.8",
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,16 @@ 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
+ // `readLedger` above already resolves the same way: `<dsh home>/engram`.
245
+ const engramDir = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "engram");
246
+ log(` ${describePushLegs(readPushLegsHealth(engramDir))}`);
247
+ log("");
248
+ } catch {}
249
+
239
250
  if (status === undefined) {
240
251
  log(" 尚无状态文件(插件还没写过,或本机没跑过 dsh web)。");
241
252
  } else {
@@ -299,7 +310,14 @@ async function printStatus(presetsDirFlag) {
299
310
  const note = cs === "wired"
300
311
  ? `已接管(dsh-engram/compaction 行${hasPrune ? " + dsh-engram/prune 行" : " · 缺 prune 行:L1 在这个预设里看不见边界,重跑 enable 补齐"})`
301
312
  : cs === "stock"
302
- ? "出厂布局,重启 dsh web 后自动接管"
313
+ ? swapCompactionRow(text) === null
314
+ // "stock" only says the stock row is present; it does not promise
315
+ // the swap will take. `minimal` ships a compaction group without
316
+ // the result pruner, so the swap refuses to guess and the preset
317
+ // stays stock forever — saying "重启后自动接管" there would be a
318
+ // status line that lies.
319
+ ? "出厂布局但与标准形状不同(行不全),自动接管不会发生;要 Context GC 请手工接线或改用 standard"
320
+ : "出厂布局,重启 dsh web 后自动接管"
303
321
  : "自定义/无 compaction,插件不碰";
304
322
  log(` ${preset.id}: ${cs} — ${note}`);
305
323
  }