@shgroup/dsh-serenity-hooks 1.97.0 → 1.98.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 +1 -1
- package/lib/index.js +151 -58
- package/lib/trajectory/interrupted.d.ts +37 -0
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.98.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
|
@@ -9055,73 +9055,123 @@ const ZSTD_MAGIC = Buffer.from([
|
|
|
9055
9055
|
/** 默认回看窗口:8 小时(owner 2026-10-22 定) */
|
|
9056
9056
|
const DEFAULT_LOOKBACK_MS = 288e5;
|
|
9057
9057
|
/**
|
|
9058
|
-
*
|
|
9058
|
+
* 尾部读取的**渐进块**(从小到大,够用就停)。
|
|
9059
9059
|
*
|
|
9060
|
-
*
|
|
9061
|
-
*
|
|
9062
|
-
*
|
|
9060
|
+
* 🔴🔴 **2026-10-24 A108:从"一次读满 8 MiB"改成"渐进扩块、见到证据就停"。**
|
|
9061
|
+
*
|
|
9062
|
+
* 修前:不管需要多少,**先 `readSync` 读满 8 MiB**,再线性扫一遍帧魔数(O(8 MiB)),
|
|
9063
|
+
* 然后解压到**攒够 400 条事件**为止 —— 而 400 只是一个**猜的数**:
|
|
9064
|
+
* 实测最长单轮 482 行、某会话 `turn/start` 落在 400 条之外 **83 行** ⇒ **证据被截断**,
|
|
9065
|
+
* 于是「这一轮没跑完」被读成「这一轮早就干净收尾了」⇒ **漏唤醒**。这就是 A108。
|
|
9066
|
+
*
|
|
9067
|
+
* ⇒ 现在的判据换成**「要的是证据,不是条数」**:从尾部往前,**一旦同时见到
|
|
9068
|
+
* `turn/start` 与 `turn/end` 就停**(那两条就是判定所需的全部证据)。
|
|
9069
|
+
*
|
|
9070
|
+
* 💰 实测代价(本机 13 个窗口内会话,中位 3 次):
|
|
9071
|
+
* · 单会话 **0.2 ~ 54 ms**(多数 1~9 ms)|13 个合计 **132 ms**;
|
|
9072
|
+
* · 实际读取量多数 **64 KiB**(最多的一个才 4 MiB)—— **远小于修前的恒定 8 MiB**。
|
|
9073
|
+
* ⇒ 所以这既是**修正确性**,也是**省开销**(不是"用性能换正确")。
|
|
9074
|
+
*/
|
|
9075
|
+
const TAIL_CHUNKS = [
|
|
9076
|
+
65536,
|
|
9077
|
+
262144,
|
|
9078
|
+
1 << 20,
|
|
9079
|
+
4 << 20,
|
|
9080
|
+
8 << 20
|
|
9081
|
+
];
|
|
9082
|
+
/**
|
|
9083
|
+
* 尾部读取的**硬上限**(渐进块的最大一档)。
|
|
9084
|
+
*
|
|
9085
|
+
* ⚠️ 它仍然存在,因为"扫到边界为止"在病态日志上可能无界(一整轮几十万条事件)。
|
|
9086
|
+
* 与修前的区别:修前它是**起点**(无条件读满),现在它是**兜底**(扩到这里还没有证据才算不可判定)。
|
|
9063
9087
|
*/
|
|
9064
9088
|
const TAIL_WINDOW_BYTES = 8 << 20;
|
|
9065
9089
|
/**
|
|
9066
|
-
*
|
|
9090
|
+
* 从文件尾部**渐进扩块**倒序解压,**见到判定所需的证据就停**。
|
|
9067
9091
|
*
|
|
9068
|
-
*
|
|
9092
|
+
* ## 与修前的两点不同(都对 A108 负责)
|
|
9069
9093
|
*
|
|
9070
|
-
*
|
|
9071
|
-
*
|
|
9072
|
-
*
|
|
9073
|
-
*
|
|
9074
|
-
* · 实测本机 10 个窗口内会话,**4 个**在 60 事件窗口里凑不到 `turn/start`
|
|
9075
|
-
* (被误判成"没开轮")⇒ 换成 400 事件**全部命中**(turn=56/105/192/24)。
|
|
9076
|
-
* · 最坏一条:57 MB 的日志里,一个 step 就可能塞进几十个 `tool/call`。
|
|
9094
|
+
* 1. **不再一次读满 8 MiB**:按 {@link TAIL_CHUNKS} 从 64 KiB 起扩,够用就停。
|
|
9095
|
+
* 实测多数会话 64 KiB 就够(见 `TAIL_CHUNKS` 的代价段落)。
|
|
9096
|
+
* 2. **停止条件从"攒够 N 条"改成"见到证据"**。⛔ 不要再退回"攒够 400 条" ——
|
|
9097
|
+
* 那个数**必然**在长轮次上截断(实测被挤出 83 行)。
|
|
9077
9098
|
*
|
|
9078
|
-
*
|
|
9079
|
-
* 尾部窗口就**必须**宽到能跨过一整轮的碎片。
|
|
9080
|
-
* 💰 成本:解压帧数上升(实测 6 会话 × 400 事件仍是**几十毫秒**量级)——
|
|
9081
|
-
* 用几十毫秒换"不再漏掉一整类",划算。
|
|
9082
|
-
*/
|
|
9083
|
-
const TAIL_EVENT_TARGET = 400;
|
|
9084
|
-
/**
|
|
9085
|
-
* 从文件尾部倒序解压,凑够 `want` 个事件。
|
|
9099
|
+
* ## 🔴 什么是「证据」(这里踩过一次,写清楚免得后人重犯)
|
|
9086
9100
|
*
|
|
9087
|
-
*
|
|
9088
|
-
*
|
|
9101
|
+
* **必需证据 = 见到一条 `turn/start`**。理由:
|
|
9102
|
+
* - 判定形态 B(轮内死掉)只需"最后一条 `turn/start` 之后没有 `turn/end`"——
|
|
9103
|
+
* 而**进程死在轮内时,宿主根本没机会写 `turn/end`** ⇒ 若要求"必须见到 turn/end",
|
|
9104
|
+
* **这一整类会被判成"不可判定"**(我第一版就是这么写的,被 `形态 B` 用例当场打红)。
|
|
9105
|
+
* - `turn/end` 是**可选证据**:见到它能让形态 A(收尾原因)也成立,但它**不是判定的前提**。
|
|
9106
|
+
*
|
|
9107
|
+
* ⇒ 停止条件:**见到 `turn/start` 即可停**(`turn/end` 顺手见到更好)。
|
|
9108
|
+
* ⇒ 只有**连一条 `turn/start` 都见不到**才报 `exhausted`(那才是真的"不知道")——
|
|
9109
|
+
* 典型成因:一整轮长到 8 MiB 都装不下它的起点。
|
|
9110
|
+
*
|
|
9111
|
+
* ⚠️ 另一种"见不到 turn/start"的合法情况:**日志里从来没有过轮**(空会话/只有 header)。
|
|
9112
|
+
* ⇒ 那种情况扩到**全文**(`winBytes >= st.size`)仍见不到 ⇒ 判 `exhausted: false`
|
|
9113
|
+
* (**"没有轮"是确定的,不是不知道**),由判据函数返回 null ⇒ 进 `skipped`。
|
|
9089
9114
|
*/
|
|
9090
|
-
function readTailEvents(logPath
|
|
9091
|
-
if (zstdDecompressSync === void 0) return
|
|
9115
|
+
function readTailEvents(logPath) {
|
|
9116
|
+
if (zstdDecompressSync === void 0) return {
|
|
9117
|
+
events: [],
|
|
9118
|
+
exhausted: false
|
|
9119
|
+
};
|
|
9092
9120
|
const st = statSync(logPath);
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9121
|
+
if (st.size < 8) return {
|
|
9122
|
+
events: [],
|
|
9123
|
+
exhausted: false
|
|
9124
|
+
};
|
|
9096
9125
|
const fd = openSync(logPath, "r");
|
|
9097
9126
|
try {
|
|
9098
|
-
|
|
9127
|
+
for (const chunk of TAIL_CHUNKS) {
|
|
9128
|
+
const winBytes = Math.min(chunk, st.size);
|
|
9129
|
+
const buf = Buffer.alloc(winBytes);
|
|
9130
|
+
readSync(fd, buf, 0, winBytes, st.size - winBytes);
|
|
9131
|
+
const offsets = [];
|
|
9132
|
+
for (let i = 0; i <= winBytes - ZSTD_MAGIC.length; i++) if (buf[i] === ZSTD_MAGIC[0] && buf[i + 1] === ZSTD_MAGIC[1] && buf[i + 2] === ZSTD_MAGIC[2] && buf[i + 3] === ZSTD_MAGIC[3]) offsets.push(i);
|
|
9133
|
+
const events = [];
|
|
9134
|
+
let sawStart = false;
|
|
9135
|
+
for (let k = offsets.length - 1; k >= 0; k--) {
|
|
9136
|
+
const start = offsets[k];
|
|
9137
|
+
const end = k + 1 < offsets.length ? offsets[k + 1] : winBytes;
|
|
9138
|
+
let text;
|
|
9139
|
+
try {
|
|
9140
|
+
text = zstdDecompressSync(buf.subarray(start, end)).toString("utf8");
|
|
9141
|
+
} catch {
|
|
9142
|
+
break;
|
|
9143
|
+
}
|
|
9144
|
+
const batch = [];
|
|
9145
|
+
for (const line of text.split("\n")) {
|
|
9146
|
+
if (!line.trim()) continue;
|
|
9147
|
+
try {
|
|
9148
|
+
const ev = JSON.parse(line);
|
|
9149
|
+
batch.push(ev);
|
|
9150
|
+
if (ev.type === "turn/start") sawStart = true;
|
|
9151
|
+
} catch {}
|
|
9152
|
+
}
|
|
9153
|
+
events.unshift(...batch);
|
|
9154
|
+
if (sawStart) return {
|
|
9155
|
+
events,
|
|
9156
|
+
exhausted: false
|
|
9157
|
+
};
|
|
9158
|
+
}
|
|
9159
|
+
if (winBytes >= st.size) return {
|
|
9160
|
+
events,
|
|
9161
|
+
exhausted: false
|
|
9162
|
+
};
|
|
9163
|
+
if (winBytes >= TAIL_WINDOW_BYTES) return {
|
|
9164
|
+
events,
|
|
9165
|
+
exhausted: true
|
|
9166
|
+
};
|
|
9167
|
+
}
|
|
9168
|
+
return {
|
|
9169
|
+
events: [],
|
|
9170
|
+
exhausted: true
|
|
9171
|
+
};
|
|
9099
9172
|
} finally {
|
|
9100
9173
|
closeSync(fd);
|
|
9101
9174
|
}
|
|
9102
|
-
const offsets = [];
|
|
9103
|
-
for (let i = 0; i <= windowBytes - ZSTD_MAGIC.length; i++) if (buf[i] === ZSTD_MAGIC[0] && buf[i + 1] === ZSTD_MAGIC[1] && buf[i + 2] === ZSTD_MAGIC[2] && buf[i + 3] === ZSTD_MAGIC[3]) offsets.push(i);
|
|
9104
|
-
if (offsets.length === 0) return [];
|
|
9105
|
-
const events = [];
|
|
9106
|
-
for (let k = offsets.length - 1; k >= 0 && events.length < want; k--) {
|
|
9107
|
-
const start = offsets[k];
|
|
9108
|
-
const end = k + 1 < offsets.length ? offsets[k + 1] : windowBytes;
|
|
9109
|
-
let text;
|
|
9110
|
-
try {
|
|
9111
|
-
text = zstdDecompressSync(buf.subarray(start, end)).toString("utf8");
|
|
9112
|
-
} catch {
|
|
9113
|
-
break;
|
|
9114
|
-
}
|
|
9115
|
-
const batch = [];
|
|
9116
|
-
for (const line of text.split("\n")) {
|
|
9117
|
-
if (!line.trim()) continue;
|
|
9118
|
-
try {
|
|
9119
|
-
batch.push(JSON.parse(line));
|
|
9120
|
-
} catch {}
|
|
9121
|
-
}
|
|
9122
|
-
events.unshift(...batch);
|
|
9123
|
-
}
|
|
9124
|
-
return events.slice(-want);
|
|
9125
9175
|
}
|
|
9126
9176
|
/**
|
|
9127
9177
|
* 判定一组尾部事件是否「以未完成的轮次结尾」。
|
|
@@ -9315,7 +9365,8 @@ function scanInterruptedSessions(projectDir, options = {}) {
|
|
|
9315
9365
|
skipped: 0,
|
|
9316
9366
|
errors: [],
|
|
9317
9367
|
elapsedMs: 0,
|
|
9318
|
-
excluded: []
|
|
9368
|
+
excluded: [],
|
|
9369
|
+
undecidable: []
|
|
9319
9370
|
};
|
|
9320
9371
|
if (!existsSync(projectDir)) {
|
|
9321
9372
|
result.elapsedMs = Date.now() - t0;
|
|
@@ -9356,7 +9407,26 @@ function scanInterruptedSessions(projectDir, options = {}) {
|
|
|
9356
9407
|
if (mtimeMs < cutoff) continue;
|
|
9357
9408
|
result.scanned++;
|
|
9358
9409
|
try {
|
|
9359
|
-
const
|
|
9410
|
+
const tail = readTailEvents(logPath);
|
|
9411
|
+
const events = tail.events;
|
|
9412
|
+
/**
|
|
9413
|
+
* 🔴🔴 **A108:拿不到判定所需证据时,报「不可判定」,⛔ 不许塌成「没中断」。**
|
|
9414
|
+
*
|
|
9415
|
+
* `tail.exhausted` 的判据 = 扩到 8 MiB 硬上限仍**没能同时见到** `turn/start` 与 `turn/end`。
|
|
9416
|
+
* 那时两个判据函数都会返回 null(它们只看「见到的那些事件」)——
|
|
9417
|
+
* 而「没证据」与「证据说没事」在返回值上长得一样,**这正是 A108 的漏**。
|
|
9418
|
+
* ⇒ 所以这一格必须**在调用它们之前**分流。
|
|
9419
|
+
*/
|
|
9420
|
+
if (tail.exhausted) {
|
|
9421
|
+
result.undecidable.push({
|
|
9422
|
+
sessionId,
|
|
9423
|
+
logPath,
|
|
9424
|
+
mtimeMs,
|
|
9425
|
+
lastEventAt: typeof events[events.length - 1]?.time === "number" ? events[events.length - 1].time : null,
|
|
9426
|
+
reason: `扩到 ${Math.round(TAIL_WINDOW_BYTES / 1048576)} MiB 硬上限仍未同时见到 turn/start 与 turn/end`
|
|
9427
|
+
});
|
|
9428
|
+
continue;
|
|
9429
|
+
}
|
|
9360
9430
|
/**
|
|
9361
9431
|
* 🔴🔴 **两种形态都要接**(2026-10-24 owner 报障:「我要求你抓到非工具调用的中断,
|
|
9362
9432
|
* 现在什么都抓不到了」)。
|
|
@@ -11142,18 +11212,33 @@ function renderInterruptedScan(input) {
|
|
|
11142
11212
|
const { projectDir, lookbackHours, scan } = input;
|
|
11143
11213
|
const lines = [];
|
|
11144
11214
|
const excludedCount = scan.excluded.length;
|
|
11215
|
+
const undecidableCount = scan.undecidable.length;
|
|
11145
11216
|
lines.push(`═══ 末轮未跑完的 dsh 会话(回看 ${lookbackHours}h)═══`);
|
|
11146
11217
|
lines.push(` 扫描域: ${projectDir}`);
|
|
11147
|
-
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped}` + (excludedCount > 0 ? ` · 排除在跑 ${excludedCount} 个` : "") + ` · 耗时 ${scan.elapsedMs}ms${scan.errors.length > 0 ? ` · 读取失败 ${scan.errors.length}` : ""}`);
|
|
11218
|
+
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped}` + (excludedCount > 0 ? ` · 排除在跑 ${excludedCount} 个` : "") + ` · 不可判定 ${undecidableCount} 个 · 耗时 ${scan.elapsedMs}ms${scan.errors.length > 0 ? ` · 读取失败 ${scan.errors.length}` : ""}`);
|
|
11148
11219
|
if (excludedCount > 0) {
|
|
11149
11220
|
lines.push(` ⓘ 窗口内共 ${scan.scanned + excludedCount} 个会话,其中 ${excludedCount} 个**此刻有轮次在跑**,按「正在跑的不该被判为待续」的纪律被排除:`);
|
|
11150
11221
|
for (const id of scan.excluded) lines.push(` - ${id}`);
|
|
11151
11222
|
lines.push(" ⚠️ 这些**没有**被检查过。若你认为其中某个其实已死(进程重启后残留的\"在跑\"标记),");
|
|
11152
11223
|
lines.push(" 请告知 ACC 维护方 —— 那是「在跑」表的判据问题,不是扫描的问题。");
|
|
11153
11224
|
}
|
|
11225
|
+
if (undecidableCount > 0) {
|
|
11226
|
+
lines.push("");
|
|
11227
|
+
lines.push(` ⚠️ **不可判定 ${undecidableCount} 个** —— 这些**既不是"命中"也不是"没事"**,是"判不出来":`);
|
|
11228
|
+
for (const u of scan.undecidable) {
|
|
11229
|
+
const at = u.lastEventAt !== null ? new Date(u.lastEventAt).toISOString() : "(时间未知)";
|
|
11230
|
+
lines.push(` - ${u.sessionId}(末事件 ${at})`);
|
|
11231
|
+
lines.push(` ${u.reason}`);
|
|
11232
|
+
}
|
|
11233
|
+
lines.push(" ⇒ 成因通常是:**一整轮长到 8 MiB 都装不下它的起点**(`turn/start` 在窗口之外)。");
|
|
11234
|
+
lines.push(" 🔴 **是否处理由你决定**(ACC 不代判)。本仓纪律:**宁可多醒不可漏** ⇒ 倾向按\"该接走\"处理。");
|
|
11235
|
+
}
|
|
11154
11236
|
if (scan.sessions.length === 0) {
|
|
11155
11237
|
lines.push("");
|
|
11156
|
-
|
|
11238
|
+
const tails = [];
|
|
11239
|
+
if (excludedCount > 0) tails.push(`另有 ${excludedCount} 个在跑,未检查`);
|
|
11240
|
+
if (undecidableCount > 0) tails.push(`另有 ${undecidableCount} 个**不可判定**`);
|
|
11241
|
+
lines.push(tails.length > 0 ? `✓ 被检查的 ${scan.scanned} 个里没有末轮未跑完的 —— ${tails.join(";")}(见上)。` : "✓ 没有末轮未跑完的会话 —— 无需处理。");
|
|
11157
11242
|
return lines.join("\n");
|
|
11158
11243
|
}
|
|
11159
11244
|
lines.push("");
|
|
@@ -11194,14 +11279,22 @@ function renderContinueResult(input) {
|
|
|
11194
11279
|
const { projectDir, lookbackHours, scan, outcomes, dryRun } = input;
|
|
11195
11280
|
const lines = [];
|
|
11196
11281
|
const excludedCount = scan.excluded.length;
|
|
11282
|
+
const undecidableCount = scan.undecidable.length;
|
|
11197
11283
|
const head = dryRun ? "续跑预告(dryRun,未注入任何消息)" : "中断续跑结果";
|
|
11198
11284
|
lines.push(`═══ ${head}(回看 ${lookbackHours}h)═══`);
|
|
11199
11285
|
lines.push(` 扫描域: ${projectDir}`);
|
|
11200
|
-
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped}` + (excludedCount > 0 ? ` · 排除在跑 ${excludedCount} 个` : "") + ` · 耗时 ${scan.elapsedMs}ms`);
|
|
11286
|
+
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped}` + (excludedCount > 0 ? ` · 排除在跑 ${excludedCount} 个` : "") + ` · 不可判定 ${undecidableCount} 个 · 耗时 ${scan.elapsedMs}ms`);
|
|
11201
11287
|
if (excludedCount > 0) lines.push(` ⓘ 另有 ${excludedCount} 个会话**此刻有轮次在跑**,未检查:${scan.excluded.join("、")}`);
|
|
11288
|
+
if (undecidableCount > 0) {
|
|
11289
|
+
lines.push(` ⚠️ 另有 ${undecidableCount} 个**不可判定**(既非同中、也非没事),**未处理**:` + scan.undecidable.map((u) => u.sessionId).join("、"));
|
|
11290
|
+
lines.push(" ⇒ 若要处理它们,须人工/调用方判断(ACC 不代判)。");
|
|
11291
|
+
}
|
|
11202
11292
|
if (scan.sessions.length === 0) {
|
|
11203
11293
|
lines.push("");
|
|
11204
|
-
|
|
11294
|
+
const tails = [];
|
|
11295
|
+
if (excludedCount > 0) tails.push(`另有 ${excludedCount} 个在跑,未续`);
|
|
11296
|
+
if (undecidableCount > 0) tails.push(`另有 ${undecidableCount} 个不可判定`);
|
|
11297
|
+
lines.push(tails.length > 0 ? `✓ 被检查的 ${scan.scanned} 个里没有末轮未跑完的 —— ${tails.join(";")}(见上)。` : "✓ 没有末轮未跑完的会话 —— 无需续跑。");
|
|
11205
11298
|
return lines.join("\n");
|
|
11206
11299
|
}
|
|
11207
11300
|
lines.push("");
|
|
@@ -56,6 +56,22 @@
|
|
|
56
56
|
export declare const ZSTD_AVAILABLE: boolean;
|
|
57
57
|
/** 默认回看窗口:8 小时(owner 2026-10-22 定) */
|
|
58
58
|
export declare const DEFAULT_LOOKBACK_MS: number;
|
|
59
|
+
/**
|
|
60
|
+
* 🔵 **`TAIL_EVENT_TARGET`(原值 400)已于 2026-10-24 A108 删除** —— 它是**猜的数**,
|
|
61
|
+
* 而猜错的代价是**漏唤醒**:实测最长单轮 482 行,某会话的 `turn/start` 落在 400 条之外
|
|
62
|
+
* **83 行** ⇒ 证据被截断 ⇒「没跑完」被读成「早就干净收尾」。
|
|
63
|
+
*
|
|
64
|
+
* ## 换成什么了
|
|
65
|
+
* 停止条件改成**「见到证据就停」**(同时见到 `turn/start` 与 `turn/end`),见
|
|
66
|
+
* {@link readTailEvents} 与 {@link TAIL_CHUNKS}。
|
|
67
|
+
*
|
|
68
|
+
* 🔴 **宿主自己的权威判据也不数条数**:`@deepseek-ai/dsh-session` 的 `openTurnClosers`
|
|
69
|
+
* **遍历全量事件**、只维护一个 `openTurn` 状态(`turn/start` 置、`turn/end` 清),
|
|
70
|
+
* 扫完 `openTurn !== null` 即"这一轮没收"。它**没有窗口**。
|
|
71
|
+
* ⚠️ 我们不照搬它,是因为它要读**全量**日志(崩溃恢复只在载入时做一次,负担得起),
|
|
72
|
+
* 而本扫描**每 5min × 全部会话** —— 照搬会烧穿。⇒ 我们取它的**结论形态**(要证据、要能说"不知道"),
|
|
73
|
+
* 不取它的**读法**(全量)。
|
|
74
|
+
*/
|
|
59
75
|
/** 日志里被我们识别的事件形状(只取用得到的字段) */
|
|
60
76
|
interface SessionEvent {
|
|
61
77
|
type?: string;
|
|
@@ -138,6 +154,27 @@ export interface ScanResult {
|
|
|
138
154
|
* 任何"我替你把某些东西藏起来"的实现,都必须报告藏了几个、藏了谁。
|
|
139
155
|
*/
|
|
140
156
|
excluded: string[];
|
|
157
|
+
/**
|
|
158
|
+
* 🔴🔴 **A108 第二半(2026-10-24):判不出来的那些。**
|
|
159
|
+
*
|
|
160
|
+
* **为什么必须单开一桶**:修前「扩到上限仍见不到轮次边界」与「确实没有未完成轮次」
|
|
161
|
+
* **返回值一模一样**(都是"没命中")⇒ **"不知道"被塌成"没中断"** ⇒ 漏唤醒。
|
|
162
|
+
*
|
|
163
|
+
* 判据:`exhausted` = 扩到 {@link TAIL_WINDOW_BYTES} 硬上限仍**没能同时见到**
|
|
164
|
+
* `turn/start` 与 `turn/end`。典型成因:**一整轮长到 8 MiB 都装不下**。
|
|
165
|
+
*
|
|
166
|
+
* ⚠️ **这两条口径不许合并**(与 `excluded` 同理,D245 同族):
|
|
167
|
+
* - `skipped` = **看过了,判定是"没事"**;
|
|
168
|
+
* - `undecidable` = **没看到足够的东西,不敢判**。
|
|
169
|
+
* ⇒ 调用方拿到 `undecidable` 非空时**必须自己决定**(本仓纪律:宁可多醒不可漏)。
|
|
170
|
+
*/
|
|
171
|
+
undecidable: Array<{
|
|
172
|
+
sessionId: string;
|
|
173
|
+
logPath: string;
|
|
174
|
+
mtimeMs: number;
|
|
175
|
+
lastEventAt: number | null;
|
|
176
|
+
reason: string;
|
|
177
|
+
}>;
|
|
141
178
|
}
|
|
142
179
|
/**
|
|
143
180
|
* 尾部窗口里**最后一条 `turn/start` 的轮号**(D244 输出面,给调用方做 A108 的现场判断)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.98.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": {
|