dsh-expert-group 0.1.1

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.
Files changed (45) hide show
  1. package/LICENSE +38 -0
  2. package/README.md +355 -0
  3. package/THIRD_PARTY_NOTICES.md +35 -0
  4. package/assets/notification-whoosh.wav +0 -0
  5. package/bin/prompt-inspector.mjs +63 -0
  6. package/client-bundle.js +14891 -0
  7. package/client-expert-group-model.js +426 -0
  8. package/client-expert-group-sidebar.js +5401 -0
  9. package/client-freshness.js +364 -0
  10. package/client-model-config.js +2155 -0
  11. package/client-notifications.js +38 -0
  12. package/client.js +6487 -0
  13. package/cordis.patch.yml +10 -0
  14. package/fanout-host.js +1507 -0
  15. package/fanout.js +2981 -0
  16. package/host/composition.js +155 -0
  17. package/host/expert-group-runtime.js +822 -0
  18. package/host/expert-group-snapshot.js +450 -0
  19. package/host/fanout-runtime.js +822 -0
  20. package/host/handoff.js +80 -0
  21. package/host/lineage.js +274 -0
  22. package/host/notifications.js +189 -0
  23. package/host/persistence-health.js +389 -0
  24. package/host/recovery.js +242 -0
  25. package/host/rpc-transport.js +573 -0
  26. package/host/settings-runtime.js +168 -0
  27. package/host/settings-source.js +265 -0
  28. package/host/storage-binding.js +94 -0
  29. package/host/system-toast.js +916 -0
  30. package/image-routing.js +263 -0
  31. package/index.js +9327 -0
  32. package/install.ps1 +219 -0
  33. package/orchestrator.js +239 -0
  34. package/package.json +142 -0
  35. package/prompt-contracts.js +148 -0
  36. package/prompt-inspector.js +584 -0
  37. package/prompt-sidecar.js +610 -0
  38. package/resource-budget.js +540 -0
  39. package/role-registry.js +286 -0
  40. package/rpc-host.js +310 -0
  41. package/scripts/assemble-client.mjs +605 -0
  42. package/scripts/build.mjs +124 -0
  43. package/shared.js +4003 -0
  44. package/storage.js +190 -0
  45. package/uninstall.ps1 +47 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Host 关注点模块:pending handoff admission 注册表。
3
+ *
4
+ * 接管原先 index.js 里的 pendingHandoffAdmissions 数组与其增删查操作;
5
+ * admission 的编排(cohort/token/暂停/释放)仍留在 index.js,任务 06 再收缩。
6
+ *
7
+ * @module dsh-expert-group/host/handoff
8
+ */
9
+ export function createHandoffRegistryModule() {
10
+ const entries = [];
11
+ let disposed = false;
12
+
13
+ return Object.freeze({
14
+ /**
15
+ * 登记一个 admission entry(调用方负责构造 entry 字段)。
16
+ *
17
+ * 已销毁后静默丢弃并返回 undefined,不再抛错:注册路径可能落在迟到的 promise
18
+ * 回调里(admission 的 await 期间插件被卸载,微任务/后续 await 恢复后才回到
19
+ * 这里),此时抛出的异常没有任何调用方接收,只会变成 unhandled rejection。
20
+ * 与 host/recovery.js 的 queueRebuild 保持同一迟到语义:先判销毁、再做形状校验,
21
+ * 因此销毁后的畸形 entry 也一并丢弃,而不是半途抛错。
22
+ *
23
+ * 参数形状校验仍然保留:未销毁时的调用方缺陷(null/非对象)照旧抛 TypeError,
24
+ * 只有"已销毁"这一竞态态才降级为丢弃。
25
+ *
26
+ * @param {unknown} entry - 待登记的 admission entry。
27
+ * @returns {object|undefined} 登记成功时返回原 entry;已销毁时返回 undefined(丢弃指示)。
28
+ */
29
+ add(entry) {
30
+ if (disposed) return void 0;
31
+ if (entry === null || typeof entry !== "object") throw new TypeError("handoff admission 必须是对象");
32
+ entries.push(entry);
33
+ return entry;
34
+ },
35
+ contains: (entry) => entry !== undefined && entries.includes(entry),
36
+ remove(entry) {
37
+ if (entry === undefined) return false;
38
+ const index = entries.indexOf(entry);
39
+ if (index === -1) return false;
40
+ entries.splice(index, 1);
41
+ return true;
42
+ },
43
+ /** 只读快照;调用方不得依赖其身份。 */
44
+ entries: () => [...entries],
45
+ find(predicate) {
46
+ if (typeof predicate !== "function") throw new TypeError("find 需要谓词");
47
+ return entries.find(predicate);
48
+ },
49
+ filter(predicate) {
50
+ if (typeof predicate !== "function") throw new TypeError("filter 需要谓词");
51
+ return entries.filter(predicate);
52
+ },
53
+ some(predicate) {
54
+ if (typeof predicate !== "function") throw new TypeError("some 需要谓词");
55
+ return entries.some(predicate);
56
+ },
57
+ /** 按父 Agent 清理;返回被移除的 entry 数。 */
58
+ removeForParent(parent) {
59
+ let removed = 0;
60
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
61
+ if (entries[index]?.parent === parent) {
62
+ entries.splice(index, 1);
63
+ removed += 1;
64
+ }
65
+ }
66
+ return removed;
67
+ },
68
+ clear() {
69
+ const removed = entries.length;
70
+ entries.length = 0;
71
+ return removed;
72
+ },
73
+ get size() { return entries.length; },
74
+ describe: () => ({ pending: entries.length, disposed }),
75
+ dispose() {
76
+ disposed = true;
77
+ entries.length = 0;
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Host 关注点模块:root/child 血缘与 fan-out 父任务 ownership。
3
+ *
4
+ * 接管原先散落在 apply 里的 root 代际表(current/known/generation token)、
5
+ * fan-out 父任务 -> 父会话/父 Agent 映射、deferred 任务索引与父任务 id 分配。
6
+ * 这些索引只保存寻址关系,不复制 task/result 数据;完整状态仍由 fan-out manager 持有。
7
+ *
8
+ * 依赖全部注入,模块不读取 index.js 的闭包变量,也不创建 DSH 服务。
9
+ *
10
+ * @module dsh-expert-group/host/lineage
11
+ */
12
+
13
+ function isNonEmptyString(value) {
14
+ return typeof value === "string" && value.length > 0;
15
+ }
16
+
17
+ /**
18
+ * @param {object} options
19
+ * @param {WeakSet} [options.disposedRoots] - 已销毁 root 的 exact 对象集合(由组合根持有)。
20
+ * @param {Function} options.isRootAgent - 判断对象是否是根 Agent 身份。
21
+ * @param {Function} options.getAgent - agents.get(id)。
22
+ * @param {Function} [options.getTask] - fan-out manager.getTask(id)(用于 id 冲突检测)。
23
+ * @param {Function} [options.listTasks] - fan-out manager.listTasks()。
24
+ * @param {Function} [options.now] - 时钟,默认 Date.now。
25
+ */
26
+ export function createLineageModule({
27
+ disposedRoots,
28
+ isRootAgent,
29
+ getAgent,
30
+ getTask = () => null,
31
+ listTasks = () => [],
32
+ now = Date.now,
33
+ isGenerationBlocked = () => false,
34
+ } = {}) {
35
+ if (typeof isRootAgent !== "function") throw new TypeError("createLineageModule 需要 isRootAgent");
36
+ if (typeof getAgent !== "function") throw new TypeError("createLineageModule 需要 getAgent");
37
+ if (typeof now !== "function") throw new TypeError("createLineageModule 需要 now");
38
+
39
+ const knownRoots = new WeakSet();
40
+ const currentRoots = new Map();
41
+ const generationTokens = new WeakMap();
42
+ const parentSessions = new Map();
43
+ const parentAgents = new Map();
44
+ /**
45
+ * parentTaskId -> 绑定时父 Agent 的 generation token(exact 对象 + 活跃代际证据)。
46
+ *
47
+ * 为什么需要它:`parentSessions/parentAgents` 只回答「这个任务属于哪个会话/哪个对象」,
48
+ * 无法区分「绑定时的那一代 root」与「同 id 的 replacement」。续作(continuation)是**写**
49
+ * 权限,只能在「当前调用者就是绑定时那一代 live root」时才授予:
50
+ * - 同一 root 存活:token.agent === 调用者 且 isLiveGeneration(token) 为真;
51
+ * - root 被销毁/替换:token.active 被置 false 或 isLiveRoot 失败 → 拒绝;
52
+ * - 冷恢复后只从持久化 parentSessions 重建(restoreParentSessions)时不铸 token →
53
+ * 没有 exact 代际证据 → 续作 fail-closed(只允许寻址/读取)。
54
+ */
55
+ const parentTokens = new Map();
56
+ const deferredTasks = new Map();
57
+ const deferredParents = new Map();
58
+ let sequence = 0;
59
+ let disposed = false;
60
+
61
+ const isDisposed = (agent) => disposedRoots !== undefined && disposedRoots !== null && disposedRoots.has(agent);
62
+
63
+ const api = {
64
+ // ── root 代际 ──────────────────────────────────────────────────────────
65
+ /** 登记 root 为当前代际,并分配(或复用)其 generation token。 */
66
+ activateRoot(agent) {
67
+ if (!isRootAgent(agent) || !isNonEmptyString(agent?.id)) return void 0;
68
+ knownRoots.add(agent);
69
+ currentRoots.set(agent.id, agent);
70
+ return api.tokenFor(agent);
71
+ },
72
+ /** 当前代际的 exact root 对象。 */
73
+ activeRoot(id) {
74
+ return isNonEmptyString(id) ? currentRoots.get(id) : void 0;
75
+ },
76
+ /** 该对象是否是当前代际 root(同 id 的新 root 会取代旧对象)。 */
77
+ isCurrentRoot(agent) {
78
+ return isRootAgent(agent) && isNonEmptyString(agent?.id) && currentRoots.get(agent.id) === agent;
79
+ },
80
+ /** root replacement/dispose 时移除当前代际映射。 */
81
+ releaseRoot(agent) {
82
+ if (!isNonEmptyString(agent?.id)) return false;
83
+ if (currentRoots.get(agent.id) !== agent) return false;
84
+ currentRoots.delete(agent.id);
85
+ return true;
86
+ },
87
+ /** 是否曾经是本插件的 root(用于 dispose 判定)。 */
88
+ isKnownRoot: (agent) => knownRoots.has(agent),
89
+ /** exact root 对象的 generation token;非 root 返回 undefined。 */
90
+ tokenFor(agent) {
91
+ if (!isRootAgent(agent)) return void 0;
92
+ let token = generationTokens.get(agent);
93
+ if (token === void 0) {
94
+ token = { agent, active: true };
95
+ generationTokens.set(agent, token);
96
+ }
97
+ return token;
98
+ },
99
+ /** 只读查询:不创建 token。 */
100
+ tokenOf(agent) {
101
+ return generationTokens.get(agent);
102
+ },
103
+ /** token 仍是该 exact root 的活跃代际。 */
104
+ isLiveGeneration(agent, token) {
105
+ // 组合根可注入阻塞谓词(卸载/handoff 停止);一旦阻塞,旧 token 立即失效。
106
+ if (isGenerationBlocked()) return false;
107
+ return token !== void 0 && token.agent === agent && token.active === true
108
+ && generationTokens.get(agent) === token && api.isLiveRoot(agent);
109
+ },
110
+ /** root 仍在当前代际且未被销毁(与 index.js 旧 isLiveRootAgent 等价)。 */
111
+ isLiveRoot(agent) {
112
+ return isRootAgent(agent)
113
+ && !isDisposed(agent)
114
+ && isNonEmptyString(agent?.id)
115
+ && currentRoots.get(agent.id) === agent
116
+ && getAgent(agent.id) === agent;
117
+ },
118
+ isDisposed,
119
+ /** 标记 exact 对象已销毁;token 同时失活。 */
120
+ markDisposed(agent) {
121
+ if (agent === null || typeof agent !== "object") return false;
122
+ disposedRoots?.add?.(agent);
123
+ const token = generationTokens.get(agent);
124
+ if (token !== void 0) token.active = false;
125
+ return true;
126
+ },
127
+ knownRootCount: () => currentRoots.size,
128
+
129
+ // ── fan-out 父任务 ownership ────────────────────────────────────────────
130
+ /**
131
+ * 绑定父任务到父会话/父 Agent,并记录绑定时的 generation token。
132
+ *
133
+ * token 只在**绑定当时**由该 exact parentAgent 铸造(或外部显式传入),之后不再刷新:
134
+ * 只有它仍然 `isLiveGeneration` 时,这个任务的可写权限(续作)才成立。这样同 id 的
135
+ * replacement 即使让 `getAgent(id)` 指向新对象,也拿不到旧任务的可写权限。
136
+ */
137
+ bindParentTask({ parentTaskId, parentId, parentAgent, token } = {}) {
138
+ if (!isNonEmptyString(parentTaskId) || !isNonEmptyString(parentId)) return false;
139
+ parentSessions.set(parentTaskId, parentId);
140
+ if (parentAgent !== undefined && parentAgent !== null) {
141
+ parentAgents.set(parentTaskId, parentAgent);
142
+ const recorded = token !== undefined && token !== null ? token : api.tokenFor(parentAgent);
143
+ if (recorded !== undefined && recorded !== null) parentTokens.set(parentTaskId, recorded);
144
+ else parentTokens.delete(parentTaskId);
145
+ }
146
+ deferredTasks.delete(parentTaskId);
147
+ deferredParents.delete(parentTaskId);
148
+ return true;
149
+ },
150
+ /** 绑定时的 exact generation token;无证据(未绑定/只从快照恢复寻址)返回 undefined。 */
151
+ parentTokenOf: (parentTaskId) => (isNonEmptyString(parentTaskId) ? parentTokens.get(parentTaskId) : void 0),
152
+ /** 父 Agent:缓存失效(被替换)时按 parentId 重新解析。 */
153
+ parentAgentOf(parentTaskId) {
154
+ const cached = parentAgents.get(parentTaskId);
155
+ if (cached !== undefined && getAgent(cached.id) === cached) return cached;
156
+ const parentId = parentSessions.get(parentTaskId);
157
+ if (!isNonEmptyString(parentId)) return void 0;
158
+ const parent = getAgent(parentId);
159
+ if (parent !== void 0) parentAgents.set(parentTaskId, parent);
160
+ return parent;
161
+ },
162
+ /** 为父任务分配稳定、单调、与既有任务不冲突的 id。 */
163
+ allocateParentTaskId(parent) {
164
+ if (!isNonEmptyString(parent?.id)) throw new TypeError("allocateParentTaskId 需要带 id 的父 Agent");
165
+ let candidate;
166
+ do {
167
+ sequence += 1;
168
+ candidate = "fanout-" + parent.id + "-" + now().toString(36) + "-" + sequence.toString(36);
169
+ } while (getTask(candidate) !== null);
170
+ return candidate;
171
+ },
172
+ /** 释放单个父任务的全部寻址关系(历史仍由 manager 保留)。 */
173
+ releaseParentTask(parentTaskId) {
174
+ const removed = parentSessions.delete(parentTaskId);
175
+ parentAgents.delete(parentTaskId);
176
+ parentTokens.delete(parentTaskId);
177
+ deferredTasks.delete(parentTaskId);
178
+ deferredParents.delete(parentTaskId);
179
+ return removed;
180
+ },
181
+ /** root replacement/dispose 后摘除该 Agent 的 fan-out ownership。 */
182
+ releaseAgent(agent) {
183
+ if (agent === undefined || agent === null || !isNonEmptyString(agent.id)) return 0;
184
+ let removed = 0;
185
+ for (const [parentTaskId, recordedAgent] of [...parentAgents]) {
186
+ if (recordedAgent === agent) {
187
+ api.releaseParentTask(parentTaskId);
188
+ removed += 1;
189
+ }
190
+ }
191
+ for (const [parentTaskId, parentId] of [...parentSessions]) {
192
+ if (parentId === agent.id && !parentAgents.has(parentTaskId)) {
193
+ api.releaseParentTask(parentTaskId);
194
+ removed += 1;
195
+ }
196
+ }
197
+ for (const [parentTaskId, parentId] of [...deferredParents]) {
198
+ if (parentId === agent.id) {
199
+ api.releaseParentTask(parentTaskId);
200
+ removed += 1;
201
+ }
202
+ }
203
+ return removed;
204
+ },
205
+ /**
206
+ * 从持久化快照恢复 parentSessions 索引(只接受非空字符串对)。
207
+ *
208
+ * 这里**只恢复寻址**(读路径/RPC 归属过滤),不铸造 generation token:冷恢复后的
209
+ * 任务是否可写(续作)必须由后续的真实绑定(`bindParentTask`,带 live root 的 exact
210
+ * token)决定,否则同 id replacement 会继承旧任务的可写权限。
211
+ */
212
+ restoreParentSessions(snapshot) {
213
+ const source = snapshot?.parentSessions;
214
+ if (source === null || typeof source !== "object" || Array.isArray(source)) return 0;
215
+ let restored = 0;
216
+ for (const [parentTaskId, parentId] of Object.entries(source)) {
217
+ if (!isNonEmptyString(parentTaskId) || !isNonEmptyString(parentId)) continue;
218
+ parentSessions.set(parentTaskId, parentId);
219
+ const parent = getAgent(parentId);
220
+ if (parent !== void 0) parentAgents.set(parentTaskId, parent);
221
+ // 不设置 parentTokens:没有绑定当时的 exact 代际证据 → 续作 fail-closed。
222
+ parentTokens.delete(parentTaskId);
223
+ restored += 1;
224
+ }
225
+ return restored;
226
+ },
227
+ /** 快照投影:parentTaskId -> parentSessionId。 */
228
+ parentSessionsSnapshot() {
229
+ const result = {};
230
+ for (const [parentTaskId, parentId] of parentSessions) {
231
+ if (isNonEmptyString(parentTaskId) && isNonEmptyString(parentId)) result[parentTaskId] = parentId;
232
+ }
233
+ return result;
234
+ },
235
+ parentSessionOf: (parentTaskId) => parentSessions.get(parentTaskId),
236
+ /** 缺失 root 的 queued 任务:独立保留,不阻塞其它任务。 */
237
+ deferTask({ parentTaskId, parentId, task } = {}) {
238
+ if (!isNonEmptyString(parentTaskId)) return false;
239
+ deferredTasks.set(parentTaskId, task);
240
+ if (isNonEmptyString(parentId)) deferredParents.set(parentTaskId, parentId);
241
+ return true;
242
+ },
243
+ deferredTaskOf: (parentTaskId) => deferredTasks.get(parentTaskId),
244
+ deferredTasks: () => [...deferredTasks.values()],
245
+ deferredParentOf: (parentTaskId) => deferredParents.get(parentTaskId),
246
+ /** RPC/反馈使用的 ownership 查询:只有当前代际映射才算归属。 */
247
+ resolveOwner(parentTaskId) {
248
+ return isNonEmptyString(parentTaskId) ? parentSessions.get(parentTaskId) : void 0;
249
+ },
250
+ tasksOwnedBy(sessionId) {
251
+ if (!isNonEmptyString(sessionId)) return [];
252
+ return listTasks().filter((task) => api.resolveOwner(task?.parentTaskId) === sessionId);
253
+ },
254
+ describe() {
255
+ return {
256
+ currentRoots: currentRoots.size,
257
+ parentTasks: parentSessions.size,
258
+ deferredTasks: deferredTasks.size,
259
+ disposed,
260
+ };
261
+ },
262
+ dispose() {
263
+ disposed = true;
264
+ currentRoots.clear();
265
+ parentSessions.clear();
266
+ parentAgents.clear();
267
+ parentTokens.clear();
268
+ deferredTasks.clear();
269
+ deferredParents.clear();
270
+ sequence = 0;
271
+ },
272
+ };
273
+ return Object.freeze(api);
274
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Host 关注点模块:root run 终止快照注册表。
3
+ *
4
+ * 旧实现同时持有"事件队列 registry"和"root completion 去重状态机",客户端必须靠
5
+ * 短时轮询按游标消费事件批次,投递正确性因此依赖轮询时机与队列 TTL。新实现只保留
6
+ * 生命周期权威真正需要的那一份状态:**每个 root run 的当前终止快照**。
7
+ *
8
+ * 快照只在 root 主代理 turn 已终止、且该 root 的全部递归后代专家都不在运行时写入
9
+ * (递归判定 fail closed,见 shared.snapshotRunningRootDescendants),并一直保留到
10
+ * 下一次真实用户输入开启新 run 或 root 被 dispose。客户端任何时刻读取同一份权威结果,
11
+ * 因此不需要"短时事件轮询"就能拿到正确状态。
12
+ *
13
+ * 终止原因不再被过滤成"只有成功才通知":completed / blocked / error / aborted /
14
+ * interrupted / max-tokens 都会成为快照原因,由客户端按原因显示不同文案。
15
+ *
16
+ * @module dsh-expert-group/host/notifications
17
+ */
18
+ import { snapshotRunningRootDescendants } from "../shared.js";
19
+
20
+ /**
21
+ * DSH turn/end 的已知终止原因。TurnEndReason 是 merge-extensible 联合类型,
22
+ * 表外的 kind 一律归一为 unknown,绝不能被当作"completed"。
23
+ */
24
+ export const ROOT_TERMINATION_REASONS = Object.freeze([
25
+ "completed",
26
+ "blocked",
27
+ "error",
28
+ "aborted",
29
+ "interrupted",
30
+ "max-tokens",
31
+ ]);
32
+
33
+ /** 归一化终止原因:缺失或未知 kind 都返回 unknown。 */
34
+ export function normalizeRootTerminationReason(reason) {
35
+ const kind = typeof reason?.kind === "string" ? reason.kind.trim() : "";
36
+ if (kind.length === 0) return "unknown";
37
+ return ROOT_TERMINATION_REASONS.includes(kind) ? kind : "unknown";
38
+ }
39
+
40
+ const EMPTY_VIEW = Object.freeze({ rootId: null, generation: null, runSeq: 0, snapshot: null });
41
+
42
+ /**
43
+ * 创建 root run 终止快照模块。
44
+ *
45
+ * @param {object} [options]
46
+ * @param {() => unknown} [options.listAgents] - 读取全部 live Agent 的查询;缺失即未知。
47
+ * @param {() => number} [options.now] - 可注入时钟,测试用它固定快照时间戳。
48
+ */
49
+ export function createNotificationModule({ listAgents, now = () => Date.now() } = {}) {
50
+ /** rootId -> { generation, runSeq, publishedRunSeq, snapshot } */
51
+ const roots = new Map();
52
+ // 发布序号单调递增:客户端用 generation+runSeq+seq 去重,因此撤销后又真正终止的
53
+ // 同一次 run 仍然可以被提示,而同一次发布永远只提示一次。
54
+ let publishSeq = 0;
55
+ let disposed = false;
56
+
57
+ const timestamp = () => {
58
+ const value = Number(now());
59
+ return Number.isFinite(value) ? Math.trunc(value) : Date.now();
60
+ };
61
+ const validIdentity = (rootId, generation) => typeof rootId === "string" && rootId.length > 0
62
+ && Number.isSafeInteger(generation) && generation > 0;
63
+
64
+ /**
65
+ * 读取(可选创建)某个 exact root generation 的状态。generation 单调:迟到的旧代际
66
+ * 不能读取或覆盖新代际的状态,同 id 的 replacement 因此不会继承旧 root 的快照。
67
+ */
68
+ function stateOf(rootId, generation, create) {
69
+ if (!validIdentity(rootId, generation)) return void 0;
70
+ const existing = roots.get(rootId);
71
+ if (existing !== void 0 && generation < existing.generation) return void 0;
72
+ if (existing !== void 0 && existing.generation === generation) return existing;
73
+ if (create !== true) return void 0;
74
+ const created = { generation, runSeq: 0, publishedRunSeq: null, snapshot: null };
75
+ roots.set(rootId, created);
76
+ return created;
77
+ }
78
+
79
+ const api = {
80
+ /** 插件启动/恢复时为已存在的 root 建立空状态:不能继承同 id 旧 root 的终止快照。 */
81
+ activateRoot({ rootId, generation } = {}) {
82
+ if (disposed) return false;
83
+ const state = stateOf(rootId, generation, true);
84
+ if (state === void 0) return false;
85
+ state.snapshot = null;
86
+ state.publishedRunSeq = null;
87
+ return true;
88
+ },
89
+ /**
90
+ * 真实用户输入开启新 run:runSeq 单调递增并清空上一 run 的快照与发布资格。
91
+ * 这是"同一 root 再次提示"的唯一入口——宿主注入的 subagent feedback 不经过这里。
92
+ */
93
+ beginRootRun({ rootId, generation } = {}) {
94
+ if (disposed) return false;
95
+ const state = stateOf(rootId, generation, true);
96
+ if (state === void 0) return false;
97
+ state.runSeq += 1;
98
+ state.publishedRunSeq = null;
99
+ state.snapshot = null;
100
+ return true;
101
+ },
102
+ /**
103
+ * root turn 已终止:只有递归后代门控确认"没有任何专家在运行"时才写入快照。
104
+ * 门控未知(agents.list 缺失/抛错/身份或状态不完整)与仍有后代在运行时都拒绝发布,
105
+ * 并且不消耗本 run 的发布资格——专家真正结束的那一轮仍然可以通知。
106
+ * @returns {{published: boolean, reason?: string, snapshot?: object}}
107
+ */
108
+ recordTermination({ rootId, generation, reason, turn } = {}) {
109
+ if (disposed) return { published: false, reason: "disposed" };
110
+ const state = stateOf(rootId, generation, false);
111
+ if (state === void 0) return { published: false, reason: "identity" };
112
+ // 每个 root run 至多一次:同 run 的后续 turn(例如子代理结算回唤出的续接 turn)
113
+ // 一律拒绝,直到 beginRootRun 开启新 run。
114
+ if (state.publishedRunSeq === state.runSeq) return { published: false, reason: "already-published" };
115
+ const gate = snapshotRunningRootDescendants(listAgents, rootId);
116
+ if (gate.known !== true) return { published: false, reason: gate.reason ?? "runtime-child-query-unknown" };
117
+ if (gate.blocked === true) return { published: false, reason: gate.reason ?? "runtime-running-descendant" };
118
+ const snapshot = Object.freeze({
119
+ seq: ++publishSeq,
120
+ runSeq: state.runSeq,
121
+ generation: state.generation,
122
+ reason: normalizeRootTerminationReason(reason),
123
+ turn: Number.isSafeInteger(turn) ? turn : null,
124
+ at: timestamp(),
125
+ });
126
+ state.snapshot = snapshot;
127
+ state.publishedRunSeq = state.runSeq;
128
+ return { published: true, snapshot };
129
+ },
130
+ /**
131
+ * 同一 turn 在 turn/end 之后又开始新 step,说明 listener 通过 steer 续接了本次运行,
132
+ * 刚写入的"终止"并不成立:撤销快照并归还本 run 的发布资格,让真正的终止仍能通知。
133
+ * 只有 turn 完全一致的撤销才生效,后续新 turn 不会被误撤销。
134
+ *
135
+ * 代际校验(fail-closed):撤销面只拿得到 rootId,无法自己区分调用方属于哪一代,
136
+ * 而 replacement 之后同一 rootId 的下一代际**可以重用同一个 turn 号**——旧代际 root
137
+ * 的迟到 step/start 因此能撤销新代际刚发布的终止快照。调用方声明 generation 时
138
+ * 必须与当前状态代际严格一致(缺失、非法或不等都拒绝)。
139
+ *
140
+ * 兼容边界:未声明 generation 的历史调用方(index.js 的 session/event 路由)保留
141
+ * 原来的同 turn 语义——上游用 exact root Agent + Session 身份守卫拦住了跨代际事件;
142
+ * 新增调用方一律应带上 generation,才能获得完整的跨代际保护。
143
+ *
144
+ * @param {object} [input]
145
+ * @param {string} [input.rootId]
146
+ * @param {number} [input.turn] - 可省略;省略时不校验 turn 号(历史语义)。
147
+ * @param {number} [input.generation] - 调用方所属的 root 代际;声明即强校验。
148
+ * @returns {boolean} 真正撤销返回 true。
149
+ */
150
+ revokeTermination({ rootId, turn, generation } = {}) {
151
+ if (disposed) return false;
152
+ const state = roots.get(rootId);
153
+ if (state === void 0 || state.snapshot === null) return false;
154
+ if (generation !== undefined && generation !== state.generation) return false;
155
+ if (Number.isSafeInteger(turn) && state.snapshot.turn !== turn) return false;
156
+ state.snapshot = null;
157
+ state.publishedRunSeq = null;
158
+ return true;
159
+ },
160
+ /** 只读视图:RPC 与测试共用的权威读取面;未知 root 返回空视图而不是报错。 */
161
+ snapshotOf(rootId) {
162
+ if (disposed || typeof rootId !== "string" || rootId.length === 0) return EMPTY_VIEW;
163
+ const state = roots.get(rootId);
164
+ if (state === void 0) return Object.freeze({ rootId, generation: null, runSeq: 0, snapshot: null });
165
+ return Object.freeze({ rootId, generation: state.generation, runSeq: state.runSeq, snapshot: state.snapshot });
166
+ },
167
+ /** root replacement/dispose:删除该 root 的全部状态;旧代际的迟到退休无效。 */
168
+ retireRoot(rootId, generation) {
169
+ if (disposed) return false;
170
+ const state = roots.get(rootId);
171
+ if (state === void 0 || state.generation !== generation) return false;
172
+ roots.delete(rootId);
173
+ return true;
174
+ },
175
+ describe: () => Object.freeze({
176
+ roots: roots.size,
177
+ hasAgentQuery: typeof listAgents === "function",
178
+ reasons: [...ROOT_TERMINATION_REASONS],
179
+ disposed,
180
+ }),
181
+ /** 幂等清理:丢弃全部 root 状态;之后不接受新工作也不返回旧快照。 */
182
+ dispose() {
183
+ if (disposed) return;
184
+ disposed = true;
185
+ roots.clear();
186
+ },
187
+ };
188
+ return Object.freeze(api);
189
+ }