@shgroup/dsh-serenity-hooks 1.92.0 → 1.93.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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.92.0",
3
+ "version": "1.93.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -9010,10 +9010,33 @@ const ZSTD_MAGIC = Buffer.from([
9010
9010
  ]);
9011
9011
  /** 默认回看窗口:8 小时(owner 2026-10-22 定) */
9012
9012
  const DEFAULT_LOOKBACK_MS = 288e5;
9013
- /** 每次从文件尾部读取的字节数(够覆盖数百个末尾事件) */
9014
- const TAIL_WINDOW_BYTES = 1 << 20;
9015
- /** 需要凑够的末尾事件数(够跨过 turn/end + step/end + end-seed 即可,留余量) */
9016
- const TAIL_EVENT_TARGET = 60;
9013
+ /**
9014
+ * 每次从文件尾部读取的字节数。
9015
+ *
9016
+ * 🔴 **2026-10-24 从 1 MiB 提到 8 MiB**:与 `TAIL_EVENT_TARGET` 提到 400 配套。
9017
+ * 1 MiB 常常装不下 400 个事件(大 step 的 `tool/result` 单条就可几十 KB)——
9018
+ * 那样"要 400 个"只是愿望,实际被字节数截断(**两种上限里小的那个说了算**)。
9019
+ */
9020
+ const TAIL_WINDOW_BYTES = 8 << 20;
9021
+ /**
9022
+ * 需要凑够的末尾事件数。
9023
+ *
9024
+ * 🔴🔴 **2026-10-24 从 60 提到 400 —— 这是"啥都扫不到"的直接成因(实测,不是估算)**。
9025
+ *
9026
+ * 原值 60 的前提是「`turn/end` + `step/end` + `end-seed` 三个事件就在尾部附近」——
9027
+ * 那个前提**在"轮内被打断"时完全不成立**:
9028
+ * · 一轮跑到一半死掉 ⇒ 尾部全是 `tool/call` / `tool/result` / `assistant/message` / `step/start`,
9029
+ * **一条 `turn/start`/`turn/end` 都凑不齐**;
9030
+ * · 实测本机 10 个窗口内会话,**4 个**在 60 事件窗口里凑不到 `turn/start`
9031
+ * (被误判成"没开轮")⇒ 换成 400 事件**全部命中**(turn=56/105/192/24)。
9032
+ * · 最坏一条:57 MB 的日志里,一个 step 就可能塞进几十个 `tool/call`。
9033
+ *
9034
+ * ⚠️ **这不是"调参",是判据前提变了**:一旦允许"轮内任意位置中断"这个形态,
9035
+ * 尾部窗口就**必须**宽到能跨过一整轮的碎片。
9036
+ * 💰 成本:解压帧数上升(实测 6 会话 × 400 事件仍是**几十毫秒**量级)——
9037
+ * 用几十毫秒换"不再漏掉一整类",划算。
9038
+ */
9039
+ const TAIL_EVENT_TARGET = 400;
9017
9040
  /**
9018
9041
  * 从文件尾部倒序解压,凑够 `want` 个事件。
9019
9042
  *
@@ -9102,7 +9125,12 @@ function readTailEvents(logPath, want) {
9102
9125
  */
9103
9126
  const UNFINISHED_KINDS = /* @__PURE__ */ new Set(["interrupted", "aborted"]);
9104
9127
  /**
9105
- * 命中判定。
9128
+ * 命中判定(**形态 A:靠 `turn/end.reason.kind`**)。
9129
+ *
9130
+ * ⚠️ **本函数刻意不判「开了没收」** —— 那种形态与「此刻正在跑」在日志读数上**不可分**,
9131
+ * 只有扫描层手里有 `liveIds`(正在跑的会话 id)才判得准。
9132
+ * ⇒ 分工:**本函数判"收尾原因",`scanInterruptedSessions` 判"开了没收"**(见 `detectUnclosedTurn`)。
9133
+ * ⛔ 不要把这些情形塞进本函数 —— 会让它同时依赖"日志形态"与"进程状态",那种判据无法单独证伪。
9106
9134
  *
9107
9135
  * @returns 命中时给出被中断的轮次与"可能有副作用"的工具名;不命中 null
9108
9136
  */
@@ -9125,6 +9153,61 @@ function detectInterruptedEnd(events) {
9125
9153
  };
9126
9154
  }
9127
9155
  /**
9156
+ * 命中判定(**形态 B:一轮开了却没收**)—— 🔴🔴 **2026-10-24 owner 报障的正解**。
9157
+ *
9158
+ * ## 为什么这一格以前整类漏掉
9159
+ *
9160
+ * 进程在**轮内任意位置**死掉时,宿主**永远没机会写 `turn/end`**
9161
+ * ⇒ 日志就停在 `tool/call` / `step/start` / `assistant/message` 上。
9162
+ * 修前 `detectInterruptedEnd` 首行是 `if (idx < 0) return null`(找不到 turn/end 就放弃)
9163
+ * ⇒ **这一整类被判成"没中断"**。
9164
+ *
9165
+ * ## 判据
9166
+ * **最后一条 `turn/start` 之后没有 `turn/end`** ⇒ 那一轮开了没收。
9167
+ *
9168
+ * ## 为什么必须由**扫描层**调用(而不是并进 `detectInterruptedEnd`)
9169
+ * 「开了没收」与「此刻正在跑」在**日志读数上完全一样**。
9170
+ * 唯一能分开它们的东西是 `liveIds`(宿主报的"此刻在跑的会话")——
9171
+ * 而那是**扫描层**的参数,不是日志的属性。
9172
+ * ⇒ 本函数只回答"日志形态是不是开了没收",**由调用方保证已排除在跑的会话**。
9173
+ * ⚠️ 判据纪律:两个不同的问题(日志形态 / 进程状态)**不许塞进同一个函数**,
9174
+ * 否则那个函数无法被单独证伪(改任一侧都可能让测试红,而说不清是哪侧错了)。
9175
+ *
9176
+ * @param events 尾部事件
9177
+ * @returns 命中时给出未收尾的轮次与"可能有副作用"的工具名;没有未收尾的轮次 ⇒ null
9178
+ */
9179
+ function detectUnclosedTurn(events) {
9180
+ if (events.length === 0) return null;
9181
+ let lastEnd = -1;
9182
+ for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === "turn/end") {
9183
+ lastEnd = i;
9184
+ break;
9185
+ }
9186
+ let startIdx = -1;
9187
+ for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === "turn/start") {
9188
+ startIdx = i;
9189
+ break;
9190
+ }
9191
+ if (startIdx < 0 || startIdx < lastEnd) return null;
9192
+ /**
9193
+ * ⚠️ **不要拿 `session/end-seed` 当"已封口"判据**(我第一版这样写,当场被真机用例打红)。
9194
+ *
9195
+ * 真实日志里最常见的恢复形态就是
9196
+ * `turn/end[interrupted] → session/end-seed → turn/start(新轮)`
9197
+ * —— `end-seed` 是**上一轮**的收尾标记,**不是新一轮的**。
9198
+ * 拿它判"新一轮已封口" ⇒ 会把「新一轮开了就死」误判成"已接续" ⇒ **又回到"啥都扫不到"**。
9199
+ *
9200
+ * ⇒ 判据保持最简:**最后一条 `turn/start` 是否晚于最后一条 `turn/end`**。
9201
+ * "会不会重复拉起正在跑的会话"由 `liveIds` 负责(扫描层),不是本函数的事。
9202
+ */
9203
+ const t = events[startIdx]?.data?.turn;
9204
+ const turn = typeof t === "number" ? t : null;
9205
+ return {
9206
+ turn,
9207
+ pendingTools: findPendingTools(events, turn)
9208
+ };
9209
+ }
9210
+ /**
9128
9211
  * 找出末轮里发起了但没有结果的工具调用名。
9129
9212
  *
9130
9213
  * ⚠️ **尽力而为**,非权威:尾部窗口可能截断更早的调用。上层应把它当
@@ -9196,7 +9279,22 @@ function scanInterruptedSessions(projectDir, options = {}) {
9196
9279
  result.scanned++;
9197
9280
  try {
9198
9281
  const events = readTailEvents(logPath, TAIL_EVENT_TARGET);
9199
- const hit = detectInterruptedEnd(events);
9282
+ /**
9283
+ * 🔴🔴 **两种形态都要接**(2026-10-24 owner 报障:「我要求你抓到非工具调用的中断,
9284
+ * 现在什么都抓不到了」)。
9285
+ *
9286
+ * - 形态 A `detectInterruptedEnd`:日志里有 `turn/end`,且它的 `reason.kind` 是
9287
+ * `interrupted`(崩溃后被 resume 补收尾)/`aborted`(被正常终止)。
9288
+ * - 形态 B `detectUnclosedTurn`:**日志里压根没有 `turn/end`** —— 进程在轮内任意位置死掉,
9289
+ * 宿主没机会写收尾 ⇒ 日志停在 `tool/call` / `step/start` / `assistant/message`。
9290
+ * ⚠️ **这一格必须靠"已排除在跑的会话"才成立**,而 `liveIds` 恰好在本函数手里
9291
+ * (循环开头 `if (liveIds.has(sessionId)) continue`)⇒ 这就是它该待在这里、而不是
9292
+ * 并进 `detectInterruptedEnd` 的原因。
9293
+ *
9294
+ * 实测(本机 10 个窗口内会话):**4 条**只能由形态 B 抓到(末条 `tool/call` / `step/start`),
9295
+ * 修前全落到 `skipped`。
9296
+ */
9297
+ const hit = detectInterruptedEnd(events) ?? detectUnclosedTurn(events);
9200
9298
  if (!hit) {
9201
9299
  result.skipped++;
9202
9300
  continue;
@@ -100,7 +100,12 @@ export interface ScanResult {
100
100
  elapsedMs: number;
101
101
  }
102
102
  /**
103
- * 命中判定。
103
+ * 命中判定(**形态 A:靠 `turn/end.reason.kind`**)。
104
+ *
105
+ * ⚠️ **本函数刻意不判「开了没收」** —— 那种形态与「此刻正在跑」在日志读数上**不可分**,
106
+ * 只有扫描层手里有 `liveIds`(正在跑的会话 id)才判得准。
107
+ * ⇒ 分工:**本函数判"收尾原因",`scanInterruptedSessions` 判"开了没收"**(见 `detectUnclosedTurn`)。
108
+ * ⛔ 不要把这些情形塞进本函数 —— 会让它同时依赖"日志形态"与"进程状态",那种判据无法单独证伪。
104
109
  *
105
110
  * @returns 命中时给出被中断的轮次与"可能有副作用"的工具名;不命中 null
106
111
  */
@@ -108,6 +113,34 @@ export declare function detectInterruptedEnd(events: SessionEvent[]): {
108
113
  turn: number | null;
109
114
  pendingTools: string[];
110
115
  } | null;
116
+ /**
117
+ * 命中判定(**形态 B:一轮开了却没收**)—— 🔴🔴 **2026-10-24 owner 报障的正解**。
118
+ *
119
+ * ## 为什么这一格以前整类漏掉
120
+ *
121
+ * 进程在**轮内任意位置**死掉时,宿主**永远没机会写 `turn/end`**
122
+ * ⇒ 日志就停在 `tool/call` / `step/start` / `assistant/message` 上。
123
+ * 修前 `detectInterruptedEnd` 首行是 `if (idx < 0) return null`(找不到 turn/end 就放弃)
124
+ * ⇒ **这一整类被判成"没中断"**。
125
+ *
126
+ * ## 判据
127
+ * **最后一条 `turn/start` 之后没有 `turn/end`** ⇒ 那一轮开了没收。
128
+ *
129
+ * ## 为什么必须由**扫描层**调用(而不是并进 `detectInterruptedEnd`)
130
+ * 「开了没收」与「此刻正在跑」在**日志读数上完全一样**。
131
+ * 唯一能分开它们的东西是 `liveIds`(宿主报的"此刻在跑的会话")——
132
+ * 而那是**扫描层**的参数,不是日志的属性。
133
+ * ⇒ 本函数只回答"日志形态是不是开了没收",**由调用方保证已排除在跑的会话**。
134
+ * ⚠️ 判据纪律:两个不同的问题(日志形态 / 进程状态)**不许塞进同一个函数**,
135
+ * 否则那个函数无法被单独证伪(改任一侧都可能让测试红,而说不清是哪侧错了)。
136
+ *
137
+ * @param events 尾部事件
138
+ * @returns 命中时给出未收尾的轮次与"可能有副作用"的工具名;没有未收尾的轮次 ⇒ null
139
+ */
140
+ export declare function detectUnclosedTurn(events: SessionEvent[]): {
141
+ turn: number | null;
142
+ pendingTools: string[];
143
+ } | null;
111
144
  /**
112
145
  * 扫描一个 **project 目录**(形如 `<sessions-root>/--home-yh-home-home-serenity--/`)。
113
146
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.92.0",
3
+ "version": "1.93.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {