@shgroup/dsh-serenity-hooks 1.43.0 → 1.45.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.
@@ -78,7 +78,7 @@ const simpleSettingsSchema = z.object({
78
78
  acpEnabled: z.boolean().default(false),
79
79
  acpHttpPort: z.number().min(1024).max(65535).default(ACP_HTTP_PORT),
80
80
  publicAskEnabled: z.boolean().default(false),
81
- wakeSchedulerEnabled: z.boolean().default(true)
81
+ croEnabled: z.boolean().default(true)
82
82
  });
83
83
  /** 从插件 Config 提取 entry 默认(settings base 层) */
84
84
  function entryDefaults(config) {
@@ -91,7 +91,7 @@ function entryDefaults(config) {
91
91
  acpEnabled: config.acp?.enabled ?? false,
92
92
  acpHttpPort: config.acp?.httpPort ?? ACP_HTTP_PORT,
93
93
  publicAskEnabled: config.publicAsk?.enabled ?? false,
94
- wakeSchedulerEnabled: true
94
+ croEnabled: true
95
95
  };
96
96
  }
97
97
  /** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
@@ -109,7 +109,7 @@ function defaultSimpleSettings() {
109
109
  acpEnabled: false,
110
110
  acpHttpPort: ACP_HTTP_PORT,
111
111
  publicAskEnabled: false,
112
- wakeSchedulerEnabled: true
112
+ croEnabled: true
113
113
  };
114
114
  }
115
115
  /**
@@ -63,12 +63,18 @@ interface SerenitySimpleSettings {
63
63
  acpHttpPort: number;
64
64
  /** F4d 建议问答页总开关(实验性;默认关——按认知容器暴露问答页,key 认证) */
65
65
  publicAskEnabled: boolean;
66
- /** **唤醒调度器全局闸** —— v1.34 更名(原 `trajectoryEnabled`):投递已登记的"未来时刻 + 一条 message"。
67
- * **缺省开**:它是 trajectory 的基础能力(D58),且**不得**被"关掉周期自唤醒"连带关掉
68
- * ——用户 2026-09-15 裁决:「auto-trajectory 的开关只关闭 auto-trajectory 唤醒」。
69
- * (原并列的周期自唤醒闸 `autopilotWakeEnabled` / 旧键 `autopilotEnabled` 已随 ACC 侧
70
- * autopilot 于 2026-09-15 整段退场删除——现只剩这一座闸。) */
71
- wakeSchedulerEnabled: boolean;
66
+ /** **CRO(轨迹自编程唤起)总开关** —— 2026-09-21 所有者令新增。
67
+ *
68
+ * CRO = 轨迹目录下放一个 `continuous-re-occurrence.ts`,由 ACC 5min tick spawn,
69
+ * 由**程序**决定"要不要唤、何时唤、唤起的提示词是什么"。
70
+ *
71
+ * **缺省开**:① 它已在生产上稳定运行(S151 / S185 各有程序在跑);② 它的**默认存在感为零**
72
+ * ——没有程序文件的轨迹本来就不参与;③ 缺省关会让"已上线的程序突然不跑"(等于静默回退)。
73
+ * **只关 CRO 阶段**:`send-later` / `send-now` / 唤醒表投递**照常**(见 `cro.ts` 头注的承诺)。
74
+ *
75
+ * ⚠️ 与它**同时废止**的键:`wakeSchedulerEnabled`(所有者 2026-09-21 令「send-later 的开关
76
+ * 不再重要了,砍掉」)——唤醒调度器**不再有任何闸**(恒开);旧配置里的该键**静默忽略**。 */
77
+ croEnabled: boolean;
72
78
  }
73
79
  /** 从插件 Config 提取 entry 默认(settings base 层) */
74
80
  export declare function entryDefaults(config: SimpleConfigFragment): SerenitySimpleSettings;
@@ -1,3 +1,21 @@
1
+ import { dirname, join } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/constants.ts
5
+ /** 常量(纯模块,零 DSH 依赖) */
6
+ /**
7
+ * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
8
+ * 发布时只需改 package.json 的 version。
9
+ */
10
+ const ACC_VERSION = (() => {
11
+ try {
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")).version ?? "0.0.0";
14
+ } catch {
15
+ return "0.0.0";
16
+ }
17
+ })();
18
+ //#endregion
1
19
  //#region src/skiff-registry.ts
2
20
  const skiffSessions = /* @__PURE__ */ new Map();
3
21
  /** 查 sessionId 的 Skiff 角色名(无 → null)——向后兼容(guards/seams 依赖) */
@@ -22,4 +40,4 @@ function skiffSessionSnapshot() {
22
40
  return new Map(skiffSessions);
23
41
  }
24
42
  //#endregion
25
- export { unregisterSkiffSession as a, skiffSessionSnapshot as i, skiffRoleFor as n, skiffSessionInfo as r, registerSkiffSession as t };
43
+ export { unregisterSkiffSession as a, skiffSessionSnapshot as i, skiffRoleFor as n, ACC_VERSION as o, skiffSessionInfo as r, registerSkiffSession as t };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { h as resolveInside, m as readUtf8, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-NlLr_sxy.js";
2
+ import { g as resolveInside, h as readUtf8, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-DG4Oc7I1.js";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  //#region src/skiff-role.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { r as cccRootForCwd } from "./ccc-roots-Bd_SjEs7.js";
1
+ import { r as cccRootForCwd } from "./ccc-roots-FxWXuZPw.js";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
4
4
  //#region src/time.ts
@@ -55,17 +55,34 @@ export declare function parseDeclaredSkills(sessionMd: string): string[];
55
55
  */
56
56
  export declare function hasTrajectorySkillsDeclaration(root: string, mdPath: string): boolean;
57
57
  /**
58
- * 装配注入正文(规格 §3):按声明顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
58
+ * 合并两条声明来源(v1.44.0):**CCC 级在前,轨迹级追加**,同名去重取首次位置。
59
+ *
60
+ * 两条来源(owner 令 2026-09-20):
61
+ * · **CCC 级** = `.opencode/serenity.json` 的 `trajectory.skills`(本 CCC 所有轨迹共有的底座);
62
+ * · **轨迹级** = 该轨迹 `SESSION.md` frontmatter 的 `skills:`(这条轨迹额外的)。
63
+ *
64
+ * 为什么是**并集**而不是二选一(R↓):C1 设计当初否决"纯 CCC 全局"的理由是"会**丢掉
65
+ * per-trajectory**";并集把两侧都保住——容器给底座,单条轨迹仍可加自己的额外项。
66
+ * 为什么 CCC 级**在前**:底座先立、增量随后,与"入口身份块 order=-50 → 本 section order=-45"
67
+ * 的既有先后观感一致。
68
+ */
69
+ export declare function mergeSkillNames(cccSkills: readonly string[], trajSkills: readonly string[]): string[];
70
+ /**
71
+ * 装配注入正文(规格 §3):按**合并后的**顺序拼接各 skill 的 `SKILL.md` 全文,每段带来源抬头
59
72
  * `=== skill: <name> ===`;拿不到全文的一律以 `[缺失]` 响亮提示占位(**不静默**)。
60
73
  *
61
- * 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter)⇒ **先过 `isSafeSkillName`**;
74
+ * 安全(规格 §3 🔒):名字来自 CCC 数据(frontmatter / CCC 配置)⇒ **先过 `isSafeSkillName`**;
62
75
  * 不安全 ⇒ 按缺失处理,且不调用 `findSkillMd` ⇒ **不发生任何以该名拼出的 fs 访问**。
63
76
  *
64
- * 长度守卫:复用已导出的 `truncateContent`,超限截断并在末尾写明 `truncated`。
77
+ * 长度守卫:复用已导出的 `truncateContent`,**在合并之后**统一截断(两条来源共享同一上限,
78
+ * 不是各给 32 KB——否则上限会随来源数翻倍)。
65
79
  *
66
80
  * @param root CCC 根
67
81
  * @param mdPath 轨迹的 SESSION.md 路径(**来自绑定的 `mdPath`**,不用 label 拼)
68
82
  * @param maxChars 总长上限(字符)
69
- * @returns 注入正文;无声明 `''`(空串 = 宿主不产出该 section 块);SESSION.md 缺失 → 响亮提示
83
+ * @param cccSkills CCC 级声明(`readTrajectorySkills`;缺省空 = 只有轨迹级,行为与 v1.43 一致)
84
+ * @returns 注入正文;两条来源皆无声明 → `''`(空串 = 宿主不产出该 section 块);
85
+ * SESSION.md 缺失/读失败 → 响亮提示(若同时有 CCC 级声明,则提示在前、正文随后——
86
+ * **提示不因"别处有内容"而被吞掉**)
70
87
  */
71
- export declare function buildTrajectorySkillsSection(root: string, mdPath: string, maxChars?: number): string;
88
+ export declare function buildTrajectorySkillsSection(root: string, mdPath: string, maxChars?: number, cccSkills?: readonly string[]): string;
@@ -1,8 +1,8 @@
1
1
  import { r as hostService, t as hostAgents } from "./access-fiehjxV6.js";
2
- import { h as resolveInside } from "./ccc-NlLr_sxy.js";
3
- import { o as listCccs } from "./ccc-roots-Bd_SjEs7.js";
4
- import { D as localHuman, O as localIdStamp, T as isoLocal, b as sessionsRoot, n as listBoundSessionIds, p as findSession } from "./trajectory-bound-kxvMuCui.js";
5
- import { t as readSimpleSettings } from "./settings-section-DMDUoQum.js";
2
+ import { g as resolveInside } from "./ccc-DG4Oc7I1.js";
3
+ import { o as listCccs } from "./ccc-roots-FxWXuZPw.js";
4
+ import { D as localHuman, O as localIdStamp, T as isoLocal, b as sessionsRoot, n as listBoundSessionIds, p as findSession } from "./trajectory-bound-BzdzsDPI.js";
5
+ import { t as readSimpleSettings } from "./settings-section-CIuyoSOX.js";
6
6
  import { basename, dirname, join } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { spawn } from "node:child_process";
@@ -44,7 +44,7 @@ function emptyRegistry() {
44
44
  };
45
45
  }
46
46
  /** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整表) */
47
- function asEntry(value) {
47
+ function asEntry$1(value) {
48
48
  const e = value;
49
49
  if (!e || typeof e.id !== "string" || typeof e.target !== "string") return null;
50
50
  if (typeof e.at !== "string" || typeof e.message !== "string") return null;
@@ -78,7 +78,7 @@ function loadWakeRegistry(root) {
78
78
  const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
79
79
  const entries = [];
80
80
  for (const item of raw) {
81
- const e = asEntry(item);
81
+ const e = asEntry$1(item);
82
82
  if (e) entries.push(e);
83
83
  }
84
84
  return {
@@ -959,6 +959,210 @@ function registerCroTurnTracking(ctx) {
959
959
  });
960
960
  }
961
961
  //#endregion
962
+ //#region src/cro-log.ts
963
+ /**
964
+ * cro-log.ts — CRO 唤起日志(**ACC 侧**,每轨迹一份固定名文件,2026-09-20 S142 §7.16)
965
+ *
966
+ * ## 所有者令(本模块的存在理由)
967
+ *
968
+ * 「S151 和 S185 的验证都说明,trajectory 倾向于自己做个结构化记录来记录自身 CRO 的执行情况,
969
+ * 这是个结构化流水账;**我们 ACC 有必要进行 CRO 唤醒的日志记录**,当 CRO 存在时,每次**成功唤醒**
970
+ * 写相关信息,但是保留数量要少,**只保留仅两日**的记录即可,**这个时间是程序化的**」
971
+ * +「**这个日志写结构化文件到 trajectory 目录,文件名固定即可**」
972
+ * +「**失败成功都记录**」。
973
+ *
974
+ * ## 它解决什么麻烦(白话)
975
+ *
976
+ * 在此之前「ACC 到底叫过谁、叫了几次、哪次**没叫成**」**ACC 自己一个字都不留** ——
977
+ * 要查只能去读**被叫的那个程序**写的记录;程序没写 / 写坏 / 被换掉 ⇒ **这段历史就是空白**。
978
+ * ⇒ 本模块让 ACC **自己那一侧**留一份只记两天的流水,使「唤起历史」**不再依赖被观测者是否老实**。
979
+ *
980
+ * ## 🔴 为什么**不是**第二真相源(本容器铁律,勿删此论证)
981
+ *
982
+ * 轨迹目录里因此**并排两个文件、主语各一**:
983
+ *
984
+ * | 文件 | 谁写 | 主语 | 回答 |
985
+ * |---|---|---|---|
986
+ * | `<轨迹目录>/cro-state.json`(CCC 侧程序自建,名字自定) | **CCC 的 CRO 程序** | 程序 | 「**我判了什么**」(含"没叫"的判定、心跳阈值、环境谓词) |
987
+ * | `<轨迹目录>/cro-wake-log.json`(**本模块**) | **ACC** | ACC | 「**我把什么送出去了、送成没有**」 |
988
+ *
989
+ * **内容不重叠**:本模块**不复制**判定细节(不存 `lastDecision` / 阈值 / 谓词);
990
+ * 而程序**不可能**知道投递结果(CRO 投递是 fire-and-forget)。
991
+ * ⇒ 两者**互补**:完整审计 = 两者合看(本模块 = 投递面|程序状态 = 判定面)。
992
+ *
993
+ * ## 🔴 轮转(所有者明示「程序化的」)
994
+ *
995
+ * **写入时按窗裁剪**:每次落盘只保留 `at >= now - CRO_LOG_RETENTION_MS` 的条目,
996
+ * 随后**原子写**(`tmp + rename`,与 `wake-registry.ts` 同款形态)。
997
+ * ⇒ 清理与写入**是同一笔操作** ⇒ **结构上不存在"忘了清"的可能**(比"另设一个每 tick 的清理步"更硬:
998
+ * 那多一个"可能忘了跑"的面)。窗宽是**常量**,**无配置键、无人工清理动作**。
999
+ *
1000
+ * ## 🔴 铁律(与 `cro.ts` 同族)
1001
+ *
1002
+ * **本模块的每个导出函数都不抛错**:写盘 / 裁剪 / 解析失败一律返回结构化失败,
1003
+ * 由调用方只记一行 tick 日志 ⇒ **CRO 流水的问题绝不影响投递与既有链路**(设计 §5)。
1004
+ *
1005
+ * ## 保留量级(实测,非估计)
1006
+ *
1007
+ * 实测最高 = S185 的 40min 心跳(≈36 条/天);两天窗 ⇒ **≤ ~80 条、< 20 KB**。
1008
+ * 最坏(某程序每 5min 都叫)⇒ 288 条/天 ⇒ 两天 **≤ ~600 条、< 100 KB** —— 轮转**自带上限**。
1009
+ */
1010
+ /** 固定文件名(所有者令:「文件名固定即可」)——与程序自建的 `cro-state.json` 并排、主语分明 */
1011
+ const CRO_LOG_FILENAME = "cro-wake-log.json";
1012
+ /** 保留窗 = 2 日(所有者令「只保留仅两日」);**常量** ⇒ 「这个时间是程序化的」 */
1013
+ const CRO_LOG_RETENTION_MS = 1728e5;
1014
+ const CRO_LOG_VERSION = 1;
1015
+ /** 日志绝对路径 */
1016
+ function croWakeLogPath(root, dirName) {
1017
+ return join(sessionsRoot(root), dirName, CRO_LOG_FILENAME);
1018
+ }
1019
+ function emptyLog() {
1020
+ return {
1021
+ version: CRO_LOG_VERSION,
1022
+ entries: []
1023
+ };
1024
+ }
1025
+ /** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整档) */
1026
+ function asEntry(value) {
1027
+ const e = value;
1028
+ if (!e || typeof e !== "object") return null;
1029
+ if (typeof e.at !== "string" || e.at === "") return null;
1030
+ if (typeof e.ok !== "boolean") return null;
1031
+ if (typeof e.detail !== "string") return null;
1032
+ const digest = e.promptDigest ?? {};
1033
+ return {
1034
+ at: e.at,
1035
+ ok: e.ok,
1036
+ detail: e.detail,
1037
+ tick: typeof e.tick === "number" ? e.tick : 0,
1038
+ reason: typeof e.reason === "string" ? e.reason : null,
1039
+ promptDigest: {
1040
+ length: typeof digest.length === "number" ? digest.length : 0,
1041
+ head: typeof digest.head === "string" ? digest.head : ""
1042
+ }
1043
+ };
1044
+ }
1045
+ /** 提示词摘要(长度 + 前 N 字;超长不截断标记——长度字段本身就是信号) */
1046
+ function summarizeCroPrompt(prompt) {
1047
+ const text = typeof prompt === "string" ? prompt : "";
1048
+ return {
1049
+ length: [...text].length,
1050
+ head: [...text].slice(0, 80).join("")
1051
+ };
1052
+ }
1053
+ /**
1054
+ * 读取日志(**永不抛**;读坏 ⇒ 当作空档 + 返回 error 文本)。
1055
+ * @param root CCC 根
1056
+ * @param dirName 轨迹目录名
1057
+ */
1058
+ function loadCroWakeLog(root, dirName) {
1059
+ const path = croWakeLogPath(root, dirName);
1060
+ if (!existsSync(path)) return {
1061
+ log: emptyLog(),
1062
+ error: null
1063
+ };
1064
+ try {
1065
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
1066
+ const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
1067
+ const entries = [];
1068
+ for (const item of raw) {
1069
+ const e = asEntry(item);
1070
+ if (e) entries.push(e);
1071
+ }
1072
+ return {
1073
+ log: {
1074
+ version: CRO_LOG_VERSION,
1075
+ entries
1076
+ },
1077
+ error: null
1078
+ };
1079
+ } catch (err) {
1080
+ return {
1081
+ log: emptyLog(),
1082
+ error: `CRO 流水解析失败(${String(err?.message ?? err)})`
1083
+ };
1084
+ }
1085
+ }
1086
+ /**
1087
+ * 按窗裁剪(**纯函数** ⇒ 可穷举测试)。
1088
+ *
1089
+ * 判据:`Date.parse(at) >= nowMs - windowMs` 保留;反之丢弃。
1090
+ * 🔴 **`at` 不可解析者一律丢弃**(本模块只写 `isoLocal()` 产物 ⇒ 不可解析 = 被手改或损坏),
1091
+ * **并在返回值里报数**(`dropped`)——**不静默吞掉**(本容器反复栽的"安静失败"纪律)。
1092
+ */
1093
+ function pruneCroLogEntries(entries, nowMs, windowMs = CRO_LOG_RETENTION_MS) {
1094
+ const floor = nowMs - windowMs;
1095
+ const kept = [];
1096
+ let pruned = 0;
1097
+ let dropped = 0;
1098
+ for (const e of entries) {
1099
+ const t = Date.parse(e.at);
1100
+ if (Number.isNaN(t)) {
1101
+ dropped += 1;
1102
+ continue;
1103
+ }
1104
+ if (t >= floor) kept.push(e);
1105
+ else pruned += 1;
1106
+ }
1107
+ return {
1108
+ kept,
1109
+ pruned,
1110
+ dropped
1111
+ };
1112
+ }
1113
+ /**
1114
+ * 追加一条唤起记录并按窗裁剪(**本模块主入口;永不抛**)。
1115
+ *
1116
+ * 顺序 = 读 → 追加 → 裁剪 → **原子写**。任一步失败 ⇒ `{ok:false, error}`,
1117
+ * **不抛**(调用方只记一行 tick 日志;**绝不影响投递**)。
1118
+ *
1119
+ * 无 CRO 的轨迹**不会**因本函数产生文件:只有真的发生过一次唤起尝试才会建/写该档
1120
+ * (所有者令「**当 CRO 存在时**」)。
1121
+ *
1122
+ * @param root CCC 根
1123
+ * @param dirName 轨迹目录名
1124
+ * @param input 本次唤起尝试(成败都记)
1125
+ * @param nowMs 当前毫秒(可注入 ⇒ 便于测裁剪边界)
1126
+ * @returns 写入结果与裁剪计数
1127
+ */
1128
+ function appendCroWakeLog(root, dirName, input, nowMs = Date.now()) {
1129
+ const path = croWakeLogPath(root, dirName);
1130
+ try {
1131
+ const loaded = loadCroWakeLog(root, dirName);
1132
+ const entry = {
1133
+ at: isoLocal(nowMs),
1134
+ ok: input.ok,
1135
+ detail: input.detail,
1136
+ tick: input.tick,
1137
+ reason: input.reason,
1138
+ promptDigest: summarizeCroPrompt(input.prompt)
1139
+ };
1140
+ const { kept, pruned, dropped } = pruneCroLogEntries([...loaded.log.entries, entry], nowMs);
1141
+ mkdirSync(dirname(path), { recursive: true });
1142
+ const tmp = `${path}.tmp`;
1143
+ writeFileSync(tmp, `${JSON.stringify({
1144
+ version: CRO_LOG_VERSION,
1145
+ entries: kept
1146
+ }, null, 2)}\n`, "utf-8");
1147
+ renameSync(tmp, path);
1148
+ return {
1149
+ ok: true,
1150
+ error: null,
1151
+ kept: kept.length,
1152
+ pruned,
1153
+ dropped
1154
+ };
1155
+ } catch (err) {
1156
+ return {
1157
+ ok: false,
1158
+ error: `CRO 流水写入失败(${String(err?.message ?? err)})`,
1159
+ kept: 0,
1160
+ pruned: 0,
1161
+ dropped: 0
1162
+ };
1163
+ }
1164
+ }
1165
+ //#endregion
962
1166
  //#region src/clock-runtime.ts
963
1167
  /**
964
1168
  * clock-runtime.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
@@ -1076,8 +1280,8 @@ function createClock(opts) {
1076
1280
  for (const line of lines) console.log(`${opts.logFrom.prefix}${line}`);
1077
1281
  };
1078
1282
  const tick = () => {
1079
- if (!opts.gate()) {
1080
- runtime.lastSkipReason = opts.gateOffReason;
1283
+ if (opts.gate !== void 0 && !opts.gate()) {
1284
+ runtime.lastSkipReason = opts.gateOffReason ?? null;
1081
1285
  return;
1082
1286
  }
1083
1287
  if (opts.begin) try {
@@ -1111,7 +1315,7 @@ function createClock(opts) {
1111
1315
  st.wired = true;
1112
1316
  attachClockEvents(opts.ctx, api, opts.events);
1113
1317
  }
1114
- if (!opts.gate()) return;
1318
+ if (opts.gate !== void 0 && !opts.gate()) return;
1115
1319
  st.timer = setInterval(tick, TICK_MS);
1116
1320
  st.timer.unref();
1117
1321
  runtime.armed = true;
@@ -1132,7 +1336,7 @@ function createClock(opts) {
1132
1336
  start,
1133
1337
  dispose,
1134
1338
  snapshot: () => ({
1135
- enabled: opts.gate(),
1339
+ enabled: opts.gate === void 0 ? true : opts.gate(),
1136
1340
  ...runtime,
1137
1341
  ...runtime.lastTickLog ? { lastTickLog: [...runtime.lastTickLog] } : {}
1138
1342
  }),
@@ -1185,24 +1389,20 @@ function resolveWakeTarget(root, target) {
1185
1389
  };
1186
1390
  }
1187
1391
  /**
1188
- * 全局闸:**只看 `wakeSchedulerEnabled`**(缺省**开**;显式 `false` 才关)。
1392
+ * 🔴 **本钟的全局闸已于 2026-09-21 砍掉**(所有者令逐字:「**send-later 的开关不再重要了,砍掉**」)。
1393
+ *
1394
+ * **为什么砍**(R↓):唤醒已从"可选实验能力"长成**全容器的续接绳**——S142/S151/S185 的链、
1395
+ * 跨轨迹投递、`send-now`/`send-later` 都走它。留一个"能被误关的开关"的**代价**是
1396
+ * "整条链静默停摆且无人告警"(本仓已实测过同族事故:闸关 6.6h 零 tick、断链 1 天 14 小时);
1397
+ * 而**收益**只剩"关掉一个 5min unref 空检查"。⇒ **收益 < 代价,故删闸**(不是改为缺省开——是**没有这个键了**)。
1189
1398
  *
1190
- * ⚠️ 2026-09-15 用户裁决(S-1 **解耦**):「auto-trajectory 的开关**只关闭 auto-trajectory 唤醒**」。
1191
- * 本函数**不得**再把 `autopilotEnabled`(周期自唤醒闸)作为回退键 —— 旧实现是
1192
- * `trajectoryEnabled === true || autopilotEnabled === true`,后果实测:所有者关掉 auto-trajectory 后,
1193
- * **一次性唤醒 `wake-later`(今 `send-later`)被连带关掉**,注册表条目静默滞留(当日 6.6h 零 tick)。
1194
- * 缺省由 false 改 true 的理由:唤醒注册表已是一等机制(D58),条目**全部由人类/agent 显式登记**
1195
- * 未来时刻(无环境自主性),不需要"默认关"的实验保护;代价 = 一个 unref 的 5min 空检查。
1399
+ * ⚠️ **兼容**:旧配置文件里残留的 `wakeSchedulerEnabled` **静默忽略**(无回退键、无告警噪声;
1400
+ * 与旧 `bootstrap` 段的处置先例一致)。**机制本身一行未动**(所有者:"我说的只是开关")。
1196
1401
  *
1197
- * 设置服务不可用 关(保守;异常由日志与 `dashboard health` 响亮暴露,不静默)。
1402
+ * 历史(勿误读):v1.34 S-1 曾把两闸解耦(周期自唤醒 `autopilotWakeEnabled` / 本钟 `wakeSchedulerEnabled`),
1403
+ * 理由是"关 auto-trajectory 不得连带关一次性唤醒";随 ACC 侧 autopilot 于 v1.35.0 整段退场,
1404
+ * 两闸只剩本钟一座 ⇒ 现已整座移除。详见 `docs/trajectory-scheduling.md` §S-1 与 v1.44 CHANGELOG。
1198
1405
  */
1199
- function wakeSchedulerEnabled() {
1200
- try {
1201
- return readSimpleSettings().wakeSchedulerEnabled !== false;
1202
- } catch {
1203
- return false;
1204
- }
1205
- }
1206
1406
  /**
1207
1407
  * 🔴 **本钟的串行域**(C6b 硬约束):`clock.chain` 是**本实例私有**的——
1208
1408
  * 与 autopilot 的串行链**零共享**。autopilot 每轮跑 CCC 偏见脚本可能阻塞数十秒,
@@ -1218,8 +1418,6 @@ const clockOpts = {
1218
1418
  label: "trajectory 唤醒调度器",
1219
1419
  ctx: void 0,
1220
1420
  events: ["session/created", "serenity/settings-changed"],
1221
- gate: wakeSchedulerEnabled,
1222
- gateOffReason: "唤醒调度器闸关闭(wakeSchedulerEnabled=false)",
1223
1421
  body: () => runWakeTick(clockOpts.ctx),
1224
1422
  logFrom: {
1225
1423
  prefix: "[serenity-hooks] trajectory 唤醒 ",
@@ -1228,7 +1426,9 @@ const clockOpts = {
1228
1426
  startLog: () => "[serenity-hooks] ✓ trajectory 唤醒调度器启动(5min tick)"
1229
1427
  };
1230
1428
  const clock = createClock(clockOpts);
1231
- /** 调度器进程态快照(只读;`enabled` = 全局闸**当前**值,便于区分"未武装"与"闸关") */
1429
+ /** 调度器进程态快照(只读)。
1430
+ * ⚠️ `enabled` 对**本钟已无意义**(2026-09-21 起无闸 ⇒ 恒 `true`);保留字段只为沿用工厂快照形状。
1431
+ * "这轮为什么没被唤起"现在看 `armed` / `lastSkipReason`(tick 内真实原因)+ **CRO 闸**(见 `croEnabled`)。 */
1232
1432
  function wakeSchedulerState() {
1233
1433
  return clock.snapshot();
1234
1434
  }
@@ -1239,6 +1439,17 @@ function wakeSchedulerState() {
1239
1439
  * @param dirName 目标 trajectory 目录名
1240
1440
  * @returns 命中返回 agent 与取得方式(how 进 lastResult,便于诊断);未命中返回原因
1241
1441
  * (原因带 `notReady` = **环境未就绪**而非投递失败,调用方**不得**据此记一次失败)
1442
+ *
1443
+ * 🔴 **前置条件:本轨迹名下必须有一条「未取代」的绑定记录**(2026-09-20 回源码答复 S151 实测):
1444
+ * 选人是**遍历 `listBoundSessionIds(root, dirName)` 给出的 id**(按绑定时间倒序),**live 只是这条
1445
+ * 记录的一个加分项**——「绑定里的那条恰好 live ⇒ 直接注入」,否则走冷载入。
1446
+ * ⇒ **「有 live 会话」≠「有绑定」**:会话活着但该轨迹**零条未取代绑定**时,本函数**直接返回
1447
+ * `无绑定会话记录…`**(`send-now` / `send-later` 到点投递同此判据)。
1448
+ * 实证(2026-09-20):S142 三条旧绑定于 **08:03** 被 `supersededAt` 取代(D69「同轨迹只留一条」),
1449
+ * 而当前载体的绑定**直到 17:05 那次 rebuild 才写入**(`queueRebuild` 内的 `appendBound(action:'rebuild')`)
1450
+ * ⇒ **10:42 的 `send-now` 落在这个窗口里** ⇒ 报"无绑定会话记录"(当时确有 live 会话)。
1451
+ * ⚠️ 已知**注释漂移**:本行上方"→ 标题回退"在函数体内**没有对应分支**(标题回退属 v1.29.2 R2,
1452
+ * 现行实现已只认绑定);**仅登记,未改**(改它属 ACC 代码改动,须具名令)。
1242
1453
  */
1243
1454
  async function acquireWakeAgent(ctx, root, dirName) {
1244
1455
  const agents = hostAgents(ctx);
@@ -1556,6 +1767,10 @@ async function deliverCroWake(ctx, root, dirName, prompt, reason) {
1556
1767
  * @param log 本 tick 的人读摘要(就地追加)
1557
1768
  */
1558
1769
  async function runCroPhase(ctx, root, registry, log) {
1770
+ if (readSimpleSettings().croEnabled === false) {
1771
+ log.push("CRO 阶段:已关闭(croEnabled=false)——既有唤醒投递不受影响");
1772
+ return;
1773
+ }
1559
1774
  let dirs;
1560
1775
  try {
1561
1776
  dirs = listCroTrajectories(root);
@@ -1594,6 +1809,16 @@ async function runCroPhase(ctx, root, registry, log) {
1594
1809
  wakes += 1;
1595
1810
  const res = await deliverCroWake(ctx, root, dirName, outcome.prompt, outcome.decision.reason);
1596
1811
  log.push(`· ${root}: ${renderCroOutcome(dirName, outcome)} → ${res.ok ? "已投递" : `投递未成功:${res.detail}`}`);
1812
+ if (!res.notReady) {
1813
+ const rec = appendCroWakeLog(root, dirName, {
1814
+ ok: res.ok,
1815
+ detail: res.detail,
1816
+ tick: s.ticks,
1817
+ reason: outcome.decision.reason ?? null,
1818
+ prompt: outcome.prompt
1819
+ }, nowMs);
1820
+ if (!rec.ok) log.push(`· ${root}: ${dirName}: CRO 流水写入失败(已忽略,不影响投递): ${rec.error}`);
1821
+ }
1597
1822
  } else if (outcome.status === "skipped") {
1598
1823
  skips += 1;
1599
1824
  log.push(`· ${root}: ${renderCroOutcome(dirName, outcome)}`);
@@ -68,7 +68,9 @@ interface WakeTarget {
68
68
  * @returns 命中返回目录信息,未命中 null
69
69
  */
70
70
  export declare function resolveWakeTarget(root: string, target: string): WakeTarget | null;
71
- /** 调度器进程态快照(只读;`enabled` = 全局闸**当前**值,便于区分"未武装"与"闸关") */
71
+ /** 调度器进程态快照(只读)。
72
+ * ⚠️ `enabled` 对**本钟已无意义**(2026-09-21 起无闸 ⇒ 恒 `true`);保留字段只为沿用工厂快照形状。
73
+ * "这轮为什么没被唤起"现在看 `armed` / `lastSkipReason`(tick 内真实原因)+ **CRO 闸**(见 `croEnabled`)。 */
72
74
  export declare function wakeSchedulerState(): WakeSchedulerRuntime & {
73
75
  enabled: boolean;
74
76
  };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-NlLr_sxy.js";
2
+ import { s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-DG4Oc7I1.js";
3
3
  import { join, resolve } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { createHash } from "node:crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.43.0",
3
+ "version": "1.45.0",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「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。",
5
5
  "license": "MIT",
6
6
  "repository": {