@shgroup/dsh-serenity-hooks 1.42.0 → 1.44.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/README.md CHANGED
@@ -278,7 +278,34 @@ DSH 一个进程可以同时带多个工作区,每个工作区各自对接自
278
278
 
279
279
  - **目标没打开也能唤醒**:会话不在内存里时先把它载入再投递(**冷唤醒**);载入不了则条目留在登记表里并记下原因,**不静默丢弃**
280
280
  - **"周期"归工作区自己**:要一轮接一轮,就由**收到唤醒的那条轨迹**在每轮结束时再排一次下一轮——ACC 只提供"到点投递一条消息"这个原语,不替工作区决定节拍(焦点、节拍、偏见都由工作区自定;任意条轨迹并行、互不干扰)
281
- - **轨迹可以自带 skill**:在它的 `SESSION.md` 顶部 frontmatter 写 `skills: [名字, …]`,这条轨迹**被绑定期间**的每个请求都会注入这些 skill 的全文(适合把某个领域的做事方法直接挂在轨迹上);skill 名来自工作区数据,**先过安全校验才会用于拼路径**,找不到会在提示里明说"缺失"而**不静默跳过**
281
+ - **轨迹可以带上 skill**(两种写法,**取并集**):① **一处声明、全容器生效**——在 `.opencode/serenity.json` 写 `trajectory.skills: [名字, …]`,**本 CCC 的每条轨迹**被绑定期间都注入这些 skill 的全文(**连 skiff 角色会话也算**);② **只给这一条**——在它的 `SESSION.md` 顶部 frontmatter 写 `skills: [名字, …]`。两者是并集:容器级在前(底座)、轨迹级追加(这条额外的)。⚠️ 是"**绑定期间一直供着**"而非"`use` 时灌一次"——改了配置或 frontmatter**立即生效**,也不随对话压缩消失;而 `create` 只新建、**不夺走当前绑定** ⇒ 新轨迹要**显式 `use`** 才挂上。skill 名来自工作区数据,**先过安全校验才会用于拼路径**,找不到会在提示里明说"缺失"而**不静默跳过**;⚠️ **重写 `SESSION.md` 时务必原样保留顶部 frontmatter**(抹掉 = 静默撤销该轨迹的声明)
282
+
283
+ #### CRO —— 让轨迹**自己判断什么时候该被叫醒**
284
+
285
+ **问题**:上面那两条都要求**先算好一个时刻**("3 点叫我")。可**该不该醒往往不是时间说了算**:某条轨迹应该在"天亮 + 家里有人 + 非高峰"才醒;已经在干活就不该再叫一次;日志快满了,下次叫它时该**要求它先整理**。
286
+
287
+ **做法**:**让一条轨迹自己带一段程序**,由 ACC 在每次检查时跑它,**由这段程序决定"现在该不该叫我、叫我的时候说什么"**。
288
+
289
+ | | 内容 |
290
+ |---|---|
291
+ | **程序放哪** | `<CCC 根>/AGENT_SESSIONS/<轨迹目录>/continuous-re-occurrence.ts`(放**轨迹自己目录**里 ⇒ 跟随轨迹跨载体存活、天然进 git) |
292
+ | **开关** | 🔴 **没有开关**——**文件在 = 启用,文件不在 = 禁用**(无 enabled 字段、无注册表) |
293
+ | **谁跑它** | ACC 的唤醒调度器(既有 **5 分钟** tick) |
294
+ | **给它什么** | 一条 stdin 进来的 **JSON 快照**:身份 / 时间 / 轨迹身体(SESSION.md 体积与 mtime、`references/` 清单)/ 绑定与载体(绑定的、live 的、🔴 **正在跑轮次的**)/ 调度面(本轨迹在办唤醒、调度器状态) |
295
+ | **它给我什么** | stdout **一行 JSON**:`{"wake":true,"prompt":"…","reason":"…"}` 或 `{"wake":false}`(**缺省 = 不打扰**) |
296
+ | **写程序前先读** | `container_trajectory cro-guide` —— **指南 + 一份可直接拿去自测的样例快照** |
297
+
298
+ **几条设计上的硬约束**(都不是随便定的):
299
+
300
+ - 🔴 **ACC 只"起进程",从不 import 你的程序**——进程边界同时挡掉两件事:ACC 不必依赖工作区的**源码路径**(装机版在别处,两条路径就是两个真相源),以及**一个用户程序的语法错会放倒整个容器**
301
+ - 🔴 **半成品报错就行**:程序报错 / 超时(60 秒硬超时,超了 kill)/ 输出非法 ⇒ **记一行 + 跳过本轮**
302
+ - 🔴 **CRO 的任何失败都不影响既有机制**——`send-later` / `send-now` / 唤醒表投递**照常工作**(这条有实测用例钉住,不是口头承诺)
303
+ - 🔴 **`reason` 强烈建议填**:改成程序判定之后,"当时为什么叫了"**不再能从时间表重建**(原因在程序肚子里);不写,以后出事无法复现
304
+ - **程序想要记住"上次判了什么",得自己记**——写在**自己轨迹目录**里的状态文件(那是它自己的进程状态,ACC 物理上拿不到)。所以**防抖也归程序**:想"别叫太频繁"就自己记时间戳
305
+ - ⚠️ **它不自带自测**:指南里给的流程是**先用开发名写**(如 `continuous-re-occurrence.dev.ts`,**不会被启用**)→ 配自测跑绿 → **再改名为正式名**(因为"文件在 = 启用",**改名这个动作就是上线动作**)
306
+
307
+ > 🔵 **不是 autopilot 回归**:退场的 autopilot 删的是**判据内容**(周期节拍 + 提示词),留下的是**调度能力**;CRO 补的是"**在没人醒着的时候判断该不该醒**"——那正是工作区自己做不到的那件(工作区的自排是"被唤起时才跑")。
308
+
282
309
 
283
310
  > ⚠️ **历史(v1.35.0 起已退场)**:ACC 曾自带一套"**周期自唤醒 autopilot**"——插件内时钟 + 工作区配置里的 `topPrompt`/偏见脚本 + 面板「周期自唤醒」开关 + `container_admin autopilot` 三动作(status/init/generate-bias)。**v1.35.0 起整段删除**。理由:它相对当时的 `wake-later`(**今 `send-later`**)只多两样——"周期节拍"与"提示词注入",而这两样**工作区自己就能做**(见上);且后者反而更强(**支持冷会话唤醒**,而 autopilot 要求目标会话已在内存里)。老会话 / 老文档里看到 `container_admin autopilot`、`autopilot-trajectory`、`--auto` 目录后缀、`[Autopilot Trajectory · 唤起]` 等字样,**均按本条理解**:那是已删除的机制。
284
311
 
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.42.0",
3
+ "version": "1.44.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——11 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/handyman/localstore/container_admin/im-bridge/acc-diag;后两个按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.5-rc.2(deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -155,6 +155,27 @@ function readExclusiveTools(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
155
155
  if (!Array.isArray(list)) return [];
156
156
  return list.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim());
157
157
  }
158
+ /**
159
+ * 读取 CCC 声明的「轨迹默认 skill」清单(`trajectory.skills`,v1.44.0)。
160
+ *
161
+ * 语义与失败方向(R↓):
162
+ * · 有声明 ⇒ 该 CCC **每条被绑定的轨迹**都注入这些 skill(与轨迹自己的声明**并集**);
163
+ * · 未配置 / 字段缺失 / 配置损坏 ⇒ **空数组** = 无 CCC 级供给(**轨迹级声明不受影响**)。
164
+ * 这里不必像 `exclusiveTools` 那样强调 fail-closed:本键的默认态本就是"无供给"
165
+ * (空数组即该默认态),且配置损坏已有 `loadSerenityConfig` 的响亮告警兜住。
166
+ *
167
+ * 🟢 **名字**在此**不做**安全过滤——那是装配层的职责(`trajectory-skills.ts` 走 `isSafeSkillName`
168
+ * 后才拼路径)。本函数只做"取字符串、去空白、丢非字符串",**保持书写顺序**(注入顺序 = 书写顺序)。
169
+ *
170
+ * @param root CCC 根
171
+ * @param paths 候选配置相对路径
172
+ * @returns 去空白后的 skill 名列表(非字符串/空白项被丢弃)
173
+ */
174
+ function readTrajectorySkills(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
175
+ const list = loadSerenityConfig(root, paths).trajectory?.skills;
176
+ if (!Array.isArray(list)) return [];
177
+ return list.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim());
178
+ }
158
179
  const SAFE_MODE_MARKER = ".serenity-safe-on";
159
180
  function isSafeModeOn(root) {
160
181
  return existsSync(resolve(root, SAFE_MODE_MARKER));
@@ -193,4 +214,4 @@ function matchBlacklist(relPath, rules) {
193
214
  return null;
194
215
  }
195
216
  //#endregion
196
- export { findSerenityRoot as a, matchBlacklist as c, readCccName as d, readExclusiveTools as f, resolveInside as h, findGitRoot as i, pathInside as l, readUtf8 as m, SAFE_MODE_MARKER as n, isSafeModeOn as o, readHandymanConfig as p, classifyPath as r, loadSerenityConfig as s, DEFAULT_SERENITY_CONFIG_PATHS as t, readBlacklist as u };
217
+ export { findSerenityRoot as a, matchBlacklist as c, readCccName as d, readExclusiveTools as f, resolveInside as g, readUtf8 as h, findGitRoot as i, pathInside as l, readTrajectorySkills as m, SAFE_MODE_MARKER as n, isSafeModeOn as o, readHandymanConfig as p, classifyPath as r, loadSerenityConfig as s, DEFAULT_SERENITY_CONFIG_PATHS as t, readBlacklist as u };
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { i as hostSessions, r as hostService } from "./access-fiehjxV6.js";
3
- import { a as findSerenityRoot } from "./ccc-NlLr_sxy.js";
3
+ import { a as findSerenityRoot } from "./ccc-DG4Oc7I1.js";
4
4
  import { basename } from "node:path";
5
5
  //#region src/ccc-roots.ts
6
6
  /**
@@ -139,7 +139,7 @@ async function listCccs(ctx, opts = {}) {
139
139
  if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
140
140
  let rolesOf = null;
141
141
  if (opts.withRoles === true) {
142
- const { readSkiffRoles } = await import("./skiff-role-CEHL5cek.js").then((n) => n.u);
142
+ const { readSkiffRoles } = await import("./skiff-role-KqQA6pkr.js").then((n) => n.u);
143
143
  rolesOf = (root) => [...readSkiffRoles(root).keys()];
144
144
  }
145
145
  return c.roots.map((root) => ({
package/lib/ccc.d.ts CHANGED
@@ -93,6 +93,25 @@ interface SerenityConfig {
93
93
  trajectory?: {
94
94
  /** autopilot(周期自唤醒的特例;每 CCC 单例,D59) */
95
95
  autopilot?: AutopilotTrajectorySettings;
96
+ /**
97
+ * **CCC 级 skill 供给**(v1.44.0;owner 令 2026-09-20:「要求实现我的诉求……skiff 也支持」)。
98
+ *
99
+ * 语义:**本 CCC 的每条轨迹都带这些 skill**——声明的 skill 全文在该轨迹**被绑定期间**
100
+ * 动态注入系统提示词(每请求重新求值,不缓存)。与轨迹自己在 `SESSION.md` frontmatter
101
+ * 里的 `skills:` 是**并集**(CCC 级在前、轨迹级追加、同名去重)⇒ 容器给底座、
102
+ * 单条轨迹仍可加自己的额外项(这正是 C1 设计当初否决"纯 CCC 全局"的理由所在:
103
+ * 全局化会**丢掉 per-trajectory**;并集把它保住)。
104
+ *
105
+ * 归属(D23):**机制在 ACC**(读取 + 合并 + 注入 + 守卫),**声明在 CCC**(本键)——
106
+ * ACC 代码里不出现任何具体 CCC 或具体 skill 名。
107
+ *
108
+ * 失败语义:未配置 / 字段缺失 / 配置损坏 ⇒ **空数组**(= 没有 CCC 级供给;轨迹级声明照旧生效)。
109
+ * 配置损坏本身已由 `loadSerenityConfig` 响亮告警(不静默)。
110
+ *
111
+ * ⚠️ 与 `create` 的关系:**不触发**——`create` 刻意不夺绑定(U6),注入的门是"存在绑定"。
112
+ * ⚠️ 作用面:**含 skiff 角色会话**(它们同样绑定工作台轨迹)。
113
+ */
114
+ skills?: string[];
96
115
  };
97
116
  /**
98
117
  * 微信桥(F4c-3,v1.27.0 实验性):**CCC 级**配置——dsh 一个进程含多个 CCC,
@@ -292,6 +311,23 @@ export declare function loadSerenityConfig(root: string, paths?: string[]): Sere
292
311
  * @returns 去空白后的工具名列表(非字符串项被丢弃)
293
312
  */
294
313
  export declare function readExclusiveTools(root: string, paths?: string[]): string[];
314
+ /**
315
+ * 读取 CCC 声明的「轨迹默认 skill」清单(`trajectory.skills`,v1.44.0)。
316
+ *
317
+ * 语义与失败方向(R↓):
318
+ * · 有声明 ⇒ 该 CCC **每条被绑定的轨迹**都注入这些 skill(与轨迹自己的声明**并集**);
319
+ * · 未配置 / 字段缺失 / 配置损坏 ⇒ **空数组** = 无 CCC 级供给(**轨迹级声明不受影响**)。
320
+ * 这里不必像 `exclusiveTools` 那样强调 fail-closed:本键的默认态本就是"无供给"
321
+ * (空数组即该默认态),且配置损坏已有 `loadSerenityConfig` 的响亮告警兜住。
322
+ *
323
+ * 🟢 **名字**在此**不做**安全过滤——那是装配层的职责(`trajectory-skills.ts` 走 `isSafeSkillName`
324
+ * 后才拼路径)。本函数只做"取字符串、去空白、丢非字符串",**保持书写顺序**(注入顺序 = 书写顺序)。
325
+ *
326
+ * @param root CCC 根
327
+ * @param paths 候选配置相对路径
328
+ * @returns 去空白后的 skill 名列表(非字符串/空白项被丢弃)
329
+ */
330
+ export declare function readTrajectorySkills(root: string, paths?: string[]): string[];
295
331
  export declare const SAFE_MODE_MARKER = ".serenity-safe-on";
296
332
  export declare function isSafeModeOn(root: string): boolean;
297
333
  /** 黑名单条目(对齐 osp BlacklistEntry):pattern + 可选自定义拦截提示 message */
@@ -0,0 +1,48 @@
1
+ /**
2
+ * cro-guide.ts — CRO 编写指南(**ACC 内置**,S142 §7.8,2026-09-19)
3
+ *
4
+ * ## 为什么指南在 ACC 而不在 CCC(所有者令)
5
+ *
6
+ * 所有者 2026-09-19:「**ACC 内置,整个机制属于 ACC,CCC 是用户**」。
7
+ * ⇒ 契约 / 调度 / 执行 / 状态暴露 / 容错 / **编写指南** 全在 ACC;
8
+ * CCC(用户)只负责**写那段程序**。
9
+ *
10
+ * ## 🔴 单真相源纪律(本文件的核心约束)
11
+ *
12
+ * 指南**不复制**契约条款,且**样例快照是"派生"而非"手抄"**:
13
+ * {@link CRO_SAMPLE_SNAPSHOT} 由 {@link buildCroSnapshot} **真正装配**出来 ⇒
14
+ * **schema 一变,样例自动跟着变**,不存在"文档里的样例过期"这一失效模式。
15
+ * (本容器栽过多次"第二真相源",此处用代码结构而不是纪律去防它。)
16
+ *
17
+ * ## 所有者对本指南的两条明示要求(2026-09-19)
18
+ *
19
+ * 1. 🔴 **必须备注:CRO 程序是可自测的;测试通过再把文件名改成正式名**;
20
+ * 2. 🔴 **要一并给出测试用的参数**。
21
+ */
22
+ import { type CroSnapshotInput } from './cro.js';
23
+ /**
24
+ * 样例快照输入 —— **代表性数据**(不是真实某条轨迹的现状)。
25
+ *
26
+ * 取值刻意让它能演示所有者举的 S185 场景(天亮/有人/非高峰 + 已在干活 + 日志过大)。
27
+ */
28
+ export declare const CRO_SAMPLE_SNAPSHOT_INPUT: CroSnapshotInput;
29
+ /**
30
+ * 🔴 **样例快照(测试参数)** —— **由真实装配器生成**,故**永不与 schema 漂移**。
31
+ *
32
+ * 用途:写 CRO 程序时把它喂给你的程序做自测(见指南 §8「自测」)。
33
+ * @returns 缩进过的快照 JSON 文本
34
+ */
35
+ export declare function renderCroSampleSnapshot(): string;
36
+ /** 正式入口文件名(写死于此,供指南与实现共用同一常量来源) */
37
+ export { CRO_FILENAME, CRO_TIMEOUT_MS } from './cro.js';
38
+ /**
39
+ * CRO 编写指南正文(ACC 内置手册;由工具的 guide 动作输出)。
40
+ *
41
+ * 结构照既有 guide 惯例(`MSM_GUIDE` / `handyman(guide=true)`):
42
+ * 用途 → 位置 → 我给什么 → 你给我什么 → 骨架 → 常见模式 → 坑 → **自测**。
43
+ */
44
+ export declare const CRO_GUIDE: string;
45
+ /** 供工具动作取用(与 `MSM_GUIDE` 同款出口) */
46
+ export declare function croGuidePayload(): {
47
+ guide: string;
48
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
3
+ *
4
+ * ## 所有者令(本模块的存在理由)
5
+ *
6
+ * 「S151 和 S185 的验证都说明,trajectory 倾向于自己做个结构化记录来记录自身 CRO 的执行情况,
7
+ * 这是个结构化流水账;**我们 ACC 有必要进行 CRO 唤醒的日志记录**,当 CRO 存在时,每次**成功唤醒**
8
+ * 写相关信息,但是保留数量要少,**只保留仅两日**的记录即可,**这个时间是程序化的**」
9
+ * +「**这个日志写结构化文件到 trajectory 目录,文件名固定即可**」
10
+ * +「**失败成功都记录**」。
11
+ *
12
+ * ## 它解决什么麻烦(白话)
13
+ *
14
+ * 在此之前「ACC 到底叫过谁、叫了几次、哪次**没叫成**」**ACC 自己一个字都不留** ——
15
+ * 要查只能去读**被叫的那个程序**写的记录;程序没写 / 写坏 / 被换掉 ⇒ **这段历史就是空白**。
16
+ * ⇒ 本模块让 ACC **自己那一侧**留一份只记两天的流水,使「唤起历史」**不再依赖被观测者是否老实**。
17
+ *
18
+ * ## 🔴 为什么**不是**第二真相源(本容器铁律,勿删此论证)
19
+ *
20
+ * 轨迹目录里因此**并排两个文件、主语各一**:
21
+ *
22
+ * | 文件 | 谁写 | 主语 | 回答 |
23
+ * |---|---|---|---|
24
+ * | `<轨迹目录>/cro-state.json`(CCC 侧程序自建,名字自定) | **CCC 的 CRO 程序** | 程序 | 「**我判了什么**」(含"没叫"的判定、心跳阈值、环境谓词) |
25
+ * | `<轨迹目录>/cro-wake-log.json`(**本模块**) | **ACC** | ACC | 「**我把什么送出去了、送成没有**」 |
26
+ *
27
+ * **内容不重叠**:本模块**不复制**判定细节(不存 `lastDecision` / 阈值 / 谓词);
28
+ * 而程序**不可能**知道投递结果(CRO 投递是 fire-and-forget)。
29
+ * ⇒ 两者**互补**:完整审计 = 两者合看(本模块 = 投递面|程序状态 = 判定面)。
30
+ *
31
+ * ## 🔴 轮转(所有者明示「程序化的」)
32
+ *
33
+ * **写入时按窗裁剪**:每次落盘只保留 `at >= now - CRO_LOG_RETENTION_MS` 的条目,
34
+ * 随后**原子写**(`tmp + rename`,与 `wake-registry.ts` 同款形态)。
35
+ * ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
36
+ * 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
37
+ *
38
+ * ## 🔴 铁律(与 `cro.ts` 同族)
39
+ *
40
+ * **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
41
+ * 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
42
+ *
43
+ * ## 保留量级(实测,非估计)
44
+ *
45
+ * 实测最高 = S185 的 40min 心跳(≈36 条/天);两天窗 ⇒ **≤ ~80 条、< 20 KB**。
46
+ * 最坏(某程序每 5min 都叫)⇒ 288 条/天 ⇒ 两天 **≤ ~600 条、< 100 KB** —— 轮转**自带上限**。
47
+ */
48
+ /** 固定文件名(所有者令:「文件名固定即可」)——与程序自建的 `cro-state.json` 并排、主语分明 */
49
+ export declare const CRO_LOG_FILENAME = "cro-wake-log.json";
50
+ /** 保留窗 = 2 日(所有者令「只保留仅两日」);**常量** ⇒ 「这个时间是程序化的」 */
51
+ export declare const CRO_LOG_RETENTION_MS: number;
52
+ /** 提示词摘要取前多少字(**只存摘要,不搬第二份全文**——全文属程序那侧的产物) */
53
+ export declare const CRO_PROMPT_HEAD_MAX = 80;
54
+ /** 一条唤起**尝试**(成功与失败**都记**,所有者令 2026-09-20) */
55
+ export interface CroWakeLogEntry {
56
+ /** 时刻(当地 RFC3339 带偏移,遵 D67) */
57
+ at: string;
58
+ /** 投递是否成功 */
59
+ ok: boolean;
60
+ /** 投递方式(`已投递(live)` / `已投递(冷载入)`)或失败原因 */
61
+ detail: string;
62
+ /** 调度器 tick 计数(便于与 `acc-diag` ①b 对齐) */
63
+ tick: number;
64
+ /** 程序自报理由;**取不到写 `null`,不留空白**(同 §4.5.4 纪律:留空白等于把缺口也丢了) */
65
+ reason: string | null;
66
+ /** 唤起提示词**摘要**(长度 + 前 N 字) */
67
+ promptDigest: {
68
+ length: number;
69
+ head: string;
70
+ };
71
+ }
72
+ /** 文件形态(与 `wake-registry.json` 同款:`{version, entries}`) */
73
+ export interface CroWakeLog {
74
+ version: number;
75
+ entries: CroWakeLogEntry[];
76
+ }
77
+ /** 追加所需的输入(`at` 由本模块按当地时区生成,不劳调用方) */
78
+ export interface CroWakeLogInput {
79
+ ok: boolean;
80
+ detail: string;
81
+ tick: number;
82
+ reason: string | null;
83
+ /** 唤起提示词原文(本模块只留摘要) */
84
+ prompt?: string;
85
+ }
86
+ /** 日志绝对路径 */
87
+ export declare function croWakeLogPath(root: string, dirName: string): string;
88
+ /** 提示词摘要(长度 + 前 N 字;超长不截断标记——长度字段本身就是信号) */
89
+ export declare function summarizeCroPrompt(prompt: string | undefined): {
90
+ length: number;
91
+ head: string;
92
+ };
93
+ /**
94
+ * 读取日志(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本)。
95
+ * @param root CCC 根
96
+ * @param dirName 轨迹目录名
97
+ */
98
+ export declare function loadCroWakeLog(root: string, dirName: string): {
99
+ log: CroWakeLog;
100
+ error: string | null;
101
+ };
102
+ /**
103
+ * 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
104
+ *
105
+ * 判据:`Date.parse(at) >= nowMs - windowMs` 保留;反之丢弃。
106
+ * 🔴 **`at` 不可解析者一律丢弃**(本模块只写 `isoLocal()` 产物 ⇒ 不可解析 = 被手改或损坏),
107
+ * **并在返回值里报数**(`dropped`)——**不静默吞掉**(本容器反复栽的"安静失败"纪律)。
108
+ */
109
+ export declare function pruneCroLogEntries(entries: CroWakeLogEntry[], nowMs: number, windowMs?: number): {
110
+ kept: CroWakeLogEntry[];
111
+ pruned: number;
112
+ dropped: number;
113
+ };
114
+ /**
115
+ * 追加一条唤起记录并按窗裁剪(**本模块主入口;永不抛**)。
116
+ *
117
+ * 顺序 = 读 → 追加 → 裁剪 → **原子写**。任一步失败 ⇒ `{ok:false, error}`,
118
+ * **不抛**(调用方只记一行 tick 日志;**绝不影响投递**)。
119
+ *
120
+ * 无 CRO 的轨迹**不会**因本函数产生文件:只有真的发生过一次唤起尝试才会建/写该档
121
+ * (所有者令「**当 CRO 存在时**」)。
122
+ *
123
+ * @param root CCC 根
124
+ * @param dirName 轨迹目录名
125
+ * @param input 本次唤起尝试(成败都记)
126
+ * @param nowMs 当前毫秒(可注入 ⇒ 便于测裁剪边界)
127
+ * @returns 写入结果与裁剪计数
128
+ */
129
+ export declare function appendCroWakeLog(root: string, dirName: string, input: CroWakeLogInput, nowMs?: number): {
130
+ ok: boolean;
131
+ error: string | null;
132
+ kept: number;
133
+ pruned: number;
134
+ dropped: number;
135
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * cro-turns.ts — 「这条轨迹是否正在跑轮次」的追踪(设计 §3.2,2026-09-19 S142)
3
+ *
4
+ * ## 为什么需要这个模块(CRO 唯一需要 ACC 新增的状态)
5
+ *
6
+ * CRO 程序要能回答「**已经在工作则停止**」这类问题 ⇒ 输入快照里必须有
7
+ * 「**哪些载体正在跑轮次**」。而这件事**今天判不出来**:
8
+ * 调度器只掌握**会话 live 与否**,而 **`live ≠ 在跑`**——
9
+ * 一条会话可以 live 而空闲(本会话此刻就是活的反例)。
10
+ *
11
+ * ## 怎么夹出来(用**既有的**两个契约事件,不需要新宿主能力)
12
+ *
13
+ * | 事件 | 动作 | 出处 |
14
+ * |---|---|---|
15
+ * | `agent/session-start` | 标记「在跑」 | `host/contract.ts:275`(dsp 已在 `seams/context.ts:235` 订阅) |
16
+ * | `agent/turn-stopping` | 清除标记 | `host/contract.ts:277`(dsp 已在 `rebuild.ts:442` 等 4 处订阅) |
17
+ * | `agent/disposed` | **清该载体的项** | `host/contract.ts:280` |
18
+ * | `session/disposed` | **清该载体的项** | `host/contract.ts:283` |
19
+ *
20
+ * ## 🔴 清理设计(**这是本模块最容易被做坏的地方**)
21
+ *
22
+ * ⚠️ 本容器栽过 **5 次**「只增不清」的病(wake-registry / keeper 三表 / pendingRebuilds …),
23
+ * 而 `agent/disposed` 在 `HOST_EVENTS` 里的 impact 原文**恰好就是**
24
+ * 「*per-会话内存态不清理(**长跑泄漏**)*」⇒ **本模块属于该已登记病族,必须在设计期就带清理**。
25
+ *
26
+ * **三层清理**:
27
+ * 1. **正常路径**:`turn-stopping` ⇒ 清除该项;
28
+ * 2. **载体销毁**:`agent/disposed` / `session/disposed` ⇒ 清除该项;
29
+ * 3. 🔴 **异常路径(前两层都漏掉的兜底)**:读取时按 **TTL** 过滤——
30
+ * 超过 `TURN_TTL_MS` 未收到 `turn-stopping` 的项**视为不在跑**(并顺手清除)。
31
+ * 触发场景:进程被 kill / 卡死 / 事件丢失。
32
+ *
33
+ * ## TTL 取值的**方向性判据**(R↓,别用"感觉合适")
34
+ *
35
+ * 两种误判的代价**不对称**:
36
+ * · **误报"在跑"**(实际没跑)⇒ CRO 可能**不唤起** ⇒ **漏排 = 硬故障**(链停滞);
37
+ * · **误报"没跑"**(实际在跑)⇒ CRO 可能**多唤起**一次 ⇒ **多排 = 常态成本**(既有纪律:
38
+ * 「漏排是硬故障、多排是常态成本」)。
39
+ *
40
+ * ⇒ **不确定时宁可报"没跑"**(选便宜的那种错)⇒ **TTL 不宜过长**。
41
+ * 取 **30 分钟**:单轮静默超过 30 分钟仍无 `turn-stopping` 的情形罕见;
42
+ * 而真发生了,代价只是一次可能多余的唤起(5min tick 限制频率,且 CRO 程序自己还能看
43
+ * `pendingWakes` / `lastWakeAt` 二次判)。
44
+ *
45
+ * ## 键的选择
46
+ *
47
+ * **以载体(dsh 会话 id)为键**——因为事件本身就以 agent/session 为主语,直接映射最省。
48
+ * **查询时按轨迹聚合**(`listBoundSessionIds` → 「任一载体在跑」⇒ 该轨迹在跑)。
49
+ * ⚠️ 这也天然处理了「同一轨迹多载体」(S142 曾挂 4 条)的情形。
50
+ */
51
+ import type { Context } from 'cordis';
52
+ import type { Agent } from '@deepseek-ai/dsh-agent';
53
+ /** 一次「在跑」记录的 TTL(见文件头的方向性判据) */
54
+ export declare const TURN_TTL_MS: number;
55
+ /** 标记「在跑」(`agent/session-start` + `agent/status=running` 调用) */
56
+ export declare function markTurnRunning(sessionId: string, nowMs: number, turn?: number | null): void;
57
+ /** 清除「在跑」(`turn-stopping` / disposed 调用) */
58
+ export declare function clearTurnRunning(sessionId: string): void;
59
+ /**
60
+ * 列出**当前在跑**的载体 id(按 TTL 过滤 + **顺手清除过期项**)。
61
+ *
62
+ * 🔴 **本函数带副作用(清理)是刻意的**:它是"异常路径兜底"的唯一执行点——
63
+ * 只过滤不清除会让表留下永不过期的僵尸项(正是"只增不清"病)。
64
+ * @param nowMs 当前时刻
65
+ * @param ttlMs TTL(缺省 {@link TURN_TTL_MS})
66
+ * @returns 在跑的载体 id 列表(顺序不保证)
67
+ */
68
+ export declare function listRunningSessionIds(nowMs: number, ttlMs?: number): string[];
69
+ /**
70
+ * 按轨迹聚合:该轨迹**是否有载体在跑**(`listBoundSessionIds` 给候选,再与在跑集求交)。
71
+ * @param boundSessionIds 绑定该轨迹的载体 id
72
+ * @param nowMs 当前时刻
73
+ * @param ttlMs TTL
74
+ * @returns 在跑的载体 id(**可能多条**;空数组 = 没有在跑)
75
+ */
76
+ export declare function runningCarriersOf(boundSessionIds: readonly string[], nowMs: number, ttlMs?: number): string[];
77
+ /** 当前表大小(**诊断/测试用**;用于证明"清理真的发生了") */
78
+ export declare function croTurnTableSize(): number;
79
+ /** 测试用:复位(避免用例间串味) */
80
+ export declare function __resetCroTurnsForTest(): void;
81
+ /**
82
+ * 装配追踪(`index.ts` apply 调用):订阅四个既有事件 + 拆卸。
83
+ *
84
+ * **为什么订阅四个而不是两个**(R↓):
85
+ * · 前两个(`session-start` / `turn-stopping`)负责**语义**——"在不在跑";
86
+ * · 后两个(`agent/disposed` / `session/disposed`)负责**生命周期**——
87
+ * 载体没了必须清项,否则表按载体只增(本模块的头号风险)。
88
+ *
89
+ * ⚠️ **失败不抛**:任一事件通道缺失 ⇒ 响亮日志 + 该订阅跳过(**apply 不可成为启动单点**,
90
+ * 与 `registerDeepseekVisionPatch` 等既有装配同款纪律)。
91
+ * @param ctx 插件上下文
92
+ */
93
+ export declare function registerCroTurnTracking(ctx: Context): void;
94
+ /** 类型再导出(调用方少一处 import) */
95
+ export type { Agent as CroAgent };