@shgroup/dsh-serenity-hooks 1.42.0 → 1.43.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 +27 -0
- package/dsh.plugin.json +1 -1
- package/lib/cro-guide.d.ts +48 -0
- package/lib/cro-turns.d.ts +95 -0
- package/lib/cro.d.ts +306 -0
- package/lib/index.js +212 -7
- package/lib/tools/trajectory.d.ts +4 -1
- package/lib/{trajectory-bound-4xkZDfmO.js → trajectory-bound-kxvMuCui.js} +3 -2
- package/lib/trajectory-ops.d.ts +8 -2
- package/lib/{wake-scheduler-1AsvNQPW.js → wake-scheduler-DC4-imVO.js} +716 -3
- package/lib/wake-scheduler.d.ts +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -280,6 +280,33 @@ DSH 一个进程可以同时带多个工作区,每个工作区各自对接自
|
|
|
280
280
|
- **"周期"归工作区自己**:要一轮接一轮,就由**收到唤醒的那条轨迹**在每轮结束时再排一次下一轮——ACC 只提供"到点投递一条消息"这个原语,不替工作区决定节拍(焦点、节拍、偏见都由工作区自定;任意条轨迹并行、互不干扰)
|
|
281
281
|
- **轨迹可以自带 skill**:在它的 `SESSION.md` 顶部 frontmatter 写 `skills: [名字, …]`,这条轨迹**被绑定期间**的每个请求都会注入这些 skill 的全文(适合把某个领域的做事方法直接挂在轨迹上);skill 名来自工作区数据,**先过安全校验才会用于拼路径**,找不到会在提示里明说"缺失"而**不静默跳过**
|
|
282
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
|
+
|
|
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
|
|
|
285
312
|
### 6.6 安全模型
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.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": {
|
|
@@ -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,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 };
|
package/lib/cro.d.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cro.ts — CRO(Continuous Re-Occurrence · 持续再发生)机制层(S142 §7.8,2026-09-19)
|
|
3
|
+
*
|
|
4
|
+
* ## 这东西用来干什么(白话)
|
|
5
|
+
*
|
|
6
|
+
* 今天叫醒一条轨迹的方式是「几点几分叫我」。但**该不该醒,往往不是时间说了算**:
|
|
7
|
+
* 比如 S185 应该「天亮 + 家里有人 + 非高峰」才醒;已经在干活就不该再叫;
|
|
8
|
+
* 日志太大了,下次叫它时该让它先整理。
|
|
9
|
+
*
|
|
10
|
+
* CRO 让**一条轨迹自己带一段程序**,由 ACC 在每次检查时跑它,
|
|
11
|
+
* **由这段程序决定「现在该不该叫我、叫我的时候说什么」**。
|
|
12
|
+
*
|
|
13
|
+
* ## 归属(所有者 2026-09-19 定:**机制属 ACC,CCC 是用户**)
|
|
14
|
+
*
|
|
15
|
+
* | 面 | 归谁 | 内容 |
|
|
16
|
+
* |---|---|---|
|
|
17
|
+
* | **机制** | 🔴 **ACC(本模块)** | 契约 / 调度 / 执行 / 状态暴露 / 容错 |
|
|
18
|
+
* | **程序** | 🔴 **CCC(用户)** | 那段判定的 TS 代码,放**轨迹自己的目录**里 |
|
|
19
|
+
*
|
|
20
|
+
* ⇒ 与 `send-later` 同构:**工具在 ACC,用它在 CCC**。
|
|
21
|
+
*
|
|
22
|
+
* ## 命名(防混淆,写死)
|
|
23
|
+
*
|
|
24
|
+
* **`CRO` = 那段程序**(实体名),**不是** ACC 标准 §0.1 三环节(发生/存储/**再发生**)的第三环本身。
|
|
25
|
+
* 命名来源 = §0.3「**Trajectory 在寻找 Agent**」——CRO 是「再发生」这一环的自动化。
|
|
26
|
+
*
|
|
27
|
+
* ## 位置与形态(设计 §2)
|
|
28
|
+
*
|
|
29
|
+
* `<CCC 根>/AGENT_SESSIONS/<轨迹目录>/continuous-re-occurrence.ts`
|
|
30
|
+
* · **文件名全写**(所有者令);· **程序只有一个**:**文件在 = 启用,不在 = 禁用**(无 enabled 字段、无注册表)。
|
|
31
|
+
*
|
|
32
|
+
* ## 🔴 运行契约 = B 案(**ACC 只 spawn,从不 import**)
|
|
33
|
+
*
|
|
34
|
+
* 所有者 2026-09-19:「**B 是设计,A 只是文档友好**」。
|
|
35
|
+
* 若 ACC **import** 那段 TS,会出现两件事:
|
|
36
|
+
* ① ACC 依赖 CCC 的**源码路径**(npm 装机版在别处 ⇒ 两条路径都要活 = 两个真相源);
|
|
37
|
+
* ② **用户程序语法错会让 ACC 启动失败** ⇒ **一个用户程序的错误放倒整个容器**。
|
|
38
|
+
* ⇒ 进程边界把这两件事同时挡掉:**路径只需一个(文件系统),错误被隔离在子进程里**。
|
|
39
|
+
*
|
|
40
|
+
* ## 🔴 铁律:CRO 的任何失败,绝不影响既有机制(设计 §5)
|
|
41
|
+
*
|
|
42
|
+
* `send-later` / `send-now` / 唤醒表的投递**照常工作**。
|
|
43
|
+
* 先例 = `weixin-hook.ts` 的**旁路容忍**("超时 kill / 非 0 退出 / spawn 失败 → 仅日志返回,绝不抛")。
|
|
44
|
+
* ⇒ 本模块的**每一个**导出函数**都不抛错**(返回结构化失败)。
|
|
45
|
+
*/
|
|
46
|
+
/** 入口文件名(**全写**,所有者令 2026-09-19;不改缩写) */
|
|
47
|
+
export declare const CRO_FILENAME = "continuous-re-occurrence.ts";
|
|
48
|
+
/** 硬超时(沿用 `biasProvider` 先例的 60s;设计 §5 情形 2) */
|
|
49
|
+
export declare const CRO_TIMEOUT_MS = 60000;
|
|
50
|
+
/**
|
|
51
|
+
* 轨迹状态快照 —— ACC 把它**能看到的全部**序列化后喂给程序(所有者令:「信息尽可能多」)。
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ **边界(诚实标注)**:给满的是**「快照」**,不是「无限能力」——
|
|
54
|
+
* ACC **看不见**的东西(程序自己上次判了什么)**物理上给不了**(那是另一个进程的内存)。
|
|
55
|
+
* ⇒ 那类状态**归程序自己**(可在自己轨迹目录里写状态文件)。
|
|
56
|
+
*/
|
|
57
|
+
export interface CroSnapshot {
|
|
58
|
+
/** 快照格式版本(程序据此判兼容;未来加字段时递增) */
|
|
59
|
+
version: number;
|
|
60
|
+
/** 身份 */
|
|
61
|
+
identity: {
|
|
62
|
+
/** 🔴 硬锚:完整目录名(不解析编号格式,§0I U4) */
|
|
63
|
+
dirName: string;
|
|
64
|
+
/** 展示码(如 `S185`)——**派生**,不作识别依据 */
|
|
65
|
+
code: string;
|
|
66
|
+
/** CCC 根(绝对路径) */
|
|
67
|
+
cccRoot: string;
|
|
68
|
+
};
|
|
69
|
+
/** 时间(当地时区呈现,遵 D67) */
|
|
70
|
+
time: {
|
|
71
|
+
/** 当前时刻 — ISO(当地时区) */
|
|
72
|
+
now: string;
|
|
73
|
+
/** 当前 epoch 毫秒 */
|
|
74
|
+
nowMs: number;
|
|
75
|
+
/** 本地人读时刻 */
|
|
76
|
+
nowLocal: string;
|
|
77
|
+
};
|
|
78
|
+
/** 轨迹身体(SESSION.md 及其目录) */
|
|
79
|
+
body: {
|
|
80
|
+
sessionMdPath: string;
|
|
81
|
+
sessionMdBytes: number | null;
|
|
82
|
+
sessionMdMtime: string | null;
|
|
83
|
+
/** `references/` 目录清单(名 + 体积 + mtime);不存在 ⇒ 空数组 */
|
|
84
|
+
references: Array<{
|
|
85
|
+
name: string;
|
|
86
|
+
bytes: number;
|
|
87
|
+
mtime: string;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
/** 绑定与载体 */
|
|
91
|
+
binding: {
|
|
92
|
+
/** 绑定该轨迹的载体会话 id(按绑定时间倒序) */
|
|
93
|
+
boundSessionIds: string[];
|
|
94
|
+
/** 其中**当前 live** 的 */
|
|
95
|
+
liveSessionIds: string[];
|
|
96
|
+
/**
|
|
97
|
+
* 🔴 其中**正在跑轮次**的(设计 §3.2 —— 本机制**唯一需要 ACC 新增的状态**)。
|
|
98
|
+
* ⚠️ `live ≠ 在跑`:一条会话可以 live 而空闲。
|
|
99
|
+
* 宿主没有现成的 turn 运行标志 ⇒ 由 `cro-turns.ts` 用 `agent/session-start` +
|
|
100
|
+
* `agent/turn-stopping` 两个**既有契约事件**夹出来。
|
|
101
|
+
*/
|
|
102
|
+
runningSessionIds: string[];
|
|
103
|
+
};
|
|
104
|
+
/** 调度面 */
|
|
105
|
+
scheduling: {
|
|
106
|
+
/** 本轨迹在办的唤醒条目(`wake-registry` 中 state=pending 且 target 命中本轨迹) */
|
|
107
|
+
pendingWakes: Array<{
|
|
108
|
+
id: string;
|
|
109
|
+
at: string;
|
|
110
|
+
createdAt: string;
|
|
111
|
+
createdBy: string;
|
|
112
|
+
}>;
|
|
113
|
+
/** 调度器状态(armed / 全局闸 / tick 次数 / 上次跳过原因) */
|
|
114
|
+
scheduler: {
|
|
115
|
+
armed: boolean;
|
|
116
|
+
enabled: boolean;
|
|
117
|
+
ticks: number;
|
|
118
|
+
lastSkipReason: string | null;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** 快照装配的输入(**纯数据** ⇒ `buildCroSnapshot` 可穷举测试,无需 fs) */
|
|
123
|
+
export interface CroSnapshotInput {
|
|
124
|
+
dirName: string;
|
|
125
|
+
cccRoot: string;
|
|
126
|
+
nowMs: number;
|
|
127
|
+
sessionMdPath: string;
|
|
128
|
+
sessionMdBytes: number | null;
|
|
129
|
+
sessionMdMtimeMs: number | null;
|
|
130
|
+
references: Array<{
|
|
131
|
+
name: string;
|
|
132
|
+
bytes: number;
|
|
133
|
+
mtimeMs: number;
|
|
134
|
+
}>;
|
|
135
|
+
boundSessionIds: string[];
|
|
136
|
+
liveSessionIds: string[];
|
|
137
|
+
runningSessionIds: string[];
|
|
138
|
+
pendingWakes: Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
at: string;
|
|
141
|
+
createdAt: string;
|
|
142
|
+
createdBy: string;
|
|
143
|
+
}>;
|
|
144
|
+
scheduler: {
|
|
145
|
+
armed: boolean;
|
|
146
|
+
enabled: boolean;
|
|
147
|
+
ticks: number;
|
|
148
|
+
lastSkipReason: string | null;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 展示码派生(**只用于展示**,不作识别依据 —— §0I U4:锚永远是 `dirName`)。
|
|
153
|
+
*
|
|
154
|
+
* 判据(设计 §1):**编号不是固定格式,不同 CCC 格式不同**(本容器 `S###`,别处可能是 issue 号
|
|
155
|
+
* 或自定义前缀)⇒ 这里只做**尽力而为的展示性提取**,取不到就返回空串。
|
|
156
|
+
* 🔴 **绝不**因为取不到就拒绝服务——`dirName` 才是硬锚。
|
|
157
|
+
* @param dirName 轨迹目录名
|
|
158
|
+
* @returns `S###` 形态的展示码;取不到 ⇒ `''`
|
|
159
|
+
*/
|
|
160
|
+
export declare function deriveCode(dirName: string): string;
|
|
161
|
+
/**
|
|
162
|
+
* 装配快照(**纯函数**:只做数据整形,不读 fs、不看 ctx)⇒ 可穷举测试。
|
|
163
|
+
* @param input 已读好的原始数据
|
|
164
|
+
* @returns 喂给 CRO 程序的快照
|
|
165
|
+
*/
|
|
166
|
+
export declare function buildCroSnapshot(input: CroSnapshotInput): CroSnapshot;
|
|
167
|
+
/** CRO 程序输出的决策 */
|
|
168
|
+
export interface CroDecision {
|
|
169
|
+
/** 🔴 是否唤起 */
|
|
170
|
+
wake: boolean;
|
|
171
|
+
/** 🔴 唤起时的提示词(`wake=true` 时必填、非空) */
|
|
172
|
+
prompt: string | null;
|
|
173
|
+
/** 可选:判定理由(进日志,供事后重建 —— 设计 §4.2) */
|
|
174
|
+
reason: string | null;
|
|
175
|
+
}
|
|
176
|
+
/** 解析结果(**不抛错**) */
|
|
177
|
+
export type CroParseResult = {
|
|
178
|
+
ok: true;
|
|
179
|
+
decision: CroDecision;
|
|
180
|
+
} | {
|
|
181
|
+
ok: false;
|
|
182
|
+
error: string;
|
|
183
|
+
detail?: string;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* 解析 CRO 程序 stdout(**纯函数** ⇒ 可穷举测试)。
|
|
187
|
+
*
|
|
188
|
+
* ## 边界(设计 §4.1)
|
|
189
|
+
* · `wake` 缺省 / `false` ⇒ **不唤起**(**这是常态**)——`prompt` 可省;
|
|
190
|
+
* · `wake: true` 但 `prompt` 空 / 非串 ⇒ 🔴 **非法** ⇒ 返回错误(调用方**跳过本轮**,不投递空消息);
|
|
191
|
+
* · 非 JSON / 非对象 ⇒ 错误。
|
|
192
|
+
*
|
|
193
|
+
* ## 🔴 为什么 `reason` 重要(CCE:重建 > 保存)
|
|
194
|
+
* 改成程序判定后,**「当时为什么叫了」不再能从时间表重建**(原因在程序肚子里:
|
|
195
|
+
* 可能有随机、可能看了外部数据)⇒ 要求程序自报理由,把「决策依据」重新变成**可重建的**。
|
|
196
|
+
* ⇒ **强烈建议但不强制**(强制会让简单程序难写;设计 §9-6 待裁,本版取"建议")。
|
|
197
|
+
*
|
|
198
|
+
* @param stdout 程序标准输出(可含前后空白;允许多行 JSON 文本)
|
|
199
|
+
* @returns 决策 或 结构化错误
|
|
200
|
+
*/
|
|
201
|
+
export declare function parseCroOutput(stdout: string): CroParseResult;
|
|
202
|
+
/** 单次执行结果(**永不抛**) */
|
|
203
|
+
export interface CroRunResult {
|
|
204
|
+
ok: boolean;
|
|
205
|
+
/** 程序 stdout(已截断);失败时可能为部分输出 */
|
|
206
|
+
stdout: string;
|
|
207
|
+
/** 失败原因(人读;成功 ⇒ null) */
|
|
208
|
+
error: string | null;
|
|
209
|
+
/** 失败分类(稳定码,供日志/测试断言) */
|
|
210
|
+
code?: string;
|
|
211
|
+
}
|
|
212
|
+
/** runner 签名(**可注入** —— 测试捕获防真实 spawn flake;同 `weixin-hook.ts` 手法) */
|
|
213
|
+
export type CroRunner = (scriptAbs: string, stdinJson: string, timeoutMs: number) => Promise<CroRunResult>;
|
|
214
|
+
/**
|
|
215
|
+
* CRO 程序路径:`<CCC 根>/AGENT_SESSIONS/<dirName>/continuous-re-occurrence.ts`。
|
|
216
|
+
*
|
|
217
|
+
* ⚠️ 路径逃逸校验(`resolveInside`):`dirName` 来自配置/工具入参,
|
|
218
|
+
* 必须确保解析后仍在 CCC 根内(同 `weixin-hook.ts:160` 手法)。**抛错由调用方吞**。
|
|
219
|
+
* @param root CCC 根
|
|
220
|
+
* @param dirName 轨迹目录名
|
|
221
|
+
* @returns 绝对路径
|
|
222
|
+
*/
|
|
223
|
+
export declare function croScriptPath(root: string, dirName: string): string;
|
|
224
|
+
/** CRO 是否启用(**判据 = 文件在不在**;设计 §2.1:无 enabled 字段、无注册表) */
|
|
225
|
+
export declare function isCroEnabled(root: string, dirName: string): boolean;
|
|
226
|
+
/**
|
|
227
|
+
* 列出**启用了 CRO 的轨迹目录名**(供调度器每 tick 扫描,设计 §6.1)。
|
|
228
|
+
*
|
|
229
|
+
* ## 判据与形态(R↓)
|
|
230
|
+
* · 判据 = **文件在不在**(同 §2.1)——**没有注册表**,所以"谁启用了"只能靠**扫目录**:
|
|
231
|
+
* `AGENT_SESSIONS/` 下每个目录查一次 `<目录>/continuous-re-occurrence.ts`。
|
|
232
|
+
* · **一次 readdir + 每个目录一次 existsSync**:N 条轨迹的代价是 O(N) 次 `stat`,
|
|
233
|
+
* 每 5min 一次 —— 与既有 `listSessions` 同量级,可接受。
|
|
234
|
+
* · 跳过 **`_` 前缀**(`_archived` / `_skiff-logs` / `_weixin-logs` = 系统与日志目录)
|
|
235
|
+
* 与 **`.` 前缀**(隐藏)——它们**不是轨迹**,且扫它们纯属浪费。
|
|
236
|
+
* · 🔴 **本函数绝不抛错**(返回 `[]`)——它跑在调度器 tick 内,任何异常都可能影响既有链路
|
|
237
|
+
* (设计 §5 铁律)。目录读不到(不存在 / 权限)⇒ `[]` = "没有轨迹启用 CRO",语义正确。
|
|
238
|
+
*
|
|
239
|
+
* ⚠️ **为什么不做缓存**:缓存会引入"文件删了但缓存还在"的失效模式(本容器栽过的"第二真相源"),
|
|
240
|
+
* 而这里省下的只是一次 `readdir`——**不值当**。
|
|
241
|
+
*
|
|
242
|
+
* @param root CCC 根
|
|
243
|
+
* @returns 启用了 CRO 的轨迹目录名(**排序后**,保证同 tick 顺序稳定、便于日志比对)
|
|
244
|
+
*/
|
|
245
|
+
export declare function listCroTrajectories(root: string): string[];
|
|
246
|
+
/**
|
|
247
|
+
* 执行一次 CRO 程序(**永不抛** —— 旁路容忍铁律)。
|
|
248
|
+
*
|
|
249
|
+
* runner 顺序(照 `weixin-hook.ts:171-174` 的 bun 优先 / node 兜底):
|
|
250
|
+
* `bun` → `process.execPath`(同运行时)。
|
|
251
|
+
* 判据:`ENOENT`(二进制不存在)⇒ 试下一个;**其余失败视为最终结果**
|
|
252
|
+
* (程序自己报错就是报错,不该用另一个 runner 掩盖——与 hook 一致)。
|
|
253
|
+
* @param scriptAbs 程序绝对路径
|
|
254
|
+
* @param stdinJson 喂给程序的快照 JSON
|
|
255
|
+
* @param timeoutMs 硬超时
|
|
256
|
+
* @returns 执行结果(含输出的解析交由 `parseCroOutput`)
|
|
257
|
+
*/
|
|
258
|
+
export declare function runCroProcess(scriptAbs: string, stdinJson: string, timeoutMs?: number): Promise<CroRunResult>;
|
|
259
|
+
/** 读 SESSION.md 体积(供看门狗判据等复用;读不到 ⇒ null) */
|
|
260
|
+
export declare function readSessionMdBytes(mdPath: string): number | null;
|
|
261
|
+
/** 读取快照所需的 fs 侧数据(**纯读取,不抛**) */
|
|
262
|
+
export declare function readCroSnapshotInput(root: string, dirName: string, nowMs: number, extras: {
|
|
263
|
+
liveSessionIds: string[];
|
|
264
|
+
runningSessionIds: string[];
|
|
265
|
+
boundSessionIds: string[];
|
|
266
|
+
pendingWakes: CroSnapshot['scheduling']['pendingWakes'];
|
|
267
|
+
scheduler: CroSnapshot['scheduling']['scheduler'];
|
|
268
|
+
}): CroSnapshotInput;
|
|
269
|
+
/** 一次 CRO 评估的结果(四态,调用方按 status 分派) */
|
|
270
|
+
export type CroOutcome = {
|
|
271
|
+
status: 'disabled';
|
|
272
|
+
detail: string;
|
|
273
|
+
} | {
|
|
274
|
+
status: 'skipped';
|
|
275
|
+
detail: string;
|
|
276
|
+
code?: string;
|
|
277
|
+
} | {
|
|
278
|
+
status: 'no-wake';
|
|
279
|
+
decision: CroDecision;
|
|
280
|
+
detail: string;
|
|
281
|
+
} | {
|
|
282
|
+
status: 'wake';
|
|
283
|
+
decision: CroDecision;
|
|
284
|
+
prompt: string;
|
|
285
|
+
detail: string;
|
|
286
|
+
};
|
|
287
|
+
/** 评估依赖(可注入 ⇒ 单测无需真实 spawn/fs) */
|
|
288
|
+
export interface CroDeps {
|
|
289
|
+
runner?: CroRunner;
|
|
290
|
+
timeoutMs?: number;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 评估一条轨迹的 CRO 程序:**文件在 ⇒ 跑它;不在 ⇒ 未启用**(设计 §2.1)。
|
|
294
|
+
*
|
|
295
|
+
* 🔴 **本函数永不抛错**(旁路容忍铁律):任何失败都返回 `{status:'skipped'}`,
|
|
296
|
+
* 调用方据此**跳过本轮**、**不影响既有投递链路**。
|
|
297
|
+
*
|
|
298
|
+
* @param root CCC 根
|
|
299
|
+
* @param dirName 轨迹目录名
|
|
300
|
+
* @param snapshotInput 已读好的快照输入
|
|
301
|
+
* @param deps 可注入依赖(测试用)
|
|
302
|
+
* @returns 四态结果
|
|
303
|
+
*/
|
|
304
|
+
export declare function evaluateCro(root: string, dirName: string, snapshotInput: CroSnapshotInput, deps?: CroDeps): Promise<CroOutcome>;
|
|
305
|
+
/** 人读一行(日志用;`evaluateCro` 结果的稳定摘要) */
|
|
306
|
+
export declare function renderCroOutcome(dirName: string, outcome: CroOutcome): string;
|