@shgroup/dsh-serenity-hooks 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dsh.plugin.json +1 -1
- package/lib/index.js +41 -475
- package/lib/tools/trajectory.d.ts +4 -0
- package/lib/{trajectory-bound-DETSEBpL.js → trajectory-bound-U3ZU_mI6.js} +2 -1
- package/lib/trajectory-ops.d.ts +3 -1
- package/lib/wake-scheduler-Ebdyvzuq.js +798 -0
- package/lib/wake-scheduler.d.ts +38 -0
- package/package.json +1 -1
- package/lib/wake-registry-Cj2aSHME.js +0 -258
package/lib/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCw
|
|
|
5
5
|
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-B2ajFypI.js";
|
|
6
6
|
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-CEHL5cek.js";
|
|
7
7
|
import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-FTjoWQTQ.js";
|
|
8
|
-
import { _ as sessionsRoot, a as DEFAULT_SESSION_SCOPE, b as summarize, c as createSession, d as getActiveSessionInfo, f as listSessions, g as sessionEvents$1, h as resolveSessionByTitle, i as resolveSessionTrajectoryLabel, l as extractSessionMdPathFromText, m as readActiveSessionMd,
|
|
9
|
-
import { a as
|
|
8
|
+
import { _ as sessionsRoot, a as DEFAULT_SESSION_SCOPE, b as summarize, c as createSession, d as getActiveSessionInfo, f as listSessions, g as sessionEvents$1, h as resolveSessionByTitle, i as resolveSessionTrajectoryLabel, l as extractSessionMdPathFromText, m as readActiveSessionMd, o as TRAJECTORY_ACTIONS, p as parseSessionContextFromEvents, r as readLastBound, s as clearActiveSessionInfo, t as appendBound, u as findSession, v as setActiveSessionInfo, x as useSession, y as showSession } from "./trajectory-bound-U3ZU_mI6.js";
|
|
9
|
+
import { a as WAKE_CATCH_UP_MS, i as registerDisposer, n as sendToTrajectory, o as addWake, r as wakeSchedulerState, s as listWakes, t as registerWakeScheduler } from "./wake-scheduler-Ebdyvzuq.js";
|
|
10
10
|
import { a as FACE_PORTS, c as MECHANISM_PORTS, i as ACP_HTTP_PORT, l as SKIFF_DEBUG_PORT, n as registerSettingsSection, o as GATEWAY_PORT, s as MAIN_WEB_PORT, t as readSimpleSettings, u as WEIXIN_SEND_PORT } from "./settings-section-DMDUoQum.js";
|
|
11
11
|
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-CtFYm53S.js";
|
|
12
12
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -955,7 +955,7 @@ const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
|
955
955
|
const ACC_CATALOG = `═══ ACC Usage Catalog ═══
|
|
956
956
|
Directory of ACC capabilities — each area lists where to go for details (guides live with their feature; this catalog only points).
|
|
957
957
|
|
|
958
|
-
① 轨迹本体 → container_trajectory(list/show/create/use + rebuild 超限重建 + wake-later
|
|
958
|
+
① 轨迹本体 → container_trajectory(list/show/create/use + rebuild 超限重建 + wake-later 未来一次性唤醒 + send-message 即时投递)
|
|
959
959
|
原 logbook 已并入(v1.33 用户裁决:废除 logbook 词);close/archive 删(归档走 container_fs mv),
|
|
960
960
|
health/qa/summary 分别并入 use / list,hook-develop-guide 并进 container_admin msm guide
|
|
961
961
|
② 认知质量框架 → praxis(eap/neat/cce 三合一——可实践理论注入,section 渐进披露,无参即目录)
|
|
@@ -966,6 +966,7 @@ Directory of ACC capabilities — each area lists where to go for details (guide
|
|
|
966
966
|
④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
|
|
967
967
|
ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
|
|
968
968
|
⑤ 自主与接入 → container_trajectory wake-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
|
|
969
|
+
container_trajectory send-message(跨轨迹的**即时**递送:live 目标立即注入,非 live 冷载入——等效于直接 wake;**有同步回执**)
|
|
969
970
|
weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
|
|
970
971
|
⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
|
|
971
972
|
⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
|
|
@@ -1866,7 +1867,7 @@ function waitAgentIdle(ctx, agent) {
|
|
|
1866
1867
|
}
|
|
1867
1868
|
//#endregion
|
|
1868
1869
|
//#region src/skiff-core.ts
|
|
1869
|
-
const PLUGIN_SOURCE$
|
|
1870
|
+
const PLUGIN_SOURCE$5 = {
|
|
1870
1871
|
kind: "plugin",
|
|
1871
1872
|
plugin: "dsh-serenity-hooks"
|
|
1872
1873
|
};
|
|
@@ -2274,7 +2275,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
|
|
|
2274
2275
|
type: "text",
|
|
2275
2276
|
text: question
|
|
2276
2277
|
}],
|
|
2277
|
-
source: PLUGIN_SOURCE$
|
|
2278
|
+
source: PLUGIN_SOURCE$5
|
|
2278
2279
|
}));
|
|
2279
2280
|
await waitAgentIdle(ctx, agent);
|
|
2280
2281
|
const answer = lastAssistantText$2(agent);
|
|
@@ -2972,6 +2973,10 @@ function createHandymanTool(ctx) {
|
|
|
2972
2973
|
* `container_trajectory`,无别名)——trajectory 是一等概念
|
|
2973
2974
|
* (SESSION.md 是它的持久身体),本工具管它的**载体生命周期 + 一次性时间安排**。
|
|
2974
2975
|
* 动作收敛为 **6 个**:list / show / create / use / rebuild / wake-later。
|
|
2976
|
+
* 🔴 2026-09-16(所有者裁决):**第 7 个动作 `send-message`(即时投递)**
|
|
2977
|
+
* ——与 `wake-later` 共用取用通路,差别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无);
|
|
2978
|
+
* 实现落在 `wake-scheduler.ts` 的 `sendToTrajectory()`("怎么把一句话送到一条轨迹"是调度器的知识)。
|
|
2979
|
+
* 设计稿 = `docs/trajectory-send-message-design.md`(v0.2 定稿)。
|
|
2975
2980
|
*
|
|
2976
2981
|
* 与旧面(logbook 11 动作 + trajectory 12 动作)的对应(用户逐条裁决,R↓):
|
|
2977
2982
|
* · `summary` → 并入 `list`(全库统计随清单一起给)
|
|
@@ -3114,13 +3119,13 @@ function renderText$3(value) {
|
|
|
3114
3119
|
function createTrajectoryTool(ctx) {
|
|
3115
3120
|
return defineTool({
|
|
3116
3121
|
name: "container_trajectory",
|
|
3117
|
-
description: "Trajectory (AGENT_SESSIONS/ — the persistent body of a trajectory; the dsh conversation is only its rebuildable carrier). Lifecycle +
|
|
3122
|
+
description: "Trajectory (AGENT_SESSIONS/ — the persistent body of a trajectory; the dsh conversation is only its rebuildable carrier). Lifecycle + delivery: list (inventory with stats and anomaly marks) / show (read one SESSION.md) / create / use (activate for this conversation, with inline integrity check) / rebuild (clear-and-rebuild the current conversation in place, Ship of Theseus) / wake-later (schedule ONE message to any trajectory at a future instant — fire-and-forget: no receipt, no recall, no panel) / send-message (deliver ONE message to a trajectory RIGHT NOW — live: injected immediately; not live: cold-resumed, i.e. equivalent to a direct wake; returns a synchronous receipt). create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars). use requires --summary (≤20 chars) and may pass --force to switch away from the currently-bound trajectory. rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self). wake-later requires target (S### or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message. send-message requires target + message. Its receipt means the message was QUEUED into the target (it does NOT mean the target ran or replied; a busy target applies it at its next round boundary). The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived.",
|
|
3118
3123
|
parameters: {
|
|
3119
3124
|
action: {
|
|
3120
3125
|
type: "string",
|
|
3121
3126
|
enum: [...TRAJECTORY_ACTIONS],
|
|
3122
3127
|
required: true,
|
|
3123
|
-
description: "Subcommand: list (inventory + stats + anomaly marks) / show (S### or dir name or fuzzy keyword) / create (--desc or --issue) / use (activate context; inline integrity check — pass silent, problems reported as hints, not errors) / rebuild (clear-and-rebuild current conversation — requires --summary + optional --note) / wake-later (one future instant + one message, delivered to target trajectory)"
|
|
3128
|
+
description: "Subcommand: list (inventory + stats + anomaly marks) / show (S### or dir name or fuzzy keyword) / create (--desc or --issue) / use (activate context; inline integrity check — pass silent, problems reported as hints, not errors) / rebuild (clear-and-rebuild current conversation — requires --summary + optional --note) / wake-later (one future instant + one message, delivered to target trajectory) / send-message (deliver one message RIGHT NOW: live target injected immediately, non-live cold-resumed like a direct wake; returns a synchronous receipt)"
|
|
3124
3129
|
},
|
|
3125
3130
|
name: {
|
|
3126
3131
|
type: "string",
|
|
@@ -3156,7 +3161,7 @@ function createTrajectoryTool(ctx) {
|
|
|
3156
3161
|
},
|
|
3157
3162
|
target: {
|
|
3158
3163
|
type: "string",
|
|
3159
|
-
description: "wake-later: target trajectory (S### or AGENT_SESSIONS directory name)"
|
|
3164
|
+
description: "wake-later/send-message: target trajectory (S### or AGENT_SESSIONS directory name)"
|
|
3160
3165
|
},
|
|
3161
3166
|
at: {
|
|
3162
3167
|
type: "string",
|
|
@@ -3164,7 +3169,7 @@ function createTrajectoryTool(ctx) {
|
|
|
3164
3169
|
},
|
|
3165
3170
|
message: {
|
|
3166
3171
|
type: "string",
|
|
3167
|
-
description: "wake-later: the single message delivered to the target trajectory"
|
|
3172
|
+
description: "wake-later/send-message: the single message delivered to the target trajectory"
|
|
3168
3173
|
}
|
|
3169
3174
|
},
|
|
3170
3175
|
output: {
|
|
@@ -3271,6 +3276,24 @@ function createTrajectoryTool(ctx) {
|
|
|
3271
3276
|
output: `✓ 已登记唤醒\n${renderWakeEntry(res.entry)}`
|
|
3272
3277
|
};
|
|
3273
3278
|
}
|
|
3279
|
+
case "send-message": {
|
|
3280
|
+
if (!args.target || !args.message) throw new Error("send-message requires target (S### or dir name) + message");
|
|
3281
|
+
const sendScope = agentScope$2(exec);
|
|
3282
|
+
const res = await sendToTrajectory(ctx, root, args.target, args.message, resolveSessionTrajectoryLabel(exec.agent?.session, sendScope === "default" ? "" : sendScope));
|
|
3283
|
+
if (!res.ok) return {
|
|
3284
|
+
ok: false,
|
|
3285
|
+
error: res.detail
|
|
3286
|
+
};
|
|
3287
|
+
return {
|
|
3288
|
+
ok: true,
|
|
3289
|
+
output: [
|
|
3290
|
+
"✓ 已即时投递",
|
|
3291
|
+
` · ${res.detail}`,
|
|
3292
|
+
" · 语义:消息**已进入目标队列**;目标若正在跑轮次,则在**轮次边界**生效(不打断当前轮)。",
|
|
3293
|
+
" · ⚠️ 回执只到「已入队」——**不表示**目标已执行或已答复。判\"目标真的动了\"须看它自己的 SESSION.md(文件级判据)。"
|
|
3294
|
+
].join("\n")
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3274
3297
|
default: throw new Error(`Unknown action: ${args.action}`);
|
|
3275
3298
|
}
|
|
3276
3299
|
}
|
|
@@ -4767,7 +4790,7 @@ function trajectoryCompactionReminderText(input) {
|
|
|
4767
4790
|
if (escalated) return `${eventToken("compaction")} ${facts} — you have been reminded repeatedly and have NOT compacted SESSION.md. This is now mandatory: STOP the current task step, load eap (praxis eap), rewrite SESSION.md, then resume. ${principles} ${frontmatterGuard} This reminder persists until the file is under the limit.`;
|
|
4768
4791
|
return `${eventToken("compaction")} ${facts}. Pause the current work and compact this trajectory's log (SESSION.md) before continuing — that log is the persistent body of this trajectory, and its size is operational entropy (H_op) the container pays on every read. ${principles} ${frontmatterGuard} After rewriting, resume the paused work; this reminder stops once the file is under the limit.`;
|
|
4769
4792
|
}
|
|
4770
|
-
const PLUGIN_SOURCE$
|
|
4793
|
+
const PLUGIN_SOURCE$4 = {
|
|
4771
4794
|
kind: "plugin",
|
|
4772
4795
|
plugin: "dsh-serenity-hooks"
|
|
4773
4796
|
};
|
|
@@ -4853,7 +4876,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
4853
4876
|
if (blocks.length === 0) return downstream;
|
|
4854
4877
|
const reminderAll = createUserMessage({
|
|
4855
4878
|
content: blocks,
|
|
4856
|
-
source: PLUGIN_SOURCE$
|
|
4879
|
+
source: PLUGIN_SOURCE$4
|
|
4857
4880
|
});
|
|
4858
4881
|
if (downstream.kind === "block") return {
|
|
4859
4882
|
kind: "block",
|
|
@@ -5575,7 +5598,7 @@ function toolsBlock() {
|
|
|
5575
5598
|
"The ACC (this plugin) provides the following built-in tools:",
|
|
5576
5599
|
"",
|
|
5577
5600
|
" container_fs — container filesystem operations (15 subcommands: root/resolve/exists/list/tree/relative/mkdir/rm/mv/cp/touch/append/reveal/info/find)",
|
|
5578
|
-
" container_trajectory — one trajectory: its persistent body (SESSION.md) + its timeline. Lifecycle: list (inventory + stats) / show / create / use (activate; inline integrity check — silent when fine) / rebuild (clear-and-rebuild this conversation in place, Ship of Theseus);
|
|
5601
|
+
" container_trajectory — one trajectory: its persistent body (SESSION.md) + its timeline. Lifecycle: list (inventory + stats) / show / create / use (activate; inline integrity check — silent when fine) / rebuild (clear-and-rebuild this conversation in place, Ship of Theseus); delivery: wake-later (one future instant + one message to any trajectory; fire-and-forget — no receipt, no recall) / send-message (deliver one message RIGHT NOW; live target injected immediately, non-live cold-resumed like a direct wake — returns a synchronous receipt)",
|
|
5579
5602
|
" dashboard — always-on container instruments: health (CCC three-principle check + registry integrity) / time (now) / wait (N seconds)",
|
|
5580
5603
|
" container_git— git operations (status/commit/push/log)",
|
|
5581
5604
|
" msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
|
|
@@ -6117,7 +6140,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
|
|
|
6117
6140
|
}
|
|
6118
6141
|
return lines.join("\n");
|
|
6119
6142
|
}
|
|
6120
|
-
const PLUGIN_SOURCE$
|
|
6143
|
+
const PLUGIN_SOURCE$3 = {
|
|
6121
6144
|
kind: "plugin",
|
|
6122
6145
|
plugin: "dsh-serenity-hooks"
|
|
6123
6146
|
};
|
|
@@ -6135,7 +6158,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
|
|
|
6135
6158
|
}];
|
|
6136
6159
|
return createUserMessage({
|
|
6137
6160
|
content,
|
|
6138
|
-
source: PLUGIN_SOURCE$
|
|
6161
|
+
source: PLUGIN_SOURCE$3
|
|
6139
6162
|
});
|
|
6140
6163
|
}
|
|
6141
6164
|
/** 每 agent 是否已注入过(进程内存态) */
|
|
@@ -8696,7 +8719,7 @@ var rebuild_exports = /* @__PURE__ */ __exportAll({
|
|
|
8696
8719
|
sanitizeFocusLine: () => sanitizeFocusLine,
|
|
8697
8720
|
stripAckSuffix: () => stripAckSuffix
|
|
8698
8721
|
});
|
|
8699
|
-
const PLUGIN_SOURCE$
|
|
8722
|
+
const PLUGIN_SOURCE$2 = {
|
|
8700
8723
|
kind: "plugin",
|
|
8701
8724
|
plugin: "dsh-serenity-hooks"
|
|
8702
8725
|
};
|
|
@@ -9015,7 +9038,7 @@ function registerRebuildTurnHook(ctx) {
|
|
|
9015
9038
|
type: "text",
|
|
9016
9039
|
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.`
|
|
9017
9040
|
}],
|
|
9018
|
-
source: PLUGIN_SOURCE$
|
|
9041
|
+
source: PLUGIN_SOURCE$2
|
|
9019
9042
|
}));
|
|
9020
9043
|
writeRebuildDiag(resolveSerenityRootFor(agent), {
|
|
9021
9044
|
sessionId: id,
|
|
@@ -11278,7 +11301,7 @@ function buildRebuke(hits) {
|
|
|
11278
11301
|
const rebukeStates = /* @__PURE__ */ new Map();
|
|
11279
11302
|
//#endregion
|
|
11280
11303
|
//#region src/output-guard-seam.ts
|
|
11281
|
-
const PLUGIN_SOURCE$
|
|
11304
|
+
const PLUGIN_SOURCE$1 = {
|
|
11282
11305
|
kind: "plugin",
|
|
11283
11306
|
plugin: "dsh-serenity-hooks"
|
|
11284
11307
|
};
|
|
@@ -11339,7 +11362,7 @@ function registerOutputGuardHook(ctx) {
|
|
|
11339
11362
|
type: "text",
|
|
11340
11363
|
text: buildRebuke(hits)
|
|
11341
11364
|
}],
|
|
11342
|
-
source: PLUGIN_SOURCE$
|
|
11365
|
+
source: PLUGIN_SOURCE$1
|
|
11343
11366
|
}));
|
|
11344
11367
|
} catch (error) {
|
|
11345
11368
|
console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
|
|
@@ -12101,463 +12124,6 @@ qInput.focus()
|
|
|
12101
12124
|
</html>`;
|
|
12102
12125
|
}
|
|
12103
12126
|
//#endregion
|
|
12104
|
-
//#region src/host/effect.ts
|
|
12105
|
-
/** 已告警的标签(进程级去重:重复装配/多测试替身不刷屏,但绝不静默) */
|
|
12106
|
-
const warnedLabels = /* @__PURE__ */ new Set();
|
|
12107
|
-
/**
|
|
12108
|
-
* 登记一个卸载时执行的清理。
|
|
12109
|
-
* @param ctx 宿主插件上下文
|
|
12110
|
-
* @param label 日志标签(定位是哪个资源未装配)
|
|
12111
|
-
* @param cleanup 卸载时执行;抛错被吞掉(不阻断其它清理)
|
|
12112
|
-
* @returns 是否成功装配(false = 宿主无 `ctx.effect`,已告警)
|
|
12113
|
-
*/
|
|
12114
|
-
function registerDisposer(ctx, label, cleanup) {
|
|
12115
|
-
const effect = ctx?.effect;
|
|
12116
|
-
if (typeof effect !== "function") {
|
|
12117
|
-
if (!warnedLabels.has(label)) {
|
|
12118
|
-
warnedLabels.add(label);
|
|
12119
|
-
console.warn(`[serenity-hooks] ✗ 资源拆卸未装配(${label}):ctx.effect 不可用(宿主契约缺失)`);
|
|
12120
|
-
}
|
|
12121
|
-
return false;
|
|
12122
|
-
}
|
|
12123
|
-
try {
|
|
12124
|
-
effect.call(ctx, () => () => {
|
|
12125
|
-
try {
|
|
12126
|
-
cleanup();
|
|
12127
|
-
} catch (err) {
|
|
12128
|
-
console.warn(`[serenity-hooks] ✗ 资源清理失败(${label}): ${String(err?.message ?? err)}`);
|
|
12129
|
-
}
|
|
12130
|
-
}, `dsh-serenity-hooks: ${label}`);
|
|
12131
|
-
return true;
|
|
12132
|
-
} catch (err) {
|
|
12133
|
-
console.warn(`[serenity-hooks] ✗ 资源拆卸注册失败(${label}): ${String(err?.message ?? err)}`);
|
|
12134
|
-
return false;
|
|
12135
|
-
}
|
|
12136
|
-
}
|
|
12137
|
-
//#endregion
|
|
12138
|
-
//#region src/clock-runtime.ts
|
|
12139
|
-
/**
|
|
12140
|
-
* clock-runtime.ts — **时钟运行时工厂**(S142 §38 复审候选 C6b,路径 **P2**)
|
|
12141
|
-
*
|
|
12142
|
-
* 本仓有**两条各自独立运行**的 5min 时钟:
|
|
12143
|
-
* · 唤醒调度器(`wake-scheduler.ts`,一次性唤醒,D58)
|
|
12144
|
-
* · autopilot 时钟(`autopilot-trajectory.ts`,周期自唤醒,D59)
|
|
12145
|
-
* 二者的**业务判据完全不同**(谁来扫 / 扫什么 / 投递给谁 / 按什么周期),但**引擎骨架逐字同形**
|
|
12146
|
-
* ——进程态 + 快照 + 复位 + `startTimer` + tick 外壳 + 事件接线 + disposer + `timer.unref()`。
|
|
12147
|
-
* 本条把**引擎**抽到本工厂,**各钟仍各持自己的定时器与自己的串行链**。
|
|
12148
|
-
*
|
|
12149
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
12150
|
-
* 🔴 硬约束一:**两条时钟绝不可共用"排队执行"的链**(这是 P2 与 P1 的分界判据)
|
|
12151
|
-
*
|
|
12152
|
-
* autopilot 每轮要跑该 CCC 的**偏见脚本**(`fetchBiasContent`,超时 60s),可能阻塞数十秒;
|
|
12153
|
-
* 若与唤醒调度器共用一条串行链,会把另一条一起**卡住**(一次性唤醒被无关的偏见脚本拖住)。
|
|
12154
|
-
* ⇒ 本工厂把 `chain` 作为**每实例私有状态**:`createClock()` 调用一次 = 一条独立的串行域。
|
|
12155
|
-
* 实例之间**零共享**(无模块级单例、无共享 Promise、无共享 map)。
|
|
12156
|
-
* ⚠️ 若日后有人把 `chain` 提到模块级"省一次分配",**两条时钟的隔离即刻失效**——不许。
|
|
12157
|
-
*
|
|
12158
|
-
* 🔴 硬约束二:**武装门只判全局闸**(`gate()`),**不判"本次有无目标"**
|
|
12159
|
-
*
|
|
12160
|
-
* 2026-09-14 F 段实测缺陷:旧实现把"有 live CCC"当**武装前置条件**,而宿主刚重启时 live 会话
|
|
12161
|
-
* 必为空,且**"恢复旧会话"不触发 `session/created`**(只有新建会)⇒ 启动瞬间判定落空 =
|
|
12162
|
-
* **时钟永久不武装**(实测 6.6h 零 tick)。⇒ 现在「**全局闸开即武装**」,"有无目标"降级为
|
|
12163
|
-
* tick 内的廉价判定。**武装与"本次有无目标"是两件事,本工厂不得把二者耦合。**
|
|
12164
|
-
*
|
|
12165
|
-
* 🔴 硬约束三:**两条闸必须继续分开传**(v1.34 S-1 解耦)
|
|
12166
|
-
*
|
|
12167
|
-
* 唤醒调度器 `wakeSchedulerEnabled`(**缺省开**)|autopilot `autopilotWakeEnabled`
|
|
12168
|
-
* (**缺省关**)——取值方向相反。工厂取 `gate` **回调**(每次现读),**不做**任何合并或缓存。
|
|
12169
|
-
*
|
|
12170
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
12171
|
-
* 两条时钟**逐项语义差**(`ClockOptions` 的每个可配项都对应下表一行,不是随意的灵活性):
|
|
12172
|
-
*
|
|
12173
|
-
* | 项 | 唤醒调度器 | autopilot | 工厂表达 |
|
|
12174
|
-
* |----|-----------|-----------|---------|
|
|
12175
|
-
* | `chain` | 有(全局串行) | 有(全局串行) | **总是私有**(硬约束一) |
|
|
12176
|
-
* | `lastTickLog` | 有(逐行摘要) | **无** | `logFrom` 不给 = 不记 |
|
|
12177
|
-
* | `ticks` 记账 | 工厂在 body **完成后**记 | **本钟**在同步段记 | `bodyCountsTick` + `begin` |
|
|
12178
|
-
* | body 返回类型 | `string[]`(人读摘要) | `void` | 泛型 `T` |
|
|
12179
|
-
* | 启动日志 | 固定串 | 含启用 CCC 计数 | `startLog: () => string` |
|
|
12180
|
-
* | body 异常兜底 | 留痕 + `console.warn` | 同 | 工厂统一(**吞异常不改语义**) |
|
|
12181
|
-
*
|
|
12182
|
-
* ───────────────────────────────────────────────────────────────────────────
|
|
12183
|
-
* **本工厂是"加法式"重构**(P2 的性质):不新增任何判定逻辑,只把同形骨架收成一份代码;
|
|
12184
|
-
* 两个模块仍各自导出 `wakeSchedulerState()` / `autopilotClockState()` 与各自的
|
|
12185
|
-
* `__reset…ForTest()`,**对外可观测面(字段名与语义)零变化**——`acc-diag` ①b 段、
|
|
12186
|
-
* `container-status.ts:containerClocks()` 与面板都在读它们(见该文件 :225–250 的设计红线)。
|
|
12187
|
-
*
|
|
12188
|
-
* 为什么**不**取 P1(真合并成一个 `setInterval` + 两条策略)——2026-09-15 所有者裁决:
|
|
12189
|
-
* P1 只多省 ~20 行,却要把 `armed` 拆成"宿主 + 每策略"两层、并强行分开串行域,
|
|
12190
|
-
* 即**动两条正在跑的时钟的骨架**;**不值当**(裁决原文见 SESSION §12.8②)。
|
|
12191
|
-
*/
|
|
12192
|
-
/**
|
|
12193
|
-
* tick 周期(**5 分钟**)。
|
|
12194
|
-
*
|
|
12195
|
-
* 2026-09-15 S142「ACC 侧 autopilot 退场」:本值原从 `autopilot-core.ts` 导入(`TICK_MS`),
|
|
12196
|
-
* 但该模块随 autopilot 退场,而**本模块的唯一消费者是唤醒调度器**(`wake-scheduler`)——
|
|
12197
|
-
* 值本身与 autopilot 无关 ⇒ 随消费者归位到本模块(不再依赖 autopilot 判据层)。
|
|
12198
|
-
*
|
|
12199
|
-
* ⚠️ 语义不得变:该值同时是**报告文案的来源**("等下个 Nmin tick"),历史上曾因文案写死
|
|
12200
|
-
* 10min 与实现 5min 不符而误导排查(§12.7 文案纠错)。
|
|
12201
|
-
*/
|
|
12202
|
-
const TICK_MS = 3e5;
|
|
12203
|
-
/**
|
|
12204
|
-
* 挂事件 → 触发 `start()`(**每个事件一次 try/catch 吞异常**:事件通道缺失不阻断时钟)。
|
|
12205
|
-
*
|
|
12206
|
-
* 为什么吞异常:宿主(headless profile 等)可能没有事件通道;**启动时已 `start()` 过一次**,
|
|
12207
|
-
* 事件只是"跟上面板开关 / 新会话"的热启动补丁,缺它不该让整个装配抛错。
|
|
12208
|
-
* @param on 宿主的 `on`(可能不存在)
|
|
12209
|
-
* @param ctx `on` 的 `this`(宿主 API 常要求绑定)
|
|
12210
|
-
* @param clock 目标时钟
|
|
12211
|
-
* @param events 事件名列表(回调恒为 `() => clock.start()`)
|
|
12212
|
-
*/
|
|
12213
|
-
function attachClockEvents(ctx, clock, events) {
|
|
12214
|
-
const on = ctx?.on;
|
|
12215
|
-
if (typeof on !== "function") return;
|
|
12216
|
-
for (const name of events) try {
|
|
12217
|
-
on.call(ctx, name, () => clock.start());
|
|
12218
|
-
} catch {}
|
|
12219
|
-
}
|
|
12220
|
-
/**
|
|
12221
|
-
* 建一条时钟(**每钟各调一次**——本工厂**不是**单例,调用次数 = 时钟条数)。
|
|
12222
|
-
*
|
|
12223
|
-
* 顺序忠实于既有实现:`start()` 先武装并立即跑一次 tick → 再挂事件。
|
|
12224
|
-
* 拆卸由调用方经 `registerDisposer(ctx, label, clock.dispose)` 落到宿主的 `ctx.effect`。
|
|
12225
|
-
* @param opts 见 {@link ClockOptions}(每项对应一条既有的语义差)
|
|
12226
|
-
* @returns 该钟的 `{start, dispose, snapshot, reset}`
|
|
12227
|
-
*/
|
|
12228
|
-
function createClock(opts) {
|
|
12229
|
-
const runtime = {
|
|
12230
|
-
armed: false,
|
|
12231
|
-
armedAt: null,
|
|
12232
|
-
lastTickAt: null,
|
|
12233
|
-
ticks: 0,
|
|
12234
|
-
lastSkipReason: null
|
|
12235
|
-
};
|
|
12236
|
-
if (opts.logFrom) runtime.lastTickLog = [];
|
|
12237
|
-
const st = {
|
|
12238
|
-
timer: null,
|
|
12239
|
-
chain: Promise.resolve(),
|
|
12240
|
-
runtime,
|
|
12241
|
-
wired: false,
|
|
12242
|
-
opts
|
|
12243
|
-
};
|
|
12244
|
-
/** 记一次 tick(工厂内部与 body 共用同一实现——只有一处会计逻辑) */
|
|
12245
|
-
const countTick = () => {
|
|
12246
|
-
runtime.lastTickAt = Date.now();
|
|
12247
|
-
runtime.ticks += 1;
|
|
12248
|
-
};
|
|
12249
|
-
/** 逐行打印(`logFrom` 缺省 ⇒ 本钟无 tick 日志,整段为死代码路径) */
|
|
12250
|
-
const emit = (lines) => {
|
|
12251
|
-
if (!opts.logFrom) return;
|
|
12252
|
-
for (const line of lines) console.log(`${opts.logFrom.prefix}${line}`);
|
|
12253
|
-
};
|
|
12254
|
-
const tick = () => {
|
|
12255
|
-
if (!opts.gate()) {
|
|
12256
|
-
runtime.lastSkipReason = opts.gateOffReason;
|
|
12257
|
-
return;
|
|
12258
|
-
}
|
|
12259
|
-
if (opts.begin) try {
|
|
12260
|
-
opts.begin();
|
|
12261
|
-
} catch (err) {
|
|
12262
|
-
console.warn(`[serenity-hooks] ✗ tick 前置异常: ${String(err?.message ?? err)}(${opts.label})`);
|
|
12263
|
-
return;
|
|
12264
|
-
}
|
|
12265
|
-
st.chain = st.chain.then(async () => {
|
|
12266
|
-
try {
|
|
12267
|
-
const value = await opts.body();
|
|
12268
|
-
if (!opts.bodyCountsTick) countTick();
|
|
12269
|
-
if (opts.logFrom) {
|
|
12270
|
-
runtime.lastTickLog = opts.logFrom.lines(value);
|
|
12271
|
-
emit(runtime.lastTickLog);
|
|
12272
|
-
}
|
|
12273
|
-
} catch (err) {
|
|
12274
|
-
const msg = `✗ tick 异常: ${String(err?.message ?? err)}`;
|
|
12275
|
-
if (opts.logFrom) runtime.lastTickLog = [msg];
|
|
12276
|
-
console.warn(`[serenity-hooks] ${msg}(${opts.label})`);
|
|
12277
|
-
}
|
|
12278
|
-
});
|
|
12279
|
-
};
|
|
12280
|
-
/**
|
|
12281
|
-
* 幂等启动 + 武装门:**已武装 → 原样返回**(防重复起定时器);
|
|
12282
|
-
* **闸关 → 不武装**(零资源占用);闸开则**无条件**武装(硬约束二:不判"有无目标")。
|
|
12283
|
-
*/
|
|
12284
|
-
const start = () => {
|
|
12285
|
-
if (st.timer) return;
|
|
12286
|
-
if (opts.events && !st.wired) {
|
|
12287
|
-
st.wired = true;
|
|
12288
|
-
attachClockEvents(opts.ctx, api, opts.events);
|
|
12289
|
-
}
|
|
12290
|
-
if (!opts.gate()) return;
|
|
12291
|
-
st.timer = setInterval(tick, TICK_MS);
|
|
12292
|
-
st.timer.unref();
|
|
12293
|
-
runtime.armed = true;
|
|
12294
|
-
runtime.armedAt = Date.now();
|
|
12295
|
-
console.log(opts.startLog());
|
|
12296
|
-
if (opts.immediate !== false) tick();
|
|
12297
|
-
};
|
|
12298
|
-
const dispose = () => {
|
|
12299
|
-
if (st.timer) {
|
|
12300
|
-
clearInterval(st.timer);
|
|
12301
|
-
st.timer = null;
|
|
12302
|
-
}
|
|
12303
|
-
runtime.armed = false;
|
|
12304
|
-
runtime.armedAt = null;
|
|
12305
|
-
opts.onDispose?.();
|
|
12306
|
-
};
|
|
12307
|
-
const api = {
|
|
12308
|
-
start,
|
|
12309
|
-
dispose,
|
|
12310
|
-
snapshot: () => ({
|
|
12311
|
-
enabled: opts.gate(),
|
|
12312
|
-
...runtime,
|
|
12313
|
-
...runtime.lastTickLog ? { lastTickLog: [...runtime.lastTickLog] } : {}
|
|
12314
|
-
}),
|
|
12315
|
-
noteSkipReason: (reason) => {
|
|
12316
|
-
runtime.lastSkipReason = reason;
|
|
12317
|
-
},
|
|
12318
|
-
countTick,
|
|
12319
|
-
enqueue: (work) => {
|
|
12320
|
-
st.chain = st.chain.then(work, () => void 0).then(() => void 0, (err) => {
|
|
12321
|
-
console.warn(`[serenity-hooks] ✗ 串行任务异常(${opts.label}): ${String(err?.message ?? err)}`);
|
|
12322
|
-
});
|
|
12323
|
-
},
|
|
12324
|
-
reset: () => {
|
|
12325
|
-
if (st.timer) {
|
|
12326
|
-
clearInterval(st.timer);
|
|
12327
|
-
st.timer = null;
|
|
12328
|
-
}
|
|
12329
|
-
runtime.armed = false;
|
|
12330
|
-
runtime.armedAt = null;
|
|
12331
|
-
runtime.lastTickAt = null;
|
|
12332
|
-
runtime.ticks = 0;
|
|
12333
|
-
runtime.lastSkipReason = null;
|
|
12334
|
-
if (opts.logFrom) runtime.lastTickLog = [];
|
|
12335
|
-
st.wired = false;
|
|
12336
|
-
opts.onReset?.();
|
|
12337
|
-
}
|
|
12338
|
-
};
|
|
12339
|
-
return api;
|
|
12340
|
-
}
|
|
12341
|
-
//#endregion
|
|
12342
|
-
//#region src/wake-scheduler.ts
|
|
12343
|
-
const PLUGIN_SOURCE$1 = {
|
|
12344
|
-
kind: "plugin",
|
|
12345
|
-
plugin: "dsh-serenity-hooks"
|
|
12346
|
-
};
|
|
12347
|
-
/**
|
|
12348
|
-
* 解析条目的 target(`S###` 或完整目录名)→ 目录名 + SESSION.md 路径。
|
|
12349
|
-
* @param root CCC 根
|
|
12350
|
-
* @param target 条目里的目标标识
|
|
12351
|
-
* @returns 命中返回目录信息,未命中 null
|
|
12352
|
-
*/
|
|
12353
|
-
function resolveWakeTarget(root, target) {
|
|
12354
|
-
const found = findSession(sessionsRoot(root), target);
|
|
12355
|
-
if (!found) return null;
|
|
12356
|
-
const mdPath = join(found.path, "SESSION.md");
|
|
12357
|
-
if (!existsSync(mdPath)) return null;
|
|
12358
|
-
return {
|
|
12359
|
-
dirName: basename(found.path),
|
|
12360
|
-
mdPath
|
|
12361
|
-
};
|
|
12362
|
-
}
|
|
12363
|
-
/**
|
|
12364
|
-
* 全局闸:**只看 `wakeSchedulerEnabled`**(缺省**开**;显式 `false` 才关)。
|
|
12365
|
-
*
|
|
12366
|
-
* ⚠️ 2026-09-15 用户裁决(S-1 **解耦**):「auto-trajectory 的开关**只关闭 auto-trajectory 唤醒**」。
|
|
12367
|
-
* ⇒ 本函数**不得**再把 `autopilotEnabled`(周期自唤醒闸)作为回退键 —— 旧实现是
|
|
12368
|
-
* `trajectoryEnabled === true || autopilotEnabled === true`,后果实测:所有者关掉 auto-trajectory 后,
|
|
12369
|
-
* **一次性唤醒 `wake-later` 被连带关掉**,注册表条目静默滞留(当日 6.6h 零 tick)。
|
|
12370
|
-
* 缺省由 false 改 true 的理由:唤醒注册表已是一等机制(D58),条目**全部由人类/agent 显式登记**
|
|
12371
|
-
* 未来时刻(无环境自主性),不需要"默认关"的实验保护;代价 = 一个 unref 的 5min 空检查。
|
|
12372
|
-
*
|
|
12373
|
-
* 设置服务不可用 → 关(保守;异常由日志与 `dashboard health` 响亮暴露,不静默)。
|
|
12374
|
-
*/
|
|
12375
|
-
function wakeSchedulerEnabled() {
|
|
12376
|
-
try {
|
|
12377
|
-
return readSimpleSettings().wakeSchedulerEnabled !== false;
|
|
12378
|
-
} catch {
|
|
12379
|
-
return false;
|
|
12380
|
-
}
|
|
12381
|
-
}
|
|
12382
|
-
/**
|
|
12383
|
-
* 🔴 **本钟的串行域**(C6b 硬约束):`clock.chain` 是**本实例私有**的——
|
|
12384
|
-
* 与 autopilot 的串行链**零共享**。autopilot 每轮跑 CCC 偏见脚本可能阻塞数十秒,
|
|
12385
|
-
* 共用链会把一次性唤醒一起卡住(这正是 P1 被否、取 P2 的理由,见 SESSION §12.8②)。
|
|
12386
|
-
*
|
|
12387
|
-
* **为什么这里是模块级常量**(而不是 `registerWakeScheduler` 的局部量):本钟的进程态是
|
|
12388
|
-
* **进程级可观测面**(`acc-diag` ①b / `containerClocks` 在读),且 `wakeSchedulerState()` /
|
|
12389
|
-
* `__resetWakeSchedulerStateForTest()` 是**公开导出**、可在未装配时被调用 ⇒ 实例必须**恒在**
|
|
12390
|
-
* (旧实现的 `schedulerRuntime` 也正是模块级常量)。装配只做 `clock.start()`。
|
|
12391
|
-
*/
|
|
12392
|
-
/** 本钟的装配项(`ctx` 由 {@link registerWakeScheduler} 在装配点填入) */
|
|
12393
|
-
const clockOpts = {
|
|
12394
|
-
label: "trajectory 唤醒调度器",
|
|
12395
|
-
ctx: void 0,
|
|
12396
|
-
events: ["session/created", "serenity/settings-changed"],
|
|
12397
|
-
gate: wakeSchedulerEnabled,
|
|
12398
|
-
gateOffReason: "唤醒调度器闸关闭(wakeSchedulerEnabled=false)",
|
|
12399
|
-
body: () => runWakeTick(clockOpts.ctx),
|
|
12400
|
-
logFrom: {
|
|
12401
|
-
prefix: "[serenity-hooks] trajectory 唤醒 ",
|
|
12402
|
-
lines: (lines) => lines
|
|
12403
|
-
},
|
|
12404
|
-
startLog: () => "[serenity-hooks] ✓ trajectory 唤醒调度器启动(5min tick)"
|
|
12405
|
-
};
|
|
12406
|
-
const clock = createClock(clockOpts);
|
|
12407
|
-
/** 调度器进程态快照(只读;`enabled` = 全局闸**当前**值,便于区分"未武装"与"闸关") */
|
|
12408
|
-
function wakeSchedulerState() {
|
|
12409
|
-
return clock.snapshot();
|
|
12410
|
-
}
|
|
12411
|
-
/**
|
|
12412
|
-
* 取目标 agent:live 优先 → 冷会话走 sessionController.resolveAgent → 标题回退。
|
|
12413
|
-
* @param ctx 插件上下文
|
|
12414
|
-
* @param root CCC 根
|
|
12415
|
-
* @param dirName 目标 trajectory 目录名
|
|
12416
|
-
* @returns 命中返回 agent 与取得方式(how 进 lastResult,便于诊断);未命中返回原因
|
|
12417
|
-
*/
|
|
12418
|
-
async function acquireWakeAgent(ctx, root, dirName) {
|
|
12419
|
-
const agents = hostAgents(ctx);
|
|
12420
|
-
const ids = listBoundSessionIds(root, dirName);
|
|
12421
|
-
for (const id of ids) {
|
|
12422
|
-
const live = agents?.get?.(id);
|
|
12423
|
-
if (live) return {
|
|
12424
|
-
agent: live,
|
|
12425
|
-
how: `live(bound ${id})`
|
|
12426
|
-
};
|
|
12427
|
-
}
|
|
12428
|
-
if (ids.length > 0) {
|
|
12429
|
-
const sc = hostService(ctx, "sessionController");
|
|
12430
|
-
const resolved = sc?.resolveAgent;
|
|
12431
|
-
if (typeof resolved !== "function") return { error: `目标会话未加载且 sessionController 不可用(headless profile?)——绑定 id: ${ids[0]}` };
|
|
12432
|
-
const id = ids[0];
|
|
12433
|
-
try {
|
|
12434
|
-
const out = await resolved.call(sc, id);
|
|
12435
|
-
if (out?.agent) return {
|
|
12436
|
-
agent: out.agent,
|
|
12437
|
-
how: `cold-resume(${id})`
|
|
12438
|
-
};
|
|
12439
|
-
return { error: `sessionController.resolveAgent 未返回 agent(${id}): ${String(out?.error ?? "未知原因")}` };
|
|
12440
|
-
} catch (err) {
|
|
12441
|
-
return { error: `冷会话载入失败(${id}): ${String(err?.message ?? err)}` };
|
|
12442
|
-
}
|
|
12443
|
-
}
|
|
12444
|
-
return { error: `无绑定会话记录(AGENT_SESSIONS/.bindings.json 中无 ${dirName})——目标轨迹需先被 container_trajectory use 激活过` };
|
|
12445
|
-
}
|
|
12446
|
-
/** 唤醒消息(唤起即投递正文:身份锚定 + 唤醒信息 + 任务) */
|
|
12447
|
-
function buildWakeText(entry, target) {
|
|
12448
|
-
return [
|
|
12449
|
-
`[trajectory 唤醒] 到点(登记于 ${entry.createdAt},发起者 ${entry.createdBy || "未知"},id ${entry.id})。`,
|
|
12450
|
-
"",
|
|
12451
|
-
`身份锚定:继续 ${target.dirName} 的 trajectory(SESSION.md: ${target.mdPath})。`,
|
|
12452
|
-
"",
|
|
12453
|
-
"唤醒信息:",
|
|
12454
|
-
entry.message,
|
|
12455
|
-
"",
|
|
12456
|
-
"任务:按上述唤醒信息继续本轨迹的工作;完成后把进展写入 SESSION.md。"
|
|
12457
|
-
].join("\n");
|
|
12458
|
-
}
|
|
12459
|
-
/**
|
|
12460
|
-
* 投递一条唤醒(fire-and-forget:`followup` 入队即返回,不等模型结果)。
|
|
12461
|
-
* @param ctx 插件上下文
|
|
12462
|
-
* @param root CCC 根
|
|
12463
|
-
* @param entry 唤醒条目
|
|
12464
|
-
* @returns 投递结果(写回 lastResult)
|
|
12465
|
-
*/
|
|
12466
|
-
async function deliverWake(ctx, root, entry) {
|
|
12467
|
-
const target = resolveWakeTarget(root, entry.target);
|
|
12468
|
-
if (!target) return {
|
|
12469
|
-
ok: false,
|
|
12470
|
-
detail: `目标 trajectory 未命中(${entry.target})`
|
|
12471
|
-
};
|
|
12472
|
-
const acquired = await acquireWakeAgent(ctx, root, target.dirName);
|
|
12473
|
-
if ("error" in acquired) return {
|
|
12474
|
-
ok: false,
|
|
12475
|
-
detail: acquired.error
|
|
12476
|
-
};
|
|
12477
|
-
try {
|
|
12478
|
-
acquired.agent.followup(createUserMessage({
|
|
12479
|
-
content: [{
|
|
12480
|
-
type: "text",
|
|
12481
|
-
text: buildWakeText(entry, target)
|
|
12482
|
-
}],
|
|
12483
|
-
source: PLUGIN_SOURCE$1
|
|
12484
|
-
}));
|
|
12485
|
-
} catch (err) {
|
|
12486
|
-
return {
|
|
12487
|
-
ok: false,
|
|
12488
|
-
detail: `投递失败(${acquired.how}): ${String(err?.message ?? err)}`
|
|
12489
|
-
};
|
|
12490
|
-
}
|
|
12491
|
-
return {
|
|
12492
|
-
ok: true,
|
|
12493
|
-
detail: `已投递 ${target.dirName}(${acquired.how})`
|
|
12494
|
-
};
|
|
12495
|
-
}
|
|
12496
|
-
/**
|
|
12497
|
-
* 本 tick 要扫的 CCC 根集合(**并集**)。
|
|
12498
|
-
*
|
|
12499
|
-
* C2(S142 2026-09-15 Q5):原实现是 `collectLiveCccs`——**只认 live 会话** ⇒
|
|
12500
|
-
* "完全冷掉的 CCC 不能自唤醒自己"(已知边界)。现改为 `ccc-roots.listCccs` 的并集枚举
|
|
12501
|
-
* (工作区注册表 ∪ 持久化会话 ∪ live 会话):**没有 live 会话的 CCC 也会被扫到**。
|
|
12502
|
-
*
|
|
12503
|
-
* ⚠️ 边界(本步**只**改"枚举哪些 CCC",**不改**"怎么找到要交付的 agent"):
|
|
12504
|
-
* 把 message 交给某条 trajectory 仍必须靠 **live 会话**解析(见本文件
|
|
12505
|
-
* `acquireWakeAgent`);无 live 会话时该 CCC 只是**被扫到、
|
|
12506
|
-
* 无交付对象** ⇒ `deliverWake` 返回 `无绑定会话记录…`,条目回 `pending` 并留痕。
|
|
12507
|
-
* 因此本函数**不得**让 tick 抛错(异常一律由调用方 tick 兜住,见 `runWakeTick` 的 try)。
|
|
12508
|
-
* @param ctx 插件上下文
|
|
12509
|
-
* @returns CCC 根列表(无任何来源 → `[]`)
|
|
12510
|
-
*/
|
|
12511
|
-
async function collectWakeCccs(ctx) {
|
|
12512
|
-
return (await listCccs(ctx)).map((e) => e.root);
|
|
12513
|
-
}
|
|
12514
|
-
/** 一次调度 tick:遍历已知 CCC → 到期项串行投递 / 超窗项置 missed;返回人读摘要 */
|
|
12515
|
-
async function runWakeTick(ctx) {
|
|
12516
|
-
const log = [];
|
|
12517
|
-
const roots = await collectWakeCccs(ctx).catch(() => []);
|
|
12518
|
-
if (roots.length === 0) {
|
|
12519
|
-
clock.noteSkipReason("无已知 CCC 可扫(等某个会话/工作区出现)");
|
|
12520
|
-
return log;
|
|
12521
|
-
}
|
|
12522
|
-
clock.noteSkipReason(null);
|
|
12523
|
-
for (const root of roots) {
|
|
12524
|
-
const { registry, error } = loadWakeRegistry(root);
|
|
12525
|
-
if (error) {
|
|
12526
|
-
log.push(`✗ ${root}: ${error}`);
|
|
12527
|
-
continue;
|
|
12528
|
-
}
|
|
12529
|
-
const { due, expired } = splitDueWakes(registry, Date.now(), WAKE_CATCH_UP_MS);
|
|
12530
|
-
for (const e of expired) {
|
|
12531
|
-
updateWake(root, e.id, {
|
|
12532
|
-
state: "missed",
|
|
12533
|
-
lastResult: `超过补跑窗口(${WAKE_CATCH_UP_MS / 36e5}h)未投递`
|
|
12534
|
-
});
|
|
12535
|
-
log.push(`· ${root}: ${e.id} → missed(超补跑窗口)`);
|
|
12536
|
-
}
|
|
12537
|
-
for (const e of due) {
|
|
12538
|
-
const res = await deliverWake(ctx, root, e);
|
|
12539
|
-
updateWake(root, e.id, {
|
|
12540
|
-
state: res.ok ? "delivered" : "pending",
|
|
12541
|
-
attempts: e.attempts + 1,
|
|
12542
|
-
lastResult: res.detail,
|
|
12543
|
-
deliveredAt: res.ok ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
12544
|
-
});
|
|
12545
|
-
log.push(`· ${root}: ${e.id} → ${res.ok ? "delivered" : `重试(${res.detail})`}`);
|
|
12546
|
-
}
|
|
12547
|
-
}
|
|
12548
|
-
return log;
|
|
12549
|
-
}
|
|
12550
|
-
/**
|
|
12551
|
-
* 装配调度器(index.ts apply 调用):5min tick + 生命周期 disposer +
|
|
12552
|
-
* 会话出现/settings 变化时热启动(与 autopilot 同款触发面,互不干扰)。
|
|
12553
|
-
* @param ctx 插件上下文
|
|
12554
|
-
*/
|
|
12555
|
-
function registerWakeScheduler(ctx) {
|
|
12556
|
-
clockOpts.ctx = ctx;
|
|
12557
|
-
clock.start();
|
|
12558
|
-
registerDisposer(ctx, "trajectory 唤醒调度器", () => clock.dispose());
|
|
12559
|
-
}
|
|
12560
|
-
//#endregion
|
|
12561
12127
|
//#region src/weixin-output-guard.ts
|
|
12562
12128
|
const PLUGIN_SOURCE = {
|
|
12563
12129
|
kind: "plugin",
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* `container_trajectory`,无别名)——trajectory 是一等概念
|
|
6
6
|
* (SESSION.md 是它的持久身体),本工具管它的**载体生命周期 + 一次性时间安排**。
|
|
7
7
|
* 动作收敛为 **6 个**:list / show / create / use / rebuild / wake-later。
|
|
8
|
+
* 🔴 2026-09-16(所有者裁决):**第 7 个动作 `send-message`(即时投递)**
|
|
9
|
+
* ——与 `wake-later` 共用取用通路,差别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无);
|
|
10
|
+
* 实现落在 `wake-scheduler.ts` 的 `sendToTrajectory()`("怎么把一句话送到一条轨迹"是调度器的知识)。
|
|
11
|
+
* 设计稿 = `docs/trajectory-send-message-design.md`(v0.2 定稿)。
|
|
8
12
|
*
|
|
9
13
|
* 与旧面(logbook 11 动作 + trajectory 12 动作)的对应(用户逐条裁决,R↓):
|
|
10
14
|
* · `summary` → 并入 `list`(全库统计随清单一起给)
|