@shgroup/dsh-serenity-hooks 1.31.13 → 1.32.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/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as findSerenityRoot, c as matchBlacklist, d as readCccName$2, f as readHandymanConfig, i as findGitRoot, l as pathInside, m as resolveInside, n as SAFE_MODE_MARKER, o as isSafeModeOn, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DAsSHsub.js";
3
3
  import { a as readSkiffRoles, d as systemPromptSource, i as isSkiffSessionId, l as roleToolWhitelist, o as resolveRoleSystemPrompt } from "./skiff-role-BYvNnwv1.js";
4
- import { A as skiffRoleFor, C as listActiveHandymen, D as splitModel, E as requireWhitelistedModel, O as writeFailedStatus, S as handymanProgressPaths, T as readProgress, _ as unregisterSkiffSession, a as startSkiffDebugServer, b as HANDYMAN_GUIDE, c as askSkiff, d as ensureWorkspacePromptSection, f as getSkiffAgent, g as skiffTrajectoryEnabled, h as skiffSessionSnapshot, k as writeProgress, l as createSkiffAgent, m as skiffSessionInfo, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffMsmGate, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as workspaceTrajectoryLine, w as newStopToken, x as buildRoundPrompt, y as waitAgentIdle } from "./skiff-debug-w2c3UqQk.js";
4
+ import { A as skiffRoleFor, C as listActiveHandymen, D as splitModel, E as requireWhitelistedModel, O as writeFailedStatus, S as handymanProgressPaths, T as readProgress, _ as unregisterSkiffSession, a as startSkiffDebugServer, b as HANDYMAN_GUIDE, c as askSkiff, d as ensureWorkspacePromptSection, f as getSkiffAgent, g as skiffTrajectoryEnabled, h as skiffSessionSnapshot, k as writeProgress, l as createSkiffAgent, m as skiffSessionInfo, n as jscSafeJsonText, o as stopSkiffDebugServer, p as skiffMsmGate, r as renderSkiffMarkdown, s as stripThink, t as discoverCccs, u as ensureSkiffSession, v as workspaceTrajectoryLine, w as newStopToken, x as buildRoundPrompt, y as waitAgentIdle } from "./skiff-debug-K2k1JnGj.js";
5
5
  import { a as hostSettings, c as hostWebServer, i as hostSessions, n as hostInjected, o as hostSubagents, r as hostService, s as hostWeb, t as hostAgents } from "./access-fiehjxV6.js";
6
6
  import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-wWk4AIwV.js";
7
- import { C as useSession, S as summarize, _ as resolveSessionByTitle, a as archiveSessions, b as setActiveSessionInfo, c as createSession, d as getActiveSessionInfo, f as healthCheck, g as readActiveSessionMd, h as qaCheck, i as SESSION_ACTIONS, l as extractSessionMdPathFromText, m as parseSessionContextFromEvents, n as readLastBound, o as clearActiveSessionInfo, p as listSessions, r as DEFAULT_SESSION_SCOPE, s as closeSession, t as appendBound, u as findSession, v as sessionEvents, x as showSession, y as sessionsRoot } from "./session-bound-BpTayaIS.js";
8
- import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-01rTVq6v.js";
7
+ import { C as showSession, S as setActiveSessionInfo, T as useSession, _ as qaCheck, a as DEFAULT_SESSION_SCOPE, b as sessionEvents, c as clearActiveSessionInfo, d as extractSessionMdPathFromText, f as findSession, g as parseSessionContextFromEvents, h as listSessions, i as resolveSessionTrajectoryLabel, l as closeSession, m as healthCheck, n as listBoundSessionIds, o as SESSION_ACTIONS, p as getActiveSessionInfo, r as readLastBound, s as archiveSessions, t as appendBound, u as createSession, v as readActiveSessionMd, w as summarize, x as sessionsRoot, y as resolveSessionByTitle } from "./session-bound-NHozeLFc.js";
8
+ import { n as registerSettingsSection, t as readSimpleSettings } from "./settings-section-Bm-xG-NR.js";
9
9
  import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-PjEhmjlZ.js";
10
10
  import z from "@deepseek-ai/schemastery";
11
11
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -1286,6 +1286,17 @@ const HOST_SERVICES = [
1286
1286
  impact: "CCC 自动发现回落失效(诊断面受限)",
1287
1287
  required: false
1288
1288
  },
1289
+ {
1290
+ id: "sessionController",
1291
+ name: "sessionController",
1292
+ access: "lazy",
1293
+ members: [{
1294
+ name: "resolveAgent",
1295
+ kind: "function"
1296
+ }],
1297
+ impact: "trajectory 冷会话唤醒不可用(只能唤醒已加载的 live 会话)——唤醒注册表投递降级",
1298
+ required: false
1299
+ },
1289
1300
  {
1290
1301
  id: "connection",
1291
1302
  name: "connection",
@@ -1980,9 +1991,9 @@ Directory of ACC capabilities — each area lists where to go for details (guide
1980
1991
  handyman(杂工 agent 编排——guide 子命令:handyman guide)
1981
1992
  ④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
1982
1993
  ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
1983
- ⑤ 自主与接入 → autopilot-trajectory(Autopilot 一站式:all/init/random/diag/doc/check/status/guide)
1994
+ ⑤ 自主与接入 → trajectory(轨迹一站式:all/init/random/diag/doc/check/status/guide + 唤醒注册表 wake-add/wake-list/wake-rm
1984
1995
  weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
1985
- ⑥ CCC 配置总览 → container_admin msm ccc-config(8 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/autopilotTrajectory/weixin)
1996
+ ⑥ CCC 配置总览 → container_admin msm ccc-config(8 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin)
1986
1997
  ⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
1987
1998
  dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
1988
1999
 
@@ -2120,22 +2131,26 @@ validate 校验 / apply 生效 / list 查看:container_admin role <guide|valid
2120
2131
  "systemPromptFile": ".opencode/skiff/qa.md"
2121
2132
  } } } }
2122
2133
 
2123
- ── 7. autopilotTrajectory(自动巡航轨迹)──
2134
+ ── 7. trajectory(轨迹:autopilot 自主巡航 + 唤醒注册表,D58)──
2124
2135
  CCC 定义的一条自主 trajectory——时钟到点自动唤起(前台注入,用户可见可介入)。
2136
+ **autopilot 是 trajectory 的子集**:周期自唤醒的特例;trajectory 本身是一等概念(一个 CCC 内可多条并行)。
2125
2137
  未配置或 enabled=false → 完全不启动(零资源占用)。多 CCC 独立:每 CCC 自己的配置。
2126
2138
  唤起消息四段式:轨迹焦点 topPrompt(最先注入,稳定锚)→ 身份锚定 → 先验偏见
2127
2139
  (CCC 根脚本 biasProvider 输出)→ 任务。目标会话 session 必填(目录须带 --auto 后缀)。
2128
- 诊断/状态/立即唤起:autopilot-trajectory <all|check|status|diag>(msm("autopilot-trajectory", ["all"]))。
2140
+ 诊断/状态/立即唤起:trajectory <all|check|status|diag>(trajectory({action:"all"}))。
2141
+ 唤醒注册表(wake-add/wake-list/wake-rm):唤醒 = 未来时刻 + 一条 message,可唤醒自己或别的 trajectory;
2142
+ 落点 AGENT_SESSIONS/wake-registry.json,中心调度器到点投递(fire-and-forget)。
2129
2143
 
2130
2144
  Config:
2131
- { "autopilotTrajectory": {
2132
- "enabled": true,
2133
- "intervalHours": 2,
2134
- "session": "S151",
2135
- "biasProvider": "autopilot-bias.ts",
2136
- "topPrompt": "本轨迹核心目标/纪律/质量要求(CCC 自填,防漂移)",
2137
- "avoidWakeHours": { "start": 8, "end": 18 }
2138
- } }
2145
+ { "trajectory": {
2146
+ "autopilot": { // 原 autopilotTrajectory 段,字段不变(旧键仍回退读)
2147
+ "enabled": true,
2148
+ "intervalHours": 2,
2149
+ "session": "S151",
2150
+ "biasProvider": "autopilot-bias.ts",
2151
+ "topPrompt": "本轨迹核心目标/纪律/质量要求(CCC 自填,防漂移)",
2152
+ "avoidWakeHours": { "start": 8, "end": 18 }
2153
+ } } }
2139
2154
 
2140
2155
  ── 8. weixin(微信桥 F4c-3,含消息记录 hook)──
2141
2156
  CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC 独立对接微信桥。
@@ -4137,6 +4152,7 @@ var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
4137
4152
  beijingHour: () => beijingHour,
4138
4153
  buildWakeMessage: () => buildWakeMessage,
4139
4154
  collectAutopilotCccs: () => collectAutopilotCccs,
4155
+ collectLiveCccs: () => collectLiveCccs,
4140
4156
  diagLive: () => diagLive,
4141
4157
  fetchBiasContent: () => fetchBiasContent,
4142
4158
  getAutopilotStatus: () => getAutopilotStatus,
@@ -4152,7 +4168,7 @@ var autopilot_trajectory_exports = /* @__PURE__ */ __exportAll({
4152
4168
  shouldWake: () => shouldWake,
4153
4169
  wakeHistoryFor: () => wakeHistoryFor
4154
4170
  });
4155
- const PLUGIN_SOURCE$5 = {
4171
+ const PLUGIN_SOURCE$6 = {
4156
4172
  kind: "plugin",
4157
4173
  plugin: "dsh-serenity-hooks"
4158
4174
  };
@@ -4177,10 +4193,10 @@ const DEFAULT_AVOID_HOURS = {
4177
4193
  const BIAS_OUTPUT_MAX = 8192;
4178
4194
  /** 偏见脚本执行超时(正式版沙箱;原 600s 过长阻塞 tick) */
4179
4195
  const BIAS_RUN_TIMEOUT_MS = 6e4;
4180
- /** 读配置:新键 autopilotTrajectory 优先,旧键 autotrajectory 回退(正式化兼容) */
4196
+ /** 读配置:`trajectory.autopilot`(D58 新键)优先 → 旧键 autopilotTrajectory 旧键 autotrajectory */
4181
4197
  function readAutopilotSettings(root) {
4182
4198
  const cfg = loadSerenityConfig(root);
4183
- return cfg.autopilotTrajectory ?? cfg.autotrajectory ?? null;
4199
+ return cfg.trajectory?.autopilot ?? cfg.autopilotTrajectory ?? cfg.autotrajectory ?? null;
4184
4200
  }
4185
4201
  /**
4186
4202
  * 北京时间(UTC+8)当前小时——不依赖服务器时区(服务器可能 UTC/本地任意)。
@@ -4266,7 +4282,7 @@ async function fetchBiasContent(root, providerRel) {
4266
4282
  }
4267
4283
  if (!existsSync(scriptAbs)) return {
4268
4284
  text: null,
4269
- error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(或旧默认 ${LEGACY_BIAS_PROVIDER};stdout 输出偏见内容一行;msm autopilot-trajectory init 可生成模板)`
4285
+ error: `请在 CCC 根目录实现偏见内容提供者脚本: ${providerRel}(或旧默认 ${LEGACY_BIAS_PROVIDER};stdout 输出偏见内容一行;trajectory init 可生成模板)`
4270
4286
  };
4271
4287
  const runs = [["bun", [scriptAbs]], [process.execPath, [scriptAbs]]];
4272
4288
  for (const [cmd, args] of runs) try {
@@ -4390,7 +4406,7 @@ async function performAutopilotWake(ctx, root, settings, opts = {}) {
4390
4406
  type: "text",
4391
4407
  text: message
4392
4408
  }],
4393
- source: PLUGIN_SOURCE$5
4409
+ source: PLUGIN_SOURCE$6
4394
4410
  }));
4395
4411
  return {
4396
4412
  ok: true,
@@ -4674,6 +4690,20 @@ function collectAutopilotCccs(ctx, opts = {}) {
4674
4690
  }
4675
4691
  return opts.includeDisabled ? [...enabled, ...configured] : enabled;
4676
4692
  }
4693
+ /**
4694
+ * 所有 live 会话所属的 CCC 根(去重,保持出现顺序)。
4695
+ *
4696
+ * 与 {@link collectAutopilotCccs} 的区别:**不要求该 CCC 配置 trajectory.autopilot**——
4697
+ * 唤醒注册表(wake-scheduler)的条目可以指向任何 CCC 的任何 trajectory。
4698
+ *
4699
+ * @param ctx 插件上下文
4700
+ * @returns CCC 根路径列表(无 live 会话 → `[]`)
4701
+ */
4702
+ function collectLiveCccs(ctx) {
4703
+ const roots = [];
4704
+ for (const s of listLiveSessions(ctx)) if (s.cccRoot && !roots.includes(s.cccRoot)) roots.push(s.cccRoot);
4705
+ return roots;
4706
+ }
4677
4707
  function diagLive(ctx) {
4678
4708
  const liveSessions = listLiveSessions(ctx);
4679
4709
  const processCwd = process.cwd();
@@ -4708,6 +4738,305 @@ function diagLive(ctx) {
4708
4738
  };
4709
4739
  }
4710
4740
  //#endregion
4741
+ //#region src/wake-registry.ts
4742
+ /**
4743
+ * wake-registry.ts — trajectory 唤醒注册表(D58,S142 2026-09-13)
4744
+ *
4745
+ * 需求(用户原话):一个 CCC 内允许多个 trajectory 任意形态并行;某个 trajectory 可以
4746
+ * **预定未来唤醒自己或别人**,本质上是一次「未来时刻 + 一条 message」的投递;autopilot
4747
+ * 是「周期自唤醒」的特例(独立循环,D59)。协作边界:互相可见、**无阻塞无等待**。
4748
+ *
4749
+ * 本模块只负责**注册表本身**(纯数据层,无宿主依赖,可单测):
4750
+ * - 落点:CCC 内 `AGENT_SESSIONS/wake-registry.json`(随 CCC git,人可读可审计)
4751
+ * - 状态机:`pending` →(投递成功)`delivered`|(超补跑窗口)`missed`|(`wake rm` 物理移除)
4752
+ * - 投递即终结:一次性,不重投(无回执语义——发起方不 await,见设计文档 §2)
4753
+ *
4754
+ * 调度与投递在 `wake-scheduler.ts`(需要 ctx),本模块保持纯净。
4755
+ */
4756
+ var wake_registry_exports = /* @__PURE__ */ __exportAll({
4757
+ WAKE_CATCH_UP_MS: () => WAKE_CATCH_UP_MS,
4758
+ WAKE_REGISTRY_REL_PATH: () => WAKE_REGISTRY_REL_PATH,
4759
+ WAKE_REGISTRY_VERSION: () => 1,
4760
+ addWake: () => addWake,
4761
+ listWakes: () => listWakes,
4762
+ loadWakeRegistry: () => loadWakeRegistry,
4763
+ newWakeId: () => newWakeId,
4764
+ parseWakeAt: () => parseWakeAt,
4765
+ removeWake: () => removeWake,
4766
+ saveWakeRegistry: () => saveWakeRegistry,
4767
+ splitDueWakes: () => splitDueWakes,
4768
+ updateWake: () => updateWake,
4769
+ wakeRegistryPath: () => wakeRegistryPath
4770
+ });
4771
+ /** 注册表相对 CCC 根的路径(与 `.bindings.json` 同居 AGENT_SESSIONS/) */
4772
+ const WAKE_REGISTRY_REL_PATH = "AGENT_SESSIONS/wake-registry.json";
4773
+ /**
4774
+ * 补跑窗口(缺省 2h):dsh 停机期间到期的条目,若迟到不超过本窗口则补投,
4775
+ * 超过则置 `missed` 留痕(不补投——避免"半夜该做的事早上突然连环跑")。
4776
+ */
4777
+ const WAKE_CATCH_UP_MS = 72e5;
4778
+ /** 注册表绝对路径 */
4779
+ function wakeRegistryPath(root) {
4780
+ return join(root, WAKE_REGISTRY_REL_PATH);
4781
+ }
4782
+ /** 空注册表 */
4783
+ function emptyRegistry() {
4784
+ return {
4785
+ version: 1,
4786
+ entries: []
4787
+ };
4788
+ }
4789
+ /** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整表) */
4790
+ function asEntry(value) {
4791
+ const e = value;
4792
+ if (!e || typeof e.id !== "string" || typeof e.target !== "string") return null;
4793
+ if (typeof e.at !== "string" || typeof e.message !== "string") return null;
4794
+ const state = e.state === "delivered" || e.state === "missed" || e.state === "cancelled" ? e.state : "pending";
4795
+ return {
4796
+ id: e.id,
4797
+ target: e.target,
4798
+ at: e.at,
4799
+ message: e.message,
4800
+ state,
4801
+ createdBy: typeof e.createdBy === "string" ? e.createdBy : "",
4802
+ createdAt: typeof e.createdAt === "string" ? e.createdAt : "",
4803
+ attempts: typeof e.attempts === "number" && Number.isFinite(e.attempts) ? e.attempts : 0,
4804
+ lastResult: typeof e.lastResult === "string" ? e.lastResult : null,
4805
+ deliveredAt: typeof e.deliveredAt === "string" ? e.deliveredAt : null
4806
+ };
4807
+ }
4808
+ /**
4809
+ * 读注册表。文件缺失 → 空表;损坏 → 空表 + error(不抛错,调用方决定是否告警)。
4810
+ * @param root CCC 根
4811
+ * @returns 注册表与读取错误(无错为 null)
4812
+ */
4813
+ function loadWakeRegistry(root) {
4814
+ const path = wakeRegistryPath(root);
4815
+ if (!existsSync(path)) return {
4816
+ registry: emptyRegistry(),
4817
+ error: null
4818
+ };
4819
+ try {
4820
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
4821
+ const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
4822
+ const entries = [];
4823
+ for (const item of raw) {
4824
+ const e = asEntry(item);
4825
+ if (e) entries.push(e);
4826
+ }
4827
+ return {
4828
+ registry: {
4829
+ version: 1,
4830
+ entries
4831
+ },
4832
+ error: null
4833
+ };
4834
+ } catch (err) {
4835
+ return {
4836
+ registry: emptyRegistry(),
4837
+ error: `唤醒注册表解析失败(${String(err?.message ?? err)})`
4838
+ };
4839
+ }
4840
+ }
4841
+ /** 原子写(tmp + rename;与 .bindings.json 同款形态) */
4842
+ function saveWakeRegistry(root, registry) {
4843
+ const path = wakeRegistryPath(root);
4844
+ try {
4845
+ mkdirSync(dirname(path), { recursive: true });
4846
+ const tmp = `${path}.tmp`;
4847
+ writeFileSync(tmp, `${JSON.stringify({
4848
+ version: 1,
4849
+ entries: registry.entries
4850
+ }, null, 2)}\n`, "utf-8");
4851
+ renameSync(tmp, path);
4852
+ return {
4853
+ ok: true,
4854
+ error: null
4855
+ };
4856
+ } catch (err) {
4857
+ return {
4858
+ ok: false,
4859
+ error: `唤醒注册表写入失败(${String(err?.message ?? err)})`
4860
+ };
4861
+ }
4862
+ }
4863
+ /** 条目 id:`w-<UTC 到分钟>-<4 hex>`(可读 + 低碰撞;不复用由"物理移除"保证) */
4864
+ function newWakeId(nowMs, rand = Math.random) {
4865
+ const iso = new Date(nowMs).toISOString();
4866
+ return `w-${`${iso.slice(0, 10).replace(/-/g, "")}-${iso.slice(11, 16).replace(":", "")}`}-${Math.floor(rand() * 65536).toString(16).padStart(4, "0")}`;
4867
+ }
4868
+ /**
4869
+ * 解析唤醒时刻。接受两种写法(设计文档 §7):
4870
+ * - 绝对:RFC3339(含时区偏移,如 `2026-09-14T09:00:00+08:00`)
4871
+ * - 相对:`+30m` / `+2h`(便于 agent 直接登记"半小时后")
4872
+ * @param input 用户/agent 传入的时刻串
4873
+ * @param nowMs 当前时刻(相对写法基准)
4874
+ * @returns `{ ok, ms }` 或 `{ ok: false, error }`
4875
+ */
4876
+ function parseWakeAt(input, nowMs) {
4877
+ const raw = input.trim();
4878
+ if (raw === "") return {
4879
+ ok: false,
4880
+ error: "时刻为空"
4881
+ };
4882
+ const rel = /^\+(\d+(?:\.\d+)?)([mhd])$/.exec(raw);
4883
+ if (rel) {
4884
+ const n = Number(rel[1]);
4885
+ const unit = rel[2];
4886
+ return {
4887
+ ok: true,
4888
+ ms: nowMs + n * (unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5)
4889
+ };
4890
+ }
4891
+ const ms = Date.parse(raw);
4892
+ if (!Number.isFinite(ms)) return {
4893
+ ok: false,
4894
+ error: `时刻无法解析(须 RFC3339 或 +30m/+2h): ${raw}`
4895
+ };
4896
+ return {
4897
+ ok: true,
4898
+ ms
4899
+ };
4900
+ }
4901
+ /**
4902
+ * 登记一条唤醒。
4903
+ * @param root CCC 根
4904
+ * @param input 目标 trajectory / 时刻串 / message / 发起者 / 当前时刻
4905
+ * @returns 成功返回条目,失败返回稳定错误文案(不抛错)
4906
+ */
4907
+ function addWake(root, input) {
4908
+ const target = input.target.trim();
4909
+ if (target === "") return {
4910
+ ok: false,
4911
+ error: "target 为空(须 S### 或 AGENT_SESSIONS 目录名)"
4912
+ };
4913
+ const message = input.message.trim();
4914
+ if (message === "") return {
4915
+ ok: false,
4916
+ error: "message 为空(唤醒必须携带一条信息)"
4917
+ };
4918
+ const parsed = parseWakeAt(input.at, input.nowMs);
4919
+ if (!parsed.ok) return {
4920
+ ok: false,
4921
+ error: parsed.error
4922
+ };
4923
+ if (parsed.ms <= input.nowMs) return {
4924
+ ok: false,
4925
+ error: "时刻必须在未来(唤醒只允许可预期的未来)"
4926
+ };
4927
+ const { registry, error } = loadWakeRegistry(root);
4928
+ if (error) return {
4929
+ ok: false,
4930
+ error
4931
+ };
4932
+ const entry = {
4933
+ id: newWakeId(input.nowMs),
4934
+ target,
4935
+ at: new Date(parsed.ms).toISOString(),
4936
+ message,
4937
+ state: "pending",
4938
+ createdBy: input.createdBy,
4939
+ createdAt: new Date(input.nowMs).toISOString(),
4940
+ attempts: 0,
4941
+ lastResult: null,
4942
+ deliveredAt: null
4943
+ };
4944
+ registry.entries.push(entry);
4945
+ const saved = saveWakeRegistry(root, registry);
4946
+ if (!saved.ok) return {
4947
+ ok: false,
4948
+ error: saved.error ?? "写入失败"
4949
+ };
4950
+ return {
4951
+ ok: true,
4952
+ entry
4953
+ };
4954
+ }
4955
+ /**
4956
+ * 物理移除一条(`wake rm`)。审计由 SESSION.md 与工具输出承担——注册表只保留在办项。
4957
+ * @param root CCC 根
4958
+ * @param id 条目 id
4959
+ * @returns 移除结果(不存在 → 稳定错误码文案)
4960
+ */
4961
+ function removeWake(root, id) {
4962
+ const { registry, error } = loadWakeRegistry(root);
4963
+ if (error) return {
4964
+ ok: false,
4965
+ error,
4966
+ removed: null
4967
+ };
4968
+ const idx = registry.entries.findIndex((e) => e.id === id);
4969
+ if (idx < 0) return {
4970
+ ok: false,
4971
+ error: `wake_not_found: ${id}`,
4972
+ removed: null
4973
+ };
4974
+ const [removed] = registry.entries.splice(idx, 1);
4975
+ const saved = saveWakeRegistry(root, registry);
4976
+ if (!saved.ok) return {
4977
+ ok: false,
4978
+ error: saved.error,
4979
+ removed: null
4980
+ };
4981
+ return {
4982
+ ok: true,
4983
+ error: null,
4984
+ removed: removed ?? null
4985
+ };
4986
+ }
4987
+ /**
4988
+ * 到期切分(纯函数,可单测):
4989
+ * - `due`:`state === 'pending'` 且 `now >= at`(在补跑窗口内)→ 应投递
4990
+ * - `expired`:`state === 'pending'` 且 `now - at > catchUpMs` → 应置 missed(不投递)
4991
+ * @param registry 注册表
4992
+ * @param nowMs 当前时刻
4993
+ * @param catchUpMs 补跑窗口
4994
+ */
4995
+ function splitDueWakes(registry, nowMs, catchUpMs = WAKE_CATCH_UP_MS) {
4996
+ const due = [];
4997
+ const expired = [];
4998
+ for (const e of registry.entries) {
4999
+ if (e.state !== "pending") continue;
5000
+ const at = Date.parse(e.at);
5001
+ if (!Number.isFinite(at) || nowMs < at) continue;
5002
+ if (nowMs - at > catchUpMs) expired.push(e);
5003
+ else due.push(e);
5004
+ }
5005
+ return {
5006
+ due,
5007
+ expired
5008
+ };
5009
+ }
5010
+ /**
5011
+ * 更新一条(状态推进;调用方给最终状态与结果文案)。
5012
+ * @param root CCC 根
5013
+ * @param id 条目 id
5014
+ * @param patch 要写入的字段
5015
+ * @returns 写入结果
5016
+ */
5017
+ function updateWake(root, id, patch) {
5018
+ const { registry, error } = loadWakeRegistry(root);
5019
+ if (error) return {
5020
+ ok: false,
5021
+ error
5022
+ };
5023
+ const entry = registry.entries.find((e) => e.id === id);
5024
+ if (!entry) return {
5025
+ ok: false,
5026
+ error: `wake_not_found: ${id}`
5027
+ };
5028
+ Object.assign(entry, patch);
5029
+ return saveWakeRegistry(root, registry);
5030
+ }
5031
+ /** 列条目(默认全量;`pendingOnly` 只列在办项——面板/工具用) */
5032
+ function listWakes(root, opts = {}) {
5033
+ const { registry, error } = loadWakeRegistry(root);
5034
+ return {
5035
+ entries: [...opts.pendingOnly ? registry.entries.filter((e) => e.state === "pending") : registry.entries].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)),
5036
+ error
5037
+ };
5038
+ }
5039
+ //#endregion
4711
5040
  //#region src/tools/autopilot-trajectory.ts
4712
5041
  /**
4713
5042
  * autopilot-trajectory.ts — Autopilot Trajectory 一站式管理工具(v1.26.12 实验
@@ -4749,8 +5078,31 @@ const AUTOPILOT_ACTIONS = [
4749
5078
  "check",
4750
5079
  "status",
4751
5080
  "guide",
4752
- "diag-live"
5081
+ "diag-live",
5082
+ "wake-add",
5083
+ "wake-list",
5084
+ "wake-rm"
4753
5085
  ];
5086
+ /** 进程内处理的动作(不 exec 包内脚本):diag-live 诊断 + 唤醒注册表三动作 */
5087
+ const NATIVE_ACTIONS = /* @__PURE__ */ new Set([
5088
+ "diag-live",
5089
+ "wake-add",
5090
+ "wake-list",
5091
+ "wake-rm"
5092
+ ]);
5093
+ /** 渲染唤醒条目(工具输出/面板共用形态;按 at 升序) */
5094
+ function renderWakeList(entries, error) {
5095
+ const lines = [`═══ trajectory 唤醒注册表(${entries.length} 条)═══`];
5096
+ if (error) lines.push(`⚠ ${error}`);
5097
+ if (entries.length === 0) lines.push(" (无条目——用 wake-add 登记:唤醒 = 未来时刻 + 一条 message)");
5098
+ for (const e of entries) {
5099
+ lines.push(` · ${e.id} [${e.state}] at=${e.at} target=${e.target} by=${e.createdBy || "?"}`);
5100
+ lines.push(` message: ${e.message.length > 120 ? `${e.message.slice(0, 120)}…` : e.message}`);
5101
+ if (e.lastResult) lines.push(` last: ${e.lastResult}(attempts=${e.attempts})`);
5102
+ }
5103
+ lines.push("", `补跑窗口 ${WAKE_CATCH_UP_MS / 36e5}h:停机期间到期且迟到未超窗 → 补投;超窗 → missed 留痕。`);
5104
+ return lines.join("\n");
5105
+ }
4754
5106
  function agentCwd$2(exec) {
4755
5107
  return exec.agent?.session?.header?.cwd ?? process.cwd();
4756
5108
  }
@@ -4780,32 +5132,71 @@ function renderDiagLive(r) {
4780
5132
  }
4781
5133
  return lines.join("\n");
4782
5134
  }
4783
- /** 创建 autopilot-trajectory 工具(闭包捕获 ctx → diag-live 进程内诊断;v1.26.14 + v1.27.4 改名) */
5135
+ /** 创建 trajectory 工具(原 autopilot-trajectory;D58 更名 + 唤醒注册表三动作) */
4784
5136
  function createAutopilotTool(ctx) {
4785
5137
  return defineTool({
4786
- name: "autopilot-trajectory",
4787
- description: "Autopilot Trajectory(自动巡航轨迹,正式版 v1.27.4;前身 autotrajectory 实验)一站式管理。无参/action=all:全报告(背景摘要 + 就绪检查 + 状态 + 下一步)——CCC agent 看一次即完整理解并知道怎么开始;init:初始化辅助(写配置 + 生成偏见提供者脚本模板);random:运行偏见提供者脚本输出当前偏见内容;diag:唤起条件链诊断(--ccc <path> 指定 CCC,无参递归扫描 /home/yh 两层;逐条件输出 + 阻断点 + 修复建议);diag-live:**进程内诊断**(live 会话清单/标题/agent 定位/面板解析目标;排查\"面板检测不到实验 CCC\");doc:定义全文;check/status/guide:单项。**topPrompt(轨迹焦点)**:CCC 定义时自己填写本轨迹核心焦点(顶层提示词),每次唤起最先注入——稳定焦点锚定防漂移,与偏见内容(每轮随机探索)互补。多 CCC 独立(v1.27.4):每个 live+enabled CCC 各自时钟唤起。机制是 CCC 的自选动作——dsp 只提供工具与知识,不自动安装任何东西。",
4788
- parameters: { action: {
4789
- type: "string",
4790
- enum: [...AUTOPILOT_ACTIONS],
4791
- required: true,
4792
- description: "Subcommand"
4793
- } },
5138
+ name: "trajectory",
5139
+ description: "trajectory 一站式管理(D58:trajectory 是一等概念,**autopilot 是其子集**——周期自唤醒的特例)。无参/action=all:全报告(背景摘要 + 就绪检查 + 状态 + 下一步)——CCC agent 看一次即完整理解并知道怎么开始;init:初始化辅助(写配置 + 生成偏见提供者脚本模板);random:运行偏见提供者脚本输出当前偏见内容;diag:唤起条件链诊断(--ccc <path> 指定 CCC,无参递归扫描 /home/yh 两层;逐条件输出 + 阻断点 + 修复建议);diag-live:**进程内诊断**(live 会话清单/标题/agent 定位/面板解析目标);doc:定义全文;check/status/guide:单项。**唤醒注册表(wake-add/wake-list/wake-rm)**:唤醒 = **未来时刻 + 一条 message**——可对**自己**预定未来唤醒,也可唤醒**别人**(任一 trajectory);落点 CCC 内 `AGENT_SESSIONS/wake-registry.json`;到点由中心调度器投递(冷会话自动载入),**fire-and-forget:无回执、无阻塞、不等待**。wake-add 参数:target(S### 或目录名)/ at(RFC3339 或 `+30m`/`+2h`)/ message;wake-rm 参数:id。**topPrompt(轨迹焦点)**:CCC 定义时自己填写本轨迹核心焦点(顶层提示词),每次唤起最先注入——稳定焦点锚定防漂移。多 CCC 独立:每个 live CCC 各自评估。机制是 CCC 的自选动作——dsp 只提供工具与知识,不自动安装任何东西。",
5140
+ parameters: {
5141
+ action: {
5142
+ type: "string",
5143
+ enum: [...AUTOPILOT_ACTIONS],
5144
+ required: true,
5145
+ description: "Subcommand"
5146
+ },
5147
+ target: {
5148
+ type: "string",
5149
+ description: "wake-add:目标 trajectory(S### 或 AGENT_SESSIONS 目录名)"
5150
+ },
5151
+ at: {
5152
+ type: "string",
5153
+ description: "wake-add:未来时刻(RFC3339 含时区,或 +30m / +2h 相对写法)"
5154
+ },
5155
+ message: {
5156
+ type: "string",
5157
+ description: "wake-add:唤醒 message(投递给目标 trajectory 的正文)"
5158
+ },
5159
+ id: {
5160
+ type: "string",
5161
+ description: "wake-rm:条目 id(wake-list 可见)"
5162
+ }
5163
+ },
4794
5164
  output: {
4795
5165
  schema: { type: "json" },
4796
5166
  render: (_args, value) => renderText$1(value)
4797
5167
  },
4798
5168
  async execute(args, exec) {
4799
- if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
4800
5169
  const root = findSerenityRoot(agentCwd$2(exec));
5170
+ if (args.action === "diag-live") return { output: renderDiagLive(diagLive(ctx)) };
5171
+ if (NATIVE_ACTIONS.has(args.action ?? "")) {
5172
+ if (!root) return { output: "⚠ 未定位到 CCC 根(.serenity)——唤醒注册表按 CCC 存放,请在 CCC 内调用" };
5173
+ if (args.action === "wake-list") {
5174
+ const { entries, error } = listWakes(root);
5175
+ return { output: renderWakeList(entries, error) };
5176
+ }
5177
+ if (args.action === "wake-add") {
5178
+ const scope = (exec.agent?.session)?.header?.id ?? "";
5179
+ const res = addWake(root, {
5180
+ target: args.target ?? "",
5181
+ at: args.at ?? "",
5182
+ message: args.message ?? "",
5183
+ createdBy: resolveSessionTrajectoryLabel(exec.agent?.session, scope),
5184
+ nowMs: Date.now()
5185
+ });
5186
+ if (!res.ok) return { output: `✗ 登记失败:${res.error}` };
5187
+ return { output: `✓ 已登记唤醒\n${renderWakeList([res.entry], null)}` };
5188
+ }
5189
+ const removed = removeWake(root, args.id ?? "");
5190
+ return { output: removed.ok ? `✓ 已移除唤醒条目 ${args.id}` : `✗ 移除失败:${removed.error}` };
5191
+ }
4801
5192
  const result = {};
4802
5193
  if (!EXP_SCRIPT) {
4803
- result.error = "autopilot-trajectory 脚本未随安装分发(npm 包缺 experiments/autopilot-trajectory/)——请检查包完整性";
5194
+ result.error = "trajectory 脚本未随安装分发(npm 包缺 experiments/autopilot-trajectory/)——请检查包完整性";
4804
5195
  return result;
4805
5196
  }
4806
5197
  const script = EXP_SCRIPT;
4807
5198
  if (!existsSync(script)) {
4808
- result.error = `autopilot-trajectory 脚本缺失(${script})——包未随安装分发,请检查 npm 包完整性`;
5199
+ result.error = `trajectory 脚本缺失(${script})——包未随安装分发,请检查 npm 包完整性`;
4809
5200
  return result;
4810
5201
  }
4811
5202
  const env = { ...process.env };
@@ -4825,7 +5216,7 @@ function createAutopilotTool(ctx) {
4825
5216
  return result;
4826
5217
  }
4827
5218
  if (r.error?.code === "ENOENT") {
4828
- result.error = "autopilot-trajectory 需要 bun 运行时(bun not found in PATH)";
5219
+ result.error = "trajectory 需要 bun 运行时(bun not found in PATH)";
4829
5220
  return result;
4830
5221
  }
4831
5222
  result.error = r.stderr?.trim() || r.stdout?.trim() || `exit ${r.status ?? "?"}`;
@@ -5378,7 +5769,7 @@ function logbookCompactionReminderText(input) {
5378
5769
  if (escalated) return `${eventToken("compaction")} ${facts} — you have been reminded repeatedly and have NOT compacted the logbook. This is now mandatory: STOP the current task step, load eap (praxis eap), rewrite SESSION.md, then resume. ${principles} This reminder persists until the file is under the limit.`;
5379
5770
  return `${eventToken("compaction")} ${facts}. Pause the current work and compact the trajectory logbook before continuing — the logbook is the persistent body of this trajectory, and its size is operational entropy (H_op) the container pays on every read. ${principles} After rewriting, resume the paused work; this reminder stops once the file is under the limit.`;
5380
5771
  }
5381
- const PLUGIN_SOURCE$4 = {
5772
+ const PLUGIN_SOURCE$5 = {
5382
5773
  kind: "plugin",
5383
5774
  plugin: "dsh-serenity-hooks"
5384
5775
  };
@@ -5465,7 +5856,7 @@ function registerKeeper(ctx, opts = {}) {
5465
5856
  if (blocks.length === 0) return downstream;
5466
5857
  const reminderAll = createUserMessage({
5467
5858
  content: blocks,
5468
- source: PLUGIN_SOURCE$4
5859
+ source: PLUGIN_SOURCE$5
5469
5860
  });
5470
5861
  if (downstream.kind === "block") return {
5471
5862
  kind: "block",
@@ -6023,7 +6414,7 @@ function toolsBlock() {
6023
6414
  " handyman — delegate work to a worker agent on a CCC-whitelisted model; mode=\"foreground\" (default) = one serial child returns its final text; mode=\"background\" = loop-validated worker (completion code + round cap + restart + progress file), jobs=[] orchestrates parallel work",
6024
6415
  " localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
6025
6416
  " container_admin — container administration (the maintenance bay): role (Skiff roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config",
6026
- " autopilot-trajectory Autopilot Trajectory one-stop management (all/init/random/diag/doc/check/status/guide)",
6417
+ " trajectory trajectory one-stop management (all/init/random/diag/doc/check/status/guide/diag-live) + wake registry (wake-add/wake-list/wake-rm: wake = a future instant + one message, delivered to any trajectory; fire-and-forget)",
6027
6418
  "",
6028
6419
  "MSM call (registered CCC MSMs, deterministic Mech & Semi-Mech):",
6029
6420
  " Execute: msm(\"<name>\", [\"<arg1>\", \"<arg2>\"]) — run a registered MSM directly (partial name returns matching candidates)",
@@ -6513,7 +6904,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
6513
6904
  }
6514
6905
  return lines.join("\n");
6515
6906
  }
6516
- const PLUGIN_SOURCE$3 = {
6907
+ const PLUGIN_SOURCE$4 = {
6517
6908
  kind: "plugin",
6518
6909
  plugin: "dsh-serenity-hooks"
6519
6910
  };
@@ -6530,7 +6921,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
6530
6921
  }];
6531
6922
  return createUserMessage({
6532
6923
  content,
6533
- source: PLUGIN_SOURCE$3
6924
+ source: PLUGIN_SOURCE$4
6534
6925
  });
6535
6926
  }
6536
6927
  /** 每 agent 是否已注入过(进程内存态) */
@@ -6705,7 +7096,7 @@ const FILE_UPLOAD_PATH = "/serenity/file-upload";
6705
7096
  const CONFIG_PATH = "/serenity/config";
6706
7097
  const CCCS_PATH = "/serenity/cccs";
6707
7098
  const PUBLIC_ASK_PATH = "/serenity/public-ask";
6708
- const AUTOPILOT_PATH = "/serenity/autopilot-trajectory";
7099
+ const TRAJECTORY_PATH = "/serenity/trajectory";
6709
7100
  const WEIXIN_PATH = "/serenity/weixin";
6710
7101
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
6711
7102
  const IMAGE_UPLOAD_DIR = "_tmp/images_from_user";
@@ -7021,7 +7412,7 @@ function registerStatusApi(ctx, opts = {}) {
7021
7412
  workspace: url.searchParams.get("workspace") ?? void 0
7022
7413
  });
7023
7414
  const root = findSerenityRoot(workspace) ?? "";
7024
- const { discoverCccs } = await import("./skiff-debug-w2c3UqQk.js").then((n) => n.i);
7415
+ const { discoverCccs } = await import("./skiff-debug-K2k1JnGj.js").then((n) => n.i);
7025
7416
  sendJson$2(res, 200, { cccs: await discoverCccs(ctx, root) });
7026
7417
  } catch (err) {
7027
7418
  sendJson$2(res, 400, { error: err.message ?? String(err) });
@@ -7051,7 +7442,7 @@ function registerStatusApi(ctx, opts = {}) {
7051
7442
  return;
7052
7443
  }
7053
7444
  const settings = readAdvancedSettings();
7054
- const { readSimpleSettings } = await import("./settings-section-01rTVq6v.js").then((n) => n.r);
7445
+ const { readSimpleSettings } = await import("./settings-section-Bm-xG-NR.js").then((n) => n.r);
7055
7446
  const simple = readSimpleSettings();
7056
7447
  const allowed = settings.publicAsk.allowed;
7057
7448
  const port = simple.acpHttpPort ?? 3100;
@@ -7075,7 +7466,7 @@ function registerStatusApi(ctx, opts = {}) {
7075
7466
  });
7076
7467
  ctx.webServer.register({
7077
7468
  kind: "exact",
7078
- path: AUTOPILOT_PATH,
7469
+ path: TRAJECTORY_PATH,
7079
7470
  handler: async (req, res) => {
7080
7471
  try {
7081
7472
  if (req.method === "POST") {
@@ -7125,7 +7516,11 @@ function registerStatusApi(ctx, opts = {}) {
7125
7516
  sendJson$2(res, 200, { status: null });
7126
7517
  return;
7127
7518
  }
7128
- sendJson$2(res, 200, { status: getAutopilotStatus(effectiveRoot) });
7519
+ const { listWakes } = await Promise.resolve().then(() => wake_registry_exports);
7520
+ sendJson$2(res, 200, {
7521
+ status: getAutopilotStatus(effectiveRoot),
7522
+ wakes: listWakes(effectiveRoot).entries
7523
+ });
7129
7524
  } catch (err) {
7130
7525
  sendJson$2(res, 400, { error: err.message ?? String(err) });
7131
7526
  }
@@ -8427,7 +8822,7 @@ var rebuild_exports = /* @__PURE__ */ __exportAll({
8427
8822
  sanitizeFocusLine: () => sanitizeFocusLine,
8428
8823
  stripAckSuffix: () => stripAckSuffix
8429
8824
  });
8430
- const PLUGIN_SOURCE$2 = {
8825
+ const PLUGIN_SOURCE$3 = {
8431
8826
  kind: "plugin",
8432
8827
  plugin: "dsh-serenity-hooks"
8433
8828
  };
@@ -8746,7 +9141,7 @@ function registerRebuildTurnHook(ctx) {
8746
9141
  type: "text",
8747
9142
  text: `${eventToken("rebuild")} The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint.`
8748
9143
  }],
8749
- source: PLUGIN_SOURCE$2
9144
+ source: PLUGIN_SOURCE$3
8750
9145
  }));
8751
9146
  writeRebuildDiag(resolveSerenityRootFor(agent), {
8752
9147
  sessionId: id,
@@ -8920,7 +9315,7 @@ function buildRebuke(hits) {
8920
9315
  const rebukeStates = /* @__PURE__ */ new Map();
8921
9316
  //#endregion
8922
9317
  //#region src/output-guard-seam.ts
8923
- const PLUGIN_SOURCE$1 = {
9318
+ const PLUGIN_SOURCE$2 = {
8924
9319
  kind: "plugin",
8925
9320
  plugin: "dsh-serenity-hooks"
8926
9321
  };
@@ -8981,7 +9376,7 @@ function registerOutputGuardHook(ctx) {
8981
9376
  type: "text",
8982
9377
  text: buildRebuke(hits)
8983
9378
  }],
8984
- source: PLUGIN_SOURCE$1
9379
+ source: PLUGIN_SOURCE$2
8985
9380
  }));
8986
9381
  } catch (error) {
8987
9382
  console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
@@ -9747,6 +10142,196 @@ qInput.focus()
9747
10142
  </html>`;
9748
10143
  }
9749
10144
  //#endregion
10145
+ //#region src/wake-scheduler.ts
10146
+ const PLUGIN_SOURCE$1 = {
10147
+ kind: "plugin",
10148
+ plugin: "dsh-serenity-hooks"
10149
+ };
10150
+ /**
10151
+ * 解析条目的 target(`S###` 或完整目录名)→ 目录名 + SESSION.md 路径。
10152
+ * @param root CCC 根
10153
+ * @param target 条目里的目标标识
10154
+ * @returns 命中返回目录信息,未命中 null
10155
+ */
10156
+ function resolveWakeTarget(root, target) {
10157
+ const found = findSession(sessionsRoot(root), target);
10158
+ if (!found) return null;
10159
+ const mdPath = join(found.path, "SESSION.md");
10160
+ if (!existsSync(mdPath)) return null;
10161
+ return {
10162
+ dirName: basename(found.path),
10163
+ mdPath
10164
+ };
10165
+ }
10166
+ /** 全局闸:`trajectoryEnabled` 优先,旧键 `autopilotEnabled` 回退(设置服务不可用 → 关) */
10167
+ function wakeSchedulerEnabled() {
10168
+ try {
10169
+ const s = readSimpleSettings();
10170
+ return s.trajectoryEnabled === true || s.autopilotEnabled === true;
10171
+ } catch {
10172
+ return false;
10173
+ }
10174
+ }
10175
+ /**
10176
+ * 取目标 agent:live 优先 → 冷会话走 sessionController.resolveAgent → 标题回退。
10177
+ * @param ctx 插件上下文
10178
+ * @param root CCC 根
10179
+ * @param dirName 目标 trajectory 目录名
10180
+ * @returns 命中返回 agent 与取得方式(how 进 lastResult,便于诊断);未命中返回原因
10181
+ */
10182
+ async function acquireWakeAgent(ctx, root, dirName) {
10183
+ const agents = hostAgents(ctx);
10184
+ const ids = listBoundSessionIds(root, dirName);
10185
+ for (const id of ids) {
10186
+ const live = agents?.get?.(id);
10187
+ if (live) return {
10188
+ agent: live,
10189
+ how: `live(bound ${id})`
10190
+ };
10191
+ }
10192
+ if (ids.length > 0) {
10193
+ const sc = hostService(ctx, "sessionController");
10194
+ const resolved = sc?.resolveAgent;
10195
+ if (typeof resolved !== "function") return { error: `目标会话未加载且 sessionController 不可用(headless profile?)——绑定 id: ${ids[0]}` };
10196
+ const id = ids[0];
10197
+ try {
10198
+ const out = await resolved.call(sc, id);
10199
+ if (out?.agent) return {
10200
+ agent: out.agent,
10201
+ how: `cold-resume(${id})`
10202
+ };
10203
+ return { error: `sessionController.resolveAgent 未返回 agent(${id}): ${String(out?.error ?? "未知原因")}` };
10204
+ } catch (err) {
10205
+ return { error: `冷会话载入失败(${id}): ${String(err?.message ?? err)}` };
10206
+ }
10207
+ }
10208
+ return { error: `无绑定会话记录(AGENT_SESSIONS/.bindings.json 中无 ${dirName})——目标轨迹需先被 logbook use 激活过` };
10209
+ }
10210
+ /** 唤醒消息(唤起即投递正文:身份锚定 + 唤醒信息 + 任务) */
10211
+ function buildWakeText(entry, target) {
10212
+ return [
10213
+ `[trajectory 唤醒] 到点(登记于 ${entry.createdAt},发起者 ${entry.createdBy || "未知"},id ${entry.id})。`,
10214
+ "",
10215
+ `身份锚定:继续 ${target.dirName} 的 trajectory(SESSION.md: ${target.mdPath})。`,
10216
+ "",
10217
+ "唤醒信息:",
10218
+ entry.message,
10219
+ "",
10220
+ "任务:按上述唤醒信息继续本轨迹的工作;完成后把进展写入 SESSION.md。"
10221
+ ].join("\n");
10222
+ }
10223
+ /**
10224
+ * 投递一条唤醒(fire-and-forget:`followup` 入队即返回,不等模型结果)。
10225
+ * @param ctx 插件上下文
10226
+ * @param root CCC 根
10227
+ * @param entry 唤醒条目
10228
+ * @returns 投递结果(写回 lastResult)
10229
+ */
10230
+ async function deliverWake(ctx, root, entry) {
10231
+ const target = resolveWakeTarget(root, entry.target);
10232
+ if (!target) return {
10233
+ ok: false,
10234
+ detail: `目标 trajectory 未命中(${entry.target})`
10235
+ };
10236
+ const acquired = await acquireWakeAgent(ctx, root, target.dirName);
10237
+ if ("error" in acquired) return {
10238
+ ok: false,
10239
+ detail: acquired.error
10240
+ };
10241
+ try {
10242
+ acquired.agent.followup(createUserMessage({
10243
+ content: [{
10244
+ type: "text",
10245
+ text: buildWakeText(entry, target)
10246
+ }],
10247
+ source: PLUGIN_SOURCE$1
10248
+ }));
10249
+ } catch (err) {
10250
+ return {
10251
+ ok: false,
10252
+ detail: `投递失败(${acquired.how}): ${String(err?.message ?? err)}`
10253
+ };
10254
+ }
10255
+ return {
10256
+ ok: true,
10257
+ detail: `已投递 ${target.dirName}(${acquired.how})`
10258
+ };
10259
+ }
10260
+ /** 一次调度 tick:遍历 live CCC → 到期项串行投递 / 超窗项置 missed;返回人读摘要 */
10261
+ async function runWakeTick(ctx) {
10262
+ const log = [];
10263
+ for (const root of collectLiveCccs(ctx)) {
10264
+ const { registry, error } = loadWakeRegistry(root);
10265
+ if (error) {
10266
+ log.push(`✗ ${root}: ${error}`);
10267
+ continue;
10268
+ }
10269
+ const { due, expired } = splitDueWakes(registry, Date.now(), WAKE_CATCH_UP_MS);
10270
+ for (const e of expired) {
10271
+ updateWake(root, e.id, {
10272
+ state: "missed",
10273
+ lastResult: `超过补跑窗口(${WAKE_CATCH_UP_MS / 36e5}h)未投递`
10274
+ });
10275
+ log.push(`· ${root}: ${e.id} → missed(超补跑窗口)`);
10276
+ }
10277
+ for (const e of due) {
10278
+ const res = await deliverWake(ctx, root, e);
10279
+ updateWake(root, e.id, {
10280
+ state: res.ok ? "delivered" : "pending",
10281
+ attempts: e.attempts + 1,
10282
+ lastResult: res.detail,
10283
+ deliveredAt: res.ok ? (/* @__PURE__ */ new Date()).toISOString() : null
10284
+ });
10285
+ log.push(`· ${root}: ${e.id} → ${res.ok ? "delivered" : `重试(${res.detail})`}`);
10286
+ }
10287
+ }
10288
+ return log;
10289
+ }
10290
+ /**
10291
+ * 装配调度器(index.ts apply 调用):5min tick + 生命周期 disposer +
10292
+ * 会话出现/settings 变化时热启动(与 autopilot 同款触发面,互不干扰)。
10293
+ * @param ctx 插件上下文
10294
+ */
10295
+ function registerWakeScheduler(ctx) {
10296
+ let timer = null;
10297
+ /** 全局串行链(同一时刻只跑一次 tick;投递本身按条目串行) */
10298
+ let chain = Promise.resolve();
10299
+ const tick = () => {
10300
+ if (!wakeSchedulerEnabled()) return;
10301
+ if (collectLiveCccs(ctx).length === 0) return;
10302
+ chain = chain.then(async () => {
10303
+ try {
10304
+ const log = await runWakeTick(ctx);
10305
+ for (const line of log) console.log(`[serenity-hooks] trajectory 唤醒 ${line}`);
10306
+ } catch (err) {
10307
+ console.warn(`[serenity-hooks] ✗ trajectory 唤醒 tick 异常: ${String(err?.message ?? err)}`);
10308
+ }
10309
+ });
10310
+ };
10311
+ const startTimer = () => {
10312
+ if (timer) return;
10313
+ if (!wakeSchedulerEnabled()) return;
10314
+ if (collectLiveCccs(ctx).length === 0) return;
10315
+ timer = setInterval(tick, TICK_MS);
10316
+ timer.unref();
10317
+ console.log("[serenity-hooks] ✓ trajectory 唤醒调度器启动(5min tick)");
10318
+ tick();
10319
+ };
10320
+ startTimer();
10321
+ try {
10322
+ ctx.on("session/created", () => startTimer());
10323
+ } catch {}
10324
+ try {
10325
+ ctx.on("serenity/settings-changed", () => startTimer());
10326
+ } catch {}
10327
+ registerDisposer(ctx, "trajectory 唤醒调度器", () => {
10328
+ if (timer) {
10329
+ clearInterval(timer);
10330
+ timer = null;
10331
+ }
10332
+ });
10333
+ }
10334
+ //#endregion
9750
10335
  //#region src/weixin-output-guard.ts
9751
10336
  const PLUGIN_SOURCE = {
9752
10337
  kind: "plugin",
@@ -10654,7 +11239,7 @@ function matchCcc(input, candidates) {
10654
11239
  }
10655
11240
  /** 组装候选列表(discoverCccs 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
10656
11241
  async function collectCandidates(ctx) {
10657
- const { discoverCccs } = await import("./skiff-debug-w2c3UqQk.js").then((n) => n.i);
11242
+ const { discoverCccs } = await import("./skiff-debug-K2k1JnGj.js").then((n) => n.i);
10658
11243
  const entries = await discoverCccs(ctx, process.cwd());
10659
11244
  const seen = /* @__PURE__ */ new Set();
10660
11245
  const out = [];
@@ -11717,6 +12302,7 @@ function apply(ctx, config) {
11717
12302
  registerSkiff(ctx);
11718
12303
  registerAcp(ctx);
11719
12304
  registerAutopilot(ctx);
12305
+ registerWakeScheduler(ctx);
11720
12306
  registerWeixinBridge(ctx);
11721
12307
  registerWeixinSendApi(ctx);
11722
12308
  registerWeixinOutputGuard(ctx);