@shgroup/dsh-serenity-hooks 1.34.0 → 1.34.2
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 +1 -0
- package/dsh.plugin.json +1 -1
- package/lib/acp-core.d.ts +2 -22
- package/lib/acp-http.d.ts +8 -0
- package/lib/api.d.ts +12 -4
- package/lib/autopilot-chain.d.ts +76 -0
- package/lib/autopilot-core-DCRkiWEe.js +229 -0
- package/lib/autopilot-core.d.ts +132 -0
- package/lib/autopilot-ops.d.ts +48 -0
- package/lib/autopilot-trajectory-D9Znxo75.js +782 -0
- package/lib/autopilot-trajectory.d.ts +64 -97
- package/lib/ccc-roots-Bd_SjEs7.js +152 -0
- package/lib/ccc-roots.d.ts +105 -0
- package/lib/ccc.d.ts +3 -6
- package/lib/client.js +2 -2
- package/lib/clock-runtime.d.ts +197 -0
- package/lib/config-ops.d.ts +9 -14
- package/lib/constants.d.ts +0 -2
- package/lib/container-status.d.ts +183 -0
- package/lib/diag-ops.d.ts +25 -10
- package/lib/face-host.d.ts +110 -0
- package/lib/fs-ops.d.ts +3 -2
- package/lib/gateway-auth.d.ts +2 -5
- package/lib/gateway.d.ts +0 -34
- package/lib/git-ops.d.ts +3 -2
- package/lib/handyman-ops.d.ts +3 -2
- package/lib/handyman-preset-inherit.d.ts +2 -1
- package/lib/host/access.d.ts +8 -7
- package/lib/host/contract.d.ts +29 -6
- package/lib/host/type-contract.d.ts +1 -2
- package/lib/im-bridge.d.ts +6 -5
- package/lib/index.js +8336 -5607
- package/lib/invariant.d.ts +0 -15
- package/lib/invariant.js +1 -1
- package/lib/kit-ops.d.ts +11 -31
- package/lib/localstore-ops.d.ts +3 -17
- package/lib/msm-ops.d.ts +34 -52
- package/lib/opencode-provider.d.ts +4 -5
- package/lib/output-guard-seam.d.ts +0 -3
- package/lib/output-guard.d.ts +12 -3
- package/lib/ports.d.ts +59 -0
- package/lib/rebuild.d.ts +1 -1
- package/lib/seams/bootstrap.d.ts +4 -3
- package/lib/seams/compact.d.ts +2 -1
- package/lib/seams/context.d.ts +2 -4
- package/lib/seams/env.d.ts +2 -1
- package/lib/seams/guards.d.ts +4 -43
- package/lib/seams/keeper.d.ts +1 -1
- package/lib/seams/system-prompt.d.ts +1 -5
- package/lib/session-cleanup.d.ts +3 -2
- package/lib/{settings-section-BuPGQv_H.js → settings-section-DTYZjwUs.js} +65 -9
- package/lib/settings-section.d.ts +3 -31
- package/lib/skiff-core.d.ts +4 -9
- package/lib/skiff-debug.d.ts +15 -21
- package/lib/skiff-registry-FTjoWQTQ.js +25 -0
- package/lib/skiff-registry.d.ts +2 -1
- package/lib/{skiff-role-DAfPb6YQ.js → skiff-role-CEHL5cek.js} +1 -1
- package/lib/skiff-role.d.ts +2 -2
- package/lib/skills/opencode-scan.d.ts +3 -2
- package/lib/skills-discovery.d.ts +15 -2
- package/lib/status.d.ts +2 -1
- package/lib/tools/cce.d.ts +1 -2
- package/lib/tools/container-admin.d.ts +7 -3
- package/lib/tools/eap.d.ts +2 -3
- package/lib/tools/handyman.d.ts +0 -29
- package/lib/tools/msm.d.ts +0 -4
- package/lib/tools/neat.d.ts +1 -2
- package/lib/tools/praxis.d.ts +0 -1
- package/lib/tools/skiff-admin.d.ts +2 -2
- package/lib/tools/trajectory.d.ts +7 -15
- package/lib/totp.d.ts +0 -4
- package/lib/trajectory-assistant.d.ts +2 -1
- package/lib/trajectory-bound-Cax1p9ut.js +190 -0
- package/lib/trajectory-bound.d.ts +3 -4
- package/lib/{trajectory-bound-C8DUxi6_.js → trajectory-ops-mrXK0H0I.js} +2 -159
- package/lib/trajectory-ops.d.ts +4 -9
- package/lib/trajectory-skills.d.ts +71 -0
- package/lib/{wake-registry-D0Vj0WXb.js → wake-registry-CYxIb484.js} +7 -20
- package/lib/wake-registry.d.ts +3 -11
- package/lib/wake-scheduler.d.ts +13 -49
- package/lib/web-fetch-provider.d.ts +0 -21
- package/lib/{weixin-api-PjEhmjlZ.js → weixin-api-CtFYm53S.js} +3 -9
- package/lib/weixin-api.d.ts +6 -37
- package/lib/weixin-bridge.d.ts +5 -24
- package/lib/weixin-hook.d.ts +6 -12
- package/lib/weixin-output-guard.d.ts +2 -1
- package/lib/{weixin-route-D1ND823E.js → weixin-route-B2ajFypI.js} +4 -5
- package/lib/weixin-route.d.ts +0 -2
- package/lib/weixin-send-api.d.ts +15 -6
- package/package.json +2 -4
- package/experiments/autopilot-trajectory/SKILL.md +0 -115
- package/experiments/autopilot-trajectory/scripts/autopilot-trajectory.ts +0 -546
- package/lib/autopilot-script.d.ts +0 -40
- package/lib/skiff-debug-D83-Njy7.js +0 -2867
- package/lib/{ccc-eu_IPF94.js → ccc-NlLr_sxy.js} +1 -1
- package/lib/{session-cleanup-CMT4mHWt.js → session-cleanup-B6IHZgww.js} +1 -1
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { i as hostSessions, t as hostAgents } from "./access-fiehjxV6.js";
|
|
3
|
+
import { r as cccRootForCwd } from "./ccc-roots-Bd_SjEs7.js";
|
|
4
|
+
import { p as sessionEvents } from "./trajectory-ops-mrXK0H0I.js";
|
|
5
|
+
import { r as readLastBound } from "./trajectory-bound-Cax1p9ut.js";
|
|
6
|
+
import { _ as shouldWake, a as TICK_MS, d as isAutopilotSession, g as resolveTargetMd, i as MIN_INTERVAL_HOURS, l as fetchBiasContent, m as readSelfGeneratedMotivation, n as DEFAULT_AVOID_HOURS, p as readAutopilotSettings, s as beijingHour, u as inAllowedWakeWindow } from "./autopilot-core-DCRkiWEe.js";
|
|
7
|
+
import { t as readSimpleSettings } from "./settings-section-DTYZjwUs.js";
|
|
8
|
+
import { basename, dirname } from "node:path";
|
|
9
|
+
import { statSync } from "node:fs";
|
|
10
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
11
|
+
//#region src/clock-runtime.ts
|
|
12
|
+
/**
|
|
13
|
+
* clock-runtime.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
|
|
14
|
+
*
|
|
15
|
+
* 本仓有**两条各自独立运行**的 5min 时钟:
|
|
16
|
+
* · 唤醒调度器(`wake-scheduler.ts`,一次性唤醒,D58)
|
|
17
|
+
* · autopilot 时钟(`autopilot-trajectory.ts`,周期自唤醒,D59)
|
|
18
|
+
* 二者的**业务判据完全不同**(谁来扫 / 扫什么 / 投递给谁 / 按什么周期),但**引擎骨架逐字同形**
|
|
19
|
+
* ——进程态 + 快照 + 复位 + `startTimer` + tick 外壳 + 事件接线 + disposer + `timer.unref()`。
|
|
20
|
+
* 本条把**引擎**抽到本工厂,**各钟仍各持自己的定时器与自己的串行链**。
|
|
21
|
+
*
|
|
22
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
23
|
+
* 🔴 硬约束一:**两条时钟绝不可共用"排队执行"的链**(这是 P2 与 P1 的分界判据)
|
|
24
|
+
*
|
|
25
|
+
* autopilot 每轮要跑该 CCC 的**偏见脚本**(`fetchBiasContent`,超时 60s),可能阻塞数十秒;
|
|
26
|
+
* 若与唤醒调度器共用一条串行链,会把另一条一起**卡住**(一次性唤醒被无关的偏见脚本拖住)。
|
|
27
|
+
* ⇒ 本工厂把 `chain` 作为**每实例私有状态**:`createClock()` 调用一次 = 一条独立的串行域。
|
|
28
|
+
* 实例之间**零共享**(无模块级单例、无共享 Promise、无共享 map)。
|
|
29
|
+
* ⚠️ 若日后有人把 `chain` 提到模块级"省一次分配",**两条时钟的隔离即刻失效**——不许。
|
|
30
|
+
*
|
|
31
|
+
* 🔴 硬约束二:**武装门只判全局闸**(`gate()`),**不判"本次有无目标"**
|
|
32
|
+
*
|
|
33
|
+
* 2026-09-14 F 段实测缺陷:旧实现把"有 live CCC"当**武装前置条件**,而宿主刚重启时 live 会话
|
|
34
|
+
* 必为空,且**"恢复旧会话"不触发 `session/created`**(只有新建会)⇒ 启动瞬间判定落空 =
|
|
35
|
+
* **时钟永久不武装**(实测 6.6h 零 tick)。⇒ 现在「**全局闸开即武装**」,"有无目标"降级为
|
|
36
|
+
* tick 内的廉价判定。**武装与"本次有无目标"是两件事,本工厂不得把二者耦合。**
|
|
37
|
+
*
|
|
38
|
+
* 🔴 硬约束三:**两条闸必须继续分开传**(v1.34 S-1 解耦)
|
|
39
|
+
*
|
|
40
|
+
* 唤醒调度器 `wakeSchedulerEnabled`(**缺省开**)|autopilot `autopilotWakeEnabled`
|
|
41
|
+
* (**缺省关**)——取值方向相反。工厂取 `gate` **回调**(每次现读),**不做**任何合并或缓存。
|
|
42
|
+
*
|
|
43
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
44
|
+
* 两条时钟**逐项语义差**(`ClockOptions` 的每个可配项都对应下表一行,不是随意的灵活性):
|
|
45
|
+
*
|
|
46
|
+
* | 项 | 唤醒调度器 | autopilot | 工厂表达 |
|
|
47
|
+
* |----|-----------|-----------|---------|
|
|
48
|
+
* | `chain` | 有(全局串行) | 有(全局串行) | **总是私有**(硬约束一) |
|
|
49
|
+
* | `lastTickLog` | 有(逐行摘要) | **无** | `logFrom` 不给 = 不记 |
|
|
50
|
+
* | `ticks` 记账 | 工厂在 body **完成后**记 | **本钟**在同步段记 | `bodyCountsTick` + `begin` |
|
|
51
|
+
* | body 返回类型 | `string[]`(人读摘要) | `void` | 泛型 `T` |
|
|
52
|
+
* | 启动日志 | 固定串 | 含启用 CCC 计数 | `startLog: () => string` |
|
|
53
|
+
* | body 异常兜底 | 留痕 + `console.warn` | 同 | 工厂统一(**吞异常不改语义**) |
|
|
54
|
+
*
|
|
55
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
56
|
+
* **本工厂是"加法式"重构**(P2 的性质):不新增任何判定逻辑,只把同形骨架收成一份代码;
|
|
57
|
+
* 两个模块仍各自导出 `wakeSchedulerState()` / `autopilotClockState()` 与各自的
|
|
58
|
+
* `__reset…ForTest()`,**对外可观测面(字段名与语义)零变化**——`acc-diag` ①b 段、
|
|
59
|
+
* `container-status.ts:containerClocks()` 与面板都在读它们(见该文件 :225–250 的设计红线)。
|
|
60
|
+
*
|
|
61
|
+
* 为什么**不**取 P1(真合并成一个 `setInterval` + 两条策略)——2026-09-15 所有者裁决:
|
|
62
|
+
* P1 只多省 ~20 行,却要把 `armed` 拆成"宿主 + 每策略"两层、并强行分开串行域,
|
|
63
|
+
* 即**动两条正在跑的时钟的骨架**;**不值当**(裁决原文见 SESSION §12.8②)。
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* 挂事件 → 触发 `start()`(**每个事件一次 try/catch 吞异常**:事件通道缺失不阻断时钟)。
|
|
67
|
+
*
|
|
68
|
+
* 为什么吞异常:宿主(headless profile 等)可能没有事件通道;**启动时已 `start()` 过一次**,
|
|
69
|
+
* 事件只是"跟上面板开关 / 新会话"的热启动补丁,缺它不该让整个装配抛错。
|
|
70
|
+
* @param on 宿主的 `on`(可能不存在)
|
|
71
|
+
* @param ctx `on` 的 `this`(宿主 API 常要求绑定)
|
|
72
|
+
* @param clock 目标时钟
|
|
73
|
+
* @param events 事件名列表(回调恒为 `() => clock.start()`)
|
|
74
|
+
*/
|
|
75
|
+
function attachClockEvents(ctx, clock, events) {
|
|
76
|
+
const on = ctx?.on;
|
|
77
|
+
if (typeof on !== "function") return;
|
|
78
|
+
for (const name of events) try {
|
|
79
|
+
on.call(ctx, name, () => clock.start());
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 建一条时钟(**每钟各调一次**——本工厂**不是**单例,调用次数 = 时钟条数)。
|
|
84
|
+
*
|
|
85
|
+
* 顺序忠实于既有实现:`start()` 先武装并立即跑一次 tick → 再挂事件。
|
|
86
|
+
* 拆卸由调用方经 `registerDisposer(ctx, label, clock.dispose)` 落到宿主的 `ctx.effect`。
|
|
87
|
+
* @param opts 见 {@link ClockOptions}(每项对应一条既有的语义差)
|
|
88
|
+
* @returns 该钟的 `{start, dispose, snapshot, reset}`
|
|
89
|
+
*/
|
|
90
|
+
function createClock(opts) {
|
|
91
|
+
const runtime = {
|
|
92
|
+
armed: false,
|
|
93
|
+
armedAt: null,
|
|
94
|
+
lastTickAt: null,
|
|
95
|
+
ticks: 0,
|
|
96
|
+
lastSkipReason: null
|
|
97
|
+
};
|
|
98
|
+
if (opts.logFrom) runtime.lastTickLog = [];
|
|
99
|
+
const st = {
|
|
100
|
+
timer: null,
|
|
101
|
+
chain: Promise.resolve(),
|
|
102
|
+
runtime,
|
|
103
|
+
wired: false,
|
|
104
|
+
opts
|
|
105
|
+
};
|
|
106
|
+
/** 记一次 tick(工厂内部与 body 共用同一实现——只有一处会计逻辑) */
|
|
107
|
+
const countTick = () => {
|
|
108
|
+
runtime.lastTickAt = Date.now();
|
|
109
|
+
runtime.ticks += 1;
|
|
110
|
+
};
|
|
111
|
+
/** 逐行打印(`logFrom` 缺省 ⇒ 本钟无 tick 日志,整段为死代码路径) */
|
|
112
|
+
const emit = (lines) => {
|
|
113
|
+
if (!opts.logFrom) return;
|
|
114
|
+
for (const line of lines) console.log(`${opts.logFrom.prefix}${line}`);
|
|
115
|
+
};
|
|
116
|
+
const tick = () => {
|
|
117
|
+
if (!opts.gate()) {
|
|
118
|
+
runtime.lastSkipReason = opts.gateOffReason;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (opts.begin) try {
|
|
122
|
+
opts.begin();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn(`[serenity-hooks] ✗ tick 前置异常: ${String(err?.message ?? err)}(${opts.label})`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
st.chain = st.chain.then(async () => {
|
|
128
|
+
try {
|
|
129
|
+
const value = await opts.body();
|
|
130
|
+
if (!opts.bodyCountsTick) countTick();
|
|
131
|
+
if (opts.logFrom) {
|
|
132
|
+
runtime.lastTickLog = opts.logFrom.lines(value);
|
|
133
|
+
emit(runtime.lastTickLog);
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const msg = `✗ tick 异常: ${String(err?.message ?? err)}`;
|
|
137
|
+
if (opts.logFrom) runtime.lastTickLog = [msg];
|
|
138
|
+
console.warn(`[serenity-hooks] ${msg}(${opts.label})`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* 幂等启动 + 武装门:**已武装 → 原样返回**(防重复起定时器);
|
|
144
|
+
* **闸关 → 不武装**(零资源占用);闸开则**无条件**武装(硬约束二:不判"有无目标")。
|
|
145
|
+
*/
|
|
146
|
+
const start = () => {
|
|
147
|
+
if (st.timer) return;
|
|
148
|
+
if (opts.events && !st.wired) {
|
|
149
|
+
st.wired = true;
|
|
150
|
+
attachClockEvents(opts.ctx, api, opts.events);
|
|
151
|
+
}
|
|
152
|
+
if (!opts.gate()) return;
|
|
153
|
+
st.timer = setInterval(tick, TICK_MS);
|
|
154
|
+
st.timer.unref();
|
|
155
|
+
runtime.armed = true;
|
|
156
|
+
runtime.armedAt = Date.now();
|
|
157
|
+
console.log(opts.startLog());
|
|
158
|
+
if (opts.immediate !== false) tick();
|
|
159
|
+
};
|
|
160
|
+
const dispose = () => {
|
|
161
|
+
if (st.timer) {
|
|
162
|
+
clearInterval(st.timer);
|
|
163
|
+
st.timer = null;
|
|
164
|
+
}
|
|
165
|
+
runtime.armed = false;
|
|
166
|
+
runtime.armedAt = null;
|
|
167
|
+
opts.onDispose?.();
|
|
168
|
+
};
|
|
169
|
+
const api = {
|
|
170
|
+
start,
|
|
171
|
+
dispose,
|
|
172
|
+
snapshot: () => ({
|
|
173
|
+
enabled: opts.gate(),
|
|
174
|
+
...runtime,
|
|
175
|
+
...runtime.lastTickLog ? { lastTickLog: [...runtime.lastTickLog] } : {}
|
|
176
|
+
}),
|
|
177
|
+
noteSkipReason: (reason) => {
|
|
178
|
+
runtime.lastSkipReason = reason;
|
|
179
|
+
},
|
|
180
|
+
countTick,
|
|
181
|
+
enqueue: (work) => {
|
|
182
|
+
st.chain = st.chain.then(work, () => void 0).then(() => void 0, (err) => {
|
|
183
|
+
console.warn(`[serenity-hooks] ✗ 串行任务异常(${opts.label}): ${String(err?.message ?? err)}`);
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
reset: () => {
|
|
187
|
+
if (st.timer) {
|
|
188
|
+
clearInterval(st.timer);
|
|
189
|
+
st.timer = null;
|
|
190
|
+
}
|
|
191
|
+
runtime.armed = false;
|
|
192
|
+
runtime.armedAt = null;
|
|
193
|
+
runtime.lastTickAt = null;
|
|
194
|
+
runtime.ticks = 0;
|
|
195
|
+
runtime.lastSkipReason = null;
|
|
196
|
+
if (opts.logFrom) runtime.lastTickLog = [];
|
|
197
|
+
st.wired = false;
|
|
198
|
+
opts.onReset?.();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
return api;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/host/effect.ts
|
|
205
|
+
/** 已告警的标签(进程级去重:重复装配/多测试替身不刷屏,但绝不静默) */
|
|
206
|
+
const warnedLabels = /* @__PURE__ */ new Set();
|
|
207
|
+
/**
|
|
208
|
+
* 登记一个卸载时执行的清理。
|
|
209
|
+
* @param ctx 宿主插件上下文
|
|
210
|
+
* @param label 日志标签(定位是哪个资源未装配)
|
|
211
|
+
* @param cleanup 卸载时执行;抛错被吞掉(不阻断其它清理)
|
|
212
|
+
* @returns 是否成功装配(false = 宿主无 `ctx.effect`,已告警)
|
|
213
|
+
*/
|
|
214
|
+
function registerDisposer(ctx, label, cleanup) {
|
|
215
|
+
const effect = ctx?.effect;
|
|
216
|
+
if (typeof effect !== "function") {
|
|
217
|
+
if (!warnedLabels.has(label)) {
|
|
218
|
+
warnedLabels.add(label);
|
|
219
|
+
console.warn(`[serenity-hooks] ✗ 资源拆卸未装配(${label}):ctx.effect 不可用(宿主契约缺失)`);
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
effect.call(ctx, () => () => {
|
|
225
|
+
try {
|
|
226
|
+
cleanup();
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.warn(`[serenity-hooks] ✗ 资源清理失败(${label}): ${String(err?.message ?? err)}`);
|
|
229
|
+
}
|
|
230
|
+
}, `dsh-serenity-hooks: ${label}`);
|
|
231
|
+
return true;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.warn(`[serenity-hooks] ✗ 资源拆卸注册失败(${label}): ${String(err?.message ?? err)}`);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/autopilot-trajectory.ts
|
|
239
|
+
var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
|
|
240
|
+
AUDIT_HISTORY_MAX: () => 50,
|
|
241
|
+
autopilotClockState: () => autopilotClockState,
|
|
242
|
+
autopilotGloballyEnabled: () => autopilotGloballyEnabled,
|
|
243
|
+
autopilotRuntimeFacts: () => autopilotRuntimeFacts,
|
|
244
|
+
autopilotWakeInFlight: () => autopilotWakeInFlight,
|
|
245
|
+
buildWakeMessage: () => buildWakeMessage,
|
|
246
|
+
collectAutopilotCccs: () => collectAutopilotCccs,
|
|
247
|
+
diagLive: () => diagLive,
|
|
248
|
+
getAutopilotStatus: () => getAutopilotStatus,
|
|
249
|
+
listLiveSessions: () => listLiveSessions,
|
|
250
|
+
performAutopilotWake: () => performAutopilotWake,
|
|
251
|
+
recordWake: () => recordWake,
|
|
252
|
+
registerAutopilot: () => registerAutopilot,
|
|
253
|
+
wakeHistoryFor: () => wakeHistoryFor
|
|
254
|
+
});
|
|
255
|
+
const PLUGIN_SOURCE = {
|
|
256
|
+
kind: "plugin",
|
|
257
|
+
plugin: "dsh-serenity-hooks"
|
|
258
|
+
};
|
|
259
|
+
/** 唤起消息(四段式:轨迹焦点[CCC 定义,稳定] / 身份锚定 / 先验偏见[自生动机+偏见内容] / 任务)——注入前台会话,用户可见 */
|
|
260
|
+
function buildWakeMessage(opts) {
|
|
261
|
+
const intervalLabel = opts.intervalHours >= 1 ? `${opts.intervalHours} 小时` : `约 ${Math.round(opts.intervalHours * 60)} 分钟`;
|
|
262
|
+
const lines = [];
|
|
263
|
+
if (opts.topPrompt) {
|
|
264
|
+
lines.push(`[轨迹焦点] ${opts.topPrompt}`);
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
lines.push(`[Autopilot Trajectory · 唤起] — 距上次轨迹活动已满 ${intervalLabel},自动继续。`, "", `身份锚定:继续 ${opts.sessionName} 的 trajectory(SESSION.md: ${opts.mdPath})。`, "先验偏见:");
|
|
268
|
+
if (opts.motivation) lines.push(` · 自生动机:${opts.motivation}`);
|
|
269
|
+
if (opts.biasContent) lines.push(` · 偏见内容:${opts.biasContent}`);
|
|
270
|
+
if (!opts.motivation && !opts.biasContent) lines.push(" · (无——本轮纯自主探索)");
|
|
271
|
+
lines.push("", "任务:执行一轮自主认知(探索/反事实检验),把产出写入 SESSION.md", "「自主探索日志」段,并预写「下一轮动机」段。完成后自然结束。");
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
/** 审计历史(root → 最近唤醒记录;进程生命周期;上限 AUDIT_HISTORY_MAX/CCC) */
|
|
275
|
+
const wakeHistory = /* @__PURE__ */ new Map();
|
|
276
|
+
/** 追加审计记录(ring 截断) */
|
|
277
|
+
function recordWake(root, rec) {
|
|
278
|
+
const list = wakeHistory.get(root) ?? [];
|
|
279
|
+
list.push(rec);
|
|
280
|
+
if (list.length > 50) list.splice(0, list.length - 50);
|
|
281
|
+
wakeHistory.set(root, list);
|
|
282
|
+
}
|
|
283
|
+
/** 读审计历史(root 的最近记录;无 → []) */
|
|
284
|
+
function wakeHistoryFor(root) {
|
|
285
|
+
return [...wakeHistory.get(root) ?? []];
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 执行一次唤起(时钟 tick 与面板「立即唤起」共用):
|
|
289
|
+
* - force=false(时钟):完整校验——enabled / 目标命中 / --auto 标志 / 窗口 / 间隔 / 偏见脚本
|
|
290
|
+
* - force=true(手动调试,用户在场):跳过窗口/间隔,仍校验 enabled / 目标命中 / --auto / 偏见脚本
|
|
291
|
+
* (v1.27.12 移除每日唤起预算——force 无预算可跳,时钟也不受限)
|
|
292
|
+
* 成功 → 注入前台会话(agent.steer,用户可见);返回结果供调用方(tick 打日志 / 面板显示 / 审计)。
|
|
293
|
+
*/
|
|
294
|
+
async function performAutopilotWake(ctx, root, settings, opts = {}) {
|
|
295
|
+
if (!settings?.enabled) return {
|
|
296
|
+
ok: false,
|
|
297
|
+
detail: "Autopilot Trajectory 未启用(enabled=false)"
|
|
298
|
+
};
|
|
299
|
+
const mdPath = resolveTargetMd(root, settings);
|
|
300
|
+
if (!mdPath) return {
|
|
301
|
+
ok: false,
|
|
302
|
+
detail: "目标会话未命中(session 未配置或 AGENT_SESSIONS 无匹配)"
|
|
303
|
+
};
|
|
304
|
+
if (!isAutopilotSession(mdPath)) return {
|
|
305
|
+
ok: false,
|
|
306
|
+
detail: "目标会话目录无 --auto 标志(AGENT_SESSIONS/<date>--<desc>--auto/)"
|
|
307
|
+
};
|
|
308
|
+
if (!opts.force) {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
if (!inAllowedWakeWindow(now, settings.avoidWakeHours)) return {
|
|
311
|
+
ok: false,
|
|
312
|
+
detail: "当前在北京高峰避开窗口内(不唤起)"
|
|
313
|
+
};
|
|
314
|
+
try {
|
|
315
|
+
const mtime = statSync(mdPath).mtimeMs;
|
|
316
|
+
const hours = Math.max(MIN_INTERVAL_HOURS, settings.intervalHours ?? 12);
|
|
317
|
+
if (now - mtime < hours * 36e5) return {
|
|
318
|
+
ok: false,
|
|
319
|
+
detail: `距上次轨迹活动不足 ${hours}h(等待中)`
|
|
320
|
+
};
|
|
321
|
+
} catch {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
detail: "SESSION.md 读取失败(statSync)"
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const agent = resolveTargetAgent(ctx, mdPath);
|
|
329
|
+
if (!agent) return {
|
|
330
|
+
ok: false,
|
|
331
|
+
detail: `目标会话 agent 不可得——${diagnoseTargetUnavailable(ctx, mdPath) ?? "目标会话未打开或命名未生效"}`
|
|
332
|
+
};
|
|
333
|
+
const provider = settings.biasProvider?.trim() || "autopilot-bias.ts";
|
|
334
|
+
const biasRes = await fetchBiasContent(root, provider);
|
|
335
|
+
if (biasRes.error) return {
|
|
336
|
+
ok: false,
|
|
337
|
+
detail: `偏见内容缺失:${biasRes.error}`
|
|
338
|
+
};
|
|
339
|
+
const motivation = readSelfGeneratedMotivation(mdPath);
|
|
340
|
+
const message = buildWakeMessage({
|
|
341
|
+
sessionName: basename(dirname(mdPath)),
|
|
342
|
+
mdPath,
|
|
343
|
+
intervalHours: Math.max(MIN_INTERVAL_HOURS, settings.intervalHours ?? 12),
|
|
344
|
+
topPrompt: settings.topPrompt?.trim() || null,
|
|
345
|
+
motivation,
|
|
346
|
+
biasContent: biasRes.text
|
|
347
|
+
});
|
|
348
|
+
agent.steer(createUserMessage({
|
|
349
|
+
content: [{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: message
|
|
352
|
+
}],
|
|
353
|
+
source: PLUGIN_SOURCE
|
|
354
|
+
}));
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
detail: `已唤起 ${basename(dirname(mdPath))}(偏见提供者 ${provider})`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* **周期自唤醒全局闸**(v1.34 更名收窄,原 `autopilotEnabled`):关 = 定时器不武装 + tick 不唤起。
|
|
362
|
+
*
|
|
363
|
+
* 语义边界(用户 2026-09-15 裁决 S-1):**只管周期自唤醒** —— 一次性唤醒(`container_trajectory wake-later`)
|
|
364
|
+
* 归 `wakeSchedulerEnabled`,两者**互不连带**(旧实现共用一闸是缺陷,见 wake-scheduler 注释)。
|
|
365
|
+
*
|
|
366
|
+
* 读取顺序(迁移期,不回写):`autopilotWakeEnabled`(新键)→ `autopilotEnabled`(旧键)→ false。
|
|
367
|
+
* 用 `??` 而非 `||` 的理由:**新键显式 false 必须能覆盖旧键 true**(否则"关不掉")。
|
|
368
|
+
* 抽成模块级函数是为了让 {@link autopilotClockState}(诊断)与 tick 读**同一判据**。
|
|
369
|
+
*
|
|
370
|
+
* v1.34.1(⑥ C6a):**导出**——CCC 工具面(`container_admin autopilot status`)与条件链
|
|
371
|
+
* ({@link autopilotRuntimeFacts})必须读**这一份**闸值。旧独立脚本读不到它 ⇒ 闸关着也印
|
|
372
|
+
* 「✅ 唤起条件全部满足」(8 项分歧之首,见 SESSION §12.13 与现状稿 §4.4)。
|
|
373
|
+
*/
|
|
374
|
+
function autopilotGloballyEnabled() {
|
|
375
|
+
try {
|
|
376
|
+
const s = readSimpleSettings();
|
|
377
|
+
return s.autopilotWakeEnabled ?? s.autopilotEnabled ?? false;
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 🔴 **本钟的串行域**(C6b 硬约束):`clock.chain` 是**本实例私有**的——
|
|
384
|
+
* 与唤醒调度器的串行链**零共享**。本钟每轮要跑 CCC 的偏见脚本(`fetchBiasContent`,
|
|
385
|
+
* 超时 60s),可能阻塞数十秒;共用链会把另一条钟一起卡住(这正是 P1 被否、取 P2 的理由,
|
|
386
|
+
* 见 SESSION §12.8②)。**这就是"两条时钟不共用排队执行链"的落点。**
|
|
387
|
+
*
|
|
388
|
+
* **为什么这里是模块级常量**(而不是 `registerAutopilot` 的局部量):本钟的进程态是
|
|
389
|
+
* **进程级可观测面**(`acc-diag` ①b / `containerClocks` / `autopilot-ops.runtimeBlock` 在读),
|
|
390
|
+
* 且 `autopilotClockState()` / `__resetAutopilotClockStateForTest()` 是**公开导出**、
|
|
391
|
+
* 可在未装配时被调用 ⇒ 实例必须**恒在**(旧实现的 `clockRuntime` 也正是模块级常量)。
|
|
392
|
+
* 装配只做 `clock.start()`。
|
|
393
|
+
*/
|
|
394
|
+
const clockOpts = {
|
|
395
|
+
label: "Autopilot Trajectory 时钟",
|
|
396
|
+
ctx: void 0,
|
|
397
|
+
events: ["session/created", "serenity/settings-changed"],
|
|
398
|
+
gate: autopilotGloballyEnabled,
|
|
399
|
+
gateOffReason: "周期自唤醒闸关闭(autopilotWakeEnabled=false)",
|
|
400
|
+
bodyCountsTick: true,
|
|
401
|
+
begin: () => runAutopilotBegin(clockOpts.ctx),
|
|
402
|
+
body: () => runAutopilotTick(clockOpts.ctx),
|
|
403
|
+
startLog: () => `[serenity-hooks] ✓ Autopilot Trajectory 定时器启动(${collectAutopilotCccs(clockOpts.ctx).length} 个 CCC 启用)`,
|
|
404
|
+
onReset: () => runningByRoot.clear()
|
|
405
|
+
};
|
|
406
|
+
const clock = createClock(clockOpts);
|
|
407
|
+
/**
|
|
408
|
+
* autopilot **进程态**快照(只读;`enabled` = 全局闸**当前**值)。
|
|
409
|
+
* ⚠️ 与 {@link wakeSchedulerState} 同规格,但**本钟无 `lastTickLog`**(见 {@link ClockOptions.logFrom})。
|
|
410
|
+
*/
|
|
411
|
+
function autopilotClockState() {
|
|
412
|
+
return clock.snapshot();
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* **重入守卫状态**(per-CCC:该 CCC 是否有一轮唤起正在进行)。
|
|
416
|
+
*
|
|
417
|
+
* 模块级而非常量闭包(⑥ C6a):条件链要如实回答"此刻是否已有唤起在跑"——旧独立脚本
|
|
418
|
+
* **完全没有这一判据**(8 项分歧之一)。若在 `registerAutopilot` 闭包里再造一份镜像 map,
|
|
419
|
+
* 就是"两份判断";故状态**提到模块级**,tick 与诊断读**同一张表**。
|
|
420
|
+
* 与既有 `wakeHistory` 同规格(它本就是模块级进程态)。
|
|
421
|
+
*/
|
|
422
|
+
const runningByRoot = /* @__PURE__ */ new Map();
|
|
423
|
+
/** 该 CCC 是否有唤起轮正在进行中(tick 的 per-CCC 防重入判据;诊断读同一份) */
|
|
424
|
+
function autopilotWakeInFlight(root) {
|
|
425
|
+
return runningByRoot.get(root) === true;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* 装配(index.ts apply 调用)。时钟唤起(v1.26.14 修复 + v1.27.4 多 CCC 独立):
|
|
429
|
+
*
|
|
430
|
+
* v1.26.14 根因:旧实现启动时一次性解析 root——web 进程启动时 live 会话往往为空 → 定时器不启动。
|
|
431
|
+
* 修复:① 每次 tick 动态解析 root+settings ② 监听 session/created 启动定时器 ③ 优先实验 CCC。
|
|
432
|
+
*
|
|
433
|
+
* **v1.27.4 多 CCC 独立(用户"4个CCC能各自有autotrajectory吗")**:
|
|
434
|
+
* - 单定时器保留(TICK_MS 5min),每次 tick **遍历所有 live+enabled CCC**(collectAutopilotCccs)
|
|
435
|
+
* 各自评估 shouldWake + 各自唤起——每 CCC 的 interval/session/bias/topPrompt/窗口独立
|
|
436
|
+
* - `running` 守卫 **per-CCC**(不同 CCC 唤起互不阻塞)
|
|
437
|
+
* - **全局串行化**(wakeChain:同 tick 多 CCC 到点 → 依次唤起,防模型并发挤兑)
|
|
438
|
+
*
|
|
439
|
+
* 零资源占用语义保留:无 enabled CCC → tick 内直接 return(定时器存在但每 5min 一次空检查,
|
|
440
|
+
* unref 不阻塞进程退出)。
|
|
441
|
+
*/
|
|
442
|
+
function registerAutopilot(ctx) {
|
|
443
|
+
clockOpts.ctx = ctx;
|
|
444
|
+
clock.start();
|
|
445
|
+
registerDisposer(ctx, "autopilot 时钟", () => clock.dispose());
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* tick 的**同步前置阶段**(`clock-runtime` 在 body 前同步调用)——判据**一行未改**:
|
|
449
|
+
* 枚举 live+enabled CCC;为空 ⇒ 记跳过原因且**不计 tick**;非空 ⇒ 清空跳过原因并记一次 tick。
|
|
450
|
+
*
|
|
451
|
+
* ⚠️ 为什么与 {@link runAutopilotTick} 分开(不是风格,是**观测面正确性**):
|
|
452
|
+
* 本钟的 `ticks` / `lastSkipReason` 在旧实现里是在 tick 的**同步段**定下来的——诊断
|
|
453
|
+
* (`autopilot-ops.runtimeBlock` / `autopilotClockState()`)在"启动即 tick"的那一瞬间就要读到
|
|
454
|
+
* 本拍的值。若把它们挪进 async body,那一瞬间会读到上一拍的旧值(假报告)。
|
|
455
|
+
* 两条回归钉守着这一点:『启动时无 live 会话 ⇒ ticks=0 + skipReason 留痕』
|
|
456
|
+
* 与『配置关闭 ⇒ ticks=0』(`tests/autopilot-trajectory.test.ts`)。
|
|
457
|
+
* @param ctx 插件上下文
|
|
458
|
+
*/
|
|
459
|
+
function runAutopilotBegin(ctx) {
|
|
460
|
+
if (collectAutopilotCccs(ctx).length === 0) {
|
|
461
|
+
clock.noteSkipReason("无 live+enabled CCC(等会话出现 / 配置生效)");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
clock.noteSkipReason(null);
|
|
465
|
+
clock.countTick();
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* 一次 autopilot tick 的**业务体**(引擎外壳由 `clock-runtime` 提供)。
|
|
469
|
+
*
|
|
470
|
+
* 判据**一行未改**,仍全部本函数自持:枚举 live+enabled CCC → 逐 CCC 防重入 +
|
|
471
|
+
* `shouldWake` 评估 → 命中者接到**本钟私有**串行链尾(`clock.enqueue`)。
|
|
472
|
+
* 同步的"记账/跳过留痕"在 {@link runAutopilotBegin}。
|
|
473
|
+
* @param ctx 插件上下文
|
|
474
|
+
*/
|
|
475
|
+
function runAutopilotTick(ctx) {
|
|
476
|
+
const roots = collectAutopilotCccs(ctx);
|
|
477
|
+
if (roots.length === 0) return;
|
|
478
|
+
for (const root of roots) {
|
|
479
|
+
if (runningByRoot.get(root)) continue;
|
|
480
|
+
const settings = readAutopilotSettings(root);
|
|
481
|
+
if (!settings?.enabled) continue;
|
|
482
|
+
const mdPath = resolveTargetMd(root, settings);
|
|
483
|
+
if (!shouldWake(settings, mdPath, Date.now(), false, wakeHistoryFor(root))) continue;
|
|
484
|
+
runningByRoot.set(root, true);
|
|
485
|
+
clock.enqueue(async () => {
|
|
486
|
+
try {
|
|
487
|
+
const res = await performAutopilotWake(ctx, root, settings, { force: false });
|
|
488
|
+
recordWake(root, {
|
|
489
|
+
time: Date.now(),
|
|
490
|
+
ok: res.ok,
|
|
491
|
+
detail: res.detail
|
|
492
|
+
});
|
|
493
|
+
if (res.ok) console.log(`[serenity-hooks] ✓ Autopilot Trajectory 唤起(${res.detail})`);
|
|
494
|
+
else console.warn(`[serenity-hooks] ✗ Autopilot Trajectory 唤起跳过:${res.detail}`);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
recordWake(root, {
|
|
497
|
+
time: Date.now(),
|
|
498
|
+
ok: false,
|
|
499
|
+
detail: `唤起异常: ${String(err?.message ?? err)}`
|
|
500
|
+
});
|
|
501
|
+
console.warn(`[serenity-hooks] ✗ Autopilot Trajectory 唤起失败: ${String(err?.message ?? err)}`);
|
|
502
|
+
} finally {
|
|
503
|
+
runningByRoot.set(root, false);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 目标 agent:从 SESSION.md 反向定位 dsh 会话(cwd 归属校验 + **bound 精确匹配优先**,
|
|
510
|
+
* 标题 F3 命名回退);不可得 → null。
|
|
511
|
+
*
|
|
512
|
+
* v1.29.2(R2,用户"autopilot 会话绑定应当更稳固——唤起的时候能唤起最新的、
|
|
513
|
+
* 绑定 autopilot SESSION 的会话"):v1.29.1 `serenity/bound` 事件是权威绑定
|
|
514
|
+
* (session use 激活时 append,dirName = 完整 AGENT_SESSIONS 目录名,编码无关 U4)。
|
|
515
|
+
* 旧实现只按标题猜(=== sid / startsWith(sid-))——若绑定该 SESSION 的 dsh 会话
|
|
516
|
+
* 标题不是 S###- 前缀(LLM 改过/重建后 rename 异常),绑定明明在却唤起失败。
|
|
517
|
+
* 加强:**bound.dirName 精确匹配优先**(权威证据),标题匹配降级为回退(存量
|
|
518
|
+
* 无 bound 的 live 会话兼容);多候选(罕见:同 SESSION 绑定多个 live 会话)取
|
|
519
|
+
* **绑定最新**(bound.at 最大——最近 use 过 = 最可能当前在用)。
|
|
520
|
+
*/
|
|
521
|
+
function resolveTargetAgent(ctx, mdPath) {
|
|
522
|
+
const dirName = basename(dirname(mdPath));
|
|
523
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
524
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
525
|
+
const targetRoot = cccRootForCwd(mdPath);
|
|
526
|
+
const sessions = hostSessions(ctx);
|
|
527
|
+
const agents = hostAgents(ctx);
|
|
528
|
+
const candidates = [];
|
|
529
|
+
try {
|
|
530
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
531
|
+
const sess = s;
|
|
532
|
+
const cwd = sess?.header?.cwd ?? "";
|
|
533
|
+
if (targetRoot) {
|
|
534
|
+
if (cccRootForCwd(cwd) !== targetRoot) continue;
|
|
535
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
536
|
+
const bound = readLastBound(sess);
|
|
537
|
+
const title = readSessionTitle(sess);
|
|
538
|
+
const boundMatched = bound !== null && bound.dirName === dirName;
|
|
539
|
+
const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
|
|
540
|
+
if (boundMatched) candidates.push({
|
|
541
|
+
session: sess,
|
|
542
|
+
boundAt: bound?.at ?? null,
|
|
543
|
+
titleMatched
|
|
544
|
+
});
|
|
545
|
+
else if (titleMatched) candidates.push({
|
|
546
|
+
session: sess,
|
|
547
|
+
boundAt: null,
|
|
548
|
+
titleMatched
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
} catch {}
|
|
552
|
+
if (candidates.length === 0) return null;
|
|
553
|
+
candidates.sort((a, b) => {
|
|
554
|
+
const aBound = a.boundAt !== null;
|
|
555
|
+
if (aBound !== (b.boundAt !== null)) return aBound ? -1 : 1;
|
|
556
|
+
return (b.boundAt ?? 0) - (a.boundAt ?? 0);
|
|
557
|
+
});
|
|
558
|
+
for (const c of candidates) {
|
|
559
|
+
const agent = agents?.get?.(c.session.id ?? "");
|
|
560
|
+
if (agent) return agent;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* 从 dsh 会话 log 读取标题(latest-wins `session/title` 事件)——
|
|
566
|
+
* **标题不在 sessions.list() 条目上**(wire/对象均无 title 字段;F3 命名经
|
|
567
|
+
* sessionTitle.rename 写进 session log),必须从 events 提取(rebuild 同款读取模式)。
|
|
568
|
+
*/
|
|
569
|
+
function readSessionTitle(session) {
|
|
570
|
+
try {
|
|
571
|
+
const events = sessionEvents(session);
|
|
572
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
573
|
+
const e = events[i];
|
|
574
|
+
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
575
|
+
}
|
|
576
|
+
} catch {}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* 诊断:目标会话 agent 为何不可得(供 performAutopilotWake 失败信息——
|
|
581
|
+
* 区分"无 live 会话" / "标题不匹配" / "bound 或标题命中但 agent 未加载")。
|
|
582
|
+
* v1.29.2(R2):bound 命中但 agent 未加载也归入「已匹配未加载」(原只查标题)。
|
|
583
|
+
* 返回诊断文本(无则 null)。
|
|
584
|
+
*/
|
|
585
|
+
function diagnoseTargetUnavailable(ctx, mdPath) {
|
|
586
|
+
const dirName = basename(dirname(mdPath));
|
|
587
|
+
const idMatch = dirName.match(/--S(\d{3,})--/);
|
|
588
|
+
const sid = idMatch ? `S${idMatch[1]}` : null;
|
|
589
|
+
const targetRoot = cccRootForCwd(mdPath);
|
|
590
|
+
const sameCccTitles = [];
|
|
591
|
+
const agentMissing = { matched: false };
|
|
592
|
+
try {
|
|
593
|
+
const sessions = hostSessions(ctx);
|
|
594
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
595
|
+
const sess = s;
|
|
596
|
+
const cwd = sess?.header?.cwd ?? "";
|
|
597
|
+
if (targetRoot) {
|
|
598
|
+
if (cccRootForCwd(cwd) !== targetRoot) continue;
|
|
599
|
+
} else if (cwd !== "" && !mdPath.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) continue;
|
|
600
|
+
const bound = readLastBound(sess);
|
|
601
|
+
const title = readSessionTitle(sess);
|
|
602
|
+
if (title) sameCccTitles.push(title);
|
|
603
|
+
const boundMatched = bound !== null && bound.dirName === dirName;
|
|
604
|
+
const titleMatched = sid !== null && title !== null && (title === sid || title.startsWith(`${sid}-`));
|
|
605
|
+
if (boundMatched || titleMatched) {
|
|
606
|
+
if (!hostAgents(ctx)?.get?.(sess.id ?? "")) agentMissing.matched = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} catch {}
|
|
610
|
+
if (agentMissing.matched) return `会话已绑定/匹配 ${sid ?? dirName} 但 agent 未加载(会话可能刚创建/正在恢复——稍后重试)`;
|
|
611
|
+
if (sameCccTitles.length > 0) return `目标 CCC 内 live 会话标题: [${sameCccTitles.join(", ")}]——均不匹配 ${sid ?? dirName}(目标会话未在 WebUI 打开,或绑定/命名未生效)`;
|
|
612
|
+
return `目标 CCC 内无 live 会话(先在 WebUI 打开 ${sid ?? dirName} 会话后重试)`;
|
|
613
|
+
}
|
|
614
|
+
/** Autopilot 绑定的回退 CCC 根:进程 cwd 上溯 .serenity 优先,回退任一 live 会话 root */
|
|
615
|
+
function resolveAutopilotRoot(ctx) {
|
|
616
|
+
const fromCwd = cccRootForCwd(process.cwd());
|
|
617
|
+
if (fromCwd) return fromCwd;
|
|
618
|
+
try {
|
|
619
|
+
const sessions = hostSessions(ctx);
|
|
620
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
621
|
+
const r = cccRootForCwd(s?.header?.cwd ?? "");
|
|
622
|
+
if (r) return r;
|
|
623
|
+
}
|
|
624
|
+
} catch {}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
function getAutopilotStatus(root) {
|
|
628
|
+
const cfg = readAutopilotSettings(root);
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
const base = {
|
|
631
|
+
configured: cfg !== null,
|
|
632
|
+
enabled: cfg?.enabled ?? false,
|
|
633
|
+
intervalHours: Math.max(MIN_INTERVAL_HOURS, cfg?.intervalHours ?? 12),
|
|
634
|
+
biasProvider: cfg?.biasProvider?.trim() || "autopilot-bias.ts",
|
|
635
|
+
topPrompt: cfg?.topPrompt?.trim() || null,
|
|
636
|
+
session: cfg?.session ?? null,
|
|
637
|
+
avoidWakeHours: {
|
|
638
|
+
start: cfg?.avoidWakeHours?.start ?? DEFAULT_AVOID_HOURS.start,
|
|
639
|
+
end: cfg?.avoidWakeHours?.end ?? DEFAULT_AVOID_HOURS.end
|
|
640
|
+
},
|
|
641
|
+
beijingHour: beijingHour(now),
|
|
642
|
+
windowAllowed: inAllowedWakeWindow(now, cfg?.avoidWakeHours),
|
|
643
|
+
recentWakes: wakeHistoryFor(root).slice(-10).reverse()
|
|
644
|
+
};
|
|
645
|
+
if (!cfg) return {
|
|
646
|
+
...base,
|
|
647
|
+
target: null
|
|
648
|
+
};
|
|
649
|
+
const mdPath = resolveTargetMd(root, cfg);
|
|
650
|
+
if (!mdPath) return {
|
|
651
|
+
...base,
|
|
652
|
+
target: null
|
|
653
|
+
};
|
|
654
|
+
let idleHours = 0;
|
|
655
|
+
try {
|
|
656
|
+
idleHours = (now - statSync(mdPath).mtimeMs) / 36e5;
|
|
657
|
+
} catch {}
|
|
658
|
+
return {
|
|
659
|
+
...base,
|
|
660
|
+
target: {
|
|
661
|
+
dirName: basename(dirname(mdPath)),
|
|
662
|
+
autoFlag: isAutopilotSession(mdPath),
|
|
663
|
+
idleHours: Math.max(0, idleHours),
|
|
664
|
+
wakeable: shouldWake(cfg, mdPath, now, false, wakeHistoryFor(root))
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* live 会话清单(诊断/面板解析用;标题从 events 读)——**只枚举 live 会话本身**。
|
|
670
|
+
*
|
|
671
|
+
* C2:CCC 枚举("本机有哪些 CCC")**不在这里**,归 `ccc-roots.listCccs`(并集)。
|
|
672
|
+
* 本函数保留的是它**独有**的那部分:逐条 live 会话 + cwd 归属 + 标题 + id
|
|
673
|
+
* (诊断页需要"哪个会话属于哪个 CCC",那是会话维度、不是 CCC 维度)。
|
|
674
|
+
* @param ctx 插件上下文
|
|
675
|
+
* @returns 逐条 live 会话(`cccRoot` = 该会话 cwd 的 CCC 归属,无 → null)
|
|
676
|
+
*/
|
|
677
|
+
function listLiveSessions(ctx) {
|
|
678
|
+
const out = [];
|
|
679
|
+
try {
|
|
680
|
+
const sessions = hostSessions(ctx);
|
|
681
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
682
|
+
const cwd = s?.header?.cwd ?? null;
|
|
683
|
+
out.push({
|
|
684
|
+
id: s?.id ?? "",
|
|
685
|
+
cwd,
|
|
686
|
+
cccRoot: cwd ? cccRootForCwd(cwd) : null,
|
|
687
|
+
title: readSessionTitle(s)
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
} catch {}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* 收集所有「配置了 Autopilot Trajectory 的 live CCC」——**v1.27.4 多 CCC 独立**:
|
|
695
|
+
* 返回 enabled 的 CCC 列表(时钟 tick 遍历目标);无 enabled → []。
|
|
696
|
+
* (原 resolveAutoTrajectoryCcc 单目标语义被 collect 取代——面板/诊断用 enableOnly=false 取全量)
|
|
697
|
+
*/
|
|
698
|
+
function collectAutopilotCccs(ctx, opts = {}) {
|
|
699
|
+
const enabled = [];
|
|
700
|
+
const configured = [];
|
|
701
|
+
for (const s of listLiveSessions(ctx)) {
|
|
702
|
+
if (!s.cccRoot) continue;
|
|
703
|
+
const cfg = readAutopilotSettings(s.cccRoot);
|
|
704
|
+
if (!cfg) continue;
|
|
705
|
+
if (cfg.enabled) {
|
|
706
|
+
if (!enabled.includes(s.cccRoot)) enabled.push(s.cccRoot);
|
|
707
|
+
} else if (!configured.includes(s.cccRoot)) configured.push(s.cccRoot);
|
|
708
|
+
}
|
|
709
|
+
return opts.includeDisabled ? [...enabled, ...configured] : enabled;
|
|
710
|
+
}
|
|
711
|
+
function diagLive(ctx) {
|
|
712
|
+
const liveSessions = listLiveSessions(ctx);
|
|
713
|
+
const processCwd = process.cwd();
|
|
714
|
+
const processCcc = cccRootForCwd(processCwd);
|
|
715
|
+
const panelResolved = collectAutopilotCccs(ctx)[0] ?? resolveAutopilotRoot(ctx);
|
|
716
|
+
const autopilotCccs = [];
|
|
717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
718
|
+
for (const s of liveSessions) {
|
|
719
|
+
if (!s.cccRoot || seen.has(s.cccRoot)) continue;
|
|
720
|
+
const cfg = readAutopilotSettings(s.cccRoot);
|
|
721
|
+
if (!cfg) continue;
|
|
722
|
+
seen.add(s.cccRoot);
|
|
723
|
+
const status = getAutopilotStatus(s.cccRoot);
|
|
724
|
+
const mdPath = resolveTargetMd(s.cccRoot, cfg);
|
|
725
|
+
const agentResolved = mdPath ? resolveTargetAgent(ctx, mdPath) !== null : false;
|
|
726
|
+
const agentDiagnosis = mdPath && !agentResolved ? diagnoseTargetUnavailable(ctx, mdPath) : null;
|
|
727
|
+
autopilotCccs.push({
|
|
728
|
+
root: s.cccRoot,
|
|
729
|
+
enabled: status.enabled,
|
|
730
|
+
session: status.session,
|
|
731
|
+
target: status.target,
|
|
732
|
+
agentResolved,
|
|
733
|
+
agentDiagnosis
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
processCwd,
|
|
738
|
+
processCcc,
|
|
739
|
+
liveSessions,
|
|
740
|
+
autopilotCccs,
|
|
741
|
+
panelResolved
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* **条件链的运行态事实**(⑥ C6a 的核心出口)——把只有插件进程能看到的判据交给
|
|
746
|
+
* 条件链({@link buildWakeChain} 的输入):全局闸 / 重入守卫 / live 运行态。
|
|
747
|
+
*
|
|
748
|
+
* 为什么必须有(这是 C6 的动机本身):旧独立脚本只能读文件 ⇒ 结构性地看不到这三样,
|
|
749
|
+
* 于是逐条件报告**必然**与实际唤起行为分歧(8 项,见 `acc-component-relations-current-state.md` §4.4)。
|
|
750
|
+
* 搬进进程内之后,`container_admin autopilot status` 与 `acc-diag` ④ 段读的就是**同一份事实**。
|
|
751
|
+
*
|
|
752
|
+
* `ctx` 可传 `null`(**不是**"三项全不可知"):全局闸(`readSimpleSettings`)与重入守卫
|
|
753
|
+
* (模块级 `runningByRoot`)**都不需要 ctx**,无 ctx 时也应读出真值——只有 live 运行态
|
|
754
|
+
* (live 会话 / agent 可解析)依赖 ctx,那才是真正的"不可知"。
|
|
755
|
+
* 这条区分由 `autopilot-ops.test.ts` 的全局闸可见性用例逼出来(首版把三者一起降级为不可知,
|
|
756
|
+
* 使"闸关"在无 ctx 路径下隐形——正是本轮要消灭的那类假报告)。
|
|
757
|
+
*
|
|
758
|
+
* @param ctx 插件上下文(读 live 会话 + agent);null = 无进程上下文(live 标不可知)
|
|
759
|
+
* @param root 目标 CCC 根
|
|
760
|
+
* @returns 条件链事实(`live` 为 null 表示"live 运行态不可知")
|
|
761
|
+
*/
|
|
762
|
+
function autopilotRuntimeFacts(ctx, root) {
|
|
763
|
+
const settings = readAutopilotSettings(root);
|
|
764
|
+
const mdPath = settings ? resolveTargetMd(root, settings) : null;
|
|
765
|
+
let live = null;
|
|
766
|
+
if (ctx !== null) {
|
|
767
|
+
const sessionCount = listLiveSessions(ctx).filter((s) => s.cccRoot === root).length;
|
|
768
|
+
const agentResolved = mdPath ? resolveTargetAgent(ctx, mdPath) !== null : null;
|
|
769
|
+
live = {
|
|
770
|
+
sessionCount,
|
|
771
|
+
agentResolved,
|
|
772
|
+
diagnosis: mdPath && agentResolved === false ? diagnoseTargetUnavailable(ctx, mdPath) : null
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
globalGate: autopilotGloballyEnabled(),
|
|
777
|
+
running: autopilotWakeInFlight(root),
|
|
778
|
+
live
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
//#endregion
|
|
782
|
+
export { autopilot_trajectory_exports as a, registerAutopilot as c, autopilotWakeInFlight as i, registerDisposer as l, autopilotGloballyEnabled as n, diagLive as o, autopilotRuntimeFacts as r, getAutopilotStatus as s, autopilotClockState as t, createClock as u };
|