@shgroup/dsh-serenity-hooks 1.97.0 → 1.99.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 +163 -95
- 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.99.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("");
|
|
@@ -11336,43 +11429,18 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
11336
11429
|
return defineTool({
|
|
11337
11430
|
name: "cognitive_trajectory",
|
|
11338
11431
|
description: [
|
|
11339
|
-
"A
|
|
11340
|
-
"
|
|
11341
|
-
"
|
|
11342
|
-
"
|
|
11343
|
-
"
|
|
11344
|
-
"
|
|
11345
|
-
"
|
|
11346
|
-
"
|
|
11347
|
-
"
|
|
11348
|
-
"
|
|
11349
|
-
"
|
|
11350
|
-
|
|
11351
|
-
"(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged: send-now (right now) / send-later (one future instant).",
|
|
11352
|
-
"(C) REFERENCE — cro-guide: how to give a trajectory its own program that decides when it should be woken.",
|
|
11353
|
-
"(D) INSPECTION — interrupted: list dsh sessions whose LAST turn did not finish — reason.kind is \"interrupted\" (process died; the host synthesised the closer on resume) OR \"aborted\" (terminated normally: restart, dispose, user cancel). Both mean \"this turn did not complete\"; picking only one misses an entire class. PURE READ: no periodic scheduling, no auto-continue, no log mutation — deciding IF/WHEN/WHICH to resume belongs to the calling trajectory (this is the \"entry point only\" contract). The criterion is EXACTLY \"the last turn did not finish\", NOT \"was ever interrupted\": one long session may have been interrupted dozens of times historically, and treating those as pending would resurrect sessions that already recovered. Each hit reports the turn number and the tool names that had a call but no result (best-effort — those side effects are UNKNOWN, so re-running them blindly is dangerous).",
|
|
11354
|
-
"HOW TO CHOOSE (a rule, not a guess):",
|
|
11355
|
-
"not yet bound + starting new work ⇒ create (with no prior binding it activates immediately, so the new trajectory has a carrier at once).",
|
|
11356
|
-
"already bound + recording a SEPARATE future line ⇒ create (it does NOT steal your binding; activate the new one later by send-now).",
|
|
11357
|
-
"want THIS conversation to move ⇒ use. It refuses if this session already carries another trajectory (IMMUTABLE 1) — that refusal is the design, not an error to work around.",
|
|
11358
|
-
"want to make another trajectory act ⇒ send-now. The target needs no prior setup: if it has no live session, one is created and bound automatically, and your message is injected.",
|
|
11359
|
-
"RECEIPTS (read no more than is written): send-now returns a synchronous receipt meaning exactly \"injected / turn started\" — NEVER \"the target ran or replied\". To prove the target actually moved, read its SESSION.md.",
|
|
11360
|
-
"send-now never queues behind the current turn: a busy target is steered mid-turn (read in the CURRENT turn), an idle target starts a turn at once.",
|
|
11361
|
-
"send-later is fire-and-forget by design: no receipt, no recall, no panel.",
|
|
11362
|
-
"ASYMMETRY (deliberate): send-now may create a missing carrier; send-later NEVER does. Scheduling for a trajectory that never existed is almost always a mistyped name, and auto-creating would hide that error.",
|
|
11363
|
-
"CARRIER GUARD: send-now auto-creates a carrier ONLY for a trajectory that has never had one. If a declaration exists but its session is gone, it refuses and says so — one trajectory keeps one carrier. Two independent gates can refuse: (a) the declared session cannot be resolved at all; (b) it existed and is now gone.",
|
|
11364
|
-
"PARAMETERS: create needs --desc <desc> [--id <code>] [--goal], OR --issue <ticket> (exactly one), PLUS --summary (≤20 chars).",
|
|
11365
|
-
"use needs --name + --summary (≤20 chars). --force is ACCEPTED BUT REFUSED (retired): it exists only to explain why.",
|
|
11366
|
-
"rebuild needs --summary (next-phase, ≤20 chars) + optional --note (≤200 chars, task focus for the rebuilt self).",
|
|
11367
|
-
"send-now needs target + message. send-later needs target + at (RFC3339 with timezone, or +30m / +2h) + message.",
|
|
11368
|
-
"target/name accept a code (S### or custom), a full AGENT_SESSIONS directory name, or a fuzzy keyword.",
|
|
11369
|
-
"AUTHORIZATION (read before creating anything): create and send-now both cost real work and must serve a human intent. Call create only when the user asked for a new line of work (or confirmed it) — never to \"tidy up\" or pre-provision what nobody requested. When in doubt, ask instead of creating. This is a behavioral rule, not a mechanical gate: treat it as etiquette with a cost.",
|
|
11370
|
-
"uid: READS a legacy TRAJECTORY-UID only. Setting one is retired — a trajectory's code is its identity now; use create --id <code>.",
|
|
11371
|
-
"summary is appended to the dsh session title as <code>-<date>-<summary>; the code and date stay server-derived unless you pass --id.",
|
|
11372
|
-
"SELF-DECLARATION MANUAL — how to write your own charter (PROMPT.md), declare yourself resident",
|
|
11373
|
-
"(trajectory.json `resident.serves`), or restrict yourself (tool/model whitelists):",
|
|
11374
|
-
"container_admin config docs trajectory-self. Read it before changing any file in your own directory."
|
|
11375
|
-
].join(" "),
|
|
11432
|
+
"A cognitive trajectory is a durable body — its SESSION.md — that outlives every conversation carrying it.",
|
|
11433
|
+
"Three axioms hold. The trajectory is the subject and the conversation is only its carrier, so rebuilding the carrier leaves SESSION.md byte for byte identical. A trajectory has no lifecycle: it never completes, closes, or expires, and its age never counts against it — only where it is stored changes, so archiving removes it from the active list without touching its directory. And identity is the code, the fixed `YYYY-MM-DD--<code>[--<desc>]` segment of the directory name, never the summary or the title; create --id chooses it.",
|
|
11434
|
+
"What this tool never does: it never deletes a trajectory, never edits the content of SESSION.md, and never schedules recurring work, because that belongs to cro-guide.",
|
|
11435
|
+
"A trajectory is a directory under AGENT_SESSIONS/ holding a SESSION.md, a carrier is a dsh session bound to one, and the binding declaration is the file current.dsh_session_bind inside the trajectory: one file per trajectory, a single slot, never a table.",
|
|
11436
|
+
"Three rules are immutable. A session belongs to at most one trajectory for its whole life, so use refuses a session already carrying another, and there is no --force override — that flag was retired on 2026-10-20 and now only explains why; to give another trajectory a carrier, open a new session. A trajectory has at most one current carrier, and the binding declaration is the only truth about which, so other open sessions are simply unbound; never infer a binding from a session looking active. A declaration whose session no longer exists counts as unbound, and no cleanup is required, because a stale declaration deliberately records who carried the trajectory last.",
|
|
11437
|
+
"Actions are grouped by effect, and the grouping answers one question: would this change what I am bound to? The lifecycle group does change this conversation's binding — list, show, create, use, rebuild, and archive, where archiving is one-way, never overwrites, and leaves content untouched. The delivery group sends one message to another trajectory and leaves your own binding alone — send-now and send-later. One action is a reference rather than an operation: cro-guide explains how to give a trajectory its own program deciding when it should be woken. The inspection action is read-only: interrupted returns the dsh sessions whose last turn did not finish. A turn is unfinished when its reason.kind is \"interrupted\", meaning the process died, or \"aborted\", meaning it ended normally through a restart, a dispose, or a cancel; both mean the turn did not complete, so taking only one would miss a whole class. It schedules nothing, continues nothing, and mutates no log — deciding whether and when to resume belongs to the caller. The criterion is exactly \"the last turn did not finish\", never \"was ever interrupted\", since one long session may have been interrupted dozens of times and already recovered. Each hit also names the turn number and the tools that made a call without a result, but those side effects are unknown, so do not re-run them blindly.",
|
|
11438
|
+
"Choose by rule, not by guess. Not yet bound and starting new work: create, which activates itself at once when there is no prior binding. Already bound and recording a separate future line: also create, since that leaves your binding intact and you can activate it later with send-now. Want this conversation to move: use, and treat its refusal — when the session already carries another trajectory — as the design rather than an error to work around. Want another trajectory to act: send-now, whose target needs no prior setup, since a live session is created, bound, and injected when none exists.",
|
|
11439
|
+
"Read a receipt for no more than it says. send-now returns a synchronous receipt meaning the message was injected and the turn started; it never means the target ran or replied, and to prove the target moved you must read its SESSION.md. send-now never queues behind the current turn — a busy target is steered mid-turn while an idle one starts at once. send-later is fire-and-forget by design, with no receipt and no recall.",
|
|
11440
|
+
"The asymmetry is deliberate: send-now may create a missing carrier, send-later never does, because scheduling work for a trajectory that never existed is almost always a mistyped name. A carrier guard applies to send-now: it creates a carrier automatically only for a trajectory that has never had one, and otherwise refuses, either because a declaration exists but its session is gone or because the declared session cannot be resolved at all — one trajectory keeps one carrier.",
|
|
11441
|
+
"For parameters: create needs --desc with optional --id and --goal, or --issue instead, exactly one of those two forms, plus --summary of at most 20 characters. use needs --name and --summary. rebuild needs --summary naming the next phase in at most 20 characters, plus an optional --note of at most 200 characters stating the task focus for the rebuilt self. send-now needs a target and a message; send-later needs a target, an instant as RFC 3339 with a timezone or a relative form like +30m or +2h, and a message. A target or name may be a code such as S### or a custom one, a full AGENT_SESSIONS directory name, or a fuzzy keyword. --force is still accepted but always refused, being retired and existing only to explain why. uid reads a legacy TRAJECTORY-UID only, since setting one is retired and the code is now the identity. summary is appended to the dsh session title alongside the code and date, which stay server-derived unless you pass --id.",
|
|
11442
|
+
"Two rules of conduct, not operations. create and send-now both cost real work and must serve a human intent, so call create only when the user asked for a new line of work or confirmed one — never to tidy up and never to pre-provision what nobody requested; when in doubt, ask. That is etiquette with a cost rather than a mechanical gate. And to describe yourself rather than operate: your own charter in PROMPT.md, a resident declaration through resident.serves in trajectory.json, and self-restriction by tool and model whitelists all live in container_admin config docs trajectory-self — read that chapter before changing any file in your own directory."
|
|
11443
|
+
].join("\n\n"),
|
|
11376
11444
|
parameters: {
|
|
11377
11445
|
action: {
|
|
11378
11446
|
type: "string",
|
|
@@ -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.99.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": {
|