@klarkxy/dsh-fusion 0.1.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 ADDED
@@ -0,0 +1,1670 @@
1
+ import { FUSION_PLUGIN, FUSION_PURPOSE, FUSION_RPC_CHANNEL, FUSION_TOOLS, emptyFusionState, isWorking } from "./contracts.js";
2
+ import { registerHostRpc } from "@klarkxy/dsh-ai-services";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
5
+ import { z } from "zod";
6
+ import { foldSubagentDescriptor } from "@deepseek-ai/dsh-subagent";
7
+ import { defineTool } from "@deepseek-ai/dsh-tools";
8
+ //#region src/validation.ts
9
+ var FusionError = class extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.name = "FusionError";
14
+ this.code = code;
15
+ }
16
+ };
17
+ function requireFusion(condition, code, message) {
18
+ if (!condition) throw new FusionError(code, message);
19
+ }
20
+ function text(value, label, max, empty = false) {
21
+ requireFusion(typeof value === "string" && value.length <= max && (empty || value.trim().length > 0), "INVALID_INPUT", `${label} must be ${empty ? "0" : "1"}–${max} characters.`);
22
+ return value;
23
+ }
24
+ function integer(value, label) {
25
+ requireFusion(Number.isSafeInteger(value) && Number(value) >= 1, "INVALID_INPUT", `${label} must be a positive integer.`);
26
+ return value;
27
+ }
28
+ function object(value) {
29
+ requireFusion(value !== null && typeof value === "object" && !Array.isArray(value), "INVALID_INPUT", "Expected an object.");
30
+ return value;
31
+ }
32
+ function stringList(value, label, limit = 32) {
33
+ requireFusion(Array.isArray(value) && value.length <= limit, "INVALID_INPUT", `${label} must have at most ${limit} items.`);
34
+ return value.map((item) => text(item, label, 2e3));
35
+ }
36
+ function brief(value) {
37
+ const row = object(value);
38
+ return {
39
+ title: text(row.title, "title", 120),
40
+ goal: text(row.goal, "goal", 8e3),
41
+ context: text(row.context ?? "", "context", 48e3, true),
42
+ constraints: stringList(row.constraints ?? [], "constraints"),
43
+ acceptance: stringList(row.acceptance ?? [], "acceptance")
44
+ };
45
+ }
46
+ function route(value) {
47
+ const row = object(value);
48
+ return {
49
+ provider: text(row.provider, "provider", 300),
50
+ model: text(row.model, "model", 300),
51
+ ...row.reasoningEffort === void 0 ? {} : { reasoningEffort: text(row.reasoningEffort, "reasoningEffort", 100) }
52
+ };
53
+ }
54
+ function json(value, depth = 0) {
55
+ requireFusion(depth <= 12, "INVALID_INPUT", "Target is nested too deeply.");
56
+ if (value === null || typeof value === "boolean" || typeof value === "string") return;
57
+ if (typeof value === "number") {
58
+ requireFusion(Number.isFinite(value), "INVALID_INPUT", "Non-finite target value.");
59
+ return;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ requireFusion(value.length <= 128, "INVALID_INPUT", "Target array too large.");
63
+ value.forEach((item) => json(item, depth + 1));
64
+ return;
65
+ }
66
+ const row = object(value);
67
+ requireFusion(Object.keys(row).length <= 128, "INVALID_INPUT", "Target object too large.");
68
+ for (const [key, item] of Object.entries(row)) {
69
+ requireFusion(![
70
+ "__proto__",
71
+ "constructor",
72
+ "prototype"
73
+ ].includes(key), "INVALID_INPUT", "Unsafe target key.");
74
+ json(item, depth + 1);
75
+ }
76
+ }
77
+ function target(value) {
78
+ if (value === void 0) return void 0;
79
+ const row = object(value);
80
+ const domain = text(row.domain, "target domain", 100);
81
+ const data = object(row.data);
82
+ json(data);
83
+ requireFusion(JSON.stringify(data).length <= 2e5, "INVALID_INPUT", "Target exceeds storage limit.");
84
+ return {
85
+ domain,
86
+ data: structuredClone(data)
87
+ };
88
+ }
89
+ function timestamp(value) {
90
+ requireFusion(typeof value === "number" && Number.isSafeInteger(value) && value >= 0, "INVALID_STATE", "Invalid record timestamp.");
91
+ }
92
+ /** Validate persisted control records before allowing any native work to resume. */
93
+ function validateState(value) {
94
+ const row = object(value);
95
+ requireFusion(row.version === 1 && Number.isSafeInteger(row.revision) && Number(row.revision) >= 0, "INVALID_STATE", "Unsupported Fusion storage version.");
96
+ requireFusion(Array.isArray(row.pairs) && row.pairs.length <= 512, "INVALID_STATE", "Invalid Fusion pair table.");
97
+ const pairIds = /* @__PURE__ */ new Set(), leads = /* @__PURE__ */ new Set(), children = /* @__PURE__ */ new Set(), tasks = /* @__PURE__ */ new Set();
98
+ for (const value of row.pairs) {
99
+ const pair = object(value);
100
+ const pairId = text(pair.id, "pair id", 200);
101
+ requireFusion(!pairIds.has(pairId), "INVALID_STATE", "Duplicate pair identity.");
102
+ pairIds.add(pairId);
103
+ timestamp(pair.createdAt);
104
+ text(pair.project, "project identity", 8192);
105
+ const lead = text(pair.leadSessionId, "lead id", 200), child = text(pair.childSessionId, "child id", 200);
106
+ requireFusion(!leads.has(lead) && !children.has(child) && lead !== child, "INVALID_STATE", "Duplicate Fusion identity.");
107
+ leads.add(lead);
108
+ children.add(child);
109
+ requireFusion(pair.profile === "generic" || pair.profile === "writing", "INVALID_STATE", "Unknown Fusion profile.");
110
+ requireFusion(typeof pair.established === "boolean", "INVALID_STATE", "Missing admission state.");
111
+ route(pair.route);
112
+ requireFusion(Array.isArray(pair.tasks) && pair.tasks.length <= 128, "INVALID_STATE", "Invalid task table.");
113
+ for (const value of pair.tasks) {
114
+ const task = object(value), id = text(task.id, "task id", 200);
115
+ requireFusion(!tasks.has(id), "INVALID_STATE", "Duplicate task identity.");
116
+ tasks.add(id);
117
+ timestamp(task.createdAt);
118
+ timestamp(task.updatedAt);
119
+ if (task.error !== void 0) text(task.error, "task error", 16e3, true);
120
+ if (task.decision !== void 0) text(task.decision, "task decision", 16e3, true);
121
+ integer(task.revision, "task revision");
122
+ brief(task.brief);
123
+ target(task.target);
124
+ requireFusion([
125
+ "dispatching",
126
+ "working",
127
+ "decision",
128
+ "review",
129
+ "accepted",
130
+ "cancelled",
131
+ "failed",
132
+ "interrupted"
133
+ ].includes(String(task.state)), "INVALID_STATE", "Unknown task state.");
134
+ requireFusion([
135
+ "pending",
136
+ "accepted",
137
+ "uncertain"
138
+ ].includes(String(task.delivery)), "INVALID_STATE", "Unknown delivery state.");
139
+ text(task.dispatchId, "dispatch id", 200);
140
+ stringList(task.messageIds, "message ids", 64);
141
+ const reportIds = stringList(task.reportIds, "report ids", 64);
142
+ if (task.notifiedReportId !== void 0) requireFusion(reportIds.includes(text(task.notifiedReportId, "notified report id", 256)), "INVALID_STATE", "Notification references an unknown report.");
143
+ requireFusion(Array.isArray(task.candidates) && task.candidates.length <= 16 && Array.isArray(task.reviews) && task.reviews.length <= 32, "INVALID_STATE", "Invalid candidate history.");
144
+ const candidates = /* @__PURE__ */ new Map();
145
+ for (const [index, item] of task.candidates.entries()) {
146
+ const candidate = object(item);
147
+ timestamp(candidate.createdAt);
148
+ const candidateId = text(candidate.id, "candidate id", 200);
149
+ requireFusion(!candidates.has(candidateId), "INVALID_STATE", "Duplicate candidate identity.");
150
+ requireFusion(integer(candidate.revision, "candidate revision") === index + 1, "INVALID_STATE", "Invalid candidate order.");
151
+ requireFusion(integer(candidate.taskRevision, "candidate task revision") <= Number(task.revision), "INVALID_STATE", "Candidate belongs to a future task revision.");
152
+ const candidateText = text(candidate.text, "candidate", 2e5, true);
153
+ text(candidate.report, "report", 16e3, true);
154
+ requireFusion(typeof candidate.hash === "string" && /^[a-f0-9]{64}$/.test(candidate.hash), "INVALID_STATE", "Invalid candidate hash.");
155
+ requireFusion(createHash("sha256").update(candidateText, "utf8").digest("hex") === candidate.hash, "INVALID_STATE", "Candidate content does not match its stored hash.");
156
+ candidates.set(candidateId, candidate.hash);
157
+ }
158
+ if (task.cleanup !== void 0) requireFusion([
159
+ "pending",
160
+ "done",
161
+ "failed"
162
+ ].includes(String(task.cleanup)), "INVALID_STATE", "Invalid cleanup state.");
163
+ if (task.adoption !== void 0) requireFusion([
164
+ "pending",
165
+ "applied",
166
+ "dismissed",
167
+ "conflict"
168
+ ].includes(String(task.adoption)) && pair.profile === "writing" && task.target, "INVALID_STATE", "Invalid adoption state.");
169
+ if (task.application !== void 0) {
170
+ const application = object(task.application);
171
+ text(application.id, "application id", 200);
172
+ text(application.path, "application path", 8192);
173
+ text(application.beforeVersion, "application baseline", 8192, true);
174
+ const destination = target(task.target);
175
+ requireFusion(destination && (destination.data.path === void 0 || destination.data.path === application.path), "INVALID_STATE", "Application path differs from its captured target.");
176
+ const candidateId = text(application.candidateId, "application candidate", 200);
177
+ const latest = object(task.candidates.at(-1));
178
+ requireFusion(latest.id === candidateId && latest.taskRevision === task.revision && candidates.get(candidateId) === application.candidateHash, "INVALID_STATE", "Application references an unknown candidate.");
179
+ requireFusion(typeof application.afterHash === "string" && /^[a-f0-9]{64}$/.test(application.afterHash), "INVALID_STATE", "Invalid resulting file hash.");
180
+ requireFusion([
181
+ "pending",
182
+ "applied",
183
+ "conflict"
184
+ ].includes(String(application.state)) && pair.profile === "writing" && task.target && task.state === "accepted", "INVALID_STATE", "Invalid application state.");
185
+ if (application.version !== void 0) text(application.version, "application receipt version", 8192);
186
+ if (application.state === "pending") requireFusion(task.adoption === "pending", "INVALID_STATE", "Pending intent requires pending adoption.");
187
+ if (application.state === "conflict") requireFusion(task.adoption === "conflict" || task.adoption === "dismissed", "INVALID_STATE", "Conflicting intent requires conflict or dismissal.");
188
+ if (application.state === "applied") requireFusion(task.adoption === "applied" && application.version, "INVALID_STATE", "Applied intent requires a receipt.");
189
+ }
190
+ for (const item of task.reviews) {
191
+ const review = object(item);
192
+ timestamp(review.createdAt);
193
+ const candidateId = text(review.candidateId, "review candidate", 200);
194
+ const hash = text(review.candidateHash, "review hash", 64);
195
+ requireFusion(candidates.get(candidateId) === hash, "INVALID_STATE", "Review references an unknown candidate revision.");
196
+ requireFusion([
197
+ "accept",
198
+ "revise",
199
+ "reject"
200
+ ].includes(String(review.verdict)), "INVALID_STATE", "Invalid review verdict.");
201
+ text(review.feedback, "feedback", 16e3, true);
202
+ }
203
+ if (task.state === "accepted") {
204
+ const candidate = task.candidates.at(-1) && object(task.candidates.at(-1));
205
+ const review = task.reviews.at(-1) && object(task.reviews.at(-1));
206
+ requireFusion(candidate && candidate.taskRevision === task.revision && review?.verdict === "accept" && review.candidateId === candidate.id && review.candidateHash === candidate.hash, "INVALID_STATE", "Accepted task must reference the latest accepted candidate.");
207
+ }
208
+ }
209
+ }
210
+ requireFusion([...children].every((id) => !leads.has(id)), "INVALID_STATE", "Recursive Fusion identity.");
211
+ requireFusion(JSON.stringify(row).length <= 16e6, "CAPACITY", "Fusion history capacity reached. Existing records were preserved.");
212
+ return structuredClone(row);
213
+ }
214
+ //#endregion
215
+ //#region src/storage.ts
216
+ /** One atomic business record; Sessions remain the transcript authority. */
217
+ const fusionStateSchema = z.unknown().transform((value, ctx) => {
218
+ try {
219
+ return validateState(value);
220
+ } catch (error) {
221
+ ctx.addIssue({
222
+ code: "custom",
223
+ message: error instanceof Error ? error.message : "Invalid Fusion state"
224
+ });
225
+ return z.NEVER;
226
+ }
227
+ });
228
+ const fusionDomain = defineDomain({
229
+ name: "dsh_fusion",
230
+ version: 1,
231
+ tables: { state: domainTable(fusionStateSchema) }
232
+ });
233
+ const FUSION_STATE_KEY = "state";
234
+ function createFusionStore(table) {
235
+ return {
236
+ load: () => validateState(table.get("state") ?? emptyFusionState()),
237
+ save: (next) => table.put(FUSION_STATE_KEY, validateState(next))
238
+ };
239
+ }
240
+ //#endregion
241
+ //#region src/presentation.ts
242
+ /** Only a structured and matching source can suppress a cancelled child's late wakeup. */
243
+ function isOwnedChildNotice(source, childId) {
244
+ if (!source || typeof source !== "object") return false;
245
+ const row = source;
246
+ return (row.kind === "subagent-settled" || row.kind === "agent-message") && row.senderSessionId === childId;
247
+ }
248
+ //#endregion
249
+ //#region src/service.ts
250
+ const contentHash = (value) => createHash("sha256").update(value, "utf8").digest("hex");
251
+ const clone = (value) => structuredClone(value);
252
+ const currentTask = (pair) => pair.tasks.at(-1);
253
+ /** Business records only. The native continuation manager owns Agents and all message scheduling. */
254
+ var FusionService = class {
255
+ state;
256
+ pending = Promise.resolve();
257
+ enabled = true;
258
+ generation = 0;
259
+ storageFailed = false;
260
+ controller = new AbortController();
261
+ dispatches = /* @__PURE__ */ new Map();
262
+ applications = /* @__PURE__ */ new Map();
263
+ notifications = /* @__PURE__ */ new Map();
264
+ inFlight = /* @__PURE__ */ new Set();
265
+ ready;
266
+ store;
267
+ inspectAdmission;
268
+ native;
269
+ now;
270
+ id;
271
+ constructor(input) {
272
+ this.inspectAdmission = input.inspectAdmission;
273
+ this.store = input.store;
274
+ this.native = input.native;
275
+ this.now = input.now ?? Date.now;
276
+ this.id = input.id ?? randomUUID;
277
+ this.state = validateState(this.store.load() ?? emptyFusionState());
278
+ this.ready = this.change((next) => {
279
+ for (const pair of next.pairs) for (const task of pair.tasks) if (isWorking(task.state)) {
280
+ task.state = "interrupted";
281
+ task.delivery = "uncertain";
282
+ task.updatedAt = this.now();
283
+ task.error = "进程已重启。请核对执行记录后明确恢复;未自动重发任务。";
284
+ }
285
+ }, true);
286
+ }
287
+ get active() {
288
+ return this.enabled && !this.storageFailed;
289
+ }
290
+ async initialized() {
291
+ await this.ready;
292
+ }
293
+ snapshot() {
294
+ return clone(this.state);
295
+ }
296
+ pairFor(sessionId) {
297
+ const pair = this.state.pairs.find((pair) => pair.leadSessionId === sessionId || pair.childSessionId === sessionId);
298
+ return pair && clone(pair);
299
+ }
300
+ role(sessionId) {
301
+ const pair = this.state.pairs.find((pair) => pair.leadSessionId === sessionId || pair.childSessionId === sessionId);
302
+ return pair && (pair.leadSessionId === sessionId ? "lead" : "sidekick");
303
+ }
304
+ async persist(next) {
305
+ next.revision++;
306
+ validateState(next);
307
+ try {
308
+ await this.store.save(clone(next));
309
+ } catch (error) {
310
+ this.storageFailed = true;
311
+ this.generation++;
312
+ this.controller.abort();
313
+ throw error;
314
+ }
315
+ this.state = clone(next);
316
+ }
317
+ change(fn, allowInactive = false) {
318
+ const result = this.pending.then(async () => {
319
+ requireFusion(allowInactive || this.enabled, "DISABLED", "Fusion is disabled.");
320
+ requireFusion(allowInactive || !this.storageFailed, "STORAGE_FAILED", "Fusion storage failed. Reload after repairing storage; no new work was admitted.");
321
+ const next = clone(this.state), value = await fn(next);
322
+ await this.persist(next);
323
+ return clone(value);
324
+ });
325
+ this.pending = result.then(() => void 0, () => void 0);
326
+ return result;
327
+ }
328
+ owned(next, actor, side) {
329
+ const pair = next.pairs.find((pair) => (side === "lead" ? pair.leadSessionId : pair.childSessionId) === actor.sessionId);
330
+ requireFusion(pair && pair.project === actor.project && (side !== "sidekick" || actor.parentSessionId === pair.leadSessionId), "UNAUTHORIZED", "This session does not own the Fusion task.");
331
+ if (side === "lead") requireFusion(!actor.parentSessionId, "UNAUTHORIZED", "A child cannot become a Fusion Lead.");
332
+ return pair;
333
+ }
334
+ task(pair, taskId, revision) {
335
+ const task = currentTask(pair);
336
+ requireFusion(task?.id === taskId && task.revision === revision, "STALE", "This task revision is no longer current.");
337
+ return task;
338
+ }
339
+ isCurrent(pairId, taskId, revision, generation) {
340
+ if (!this.enabled || this.storageFailed || generation !== this.generation) return false;
341
+ const pair = this.state.pairs.find((pair) => pair.id === pairId), task = pair && currentTask(pair);
342
+ return task?.id === taskId && task.revision === revision && isWorking(task.state);
343
+ }
344
+ track(operation) {
345
+ this.inFlight.add(operation);
346
+ operation.finally(() => this.inFlight.delete(operation)).catch(() => {});
347
+ return operation;
348
+ }
349
+ abortNotifications(taskId) {
350
+ for (const controller of this.notifications.get(taskId) ?? []) controller.abort();
351
+ }
352
+ async delegate(actor, input) {
353
+ await this.ready;
354
+ input.signal.throwIfAborted();
355
+ requireFusion(!actor.parentSessionId && !this.roleIsChild(actor.sessionId), "UNAUTHORIZED", "Fusion only delegates from a user root session.");
356
+ text(actor.sessionId, "session id", 200);
357
+ text(actor.project, "project identity", 8192);
358
+ const brief$1 = brief(input.brief), route$1 = route(input.route), target$1 = target(input.target);
359
+ requireFusion(input.profile === "generic" || input.profile === "writing", "INVALID_INPUT", "Unknown Fusion profile.");
360
+ const reserved = await this.change(async (next) => {
361
+ input.signal.throwIfAborted();
362
+ let pair = next.pairs.find((pair) => pair.leadSessionId === actor.sessionId);
363
+ if (pair) {
364
+ requireFusion(pair.project === actor.project && pair.profile === input.profile, "CONTEXT_CHANGED", "Project or profile changed. Start a new root conversation.");
365
+ requireFusion(!currentTask(pair) || !isWorking(currentTask(pair).state), "BUSY", "The previous Fusion task needs review, cancellation or a decision first.");
366
+ requireFusion(!currentTask(pair) || !this.dispatches.has(currentTask(pair).id), "ADMISSION_PENDING", "The previous native admission is still settling.");
367
+ requireFusion(!["pending", "conflict"].includes(currentTask(pair)?.adoption ?? "") && currentTask(pair)?.application?.state !== "pending", "ADOPTION_PENDING", "Adopt or dismiss the previous candidate before delegating again.");
368
+ requireFusion(currentTask(pair)?.cleanup !== "pending" && currentTask(pair)?.cleanup !== "failed", "STOP_INCOMPLETE", "Previous child cleanup must finish before delegation.");
369
+ requireFusion(JSON.stringify(pair.route) === JSON.stringify(route$1), "ROUTE_CHANGED", "The persistent partner uses another route. Start a new conversation to change it.");
370
+ if (!pair.established && pair.tasks.length && this.inspectAdmission) {
371
+ await this.native.stop(clone(pair), false);
372
+ pair.established = await this.inspectAdmission(clone(pair), input.signal) === "present";
373
+ input.signal.throwIfAborted();
374
+ }
375
+ requireFusion(pair.established || pair.tasks.length === 0 || this.inspectAdmission, "UNCERTAIN_ADMISSION", "Initial child admission is uncertain. Start a new conversation after inspecting the old child.");
376
+ } else {
377
+ pair = {
378
+ id: this.id(),
379
+ leadSessionId: actor.sessionId,
380
+ childSessionId: this.id(),
381
+ project: actor.project,
382
+ profile: input.profile,
383
+ route: route$1,
384
+ established: false,
385
+ tasks: [],
386
+ createdAt: this.now()
387
+ };
388
+ next.pairs.push(pair);
389
+ }
390
+ const task = {
391
+ id: this.id(),
392
+ revision: 1,
393
+ state: "dispatching",
394
+ brief: brief$1,
395
+ ...target$1 ? { target: target$1 } : {},
396
+ candidates: [],
397
+ reviews: [],
398
+ messageIds: [],
399
+ dispatchId: this.id(),
400
+ reportIds: [],
401
+ delivery: "pending",
402
+ createdAt: this.now(),
403
+ updatedAt: this.now()
404
+ };
405
+ pair.tasks.push(task);
406
+ return {
407
+ pair,
408
+ task
409
+ };
410
+ });
411
+ return this.dispatch(reserved.pair, reserved.task, input.signal);
412
+ }
413
+ roleIsChild(id) {
414
+ return this.state.pairs.some((pair) => pair.childSessionId === id);
415
+ }
416
+ dispatch(pair, task, outerSignal) {
417
+ const controller = new AbortController(), generation = this.generation;
418
+ const controllers = this.dispatches.get(task.id) ?? /* @__PURE__ */ new Set();
419
+ controllers.add(controller);
420
+ this.dispatches.set(task.id, controllers);
421
+ const signal = AbortSignal.any([
422
+ outerSignal,
423
+ controller.signal,
424
+ this.controller.signal
425
+ ]);
426
+ return this.track((async () => {
427
+ try {
428
+ signal.throwIfAborted();
429
+ const message = await this.native.dispatch({
430
+ pair: clone(pair),
431
+ task: clone(task),
432
+ prompt: this.prompt(pair, task),
433
+ signal
434
+ });
435
+ const admitted = await this.change((next) => {
436
+ const current = next.pairs.find((row) => row.id === pair.id);
437
+ current.established = true;
438
+ const row = current.tasks.find((row) => row.id === task.id);
439
+ const messageId = text(message.messageId, "message id", 200);
440
+ if (!row.messageIds.includes(messageId)) row.messageIds.push(messageId);
441
+ if (row.revision === task.revision) {
442
+ row.delivery = "accepted";
443
+ if (this.enabled && !this.storageFailed && generation === this.generation && row.state === "dispatching") row.state = "working";
444
+ row.updatedAt = this.now();
445
+ }
446
+ return {
447
+ task: row,
448
+ valid: this.enabled && !this.storageFailed && generation === this.generation && ![
449
+ "cancelled",
450
+ "failed",
451
+ "interrupted"
452
+ ].includes(row.state)
453
+ };
454
+ }, true);
455
+ requireFusion(admitted.valid, "STALE", "Task was invalidated while its message was being admitted.");
456
+ return admitted.task;
457
+ } catch (error) {
458
+ await this.change((next) => {
459
+ const row = next.pairs.find((row) => row.id === pair.id)?.tasks.find((row) => row.id === task.id);
460
+ if (row && row.revision === task.revision && isWorking(row.state)) {
461
+ row.state = signal.aborted ? "cancelled" : "failed";
462
+ row.delivery = "uncertain";
463
+ row.updatedAt = this.now();
464
+ row.error = signal.aborted ? "任务已取消,未自动重试。" : "委派未确认完成,请查看执行记录后再处理。";
465
+ }
466
+ }, true);
467
+ const latest = this.pairFor(pair.leadSessionId)?.tasks.at(-1);
468
+ if (!this.enabled || latest?.id === task.id && latest.revision === task.revision && [
469
+ "cancelled",
470
+ "failed",
471
+ "interrupted"
472
+ ].includes(latest.state)) try {
473
+ await this.native.stop(pair, false);
474
+ } catch {}
475
+ throw error;
476
+ } finally {
477
+ controllers.delete(controller);
478
+ if (!controllers.size) this.dispatches.delete(task.id);
479
+ }
480
+ })());
481
+ }
482
+ prompt(pair, task) {
483
+ return `Fusion task ${task.id}; revision ${task.revision}; dispatch ${task.dispatchId}.\nGoal: ${task.brief.goal}\nContext:\n${task.brief.context}\nConstraints:\n${task.brief.constraints.join("\n")}\nAcceptance:\n${task.brief.acceptance.join("\n")}\n` + (task.target ? `Versioned destination (do not change): ${JSON.stringify(task.target)}\n` : "") + (task.decision ? `Lead feedback: ${task.decision}\n` : "") + `Use fusion_report with this taskId and taskRevision, and a unique reportId. Submit ${pair.profile === "writing" ? "the exact prose candidate" : "a concise result with verifiable evidence"}, or request a decision. Do not claim the task is complete without reporting. Stop after reporting and wait for the Lead.`;
484
+ }
485
+ async report(actor, input) {
486
+ await this.ready;
487
+ input.signal.throwIfAborted();
488
+ integer(input.taskRevision, "task revision");
489
+ text(input.reportId, "report id", 200);
490
+ text(input.text, "report text", input.kind === "candidate" ? 2e5 : 16e3, input.kind === "candidate");
491
+ text(input.report ?? "", "report summary", 16e3, true);
492
+ requireFusion(input.kind === "candidate" || input.kind === "decision", "INVALID_INPUT", "Unknown report kind.");
493
+ const generation = this.generation;
494
+ const result = await this.change((next) => {
495
+ input.signal.throwIfAborted();
496
+ const pair = this.owned(next, actor, "sidekick"), task = this.task(pair, input.taskId, input.taskRevision);
497
+ pair.established = true;
498
+ const reportKey = `${task.revision}:${input.reportId}`;
499
+ if (task.reportIds.includes(reportKey)) {
500
+ const candidate = task.candidates.at(-1);
501
+ requireFusion(input.kind === "decision" ? task.decision === input.text : candidate?.text === input.text && candidate.report === (input.report ?? ""), "DUPLICATE_CONFLICT", "A report id cannot be reused for different content.");
502
+ return {
503
+ pair,
504
+ task,
505
+ reportKey,
506
+ duplicate: true
507
+ };
508
+ }
509
+ requireFusion(task.state === "dispatching" || task.state === "working", "STALE", "The task is no longer accepting reports.");
510
+ task.reportIds.push(reportKey);
511
+ if (input.kind === "decision") {
512
+ task.state = "decision";
513
+ task.decision = input.text;
514
+ } else {
515
+ task.candidates.push({
516
+ id: this.id(),
517
+ taskRevision: task.revision,
518
+ revision: task.candidates.length + 1,
519
+ text: input.text,
520
+ hash: contentHash(input.text),
521
+ report: input.report ?? "",
522
+ createdAt: this.now()
523
+ });
524
+ task.state = "review";
525
+ }
526
+ task.updatedAt = this.now();
527
+ return {
528
+ pair,
529
+ task,
530
+ reportKey,
531
+ duplicate: false
532
+ };
533
+ });
534
+ if (result.duplicate) {
535
+ requireFusion(result.task.notifiedReportId === result.reportKey, "NOTIFICATION_UNCERTAIN", "The report was saved, but Lead notification is not confirmed. Inspect the exact task record before any explicit recovery.");
536
+ return result.task;
537
+ }
538
+ const controller = new AbortController();
539
+ const controllers = this.notifications.get(result.task.id) ?? /* @__PURE__ */ new Set();
540
+ controllers.add(controller);
541
+ this.notifications.set(result.task.id, controllers);
542
+ try {
543
+ if (this.isCurrent(result.pair.id, result.task.id, result.task.revision, generation)) {
544
+ const candidate = result.task.candidates.at(-1);
545
+ const body = result.task.state === "decision" ? `Decision requested for ${result.task.id} revision ${result.task.revision}: ${result.task.decision}` : `Candidate ready for ${result.task.id} revision ${result.task.revision}: ${candidate.id}, sha256 ${candidate.hash}. Read the exact candidate using fusion_read before fusion_review. Report: ${candidate.report}`;
546
+ const signal = AbortSignal.any([controller.signal, this.controller.signal]);
547
+ signal.throwIfAborted();
548
+ await this.track(this.native.notify({
549
+ pair: result.pair,
550
+ task: result.task,
551
+ actor,
552
+ text: body,
553
+ signal
554
+ }));
555
+ await this.change((next) => {
556
+ const row = next.pairs.find((pair) => pair.id === result.pair.id)?.tasks.find((task) => task.id === result.task.id);
557
+ if (row?.revision === result.task.revision && isWorking(row.state)) row.notifiedReportId = result.reportKey;
558
+ }, true);
559
+ }
560
+ } finally {
561
+ controllers.delete(controller);
562
+ if (controllers.size === 0) this.notifications.delete(result.task.id);
563
+ }
564
+ return clone(this.state.pairs.find((pair) => pair.id === result.pair.id)?.tasks.find((task) => task.id === result.task.id) ?? result.task);
565
+ }
566
+ read(actor, taskId, candidateId) {
567
+ const task = this.owned(this.state, actor, "lead").tasks.find((row) => row.id === taskId);
568
+ requireFusion(task, "NOT_FOUND", "Unknown Fusion task.");
569
+ const candidate = candidateId ? task.candidates.find((row) => row.id === candidateId) : task.candidates.at(-1);
570
+ requireFusion(!candidateId || candidate, "NOT_FOUND", "Unknown candidate.");
571
+ return clone({
572
+ task,
573
+ ...candidate ? { candidate } : {}
574
+ });
575
+ }
576
+ async review(actor, input) {
577
+ await this.ready;
578
+ input.signal.throwIfAborted();
579
+ text(input.feedback, "feedback", 16e3, true);
580
+ requireFusion([
581
+ "accept",
582
+ "revise",
583
+ "reject"
584
+ ].includes(input.verdict), "INVALID_INPUT", "Unknown review verdict.");
585
+ const result = await this.change((next) => {
586
+ input.signal.throwIfAborted();
587
+ const pair = this.owned(next, actor, "lead"), task = this.task(pair, input.taskId, integer(input.taskRevision, "task revision"));
588
+ const candidate = task.candidates.at(-1);
589
+ requireFusion(candidate?.id === input.candidateId && candidate.hash === input.hash && candidate.taskRevision === task.revision, "STALE", "Review must reference the current exact candidate.");
590
+ requireFusion(task.state === "review", "STALE", "The task is not awaiting review.");
591
+ requireFusion(contentHash(candidate.text) === candidate.hash, "INVALID_STATE", "Candidate content does not match its stored hash.");
592
+ task.reviews.push({
593
+ candidateId: candidate.id,
594
+ candidateHash: candidate.hash,
595
+ verdict: input.verdict,
596
+ feedback: input.feedback,
597
+ createdAt: this.now()
598
+ });
599
+ task.updatedAt = this.now();
600
+ if (input.verdict === "accept") {
601
+ task.state = "accepted";
602
+ if (pair.profile === "writing" && task.target) task.adoption = "pending";
603
+ } else if (input.verdict === "reject") task.state = "cancelled";
604
+ else {
605
+ requireFusion(task.candidates.length < 16, "RETRY_LIMIT", "Revision limit reached. Keep the candidate and ask the author for direction.");
606
+ requireFusion(input.feedback.trim(), "INVALID_INPUT", "Revision requires actionable feedback.");
607
+ task.revision++;
608
+ task.state = "dispatching";
609
+ task.dispatchId = this.id();
610
+ task.delivery = "pending";
611
+ task.decision = input.feedback;
612
+ delete task.notifiedReportId;
613
+ }
614
+ return {
615
+ pair,
616
+ task
617
+ };
618
+ });
619
+ this.abortNotifications(result.task.id);
620
+ if (input.verdict === "revise") return this.dispatch(result.pair, result.task, input.signal);
621
+ return result.task;
622
+ }
623
+ async decide(actor, input) {
624
+ await this.ready;
625
+ input.signal.throwIfAborted();
626
+ text(input.feedback, "decision", 16e3);
627
+ const result = await this.change(async (next) => {
628
+ input.signal.throwIfAborted();
629
+ const pair = this.owned(next, actor, "lead"), task = this.task(pair, input.taskId, integer(input.taskRevision, "task revision"));
630
+ requireFusion([
631
+ "decision",
632
+ "interrupted",
633
+ "failed"
634
+ ].includes(task.state), "STALE", "Only a blocked or explicitly interrupted task can resume.");
635
+ requireFusion(task.delivery !== "pending", "UNCERTAIN_ADMISSION", "Initial admission is still pending.");
636
+ if (this.inspectAdmission) {
637
+ await this.native.stop(clone(pair), false);
638
+ pair.established = await this.inspectAdmission(clone(pair), input.signal) === "present";
639
+ input.signal.throwIfAborted();
640
+ task.cleanup = "done";
641
+ } else requireFusion(pair.established, "UNCERTAIN_ADMISSION", "Inspect uncertain initial admission before resuming.");
642
+ requireFusion(task.revision < 32, "RETRY_LIMIT", "Task revision limit reached.");
643
+ task.revision++;
644
+ task.state = "dispatching";
645
+ task.delivery = "pending";
646
+ task.dispatchId = this.id();
647
+ task.decision = input.feedback;
648
+ delete task.error;
649
+ delete task.notifiedReportId;
650
+ task.updatedAt = this.now();
651
+ return {
652
+ pair,
653
+ task
654
+ };
655
+ });
656
+ this.abortNotifications(result.task.id);
657
+ return this.dispatch(result.pair, result.task, input.signal);
658
+ }
659
+ async cancel(actor, taskId, taskRevision, stopLead = false) {
660
+ await this.ready;
661
+ const present = this.task(this.owned(this.state, actor, "lead"), taskId, integer(taskRevision, "task revision"));
662
+ for (const controller of this.applications.get(present.id) ?? []) controller.abort();
663
+ const pair = await this.change((next) => {
664
+ const pair = this.owned(next, actor, "lead"), task = this.task(pair, taskId, integer(taskRevision, "task revision"));
665
+ requireFusion(task.application?.state !== "pending", "APPLICATION_UNCERTAIN", "Inspect the pending application before stopping this task.");
666
+ requireFusion(task.adoption !== "applied", "ALREADY_APPLIED", "Stopping does not undo an applied change.");
667
+ if (task.state === "accepted" && pair.profile === "writing" && task.target) task.adoption = "dismissed";
668
+ else task.state = "cancelled";
669
+ task.cleanup = "pending";
670
+ task.updatedAt = this.now();
671
+ return pair;
672
+ });
673
+ for (const controller of this.dispatches.get(taskId) ?? []) controller.abort();
674
+ this.abortNotifications(taskId);
675
+ try {
676
+ await this.track(this.native.stop(pair, stopLead));
677
+ await this.change((next) => {
678
+ this.task(this.owned(next, actor, "lead"), taskId, taskRevision).cleanup = "done";
679
+ }, true);
680
+ } catch (error) {
681
+ await this.change((next) => {
682
+ this.task(this.owned(next, actor, "lead"), taskId, taskRevision).cleanup = "failed";
683
+ }, true);
684
+ throw error;
685
+ }
686
+ }
687
+ async executionInterrupted(actor, reason, expected) {
688
+ await this.ready;
689
+ await this.change((next) => {
690
+ const pair = this.owned(next, actor, "sidekick"), task = currentTask(pair);
691
+ if (task && (!expected || task.id === expected.taskId && task.revision === expected.revision) && ["dispatching", "working"].includes(task.state)) {
692
+ task.state = "interrupted";
693
+ task.error = text(reason, "interruption reason", 16e3);
694
+ task.delivery = "uncertain";
695
+ task.updatedAt = this.now();
696
+ }
697
+ });
698
+ }
699
+ /** Explicit recovery only: saved content is re-notified, never dispatched to the Writer. */
700
+ async recover(actor, taskId, taskRevision, outerSignal) {
701
+ await this.ready;
702
+ outerSignal.throwIfAborted();
703
+ const result = await this.change((next) => {
704
+ const pair = this.owned(next, actor, "lead"), task = this.task(pair, taskId, integer(taskRevision, "task revision"));
705
+ requireFusion([
706
+ "interrupted",
707
+ "review",
708
+ "decision"
709
+ ].includes(task.state), "STALE", "This task has no recoverable report.");
710
+ const reportKey = task.reportIds.at(-1);
711
+ requireFusion(reportKey?.startsWith(`${task.revision}:`), "NO_REPORT", "There is no saved report for this revision. Resume with feedback instead.");
712
+ task.state = task.candidates.at(-1)?.taskRevision === task.revision ? "review" : "decision";
713
+ requireFusion(task.state === "review" || task.decision, "NO_REPORT", "No saved report is available.");
714
+ task.updatedAt = this.now();
715
+ return {
716
+ pair,
717
+ task,
718
+ reportKey
719
+ };
720
+ });
721
+ const controller = new AbortController(), controllers = this.notifications.get(taskId) ?? /* @__PURE__ */ new Set();
722
+ controllers.add(controller);
723
+ this.notifications.set(taskId, controllers);
724
+ const signal = AbortSignal.any([
725
+ outerSignal,
726
+ controller.signal,
727
+ this.controller.signal
728
+ ]);
729
+ try {
730
+ signal.throwIfAborted();
731
+ const candidate = result.task.candidates.at(-1);
732
+ const body = result.task.state === "review" ? `Recovered saved candidate for ${taskId} revision ${taskRevision}: ${candidate.id}, sha256 ${candidate.hash}. Read with fusion_read, then fusion_review. This is a repeated notification, not a new Writer result.` : `Recovered saved decision for ${taskId} revision ${taskRevision}: ${result.task.decision}`;
733
+ await this.track(this.native.notify({
734
+ pair: result.pair,
735
+ task: result.task,
736
+ actor,
737
+ text: body,
738
+ signal
739
+ }));
740
+ await this.change((next) => {
741
+ const task = this.task(this.owned(next, actor, "lead"), taskId, taskRevision);
742
+ requireFusion(["review", "decision"].includes(task.state), "STALE", "Recovery was cancelled.");
743
+ task.notifiedReportId = result.reportKey;
744
+ });
745
+ return this.read(actor, taskId).task;
746
+ } finally {
747
+ controllers.delete(controller);
748
+ if (!controllers.size) this.notifications.delete(taskId);
749
+ }
750
+ }
751
+ candidateAction(next, actor, input) {
752
+ requireFusion(input.sessionId === actor.sessionId, "UNAUTHORIZED", "Session identity mismatch.");
753
+ const pair = this.owned(next, actor, "lead"), task = this.task(pair, input.taskId, integer(input.taskRevision, "task revision"));
754
+ const candidate = task.candidates.at(-1);
755
+ requireFusion(candidate?.id === input.candidateId && candidate.hash === input.hash && candidate.taskRevision === task.revision, "STALE", "The exact current candidate is required.");
756
+ const review = task.reviews.at(-1);
757
+ requireFusion(review?.verdict === "accept" && review.candidateId === candidate.id && review.candidateHash === candidate.hash, "NOT_ACCEPTED", "The exact candidate has not been accepted by the Lead.");
758
+ requireFusion(pair.profile === "writing" && task.target && task.state === "accepted", "NOT_ACCEPTED", "The Lead must accept this writing candidate first.");
759
+ return {
760
+ pair,
761
+ task,
762
+ candidate,
763
+ target: task.target
764
+ };
765
+ }
766
+ /** Also used by status. An uncertain filesystem mutation is inspected, never replayed. */
767
+ async reconcile(actor, host, signal) {
768
+ await this.ready;
769
+ if (!this.pairFor(actor.sessionId)?.tasks.some((task) => task.application?.state === "pending")) return;
770
+ await this.change(async (next) => {
771
+ const pair = this.owned(next, actor, "lead");
772
+ for (const task of pair.tasks) {
773
+ const application = task.application;
774
+ if (application?.state !== "pending") continue;
775
+ const candidate = task.candidates.find((row) => row.id === application.candidateId);
776
+ requireFusion(task.target && candidate, "INVALID_STATE", "Incomplete application intent.");
777
+ await host.transact(actor, task.target, candidate, signal, async (access) => {
778
+ const current = await access.inspect();
779
+ if (current && contentHash(current.text) === application.afterHash) {
780
+ application.state = "applied";
781
+ application.version = text(current.version, "application receipt version", 8192);
782
+ task.adoption = "applied";
783
+ } else {
784
+ application.state = "conflict";
785
+ task.adoption = "conflict";
786
+ }
787
+ task.updatedAt = this.now();
788
+ });
789
+ }
790
+ });
791
+ }
792
+ async preview(actor, input, host, signal) {
793
+ await this.ready;
794
+ return this.change(async (next) => {
795
+ const { task, candidate, target } = this.candidateAction(next, actor, input);
796
+ requireFusion(!task.application || task.application.state === "conflict", "ALREADY_APPLIED", "This application has already started or completed.");
797
+ requireFusion(task.adoption !== "dismissed" && task.adoption !== "applied", "STALE", "This candidate is no longer awaiting adoption.");
798
+ return host.transact(actor, target, candidate, signal, async (access) => ({
799
+ ...await access.prepare(),
800
+ candidateId: candidate.id,
801
+ hash: candidate.hash
802
+ }));
803
+ });
804
+ }
805
+ async applyCandidate(actor, input, host, outerSignal) {
806
+ await this.ready;
807
+ text(input.expectedVersion, "expected version", 8192, true);
808
+ const controller = new AbortController(), controllers = this.applications.get(input.taskId) ?? /* @__PURE__ */ new Set();
809
+ controllers.add(controller);
810
+ this.applications.set(input.taskId, controllers);
811
+ try {
812
+ return await this.change(async (next) => {
813
+ const { task, candidate, target } = this.candidateAction(next, actor, input);
814
+ requireFusion(task.adoption !== "dismissed", "STALE", "This candidate was dismissed.");
815
+ requireFusion(!task.application || task.application.state === "conflict", "ALREADY_APPLIED", "This application has already started or completed.");
816
+ const signal = AbortSignal.any([
817
+ outerSignal,
818
+ controller.signal,
819
+ this.controller.signal
820
+ ]);
821
+ return host.transact(actor, target, candidate, signal, async (access) => {
822
+ const preview = await access.prepare();
823
+ requireFusion(preview.version === input.expectedVersion, "CONFLICT", "The preview version changed. Preview the current file again.");
824
+ signal.throwIfAborted();
825
+ task.application = {
826
+ id: this.id(),
827
+ candidateId: candidate.id,
828
+ candidateHash: candidate.hash,
829
+ path: preview.path,
830
+ beforeVersion: preview.version,
831
+ afterHash: contentHash(preview.after),
832
+ state: "pending"
833
+ };
834
+ task.adoption = "pending";
835
+ task.updatedAt = this.now();
836
+ requireFusion(JSON.stringify(next).length <= 15934464, "CAPACITY", "Fusion history has no room for the application receipt. The file was not changed.");
837
+ await this.persist(next);
838
+ signal.throwIfAborted();
839
+ try {
840
+ const receipt = await access.commit(preview.version);
841
+ requireFusion(receipt.path === preview.path, "INVALID_RECEIPT", "Host committed a different destination.");
842
+ task.application.state = "applied";
843
+ task.application.version = text(receipt.version, "application receipt version", 8192);
844
+ task.adoption = "applied";
845
+ task.updatedAt = this.now();
846
+ return task;
847
+ } catch (error) {
848
+ throw error;
849
+ }
850
+ });
851
+ });
852
+ } catch (error) {
853
+ if (this.active) await this.change((next) => {
854
+ const task = next.pairs.find((row) => row.leadSessionId === actor.sessionId && row.project === actor.project)?.tasks.at(-1), candidate = task?.candidates.at(-1);
855
+ if (task?.id === input.taskId && task.revision === input.taskRevision && candidate?.id === input.candidateId && candidate.hash === input.hash && task.state === "accepted" && task.adoption !== "applied" && task.adoption !== "dismissed") {
856
+ if (!task.application || task.application.state === "conflict") task.adoption = "conflict";
857
+ task.error = (error instanceof Error ? error.message : String(error)).slice(0, 16e3);
858
+ }
859
+ }).catch(() => {});
860
+ throw error;
861
+ } finally {
862
+ controllers.delete(controller);
863
+ if (!controllers.size) this.applications.delete(input.taskId);
864
+ }
865
+ }
866
+ async dismiss(actor, input) {
867
+ await this.ready;
868
+ return this.change((next) => {
869
+ const { task } = this.candidateAction(next, actor, input);
870
+ requireFusion(task.application?.state !== "pending" && task.adoption !== "applied", "ALREADY_APPLIED", "Inspect the application before dismissing it.");
871
+ task.adoption = "dismissed";
872
+ task.updatedAt = this.now();
873
+ return task;
874
+ });
875
+ }
876
+ /** Domain Host only: never expose an RPC that lets a browser claim a file was applied. */
877
+ async adoption(leadSessionId, taskId, candidateId, state) {
878
+ await this.ready;
879
+ await this.change((next) => {
880
+ const task = next.pairs.find((row) => row.leadSessionId === leadSessionId)?.tasks.find((row) => row.id === taskId);
881
+ requireFusion(task && task.state === "accepted" && task.candidates.at(-1)?.id === candidateId, "STALE", "No accepted candidate matches the Host receipt.");
882
+ requireFusion(task.adoption !== "applied" || state === "applied", "ALREADY_APPLIED", "An applied receipt cannot be overwritten.");
883
+ task.adoption = state;
884
+ task.updatedAt = this.now();
885
+ }, true);
886
+ }
887
+ /** Stops owned work only. Invalidate synchronously, then await admissions and cleanup. */
888
+ async dispose() {
889
+ if (!this.enabled) return;
890
+ this.enabled = false;
891
+ this.generation++;
892
+ this.controller.abort();
893
+ for (const controllers of this.dispatches.values()) for (const controller of controllers) controller.abort();
894
+ for (const taskId of this.notifications.keys()) this.abortNotifications(taskId);
895
+ const errors = [];
896
+ try {
897
+ await this.ready;
898
+ await this.change((next) => {
899
+ for (const pair of next.pairs) for (const task of pair.tasks) if (isWorking(task.state)) {
900
+ task.state = "cancelled";
901
+ task.cleanup = "pending";
902
+ task.updatedAt = this.now();
903
+ }
904
+ }, true);
905
+ } catch (error) {
906
+ errors.push(error);
907
+ }
908
+ await Promise.allSettled([...this.inFlight]);
909
+ const results = await Promise.allSettled(this.state.pairs.map((pair) => this.native.stop(clone(pair), false)));
910
+ try {
911
+ await this.change((next) => {
912
+ next.pairs.forEach((pair, index) => {
913
+ for (const task of pair.tasks) if (task.cleanup === "pending") task.cleanup = results[index].status === "fulfilled" ? "done" : "failed";
914
+ });
915
+ }, true);
916
+ } catch (error) {
917
+ errors.push(error);
918
+ }
919
+ for (const result of results) if (result.status === "rejected") errors.push(result.reason);
920
+ if (errors.length) throw new AggregateError(errors, "Fusion teardown did not complete cleanly.");
921
+ }
922
+ };
923
+ //#endregion
924
+ //#region src/native.ts
925
+ const sid = (id) => id;
926
+ /** Identity is derived from the exact live Agent, never a model-supplied role or label. */
927
+ function nativeActor(bindings, agent) {
928
+ requireFusion(agent && bindings.agents.get(agent.id) === agent, "UNAUTHORIZED", "A current live Agent is required.");
929
+ const header = agent.session.header;
930
+ requireFusion(typeof header.cwd === "string" && header.cwd.length > 0, "NO_WORKSPACE", "Fusion requires a session workspace.");
931
+ return {
932
+ sessionId: String(agent.id),
933
+ project: header.cwd,
934
+ ...header.parentSession ? { parentSessionId: String(header.parentSession) } : {}
935
+ };
936
+ }
937
+ function leadOf(bindings, pair) {
938
+ const lead = bindings.agents.get(sid(pair.leadSessionId));
939
+ const actor = nativeActor(bindings, lead);
940
+ requireFusion(actor.project === pair.project && !actor.parentSessionId, "UNAUTHORIZED", "The stored Fusion parent does not match this live workspace.");
941
+ return lead;
942
+ }
943
+ function ownedChild(bindings, pair) {
944
+ const child = bindings.agents.get(sid(pair.childSessionId));
945
+ if (!child) return void 0;
946
+ const actor = nativeActor(bindings, child);
947
+ requireFusion(actor.parentSessionId === pair.leadSessionId && actor.project === pair.project, "UNAUTHORIZED", "The live child does not belong to this Fusion pair.");
948
+ return child;
949
+ }
950
+ /** Adapter only: the pinned native continuation manager owns creation, inboxes and cold resume. */
951
+ function createNativeBridge(bindings, composition) {
952
+ return {
953
+ async dispatch({ pair, prompt, signal }) {
954
+ signal.throwIfAborted();
955
+ const lead = leadOf(bindings, pair);
956
+ if (pair.established) {
957
+ await composition.verifyContinuation?.(pair, signal);
958
+ signal.throwIfAborted();
959
+ const messageId = await bindings.subagents.sendMessage(lead, sid(pair.childSessionId), [{
960
+ type: "text",
961
+ text: prompt
962
+ }], { signal });
963
+ return { messageId: String(messageId) };
964
+ }
965
+ const provider = bindings.subagents.getProvider("spawn");
966
+ requireFusion(provider?.prepareContinuable && provider.capabilities.agentOptions && provider.capabilities.persona && provider.capabilities.toolFilter, "UNSUPPORTED_CAPABILITY", "Fusion requires the native spawn provider with continuable, model, persona and tool-filter support.");
967
+ requireFusion(!provider.inheritsParentContext, "CONTEXT_NOT_ISOLATED", "The Fusion provider must not copy the parent transcript.");
968
+ requireFusion(!bindings.agents.get(sid(pair.childSessionId)), "IDENTITY_CONFLICT", "The reserved child identity is already live.");
969
+ const tools = [...new Set(composition.tools(lead, pair))];
970
+ requireFusion(tools.includes("fusion_report"), "INVALID_COMPOSITION", "The child must have its report tool.");
971
+ requireFusion(!tools.includes("fusion_delegate") && !tools.includes("fusion_review"), "INVALID_COMPOSITION", "The child cannot own Lead controls.");
972
+ const toolFilter = pair.profile === "generic" && composition.scopedReport ? void 0 : { allow: composition.scopedReport ? tools.filter((name) => name !== "fusion_report") : tools };
973
+ const agentOptions = {
974
+ provider: pair.route.provider,
975
+ model: pair.route.model,
976
+ reasoningEffort: pair.route.reasoningEffort
977
+ };
978
+ const started = await bindings.subagents.startContinuable({
979
+ provider: "spawn",
980
+ label: pair.profile === "writing" ? "执笔" : "Fusion Sidekick",
981
+ childId: sid(pair.childSessionId),
982
+ request: {
983
+ parent: lead,
984
+ prompt: [{
985
+ type: "text",
986
+ text: prompt
987
+ }],
988
+ agentOptions,
989
+ persona: composition.persona(pair),
990
+ ...toolFilter ? { toolFilter } : {},
991
+ maxDepth: 1
992
+ },
993
+ signal
994
+ });
995
+ requireFusion(String(started.childId) === pair.childSessionId, "IDENTITY_CONFLICT", "The native provider returned another child identity.");
996
+ return { messageId: String(started.messageId) };
997
+ },
998
+ async notify({ pair, task, actor, text, signal }) {
999
+ signal.throwIfAborted();
1000
+ const lead = leadOf(bindings, pair);
1001
+ const content = [{
1002
+ type: "text",
1003
+ text: `${`[fusion:${pair.id}:${task.id}:${task.revision}]`}\n${text}`
1004
+ }];
1005
+ let messageId;
1006
+ if (actor.sessionId === pair.leadSessionId && !actor.parentSessionId && actor.project === pair.project) {
1007
+ messageId = randomUUID();
1008
+ lead.followup({
1009
+ id: messageId,
1010
+ role: "user",
1011
+ source: {
1012
+ kind: "plugin:@klarkxy/dsh-fusion",
1013
+ plugin: "@klarkxy/dsh-fusion"
1014
+ },
1015
+ content
1016
+ });
1017
+ } else {
1018
+ const child = ownedChild(bindings, pair);
1019
+ requireFusion(child && String(child.id) === actor.sessionId && actor.parentSessionId === pair.leadSessionId, "UNAUTHORIZED", "Only the current bound child can report to the Lead.");
1020
+ messageId = await bindings.subagents.sendMessage(child, sid(pair.leadSessionId), content, { signal });
1021
+ }
1022
+ if (signal.aborted) {
1023
+ lead.inbox.remove?.(messageId);
1024
+ signal.throwIfAborted();
1025
+ }
1026
+ },
1027
+ async stop(pair, stopLead) {
1028
+ const child = ownedChild(bindings, pair);
1029
+ const lead = bindings.agents.get(sid(pair.leadSessionId));
1030
+ if (lead) {
1031
+ leadOf(bindings, pair);
1032
+ const marker = `[fusion:${pair.id}:`;
1033
+ for (const message of [...lead.inbox.nextStep ?? [], ...lead.inbox.nextTurn ?? []]) if (isOwnedChildNotice(message.source, pair.childSessionId) || message.source.kind === "plugin:@klarkxy/dsh-fusion" && message.content.some((block) => block.type === "text" && block.text.includes(marker))) lead.inbox.remove(message.id);
1034
+ }
1035
+ child?.inbox.clear();
1036
+ if (stopLead && lead) lead.cancel({ kind: "user" }, { keepInbox: true });
1037
+ if (lead) await bindings.subagents.drainContinuableChildren(lead, [sid(pair.childSessionId)]);
1038
+ else if (child) {
1039
+ bindings.subagents.interrupt(sid(pair.childSessionId), {
1040
+ kind: "user",
1041
+ parentSessionId: sid(pair.leadSessionId)
1042
+ });
1043
+ await child.whenIdle();
1044
+ requireFusion(false, "PARENT_UNAVAILABLE", "Child execution stopped, but its parent must be restored before ownership cleanup can be confirmed.");
1045
+ }
1046
+ }
1047
+ };
1048
+ }
1049
+ //#endregion
1050
+ //#region src/tools.ts
1051
+ const taskFields = {
1052
+ taskId: {
1053
+ type: "string",
1054
+ required: true
1055
+ },
1056
+ taskRevision: {
1057
+ type: "integer",
1058
+ required: true
1059
+ }
1060
+ };
1061
+ const candidateFields = {
1062
+ ...taskFields,
1063
+ candidateId: {
1064
+ type: "string",
1065
+ required: true
1066
+ },
1067
+ hash: {
1068
+ type: "string",
1069
+ required: true
1070
+ }
1071
+ };
1072
+ const output = {
1073
+ schema: { type: "string" },
1074
+ render: (_args, value) => [{
1075
+ type: "text",
1076
+ text: String(value)
1077
+ }]
1078
+ };
1079
+ /** Exact execution Agent identity is bound at registration and checked again on every call. */
1080
+ function fusionTools(runtime, agent, sidekick) {
1081
+ const bound = (exec) => {
1082
+ requireFusion(exec.agent === agent, "UNAUTHORIZED", "Fusion tools cannot be borrowed by another Agent.");
1083
+ return runtime.actor(agent);
1084
+ };
1085
+ if (sidekick) return [defineTool({
1086
+ name: "fusion_report",
1087
+ description: "Report the exact candidate or a decision request for your current Fusion task. Never write the manuscript. Use a unique reportId; do not regenerate or resend after acknowledgement.",
1088
+ parameters: {
1089
+ ...taskFields,
1090
+ reportId: {
1091
+ type: "string",
1092
+ required: true
1093
+ },
1094
+ kind: {
1095
+ type: "string",
1096
+ enum: ["candidate", "decision"],
1097
+ required: true
1098
+ },
1099
+ text: {
1100
+ type: "string",
1101
+ required: true
1102
+ },
1103
+ report: { type: "string" }
1104
+ },
1105
+ output,
1106
+ async execute(args, exec) {
1107
+ return JSON.stringify(await runtime.service.report(bound(exec), {
1108
+ ...args,
1109
+ signal: exec.signal
1110
+ }));
1111
+ }
1112
+ })];
1113
+ return [
1114
+ defineTool({
1115
+ name: "fusion_delegate",
1116
+ description: "Delegate one bounded task to the same persistent Sidekick. In writing sessions the Writer authors exact prose; provide the original versioned target from read. Wait for its report, then read and review. Generic tasks retain ordinary execution tools; explicitly cancel before takeover.",
1117
+ parameters: {
1118
+ title: {
1119
+ type: "string",
1120
+ required: true
1121
+ },
1122
+ goal: {
1123
+ type: "string",
1124
+ required: true
1125
+ },
1126
+ context: { type: "string" },
1127
+ constraints: {
1128
+ type: "array",
1129
+ items: { type: "string" }
1130
+ },
1131
+ acceptance: {
1132
+ type: "array",
1133
+ items: { type: "string" }
1134
+ },
1135
+ target: {
1136
+ type: "object",
1137
+ additionalProperties: false,
1138
+ properties: {
1139
+ kind: {
1140
+ type: "string",
1141
+ enum: ["edit", "create"],
1142
+ required: true
1143
+ },
1144
+ path: {
1145
+ type: "string",
1146
+ required: true
1147
+ },
1148
+ oldText: { type: "string" },
1149
+ targetVersion: { type: "string" },
1150
+ basis: {
1151
+ type: "array",
1152
+ items: {
1153
+ type: "object",
1154
+ properties: {
1155
+ path: {
1156
+ type: "string",
1157
+ required: true
1158
+ },
1159
+ version: {
1160
+ type: "string",
1161
+ required: true
1162
+ },
1163
+ label: { type: "string" }
1164
+ },
1165
+ additionalProperties: false
1166
+ }
1167
+ }
1168
+ },
1169
+ description: "Writing target: edit requires original targetVersion and unique exact oldText; Writer text replaces that fragment. create requires a nonexistent .md/.txt path; Writer text becomes full content. basis lists original context read versions. No replacement text is accepted here."
1170
+ }
1171
+ },
1172
+ output,
1173
+ async execute(args, exec) {
1174
+ bound(exec);
1175
+ return JSON.stringify(await runtime.delegate(agent, args, exec.signal));
1176
+ }
1177
+ }),
1178
+ defineTool({
1179
+ name: "fusion_read",
1180
+ description: "Read the saved exact Sidekick candidate and its hash. Review this content without rewriting it.",
1181
+ parameters: {
1182
+ taskId: {
1183
+ type: "string",
1184
+ required: true
1185
+ },
1186
+ candidateId: { type: "string" }
1187
+ },
1188
+ output,
1189
+ async execute(args, exec) {
1190
+ return JSON.stringify(runtime.service.read(bound(exec), args.taskId, args.candidateId));
1191
+ }
1192
+ }),
1193
+ defineTool({
1194
+ name: "fusion_review",
1195
+ description: "Review the exact candidate id and hash. accept is model review only; the author still decides manuscript adoption. revise requires concrete feedback. reject ends the task.",
1196
+ parameters: {
1197
+ ...candidateFields,
1198
+ verdict: {
1199
+ type: "string",
1200
+ enum: [
1201
+ "accept",
1202
+ "revise",
1203
+ "reject"
1204
+ ],
1205
+ required: true
1206
+ },
1207
+ feedback: {
1208
+ type: "string",
1209
+ required: true
1210
+ }
1211
+ },
1212
+ output,
1213
+ async execute(args, exec) {
1214
+ return JSON.stringify(await runtime.service.review(bound(exec), {
1215
+ ...args,
1216
+ signal: exec.signal
1217
+ }));
1218
+ }
1219
+ }),
1220
+ defineTool({
1221
+ name: "fusion_decide",
1222
+ description: "Resolve a Sidekick decision or explicitly resume an interrupted task with feedback, reusing its persistent session.",
1223
+ parameters: {
1224
+ ...taskFields,
1225
+ feedback: {
1226
+ type: "string",
1227
+ required: true
1228
+ }
1229
+ },
1230
+ output,
1231
+ async execute(args, exec) {
1232
+ return JSON.stringify(await runtime.service.decide(bound(exec), {
1233
+ ...args,
1234
+ signal: exec.signal
1235
+ }));
1236
+ }
1237
+ }),
1238
+ defineTool({
1239
+ name: "fusion_cancel",
1240
+ description: "Cancel the owned Sidekick task before explicit takeover. This preserves unrelated child sessions and does not undo files already applied.",
1241
+ parameters: taskFields,
1242
+ output,
1243
+ async execute(args, exec) {
1244
+ const actor = bound(exec);
1245
+ await runtime.service.cancel(actor, args.taskId, args.taskRevision);
1246
+ return "Fusion task cancelled; takeover is now explicit.";
1247
+ }
1248
+ })
1249
+ ];
1250
+ }
1251
+ //#endregion
1252
+ //#region src/runtime.ts
1253
+ var FusionRuntime = class {
1254
+ service;
1255
+ ctx;
1256
+ ai;
1257
+ disposers = [];
1258
+ installed = /* @__PURE__ */ new Map();
1259
+ pendingNoticeClaims = /* @__PURE__ */ new Set();
1260
+ closing;
1261
+ constructor(ctx, store, ai) {
1262
+ this.ctx = ctx;
1263
+ this.ai = ai;
1264
+ const native = createNativeBridge({
1265
+ agents: ctx.agents,
1266
+ subagents: ctx.subagents
1267
+ }, {
1268
+ scopedReport: true,
1269
+ verifyContinuation: async (pair, signal) => {
1270
+ requireFusion(await this.inspectAdmission(pair, signal) === "present", "CHILD_MISSING", "The persistent Sidekick is missing. Explicitly resume to inspect recovery.");
1271
+ },
1272
+ tools: (parent, pair) => this.childTools(parent, pair),
1273
+ persona: (pair) => pair.profile === "writing" ? "You are the persistent Fusion Writer. Author exact prose candidates for the assigned brief and captured destination. You may inspect allowed context, but must never mutate manuscript files. Report via fusion_report; ask a decision when blocked. Do not delegate, contact the user directly, or treat rejected drafts as story facts. Stop after reporting." : "You are the persistent Fusion Sidekick. Execute only the assigned bounded task using native permissions. Report exact outcomes and verifiable evidence via fusion_report; report decisions when blocked. Never recursively enable Fusion or take over the Lead. Stop after reporting."
1274
+ });
1275
+ this.service = new FusionService({
1276
+ store,
1277
+ native,
1278
+ inspectAdmission: (pair, signal) => this.inspectAdmission(pair, signal)
1279
+ });
1280
+ }
1281
+ async inspectAdmission(pair, signal) {
1282
+ signal.throwIfAborted();
1283
+ const entry = (await this.ctx.subagents.listChildren(pair.leadSessionId, signal)).find((row) => String(row.id) === pair.childSessionId);
1284
+ let observation;
1285
+ try {
1286
+ observation = await this.ctx.sessionQuery.observeSession(pair.childSessionId, {
1287
+ signal,
1288
+ projectionMode: "all"
1289
+ });
1290
+ } catch (error) {
1291
+ if (error && typeof error === "object" && "code" in error && error.code === "SESSION_QUERY_SESSION_NOT_FOUND" && !entry && !this.ctx.agents.get(pair.childSessionId)) return "absent";
1292
+ throw error;
1293
+ }
1294
+ try {
1295
+ const header = observation.header, descriptor = foldSubagentDescriptor(observation.events);
1296
+ requireFusion(String(header.id) === pair.childSessionId && String(header.parentSession) === pair.leadSessionId && header.cwd === pair.project, "IDENTITY_CONFLICT", "The native Sidekick does not match this pair and workspace.");
1297
+ requireFusion(descriptor?.mode === "continuable" && descriptor.provider === "spawn" && (!entry || entry.mode === "continuable"), "UNCERTAIN_ADMISSION", "The saved child has no supported native continuation descriptor.");
1298
+ requireFusion(descriptor.agentProvider === pair.route.provider && descriptor.agentModel === pair.route.model && descriptor.agentReasoningEffort === pair.route.reasoningEffort, "ROUTE_CHANGED", "The native Sidekick route differs from its pinned Fusion route.");
1299
+ const selected = observation.projections?.values;
1300
+ if (selected?.modelSelection?.next) this.checkRoute(pair, selected.modelSelection.next);
1301
+ const live = this.ctx.agents.get(pair.childSessionId);
1302
+ if (live) this.checkRoute(pair, live.options);
1303
+ return "present";
1304
+ } finally {
1305
+ observation[Symbol.dispose]();
1306
+ }
1307
+ }
1308
+ checkRoute(pair, route) {
1309
+ requireFusion(route.provider === pair.route.provider && route.model === pair.route.model && route.reasoningEffort === pair.route.reasoningEffort, "ROUTE_CHANGED", "The Sidekick model changed outside Fusion. Restore its pinned route before continuing.");
1310
+ }
1311
+ actor(agent) {
1312
+ requireFusion(this.service.active, "DISABLED", "Fusion is disabled.");
1313
+ return nativeActor({
1314
+ agents: this.ctx.agents,
1315
+ subagents: this.ctx.subagents
1316
+ }, agent);
1317
+ }
1318
+ writing() {
1319
+ return this.ctx.get("fusionWriting");
1320
+ }
1321
+ profile(agent, pair) {
1322
+ const domain = this.writing();
1323
+ requireFusion(domain || !String(agent.session.header.agentPreset ?? "").startsWith("dsh-editor"), "DOMAIN_UNAVAILABLE", "The Editor writing domain is unavailable; delegation is blocked until it is restored.");
1324
+ if (pair?.profile === "writing") {
1325
+ requireFusion(domain && domain.matches(agent.session.header), "DOMAIN_UNAVAILABLE", "The writing domain is unavailable or this conversation changed writing mode.");
1326
+ return "writing";
1327
+ }
1328
+ return domain?.matches(agent.session.header) ? "writing" : "generic";
1329
+ }
1330
+ childTools(parent, pair) {
1331
+ if (pair.profile !== "writing") return ["fusion_report"];
1332
+ const available = parent.ctx.tools.schemas(parent).map((tool) => tool.name).filter((name) => name !== "run_code" && !FUSION_TOOLS.includes(name));
1333
+ this.profile(parent, pair);
1334
+ const allow = new Set(this.writing().writerTools);
1335
+ return [...available.filter((name) => allow.has(name)), "fusion_report"];
1336
+ }
1337
+ async start() {
1338
+ await this.service.initialized();
1339
+ const scope = this.ai.activate(FUSION_PLUGIN);
1340
+ this.disposers.push(() => scope.dispose());
1341
+ this.disposers.push(scope.registerPurpose({
1342
+ id: FUSION_PURPOSE,
1343
+ label: "Fusion 持久搭档",
1344
+ defaultTarget: {
1345
+ kind: "role",
1346
+ role: "normal"
1347
+ }
1348
+ }));
1349
+ this.disposers.push(this.ctx.on("agent/created", async ({ agent }) => {
1350
+ await this.install(agent);
1351
+ }, { global: true }));
1352
+ this.disposers.push(this.ctx.on("agent/disposed", ({ agent }) => {
1353
+ this.uninstall(agent);
1354
+ }, { global: true }));
1355
+ for (const agent of this.ctx.agents.list()) await this.install(agent);
1356
+ }
1357
+ async install(agent) {
1358
+ if (this.installed.has(agent) || !this.service.active) return;
1359
+ const header = agent.session.header, pair = this.service.pairFor(String(agent.id));
1360
+ const sidekick = pair?.childSessionId === String(agent.id);
1361
+ const root = !header.parentSession;
1362
+ const disposers = [];
1363
+ this.installed.set(agent, disposers);
1364
+ try {
1365
+ disposers.push(agent.ctx.tools.guard((exec) => {
1366
+ if (exec.agent !== agent) return "Fusion execution identity mismatch.";
1367
+ const livePair = this.service.pairFor(String(agent.id));
1368
+ if (!this.service.active) return sidekick || FUSION_TOOLS.includes(exec.name) ? "Fusion is disabled." : void 0;
1369
+ if (!root && !sidekick) return FUSION_TOOLS.includes(exec.name) ? "Only the owned Fusion Sidekick may report." : void 0;
1370
+ if (sidekick) {
1371
+ if (!livePair || livePair.childSessionId !== String(agent.id) || String(header.parentSession) !== livePair.leadSessionId || header.cwd !== livePair.project) return "Fusion child identity changed.";
1372
+ const task = livePair.tasks.at(-1);
1373
+ if (!task || !["working", "dispatching"].includes(task.state)) return "The Fusion task is no longer accepting execution.";
1374
+ if (exec.name === "fusion_report" || exec.name === "run_code") return void 0;
1375
+ if (FUSION_TOOLS.includes(exec.name)) return "Sidekick cannot control the Lead.";
1376
+ if (livePair.profile === "writing" && !this.writing()?.writerTools.includes(exec.name)) return "Writer tools are restricted to the writing domain read surface.";
1377
+ if (livePair.profile === "generic") {
1378
+ const lead = this.ctx.agents.get(livePair.leadSessionId);
1379
+ if (!lead || lead.session.header.parentSession || lead.session.header.cwd !== livePair.project) return "The Fusion Lead workspace is unavailable or changed.";
1380
+ if (!lead.ctx.tools.get(exec.name, lead)) return "This capability is not available to the Fusion Lead.";
1381
+ }
1382
+ return;
1383
+ }
1384
+ if (exec.name === "fusion_report") return "Only the owned Sidekick may report.";
1385
+ if (exec.name === "run_code") return void 0;
1386
+ try {
1387
+ if (this.profile(agent, livePair) === "writing" && !FUSION_TOOLS.includes(exec.name) && !this.writing().allowLeadTool(exec.name, exec.arguments)) return "Delegate prose to the Fusion Writer; the author adopts the exact candidate.";
1388
+ } catch {
1389
+ return "The Fusion writing domain is unavailable; execution is blocked until restored.";
1390
+ }
1391
+ }));
1392
+ if (root) {
1393
+ const claims = /* @__PURE__ */ new Map();
1394
+ disposers.push(agent.ctx.on("agent/inbox/claimed", ({ message, turn }) => {
1395
+ const pair = this.service.pairFor(String(agent.id));
1396
+ if (!pair || !isOwnedChildNotice(message.source, pair.childSessionId) || claims.has(turn)) return;
1397
+ let resolve;
1398
+ const pending = new Promise((done) => {
1399
+ resolve = done;
1400
+ });
1401
+ this.pendingNoticeClaims.add(pending);
1402
+ claims.set(turn, () => {
1403
+ claims.delete(turn);
1404
+ this.pendingNoticeClaims.delete(pending);
1405
+ resolve();
1406
+ });
1407
+ }));
1408
+ disposers.push(agent.ctx.on("session/event", (_session, event) => {
1409
+ if (event.type === "turn/end") claims.get(event.data.turn)?.();
1410
+ }));
1411
+ disposers.push(() => {
1412
+ for (const finish of [...claims.values()]) finish();
1413
+ });
1414
+ const discard = (message) => {
1415
+ const pair = this.service.pairFor(String(agent.id));
1416
+ if (!pair || !isOwnedChildNotice(message.source, pair.childSessionId)) return false;
1417
+ if (message.source.kind === "subagent-settled") return true;
1418
+ const task = pair.tasks.at(-1);
1419
+ return !this.service.active || !task || !["review", "decision"].includes(task.state) || !message.content.some((block) => block.type === "text" && block.text.includes(`[fusion:${pair.id}:${task.id}:${task.revision}]`));
1420
+ };
1421
+ disposers.push(agent.ctx.on("agent/pre-step", async (payload, next) => {
1422
+ const newTurn = payload.step === 1;
1423
+ const rejectOwnedWake = newTurn && payload.messages.length > 0 && payload.messages.every(discard);
1424
+ claims.get(payload.turn)?.();
1425
+ if (rejectOwnedWake) return { kind: "reject" };
1426
+ const decision = await next();
1427
+ if (decision.kind === "reject") return decision;
1428
+ const messages = decision.messages.filter((message) => !discard(message));
1429
+ return newTurn && decision.messages.length > 0 && messages.length === 0 ? { kind: "reject" } : {
1430
+ ...decision,
1431
+ messages
1432
+ };
1433
+ }, { prepend: true }));
1434
+ }
1435
+ if (!root && !sidekick) return;
1436
+ if (sidekick) {
1437
+ requireFusion(pair && header.cwd === pair.project && String(header.parentSession) === pair.leadSessionId, "UNAUTHORIZED", "Stored Writer identity does not match the native child.");
1438
+ if (pair.profile === "writing") {
1439
+ const registry = this.ctx.get("workspaceRegistry");
1440
+ const parent = this.ctx.sessions.get(pair.leadSessionId);
1441
+ requireFusion(registry && parent && parent.header.cwd === pair.project && !parent.header.parentSession, "WORKSPACE_UNAVAILABLE", "The Writer requires its real Lead workspace.");
1442
+ const workspace = await registry.resolveByPath(pair.project);
1443
+ requireFusion(workspace && workspace.sessionIds.some((id) => String(id) === pair.leadSessionId), "WORKSPACE_MISMATCH", "The Lead is not attached to this workspace.");
1444
+ await workspace.attachSession(agent.id);
1445
+ this.actor(agent);
1446
+ }
1447
+ const turns = /* @__PURE__ */ new Map();
1448
+ disposers.push(agent.ctx.on("session/event", (_session, event) => {
1449
+ if (event.type === "turn/start") {
1450
+ const task = this.service.pairFor(String(agent.id))?.tasks.at(-1);
1451
+ if (task) turns.set(event.data.turn, {
1452
+ taskId: task.id,
1453
+ revision: task.revision
1454
+ });
1455
+ }
1456
+ if (event.type === "turn/end") {
1457
+ const expected = turns.get(event.data.turn);
1458
+ turns.delete(event.data.turn);
1459
+ if (!expected || !this.service.active) return;
1460
+ const reason = event.data.reason.kind === "error" ? event.data.reason.error.message : `The Sidekick turn ended (${event.data.reason.kind}) without a saved report. Inspect its session before resuming.`;
1461
+ this.service.executionInterrupted(this.actor(agent), reason, expected).catch((error) => this.ctx.logger.warn("Fusion could not save child settlement", error));
1462
+ }
1463
+ }));
1464
+ this.checkRoute(pair, agent.options);
1465
+ disposers.push(agent.ctx.on("agent/request", async (_payload, next) => {
1466
+ const config = await next();
1467
+ try {
1468
+ this.checkRoute(pair, config);
1469
+ } catch (error) {
1470
+ await this.service.executionInterrupted(this.actor(agent), error instanceof Error ? error.message : String(error));
1471
+ throw error;
1472
+ }
1473
+ return config;
1474
+ }, { prepend: true }));
1475
+ if (pair.profile === "writing") requireFusion(this.writing(), "DOMAIN_UNAVAILABLE", "Writing domain required for the existing Writer.");
1476
+ }
1477
+ for (const tool of fusionTools(this, agent, sidekick)) disposers.push(agent.ctx.tools.register(tool));
1478
+ if (root) disposers.push(agent.ctx.systemPrompt.section({
1479
+ name: "fusion:lead",
1480
+ order: 95,
1481
+ text: "Fusion collaboration is enabled. You are the Lead in the original user conversation. Discuss, plan and review; use fusion_delegate for bounded Sidekick work. Only one task may be active. Read its exact saved candidate with fusion_read before fusion_review. Accept means model review, never author file adoption. In writing modes, delegate prose editing/creation with its original read versions; do not rewrite the Writer candidate while forwarding it. Resolve decisions with fusion_decide. For explicit takeover cancel the task first with fusion_cancel. Ordinary generic native tools remain available. Existing model route stays pinned to this pair. Never create another Fusion pair from child sessions."
1482
+ }));
1483
+ } catch (error) {
1484
+ this.uninstall(agent);
1485
+ throw error;
1486
+ }
1487
+ }
1488
+ uninstall(agent) {
1489
+ const disposers = this.installed.get(agent);
1490
+ this.installed.delete(agent);
1491
+ for (const dispose of disposers?.reverse() ?? []) dispose();
1492
+ }
1493
+ async delegate(agent, input, signal) {
1494
+ const actor = this.actor(agent), row = object(input), pair = this.service.pairFor(actor.sessionId);
1495
+ requireFusion(!actor.parentSessionId, "UNAUTHORIZED", "Only root sessions delegate.");
1496
+ const profile = this.profile(agent, pair);
1497
+ const route = pair?.route ?? await this.ai.resolve("fusion.sidekick", actor.sessionId);
1498
+ signal.throwIfAborted();
1499
+ this.actor(agent);
1500
+ const target = profile === "writing" ? await this.writing().capture(actor, row.target, signal) : void 0;
1501
+ signal.throwIfAborted();
1502
+ this.actor(agent);
1503
+ return this.service.delegate(actor, {
1504
+ profile,
1505
+ route: {
1506
+ provider: route.provider,
1507
+ model: route.model,
1508
+ ...route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}
1509
+ },
1510
+ brief: brief(row),
1511
+ ...target ? { target } : {},
1512
+ signal
1513
+ });
1514
+ }
1515
+ rpcActor(sessionId) {
1516
+ const session = this.ctx.sessions.get(sessionId);
1517
+ requireFusion(session && String(session.id) === sessionId, "SESSION_NOT_FOUND", "The native session is not loaded.");
1518
+ requireFusion(!session.header.parentSession && this.service.role(sessionId) !== "sidekick", "UNAUTHORIZED", "A child session cannot act as the Lead.");
1519
+ const project = text(session.header.cwd, "session workspace", 8192);
1520
+ const pair = this.service.pairFor(sessionId);
1521
+ requireFusion(!pair || pair.project === project, "CONTEXT_CHANGED", "The session workspace differs from the Fusion pair.");
1522
+ return {
1523
+ actor: {
1524
+ sessionId,
1525
+ project
1526
+ },
1527
+ session
1528
+ };
1529
+ }
1530
+ async rpc(endpoint, payload, signal) {
1531
+ await this.service.initialized();
1532
+ signal.throwIfAborted();
1533
+ requireFusion(this.service.active, "DISABLED", "Fusion is disabled.");
1534
+ const row = object(payload), sessionId = text(row.sessionId, "session id", 200);
1535
+ const { actor, session } = this.rpcActor(sessionId);
1536
+ const pair = this.service.pairFor(sessionId), profile = this.profile({ session }, pair);
1537
+ const domain = profile === "writing" ? this.writing() : void 0;
1538
+ if (domain) await this.service.reconcile(actor, domain, signal);
1539
+ signal.throwIfAborted();
1540
+ this.rpcActor(sessionId);
1541
+ if (endpoint === "status") {
1542
+ let error;
1543
+ if (!pair) try {
1544
+ await this.ai.resolve(FUSION_PURPOSE, sessionId);
1545
+ } catch (cause) {
1546
+ error = cause instanceof Error ? cause.message : String(cause);
1547
+ }
1548
+ const current = this.service.pairFor(sessionId);
1549
+ return {
1550
+ available: true,
1551
+ configured: !error,
1552
+ profile,
1553
+ revision: this.service.snapshot().revision,
1554
+ ...error ? { error } : {},
1555
+ ...current ? { pair: current } : {},
1556
+ usage: {
1557
+ leadTokens: null,
1558
+ sidekickTokens: null,
1559
+ cost: null
1560
+ },
1561
+ activity: {
1562
+ lead: this.ctx.agents.get(sessionId)?.status ?? "idle",
1563
+ sidekick: current ? this.ctx.agents.get(current.childSessionId)?.status ?? "idle" : "idle"
1564
+ }
1565
+ };
1566
+ }
1567
+ const taskId = text(row.taskId, "task id", 200), taskRevision = integer(row.taskRevision, "task revision");
1568
+ if (endpoint === "cancel") {
1569
+ await this.service.cancel(actor, taskId, taskRevision, true);
1570
+ return null;
1571
+ }
1572
+ if (endpoint === "resume") return this.service.decide(actor, {
1573
+ taskId,
1574
+ taskRevision,
1575
+ feedback: text(row.feedback, "feedback", 16e3),
1576
+ signal
1577
+ });
1578
+ if (endpoint === "recover") return this.service.recover(actor, taskId, taskRevision, signal);
1579
+ requireFusion(domain, "DOMAIN_UNAVAILABLE", "File adoption requires a writing domain.");
1580
+ const action = {
1581
+ sessionId,
1582
+ taskId,
1583
+ taskRevision,
1584
+ candidateId: text(row.candidateId, "candidate id", 200),
1585
+ hash: text(row.hash, "candidate hash", 64)
1586
+ };
1587
+ if (endpoint === "preview") return this.service.preview(actor, action, domain, signal);
1588
+ if (endpoint === "apply") return this.service.applyCandidate(actor, {
1589
+ ...action,
1590
+ expectedVersion: text(row.expectedVersion, "expected version", 8192, true)
1591
+ }, domain, signal);
1592
+ if (endpoint === "dismiss") return this.service.dismiss(actor, action);
1593
+ requireFusion(false, "NOT_FOUND", "Unknown Fusion endpoint.");
1594
+ }
1595
+ dispose() {
1596
+ if (this.closing) return this.closing;
1597
+ this.closing = (async () => {
1598
+ try {
1599
+ await this.service.dispose();
1600
+ } finally {
1601
+ for (const agent of this.installed.keys()) {
1602
+ const pair = this.service.pairFor(String(agent.id));
1603
+ if (!pair || pair.leadSessionId !== String(agent.id) || this.ctx.agents.get(agent.id) !== agent) continue;
1604
+ for (const message of [...agent.inbox.nextStep, ...agent.inbox.nextTurn]) if (isOwnedChildNotice(message.source, pair.childSessionId)) agent.inbox.remove(message.id);
1605
+ }
1606
+ while (this.pendingNoticeClaims.size) await Promise.all([...this.pendingNoticeClaims]);
1607
+ for (const agent of [...this.installed.keys()]) this.uninstall(agent);
1608
+ for (const dispose of this.disposers.reverse()) dispose();
1609
+ }
1610
+ })();
1611
+ return this.closing;
1612
+ }
1613
+ };
1614
+ //#endregion
1615
+ //#region src/index.ts
1616
+ const name = FUSION_PLUGIN;
1617
+ const inject = [
1618
+ "agents",
1619
+ "subagents",
1620
+ "tools",
1621
+ "systemPrompt",
1622
+ "sessions",
1623
+ "sessionQuery",
1624
+ "sessionProjections",
1625
+ "storageDomain",
1626
+ "aiServices",
1627
+ "connection",
1628
+ "webServer"
1629
+ ];
1630
+ async function apply(ctx) {
1631
+ const domain = await ctx.storageDomain.open(fusionDomain);
1632
+ let runtime;
1633
+ try {
1634
+ runtime = new FusionRuntime(ctx, createFusionStore(domain.table("state")), ctx.get("aiServices"));
1635
+ await runtime.start();
1636
+ } catch (error) {
1637
+ if (runtime) await runtime.dispose().catch(() => {});
1638
+ await domain.close();
1639
+ throw error;
1640
+ }
1641
+ const activeRuntime = runtime;
1642
+ ctx.effect(() => async () => {
1643
+ try {
1644
+ await activeRuntime.dispose();
1645
+ } finally {
1646
+ await domain.close();
1647
+ }
1648
+ }, "fusion.dispose");
1649
+ ctx.provide("fusion", activeRuntime);
1650
+ ctx.effect(() => registerHostRpc(ctx, FUSION_RPC_CHANNEL, async (endpoint, payload, signal) => {
1651
+ try {
1652
+ return {
1653
+ ok: true,
1654
+ value: await activeRuntime.rpc(endpoint, payload, signal)
1655
+ };
1656
+ } catch (error) {
1657
+ return {
1658
+ ok: false,
1659
+ error: {
1660
+ code: error instanceof FusionError ? error.code : "FAILED",
1661
+ message: error instanceof Error ? error.message : String(error)
1662
+ }
1663
+ };
1664
+ }
1665
+ }), "fusion.rpc");
1666
+ }
1667
+ //#endregion
1668
+ export { FusionRuntime, apply, inject, name };
1669
+
1670
+ //# sourceMappingURL=index.js.map