@shgroup/dsh-serenity-hooks 1.91.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 +1 -1
- package/lib/index.js +163 -38
- package/lib/trajectory/core.d.ts +6 -3
- package/lib/trajectory/interrupted.d.ts +41 -29
- 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.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
|
@@ -8957,10 +8957,14 @@ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
|
|
|
8957
8957
|
*
|
|
8958
8958
|
* ## 判据(三条同时成立,缺一不可)
|
|
8959
8959
|
*
|
|
8960
|
-
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind
|
|
8960
|
+
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind` ∈ {@link UNFINISHED_KINDS}
|
|
8961
|
+
* (= `interrupted` 崩溃补收尾 / `aborted` 正常终止,两者都是"这一轮没收完");
|
|
8961
8962
|
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则已被后续轮次接续);
|
|
8962
8963
|
* 3. ⛔ **不要求**末条是 `session/end-seed`(2026-10-23 放宽,原因见 {@link detectInterruptedEnd})。
|
|
8963
8964
|
*
|
|
8965
|
+
* 🔴 **2026-10-24 补入 `aborted`**:修前只认 `interrupted` ⇒「被正常终止」整类漏扫
|
|
8966
|
+
* (发布重启/换宿主最常见的形态)。判据问的是「这一轮收完了吗」,**不是"进程死得惨不惨"**。
|
|
8967
|
+
*
|
|
8964
8968
|
* ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
|
|
8965
8969
|
* (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
|
|
8966
8970
|
*
|
|
@@ -9006,10 +9010,33 @@ const ZSTD_MAGIC = Buffer.from([
|
|
|
9006
9010
|
]);
|
|
9007
9011
|
/** 默认回看窗口:8 小时(owner 2026-10-22 定) */
|
|
9008
9012
|
const DEFAULT_LOOKBACK_MS = 288e5;
|
|
9009
|
-
/**
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
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;
|
|
9013
9040
|
/**
|
|
9014
9041
|
* 从文件尾部倒序解压,凑够 `want` 个事件。
|
|
9015
9042
|
*
|
|
@@ -9053,34 +9080,59 @@ function readTailEvents(logPath, want) {
|
|
|
9053
9080
|
return events.slice(-want);
|
|
9054
9081
|
}
|
|
9055
9082
|
/**
|
|
9056
|
-
*
|
|
9083
|
+
* 判定一组尾部事件是否「以未完成的轮次结尾」。
|
|
9057
9084
|
* 不命中 → null。
|
|
9058
9085
|
*
|
|
9059
|
-
* ##
|
|
9086
|
+
* ## 🔴🔴 2026-10-24 判据放宽(owner 报障:「有的会话在重启时不是崩溃的就扫不到,还是没修好」)
|
|
9087
|
+
*
|
|
9088
|
+
* ### 漏的是什么(宿主源码实读,非推测)
|
|
9089
|
+
*
|
|
9090
|
+
* 宿主只有**两条**会写出「轮次没收完」的 `turn/end`,而它们的 `kind` **不是同一个词**:
|
|
9091
|
+
*
|
|
9092
|
+
* | 形态 | 写谁 | `reason.kind` | 修前是否命中 |
|
|
9093
|
+
* |---|---|---|---|
|
|
9094
|
+
* | 进程**崩溃**/被强杀,下一轮 resume 时补收尾 | `interruptedTurnClosers()` → `openTurnClosers(events,{kind:'interrupted'})` | **`interrupted`** | ✅ |
|
|
9095
|
+
* | 进程**正常退出**(`restart-web` 的 SIGTERM、`agent/disposed`、用户中断) | 驱动 catch 里 `abortedCancelCause(signal)` | **`aborted`** + 内层 `reason:{kind:'user'\|'parent'\|'disposed'\|'hook'}` | ❌ **漏** |
|
|
9096
|
+
*
|
|
9097
|
+
* ⇒ 修前只认 `interrupted` ⇒ **「被正常终止」这一大类全部漏掉**,而它恰恰是
|
|
9098
|
+
* **发布重启/换宿主** 最常见的形态(比崩溃常见得多)。
|
|
9060
9099
|
*
|
|
9061
|
-
*
|
|
9062
|
-
*
|
|
9063
|
-
*
|
|
9064
|
-
* · 其中满足原判据的只有 **24** 条 ⇒ **漏 2 条**,形态逐字为:
|
|
9065
|
-
* `… turn/end[interrupted] -> session/end-seed -> agent/inbox/spliced -> agent/inbox/spliced`
|
|
9066
|
-
* ⇒ 重启后宿主把这**两条**追加在 end-seed 之后(它们是"恢复时把待办消息重新塞进收件箱"),
|
|
9067
|
-
* 于是"末条必须 end-seed"这条**在恢复动作发生之后就恒不成立**。
|
|
9068
|
-
* ⚠️ 讽刺的是:**正是那些被恢复过、又再次被打断的会话**最容易长成这样
|
|
9069
|
-
* ⇒ 原判据恰好对"反复中断"的会话失明,而它们才是最需要守护的。
|
|
9100
|
+
* 🔴 **这两条是同一个状态的两种成因,不是"一个对一个不对"**:
|
|
9101
|
+
* 判据问的是「**这一轮有没有收完**」,不是「进程死得惨不惨」。
|
|
9102
|
+
* 拿死法当判据 ⇒ 换一种死法就漏一批(这正是 owner 反复看到的"还是没修好")。
|
|
9070
9103
|
*
|
|
9071
|
-
* ###
|
|
9072
|
-
*
|
|
9073
|
-
*
|
|
9074
|
-
*
|
|
9104
|
+
* ### 为什么不能只加 `aborted` 就完事
|
|
9105
|
+
* `aborted.reason.kind` 里还可能有 `hook`(宿主主动 hook 取消)——它同属"没跑完"。
|
|
9106
|
+
* ⇒ 本判据**只看外层 `kind`**,理由:外层已经回答了"这一轮收完了吗",
|
|
9107
|
+
* 内层回答的是"谁让它停的",**那是另一个问题**,混进来会让判据随"谁取消的"漂移。
|
|
9075
9108
|
*
|
|
9076
|
-
* ###
|
|
9077
|
-
*
|
|
9078
|
-
*
|
|
9079
|
-
*
|
|
9109
|
+
* ### ⛔ 刻意**不**纳入的形态(保守边界,写明理由)
|
|
9110
|
+
* - `error`:本轮**收完了**(宿主记了错误收尾),它不是"停在这里等人续";
|
|
9111
|
+
* - 完全没有 `turn/end` 的开轮(例如日志末尾是 `turn/start` 后再无事件):
|
|
9112
|
+
* 那**也可能是"进程正活着、这一轮正在跑"**,与真中断在读数上不可分
|
|
9113
|
+
* ⇒ 纳入会误唤醒在跑的会话。⚠️ 这是**已知边界**,不是遗漏。
|
|
9080
9114
|
*
|
|
9081
9115
|
* ### 为什么不会误报"曾经中断但已恢复"
|
|
9082
9116
|
* 恢复一定伴随 `turn/start`(新一轮)或新的 `turn/end`(`completed` 等)⇒ 判据 1/2 各自挡住。
|
|
9083
|
-
|
|
9117
|
+
*/
|
|
9118
|
+
/**
|
|
9119
|
+
* 「这一轮没跑完」的 `turn/end.reason.kind` 取值域。
|
|
9120
|
+
*
|
|
9121
|
+
* 🔴🔴 **2026-10-24(owner 报障)**:原判据只认 `'interrupted'` 一个词 ⇒
|
|
9122
|
+
* **「被正常终止」整类漏扫**。两个值对应的形态见 {@link detectInterruptedEnd} 的头注。
|
|
9123
|
+
*
|
|
9124
|
+
* ⚠️ **这是判据的唯一取值点** —— 别在别处再写一份字面量(两份会漂)。
|
|
9125
|
+
*/
|
|
9126
|
+
const UNFINISHED_KINDS = /* @__PURE__ */ new Set(["interrupted", "aborted"]);
|
|
9127
|
+
/**
|
|
9128
|
+
* 命中判定(**形态 A:靠 `turn/end.reason.kind`**)。
|
|
9129
|
+
*
|
|
9130
|
+
* ⚠️ **本函数刻意不判「开了没收」** —— 那种形态与「此刻正在跑」在日志读数上**不可分**,
|
|
9131
|
+
* 只有扫描层手里有 `liveIds`(正在跑的会话 id)才判得准。
|
|
9132
|
+
* ⇒ 分工:**本函数判"收尾原因",`scanInterruptedSessions` 判"开了没收"**(见 `detectUnclosedTurn`)。
|
|
9133
|
+
* ⛔ 不要把这些情形塞进本函数 —— 会让它同时依赖"日志形态"与"进程状态",那种判据无法单独证伪。
|
|
9134
|
+
*
|
|
9135
|
+
* @returns 命中时给出被中断的轮次与"可能有副作用"的工具名;不命中 null
|
|
9084
9136
|
*/
|
|
9085
9137
|
function detectInterruptedEnd(events) {
|
|
9086
9138
|
if (events.length === 0) return null;
|
|
@@ -9091,9 +9143,65 @@ function detectInterruptedEnd(events) {
|
|
|
9091
9143
|
}
|
|
9092
9144
|
if (idx < 0) return null;
|
|
9093
9145
|
const turnEnd = events[idx];
|
|
9094
|
-
|
|
9146
|
+
const kind = turnEnd?.data?.reason?.kind;
|
|
9147
|
+
if (typeof kind !== "string" || !UNFINISHED_KINDS.has(kind)) return null;
|
|
9095
9148
|
for (let i = idx + 1; i < events.length; i++) if (events[i]?.type === "turn/start") return null;
|
|
9096
|
-
const turn = typeof turnEnd
|
|
9149
|
+
const turn = typeof turnEnd?.data?.turn === "number" ? turnEnd.data.turn : null;
|
|
9150
|
+
return {
|
|
9151
|
+
turn,
|
|
9152
|
+
pendingTools: findPendingTools(events, turn)
|
|
9153
|
+
};
|
|
9154
|
+
}
|
|
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;
|
|
9097
9205
|
return {
|
|
9098
9206
|
turn,
|
|
9099
9207
|
pendingTools: findPendingTools(events, turn)
|
|
@@ -9171,7 +9279,22 @@ function scanInterruptedSessions(projectDir, options = {}) {
|
|
|
9171
9279
|
result.scanned++;
|
|
9172
9280
|
try {
|
|
9173
9281
|
const events = readTailEvents(logPath, TAIL_EVENT_TARGET);
|
|
9174
|
-
|
|
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);
|
|
9175
9298
|
if (!hit) {
|
|
9176
9299
|
result.skipped++;
|
|
9177
9300
|
continue;
|
|
@@ -10935,12 +11058,12 @@ function collectRunningSessionIds(nowMs) {
|
|
|
10935
11058
|
function renderInterruptedScan(input) {
|
|
10936
11059
|
const { projectDir, lookbackHours, scan } = input;
|
|
10937
11060
|
const lines = [];
|
|
10938
|
-
lines.push(`═══
|
|
11061
|
+
lines.push(`═══ 末轮未跑完的 dsh 会话(回看 ${lookbackHours}h)═══`);
|
|
10939
11062
|
lines.push(` 扫描域: ${projectDir}`);
|
|
10940
11063
|
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped} · 耗时 ${scan.elapsedMs}ms${scan.errors.length > 0 ? ` · 读取失败 ${scan.errors.length}` : ""}`);
|
|
10941
11064
|
if (scan.sessions.length === 0) {
|
|
10942
11065
|
lines.push("");
|
|
10943
|
-
lines.push("✓
|
|
11066
|
+
lines.push("✓ 没有末轮未跑完的会话 —— 无需处理。");
|
|
10944
11067
|
return lines.join("\n");
|
|
10945
11068
|
}
|
|
10946
11069
|
lines.push("");
|
|
@@ -10951,7 +11074,9 @@ function renderInterruptedScan(input) {
|
|
|
10951
11074
|
if (s.pendingTools.length > 0) lines.push(` ⚠️ 有调用无结果的工具(副作用未知): ${s.pendingTools.join(", ")}`);
|
|
10952
11075
|
}
|
|
10953
11076
|
lines.push("");
|
|
10954
|
-
lines.push("判据: 日志末条
|
|
11077
|
+
lines.push("判据: 日志末条 `turn/end` 的 `reason.kind` ∈ {interrupted, aborted},且其后没有新的 turn/start。");
|
|
11078
|
+
lines.push(" interrupted = 进程崩溃后被 resume 补收尾 / aborted = 被正常终止(重启、销毁、用户中断)。");
|
|
11079
|
+
lines.push(" 两者都表示「这一轮没跑完」,都会被收进来。");
|
|
10955
11080
|
lines.push("🔴 本动作**只读** —— 是否续跑、续哪条、几点续,由调用方(你的认知轨迹)决定。");
|
|
10956
11081
|
lines.push("⚠️ 未完成的工具调用「副作用未知」≠「没执行」:它们可能已写文件 / 已发消息。");
|
|
10957
11082
|
lines.push(" 续跑前应先核对副作用,而不是把那一轮整段重放。");
|
|
@@ -10977,7 +11102,7 @@ function renderContinueResult(input) {
|
|
|
10977
11102
|
lines.push(` 检查 ${scan.scanned} 个 · 命中 ${scan.sessions.length} · 跳过 ${scan.skipped} · 耗时 ${scan.elapsedMs}ms`);
|
|
10978
11103
|
if (scan.sessions.length === 0) {
|
|
10979
11104
|
lines.push("");
|
|
10980
|
-
lines.push("✓
|
|
11105
|
+
lines.push("✓ 没有末轮未跑完的会话 —— 无需续跑。");
|
|
10981
11106
|
return lines.join("\n");
|
|
10982
11107
|
}
|
|
10983
11108
|
lines.push("");
|
|
@@ -11126,7 +11251,7 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
11126
11251
|
"list (inventory + stats) / show (read one SESSION.md) / create (make a NEW trajectory) / use (bind THIS conversation) / rebuild (clear-and-rebuild THIS conversation in place) / archive (move a trajectory out of the active list; one-way, never overwrites).",
|
|
11127
11252
|
"(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged: send-now (right now) / send-later (one future instant).",
|
|
11128
11253
|
"(C) REFERENCE — cro-guide: how to give a trajectory its own program that decides when it should be woken.",
|
|
11129
|
-
"(D) INSPECTION — interrupted: list dsh sessions whose LAST turn
|
|
11254
|
+
"(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).",
|
|
11130
11255
|
"HOW TO CHOOSE (a rule, not a guess):",
|
|
11131
11256
|
"not yet bound + starting new work ⇒ create (with no prior binding it activates immediately, so the new trajectory has a carrier at once).",
|
|
11132
11257
|
"already bound + recording a SEPARATE future line ⇒ create (it does NOT steal your binding; activate the new one later by send-now).",
|
|
@@ -11422,16 +11547,16 @@ function createCognitiveTrajectoryTool(ctx) {
|
|
|
11422
11547
|
return listResident(root);
|
|
11423
11548
|
case "interrupted": {
|
|
11424
11549
|
/**
|
|
11425
|
-
*
|
|
11550
|
+
* 以未完成轮次结尾的 dsh 会话(owner 2026-10-22 令:**只出入口**)。
|
|
11426
11551
|
*
|
|
11427
11552
|
* 🔴 **边界(owner 原话:「我们 ACC 不关心周期」)**:本动作**只回答**"此刻哪些会话
|
|
11428
|
-
*
|
|
11553
|
+
* 的最后一轮没跑完",**不判周期、不自动续跑、不改日志**。
|
|
11429
11554
|
* 何时扫、续谁、怎么续 = 调用方(CCC 的认知轨迹)的事。
|
|
11430
11555
|
*
|
|
11431
11556
|
* 🔴 **为什么不是"曾经中断过"**:长会话历史上可中断几十次(实测单会话 33 次);
|
|
11432
|
-
* 按"曾经"判会把早已恢复的会话全部当成待续 ⇒
|
|
11433
|
-
*
|
|
11434
|
-
*
|
|
11557
|
+
* 按"曾经"判会把早已恢复的会话全部当成待续 ⇒ 灾难。
|
|
11558
|
+
* 🔴 **判据只在一处**:`trajectory/interrupted.ts` 的 `detectInterruptedEnd`
|
|
11559
|
+
* (⛔ 别在此处复述取值域 —— 2026-10-24 就是这份复述漏掉了 `aborted`)。
|
|
11435
11560
|
*
|
|
11436
11561
|
* 🔴 **正在跑的会话要排除**:live 会话不该被判成"待续"(它正在跑,不是中断)。
|
|
11437
11562
|
*/
|
package/lib/trajectory/core.d.ts
CHANGED
|
@@ -63,12 +63,15 @@
|
|
|
63
63
|
* 判据:它管的是**被找到**,不是**不被销毁** ⇒ 纯读、不碰生命周期、不进系统提示词。
|
|
64
64
|
* ⚠️ 这也修正了 v1.77.1 的一处说明面失真(手册曾描述一个**不存在**的动作,D179(a))。
|
|
65
65
|
* 2026-10-22(owner 令「在 trajectory 加上触发入口即可,剩下的 CCC 搞定」):新增
|
|
66
|
-
* **`interrupted`** ——
|
|
66
|
+
* **`interrupted`** —— 列出**末轮未跑完**的 dsh 会话(进程在轮次完成前退出)。
|
|
67
67
|
* 判据(R↓,为何 ACC 只出**入口**不管周期):**周期性归属调用方的认知轨迹**,
|
|
68
|
-
* ACC 只回答"
|
|
68
|
+
* ACC 只回答"此刻哪些会话的最后一轮没跑完"这一个问题。⇒ 本动作**纯读**、
|
|
69
69
|
* **不判周期、不自动续跑、不改任何会话日志**(是否续、续谁、几点续 = CCC 的事)。
|
|
70
|
-
* ⚠️ 判据的锋利处:**只认"
|
|
70
|
+
* ⚠️ 判据的锋利处:**只认"末轮未跑完"**,不认"曾经中断过"—— 长会话历史上
|
|
71
71
|
* 可中断几十次(实测单会话 33 次),按"曾经"判会把早已恢复的会话全部当成待续。
|
|
72
|
+
* 🔴 **2026-10-24(owner 报障「不是崩溃的就扫不到」)**:"未跑完"有两种宿主写法 ——
|
|
73
|
+
* `interrupted`(崩溃后 resume 补收尾)与 `aborted`(被正常终止:重启/销毁/用户中断)。
|
|
74
|
+
* 修前只认前者 ⇒ **发布重启这一类整批漏扫**。取值域只在 `trajectory/interrupted.ts` 一处定义。
|
|
72
75
|
* 2026-10-22(owner 追加令「我希望实现的是 continue-all-interrupted 方法,调用后自动找到
|
|
73
76
|
* 8h 内所有 dsh 会话注入简单的 continue 进行唤醒,这样 CCC 就不用动脑子了」):
|
|
74
77
|
* 新增 **`continue-all-interrupted`** = `interrupted` 的**动作版**(扫 → 逐条注入续跑消息)。
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ## 判据(三条同时成立,缺一不可)
|
|
19
19
|
*
|
|
20
|
-
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind
|
|
20
|
+
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind` ∈ {@link UNFINISHED_KINDS}
|
|
21
|
+
* (= `interrupted` 崩溃补收尾 / `aborted` 正常终止,两者都是"这一轮没收完");
|
|
21
22
|
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则已被后续轮次接续);
|
|
22
23
|
* 3. ⛔ **不要求**末条是 `session/end-seed`(2026-10-23 放宽,原因见 {@link detectInterruptedEnd})。
|
|
23
24
|
*
|
|
25
|
+
* 🔴 **2026-10-24 补入 `aborted`**:修前只认 `interrupted` ⇒「被正常终止」整类漏扫
|
|
26
|
+
* (发布重启/换宿主最常见的形态)。判据问的是「这一轮收完了吗」,**不是"进程死得惨不惨"**。
|
|
27
|
+
*
|
|
24
28
|
* ⚠️ **为什么不能只看"存在过 interrupted"**:一个长会话历史上可能中断几十次
|
|
25
29
|
* (本容器实测单会话 33 次)。只看"曾经中断"会把早已恢复的会话全部当成待续 ⇒ 灾难。
|
|
26
30
|
*
|
|
@@ -96,39 +100,47 @@ export interface ScanResult {
|
|
|
96
100
|
elapsedMs: number;
|
|
97
101
|
}
|
|
98
102
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* · 其中满足原判据的只有 **24** 条 ⇒ **漏 2 条**,形态逐字为:
|
|
108
|
-
* `… turn/end[interrupted] -> session/end-seed -> agent/inbox/spliced -> agent/inbox/spliced`
|
|
109
|
-
* ⇒ 重启后宿主把这**两条**追加在 end-seed 之后(它们是"恢复时把待办消息重新塞进收件箱"),
|
|
110
|
-
* 于是"末条必须 end-seed"这条**在恢复动作发生之后就恒不成立**。
|
|
111
|
-
* ⚠️ 讽刺的是:**正是那些被恢复过、又再次被打断的会话**最容易长成这样
|
|
112
|
-
* ⇒ 原判据恰好对"反复中断"的会话失明,而它们才是最需要守护的。
|
|
113
|
-
*
|
|
114
|
-
* ### 新判据(三条,缺一不可)
|
|
115
|
-
* 1. 日志里**最后一条 `turn/end`** 的 `reason.kind === 'interrupted'`;
|
|
116
|
-
* 2. 该 `turn/end` **之后没有新的 `turn/start`**(否则说明它已被后续轮次接续);
|
|
117
|
-
* 3. ⛔ **不再要求末条是 `end-seed`**。
|
|
118
|
-
*
|
|
119
|
-
* ### 为什么不改成"末条必须是某个白名单"
|
|
120
|
-
* 那就是原缺陷换个白名单再犯一次(`agent/inbox/spliced` 之外还会有别的)。
|
|
121
|
-
* **判据要问的是"最后一轮的结束原因是什么、之后有没有新轮开始"**,
|
|
122
|
-
* 而不是"日志最后一个 token 长什么样"——后者是**载体形态**,前者是**状态**。
|
|
123
|
-
*
|
|
124
|
-
* ### 为什么不会误报"曾经中断但已恢复"
|
|
125
|
-
* 恢复一定伴随 `turn/start`(新一轮)或新的 `turn/end`(`completed` 等)⇒ 判据 1/2 各自挡住。
|
|
126
|
-
* 实测该放宽**不新增误报**:原 24 条全含于新判据(`curOnly = 0`),新增 2 条全是真漏。
|
|
103
|
+
* 命中判定(**形态 A:靠 `turn/end.reason.kind`**)。
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ **本函数刻意不判「开了没收」** —— 那种形态与「此刻正在跑」在日志读数上**不可分**,
|
|
106
|
+
* 只有扫描层手里有 `liveIds`(正在跑的会话 id)才判得准。
|
|
107
|
+
* ⇒ 分工:**本函数判"收尾原因",`scanInterruptedSessions` 判"开了没收"**(见 `detectUnclosedTurn`)。
|
|
108
|
+
* ⛔ 不要把这些情形塞进本函数 —— 会让它同时依赖"日志形态"与"进程状态",那种判据无法单独证伪。
|
|
109
|
+
*
|
|
110
|
+
* @returns 命中时给出被中断的轮次与"可能有副作用"的工具名;不命中 null
|
|
127
111
|
*/
|
|
128
112
|
export declare function detectInterruptedEnd(events: SessionEvent[]): {
|
|
129
113
|
turn: number | null;
|
|
130
114
|
pendingTools: string[];
|
|
131
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;
|
|
132
144
|
/**
|
|
133
145
|
* 扫描一个 **project 目录**(形如 `<sessions-root>/--home-yh-home-home-serenity--/`)。
|
|
134
146
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
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": {
|