@shgroup/dsh-serenity-hooks 1.94.0 → 1.96.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/cro/turns.d.ts +65 -12
- package/lib/index.js +112 -29
- package/lib/trajectory/interrupted.d.ts +39 -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.96.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/cro/turns.d.ts
CHANGED
|
@@ -8,14 +8,64 @@
|
|
|
8
8
|
* 调度器只掌握**会话 live 与否**,而 **`live ≠ 在跑`**——
|
|
9
9
|
* 一条会话可以 live 而空闲(本会话此刻就是活的反例)。
|
|
10
10
|
*
|
|
11
|
-
* ##
|
|
11
|
+
* ## 怎么夹出来(用**既有的**契约事件,不需要新宿主能力)
|
|
12
12
|
*
|
|
13
13
|
* | 事件 | 动作 | 出处 |
|
|
14
14
|
* |---|---|---|
|
|
15
|
-
* | `agent/
|
|
16
|
-
* | `agent/
|
|
17
|
-
* | `agent/disposed` | **清该载体的项** | `host/contract.ts
|
|
18
|
-
* | `session/disposed` | **清该载体的项** | `host/contract.ts
|
|
15
|
+
* | `agent/status` = `running` | **置位**「在跑」 | 宿主 `AgentLoop.setPhase`:`emit("agent/status", { status })`,**只在状态真的变化时发** |
|
|
16
|
+
* | `agent/status` = `idle` | **清位** | 同上;`kick()` 的 `finally` 一定 `setPhase({kind:'idle'})` |
|
|
17
|
+
* | `agent/disposed` | **清该载体的项** | `host/contract.ts` |
|
|
18
|
+
* | `session/disposed` | **清该载体的项** | `host/contract.ts` |
|
|
19
|
+
*
|
|
20
|
+
* 🔴 **`agent/turn-stopping` 刻意不订阅** —— 它是「轮末暂停点」不是「轮结束」,见下方 D244 段。
|
|
21
|
+
*
|
|
22
|
+
* ## 🔴🔴 D243(2026-10-24):**⛔ 不许用 `agent/created` 标记「在跑」**
|
|
23
|
+
*
|
|
24
|
+
* **修前**:`agent/created` 也 `markTurnRunning(...)`。**为什么这是错的** —— 该事件是
|
|
25
|
+
* `AgentRegistry.announce(agent, source)` 发的,而 `source` 明确包含 **`resume`**:
|
|
26
|
+
* 宿主**每次重启**都会把 persisted 会话重新载入并 announce ⇒ 每个被**载入**的会话都被打成"在跑",
|
|
27
|
+
* 而**它那一轮早随旧进程死掉了** ⇒ `turn-stopping` 永不到达 ⇒ 该假标记活满 `TURN_TTL_MS`(30 min)。
|
|
28
|
+
*
|
|
29
|
+
* **代价(两个面,都已实测)**:
|
|
30
|
+
* - **面一**:CRO 守卫拿它否决唤起 ⇒ **重启后聋 30 分钟**(guard 已在自己域绕开)。
|
|
31
|
+
* - **面二**:`cognitive_trajectory interrupted` 拿它当排除集 ⇒ **把最该被接走的会话藏起来**
|
|
32
|
+
* (实测:判据命中 5 条,生产入口报「检查 5 · 命中 0」)。
|
|
33
|
+
*
|
|
34
|
+
* **判据**:`agent/created` 证明的是「**载体被发布了**」,**不是**「有轮次在跑」。
|
|
35
|
+
* 「载入」与「在跑」互不蕴含(与 D225 同族:*字段名/字面形态不是语义*)。
|
|
36
|
+
* ⇒ **唯一权威 = `agent/status`**(宿主 `status` getter:`phase.kind === 'idle' | 'maintenance'`
|
|
37
|
+
* 才是 idle,其余皆 running),且它在**每次真实状态跃迁**时发 —— 正是我们要的粒度。
|
|
38
|
+
*
|
|
39
|
+
* **代价与兜底**:删掉 `agent/created` 后,本表在"宿主重启后、某会话真正开跑之前"为空
|
|
40
|
+
* ⇒ 落到文件尾那条**便宜**的误判方向(报"没跑"),不会被拿来藏会话。
|
|
41
|
+
*
|
|
42
|
+
* ## 🔴🔴 D244(2026-10-24):**⛔ `agent/turn-stopping` 不得清表**
|
|
43
|
+
*
|
|
44
|
+
* **它看起来最像"收轮",其实不是。** 宿主 `AgentLoop.turn()` 原文:
|
|
45
|
+
*
|
|
46
|
+
* ```js
|
|
47
|
+
* if (turnEnds && this.inbox.nextStep.length === 0) {
|
|
48
|
+
* await this.dispatch.serial("agent/turn-stopping", { turn, signal }) // 暂停点
|
|
49
|
+
* signal.throwIfAborted()
|
|
50
|
+
* }
|
|
51
|
+
* if (turnEnds && this.inbox.nextStep.length === 0) break // 只有 nextStep 空才真结束
|
|
52
|
+
* target = "next-step" // 否则同轮继续
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* ⇒ 任何监听者在 `turn-stopping` 回调里 `steer`(往 `next-step` 塞消息)⇒ **同一轮继续跑**,
|
|
56
|
+
* 而 `turn/start` **不重发**、`agent/status` 也**不重发**(状态没变,还是 running)。
|
|
57
|
+
*
|
|
58
|
+
* 🔴 **实测代价**(真机 2026-10-24 18:1x):`trajectory/rebuild.ts` 的 auto-continue 正是
|
|
59
|
+
* 在 `turn-stopping` 里 `agent.steer(...)` ⇒ 一条轮**连跑 9 分钟**
|
|
60
|
+
* (`turn/start(32)` 18:12:00 → `turn/end(32)` 18:21:27),而本表**整段时间都是空的**
|
|
61
|
+
* ⇒ `interrupted` 的排除失效 ⇒ `steer` 被塞进**正在跑的那一轮**(对它是误导)。
|
|
62
|
+
* ⚠️ D243 之前这一格被 `agent/created` 的假置位**掩盖**;D243 删掉它 ⇒ 洞才暴露。
|
|
63
|
+
* ⇒ **D243 的方向是对的,它只是揭开了一个更老的语义误解**。
|
|
64
|
+
*
|
|
65
|
+
* ⇒ **清理只认 `agent/status = idle`**(`kick()` 的 `finally` 一定 `setPhase({kind:'idle'})`)。
|
|
66
|
+
* ⚠️ **已知窄代价**:`runMaintenance` 期间 `status` 也是 `idle` ⇒ 压缩等内部工作时不置位。
|
|
67
|
+
* 判可接受:那时没有 CRO 该救的轮;方向落到那条**便宜**的误判(报"没跑")。
|
|
68
|
+
* ⛔ **不要把 `clearTurnRunning` 加回 `turn-stopping`** —— 等于重新打开那个 9 分钟的洞。
|
|
19
69
|
*
|
|
20
70
|
* ## 🔴 清理设计(**这是本模块最容易被做坏的地方**)
|
|
21
71
|
*
|
|
@@ -23,12 +73,12 @@
|
|
|
23
73
|
* 而 `agent/disposed` 在 `HOST_EVENTS` 里的 impact 原文**恰好就是**
|
|
24
74
|
* 「*per-会话内存态不清理(**长跑泄漏**)*」⇒ **本模块属于该已登记病族,必须在设计期就带清理**。
|
|
25
75
|
*
|
|
26
|
-
*
|
|
27
|
-
* 1. **正常路径**:`
|
|
76
|
+
* **三层清理**(D244 起第 1 层已从 `turn-stopping` 换成 `status=idle`):
|
|
77
|
+
* 1. **正常路径**:`agent/status = idle` ⇒ 清除该项;
|
|
28
78
|
* 2. **载体销毁**:`agent/disposed` / `session/disposed` ⇒ 清除该项;
|
|
29
79
|
* 3. 🔴 **异常路径(前两层都漏掉的兜底)**:读取时按 **TTL** 过滤——
|
|
30
|
-
* 超过 `TURN_TTL_MS`
|
|
31
|
-
* 触发场景:进程被 kill / 卡死 /
|
|
80
|
+
* 超过 `TURN_TTL_MS` 未刷新的项**视为不在跑**(并顺手清除)。
|
|
81
|
+
* 触发场景:进程被 kill / 卡死 / 事件丢失(这些情况下 `idle` 永远不会来)。
|
|
32
82
|
*
|
|
33
83
|
* ## TTL 取值的**方向性判据**(R↓,别用"感觉合适")
|
|
34
84
|
*
|
|
@@ -38,7 +88,7 @@
|
|
|
38
88
|
* 「漏排是硬故障、多排是常态成本」)。
|
|
39
89
|
*
|
|
40
90
|
* ⇒ **不确定时宁可报"没跑"**(选便宜的那种错)⇒ **TTL 不宜过长**。
|
|
41
|
-
* 取 **30 分钟**:单轮静默超过 30 分钟仍无 `
|
|
91
|
+
* 取 **30 分钟**:单轮静默超过 30 分钟仍无 `idle` 的情形罕见;
|
|
42
92
|
* 而真发生了,代价只是一次可能多余的唤起(5min tick 限制频率,且 CRO 程序自己还能看
|
|
43
93
|
* `pendingWakes` / `lastWakeAt` 二次判)。
|
|
44
94
|
*
|
|
@@ -50,7 +100,7 @@
|
|
|
50
100
|
*/
|
|
51
101
|
/** 一次「在跑」记录的 TTL(见文件头的方向性判据) */
|
|
52
102
|
export declare const TURN_TTL_MS: number;
|
|
53
|
-
/**
|
|
103
|
+
/** 标记「在跑」(**唯一**生产调用点 = `agent/status=running`;另供测试与 CRO 自检置位) */
|
|
54
104
|
export declare function markTurnRunning(sessionId: string, nowMs: number, turn?: number | null): void;
|
|
55
105
|
/** 清除「在跑」(`turn-stopping` / disposed 调用) */
|
|
56
106
|
export declare function clearTurnRunning(sessionId: string): void;
|
|
@@ -80,10 +130,13 @@ export declare function __resetCroTurnsForTest(): void;
|
|
|
80
130
|
* 装配追踪(`index.ts` apply 调用):订阅四个既有事件 + 拆卸。
|
|
81
131
|
*
|
|
82
132
|
* **为什么订阅四个而不是两个**(R↓):
|
|
83
|
-
* · 前两个(`agent/
|
|
133
|
+
* · 前两个(`agent/status` / `turn-stopping`)负责**语义**——"在不在跑";
|
|
84
134
|
* · 后两个(`agent/disposed` / `session/disposed`)负责**生命周期**——
|
|
85
135
|
* 载体没了必须清项,否则表按载体只增(本模块的头号风险)。
|
|
86
136
|
*
|
|
137
|
+
* 🔴 **D243:`agent/created` 已于 2026-10-24 从订阅清单中移除**(原先共 5 个)。
|
|
138
|
+
* 理由与代价见**文件头 D243 段**;一句话:它是「载体被发布」的信,不是「轮次在跑」的信。
|
|
139
|
+
*
|
|
87
140
|
* ⚠️ **失败不抛**:任一事件通道缺失 ⇒ 响亮日志 + 该订阅跳过(**apply 不可成为启动单点**,
|
|
88
141
|
* 与 `registerDeepseekVisionPatch` 等既有装配同款纪律)。
|
|
89
142
|
*
|
package/lib/index.js
CHANGED
|
@@ -8438,14 +8438,64 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
8438
8438
|
* 调度器只掌握**会话 live 与否**,而 **`live ≠ 在跑`**——
|
|
8439
8439
|
* 一条会话可以 live 而空闲(本会话此刻就是活的反例)。
|
|
8440
8440
|
*
|
|
8441
|
-
* ##
|
|
8441
|
+
* ## 怎么夹出来(用**既有的**契约事件,不需要新宿主能力)
|
|
8442
8442
|
*
|
|
8443
8443
|
* | 事件 | 动作 | 出处 |
|
|
8444
8444
|
* |---|---|---|
|
|
8445
|
-
* | `agent/
|
|
8446
|
-
* | `agent/
|
|
8447
|
-
* | `agent/disposed` | **清该载体的项** | `host/contract.ts
|
|
8448
|
-
* | `session/disposed` | **清该载体的项** | `host/contract.ts
|
|
8445
|
+
* | `agent/status` = `running` | **置位**「在跑」 | 宿主 `AgentLoop.setPhase`:`emit("agent/status", { status })`,**只在状态真的变化时发** |
|
|
8446
|
+
* | `agent/status` = `idle` | **清位** | 同上;`kick()` 的 `finally` 一定 `setPhase({kind:'idle'})` |
|
|
8447
|
+
* | `agent/disposed` | **清该载体的项** | `host/contract.ts` |
|
|
8448
|
+
* | `session/disposed` | **清该载体的项** | `host/contract.ts` |
|
|
8449
|
+
*
|
|
8450
|
+
* 🔴 **`agent/turn-stopping` 刻意不订阅** —— 它是「轮末暂停点」不是「轮结束」,见下方 D244 段。
|
|
8451
|
+
*
|
|
8452
|
+
* ## 🔴🔴 D243(2026-10-24):**⛔ 不许用 `agent/created` 标记「在跑」**
|
|
8453
|
+
*
|
|
8454
|
+
* **修前**:`agent/created` 也 `markTurnRunning(...)`。**为什么这是错的** —— 该事件是
|
|
8455
|
+
* `AgentRegistry.announce(agent, source)` 发的,而 `source` 明确包含 **`resume`**:
|
|
8456
|
+
* 宿主**每次重启**都会把 persisted 会话重新载入并 announce ⇒ 每个被**载入**的会话都被打成"在跑",
|
|
8457
|
+
* 而**它那一轮早随旧进程死掉了** ⇒ `turn-stopping` 永不到达 ⇒ 该假标记活满 `TURN_TTL_MS`(30 min)。
|
|
8458
|
+
*
|
|
8459
|
+
* **代价(两个面,都已实测)**:
|
|
8460
|
+
* - **面一**:CRO 守卫拿它否决唤起 ⇒ **重启后聋 30 分钟**(guard 已在自己域绕开)。
|
|
8461
|
+
* - **面二**:`cognitive_trajectory interrupted` 拿它当排除集 ⇒ **把最该被接走的会话藏起来**
|
|
8462
|
+
* (实测:判据命中 5 条,生产入口报「检查 5 · 命中 0」)。
|
|
8463
|
+
*
|
|
8464
|
+
* **判据**:`agent/created` 证明的是「**载体被发布了**」,**不是**「有轮次在跑」。
|
|
8465
|
+
* 「载入」与「在跑」互不蕴含(与 D225 同族:*字段名/字面形态不是语义*)。
|
|
8466
|
+
* ⇒ **唯一权威 = `agent/status`**(宿主 `status` getter:`phase.kind === 'idle' | 'maintenance'`
|
|
8467
|
+
* 才是 idle,其余皆 running),且它在**每次真实状态跃迁**时发 —— 正是我们要的粒度。
|
|
8468
|
+
*
|
|
8469
|
+
* **代价与兜底**:删掉 `agent/created` 后,本表在"宿主重启后、某会话真正开跑之前"为空
|
|
8470
|
+
* ⇒ 落到文件尾那条**便宜**的误判方向(报"没跑"),不会被拿来藏会话。
|
|
8471
|
+
*
|
|
8472
|
+
* ## 🔴🔴 D244(2026-10-24):**⛔ `agent/turn-stopping` 不得清表**
|
|
8473
|
+
*
|
|
8474
|
+
* **它看起来最像"收轮",其实不是。** 宿主 `AgentLoop.turn()` 原文:
|
|
8475
|
+
*
|
|
8476
|
+
* ```js
|
|
8477
|
+
* if (turnEnds && this.inbox.nextStep.length === 0) {
|
|
8478
|
+
* await this.dispatch.serial("agent/turn-stopping", { turn, signal }) // 暂停点
|
|
8479
|
+
* signal.throwIfAborted()
|
|
8480
|
+
* }
|
|
8481
|
+
* if (turnEnds && this.inbox.nextStep.length === 0) break // 只有 nextStep 空才真结束
|
|
8482
|
+
* target = "next-step" // 否则同轮继续
|
|
8483
|
+
* ```
|
|
8484
|
+
*
|
|
8485
|
+
* ⇒ 任何监听者在 `turn-stopping` 回调里 `steer`(往 `next-step` 塞消息)⇒ **同一轮继续跑**,
|
|
8486
|
+
* 而 `turn/start` **不重发**、`agent/status` 也**不重发**(状态没变,还是 running)。
|
|
8487
|
+
*
|
|
8488
|
+
* 🔴 **实测代价**(真机 2026-10-24 18:1x):`trajectory/rebuild.ts` 的 auto-continue 正是
|
|
8489
|
+
* 在 `turn-stopping` 里 `agent.steer(...)` ⇒ 一条轮**连跑 9 分钟**
|
|
8490
|
+
* (`turn/start(32)` 18:12:00 → `turn/end(32)` 18:21:27),而本表**整段时间都是空的**
|
|
8491
|
+
* ⇒ `interrupted` 的排除失效 ⇒ `steer` 被塞进**正在跑的那一轮**(对它是误导)。
|
|
8492
|
+
* ⚠️ D243 之前这一格被 `agent/created` 的假置位**掩盖**;D243 删掉它 ⇒ 洞才暴露。
|
|
8493
|
+
* ⇒ **D243 的方向是对的,它只是揭开了一个更老的语义误解**。
|
|
8494
|
+
*
|
|
8495
|
+
* ⇒ **清理只认 `agent/status = idle`**(`kick()` 的 `finally` 一定 `setPhase({kind:'idle'})`)。
|
|
8496
|
+
* ⚠️ **已知窄代价**:`runMaintenance` 期间 `status` 也是 `idle` ⇒ 压缩等内部工作时不置位。
|
|
8497
|
+
* 判可接受:那时没有 CRO 该救的轮;方向落到那条**便宜**的误判(报"没跑")。
|
|
8498
|
+
* ⛔ **不要把 `clearTurnRunning` 加回 `turn-stopping`** —— 等于重新打开那个 9 分钟的洞。
|
|
8449
8499
|
*
|
|
8450
8500
|
* ## 🔴 清理设计(**这是本模块最容易被做坏的地方**)
|
|
8451
8501
|
*
|
|
@@ -8453,12 +8503,12 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
8453
8503
|
* 而 `agent/disposed` 在 `HOST_EVENTS` 里的 impact 原文**恰好就是**
|
|
8454
8504
|
* 「*per-会话内存态不清理(**长跑泄漏**)*」⇒ **本模块属于该已登记病族,必须在设计期就带清理**。
|
|
8455
8505
|
*
|
|
8456
|
-
*
|
|
8457
|
-
* 1. **正常路径**:`
|
|
8506
|
+
* **三层清理**(D244 起第 1 层已从 `turn-stopping` 换成 `status=idle`):
|
|
8507
|
+
* 1. **正常路径**:`agent/status = idle` ⇒ 清除该项;
|
|
8458
8508
|
* 2. **载体销毁**:`agent/disposed` / `session/disposed` ⇒ 清除该项;
|
|
8459
8509
|
* 3. 🔴 **异常路径(前两层都漏掉的兜底)**:读取时按 **TTL** 过滤——
|
|
8460
|
-
* 超过 `TURN_TTL_MS`
|
|
8461
|
-
* 触发场景:进程被 kill / 卡死 /
|
|
8510
|
+
* 超过 `TURN_TTL_MS` 未刷新的项**视为不在跑**(并顺手清除)。
|
|
8511
|
+
* 触发场景:进程被 kill / 卡死 / 事件丢失(这些情况下 `idle` 永远不会来)。
|
|
8462
8512
|
*
|
|
8463
8513
|
* ## TTL 取值的**方向性判据**(R↓,别用"感觉合适")
|
|
8464
8514
|
*
|
|
@@ -8468,7 +8518,7 @@ function registerDisposer(ctx, label, cleanup) {
|
|
|
8468
8518
|
* 「漏排是硬故障、多排是常态成本」)。
|
|
8469
8519
|
*
|
|
8470
8520
|
* ⇒ **不确定时宁可报"没跑"**(选便宜的那种错)⇒ **TTL 不宜过长**。
|
|
8471
|
-
* 取 **30 分钟**:单轮静默超过 30 分钟仍无 `
|
|
8521
|
+
* 取 **30 分钟**:单轮静默超过 30 分钟仍无 `idle` 的情形罕见;
|
|
8472
8522
|
* 而真发生了,代价只是一次可能多余的唤起(5min tick 限制频率,且 CRO 程序自己还能看
|
|
8473
8523
|
* `pendingWakes` / `lastWakeAt` 二次判)。
|
|
8474
8524
|
*
|
|
@@ -8483,9 +8533,9 @@ const TURN_TTL_MS = 18e5;
|
|
|
8483
8533
|
/**
|
|
8484
8534
|
* 进程内的「正在跑」表(键 = 载体 dsh 会话 id)。
|
|
8485
8535
|
*
|
|
8486
|
-
* ⚠️ **仅内存**:进程重启即空 ——
|
|
8487
|
-
*
|
|
8488
|
-
*
|
|
8536
|
+
* ⚠️ **仅内存**:进程重启即空 —— 这是**可接受的,且是刻意选的**:
|
|
8537
|
+
* 空表 = "没有在跑" ⇒ 落到上面那条**便宜**的误判方向(漏排是硬故障、多排是常态成本)。
|
|
8538
|
+
* 🔴 **D243**:刻意**不**在重启后用 `agent/created` 去"补"这张表 —— 那会把「载入」当「在跑」。
|
|
8489
8539
|
*/
|
|
8490
8540
|
const runningBySession = /* @__PURE__ */ new Map();
|
|
8491
8541
|
/** 取载体 id(事件 payload 的 `agent` 面;取不到 ⇒ null) */
|
|
@@ -8493,7 +8543,7 @@ function sessionIdOf$2(agent) {
|
|
|
8493
8543
|
const id = agent?.session?.id;
|
|
8494
8544
|
return typeof id === "string" && id !== "" ? id : null;
|
|
8495
8545
|
}
|
|
8496
|
-
/**
|
|
8546
|
+
/** 标记「在跑」(**唯一**生产调用点 = `agent/status=running`;另供测试与 CRO 自检置位) */
|
|
8497
8547
|
function markTurnRunning(sessionId, nowMs, turn = null) {
|
|
8498
8548
|
if (!sessionId) return;
|
|
8499
8549
|
runningBySession.set(sessionId, {
|
|
@@ -8541,10 +8591,13 @@ function runningCarriersOf(boundSessionIds, nowMs, ttlMs = TURN_TTL_MS) {
|
|
|
8541
8591
|
* 装配追踪(`index.ts` apply 调用):订阅四个既有事件 + 拆卸。
|
|
8542
8592
|
*
|
|
8543
8593
|
* **为什么订阅四个而不是两个**(R↓):
|
|
8544
|
-
* · 前两个(`agent/
|
|
8594
|
+
* · 前两个(`agent/status` / `turn-stopping`)负责**语义**——"在不在跑";
|
|
8545
8595
|
* · 后两个(`agent/disposed` / `session/disposed`)负责**生命周期**——
|
|
8546
8596
|
* 载体没了必须清项,否则表按载体只增(本模块的头号风险)。
|
|
8547
8597
|
*
|
|
8598
|
+
* 🔴 **D243:`agent/created` 已于 2026-10-24 从订阅清单中移除**(原先共 5 个)。
|
|
8599
|
+
* 理由与代价见**文件头 D243 段**;一句话:它是「载体被发布」的信,不是「轮次在跑」的信。
|
|
8600
|
+
*
|
|
8548
8601
|
* ⚠️ **失败不抛**:任一事件通道缺失 ⇒ 响亮日志 + 该订阅跳过(**apply 不可成为启动单点**,
|
|
8549
8602
|
* 与 `registerDeepseekVisionPatch` 等既有装配同款纪律)。
|
|
8550
8603
|
*
|
|
@@ -8574,22 +8627,13 @@ function registerCroTurnTracking(ctx) {
|
|
|
8574
8627
|
console.log(`[serenity-hooks] ✗ CRO turn 追踪未装配(${event} 不可用): ${String(err?.message ?? err)}`);
|
|
8575
8628
|
}
|
|
8576
8629
|
};
|
|
8577
|
-
on("agent/created", (payload) => {
|
|
8578
|
-
const id = sessionIdOf$2(payload?.agent);
|
|
8579
|
-
if (id) markTurnRunning(id, Date.now());
|
|
8580
|
-
});
|
|
8581
8630
|
on("agent/status", (payload) => {
|
|
8582
8631
|
const p = payload;
|
|
8583
8632
|
const id = sessionIdOf$2(p?.agent);
|
|
8584
8633
|
if (!id) return;
|
|
8585
|
-
|
|
8586
|
-
if (p.status === "running") markTurnRunning(id, Date.now(), turn);
|
|
8634
|
+
if (p.status === "running") markTurnRunning(id, Date.now());
|
|
8587
8635
|
else if (p.status === "idle") clearTurnRunning(id);
|
|
8588
8636
|
});
|
|
8589
|
-
on("agent/turn-stopping", (payload) => {
|
|
8590
|
-
const id = sessionIdOf$2(payload?.agent);
|
|
8591
|
-
if (id) clearTurnRunning(id);
|
|
8592
|
-
});
|
|
8593
8637
|
on("agent/disposed", (payload) => {
|
|
8594
8638
|
const id = sessionIdOf$2(payload?.agent);
|
|
8595
8639
|
if (id) clearTurnRunning(id);
|
|
@@ -9125,6 +9169,33 @@ function readTailEvents(logPath, want) {
|
|
|
9125
9169
|
*/
|
|
9126
9170
|
const UNFINISHED_KINDS = /* @__PURE__ */ new Set(["interrupted", "aborted"]);
|
|
9127
9171
|
/**
|
|
9172
|
+
* 尾部窗口里**最后一条 `turn/start` 的轮号**(D244 输出面,给调用方做 A108 的现场判断)。
|
|
9173
|
+
*
|
|
9174
|
+
* ⚠️ 与 {@link detectUnclosedTurn} 不同:**它不判"是否未收尾"**,只**如实报告窗口里有什么**。
|
|
9175
|
+
* 分开的理由:前者是判据(会 deciding 命中),后者是读数(零判据风险)——
|
|
9176
|
+
* 把读数塞进判据函数,调用方就没法在"判据因为窗口截断而沉默"时看出**为什么**沉默。
|
|
9177
|
+
*
|
|
9178
|
+
* @returns 轮号;窗口内没有 `turn/start` 或轮号缺失 ⇒ `null`
|
|
9179
|
+
*/
|
|
9180
|
+
function lastTurnStartTurnOf(events) {
|
|
9181
|
+
for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === "turn/start") {
|
|
9182
|
+
const t = events[i]?.data?.turn;
|
|
9183
|
+
return typeof t === "number" ? t : null;
|
|
9184
|
+
}
|
|
9185
|
+
return null;
|
|
9186
|
+
}
|
|
9187
|
+
/**
|
|
9188
|
+
* 尾部窗口里**最后一条 `turn/end` 的 `reason.kind`**(同上:读数,不是判据)。
|
|
9189
|
+
* @returns kind;窗口内没有 `turn/end`(形态 B 的典型)或 kind 缺失 ⇒ `null`
|
|
9190
|
+
*/
|
|
9191
|
+
function lastTurnEndKindOf(events) {
|
|
9192
|
+
for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === "turn/end") {
|
|
9193
|
+
const k = events[i]?.data?.reason?.kind;
|
|
9194
|
+
return typeof k === "string" ? k : null;
|
|
9195
|
+
}
|
|
9196
|
+
return null;
|
|
9197
|
+
}
|
|
9198
|
+
/**
|
|
9128
9199
|
* 命中判定(**形态 A:靠 `turn/end.reason.kind`**)。
|
|
9129
9200
|
*
|
|
9130
9201
|
* ⚠️ **本函数刻意不判「开了没收」** —— 那种形态与「此刻正在跑」在日志读数上**不可分**,
|
|
@@ -9294,7 +9365,9 @@ function scanInterruptedSessions(projectDir, options = {}) {
|
|
|
9294
9365
|
* 实测(本机 10 个窗口内会话):**4 条**只能由形态 B 抓到(末条 `tool/call` / `step/start`),
|
|
9295
9366
|
* 修前全落到 `skipped`。
|
|
9296
9367
|
*/
|
|
9297
|
-
const
|
|
9368
|
+
const formA = detectInterruptedEnd(events);
|
|
9369
|
+
const formB = formA === null ? detectUnclosedTurn(events) : null;
|
|
9370
|
+
const hit = formA ?? formB;
|
|
9298
9371
|
if (!hit) {
|
|
9299
9372
|
result.skipped++;
|
|
9300
9373
|
continue;
|
|
@@ -9306,7 +9379,10 @@ function scanInterruptedSessions(projectDir, options = {}) {
|
|
|
9306
9379
|
turn: hit.turn,
|
|
9307
9380
|
lastEventAt: typeof tailTime === "number" ? tailTime : null,
|
|
9308
9381
|
mtimeMs,
|
|
9309
|
-
pendingTools: hit.pendingTools
|
|
9382
|
+
pendingTools: hit.pendingTools,
|
|
9383
|
+
form: formA !== null ? "interrupted-end" : "unclosed",
|
|
9384
|
+
lastTurnEndKind: lastTurnEndKindOf(events),
|
|
9385
|
+
lastTurnStartTurn: lastTurnStartTurnOf(events)
|
|
9310
9386
|
});
|
|
9311
9387
|
} catch (err) {
|
|
9312
9388
|
result.errors.push({
|
|
@@ -11070,13 +11146,20 @@ function renderInterruptedScan(input) {
|
|
|
11070
11146
|
for (const s of scan.sessions) {
|
|
11071
11147
|
const at = s.lastEventAt !== null ? new Date(s.lastEventAt).toISOString() : "(时间未知)";
|
|
11072
11148
|
lines.push(` · ${s.sessionId}`);
|
|
11149
|
+
const formText = s.form === "unclosed" ? "未收尾(窗口内无 turn/end)" : "有收尾(reason.kind 命中)";
|
|
11073
11150
|
lines.push(` 轮次 turn=${s.turn ?? "?"} · 末事件 ${at}`);
|
|
11151
|
+
lines.push(` 形态 ${s.form} = ${formText} · 末条 turn/end.kind=${s.lastTurnEndKind ?? "null"} · 窗口内最后 turn/start=${s.lastTurnStartTurn ?? "null"}`);
|
|
11074
11152
|
if (s.pendingTools.length > 0) lines.push(` ⚠️ 有调用无结果的工具(副作用未知): ${s.pendingTools.join(", ")}`);
|
|
11075
11153
|
}
|
|
11076
11154
|
lines.push("");
|
|
11077
|
-
lines.push("
|
|
11155
|
+
lines.push("判据(两种形态,任一命中即收):");
|
|
11156
|
+
lines.push(" A `interrupted-end`: 末条 `turn/end.reason.kind` ∈ {interrupted, aborted},且其后没有新的 turn/start。");
|
|
11078
11157
|
lines.push(" interrupted = 进程崩溃后被 resume 补收尾 / aborted = 被正常终止(重启、销毁、用户中断)。");
|
|
11079
|
-
lines.push("
|
|
11158
|
+
lines.push(" B `unclosed`: 窗口内最后一条 `turn/start` 之后没有 `turn/end`(进程在轮内任意位置死掉,宿主来不及写收尾)。");
|
|
11159
|
+
lines.push("🔴 **怎么用「形态」判该不该续**(调用方口径,ACC 不代判):");
|
|
11160
|
+
lines.push(" · 投递方式 `cold-resume` ⇒ 该会话**未被载入本进程**(死着)⇒ 真需要接走;");
|
|
11161
|
+
lines.push(" · 投递方式 `live+steer` ⇒ 它在进程里 ⇒ ⚠️ **「载入」≠「空闲」**:");
|
|
11162
|
+
lines.push(" `form=unclosed` 且 `窗口内最后 turn/start` == 命中 turn ⇒ 可能**那一轮正在跑** ⇒ 先核再注入。");
|
|
11080
11163
|
lines.push("🔴 本动作**只读** —— 是否续跑、续哪条、几点续,由调用方(你的认知轨迹)决定。");
|
|
11081
11164
|
lines.push("⚠️ 未完成的工具调用「副作用未知」≠「没执行」:它们可能已写文件 / 已发消息。");
|
|
11082
11165
|
lines.push(" 续跑前应先核对副作用,而不是把那一轮整段重放。");
|
|
@@ -82,6 +82,30 @@ export interface InterruptedSession {
|
|
|
82
82
|
mtimeMs: number;
|
|
83
83
|
/** 该轮里"有 call 无 result"的工具名(**尽力而为**,可能因尾部窗口截断而不全) */
|
|
84
84
|
pendingTools: string[];
|
|
85
|
+
/**
|
|
86
|
+
* 🔴 **命中形态**(D244 输出面,guard 2026-10-24 要的判别式 Y 的一半)。
|
|
87
|
+
*
|
|
88
|
+
* | 值 | 含义 | 末条 `turn/end` |
|
|
89
|
+
* |---|---|---|
|
|
90
|
+
* | `interrupted-end` | 形态 A:日志**有** `turn/end`,其 `reason.kind ∈ {interrupted, aborted}` | 存在,见 {@link lastTurnEndKind} |
|
|
91
|
+
* | `unclosed` | 形态 B:**开了没收** —— 窗口内最后一条 `turn/start` 之后没有 `turn/end` | **可能不存在** |
|
|
92
|
+
*
|
|
93
|
+
* 🔴 **为什么必须暴露它**:调用方(如 guard 的守护程序)要能自己判「该不该注入」,
|
|
94
|
+
* 而这两种形态的**可用判据完全不同** —— 形态 B 连"末条 turn/end 的 kind"都不存在。
|
|
95
|
+
* 把决定权留在 ACC 里 = 让调用方没法自己躲开误注入(D237 同族:读数要能改变下一步)。
|
|
96
|
+
*/
|
|
97
|
+
form: 'interrupted-end' | 'unclosed';
|
|
98
|
+
/**
|
|
99
|
+
* 形态 A:末条 `turn/end` 的 `reason.kind`;形态 B:**恒为 `null`**(那时没有末条 turn/end)。
|
|
100
|
+
* ⚠️ `null` 在这里**承载语义**("该格不存在"),不是"没读到"(D233 同族)——
|
|
101
|
+
* 两种成因在读数上不同:形态 B 的 `form` 会是 `unclosed`。
|
|
102
|
+
*/
|
|
103
|
+
lastTurnEndKind: string | null;
|
|
104
|
+
/**
|
|
105
|
+
* 日志尾部窗口里**最后一条 `turn/start` 的轮号**;窗口内没有则 `null`。
|
|
106
|
+
* 与 {@link turn} 对照可看出「窗口是否把这一轮的起点挤出去了」(A108 的现场判据)。
|
|
107
|
+
*/
|
|
108
|
+
lastTurnStartTurn: number | null;
|
|
85
109
|
}
|
|
86
110
|
/** 扫描结果 */
|
|
87
111
|
export interface ScanResult {
|
|
@@ -99,6 +123,21 @@ export interface ScanResult {
|
|
|
99
123
|
/** 扫描耗时(ms) */
|
|
100
124
|
elapsedMs: number;
|
|
101
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* 尾部窗口里**最后一条 `turn/start` 的轮号**(D244 输出面,给调用方做 A108 的现场判断)。
|
|
128
|
+
*
|
|
129
|
+
* ⚠️ 与 {@link detectUnclosedTurn} 不同:**它不判"是否未收尾"**,只**如实报告窗口里有什么**。
|
|
130
|
+
* 分开的理由:前者是判据(会 deciding 命中),后者是读数(零判据风险)——
|
|
131
|
+
* 把读数塞进判据函数,调用方就没法在"判据因为窗口截断而沉默"时看出**为什么**沉默。
|
|
132
|
+
*
|
|
133
|
+
* @returns 轮号;窗口内没有 `turn/start` 或轮号缺失 ⇒ `null`
|
|
134
|
+
*/
|
|
135
|
+
export declare function lastTurnStartTurnOf(events: SessionEvent[]): number | null;
|
|
136
|
+
/**
|
|
137
|
+
* 尾部窗口里**最后一条 `turn/end` 的 `reason.kind`**(同上:读数,不是判据)。
|
|
138
|
+
* @returns kind;窗口内没有 `turn/end`(形态 B 的典型)或 kind 缺失 ⇒ `null`
|
|
139
|
+
*/
|
|
140
|
+
export declare function lastTurnEndKindOf(events: SessionEvent[]): string | null;
|
|
102
141
|
/**
|
|
103
142
|
* 命中判定(**形态 A:靠 `turn/end.reason.kind`**)。
|
|
104
143
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.96.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": {
|