@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/wake-scheduler.d.ts
CHANGED
|
@@ -70,6 +70,44 @@ export declare function buildWakeText(entry: WakeEntry, target: WakeTarget): str
|
|
|
70
70
|
* @returns 投递结果(写回 lastResult)
|
|
71
71
|
*/
|
|
72
72
|
export declare function deliverWake(ctx: Context, root: string, entry: WakeEntry): Promise<WakeDeliveryResult>;
|
|
73
|
+
/**
|
|
74
|
+
* 即时消息正文(**刻意不复用** {@link buildWakeText}):唤醒文本以「**到点**(登记于…)」开头,
|
|
75
|
+
* 对即时投递那是**错的事实**。本函数显式区分"这是即时消息、不是到点唤醒",
|
|
76
|
+
* 并写明"消息已入队、若此前在跑轮次则本轮结束后生效"(防目标误判时序)。
|
|
77
|
+
* @param target 目标轨迹(目录名 + SESSION.md)
|
|
78
|
+
* @param message 递送正文
|
|
79
|
+
* @param sender 发起者轨迹标识(供目标与人类识别来源)
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildSendText(target: WakeTarget, message: string, sender: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* 即时投递(`container_trajectory send-message`)——与唤醒**共用取用通路**,
|
|
84
|
+
* 区别只在**时刻**(现在 vs 未来)与**回执**(有 vs 无):
|
|
85
|
+
* · `wake-later`:未来时刻 + 一条 message → 落注册表 → 本文件的调度器到点投递(**fire-and-forget,无回执**)
|
|
86
|
+
* · `send-message`:**此刻**把一条 message 递进目标队列 → **同步回执** → **不落注册表**
|
|
87
|
+
*
|
|
88
|
+
* **为什么冷路径落在这里(R↓,2026-09-16 所有者裁决)**:`addWake` 显式拒绝 `at ≤ now`
|
|
89
|
+
* ("时刻必须在未来"——那是"**预约**"语义的地基,不许放宽)⇒ 即时投递**不能**靠一条
|
|
90
|
+
* `at=now` 的注册表条目实现(那还会让调度器把一次性即时投递当 `pending` **每 tick 重试**)。
|
|
91
|
+
* 必须直接走 {@link acquireWakeAgent}(live 优先 → `sessionController.resolveAgent` 冷载入)。
|
|
92
|
+
* 这正对应所有者要的「**会话不活跃则等效于直接 wake**」:**同一条冷载入通路**,只是**不等 tick**。
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ **承诺边界(不许含糊)**:回执只到「**已入队**」(`followup` 未抛错),
|
|
95
|
+
* **到不了**"目标已执行/已答复" —— 与 `deliverWake` 同一层。目标若正在跑轮次,
|
|
96
|
+
* 消息**排在轮次边界**(**不打断当前轮**)。判"目标真的动了"必须用**文件级判据**
|
|
97
|
+
* (它自己的 SESSION.md),**不得凭回执结案**(§12.B 的 `delivered ≠ 跑了一轮`)。
|
|
98
|
+
*
|
|
99
|
+
* **归属**:本函数放在调度器侧而非工具侧 —— "怎么把一句话送到一条轨迹"是**调度器的知识**,
|
|
100
|
+
* 工具只该知道"有这个动作"。这样 `trajectory.ts` 完全不碰调度器内部(v1.35.0 硬约束①:
|
|
101
|
+
* `wake-later` 链路**一行为不动**,本次改动 = **纯新增**)。
|
|
102
|
+
*
|
|
103
|
+
* @param ctx 插件上下文
|
|
104
|
+
* @param root CCC 根
|
|
105
|
+
* @param target `S###` 或 AGENT_SESSIONS 目录名
|
|
106
|
+
* @param message 递送正文
|
|
107
|
+
* @param sender 发起者轨迹标识
|
|
108
|
+
* @returns 投递结果(**同步**返回给调用方)
|
|
109
|
+
*/
|
|
110
|
+
export declare function sendToTrajectory(ctx: Context, root: string, target: string, message: string, sender: string): Promise<WakeDeliveryResult>;
|
|
73
111
|
/**
|
|
74
112
|
* 装配调度器(index.ts apply 调用):5min tick + 生命周期 disposer +
|
|
75
113
|
* 会话出现/settings 变化时热启动(与 autopilot 同款触发面,互不干扰)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.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": {
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
//#region src/wake-registry.ts
|
|
4
|
-
/**
|
|
5
|
-
* wake-registry.ts — trajectory 唤醒注册表(D58,S142 2026-09-13)
|
|
6
|
-
*
|
|
7
|
-
* 需求(用户原话):一个 CCC 内允许多个 trajectory 任意形态并行;某个 trajectory 可以
|
|
8
|
-
* **预定未来唤醒自己或别人**,本质上是一次「未来时刻 + 一条 message」的投递;「周期自唤醒」
|
|
9
|
-
* 曾是它的特例(原 ACC autopilot / D59,该机制已于 2026-09-15 整段退场)。协作边界:
|
|
10
|
-
* 互相可见、**无阻塞无等待**。
|
|
11
|
-
*
|
|
12
|
-
* 本模块只负责**注册表本身**(纯数据层,无宿主依赖,可单测):
|
|
13
|
-
* - 落点:CCC 内 `AGENT_SESSIONS/wake-registry.json`(随 CCC git,人可读可审计)
|
|
14
|
-
* - 状态机:`pending` →(投递成功)`delivered`|(超补跑窗口)`missed`|(`wake rm` 物理移除)
|
|
15
|
-
* - 投递即终结:一次性,不重投(无回执语义——发起方不 await,见设计文档 §2)
|
|
16
|
-
*
|
|
17
|
-
* 调度与投递在 `wake-scheduler.ts`(需要 ctx),本模块保持纯净。
|
|
18
|
-
*/
|
|
19
|
-
/** 注册表相对 CCC 根的路径(与 `.bindings.json` 同居 AGENT_SESSIONS/) */
|
|
20
|
-
const WAKE_REGISTRY_REL_PATH = "AGENT_SESSIONS/wake-registry.json";
|
|
21
|
-
/** 注册表格式版本(结构变更时 +1;读取端只接受本版本) */
|
|
22
|
-
const WAKE_REGISTRY_VERSION = 1;
|
|
23
|
-
/**
|
|
24
|
-
* 补跑窗口(缺省 2h):dsh 停机期间到期的条目,若迟到不超过本窗口则补投,
|
|
25
|
-
* 超过则置 `missed` 留痕(不补投——避免"半夜该做的事早上突然连环跑")。
|
|
26
|
-
*/
|
|
27
|
-
const WAKE_CATCH_UP_MS = 72e5;
|
|
28
|
-
/** 注册表绝对路径 */
|
|
29
|
-
function wakeRegistryPath(root) {
|
|
30
|
-
return join(root, WAKE_REGISTRY_REL_PATH);
|
|
31
|
-
}
|
|
32
|
-
/** 空注册表 */
|
|
33
|
-
function emptyRegistry() {
|
|
34
|
-
return {
|
|
35
|
-
version: WAKE_REGISTRY_VERSION,
|
|
36
|
-
entries: []
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/** 条目形状校验(容忍手改:字段缺失/类型不符 → 丢弃该条,不阻断整表) */
|
|
40
|
-
function asEntry(value) {
|
|
41
|
-
const e = value;
|
|
42
|
-
if (!e || typeof e.id !== "string" || typeof e.target !== "string") return null;
|
|
43
|
-
if (typeof e.at !== "string" || typeof e.message !== "string") return null;
|
|
44
|
-
const state = e.state === "delivered" || e.state === "missed" || e.state === "cancelled" ? e.state : "pending";
|
|
45
|
-
return {
|
|
46
|
-
id: e.id,
|
|
47
|
-
target: e.target,
|
|
48
|
-
at: e.at,
|
|
49
|
-
message: e.message,
|
|
50
|
-
state,
|
|
51
|
-
createdBy: typeof e.createdBy === "string" ? e.createdBy : "",
|
|
52
|
-
createdAt: typeof e.createdAt === "string" ? e.createdAt : "",
|
|
53
|
-
attempts: typeof e.attempts === "number" && Number.isFinite(e.attempts) ? e.attempts : 0,
|
|
54
|
-
lastResult: typeof e.lastResult === "string" ? e.lastResult : null,
|
|
55
|
-
deliveredAt: typeof e.deliveredAt === "string" ? e.deliveredAt : null
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* 读注册表。文件缺失 → 空表;损坏 → 空表 + error(不抛错,调用方决定是否告警)。
|
|
60
|
-
* @param root CCC 根
|
|
61
|
-
* @returns 注册表与读取错误(无错为 null)
|
|
62
|
-
*/
|
|
63
|
-
function loadWakeRegistry(root) {
|
|
64
|
-
const path = wakeRegistryPath(root);
|
|
65
|
-
if (!existsSync(path)) return {
|
|
66
|
-
registry: emptyRegistry(),
|
|
67
|
-
error: null
|
|
68
|
-
};
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
71
|
-
const raw = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
72
|
-
const entries = [];
|
|
73
|
-
for (const item of raw) {
|
|
74
|
-
const e = asEntry(item);
|
|
75
|
-
if (e) entries.push(e);
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
registry: {
|
|
79
|
-
version: WAKE_REGISTRY_VERSION,
|
|
80
|
-
entries
|
|
81
|
-
},
|
|
82
|
-
error: null
|
|
83
|
-
};
|
|
84
|
-
} catch (err) {
|
|
85
|
-
return {
|
|
86
|
-
registry: emptyRegistry(),
|
|
87
|
-
error: `唤醒注册表解析失败(${String(err?.message ?? err)})`
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** 原子写(tmp + rename;与 .bindings.json 同款形态) */
|
|
92
|
-
function saveWakeRegistry(root, registry) {
|
|
93
|
-
const path = wakeRegistryPath(root);
|
|
94
|
-
try {
|
|
95
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
-
const tmp = `${path}.tmp`;
|
|
97
|
-
writeFileSync(tmp, `${JSON.stringify({
|
|
98
|
-
version: WAKE_REGISTRY_VERSION,
|
|
99
|
-
entries: registry.entries
|
|
100
|
-
}, null, 2)}\n`, "utf-8");
|
|
101
|
-
renameSync(tmp, path);
|
|
102
|
-
return {
|
|
103
|
-
ok: true,
|
|
104
|
-
error: null
|
|
105
|
-
};
|
|
106
|
-
} catch (err) {
|
|
107
|
-
return {
|
|
108
|
-
ok: false,
|
|
109
|
-
error: `唤醒注册表写入失败(${String(err?.message ?? err)})`
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/** 条目 id:`w-<UTC 到分钟>-<4 hex>`(可读 + 低碰撞;不复用由"物理移除"保证) */
|
|
114
|
-
function newWakeId(nowMs, rand = Math.random) {
|
|
115
|
-
const iso = new Date(nowMs).toISOString();
|
|
116
|
-
return `w-${`${iso.slice(0, 10).replace(/-/g, "")}-${iso.slice(11, 16).replace(":", "")}`}-${Math.floor(rand() * 65536).toString(16).padStart(4, "0")}`;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* 解析唤醒时刻。接受两种写法(设计文档 §7):
|
|
120
|
-
* - 绝对:RFC3339(含时区偏移,如 `2026-09-14T09:00:00+08:00`)
|
|
121
|
-
* - 相对:`+30m` / `+2h`(便于 agent 直接登记"半小时后")
|
|
122
|
-
* @param input 用户/agent 传入的时刻串
|
|
123
|
-
* @param nowMs 当前时刻(相对写法基准)
|
|
124
|
-
* @returns `{ ok, ms }` 或 `{ ok: false, error }`
|
|
125
|
-
*/
|
|
126
|
-
function parseWakeAt(input, nowMs) {
|
|
127
|
-
const raw = input.trim();
|
|
128
|
-
if (raw === "") return {
|
|
129
|
-
ok: false,
|
|
130
|
-
error: "时刻为空"
|
|
131
|
-
};
|
|
132
|
-
const rel = /^\+(\d+(?:\.\d+)?)([mhd])$/.exec(raw);
|
|
133
|
-
if (rel) {
|
|
134
|
-
const n = Number(rel[1]);
|
|
135
|
-
const unit = rel[2];
|
|
136
|
-
return {
|
|
137
|
-
ok: true,
|
|
138
|
-
ms: nowMs + n * (unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5)
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
const ms = Date.parse(raw);
|
|
142
|
-
if (!Number.isFinite(ms)) return {
|
|
143
|
-
ok: false,
|
|
144
|
-
error: `时刻无法解析(须 RFC3339 或 +30m/+2h): ${raw}`
|
|
145
|
-
};
|
|
146
|
-
return {
|
|
147
|
-
ok: true,
|
|
148
|
-
ms
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* 登记一条唤醒。
|
|
153
|
-
* @param root CCC 根
|
|
154
|
-
* @param input 目标 trajectory / 时刻串 / message / 发起者 / 当前时刻
|
|
155
|
-
* @returns 成功返回条目,失败返回稳定错误文案(不抛错)
|
|
156
|
-
*/
|
|
157
|
-
function addWake(root, input) {
|
|
158
|
-
const target = input.target.trim();
|
|
159
|
-
if (target === "") return {
|
|
160
|
-
ok: false,
|
|
161
|
-
error: "target 为空(须 S### 或 AGENT_SESSIONS 目录名)"
|
|
162
|
-
};
|
|
163
|
-
const message = input.message.trim();
|
|
164
|
-
if (message === "") return {
|
|
165
|
-
ok: false,
|
|
166
|
-
error: "message 为空(唤醒必须携带一条信息)"
|
|
167
|
-
};
|
|
168
|
-
const parsed = parseWakeAt(input.at, input.nowMs);
|
|
169
|
-
if (!parsed.ok) return {
|
|
170
|
-
ok: false,
|
|
171
|
-
error: parsed.error
|
|
172
|
-
};
|
|
173
|
-
if (parsed.ms <= input.nowMs) return {
|
|
174
|
-
ok: false,
|
|
175
|
-
error: "时刻必须在未来(唤醒只允许可预期的未来)"
|
|
176
|
-
};
|
|
177
|
-
const { registry, error } = loadWakeRegistry(root);
|
|
178
|
-
if (error) return {
|
|
179
|
-
ok: false,
|
|
180
|
-
error
|
|
181
|
-
};
|
|
182
|
-
const entry = {
|
|
183
|
-
id: newWakeId(input.nowMs),
|
|
184
|
-
target,
|
|
185
|
-
at: new Date(parsed.ms).toISOString(),
|
|
186
|
-
message,
|
|
187
|
-
state: "pending",
|
|
188
|
-
createdBy: input.createdBy,
|
|
189
|
-
createdAt: new Date(input.nowMs).toISOString(),
|
|
190
|
-
attempts: 0,
|
|
191
|
-
lastResult: null,
|
|
192
|
-
deliveredAt: null
|
|
193
|
-
};
|
|
194
|
-
registry.entries.push(entry);
|
|
195
|
-
const saved = saveWakeRegistry(root, registry);
|
|
196
|
-
if (!saved.ok) return {
|
|
197
|
-
ok: false,
|
|
198
|
-
error: saved.error ?? "写入失败"
|
|
199
|
-
};
|
|
200
|
-
return {
|
|
201
|
-
ok: true,
|
|
202
|
-
entry
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* 到期切分(纯函数,可单测):
|
|
207
|
-
* - `due`:`state === 'pending'` 且 `now >= at`(在补跑窗口内)→ 应投递
|
|
208
|
-
* - `expired`:`state === 'pending'` 且 `now - at > catchUpMs` → 应置 missed(不投递)
|
|
209
|
-
* @param registry 注册表
|
|
210
|
-
* @param nowMs 当前时刻
|
|
211
|
-
* @param catchUpMs 补跑窗口
|
|
212
|
-
*/
|
|
213
|
-
function splitDueWakes(registry, nowMs, catchUpMs = WAKE_CATCH_UP_MS) {
|
|
214
|
-
const due = [];
|
|
215
|
-
const expired = [];
|
|
216
|
-
for (const e of registry.entries) {
|
|
217
|
-
if (e.state !== "pending") continue;
|
|
218
|
-
const at = Date.parse(e.at);
|
|
219
|
-
if (!Number.isFinite(at) || nowMs < at) continue;
|
|
220
|
-
if (nowMs - at > catchUpMs) expired.push(e);
|
|
221
|
-
else due.push(e);
|
|
222
|
-
}
|
|
223
|
-
return {
|
|
224
|
-
due,
|
|
225
|
-
expired
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* 更新一条(状态推进;调用方给最终状态与结果文案)。
|
|
230
|
-
* @param root CCC 根
|
|
231
|
-
* @param id 条目 id
|
|
232
|
-
* @param patch 要写入的字段
|
|
233
|
-
* @returns 写入结果
|
|
234
|
-
*/
|
|
235
|
-
function updateWake(root, id, patch) {
|
|
236
|
-
const { registry, error } = loadWakeRegistry(root);
|
|
237
|
-
if (error) return {
|
|
238
|
-
ok: false,
|
|
239
|
-
error
|
|
240
|
-
};
|
|
241
|
-
const entry = registry.entries.find((e) => e.id === id);
|
|
242
|
-
if (!entry) return {
|
|
243
|
-
ok: false,
|
|
244
|
-
error: `wake_not_found: ${id}`
|
|
245
|
-
};
|
|
246
|
-
Object.assign(entry, patch);
|
|
247
|
-
return saveWakeRegistry(root, registry);
|
|
248
|
-
}
|
|
249
|
-
/** 列条目(默认全量;`pendingOnly` 只列在办项——面板/工具用) */
|
|
250
|
-
function listWakes(root, opts = {}) {
|
|
251
|
-
const { registry, error } = loadWakeRegistry(root);
|
|
252
|
-
return {
|
|
253
|
-
entries: [...opts.pendingOnly ? registry.entries.filter((e) => e.state === "pending") : registry.entries].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)),
|
|
254
|
-
error
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
//#endregion
|
|
258
|
-
export { splitDueWakes as a, loadWakeRegistry as i, addWake as n, updateWake as o, listWakes as r, WAKE_CATCH_UP_MS as t };
|