@khorsheed/dsh-room 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.
Files changed (95) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +48 -0
  4. package/README.zh.md +48 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/client.js +11481 -0
  7. package/lib/index.js +3231 -0
  8. package/lib/invariant.js +26 -0
  9. package/lib/journal-CuRnWk66.js +435 -0
  10. package/lib/tool.js +350 -0
  11. package/lib/typert.host.d.ts +3 -0
  12. package/lib/typert.host.js +2615 -0
  13. package/lib/typert.remote-client.d.ts +55 -0
  14. package/lib/typert.remote-client.js +1711 -0
  15. package/lib/types/adapter.d.ts +42 -0
  16. package/lib/types/adapter.js +41 -0
  17. package/lib/types/agent-setup.d.ts +141 -0
  18. package/lib/types/agent-setup.js +145 -0
  19. package/lib/types/client/InviteAgentAction.d.ts +13 -0
  20. package/lib/types/client/InviteAgentAction.js +45 -0
  21. package/lib/types/client/InviteDialog.d.ts +90 -0
  22. package/lib/types/client/InviteDialog.js +216 -0
  23. package/lib/types/client/MemberCard.d.ts +33 -0
  24. package/lib/types/client/MemberCard.js +32 -0
  25. package/lib/types/client/MembersView.d.ts +5 -0
  26. package/lib/types/client/MembersView.js +168 -0
  27. package/lib/types/client/RoomComposer.d.ts +21 -0
  28. package/lib/types/client/RoomComposer.js +211 -0
  29. package/lib/types/client/RoomDockCapsules.d.ts +64 -0
  30. package/lib/types/client/RoomDockCapsules.js +277 -0
  31. package/lib/types/client/RoomEventView.d.ts +11 -0
  32. package/lib/types/client/RoomEventView.js +17 -0
  33. package/lib/types/client/RoomModelPicker.d.ts +18 -0
  34. package/lib/types/client/RoomModelPicker.js +134 -0
  35. package/lib/types/client/RoomPlanView.d.ts +17 -0
  36. package/lib/types/client/RoomPlanView.js +88 -0
  37. package/lib/types/client/RoomQueueStrip.d.ts +26 -0
  38. package/lib/types/client/RoomQueueStrip.js +28 -0
  39. package/lib/types/client/RoomRecoveryView.d.ts +11 -0
  40. package/lib/types/client/RoomRecoveryView.js +38 -0
  41. package/lib/types/client/RoomRelayView.d.ts +12 -0
  42. package/lib/types/client/RoomRelayView.js +20 -0
  43. package/lib/types/client/RoomRunView.d.ts +15 -0
  44. package/lib/types/client/RoomRunView.js +50 -0
  45. package/lib/types/client/RoomSpeechView.d.ts +18 -0
  46. package/lib/types/client/RoomSpeechView.js +62 -0
  47. package/lib/types/client/RoomStatsLine.d.ts +61 -0
  48. package/lib/types/client/RoomStatsLine.js +138 -0
  49. package/lib/types/client/RoomTaskLineView.d.ts +13 -0
  50. package/lib/types/client/RoomTaskLineView.js +21 -0
  51. package/lib/types/client/RoomTodoStrip.d.ts +24 -0
  52. package/lib/types/client/RoomTodoStrip.js +64 -0
  53. package/lib/types/client/format.d.ts +31 -0
  54. package/lib/types/client/format.js +48 -0
  55. package/lib/types/client/icons.d.ts +27 -0
  56. package/lib/types/client/icons.js +11 -0
  57. package/lib/types/client/index.d.ts +39 -0
  58. package/lib/types/client/index.js +471 -0
  59. package/lib/types/client/locales.d.ts +422 -0
  60. package/lib/types/client/locales.js +413 -0
  61. package/lib/types/client/member-color.d.ts +13 -0
  62. package/lib/types/client/member-color.js +22 -0
  63. package/lib/types/client/name-pool.d.ts +17 -0
  64. package/lib/types/client/name-pool.js +25 -0
  65. package/lib/types/client/nodes.d.ts +136 -0
  66. package/lib/types/client/nodes.js +212 -0
  67. package/lib/types/client/preset-visibility.d.ts +91 -0
  68. package/lib/types/client/preset-visibility.js +113 -0
  69. package/lib/types/client/request-ids.d.ts +7 -0
  70. package/lib/types/client/request-ids.js +21 -0
  71. package/lib/types/client/room-store.d.ts +137 -0
  72. package/lib/types/client/room-store.js +244 -0
  73. package/lib/types/client/slots.d.ts +319 -0
  74. package/lib/types/client/slots.js +1 -0
  75. package/lib/types/deliveries.d.ts +5 -0
  76. package/lib/types/deliveries.js +20 -0
  77. package/lib/types/dispatch.d.ts +105 -0
  78. package/lib/types/dispatch.js +467 -0
  79. package/lib/types/index.d.ts +301 -0
  80. package/lib/types/index.js +1200 -0
  81. package/lib/types/invariant.d.ts +16 -0
  82. package/lib/types/invariant.js +24 -0
  83. package/lib/types/journal.d.ts +117 -0
  84. package/lib/types/journal.js +434 -0
  85. package/lib/types/plan-service.d.ts +34 -0
  86. package/lib/types/plan-service.js +266 -0
  87. package/lib/types/plan.d.ts +168 -0
  88. package/lib/types/plan.js +494 -0
  89. package/lib/types/tool.d.ts +56 -0
  90. package/lib/types/tool.js +389 -0
  91. package/lib/types/types.d.ts +813 -0
  92. package/lib/types/types.js +1 -0
  93. package/locale/en.json +6 -0
  94. package/locale/zh.json +6 -0
  95. package/package.json +131 -0
package/lib/index.js ADDED
@@ -0,0 +1,3231 @@
1
+ import { a as memberId, c as pendingInstructions, d as rosterStaleSince, f as replayDeliveries, i as isRoomLog, l as previousCursor, n as ROOM_EVENT_TYPES, o as parseMentions, r as coordinatorMember, s as parseRelayDirective, t as MAIN_AGENT_MEMBER, u as replay } from "./journal-CuRnWk66.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { SessionId } from "@deepseek-ai/dsh-session";
4
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
+ //#region lib/types/plan.js
7
+ const nonempty = (value, field) => {
8
+ if (typeof value !== "string" || value.trim() === "" || value.length > 32e3) throw new Error(`${field} must be non-empty and at most 32000 characters`);
9
+ };
10
+ const identifier = (value) => {
11
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}$/.test(value)) throw new Error("Invalid plan identity");
12
+ };
13
+ const references = (values, field, required = false) => {
14
+ if (!Array.isArray(values) || values.length > 100 || required && values.length === 0) throw new Error(`${field} requires ${required ? "1" : "0"}–100 entries`);
15
+ for (const value of values) nonempty(value, field);
16
+ };
17
+ function evidence(value) {
18
+ nonempty(value.summary, "Evidence summary");
19
+ references(value.references, "Evidence references", true);
20
+ references(value.artifacts, "Artifact locations");
21
+ }
22
+ function validateBudget(value) {
23
+ for (const [key, maximum] of [
24
+ ["maxParallel", 8],
25
+ ["maxAttempts", 100],
26
+ ["maxAttemptsPerTask", 10],
27
+ ["maxActiveMs", 864e5]
28
+ ]) if (!Number.isSafeInteger(value[key]) || value[key] < 1 || value[key] > maximum) throw new Error(`Invalid goal budget ${key}`);
29
+ }
30
+ const clone = (plan) => structuredClone(plan);
31
+ function stopClock(plan, now) {
32
+ if (plan.resumedAt !== void 0) plan.activeMs += Math.max(0, now - plan.resumedAt);
33
+ delete plan.resumedAt;
34
+ }
35
+ function activePlanMs(plan, now) {
36
+ return plan.activeMs + (plan.resumedAt === void 0 ? 0 : Math.max(0, now - plan.resumedAt));
37
+ }
38
+ function latestAttempt(task) {
39
+ return task.attempts.at(-1);
40
+ }
41
+ function acceptedTask(plan, taskId) {
42
+ const task = plan.tasks.find((task) => task.id === taskId);
43
+ if (task === void 0) return false;
44
+ if (task.kind === "task") return task.status === "accepted";
45
+ const children = plan.tasks.filter((child) => child.parentId === task.id);
46
+ return children.length > 0 && children.every((child) => acceptedTask(plan, child.id));
47
+ }
48
+ function dependenciesOf(plan, task) {
49
+ const dependencies = [...task.dependsOn];
50
+ const seen = /* @__PURE__ */ new Set([task.id]);
51
+ let parentId = task.parentId;
52
+ while (parentId !== void 0) {
53
+ if (seen.has(parentId)) throw new Error("Parent cycle");
54
+ seen.add(parentId);
55
+ const parent = plan.tasks.find((task) => task.id === parentId);
56
+ if (parent === void 0) throw new Error("Missing parent");
57
+ dependencies.push(...parent.dependsOn);
58
+ parentId = parent.parentId;
59
+ }
60
+ return dependencies;
61
+ }
62
+ /** Accepted leaves behind direct and inherited group prerequisites, once each. */
63
+ function dependencyResults(plan, task) {
64
+ const leaves = /* @__PURE__ */ new Map();
65
+ const visit = (id) => {
66
+ const dependency = plan.tasks.find((candidate) => candidate.id === id);
67
+ if (dependency === void 0 || !acceptedTask(plan, id)) return;
68
+ if (dependency.kind === "task") leaves.set(id, dependency);
69
+ else for (const child of plan.tasks.filter((candidate) => candidate.parentId === id)) visit(child.id);
70
+ };
71
+ for (const id of dependenciesOf(plan, task)) visit(id);
72
+ return [...leaves.values()].map((dependency) => {
73
+ const submission = dependency.attempts.at(-1)?.submission;
74
+ return {
75
+ taskId: dependency.id,
76
+ ...submission === void 0 ? {} : { submission }
77
+ };
78
+ });
79
+ }
80
+ function readyPlanTasks(plan) {
81
+ if (plan.status !== "running") return [];
82
+ return plan.tasks.filter((task) => task.kind === "task" && task.status === "pending" && dependenciesOf(plan, task).every((id) => acceptedTask(plan, id)));
83
+ }
84
+ function activePlanAttempts(plan) {
85
+ return plan.tasks.flatMap((task) => task.attempts.filter((attempt) => attempt.settledAt === void 0 && [
86
+ "queued",
87
+ "running",
88
+ "submitted",
89
+ "uncertain"
90
+ ].includes(attempt.status)));
91
+ }
92
+ function taskAndAttempt(plan, taskId, attemptId) {
93
+ const task = plan.tasks.find((task) => task.id === taskId);
94
+ const attempt = task?.attempts.at(-1);
95
+ if (task === void 0 || attempt?.id !== attemptId) throw new Error("Attempt is missing or superseded");
96
+ return [task, attempt];
97
+ }
98
+ function validateGraph(plan, memberIds) {
99
+ const stages = /* @__PURE__ */ new Set();
100
+ for (const stage of plan.stages) {
101
+ identifier(stage.id);
102
+ nonempty(stage.title, "Stage title");
103
+ if (stages.has(stage.id)) throw new Error("Duplicate stage");
104
+ stages.add(stage.id);
105
+ }
106
+ const tasks = /* @__PURE__ */ new Map();
107
+ if (plan.tasks.length > 200 || plan.stages.length > 50) throw new Error("Plan exceeds 200 tasks or 50 stages");
108
+ for (const task of plan.tasks) {
109
+ identifier(task.id);
110
+ nonempty(task.title, "Task title");
111
+ if (tasks.has(task.id) || !stages.has(task.stageId)) throw new Error("Duplicate task or missing stage");
112
+ if (task.kind !== "group" && task.kind !== "task") throw new Error("Invalid task kind");
113
+ if (task.kind === "task") {
114
+ if (task.ownerMemberId === void 0 || !memberIds.has(task.ownerMemberId)) throw new Error("Task owner is not in the room");
115
+ nonempty(task.instruction, "Execution instruction");
116
+ references(task.criteria, "Acceptance criteria", true);
117
+ }
118
+ references(task.inputRefs, "Input references");
119
+ references(task.artifactPaths, "Planned artifacts");
120
+ references(task.dependsOn, "Task dependencies");
121
+ tasks.set(task.id, task);
122
+ }
123
+ const edges = /* @__PURE__ */ new Map();
124
+ for (const task of plan.tasks) {
125
+ if (task.dependsOn.some((id) => !tasks.has(id))) throw new Error("Missing task dependency");
126
+ edges.set(task.id, dependenciesOf(plan, task));
127
+ if (task.parentId !== void 0) {
128
+ const parent = tasks.get(task.parentId);
129
+ if (parent?.kind !== "group" || parent.stageId !== task.stageId) throw new Error("Parent must be a group in the same stage");
130
+ }
131
+ }
132
+ for (const task of plan.tasks) if (task.parentId !== void 0) edges.get(task.parentId).push(task.id);
133
+ const visited = /* @__PURE__ */ new Set();
134
+ const visiting = /* @__PURE__ */ new Set();
135
+ const visit = (id) => {
136
+ if (visiting.has(id)) throw new Error("Task dependency or parent cycle");
137
+ if (visited.has(id)) return;
138
+ visiting.add(id);
139
+ for (const dependency of edges.get(id) ?? []) visit(dependency);
140
+ visiting.delete(id);
141
+ visited.add(id);
142
+ };
143
+ for (const id of tasks.keys()) visit(id);
144
+ }
145
+ /** Pure transition: the service persists the returned revision before dispatch. */
146
+ function changePlan(current, command, context) {
147
+ identifier(command.requestId);
148
+ const signature = planCommandSignature(command);
149
+ const receipt = current?.requests.find((receipt) => receipt.id === command.requestId);
150
+ if (receipt !== void 0) {
151
+ if (receipt.signature !== signature) throw new Error("Plan request identity was reused for different input");
152
+ return clone(current);
153
+ }
154
+ if (context.actor.kind === "worker" && command.action !== "submit") throw new Error("Only the coordinator or human may organize or review a plan");
155
+ if (command.action === "pause" && command.goalId !== void 0 && command.goalId !== current?.id) throw new Error("Goal changed; read the current plan before pausing");
156
+ const humanPause = context.actor.kind === "human" && command.action === "pause" && command.goalId === current?.id && current !== void 0;
157
+ if (command.expectedRevision !== (current?.revision ?? 0) && context.actor.kind !== "worker" && !humanPause) throw new Error("Plan revision changed; read the current plan before retrying");
158
+ let plan;
159
+ if (command.action === "create") {
160
+ if (current !== void 0 && !["completed", "cancelled"].includes(current.status)) throw new Error("An unfinished plan already exists");
161
+ if (current?.id === command.id) throw new Error("A new goal requires a new identity");
162
+ identifier(command.id);
163
+ nonempty(command.objective, "Goal");
164
+ validateBudget(command.budget);
165
+ plan = {
166
+ version: 1,
167
+ id: command.id,
168
+ revision: (current?.revision ?? 0) + 1,
169
+ objective: command.objective,
170
+ status: command.mode === "execute" ? "running" : "draft",
171
+ budget: structuredClone(command.budget),
172
+ activeMs: 0,
173
+ ...command.mode === "execute" ? { resumedAt: context.now } : {},
174
+ stages: [],
175
+ tasks: [],
176
+ requests: []
177
+ };
178
+ } else {
179
+ if (current === void 0) throw new Error("No formal goal exists");
180
+ plan = clone(current);
181
+ if (["completed", "cancelled"].includes(plan.status) && command.action !== "reconcile") throw new Error("The plan is closed");
182
+ switch (command.action) {
183
+ case "extend":
184
+ plan.stages.push(...structuredClone(command.stages));
185
+ for (const task of command.tasks) plan.tasks.push({
186
+ ...structuredClone(task),
187
+ status: "pending",
188
+ attempts: []
189
+ });
190
+ validateGraph(plan, context.memberIds);
191
+ for (const task of command.tasks) if (task.parentId !== void 0 && acceptedTask(current, task.parentId)) throw new Error("Cannot extend an accepted group; add a new group");
192
+ break;
193
+ case "pause":
194
+ nonempty(command.reason, "Pause reason");
195
+ stopClock(plan, context.now);
196
+ plan.status = "paused";
197
+ plan.reason = command.reason;
198
+ break;
199
+ case "budget":
200
+ if (context.actor.kind !== "human") throw new Error("Only a human may change the authorized budget");
201
+ validateBudget(command.budget);
202
+ plan.budget = structuredClone(command.budget);
203
+ break;
204
+ case "resume":
205
+ if (activePlanAttempts(plan).some((attempt) => attempt.status === "uncertain")) throw new Error("Reconcile uncertain attempts before resuming");
206
+ if (activePlanMs(plan, context.now) >= plan.budget.maxActiveMs) throw new Error("The active-time budget is exhausted");
207
+ if (plan.status !== "running") plan.resumedAt = context.now;
208
+ plan.status = "running";
209
+ delete plan.reason;
210
+ break;
211
+ case "cancel":
212
+ nonempty(command.reason, "Cancellation reason");
213
+ stopClock(plan, context.now);
214
+ plan.status = "cancelled";
215
+ plan.reason = command.reason;
216
+ for (const task of plan.tasks) if (task.status === "pending") {
217
+ task.status = "cancelled";
218
+ for (const attempt of task.attempts) if (attempt.status === "queued") {
219
+ attempt.status = "cancelled";
220
+ attempt.settledAt = context.now;
221
+ }
222
+ }
223
+ break;
224
+ case "submit": {
225
+ const [task, attempt] = taskAndAttempt(plan, command.taskId, command.attemptId);
226
+ if (context.actor.kind === "worker" && task.ownerMemberId !== context.actor.memberId) throw new Error("Workers may only submit their own current attempt");
227
+ if (attempt.status !== "running") throw new Error("Only the running attempt may submit evidence");
228
+ evidence(command.evidence);
229
+ attempt.submission = structuredClone(command.evidence);
230
+ attempt.status = "submitted";
231
+ task.status = "submitted";
232
+ break;
233
+ }
234
+ case "review": {
235
+ const [task, attempt] = taskAndAttempt(plan, command.taskId, command.attemptId);
236
+ if (attempt.status !== "submitted" || attempt.settledAt === void 0 || attempt.submission === void 0) throw new Error("Review requires a settled submission");
237
+ nonempty(command.reason, "Review decision");
238
+ references(command.references, "Review evidence", true);
239
+ attempt.review = {
240
+ decision: command.decision,
241
+ by: context.actor.memberId,
242
+ reason: command.reason,
243
+ references: [...command.references],
244
+ time: context.now
245
+ };
246
+ attempt.status = command.decision === "accepted" ? "accepted" : "rejected";
247
+ task.status = command.decision === "accepted" ? "accepted" : "pending";
248
+ break;
249
+ }
250
+ case "retry": {
251
+ const task = plan.tasks.find((task) => task.id === command.taskId);
252
+ if (task?.kind !== "task" || task.status !== "failed") throw new Error("Only a failed task can be retried");
253
+ nonempty(command.reason, "Retry reason");
254
+ task.status = "pending";
255
+ const attempt = latestAttempt(task);
256
+ if (attempt !== void 0) attempt.retry = {
257
+ by: context.actor.memberId,
258
+ reason: command.reason,
259
+ time: context.now
260
+ };
261
+ break;
262
+ }
263
+ case "reconcile": {
264
+ if (context.actor.kind !== "human") throw new Error("Only a human may reconcile an uncertain execution");
265
+ const [task, attempt] = taskAndAttempt(plan, command.taskId, command.attemptId);
266
+ if (attempt.status !== "uncertain") throw new Error("Attempt is not uncertain");
267
+ evidence(command.evidence);
268
+ attempt.submission = structuredClone(command.evidence);
269
+ attempt.status = command.outcome;
270
+ attempt.settledAt = context.now;
271
+ delete attempt.error;
272
+ task.status = command.outcome === "submitted" ? "submitted" : "failed";
273
+ if (plan.reason === "Reconcile uncertain attempts before continuing" && !activePlanAttempts(plan).some((attempt) => attempt.status === "uncertain")) plan.reason = "Interrupted attempts reconciled; review and resume explicitly";
274
+ break;
275
+ }
276
+ case "complete":
277
+ if (!plan.tasks.some((task) => task.kind === "task" && task.status === "accepted") || plan.tasks.some((task) => task.kind === "task" && !["accepted", "cancelled"].includes(task.status))) throw new Error("All remaining leaf tasks must be accepted before completing the goal");
278
+ evidence(command.evidence);
279
+ stopClock(plan, context.now);
280
+ plan.status = "completed";
281
+ plan.completion = structuredClone(command.evidence);
282
+ }
283
+ plan.revision++;
284
+ }
285
+ plan.requests.push({
286
+ id: command.requestId,
287
+ signature
288
+ });
289
+ return plan;
290
+ }
291
+ /** Reserve a single executable leaf and count the attempt before side effects. */
292
+ function queuePlanAttempt(current, taskId, attemptId, deliveryId, now) {
293
+ const plan = clone(current);
294
+ if (!readyPlanTasks(plan).some((task) => task.id === taskId)) throw new Error("Task is not ready for execution");
295
+ if (activePlanAttempts(plan).length >= plan.budget.maxParallel) throw new Error("Goal concurrency is full");
296
+ const task = plan.tasks.find((task) => task.id === taskId);
297
+ if (activePlanMs(plan, now) >= plan.budget.maxActiveMs || task.attempts.length >= plan.budget.maxAttemptsPerTask || plan.tasks.reduce((count, task) => count + task.attempts.length, 0) >= plan.budget.maxAttempts) {
298
+ stopClock(plan, now);
299
+ plan.status = "paused";
300
+ plan.reason = "Execution budget exhausted";
301
+ plan.revision++;
302
+ return plan;
303
+ }
304
+ identifier(attemptId);
305
+ nonempty(deliveryId, "Delivery identity");
306
+ if (plan.tasks.some((task) => task.attempts.some((attempt) => attempt.id === attemptId || attempt.deliveryId === deliveryId))) throw new Error("Attempt or delivery identity already exists");
307
+ task.attempts.push({
308
+ id: attemptId,
309
+ deliveryId,
310
+ number: task.attempts.length + 1,
311
+ status: "queued",
312
+ createdAt: now
313
+ });
314
+ task.status = "running";
315
+ plan.revision++;
316
+ return plan;
317
+ }
318
+ /** Durable admission; a pause after reservation prevents the native side effect. */
319
+ function admitPlanAttempt(current, taskId, attemptId, now) {
320
+ const plan = clone(current);
321
+ if (plan.status !== "running" || activePlanMs(plan, now) >= plan.budget.maxActiveMs) throw new Error("Goal is paused or its active-time budget is exhausted");
322
+ const [, attempt] = taskAndAttempt(plan, taskId, attemptId);
323
+ if (attempt.status !== "queued") throw new Error("Attempt is not queued");
324
+ attempt.status = "running";
325
+ attempt.startedAt = now;
326
+ plan.revision++;
327
+ return plan;
328
+ }
329
+ /** A normal native result is a submission to review, never implicit acceptance. */
330
+ function settlePlanAttempt(current, taskId, attemptId, result, now) {
331
+ const plan = clone(current);
332
+ const task = plan.tasks.find((task) => task.id === taskId);
333
+ const attempt = task?.attempts.at(-1);
334
+ if (task === void 0 || attempt?.id !== attemptId || attempt.settledAt !== void 0 || attempt.status === "uncertain") return plan;
335
+ if (attempt.startedAt === void 0 && current.status === "paused") {
336
+ attempt.status = "cancelled";
337
+ attempt.error = "Paused before native admission";
338
+ attempt.settledAt = now;
339
+ task.status = "pending";
340
+ plan.revision++;
341
+ return plan;
342
+ }
343
+ if (result.state === "done") {
344
+ if (result.evidence !== void 0 && attempt.submission === void 0) {
345
+ evidence(result.evidence);
346
+ attempt.submission = structuredClone(result.evidence);
347
+ }
348
+ if (attempt.submission === void 0) {
349
+ attempt.status = "failed";
350
+ task.status = "failed";
351
+ attempt.error = "Execution finished without submission evidence";
352
+ } else {
353
+ attempt.status = "submitted";
354
+ task.status = "submitted";
355
+ }
356
+ } else {
357
+ attempt.status = result.state;
358
+ task.status = result.state === "cancelled" ? "cancelled" : "failed";
359
+ if (result.error !== void 0) attempt.error = result.error;
360
+ }
361
+ attempt.settledAt = now;
362
+ plan.revision++;
363
+ return plan;
364
+ }
365
+ /** Restart always pauses automation and fences native executions with unknown outcomes. */
366
+ function recoverPlan(current, now) {
367
+ const plan = clone(current);
368
+ if (plan.status === "completed") return plan;
369
+ if (plan.status === "running") {
370
+ stopClock(plan, now);
371
+ plan.status = "paused";
372
+ plan.reason = "Host restarted; review and resume explicitly";
373
+ }
374
+ for (const task of plan.tasks) {
375
+ const attempt = latestAttempt(task);
376
+ if (attempt !== void 0 && attempt.settledAt === void 0 && ["running", "submitted"].includes(attempt.status)) {
377
+ attempt.status = "uncertain";
378
+ attempt.error = "Execution outcome is unknown after restart";
379
+ task.status = "failed";
380
+ if (plan.status !== "cancelled") plan.status = "paused";
381
+ plan.reason = "Reconcile uncertain attempts before continuing";
382
+ }
383
+ }
384
+ plan.revision++;
385
+ return plan;
386
+ }
387
+ const shortText = {
388
+ type: "string",
389
+ minLength: 1,
390
+ maxLength: 32e3
391
+ };
392
+ const idSchema = {
393
+ type: "string",
394
+ pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}$"
395
+ };
396
+ const strings = {
397
+ type: "array",
398
+ items: shortText,
399
+ maxItems: 100
400
+ };
401
+ const object = (properties, optional = []) => ({
402
+ type: "object",
403
+ properties,
404
+ required: Object.keys(properties).filter((key) => !optional.includes(key)),
405
+ additionalProperties: false
406
+ });
407
+ const enumeration = (...values) => ({
408
+ type: "string",
409
+ enum: values
410
+ });
411
+ const integer = (minimum, maximum) => ({
412
+ type: "integer",
413
+ minimum,
414
+ maximum
415
+ });
416
+ const evidenceSchema = object({
417
+ summary: shortText,
418
+ references: {
419
+ ...strings,
420
+ minItems: 1
421
+ },
422
+ artifacts: strings
423
+ });
424
+ const budgetSchema = object({
425
+ maxParallel: integer(1, 8),
426
+ maxAttempts: integer(1, 100),
427
+ maxAttemptsPerTask: integer(1, 10),
428
+ maxActiveMs: integer(1, 864e5)
429
+ });
430
+ const taskSchema = object({
431
+ id: idSchema,
432
+ title: shortText,
433
+ stageId: idSchema,
434
+ parentId: idSchema,
435
+ kind: enumeration("task", "group"),
436
+ ownerMemberId: idSchema,
437
+ instruction: {
438
+ type: "string",
439
+ maxLength: 32e3
440
+ },
441
+ criteria: strings,
442
+ inputRefs: strings,
443
+ artifactPaths: strings,
444
+ dependsOn: {
445
+ type: "array",
446
+ items: idSchema,
447
+ maxItems: 100
448
+ }
449
+ }, ["parentId", "ownerMemberId"]);
450
+ const base = {
451
+ requestId: idSchema,
452
+ expectedRevision: integer(0, Number.MAX_SAFE_INTEGER)
453
+ };
454
+ const commandSchema = { oneOf: [
455
+ object({
456
+ ...base,
457
+ action: enumeration("create"),
458
+ id: idSchema,
459
+ objective: shortText,
460
+ mode: enumeration("draft", "execute"),
461
+ budget: budgetSchema
462
+ }),
463
+ object({
464
+ ...base,
465
+ action: enumeration("extend"),
466
+ stages: {
467
+ type: "array",
468
+ items: object({
469
+ id: idSchema,
470
+ title: shortText
471
+ }),
472
+ maxItems: 50
473
+ },
474
+ tasks: {
475
+ type: "array",
476
+ items: taskSchema,
477
+ maxItems: 200
478
+ }
479
+ }),
480
+ object({
481
+ ...base,
482
+ action: enumeration("pause"),
483
+ reason: shortText,
484
+ goalId: idSchema
485
+ }, ["goalId"]),
486
+ object({
487
+ ...base,
488
+ action: enumeration("resume")
489
+ }),
490
+ object({
491
+ ...base,
492
+ action: enumeration("budget"),
493
+ budget: budgetSchema
494
+ }),
495
+ object({
496
+ ...base,
497
+ action: enumeration("cancel"),
498
+ reason: shortText
499
+ }),
500
+ object({
501
+ ...base,
502
+ action: enumeration("submit"),
503
+ taskId: idSchema,
504
+ attemptId: idSchema,
505
+ evidence: evidenceSchema
506
+ }),
507
+ object({
508
+ ...base,
509
+ action: enumeration("review"),
510
+ taskId: idSchema,
511
+ attemptId: idSchema,
512
+ decision: enumeration("accepted", "rework"),
513
+ reason: shortText,
514
+ references: {
515
+ ...strings,
516
+ minItems: 1
517
+ }
518
+ }),
519
+ object({
520
+ ...base,
521
+ action: enumeration("retry"),
522
+ taskId: idSchema,
523
+ reason: shortText
524
+ }),
525
+ object({
526
+ ...base,
527
+ action: enumeration("reconcile"),
528
+ taskId: idSchema,
529
+ attemptId: idSchema,
530
+ outcome: enumeration("failed", "submitted"),
531
+ evidence: evidenceSchema
532
+ }),
533
+ object({
534
+ ...base,
535
+ action: enumeration("complete"),
536
+ evidence: evidenceSchema
537
+ })
538
+ ] };
539
+ /** Validate only the small JSON-schema vocabulary used by the published contract. */
540
+ function validateWire(value, schema, path) {
541
+ const fail = () => {
542
+ throw new Error(`Invalid plan command at ${path}`);
543
+ };
544
+ if (schema.oneOf !== void 0) {
545
+ for (const alternative of schema.oneOf) try {
546
+ validateWire(value, alternative, path);
547
+ return;
548
+ } catch {}
549
+ fail();
550
+ }
551
+ if (schema.type === "string") {
552
+ if (typeof value !== "string") fail();
553
+ const text = value;
554
+ if (schema.minLength !== void 0 && text.length < schema.minLength || schema.maxLength !== void 0 && text.length > schema.maxLength || schema.enum !== void 0 && !schema.enum.includes(text) || schema.pattern !== void 0 && !new RegExp(schema.pattern).test(text)) fail();
555
+ } else if (schema.type === "integer") {
556
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < schema.minimum || value > schema.maximum) fail();
557
+ } else if (schema.type === "array") {
558
+ if (!Array.isArray(value)) fail();
559
+ const items = value;
560
+ if (items.length < (schema.minItems ?? 0) || items.length > (schema.maxItems ?? Infinity)) fail();
561
+ for (const [index, item] of items.entries()) validateWire(item, schema.items, `${path}[${index}]`);
562
+ } else if (schema.type === "object") {
563
+ if (value === null || typeof value !== "object" || Array.isArray(value)) fail();
564
+ const record = value;
565
+ if (schema.required?.some((key) => !Object.hasOwn(record, key)) || Object.keys(record).some((key) => !Object.hasOwn(schema.properties, key))) fail();
566
+ for (const [key, item] of Object.entries(record)) validateWire(item, schema.properties[key], `${path}.${key}`);
567
+ }
568
+ }
569
+ /** One bounded, strict wire grammar is shared by native tools, MCP and the UI. */
570
+ function parsePlanCommand(text) {
571
+ if (typeof text !== "string" || text.length > 131072) throw new Error("Plan command exceeds 128 KiB");
572
+ const value = JSON.parse(text);
573
+ validateWire(value, commandSchema, "command");
574
+ return value;
575
+ }
576
+ /** JSON schema supplied through room_read so both harness families see one contract. */
577
+ function planCommandContract() {
578
+ return structuredClone(commandSchema);
579
+ }
580
+ /** Object field order is not part of a retry identity; array order remains meaningful. */
581
+ function planCommandSignature(command) {
582
+ const stable = (value) => Array.isArray(value) ? value.map(stable) : value !== null && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, stable(item)])) : value;
583
+ return JSON.stringify(stable(command));
584
+ }
585
+ //#endregion
586
+ //#region lib/types/plan-service.js
587
+ function readPlan(room) {
588
+ const event = room.snapshotEvents().filter((event) => event.type === "room/plan-state").at(-1);
589
+ return event?.type === "room/plan-state" ? structuredClone(event.data) : void 0;
590
+ }
591
+ /** One serialized writer for durable goal transitions and automatic leaf dispatch. */
592
+ var PlanService = class {
593
+ ctx;
594
+ engine;
595
+ tails = /* @__PURE__ */ new Map();
596
+ recovered = /* @__PURE__ */ new Set();
597
+ timers = /* @__PURE__ */ new Map();
598
+ constructor(ctx, engine) {
599
+ this.ctx = ctx;
600
+ this.engine = engine;
601
+ }
602
+ locked(room, action) {
603
+ const next = (this.tails.get(room.id) ?? Promise.resolve()).catch(() => {}).then(action);
604
+ this.tails.set(room.id, next);
605
+ next.finally(() => {
606
+ if (this.tails.get(room.id) === next) this.tails.delete(room.id);
607
+ }).catch(() => {});
608
+ return next;
609
+ }
610
+ /** Cold inspection projects the restart pause without writing or starting agents. */
611
+ view(sessionId, view) {
612
+ if (this.recovered.has(sessionId)) return view;
613
+ const { requests: _requests, ...recovered } = recoverPlan({
614
+ ...view,
615
+ requests: []
616
+ }, Date.now());
617
+ return recovered;
618
+ }
619
+ async recover(room) {
620
+ return this.locked(room, () => this.recoverUnlocked(room));
621
+ }
622
+ async recoverUnlocked(room) {
623
+ if (this.recovered.has(room.id)) return;
624
+ const current = readPlan(room);
625
+ if (current !== void 0) {
626
+ const recovered = recoverPlan(current, Date.now());
627
+ const events = room.snapshotEvents();
628
+ for (const task of recovered.tasks) {
629
+ const attempt = task.attempts.at(-1);
630
+ if (attempt?.status !== "queued") continue;
631
+ const dispatch = events.find((event) => event.type === "room/dispatch" && event.data.id === attempt.deliveryId);
632
+ if (dispatch?.type !== "room/dispatch") continue;
633
+ const edge = events.filter((event) => event.type === "room/delivery-state" && event.data.dispatchSeq === dispatch.seq).at(-1);
634
+ if (edge?.type === "room/delivery-state" && ["running", "uncertain"].includes(edge.data.state)) {
635
+ attempt.status = "uncertain";
636
+ attempt.error = "Restart interrupted the durable admission boundary";
637
+ task.status = "failed";
638
+ recovered.status = "paused";
639
+ recovered.reason = "Reconcile uncertain attempts before continuing";
640
+ }
641
+ }
642
+ room.append("room/plan-state", recovered);
643
+ await this.ctx.sessions.flush(room);
644
+ }
645
+ this.recovered.add(room.id);
646
+ }
647
+ async write(room, plan) {
648
+ room.append("room/plan-state", plan);
649
+ await this.ctx.sessions.flush(room);
650
+ this.armDeadline(room, plan);
651
+ }
652
+ armDeadline(room, plan) {
653
+ clearTimeout(this.timers.get(room.id));
654
+ this.timers.delete(room.id);
655
+ if (plan.status !== "running") return;
656
+ const remaining = Math.max(1, plan.budget.maxActiveMs - activePlanMs(plan, Date.now()));
657
+ const timer = setTimeout(() => {
658
+ this.locked(room, async () => {
659
+ const current = readPlan(room);
660
+ if (current?.status !== "running") return;
661
+ if (activePlanMs(current, Date.now()) < current.budget.maxActiveMs) {
662
+ this.armDeadline(room, current);
663
+ return;
664
+ }
665
+ const paused = changePlan(current, {
666
+ action: "pause",
667
+ reason: "Active-time budget exhausted",
668
+ requestId: randomUUID(),
669
+ expectedRevision: current.revision
670
+ }, {
671
+ actor: {
672
+ kind: "coordinator",
673
+ memberId: "budget-timer"
674
+ },
675
+ memberIds: /* @__PURE__ */ new Set(),
676
+ now: Date.now()
677
+ });
678
+ await this.write(room, paused);
679
+ }).catch((error) => this.ctx.logger.warn(`room: goal deadline persistence failed: ${String(error)}`));
680
+ }, Math.min(remaining, 2147483647));
681
+ timer.unref?.();
682
+ this.timers.set(room.id, timer);
683
+ }
684
+ async command(room, json, actor) {
685
+ const command = parsePlanCommand(json);
686
+ return this.locked(room, async () => {
687
+ await this.recoverUnlocked(room);
688
+ const events = room.snapshotEvents();
689
+ const state = replay(events);
690
+ const members = new Set(state.members.map((member) => memberId(events, member)));
691
+ if (actor.kind !== "human" && !members.has(actor.memberId)) throw new Error("Calling member left the room");
692
+ const coordinator = coordinatorMember(state, events);
693
+ if (actor.kind === "coordinator" && (coordinator === void 0 || memberId(events, coordinator) !== actor.memberId)) throw new Error("Coordinator changed before command admission");
694
+ const earlier = events.filter((event) => event.type === "room/plan-state").flatMap((event) => event.type === "room/plan-state" ? event.data.requests : []).find((receipt) => receipt.id === command.requestId);
695
+ if (earlier !== void 0) {
696
+ if (earlier.signature !== planCommandSignature(command)) throw new Error("Plan request identity was reused for different input");
697
+ await this.ctx.sessions.flush(room);
698
+ await this.pump(room);
699
+ await this.engine().recover(room);
700
+ return readPlan(room);
701
+ }
702
+ const current = readPlan(room);
703
+ const next = changePlan(current, command, {
704
+ actor,
705
+ memberIds: members,
706
+ now: Date.now()
707
+ });
708
+ const wake = actor.kind === "human" && coordinator !== void 0 && next.status === "running" && (command.action === "create" && command.mode === "execute" || command.action === "resume" && (current?.status === "draft" || next.tasks.length === 0)) ? room.append("room/dispatch", {
709
+ id: randomUUID(),
710
+ origin: "report",
711
+ reportFor: `goal-start:${next.id}:${command.requestId}`,
712
+ targets: [coordinator.name],
713
+ targetIds: [memberId(events, coordinator)],
714
+ plan: { goalId: next.id },
715
+ text: `The human authorized execution of goal ${next.id}: ${next.objective}. Read room_read, organize appropriate stages and tasks with room_plan, invite members when useful, and dispatch within the recorded budget. Small work can remain direct. Review submitted evidence before accepting tasks; wait for correlated reports instead of polling.`
716
+ }) : void 0;
717
+ if (command.action === "reconcile") this.reconcileDelivery(room, next, command.taskId, command.attemptId, command.requestId);
718
+ await this.write(room, next);
719
+ await this.pump(room);
720
+ if (wake !== void 0 && coordinator !== void 0) this.engine().dispatch(room, coordinator.name, wake.data.text, {
721
+ dispatchSeq: wake.seq,
722
+ targetId: memberId(events, coordinator)
723
+ });
724
+ if (command.action === "resume" || command.action === "cancel" || command.action === "complete") await this.engine().recover(room);
725
+ return readPlan(room);
726
+ });
727
+ }
728
+ allows(room, dispatchSeq) {
729
+ if (dispatchSeq === void 0) return true;
730
+ const event = room.snapshotEvents().find((event) => event.seq === dispatchSeq);
731
+ if (event?.type !== "room/dispatch" || event.data.plan === void 0) return true;
732
+ const plan = readPlan(room);
733
+ return this.recovered.has(room.id) && plan?.id === event.data.plan.goalId && plan.status === "running";
734
+ }
735
+ async admitted(room, dispatchSeq) {
736
+ if (dispatchSeq === void 0) return;
737
+ const event = room.snapshotEvents().find((event) => event.seq === dispatchSeq);
738
+ if (event?.type !== "room/dispatch" || event.data.plan === void 0) return;
739
+ const link = event.data.plan;
740
+ await this.locked(room, async () => {
741
+ await this.recoverUnlocked(room);
742
+ const plan = readPlan(room);
743
+ if (plan?.id !== link.goalId || plan.status !== "running") throw new Error("Goal no longer permits automatic work");
744
+ if (link.taskId !== void 0 && link.attemptId !== void 0) await this.write(room, admitPlanAttempt(plan, link.taskId, link.attemptId, Date.now()));
745
+ });
746
+ }
747
+ async settled(room, dispatchSeq, outcome) {
748
+ if (dispatchSeq === void 0) return;
749
+ const event = room.snapshotEvents().find((event) => event.seq === dispatchSeq);
750
+ if (event?.type !== "room/dispatch" || event.data.plan?.taskId === void 0 || event.data.plan.attemptId === void 0) return;
751
+ const { goalId, taskId, attemptId } = event.data.plan;
752
+ await this.locked(room, async () => {
753
+ const current = readPlan(room);
754
+ if (current?.id !== goalId) return;
755
+ const next = settlePlanAttempt(current, taskId, attemptId, {
756
+ state: outcome.state,
757
+ ...outcome.error === void 0 ? {} : { error: outcome.error },
758
+ ...outcome.state === "done" && outcome.text?.trim() ? { evidence: {
759
+ summary: outcome.text.slice(0, 32e3),
760
+ references: [`room-delivery:${room.id}:${dispatchSeq}`],
761
+ artifacts: []
762
+ } } : {}
763
+ }, Date.now());
764
+ await this.write(room, next);
765
+ await this.pump(room);
766
+ });
767
+ }
768
+ /** Human goal reconciliation settles its room delivery and supersedes stale queued reports. */
769
+ reconcileDelivery(room, plan, taskId, attemptId, requestId) {
770
+ const attempt = plan.tasks.find((task) => task.id === taskId)?.attempts.find((attempt) => attempt.id === attemptId);
771
+ if (attempt === void 0) return;
772
+ const events = room.snapshotEvents();
773
+ const dispatch = events.find((event) => event.type === "room/dispatch" && event.data.id === attempt.deliveryId);
774
+ if (dispatch?.type !== "room/dispatch") return;
775
+ const state = replay(events);
776
+ for (const target of dispatch.data.targetIds ?? []) {
777
+ const deliveryId = `${dispatch.seq}:${target}`;
778
+ const member = state.members.find((member) => memberId(events, member) === target);
779
+ const run = state.runs.find((run) => run.member === member?.name);
780
+ if (run?.runId === deliveryId && ["running", "failed"].includes(run.state)) room.append("room/run-state", {
781
+ member: run.member,
782
+ runId: run.runId,
783
+ startedAt: run.startedAt,
784
+ state: attempt.status === "submitted" ? "done" : "failed",
785
+ ...attempt.status === "failed" ? { error: attempt.submission?.summary ?? "Human reconciled the attempt as failed" } : {}
786
+ });
787
+ room.append("room/delivery-state", {
788
+ id: deliveryId,
789
+ dispatchSeq: dispatch.seq,
790
+ memberId: target,
791
+ state: attempt.status === "submitted" ? "done" : "failed",
792
+ text: attempt.submission?.summary ?? "Human reconciliation"
793
+ });
794
+ for (const oldReport of events) {
795
+ if (oldReport.type !== "room/dispatch" || oldReport.data.reportFor !== deliveryId) continue;
796
+ for (const recipient of oldReport.data.targetIds ?? []) {
797
+ const oldId = `${oldReport.seq}:${recipient}`;
798
+ if (state.deliveries?.find((delivery) => delivery.id === oldId)?.status === "queued") room.append("room/delivery-state", {
799
+ id: oldId,
800
+ dispatchSeq: oldReport.seq,
801
+ memberId: recipient,
802
+ state: "cancelled",
803
+ error: "Superseded by human reconciliation"
804
+ });
805
+ }
806
+ }
807
+ }
808
+ const coordinator = coordinatorMember(state, events);
809
+ if (coordinator !== void 0) room.append("room/dispatch", {
810
+ id: randomUUID(),
811
+ origin: "report",
812
+ reportFor: `reconcile:${attempt.id}:${requestId}`,
813
+ targets: [coordinator.name],
814
+ targetIds: [memberId(events, coordinator)],
815
+ plan: { goalId: plan.id },
816
+ text: `Human reconciled task ${taskId}, attempt ${attemptId} as ${attempt.status}. Evidence: ${JSON.stringify(attempt.submission)}. Read the current plan before reviewing or retrying.`
817
+ });
818
+ }
819
+ async pump(room) {
820
+ let plan = readPlan(room);
821
+ if (plan?.status !== "running") return;
822
+ for (const task of readyPlanTasks(plan)) {
823
+ const events = room.snapshotEvents();
824
+ const state = replay(events);
825
+ const owner = state.members.find((member) => memberId(events, member) === task.ownerMemberId);
826
+ const coordinator = coordinatorMember(state, events);
827
+ if (owner === void 0 || coordinator === void 0) {
828
+ await this.write(room, changePlan(plan, {
829
+ action: "pause",
830
+ reason: "A task owner or coordinator is unavailable",
831
+ requestId: randomUUID(),
832
+ expectedRevision: plan.revision
833
+ }, {
834
+ actor: {
835
+ kind: "coordinator",
836
+ memberId: "scheduler"
837
+ },
838
+ memberIds: /* @__PURE__ */ new Set(),
839
+ now: Date.now()
840
+ }));
841
+ return;
842
+ }
843
+ const attemptId = randomUUID();
844
+ const dispatchId = randomUUID();
845
+ let reserved;
846
+ try {
847
+ reserved = queuePlanAttempt(plan, task.id, attemptId, dispatchId, Date.now());
848
+ } catch (error) {
849
+ if (String(error).includes("concurrency is full")) return;
850
+ throw error;
851
+ }
852
+ if (reserved.status !== "running") {
853
+ await this.write(room, reserved);
854
+ return;
855
+ }
856
+ const dependencyEvidence = dependencyResults(plan, task);
857
+ const text = [
858
+ `Goal ${plan.id}, task ${task.id}, attempt ${attemptId}.`,
859
+ `Objective: ${plan.objective}`,
860
+ task.instruction,
861
+ `Acceptance criteria: ${JSON.stringify(task.criteria)}`,
862
+ `Inputs: ${JSON.stringify(task.inputRefs)}. Expected artifacts: ${JSON.stringify(task.artifactPaths)}.`,
863
+ `Accepted dependency results: ${JSON.stringify(dependencyEvidence)}`,
864
+ "Submit your result and evidence with room_plan action submit (taskId, attemptId, evidence: {summary,references,artifacts}, requestId, expectedRevision from room_read). Only the coordinator or human accepts results; do not claim acceptance yourself."
865
+ ].join("\n");
866
+ const dispatch = room.append("room/dispatch", {
867
+ id: dispatchId,
868
+ origin: "coordinator",
869
+ targets: [owner.name],
870
+ targetIds: [task.ownerMemberId],
871
+ text,
872
+ plan: {
873
+ goalId: plan.id,
874
+ taskId: task.id,
875
+ attemptId
876
+ },
877
+ ...memberId(events, coordinator) === task.ownerMemberId ? {} : { replyTo: memberId(events, coordinator) }
878
+ });
879
+ await this.write(room, reserved);
880
+ this.engine().dispatch(room, owner.name, text, {
881
+ dispatchSeq: dispatch.seq,
882
+ targetId: task.ownerMemberId
883
+ });
884
+ plan = reserved;
885
+ }
886
+ }
887
+ dispose() {
888
+ for (const timer of this.timers.values()) clearTimeout(timer);
889
+ this.timers.clear();
890
+ }
891
+ };
892
+ //#endregion
893
+ //#region lib/types/agent-setup.js
894
+ /**
895
+ * Which preset a session actually runs, newest selection winning. The 0.1.2
896
+ * host folds the header plus `agent-preset/selected` events through
897
+ * `agentPresetProjectionDefinition`; the rc host's `resolveSessionPreset` is
898
+ * the same fold. A host with neither yields undefined: the resume falls back
899
+ * to the deployment's default preset, the same outcome a preset-less session
900
+ * had before.
901
+ * @param host - the agent-presets module namespace, structurally probed.
902
+ * @param session - the session's header and event log.
903
+ * @returns the preset id, or `undefined` when the session names none.
904
+ */
905
+ function deriveSessionPreset(host, session) {
906
+ if (host.agentPresetProjectionDefinition !== void 0) {
907
+ const projection = host.agentPresetProjectionDefinition;
908
+ let state = projection.init(session.header);
909
+ for (const event of session.events) state = projection.apply(state, event);
910
+ return state ?? void 0;
911
+ }
912
+ return host.resolveSessionPreset?.(session);
913
+ }
914
+ let presetRegistryProbe;
915
+ let probedHost = {};
916
+ /**
917
+ * Resolve the preset-registry module by name. The 0.1.7-rc.1 host renamed
918
+ * `@deepseek-ai/dsh-agent-presets` to `@deepseek-ai/dsh-agent-preset-registry`
919
+ * (the API carried over); 0.1.5/0.1.6 hosts install only the old name, and a
920
+ * host without preset composition installs neither — `null`, and the
921
+ * derivation host stays `{}`, the established no-preset degrade (the resume
922
+ * falls back to the deployment's default preset). A STATIC import of either
923
+ * name is a module-resolution failure at load on hosts installing only the
924
+ * other — pack-dist strips `dependencies`, so the specifier resolves against
925
+ * the host's install tree. Both specifiers stay static string literals so
926
+ * tsdown externalizes them and tsc type-checks them.
927
+ * @returns the module namespace, or null when neither name resolves.
928
+ */
929
+ function loadPresetRegistry() {
930
+ presetRegistryProbe ??= import("@deepseek-ai/dsh-agent-preset-registry").catch(() => import("@deepseek-ai/dsh-agent-presets")).catch(() => null);
931
+ return presetRegistryProbe;
932
+ }
933
+ /**
934
+ * The derivation input resolved by the last preload. `{}` before it, which
935
+ * the derivation reads as "no preset surface" — the no-preset degrade.
936
+ */
937
+ function presetDerivationHost() {
938
+ return probedHost;
939
+ }
940
+ /**
941
+ * Populate {@link presetDerivationHost} once, at apply time — every
942
+ * derivation happens on the resume/create paths that run after apply.
943
+ */
944
+ async function preloadPresetDerivationHost() {
945
+ probedHost = await loadPresetRegistry() ?? {};
946
+ }
947
+ /**
948
+ * Probe the agent-presets roster.
949
+ * @param ctx - host context.
950
+ * @returns the roster slice, or undefined when the composition mounts none
951
+ * (the agent then composes plain — the pre-presets host behavior).
952
+ */
953
+ function probeAgentPresets(ctx) {
954
+ const service = ctx.get("agentPresets");
955
+ if (service === void 0 || service === null) return void 0;
956
+ return typeof service.resolve === "function" && typeof service.mount === "function" ? service : void 0;
957
+ }
958
+ /**
959
+ * Compose the create/resume setup for a room session's agent, mirroring the
960
+ * official web path: resolve first (creation records the id on the header),
961
+ * then mount into the agent's scope during setup.
962
+ * @param ctx - host context.
963
+ * @param presetId - the preset to compose; undefined resolves the default.
964
+ * @returns the composition (empty when no presets roster is mounted).
965
+ */
966
+ async function composeRoomAgent(ctx, presetId) {
967
+ const presets = probeAgentPresets(ctx);
968
+ if (presets === void 0) return {};
969
+ const resolved = await presets.resolve(presetId);
970
+ return {
971
+ agentPreset: resolved.id,
972
+ setup: async (agentCtx) => {
973
+ await presets.mount(agentCtx, resolved.id);
974
+ }
975
+ };
976
+ }
977
+ /**
978
+ * The preset a room session runs, read from its log (a blank-window switch
979
+ * wins over the creation header — the official reconstruction rule).
980
+ * @param session - the live room session.
981
+ * @returns the preset id, or undefined when none was recorded.
982
+ */
983
+ function roomSessionPreset(session) {
984
+ return deriveSessionPreset(presetDerivationHost(), {
985
+ header: session.header,
986
+ events: session.snapshotEvents()
987
+ });
988
+ }
989
+ /**
990
+ * Probe the session-persistence service.
991
+ * @param ctx - host context.
992
+ * @returns the handle-opening slice, or undefined when no backend is mounted.
993
+ */
994
+ function probeSessionPersistence(ctx) {
995
+ const service = ctx.get("sessionPersistence");
996
+ if (service === void 0 || service === null) return void 0;
997
+ return typeof service.open === "function" ? service : void 0;
998
+ }
999
+ /**
1000
+ * Read a cold session's log from persistence without attaching or resuming
1001
+ * anything (the read-only remotes' cold path). The 0.1.5 handle-based API
1002
+ * replaced the one-shot `inspect`: open a read handle (no write ownership),
1003
+ * read the full log, close.
1004
+ * @param ctx - host context.
1005
+ * @param sessionId - the persisted session.
1006
+ * @returns the inspection, or undefined when absent/unreadable.
1007
+ */
1008
+ async function inspectCold(ctx, sessionId) {
1009
+ const persistence = probeSessionPersistence(ctx);
1010
+ if (persistence === void 0) return void 0;
1011
+ try {
1012
+ const handle = await persistence.open(sessionId, "read");
1013
+ try {
1014
+ const cold = await handle.read(0);
1015
+ return {
1016
+ meta: structuredClone(handle.header),
1017
+ inheritedEventCount: handle.inheritedEventCount,
1018
+ events: [...cold.events]
1019
+ };
1020
+ } finally {
1021
+ await handle.close();
1022
+ }
1023
+ } catch {
1024
+ return;
1025
+ }
1026
+ }
1027
+ //#endregion
1028
+ //#region lib/types/adapter.js
1029
+ /** The methods a service must carry to quack like the facade. */
1030
+ const FACADE_METHODS = [
1031
+ "start",
1032
+ "resume",
1033
+ "cancel"
1034
+ ];
1035
+ /**
1036
+ * Probe the local-agent facade.
1037
+ * @param ctx - host context.
1038
+ * @returns the facade when `localAgent` is present and carries the full M1
1039
+ * method set, undefined otherwise (absent core, or a pre-facade version).
1040
+ */
1041
+ function probeLocalAgent(ctx) {
1042
+ const service = ctx.get("localAgent");
1043
+ if (service === void 0 || service === null) return void 0;
1044
+ return FACADE_METHODS.every((method) => typeof service[method] === "function") ? service : void 0;
1045
+ }
1046
+ /**
1047
+ * Probe the registry's roster slice (independent of the delegation facade:
1048
+ * a core without M1 still serves the roster, and the invite dialog greys
1049
+ * undispatchable providers instead of hiding them).
1050
+ * @param ctx - host context.
1051
+ * @returns the roster slice, or undefined.
1052
+ */
1053
+ function probeLocalAgentRoster(ctx) {
1054
+ const service = ctx.get("localAgent");
1055
+ if (service === void 0 || service === null) return void 0;
1056
+ return typeof service["roster"] === "function" && typeof service["statusOf"] === "function" ? service : void 0;
1057
+ }
1058
+ /**
1059
+ * Extract a member's reply text from a settled run: the text blocks of its
1060
+ * final output, joined. Non-text blocks (images etc.) are dropped — the
1061
+ * blackboard is a text layer.
1062
+ * @param output - the settled run's output content.
1063
+ * @returns the reply text (empty string when the run produced no text).
1064
+ */
1065
+ function runOutputText(output) {
1066
+ return output.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
1067
+ }
1068
+ /** The producer-owned source kind identifying every room-dispatch followup. */
1069
+ const ROOM_KIND = "room";
1070
+ /** The producer-owned source of a room main-agent followup. */
1071
+ function roomSource() {
1072
+ return Object.freeze({ kind: ROOM_KIND });
1073
+ }
1074
+ /**
1075
+ * Whether a durable source belongs to the room producer, in every form the
1076
+ * log can hold: the current producer-owned kind (`room`), the V3→V4 migrated
1077
+ * kind (`plugin:@khorsheed/dsh-room`), and the released V3 wrapper a pre-V4
1078
+ * (0.1.5) host still serves verbatim.
1079
+ * @param source - the persisted message source to test.
1080
+ * @returns true when the source names this producer.
1081
+ */
1082
+ function isRoomSource(source) {
1083
+ if (typeof source !== "object" || source === null) return false;
1084
+ const record = source;
1085
+ return record.kind === "room" || record.kind === `plugin:@khorsheed/dsh-room` || record.kind === "plugin" && record.plugin === "@khorsheed/dsh-room";
1086
+ }
1087
+ function closedGoal(state, goalId) {
1088
+ return goalId !== void 0 && state.plan !== void 0 && (state.plan.id !== goalId || ["completed", "cancelled"].includes(state.plan.status));
1089
+ }
1090
+ /**
1091
+ * The roster section: one line per OTHER member (name, provider, one-line
1092
+ * role) plus the notification protocol — the fallback channel's trailing
1093
+ * own-line `@name <content>` format lives here so a member can reach others
1094
+ * without any bridge tooling. Carried only when the member has never seen
1095
+ * the roster or it changed since their last dispatch ({@link rosterStaleSince});
1096
+ * the member's own session holds it in between. The room GOAL is NOT part of
1097
+ * this section: it rides every dispatch on its own line (cheap, and it
1098
+ * orients the task).
1099
+ */
1100
+ function rosterSection(state, self) {
1101
+ return [
1102
+ "【成员名册】",
1103
+ ...state.members.filter((member) => member.name !== self).map((member) => {
1104
+ const provider = member.kind === "main-agent" ? "原生 DSH" : member.provider ?? "";
1105
+ const role = member.instructions?.split("\n", 1)[0]?.trim();
1106
+ return `- ${member.name}(${provider})${role === void 0 || role === "" ? "" : `:${role}`}`;
1107
+ }),
1108
+ "通知协议:要通知某个成员,在回复末尾独占一行写 `@名字 <内容>`;该行会被转交给对方(一阶段需房间主人确认)。"
1109
+ ].join("\n");
1110
+ }
1111
+ /** The notifications section: confirmed-undelivered relays addressed to the member. */
1112
+ function notificationsSection(relays) {
1113
+ return ["【通知】", ...relays.map((relay) => `- ${relay.from} 给你的通知: ${relay.content}`)].join("\n");
1114
+ }
1115
+ /**
1116
+ * Assemble the uniform member prompt: role-instructions carry, then the room
1117
+ * goal (every dispatch), then the roster (only when stale for this member),
1118
+ * then the member's confirmed-undelivered notifications (except the ones
1119
+ * this dispatch's own text already delivers), then this dispatch's text.
1120
+ */
1121
+ function assemblePrompt(room, member, cursor, text, relayIds) {
1122
+ const state = replay(room.snapshotEvents());
1123
+ const carried = state.relays.filter((relay) => relay.state === "confirmed" && relay.to === member.name && !relayIds.includes(relay.id));
1124
+ const sections = [];
1125
+ const handoff = room.snapshotEvents().filter((event) => event.type === "room/coordinator" && event.data.memberId === memberId(room.snapshotEvents(), member)).at(-1);
1126
+ if (handoff?.type === "room/coordinator" && (cursor === void 0 || handoff.seq > cursor)) sections.push(handoff.data.handoff);
1127
+ if (coordinatorMember(state, room.snapshotEvents())?.name === member.name) sections.push("You are the room coordinator. Handle ordinary chat directly; delegate small work in the background using room tools. Establish a goal plan only when useful for the requested execution. Delegate results arrive as correlated reports; do not poll while waiting.");
1128
+ const pending = pendingInstructions(room.snapshotEvents(), member.name, cursor);
1129
+ if (pending?.kind === "initial") sections.push(`你的角色指令:${pending.instructions}`);
1130
+ if (pending?.kind === "update") sections.push(`你的角色指令更新为:${pending.instructions}`);
1131
+ if (state.goal !== void 0) sections.push(`本房间的目标:${state.goal}`);
1132
+ if (rosterStaleSince(room.snapshotEvents(), cursor)) sections.push(rosterSection(state, member.name));
1133
+ if (carried.length > 0) sections.push(notificationsSection(carried));
1134
+ sections.push(text);
1135
+ return {
1136
+ prompt: sections.join("\n\n"),
1137
+ carried
1138
+ };
1139
+ }
1140
+ /** The human-readable reason a run failed (Error message, else String()). */
1141
+ function faultMessage(error) {
1142
+ return error instanceof Error ? error.message : String(error);
1143
+ }
1144
+ /**
1145
+ * The per-room dispatch engine. Owned by the RoomService; all state lives in
1146
+ * the journal except the in-process FIFO queues (a restart simply has no
1147
+ * in-flight runs).
1148
+ */
1149
+ var DispatchEngine = class {
1150
+ ctx;
1151
+ hooks;
1152
+ /** memberKey → the queue tail promise. */
1153
+ queues = /* @__PURE__ */ new Map();
1154
+ scheduled = /* @__PURE__ */ new Set();
1155
+ inFlight = /* @__PURE__ */ new Map();
1156
+ /**
1157
+ * @param ctx - host context carrying the session store and agents registry.
1158
+ */
1159
+ constructor(ctx, hooks = {}) {
1160
+ this.ctx = ctx;
1161
+ this.hooks = hooks;
1162
+ }
1163
+ /**
1164
+ * Enqueue a dispatch for execution. Fire-and-forget: the outcome is
1165
+ * journaled as run-state/speech events, never returned to the caller.
1166
+ * @param room - the room session.
1167
+ * @param memberName - the dispatch target.
1168
+ * @param text - the dispatch text.
1169
+ * @param options - dispatch-event seq and/or the relays this run delivers.
1170
+ */
1171
+ hasPending(room, memberName) {
1172
+ const member = replay(room.snapshotEvents()).members.find((member) => member.name === memberName);
1173
+ return member !== void 0 && ([...this.inFlight.values()].includes(`${room.id} ${memberId(room.snapshotEvents(), member)}`) || replayDeliveries(room.snapshotEvents()).some((delivery) => delivery.memberId === member.id && [
1174
+ "queued",
1175
+ "running",
1176
+ "uncertain"
1177
+ ].includes(delivery.status)));
1178
+ }
1179
+ dispatch(room, memberName, text, options = {}) {
1180
+ const events = room.snapshotEvents();
1181
+ const member = replay(events).members.find((entry) => entry.name === memberName);
1182
+ if (member === void 0) return;
1183
+ const targetId = options.targetId ?? memberId(events, member);
1184
+ const key = `${room.id} ${targetId}`;
1185
+ const deliveryId = options.dispatchSeq === void 0 ? void 0 : `${options.dispatchSeq}:${targetId}`;
1186
+ if (deliveryId !== void 0) {
1187
+ const scopedId = `${room.id}:${deliveryId}`;
1188
+ if (this.scheduled.has(scopedId) || events.some((event) => event.type === "room/delivery-state" && event.data.id === deliveryId)) return;
1189
+ this.scheduled.add(scopedId);
1190
+ }
1191
+ const tail = this.queues.get(key) ?? Promise.resolve();
1192
+ let submitted;
1193
+ const submission = new Promise((resolve) => {
1194
+ submitted = resolve;
1195
+ });
1196
+ const next = tail.then(() => this.run(room, memberName, text, {
1197
+ ...options,
1198
+ targetId
1199
+ }, submitted)).catch((error) => {
1200
+ this.ctx.logger.warn(`room: dispatch to "${memberName}" faulted: ${String(error)}`);
1201
+ }).finally(() => {
1202
+ submitted();
1203
+ this.inFlight.delete(next);
1204
+ if (deliveryId !== void 0) this.scheduled.delete(`${room.id}:${deliveryId}`);
1205
+ });
1206
+ this.inFlight.set(next, key);
1207
+ this.queues.set(key, submission);
1208
+ submission.then(() => {
1209
+ if (this.queues.get(key) === submission) this.queues.delete(key);
1210
+ });
1211
+ }
1212
+ /** Drain includes reports queued by the runs being drained. */
1213
+ async idle() {
1214
+ while (this.inFlight.size > 0) await Promise.all([...this.inFlight.keys()]);
1215
+ }
1216
+ /** Read-only recovery projection: persisted running edges are not live processes. */
1217
+ view(sessionId, state) {
1218
+ const error = "Execution outcome is unknown after restart; reconcile before retrying";
1219
+ return {
1220
+ ...state,
1221
+ ...state.deliveries === void 0 ? {} : { deliveries: state.deliveries.map((row) => row.status === "queued" && closedGoal(state, row.plan?.goalId) ? {
1222
+ ...row,
1223
+ status: "cancelled"
1224
+ } : row.status === "running" && !this.scheduled.has(`${sessionId}:${row.id}`) ? {
1225
+ ...row,
1226
+ status: "uncertain",
1227
+ error
1228
+ } : row) },
1229
+ runs: state.runs.map((run) => {
1230
+ const member = state.members.find((row) => row.name === run.member);
1231
+ const active = run.runId === void 0 ? member !== void 0 && [...this.inFlight.values()].includes(`${sessionId} ${member.id}`) : this.scheduled.has(`${sessionId}:${run.runId}`);
1232
+ return run.state === "running" && !active ? {
1233
+ ...run,
1234
+ state: "failed",
1235
+ error
1236
+ } : run;
1237
+ })
1238
+ };
1239
+ }
1240
+ /** Restore accepted targets, never repeat an execution whose settlement is unknown. */
1241
+ async recover(room) {
1242
+ const events = room.snapshotEvents();
1243
+ const state = replay(events);
1244
+ for (const dispatch of events) {
1245
+ if (dispatch.type !== "room/dispatch" || dispatch.data.targetIds === void 0) continue;
1246
+ for (const targetId of dispatch.data.targetIds) {
1247
+ const id = `${dispatch.seq}:${targetId}`;
1248
+ if (this.scheduled.has(`${room.id}:${id}`)) continue;
1249
+ const last = events.filter((event) => event.type === "room/delivery-state" && event.data.id === id).at(-1);
1250
+ if (last === void 0 && closedGoal(state, dispatch.data.plan?.goalId)) {
1251
+ room.append("room/delivery-state", {
1252
+ id,
1253
+ dispatchSeq: dispatch.seq,
1254
+ memberId: targetId,
1255
+ state: "cancelled",
1256
+ error: "Goal closed before delivery admission"
1257
+ });
1258
+ await this.ctx.sessions.flush(room);
1259
+ continue;
1260
+ }
1261
+ if (last?.type === "room/delivery-state") {
1262
+ if (last.data.state === "running") {
1263
+ room.append("room/delivery-state", {
1264
+ id,
1265
+ dispatchSeq: dispatch.seq,
1266
+ memberId: targetId,
1267
+ state: "uncertain",
1268
+ error: "Host restarted before a durable settlement; reconcile before retrying"
1269
+ });
1270
+ const target = state.members.find((member) => memberId(events, member) === targetId);
1271
+ if (target !== void 0) {
1272
+ const run = state.runs.find((run) => run.member === target.name);
1273
+ if (run?.state === "running" && (run.runId === void 0 || run.runId === id)) room.append("room/run-state", {
1274
+ member: target.name,
1275
+ state: "failed",
1276
+ startedAt: run.startedAt,
1277
+ ...run.runId === void 0 ? {} : { runId: run.runId },
1278
+ error: "Execution outcome is unknown after restart; reconcile before retrying"
1279
+ });
1280
+ }
1281
+ await this.ctx.sessions.flush(room);
1282
+ await this.report(room, dispatch.seq, id, targetId, "uncertain", "Execution outcome is unknown after restart; reconcile before retrying");
1283
+ } else await this.report(room, dispatch.seq, id, targetId, last.data.state, last.data.text ?? last.data.error ?? "");
1284
+ continue;
1285
+ }
1286
+ const target = state.members.find((member) => memberId(events, member) === targetId);
1287
+ if (target !== void 0 && !state.deliveries?.some((row) => row.memberId === targetId && (row.status === "running" || row.status === "uncertain"))) this.dispatch(room, target.name, dispatch.data.text, {
1288
+ dispatchSeq: dispatch.seq,
1289
+ targetId
1290
+ });
1291
+ }
1292
+ }
1293
+ }
1294
+ /** Execute one dispatch: run-state running → member turn → settle edges. */
1295
+ async run(room, memberName, text, options, submitted) {
1296
+ if (this.hooks.allows?.(room, options.dispatchSeq) === false) return;
1297
+ const state = replay(room.snapshotEvents());
1298
+ const member = state.members.find((entry) => options.targetId === void 0 ? entry.name === memberName : memberId(room.snapshotEvents(), entry) === options.targetId);
1299
+ if (member === void 0) return;
1300
+ if (state.deliveries?.some((delivery) => delivery.memberId === member.id && delivery.status === "uncertain")) return;
1301
+ memberName = member.name;
1302
+ const identity = memberId(room.snapshotEvents(), member);
1303
+ const deliveryId = options.dispatchSeq === void 0 ? void 0 : `${options.dispatchSeq}:${identity}`;
1304
+ const runId = deliveryId ?? randomUUID();
1305
+ const cursor = previousCursor(room.snapshotEvents(), memberName, options.dispatchSeq ?? Number.MAX_SAFE_INTEGER);
1306
+ let startedAt;
1307
+ const admitted = async () => {
1308
+ if (startedAt !== void 0) throw new Error("Room delivery was admitted twice");
1309
+ startedAt = Date.now();
1310
+ if (deliveryId !== void 0) room.append("room/delivery-state", {
1311
+ id: deliveryId,
1312
+ dispatchSeq: options.dispatchSeq,
1313
+ memberId: identity,
1314
+ state: "running"
1315
+ });
1316
+ memberName = replay(room.snapshotEvents()).members.find((entry) => memberId(room.snapshotEvents(), entry) === identity)?.name ?? memberName;
1317
+ room.append("room/run-state", {
1318
+ member: memberName,
1319
+ state: "running",
1320
+ startedAt,
1321
+ runId
1322
+ });
1323
+ await this.hooks.admitted?.(room, options.dispatchSeq);
1324
+ await this.ctx.sessions.flush(room);
1325
+ return startedAt;
1326
+ };
1327
+ let outcome;
1328
+ try {
1329
+ outcome = member.kind === "main-agent" ? await this.runMainAgent(room, member, cursor, text, await admitted(), options.relayIds ?? []) : await this.runCliMember(room, member, cursor, text, admitted, options.relayIds ?? [], submitted);
1330
+ } catch (error) {
1331
+ this.ctx.logger.warn(`room: dispatch to "${memberName}" failed: ${String(error)}`);
1332
+ outcome = {
1333
+ state: "failed",
1334
+ error: faultMessage(error)
1335
+ };
1336
+ }
1337
+ outcome = await this.settle(room, member, runId, startedAt ?? Date.now(), outcome);
1338
+ await this.hooks.settled?.(room, options.dispatchSeq, outcome);
1339
+ if (deliveryId !== void 0) {
1340
+ room.append("room/delivery-state", {
1341
+ id: deliveryId,
1342
+ dispatchSeq: options.dispatchSeq,
1343
+ memberId: identity,
1344
+ state: outcome.state,
1345
+ ...outcome.text === void 0 ? {} : { text: outcome.text },
1346
+ ...outcome.error === void 0 ? {} : { error: outcome.error }
1347
+ });
1348
+ await this.ctx.sessions.flush(room);
1349
+ await this.report(room, options.dispatchSeq, deliveryId, identity, outcome.state, outcome.text ?? outcome.error ?? "");
1350
+ }
1351
+ }
1352
+ async report(room, dispatchSeq, deliveryId, fromId, outcome, text) {
1353
+ const events = room.snapshotEvents();
1354
+ const source = events.find((event) => event.seq === dispatchSeq);
1355
+ if (source?.type !== "room/dispatch" || source.data.origin !== "coordinator" || source.data.replyTo === void 0) return;
1356
+ if (events.some((event) => event.type === "room/dispatch" && event.data.reportFor === deliveryId)) return;
1357
+ const state = replay(events);
1358
+ if (closedGoal(state, source.data.plan?.goalId)) return;
1359
+ const recipient = source.data.plan === void 0 ? state.members.find((member) => memberId(events, member) === source.data.replyTo) : coordinatorMember(state, events);
1360
+ const sender = state.members.find((member) => memberId(events, member) === fromId);
1361
+ if (recipient === void 0) return;
1362
+ const reportText = `Delegation ${deliveryId} (${sender?.name ?? fromId}) ${outcome}.\n${text.slice(0, 32e3)}`;
1363
+ const report = room.append("room/dispatch", {
1364
+ id: randomUUID(),
1365
+ origin: "report",
1366
+ reportFor: deliveryId,
1367
+ targets: [recipient.name],
1368
+ targetIds: [memberId(events, recipient)],
1369
+ text: reportText,
1370
+ ...source.data.plan === void 0 ? {} : { plan: { goalId: source.data.plan.goalId } }
1371
+ });
1372
+ await this.ctx.sessions.flush(room);
1373
+ if (this.hooks.allows?.(room, report.seq) !== false) this.dispatch(room, recipient.name, reportText, {
1374
+ dispatchSeq: report.seq,
1375
+ targetId: memberId(events, recipient)
1376
+ });
1377
+ }
1378
+ /**
1379
+ * Mark the relays this run delivered as sent. Delivery = the member's own
1380
+ * session received the prompt (the followup accepted / the CLI run
1381
+ * started): the text lives in the member's private context from then on,
1382
+ * so a later run failure cannot un-deliver it.
1383
+ */
1384
+ async markSent(room, relays) {
1385
+ if (relays.length === 0) return;
1386
+ for (const relay of relays) room.append("room/relay-resolved", {
1387
+ id: relay.id,
1388
+ state: "sent"
1389
+ });
1390
+ await this.ctx.sessions.flush(room);
1391
+ }
1392
+ /** Main-agent member turn: a producer-sourced followup on the room's own agent. */
1393
+ async runMainAgent(room, member, cursor, text, _startedAt, relayIds) {
1394
+ const agent = this.ctx.agents.get(room.id);
1395
+ if (agent === void 0) return {
1396
+ state: "failed",
1397
+ error: "the room session has no live agent"
1398
+ };
1399
+ const { prompt, carried } = assemblePrompt(room, member, cursor, text, relayIds);
1400
+ agent.followup(createUserMessage({
1401
+ content: [{
1402
+ type: "text",
1403
+ text: prompt
1404
+ }],
1405
+ source: roomSource()
1406
+ }));
1407
+ await this.markSent(room, [...carried, ...replay(room.snapshotEvents()).relays.filter((relay) => relayIds.includes(relay.id))]);
1408
+ await agent.whenIdle();
1409
+ return { state: "done" };
1410
+ }
1411
+ /** CLI member turn: facade start (first round) or resume, then the speech mirror. */
1412
+ async runCliMember(room, member, cursor, text, admitted, relayIds, submitted) {
1413
+ const facade = probeLocalAgent(this.ctx);
1414
+ if (facade === void 0) {
1415
+ this.ctx.logger.warn(`room: cannot dispatch to "${member.name}": local-agent facade unavailable`);
1416
+ throw new Error("the local-agent delegation facade is unavailable");
1417
+ }
1418
+ const provider = member.provider ?? "";
1419
+ let startedAt;
1420
+ const controlled = facade.supportsMemberConfiguration?.(provider) === true;
1421
+ const admission = controlled ? { onAdmitted: async () => {
1422
+ startedAt = await admitted();
1423
+ } } : {};
1424
+ if (!controlled) startedAt = await admitted();
1425
+ const { prompt, carried } = assemblePrompt(room, member, cursor, text, relayIds);
1426
+ const prepared = member.childSessionId !== void 0 && facade.isPreparedMember?.(member.childSessionId) === true;
1427
+ const pendingRun = prepared ? facade.start(room.id, provider, [{
1428
+ type: "text",
1429
+ text: prompt
1430
+ }], {
1431
+ preparedMemberId: member.childSessionId,
1432
+ ...admission
1433
+ }) : member.childSessionId === void 0 ? facade.start(room.id, provider, [{
1434
+ type: "text",
1435
+ text: prompt
1436
+ }], member.model === void 0 && member.cwd === void 0 && !controlled ? void 0 : {
1437
+ ...admission,
1438
+ ...member.model === void 0 ? {} : { model: member.model },
1439
+ ...member.cwd === void 0 ? {} : { cwd: member.cwd }
1440
+ }) : facade.resume(room.id, provider, member.childSessionId, [{
1441
+ type: "text",
1442
+ text: prompt
1443
+ }], member.cwd === void 0 && !controlled ? void 0 : {
1444
+ ...admission,
1445
+ ...member.cwd === void 0 ? {} : { cwd: member.cwd }
1446
+ });
1447
+ if (controlled && member.childSessionId !== void 0 && !prepared) submitted();
1448
+ const run = await pendingRun;
1449
+ if (startedAt === void 0) {
1450
+ await run.dispose();
1451
+ throw new Error("Provider published a room run without admission");
1452
+ }
1453
+ try {
1454
+ if (member.childSessionId === void 0) room.append("room/member-updated", {
1455
+ name: replay(room.snapshotEvents()).members.find((entry) => entry.id === member.id)?.name ?? member.name,
1456
+ childSessionId: run.id
1457
+ });
1458
+ await this.markSent(room, [...carried, ...replay(room.snapshotEvents()).relays.filter((relay) => relayIds.includes(relay.id))]);
1459
+ await this.ctx.sessions.flush(room);
1460
+ } catch (error) {
1461
+ await run.result.catch(() => {});
1462
+ throw error;
1463
+ }
1464
+ if (controlled) submitted();
1465
+ const result = await run.result;
1466
+ const output = runOutputText(result.output);
1467
+ const interrupted = result.stopReason === "completed" ? void 0 : result.stopReason === "aborted" ? "cancelled" : "failed";
1468
+ const speech = output !== "" || interrupted === void 0 ? room.append("room/speech", {
1469
+ member: replay(room.snapshotEvents()).members.find((entry) => entry.id === member.id)?.name ?? member.name,
1470
+ text: output,
1471
+ childSessionId: run.id,
1472
+ durationMs: Date.now() - startedAt,
1473
+ ...interrupted === void 0 ? {} : { interrupted }
1474
+ }) : void 0;
1475
+ if (speech !== void 0) await this.ctx.sessions.flush(room);
1476
+ if (result.stopReason === "completed") {
1477
+ const directive = parseRelayDirective(runOutputText(result.output));
1478
+ const roster = replay(room.snapshotEvents()).members;
1479
+ if (directive !== void 0 && directive.to !== member.name && roster.some((entry) => entry.name === directive.to)) room.append("room/relay", {
1480
+ id: randomUUID(),
1481
+ from: member.name,
1482
+ to: directive.to,
1483
+ content: directive.content,
1484
+ provenance: {
1485
+ kind: "speech-fallback",
1486
+ speechSeq: speech.seq
1487
+ }
1488
+ });
1489
+ await this.ctx.sessions.flush(room);
1490
+ return {
1491
+ state: "done",
1492
+ text: runOutputText(result.output)
1493
+ };
1494
+ } else if (result.stopReason === "aborted") return {
1495
+ state: "cancelled",
1496
+ text: output
1497
+ };
1498
+ else return {
1499
+ state: "failed",
1500
+ text: output,
1501
+ error: `the member run ended with stopReason "${result.stopReason}"`
1502
+ };
1503
+ }
1504
+ /** Settle only this execution and its own task rows, preserving newer runs. */
1505
+ async settle(room, member, runId, startedAt, outcome) {
1506
+ const events = room.snapshotEvents();
1507
+ const memberName = replay(events).members.find((entry) => memberId(events, entry) === memberId(events, member))?.name ?? member.name;
1508
+ const terminal = events.find((event) => event.type === "room/run-state" && event.data.runId === runId && event.data.state !== "running");
1509
+ if (terminal?.type === "room/run-state" && terminal.data.state !== "running") return {
1510
+ ...outcome,
1511
+ state: terminal.data.state,
1512
+ ...terminal.data.error === void 0 ? {} : { error: terminal.data.error }
1513
+ };
1514
+ room.append("room/run-state", {
1515
+ member: memberName,
1516
+ runId,
1517
+ state: outcome.state,
1518
+ startedAt,
1519
+ elapsedMs: Date.now() - startedAt,
1520
+ ...outcome.error === void 0 ? {} : { error: outcome.error }
1521
+ });
1522
+ for (const task of replay(room.snapshotEvents()).tasks) if (task.member === memberName && task.status === "in_progress" && task.deliveryId === runId) room.append("room/task-updated", {
1523
+ id: task.id,
1524
+ status: outcome.state
1525
+ });
1526
+ await this.ctx.sessions.flush(room);
1527
+ return outcome;
1528
+ }
1529
+ };
1530
+ //#endregion
1531
+ //#region lib/types/index.js
1532
+ /**
1533
+ * room host half: the `room` Typert Remote service. (The `room_invite` /
1534
+ * `room_task` / `room_message` model tools left this package's profile-root
1535
+ * registration in the M4' tool-row split — their definition factories live
1536
+ * in ./tool.ts and the companion `@khorsheed/dsh-room-tool` mounts them
1537
+ * inside agent-preset compositions.) The room's entire state is the session's `room/*` custom-event
1538
+ * journal (log-only events — persistence and reload-replay come free, the
1539
+ * model never sees them, and harnesses without this plugin replay the session
1540
+ * safely); every read folds the journal through the pure replay, and every
1541
+ * mutating Remote appends and flushes. Dispatch records and first tasks are
1542
+ * executed by the DispatchEngine: main-agent members get a producer-sourced
1543
+ * followup on the room's own agent, CLI members go through the probed
1544
+ * local-agent delegation facade (absent facade = degraded CLI capability,
1545
+ * never a boot failure). The member-notification gate lives here too: the
1546
+ * local-agent bridge duck-type-calls `receiveMemberMessage` (service face via
1547
+ * ctx.provide, off the wire), the human confirms/dismisses through the
1548
+ * confirmRelay/dismissRelay Remotes.
1549
+ * @module @khorsheed/dsh-room
1550
+ */
1551
+ var __runInitializers = function(thisArg, initializers, value) {
1552
+ var useValue = arguments.length > 2;
1553
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
1554
+ return useValue ? value : void 0;
1555
+ };
1556
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
1557
+ function accept(f) {
1558
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
1559
+ return f;
1560
+ }
1561
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
1562
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
1563
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
1564
+ var _, done = false;
1565
+ for (var i = decorators.length - 1; i >= 0; i--) {
1566
+ var context = {};
1567
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
1568
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
1569
+ context.addInitializer = function(f) {
1570
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
1571
+ extraInitializers.push(accept(f || null));
1572
+ };
1573
+ var result = (0, decorators[i])(kind === "accessor" ? {
1574
+ get: descriptor.get,
1575
+ set: descriptor.set
1576
+ } : descriptor[key], context);
1577
+ if (kind === "accessor") {
1578
+ if (result === void 0) continue;
1579
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
1580
+ if (_ = accept(result.get)) descriptor.get = _;
1581
+ if (_ = accept(result.set)) descriptor.set = _;
1582
+ if (_ = accept(result.init)) initializers.unshift(_);
1583
+ } else if (_ = accept(result)) {
1584
+ if (kind === "field") initializers.unshift(_);
1585
+ else descriptor[key] = _;
1586
+ }
1587
+ }
1588
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
1589
+ done = true;
1590
+ };
1591
+ var __rewriteRelativeImportExtension = function(path, preserveJsx) {
1592
+ if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
1593
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
1594
+ });
1595
+ return path;
1596
+ };
1597
+ const catalogSpecifier = [
1598
+ "@deepseek-ai/dsh-session",
1599
+ "src",
1600
+ "known-event-types"
1601
+ ].join("/");
1602
+ const catalogModule = await import(__rewriteRelativeImportExtension(catalogSpecifier)).catch(() => import("@deepseek-ai/dsh-session"));
1603
+ for (const type of ROOM_EVENT_TYPES) catalogModule.KNOWN_SESSION_EVENT_TYPES.add(type);
1604
+ /** Addressing/display names must stay parseable by the composer @-grammar. */
1605
+ function validName(name) {
1606
+ return name !== "" && !/\s/.test(name) && !name.includes("@");
1607
+ }
1608
+ /** A task title is the dispatch text's first line, truncated. */
1609
+ function taskTitle(text) {
1610
+ const first = text.trim().split("\n", 1)[0] ?? "";
1611
+ return first.length > 60 ? `${first.slice(0, 60)}…` : first;
1612
+ }
1613
+ /**
1614
+ * room Remote service: promotion, roster management, the human @-message
1615
+ * intake, the notification gate, the task board, and run cancellation. A
1616
+ * room IS a normal session; the journal is its only state. A room is never
1617
+ * CREATED apart from a session — inviting an agent into any session promotes
1618
+ * it (see ensureRoom; the room-session-promotion proposal).
1619
+ */
1620
+ let RoomService = (() => {
1621
+ let _classSuper = TypertRemoteService;
1622
+ let _instanceExtraInitializers = [];
1623
+ let _isRoom_decorators;
1624
+ let _listProviders_decorators;
1625
+ let _getState_decorators;
1626
+ let _invite_decorators;
1627
+ let _updateMember_decorators;
1628
+ let _prepareMember_decorators;
1629
+ let _reconcileDelivery_decorators;
1630
+ let _setCoordinator_decorators;
1631
+ let _removeMember_decorators;
1632
+ let _postMessage_decorators;
1633
+ let _planCommand_decorators;
1634
+ let _confirmRelay_decorators;
1635
+ let _dismissRelay_decorators;
1636
+ let _addTask_decorators;
1637
+ let _closeTask_decorators;
1638
+ let _setGoal_decorators;
1639
+ let _cancel_decorators;
1640
+ return class RoomService extends _classSuper {
1641
+ static {
1642
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
1643
+ _isRoom_decorators = [Remote("isRoom")];
1644
+ _listProviders_decorators = [Remote("listProviders")];
1645
+ _getState_decorators = [Remote("getState")];
1646
+ _invite_decorators = [Remote("invite")];
1647
+ _updateMember_decorators = [Remote("updateMember")];
1648
+ _prepareMember_decorators = [Remote("prepareMember")];
1649
+ _reconcileDelivery_decorators = [Remote("reconcileDelivery")];
1650
+ _setCoordinator_decorators = [Remote("setCoordinator")];
1651
+ _removeMember_decorators = [Remote("removeMember")];
1652
+ _postMessage_decorators = [Remote("postMessage")];
1653
+ _planCommand_decorators = [Remote("planCommand")];
1654
+ _confirmRelay_decorators = [Remote("confirmRelay")];
1655
+ _dismissRelay_decorators = [Remote("dismissRelay")];
1656
+ _addTask_decorators = [Remote("addTask")];
1657
+ _closeTask_decorators = [Remote("closeTask")];
1658
+ _setGoal_decorators = [Remote("setGoal")];
1659
+ _cancel_decorators = [Remote("cancel")];
1660
+ __esDecorate(this, null, _isRoom_decorators, {
1661
+ kind: "method",
1662
+ name: "isRoom",
1663
+ static: false,
1664
+ private: false,
1665
+ access: {
1666
+ has: (obj) => "isRoom" in obj,
1667
+ get: (obj) => obj.isRoom
1668
+ },
1669
+ metadata: _metadata
1670
+ }, null, _instanceExtraInitializers);
1671
+ __esDecorate(this, null, _listProviders_decorators, {
1672
+ kind: "method",
1673
+ name: "listProviders",
1674
+ static: false,
1675
+ private: false,
1676
+ access: {
1677
+ has: (obj) => "listProviders" in obj,
1678
+ get: (obj) => obj.listProviders
1679
+ },
1680
+ metadata: _metadata
1681
+ }, null, _instanceExtraInitializers);
1682
+ __esDecorate(this, null, _getState_decorators, {
1683
+ kind: "method",
1684
+ name: "getState",
1685
+ static: false,
1686
+ private: false,
1687
+ access: {
1688
+ has: (obj) => "getState" in obj,
1689
+ get: (obj) => obj.getState
1690
+ },
1691
+ metadata: _metadata
1692
+ }, null, _instanceExtraInitializers);
1693
+ __esDecorate(this, null, _invite_decorators, {
1694
+ kind: "method",
1695
+ name: "invite",
1696
+ static: false,
1697
+ private: false,
1698
+ access: {
1699
+ has: (obj) => "invite" in obj,
1700
+ get: (obj) => obj.invite
1701
+ },
1702
+ metadata: _metadata
1703
+ }, null, _instanceExtraInitializers);
1704
+ __esDecorate(this, null, _updateMember_decorators, {
1705
+ kind: "method",
1706
+ name: "updateMember",
1707
+ static: false,
1708
+ private: false,
1709
+ access: {
1710
+ has: (obj) => "updateMember" in obj,
1711
+ get: (obj) => obj.updateMember
1712
+ },
1713
+ metadata: _metadata
1714
+ }, null, _instanceExtraInitializers);
1715
+ __esDecorate(this, null, _prepareMember_decorators, {
1716
+ kind: "method",
1717
+ name: "prepareMember",
1718
+ static: false,
1719
+ private: false,
1720
+ access: {
1721
+ has: (obj) => "prepareMember" in obj,
1722
+ get: (obj) => obj.prepareMember
1723
+ },
1724
+ metadata: _metadata
1725
+ }, null, _instanceExtraInitializers);
1726
+ __esDecorate(this, null, _reconcileDelivery_decorators, {
1727
+ kind: "method",
1728
+ name: "reconcileDelivery",
1729
+ static: false,
1730
+ private: false,
1731
+ access: {
1732
+ has: (obj) => "reconcileDelivery" in obj,
1733
+ get: (obj) => obj.reconcileDelivery
1734
+ },
1735
+ metadata: _metadata
1736
+ }, null, _instanceExtraInitializers);
1737
+ __esDecorate(this, null, _setCoordinator_decorators, {
1738
+ kind: "method",
1739
+ name: "setCoordinator",
1740
+ static: false,
1741
+ private: false,
1742
+ access: {
1743
+ has: (obj) => "setCoordinator" in obj,
1744
+ get: (obj) => obj.setCoordinator
1745
+ },
1746
+ metadata: _metadata
1747
+ }, null, _instanceExtraInitializers);
1748
+ __esDecorate(this, null, _removeMember_decorators, {
1749
+ kind: "method",
1750
+ name: "removeMember",
1751
+ static: false,
1752
+ private: false,
1753
+ access: {
1754
+ has: (obj) => "removeMember" in obj,
1755
+ get: (obj) => obj.removeMember
1756
+ },
1757
+ metadata: _metadata
1758
+ }, null, _instanceExtraInitializers);
1759
+ __esDecorate(this, null, _postMessage_decorators, {
1760
+ kind: "method",
1761
+ name: "postMessage",
1762
+ static: false,
1763
+ private: false,
1764
+ access: {
1765
+ has: (obj) => "postMessage" in obj,
1766
+ get: (obj) => obj.postMessage
1767
+ },
1768
+ metadata: _metadata
1769
+ }, null, _instanceExtraInitializers);
1770
+ __esDecorate(this, null, _planCommand_decorators, {
1771
+ kind: "method",
1772
+ name: "planCommand",
1773
+ static: false,
1774
+ private: false,
1775
+ access: {
1776
+ has: (obj) => "planCommand" in obj,
1777
+ get: (obj) => obj.planCommand
1778
+ },
1779
+ metadata: _metadata
1780
+ }, null, _instanceExtraInitializers);
1781
+ __esDecorate(this, null, _confirmRelay_decorators, {
1782
+ kind: "method",
1783
+ name: "confirmRelay",
1784
+ static: false,
1785
+ private: false,
1786
+ access: {
1787
+ has: (obj) => "confirmRelay" in obj,
1788
+ get: (obj) => obj.confirmRelay
1789
+ },
1790
+ metadata: _metadata
1791
+ }, null, _instanceExtraInitializers);
1792
+ __esDecorate(this, null, _dismissRelay_decorators, {
1793
+ kind: "method",
1794
+ name: "dismissRelay",
1795
+ static: false,
1796
+ private: false,
1797
+ access: {
1798
+ has: (obj) => "dismissRelay" in obj,
1799
+ get: (obj) => obj.dismissRelay
1800
+ },
1801
+ metadata: _metadata
1802
+ }, null, _instanceExtraInitializers);
1803
+ __esDecorate(this, null, _addTask_decorators, {
1804
+ kind: "method",
1805
+ name: "addTask",
1806
+ static: false,
1807
+ private: false,
1808
+ access: {
1809
+ has: (obj) => "addTask" in obj,
1810
+ get: (obj) => obj.addTask
1811
+ },
1812
+ metadata: _metadata
1813
+ }, null, _instanceExtraInitializers);
1814
+ __esDecorate(this, null, _closeTask_decorators, {
1815
+ kind: "method",
1816
+ name: "closeTask",
1817
+ static: false,
1818
+ private: false,
1819
+ access: {
1820
+ has: (obj) => "closeTask" in obj,
1821
+ get: (obj) => obj.closeTask
1822
+ },
1823
+ metadata: _metadata
1824
+ }, null, _instanceExtraInitializers);
1825
+ __esDecorate(this, null, _setGoal_decorators, {
1826
+ kind: "method",
1827
+ name: "setGoal",
1828
+ static: false,
1829
+ private: false,
1830
+ access: {
1831
+ has: (obj) => "setGoal" in obj,
1832
+ get: (obj) => obj.setGoal
1833
+ },
1834
+ metadata: _metadata
1835
+ }, null, _instanceExtraInitializers);
1836
+ __esDecorate(this, null, _cancel_decorators, {
1837
+ kind: "method",
1838
+ name: "cancel",
1839
+ static: false,
1840
+ private: false,
1841
+ access: {
1842
+ has: (obj) => "cancel" in obj,
1843
+ get: (obj) => obj.cancel
1844
+ },
1845
+ metadata: _metadata
1846
+ }, null, _instanceExtraInitializers);
1847
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
1848
+ enumerable: true,
1849
+ configurable: true,
1850
+ writable: true,
1851
+ value: _metadata
1852
+ });
1853
+ }
1854
+ static inject = ["sessions", "agents"];
1855
+ /** The dispatch engine executing this service's dispatch records. */
1856
+ plans = __runInitializers(this, _instanceExtraInitializers);
1857
+ engine;
1858
+ handoffs = /* @__PURE__ */ new Set();
1859
+ /** sessionId → in-flight cold resume (mutations on a cold room dedupe). */
1860
+ resumes = /* @__PURE__ */ new Map();
1861
+ /**
1862
+ * @param ctx - host context carrying the session store.
1863
+ */
1864
+ constructor(ctx) {
1865
+ super(ctx, "room");
1866
+ ctx.effect(async () => {
1867
+ await preloadPresetDerivationHost();
1868
+ return () => {};
1869
+ }, "room: preset-registry probe");
1870
+ ctx.effect(async () => {
1871
+ const loader = ctx.get("loader");
1872
+ if (loader?.import === void 0) return () => {};
1873
+ for (const specifier of ["@deepseek-ai/dsh-session", catalogSpecifier]) try {
1874
+ const catalog = await loader.import(specifier);
1875
+ for (const type of ROOM_EVENT_TYPES) catalog.KNOWN_SESSION_EVENT_TYPES?.add(type);
1876
+ } catch {}
1877
+ return () => {};
1878
+ }, "room: runtime event vocabulary");
1879
+ this.plans = new PlanService(ctx, () => this.engine);
1880
+ this.engine = new DispatchEngine(ctx, {
1881
+ allows: (room, seq) => this.plans.allows(room, seq),
1882
+ admitted: (room, seq) => this.plans.admitted(room, seq),
1883
+ settled: (room, seq, outcome) => this.plans.settled(room, seq, outcome)
1884
+ });
1885
+ ctx.effect(() => () => this.plans.dispose());
1886
+ const nativeTurns = /* @__PURE__ */ new WeakMap();
1887
+ ctx.on("agent/pre-step", async ({ agent, messages, turn, step }, next) => {
1888
+ const events = agent.session.snapshotEvents();
1889
+ if (!isRoomLog(events) || coordinatorMember(replay(events), events)?.kind !== "cli") return next();
1890
+ if (nativeTurns.get(agent) === turn) return next();
1891
+ if (step !== 1) throw new Error("Room native turn has no explicit member dispatch");
1892
+ if (messages.some((message) => isRoomSource(message.source))) {
1893
+ nativeTurns.set(agent, turn);
1894
+ return next();
1895
+ }
1896
+ for (const message of messages) {
1897
+ if (message.source.kind !== "user" || message.content.some((block) => block.type !== "text")) throw new Error("Use the room input route for this coordinator; this input cannot be forwarded losslessly");
1898
+ const result = await this.postMessage({
1899
+ sessionId: agent.session.id,
1900
+ requestId: String(message.id),
1901
+ text: message.content.map((block) => block.type === "text" ? block.text : "").join("\n")
1902
+ });
1903
+ if (!result.ok) throw new Error(`Room input routing failed: ${result.error.code}`);
1904
+ }
1905
+ return {
1906
+ kind: "enter",
1907
+ messages: []
1908
+ };
1909
+ });
1910
+ }
1911
+ /** Resolve a LIVE session as a room: presence, marker, replayed state. */
1912
+ load(sessionId) {
1913
+ const session = this.ctx.sessions.get(sessionId);
1914
+ if (session === void 0) return {
1915
+ ok: false,
1916
+ error: { code: "session-not-found" }
1917
+ };
1918
+ if (!isRoomLog(session.snapshotEvents())) return {
1919
+ ok: false,
1920
+ error: { code: "not-a-room" }
1921
+ };
1922
+ return {
1923
+ ok: true,
1924
+ session,
1925
+ state: replay(session.snapshotEvents())
1926
+ };
1927
+ }
1928
+ /**
1929
+ * Read a session as a room without side effects: the live store first, then
1930
+ * a persistence inspection (a cold room after a host restart answers from
1931
+ * its durable log; nothing is attached or resumed on a read).
1932
+ */
1933
+ async loadCold(sessionId) {
1934
+ const live = this.ctx.sessions.get(sessionId);
1935
+ if (live !== void 0) return isRoomLog(live.snapshotEvents()) ? {
1936
+ ok: true,
1937
+ state: replay(live.snapshotEvents()),
1938
+ preset: roomSessionPreset(live)
1939
+ } : {
1940
+ ok: false,
1941
+ error: { code: "not-a-room" }
1942
+ };
1943
+ const inspected = await inspectCold(this.ctx, sessionId);
1944
+ if (inspected === void 0) return {
1945
+ ok: false,
1946
+ error: { code: "session-not-found" }
1947
+ };
1948
+ if (!isRoomLog(inspected.events)) return {
1949
+ ok: false,
1950
+ error: { code: "not-a-room" }
1951
+ };
1952
+ return {
1953
+ ok: true,
1954
+ state: replay(inspected.events),
1955
+ preset: deriveSessionPreset(presetDerivationHost(), {
1956
+ header: inspected.meta,
1957
+ events: inspected.events
1958
+ })
1959
+ };
1960
+ }
1961
+ /**
1962
+ * Resolve a room for a MUTATION: the session must be live to take appends,
1963
+ * and dispatch needs its agent live (the delegation facade resolves the
1964
+ * parent through `ctx.agents.get` — live agents only). A cold room is
1965
+ * cold-resumed through the agent factory, which republishes session +
1966
+ * agent under the preset the log records; resumes dedupe per session.
1967
+ */
1968
+ ensureLive(sessionId) {
1969
+ const hit = this.load(sessionId);
1970
+ if (hit.ok) return (async () => {
1971
+ await this.plans.recover(hit.session);
1972
+ await this.engine.recover(hit.session);
1973
+ return this.load(sessionId);
1974
+ })();
1975
+ if (hit.error.code !== "session-not-found") return Promise.resolve(hit);
1976
+ let pending = this.resumes.get(sessionId);
1977
+ if (pending === void 0) {
1978
+ pending = this.resumeRoom(sessionId).finally(() => this.resumes.delete(sessionId));
1979
+ this.resumes.set(sessionId, pending);
1980
+ }
1981
+ return pending;
1982
+ }
1983
+ /** One cold resume: refuse non-rooms before paying an agent composition. */
1984
+ async resumeRoom(sessionId) {
1985
+ const cold = await this.loadCold(sessionId);
1986
+ if (!cold.ok) return cold;
1987
+ try {
1988
+ const composition = await composeRoomAgent(this.ctx, cold.preset);
1989
+ await this.ctx.agents.resume({
1990
+ resumeSessionId: sessionId,
1991
+ ...composition.setup === void 0 ? {} : { setup: composition.setup }
1992
+ });
1993
+ } catch (error) {
1994
+ return {
1995
+ ok: false,
1996
+ error: {
1997
+ code: "resume-failed",
1998
+ message: String(error)
1999
+ }
2000
+ };
2001
+ }
2002
+ const loaded = this.load(sessionId);
2003
+ if (loaded.ok) {
2004
+ await this.plans.recover(loaded.session);
2005
+ await this.engine.recover(loaded.session);
2006
+ }
2007
+ return loaded;
2008
+ }
2009
+ /**
2010
+ * Probe whether a session is a room: replay its event log looking for the
2011
+ * `room/created` marker. Side-effect-free: a cold session answers from a
2012
+ * persistence inspection, so opening any session never resumes its agent.
2013
+ * @param request - session identity.
2014
+ * @returns true when the session carries the marker (false for unknown sessions too).
2015
+ */
2016
+ async isRoom(request) {
2017
+ const session = this.ctx.sessions.get(request.sessionId);
2018
+ if (session !== void 0) return isRoomLog(session.snapshotEvents());
2019
+ const inspected = await inspectCold(this.ctx, request.sessionId);
2020
+ return inspected !== void 0 && isRoomLog(inspected.events);
2021
+ }
2022
+ /**
2023
+ * Resolve a session for a room WRITE, promoting it when it isn't a room
2024
+ * yet: a live plain session gains the `room/created` identity marker and
2025
+ * the main agent's roster seat (the promotion IS the entry model — the
2026
+ * room-session-promotion proposal: any session an agent is invited into
2027
+ * becomes a room). Idempotent — an existing room passes through untouched,
2028
+ * and concurrent promotions converge on the log (the first `room/created`
2029
+ * wins identity). A cold EXISTING room resumes through the normal path;
2030
+ * a cold PLAIN session cannot be promoted sight-unseen (promotion writes
2031
+ * on the live session, and the surfaces that promote — the open session's
2032
+ * invite dialog, the room tools running in their own session — always hold
2033
+ * it live), so it answers `not-a-room`. NOT a Remote — the surfaces reach
2034
+ * it through the write paths (invite/messageMember); public so tests and a
2035
+ * future manual "promote" surface share the one entry.
2036
+ * @param sessionId - the session to write.
2037
+ * @returns the live session plus replayed state, or the rejection.
2038
+ */
2039
+ async ensureRoom(sessionId) {
2040
+ const live = this.ctx.sessions.get(sessionId);
2041
+ if (live === void 0) {
2042
+ const cold = await this.loadCold(sessionId);
2043
+ if (!cold.ok) return {
2044
+ ok: false,
2045
+ error: cold.error
2046
+ };
2047
+ return this.ensureLive(sessionId);
2048
+ }
2049
+ if (!isRoomLog(live.snapshotEvents())) {
2050
+ live.append("room/created", { version: 1 });
2051
+ live.append("room/member-added", {
2052
+ name: "dsh",
2053
+ kind: "main-agent",
2054
+ invitedBy: "human"
2055
+ });
2056
+ await this.ctx.sessions.flush(live);
2057
+ }
2058
+ return {
2059
+ ok: true,
2060
+ session: live,
2061
+ state: replay(live.snapshotEvents())
2062
+ };
2063
+ }
2064
+ /**
2065
+ * List the invitable CLI providers: the local-agent roster with each
2066
+ * harness's auth state, plus the delegation-facade verdict (absent facade =
2067
+ * CLI members undispatchable; the dialog greys the section instead of
2068
+ * failing). Harnesses without a delegation provider are record-only and
2069
+ * are not invitable.
2070
+ * @param _request - no parameters.
2071
+ * @returns the provider list and the facade verdict.
2072
+ */
2073
+ async listProviders(_request) {
2074
+ const localAgentAvailable = probeLocalAgent(this.ctx) !== void 0;
2075
+ const roster = probeLocalAgentRoster(this.ctx);
2076
+ if (roster === void 0) return {
2077
+ localAgentAvailable,
2078
+ providers: []
2079
+ };
2080
+ const providers = [];
2081
+ for (const row of roster.roster()) {
2082
+ const status = await roster.statusOf(row.name);
2083
+ if (status.delegationProvider === void 0) continue;
2084
+ providers.push({
2085
+ provider: status.delegationProvider,
2086
+ displayName: row.displayName,
2087
+ harness: row.name,
2088
+ authenticated: status.authenticated
2089
+ });
2090
+ }
2091
+ return {
2092
+ localAgentAvailable,
2093
+ providers
2094
+ };
2095
+ }
2096
+ /**
2097
+ * Replay the room's journal into the derived state (roster, notification
2098
+ * relays, task board, run states).
2099
+ * @param request - room session identity.
2100
+ * @returns the replayed state, or a rejection from the closed failure union.
2101
+ */
2102
+ async getState(request) {
2103
+ const loaded = await this.loadCold(request.sessionId);
2104
+ if (!loaded.ok) return {
2105
+ ok: false,
2106
+ error: loaded.error
2107
+ };
2108
+ return {
2109
+ ok: true,
2110
+ value: {
2111
+ ...this.engine.view(request.sessionId, loaded.state),
2112
+ ...loaded.state.plan === void 0 ? {} : { plan: this.plans.view(request.sessionId, loaded.state.plan) }
2113
+ }
2114
+ };
2115
+ }
2116
+ /**
2117
+ * Validate, PROMOTE (a plain session becomes a room — see ensureRoom), and
2118
+ * journal an invitation: the name must be parseable by the
2119
+ * composer @-grammar and unique, the provider non-blank AND a registered
2120
+ * delegation provider (the classic slip is the harness name `kimi` where
2121
+ * the family registered `kimi-cli` — validated against the roster's
2122
+ * `delegationProvider` set, the same probe listProviders serves; a roster-
2123
+ * less core skips the check, degrading to the old accept-anything), and
2124
+ * (CLI members are undispatchable without it) the local-agent facade must
2125
+ * be probed. A first task is journaled as a `room/dispatch` (auto-opening
2126
+ * the member's in_progress task) and handed to the engine immediately —
2127
+ * the receipt's `pendingFirstTask` means "dispatched". Shared by the
2128
+ * Remote surface (`invitedBy: 'human'`) and the room_invite tool
2129
+ * (`invitedBy: 'agent'`).
2130
+ * @param request - room session, provider, name, optional cwd, instructions and first task.
2131
+ * @param invitedBy - the invitation's origin.
2132
+ * @returns the invitation receipt, or a rejection.
2133
+ */
2134
+ async inviteMember(request, invitedBy, actorChildSessionId) {
2135
+ const loaded = await this.ensureRoom(request.sessionId);
2136
+ if (!loaded.ok) return {
2137
+ ok: false,
2138
+ error: loaded.error
2139
+ };
2140
+ if (this.handoffs.has(request.sessionId)) return {
2141
+ ok: false,
2142
+ error: { code: "coordinator-busy" }
2143
+ };
2144
+ if (invitedBy === "agent" && !this.isCurrentCoordinator(loaded.session, actorChildSessionId)) return {
2145
+ ok: false,
2146
+ error: { code: "not-coordinator" }
2147
+ };
2148
+ if (!validName(request.name)) return {
2149
+ ok: false,
2150
+ error: { code: "invalid-name" }
2151
+ };
2152
+ if (loaded.state.members.some((member) => member.name === request.name)) return {
2153
+ ok: false,
2154
+ error: { code: "duplicate-name" }
2155
+ };
2156
+ if (request.provider.trim() === "") return {
2157
+ ok: false,
2158
+ error: { code: "empty-provider" }
2159
+ };
2160
+ if (request.instructions !== void 0 && request.instructions.trim() === "") return {
2161
+ ok: false,
2162
+ error: { code: "empty-text" }
2163
+ };
2164
+ if (request.firstTask !== void 0 && request.firstTask.trim() === "") return {
2165
+ ok: false,
2166
+ error: { code: "empty-text" }
2167
+ };
2168
+ if (request.model !== void 0 && request.model.trim() === "") return {
2169
+ ok: false,
2170
+ error: { code: "empty-text" }
2171
+ };
2172
+ if (probeLocalAgent(this.ctx) === void 0) return {
2173
+ ok: false,
2174
+ error: { code: "local-agent-unavailable" }
2175
+ };
2176
+ const roster = probeLocalAgentRoster(this.ctx);
2177
+ if (roster !== void 0) {
2178
+ const available = [];
2179
+ for (const row of roster.roster()) {
2180
+ const status = await roster.statusOf(row.name);
2181
+ if (status.delegationProvider !== void 0) available.push(status.delegationProvider);
2182
+ }
2183
+ if (!available.includes(request.provider)) return {
2184
+ ok: false,
2185
+ error: {
2186
+ code: "unknown-provider",
2187
+ provider: request.provider,
2188
+ available
2189
+ }
2190
+ };
2191
+ }
2192
+ if (this.handoffs.has(request.sessionId)) return {
2193
+ ok: false,
2194
+ error: { code: "coordinator-busy" }
2195
+ };
2196
+ if (invitedBy === "agent" && !this.isCurrentCoordinator(loaded.session, actorChildSessionId)) return {
2197
+ ok: false,
2198
+ error: { code: "not-coordinator" }
2199
+ };
2200
+ if (replay(loaded.session.snapshotEvents()).members.some((member) => member.name === request.name)) return {
2201
+ ok: false,
2202
+ error: { code: "duplicate-name" }
2203
+ };
2204
+ loaded.session.append("room/member-added", {
2205
+ id: randomUUID(),
2206
+ name: request.name,
2207
+ kind: "cli",
2208
+ provider: request.provider,
2209
+ invitedBy,
2210
+ ...request.instructions === void 0 ? {} : { instructions: request.instructions },
2211
+ ...request.cwd === void 0 || request.cwd.trim() === "" ? {} : { cwd: request.cwd.trim() },
2212
+ ...request.model === void 0 || request.model.trim() === "" ? {} : { model: request.model.trim() }
2213
+ });
2214
+ let firstTaskSeq;
2215
+ if (request.firstTask !== void 0) {
2216
+ firstTaskSeq = loaded.session.append("room/dispatch", {
2217
+ id: randomUUID(),
2218
+ origin: invitedBy === "human" ? "human" : "coordinator",
2219
+ ...invitedBy === "human" ? {} : { replyTo: memberId(loaded.session.snapshotEvents(), coordinatorMember(replay(loaded.session.snapshotEvents()), loaded.session.snapshotEvents())) },
2220
+ targetIds: [memberId(loaded.session.snapshotEvents(), replay(loaded.session.snapshotEvents()).members.find((member) => member.name === request.name))],
2221
+ targets: [request.name],
2222
+ text: request.firstTask
2223
+ }).seq;
2224
+ loaded.session.append("room/task-added", {
2225
+ id: randomUUID(),
2226
+ member: request.name,
2227
+ title: taskTitle(request.firstTask),
2228
+ status: "in_progress",
2229
+ deliveryId: `${firstTaskSeq}:${memberId(loaded.session.snapshotEvents(), replay(loaded.session.snapshotEvents()).members.find((member) => member.name === request.name))}`
2230
+ });
2231
+ }
2232
+ await this.ctx.sessions.flush(loaded.session);
2233
+ if (request.firstTask !== void 0 && firstTaskSeq !== void 0) this.engine.dispatch(loaded.session, request.name, request.firstTask, { dispatchSeq: firstTaskSeq });
2234
+ return {
2235
+ ok: true,
2236
+ value: {
2237
+ name: request.name,
2238
+ pendingFirstTask: request.firstTask !== void 0
2239
+ }
2240
+ };
2241
+ }
2242
+ /**
2243
+ * Invite a CLI member (the human path; see inviteMember).
2244
+ * @param request - room session, provider, name, optional cwd, instructions and first task.
2245
+ * @returns the invitation receipt, or a rejection.
2246
+ */
2247
+ invite(request) {
2248
+ return this.inviteMember(request, "human");
2249
+ }
2250
+ /**
2251
+ * Update a member's editable fields: rename (validated like an invite name —
2252
+ * unique, parseable; the main agent is the room itself and cannot be
2253
+ * renamed), role instructions (null or a blank text CLEARS them — later
2254
+ * dispatches inject none), the member-level cwd override (null or a
2255
+ * blank text clears back to inheriting the room cwd; still roster-recorded
2256
+ * only — the delegation facade's per-call cwd override is the family's open
2257
+ * R2), and the intended model (null or a blank text clears it back to the
2258
+ * harness default; roster-recorded intent — the first dispatch binds it as
2259
+ * the facade start's `model` option, and the live member's immediate switch
2260
+ * is the client's localAgentGateway `setMemberModel` call, not this one).
2261
+ * An instructions edit rides the member's next dispatch as a context
2262
+ * update (the CLI session itself is never rewritten); a rename migrates
2263
+ * every name-keyed projection at replay (the journal fold moves the roster
2264
+ * key, tasks' member/blockedBy, relays' from/to, and the runs key).
2265
+ * @param request - room session, member name, and the fields to change.
2266
+ * @returns the update receipt (the CURRENT name), or a rejection.
2267
+ */
2268
+ async updateMember(request) {
2269
+ const loaded = await this.ensureLive(request.sessionId);
2270
+ if (!loaded.ok) return {
2271
+ ok: false,
2272
+ error: loaded.error
2273
+ };
2274
+ if (this.handoffs.has(request.sessionId)) return {
2275
+ ok: false,
2276
+ error: { code: "coordinator-busy" }
2277
+ };
2278
+ const member = loaded.state.members.find((entry) => entry.name === request.name);
2279
+ if (member === void 0) return {
2280
+ ok: false,
2281
+ error: { code: "member-not-found" }
2282
+ };
2283
+ if (request.cwd !== void 0 && (member.childSessionId !== void 0 || this.engine.hasPending(loaded.session, member.name))) return {
2284
+ ok: false,
2285
+ error: { code: "member-cwd-bound" }
2286
+ };
2287
+ if (request.model !== void 0 && member.childSessionId !== void 0) return {
2288
+ ok: false,
2289
+ error: { code: "configuration-owned-by-core" }
2290
+ };
2291
+ if (request.rename === void 0 && request.instructions === void 0 && request.cwd === void 0 && request.model === void 0) return {
2292
+ ok: false,
2293
+ error: { code: "nothing-to-update" }
2294
+ };
2295
+ if (request.rename !== void 0 && request.rename !== request.name) {
2296
+ if (member.kind === "main-agent") return {
2297
+ ok: false,
2298
+ error: { code: "main-member" }
2299
+ };
2300
+ if (!validName(request.rename)) return {
2301
+ ok: false,
2302
+ error: { code: "invalid-name" }
2303
+ };
2304
+ if (loaded.state.members.some((entry) => entry.name === request.rename)) return {
2305
+ ok: false,
2306
+ error: { code: "duplicate-name" }
2307
+ };
2308
+ }
2309
+ const instructions = typeof request.instructions === "string" && request.instructions.trim() === "" ? null : request.instructions;
2310
+ const cwd = typeof request.cwd === "string" ? request.cwd.trim() === "" ? null : request.cwd.trim() : request.cwd;
2311
+ const model = typeof request.model === "string" ? request.model.trim() === "" ? null : request.model.trim() : request.model;
2312
+ loaded.session.append("room/member-updated", {
2313
+ name: request.name,
2314
+ ...request.rename === void 0 ? {} : { rename: request.rename },
2315
+ ...instructions === void 0 ? {} : { instructions },
2316
+ ...cwd === void 0 ? {} : { cwd },
2317
+ ...model === void 0 ? {} : { model }
2318
+ });
2319
+ await this.ctx.sessions.flush(loaded.session);
2320
+ return {
2321
+ ok: true,
2322
+ value: { name: request.name }
2323
+ };
2324
+ }
2325
+ /** Native preparation creates no unrelated conversation turn and leaves the role unchanged. */
2326
+ async prepareMember(request) {
2327
+ if (this.handoffs.has(request.sessionId)) return {
2328
+ ok: false,
2329
+ error: { code: "coordinator-busy" }
2330
+ };
2331
+ this.handoffs.add(request.sessionId);
2332
+ try {
2333
+ return await this.prepareMemberInRoom(request);
2334
+ } finally {
2335
+ this.handoffs.delete(request.sessionId);
2336
+ }
2337
+ }
2338
+ async prepareMemberInRoom(request) {
2339
+ const loaded = await this.ensureLive(request.sessionId);
2340
+ if (!loaded.ok) return {
2341
+ ok: false,
2342
+ error: loaded.error
2343
+ };
2344
+ const member = loaded.state.members.find((member) => member.name === request.name);
2345
+ if (member?.kind !== "cli" || member.provider === void 0) return {
2346
+ ok: false,
2347
+ error: { code: "member-not-found" }
2348
+ };
2349
+ const family = probeLocalAgent(this.ctx);
2350
+ if (family?.prepareMember === void 0) return {
2351
+ ok: false,
2352
+ error: {
2353
+ code: "coordinator-not-ready",
2354
+ message: "Native preparation is unavailable in this family core"
2355
+ }
2356
+ };
2357
+ try {
2358
+ const childSessionId = await family.prepareMember(request.sessionId, member.provider, member.childSessionId ?? memberId(loaded.session.snapshotEvents(), member), {
2359
+ ...member.cwd === void 0 ? {} : { cwd: member.cwd },
2360
+ ...member.model === void 0 ? {} : { model: member.model }
2361
+ });
2362
+ loaded.session.append("room/member-updated", {
2363
+ name: member.name,
2364
+ childSessionId: SessionId(childSessionId)
2365
+ });
2366
+ await this.ctx.sessions.flush(loaded.session);
2367
+ return {
2368
+ ok: true,
2369
+ value: { childSessionId }
2370
+ };
2371
+ } catch (error) {
2372
+ return {
2373
+ ok: false,
2374
+ error: {
2375
+ code: "coordinator-not-ready",
2376
+ message: String(error)
2377
+ }
2378
+ };
2379
+ }
2380
+ }
2381
+ /** A human records the known result before releasing a crashed member's queued work. */
2382
+ async reconcileDelivery(request) {
2383
+ const loaded = await this.ensureLive(request.sessionId);
2384
+ if (!loaded.ok) return {
2385
+ ok: false,
2386
+ error: loaded.error
2387
+ };
2388
+ const delivery = loaded.state.deliveries?.find((row) => row.id === request.deliveryId);
2389
+ if (delivery?.status !== "uncertain") return {
2390
+ ok: false,
2391
+ error: { code: "delivery-not-uncertain" }
2392
+ };
2393
+ if (delivery.plan?.taskId !== void 0) return {
2394
+ ok: false,
2395
+ error: { code: "plan-reconciliation-required" }
2396
+ };
2397
+ if (request.evidence.trim() === "") return {
2398
+ ok: false,
2399
+ error: { code: "empty-text" }
2400
+ };
2401
+ loaded.session.append("room/delivery-state", {
2402
+ id: delivery.id,
2403
+ dispatchSeq: delivery.dispatchSeq,
2404
+ memberId: delivery.memberId,
2405
+ state: request.outcome,
2406
+ text: request.evidence.trim()
2407
+ });
2408
+ const run = loaded.state.runs.find((run) => run.runId === delivery.id);
2409
+ if (run !== void 0) loaded.session.append("room/run-state", {
2410
+ member: run.member,
2411
+ runId: delivery.id,
2412
+ startedAt: run.startedAt,
2413
+ state: request.outcome
2414
+ });
2415
+ await this.ctx.sessions.flush(loaded.session);
2416
+ await this.engine.recover(loaded.session);
2417
+ return { ok: true };
2418
+ }
2419
+ /** Explicit human role change; state is committed only after readiness and idle checks. */
2420
+ async setCoordinator(request) {
2421
+ if (this.handoffs.has(request.sessionId)) return {
2422
+ ok: false,
2423
+ error: { code: "coordinator-busy" }
2424
+ };
2425
+ this.handoffs.add(request.sessionId);
2426
+ try {
2427
+ const loaded = await this.ensureLive(request.sessionId);
2428
+ if (!loaded.ok) return {
2429
+ ok: false,
2430
+ error: loaded.error
2431
+ };
2432
+ const events = loaded.session.snapshotEvents();
2433
+ let candidate = loaded.state.members.find((member) => memberId(events, member) === request.memberId);
2434
+ const previous = coordinatorMember(loaded.state, events);
2435
+ if (candidate === void 0 || previous === void 0) return {
2436
+ ok: false,
2437
+ error: { code: "member-not-found" }
2438
+ };
2439
+ if ((loaded.state.coordinator?.revision ?? 0) !== request.expectedRevision) return {
2440
+ ok: false,
2441
+ error: { code: "coordinator-conflict" }
2442
+ };
2443
+ const agent = this.ctx.agents.get(request.sessionId);
2444
+ if (agent?.status === "running" || (agent?.inbox?.nextTurn.length ?? 0) > 0 || (agent?.inbox?.nextStep.length ?? 0) > 0 || this.engine.hasPending(loaded.session, previous.name) || this.engine.hasPending(loaded.session, candidate.name)) return {
2445
+ ok: false,
2446
+ error: { code: "coordinator-busy" }
2447
+ };
2448
+ if (candidate.kind === "cli" && candidate.childSessionId === void 0) {
2449
+ const prepared = await this.prepareMemberInRoom({
2450
+ sessionId: request.sessionId,
2451
+ name: candidate.name
2452
+ });
2453
+ if (!prepared.ok) return {
2454
+ ok: false,
2455
+ error: prepared.error
2456
+ };
2457
+ candidate = {
2458
+ ...candidate,
2459
+ childSessionId: SessionId(prepared.value.childSessionId)
2460
+ };
2461
+ }
2462
+ const refreshedEvents = loaded.session.snapshotEvents();
2463
+ const refreshed = replay(refreshedEvents);
2464
+ candidate = refreshed.members.find((member) => memberId(refreshedEvents, member) === request.memberId);
2465
+ if (candidate === void 0) return {
2466
+ ok: false,
2467
+ error: { code: "member-not-found" }
2468
+ };
2469
+ if ((refreshed.coordinator?.revision ?? 0) !== request.expectedRevision) return {
2470
+ ok: false,
2471
+ error: { code: "coordinator-conflict" }
2472
+ };
2473
+ const refreshedAgent = this.ctx.agents.get(request.sessionId);
2474
+ if (refreshedAgent?.status === "running" || (refreshedAgent?.inbox?.nextTurn.length ?? 0) > 0 || (refreshedAgent?.inbox?.nextStep.length ?? 0) > 0 || this.engine.hasPending(loaded.session, previous.name) || this.engine.hasPending(loaded.session, candidate.name)) return {
2475
+ ok: false,
2476
+ error: { code: "coordinator-busy" }
2477
+ };
2478
+ if (candidate.kind === "cli") {
2479
+ const family = this.ctx.get("localAgent");
2480
+ if (candidate.childSessionId === void 0 || family?.canCoordinateRoom?.(candidate.childSessionId) !== true) return {
2481
+ ok: false,
2482
+ error: {
2483
+ code: "coordinator-not-ready",
2484
+ message: "The member coordination tool channel is unavailable"
2485
+ }
2486
+ };
2487
+ }
2488
+ for (const participant of [previous, candidate]) {
2489
+ if (participant.kind !== "cli") continue;
2490
+ const family = this.ctx.get("localAgent");
2491
+ if (participant.childSessionId === void 0 || family?.memberConfiguration === void 0) return {
2492
+ ok: false,
2493
+ error: {
2494
+ code: "coordinator-not-ready",
2495
+ message: "Member native session preparation is required before promotion"
2496
+ }
2497
+ };
2498
+ const inbox = family.isPreparedMember?.(participant.childSessionId) === true ? void 0 : family.readMemberInbox?.(participant.childSessionId);
2499
+ if ((family.queuedMemberRounds?.(participant.childSessionId) ?? 0) > 0 || inbox?.messages.some((row) => [
2500
+ "queued",
2501
+ "running",
2502
+ "uncertain"
2503
+ ].includes(row.status))) return {
2504
+ ok: false,
2505
+ error: { code: "coordinator-busy" }
2506
+ };
2507
+ const control = family.memberConfiguration(participant.childSessionId);
2508
+ if (control.status !== "idle" || control.round !== void 0 || control.pending !== void 0 || control.lockedReason !== void 0) return {
2509
+ ok: false,
2510
+ error: {
2511
+ code: "coordinator-not-ready",
2512
+ message: "Member configuration has not converged or is locked"
2513
+ }
2514
+ };
2515
+ }
2516
+ const handoff = [
2517
+ `Coordinator handoff from ${previous.name} to ${candidate.name}.`,
2518
+ `Room session: ${request.sessionId}. Earlier native conversations remain available by their session IDs.`,
2519
+ loaded.state.goal === void 0 ? "" : `Goal: ${loaded.state.goal}`,
2520
+ loaded.state.plan === void 0 ? "" : `Formal goal ${loaded.state.plan.id}, revision ${loaded.state.plan.revision}: ${loaded.state.plan.objective} (${loaded.state.plan.status}). Read room_read for attempts, evidence, dependencies and budget; preserve the current pause state.`,
2521
+ ...loaded.state.tasks.filter((task) => !["done", "cancelled"].includes(task.status)).map((task) => `Open task ${task.id}: ${task.member}: ${task.title} (${task.status})`),
2522
+ ...events.filter((event) => event.type === "room/speech" || event.type === "room/dispatch").slice(-12).map((event) => event.type === "room/speech" ? `${event.data.member}: ${event.data.text.slice(0, 1500)}` : event.type === "room/dispatch" ? `To ${event.data.targets.join(", ")}: ${event.data.text.slice(0, 1500)}` : "")
2523
+ ].filter(Boolean).join("\n").slice(0, 16e3);
2524
+ const value = {
2525
+ version: 1,
2526
+ memberId: request.memberId,
2527
+ previousMemberId: memberId(events, previous),
2528
+ revision: request.expectedRevision + 1,
2529
+ handoff
2530
+ };
2531
+ loaded.session.append("room/coordinator", value);
2532
+ await this.ctx.sessions.flush(loaded.session);
2533
+ return {
2534
+ ok: true,
2535
+ value
2536
+ };
2537
+ } catch (error) {
2538
+ return {
2539
+ ok: false,
2540
+ error: {
2541
+ code: "coordinator-not-ready",
2542
+ message: String(error)
2543
+ }
2544
+ };
2545
+ } finally {
2546
+ this.handoffs.delete(request.sessionId);
2547
+ }
2548
+ }
2549
+ /**
2550
+ * Remove a member from the roster. An in-flight run is NOT interrupted
2551
+ * here — cancel() first when the member is running.
2552
+ * @param request - room session, member name.
2553
+ * @returns the removal receipt, or a rejection.
2554
+ */
2555
+ async removeMember(request) {
2556
+ const loaded = await this.ensureLive(request.sessionId);
2557
+ if (!loaded.ok) return {
2558
+ ok: false,
2559
+ error: loaded.error
2560
+ };
2561
+ if (this.handoffs.has(request.sessionId)) return {
2562
+ ok: false,
2563
+ error: { code: "coordinator-busy" }
2564
+ };
2565
+ if (!loaded.state.members.some((member) => member.name === request.name)) return {
2566
+ ok: false,
2567
+ error: { code: "member-not-found" }
2568
+ };
2569
+ if (coordinatorMember(loaded.state, loaded.session.snapshotEvents())?.name === request.name) return {
2570
+ ok: false,
2571
+ error: { code: "active-coordinator" }
2572
+ };
2573
+ if (loaded.state.members.find((member) => member.name === request.name)?.kind === "main-agent") return {
2574
+ ok: false,
2575
+ error: { code: "main-member" }
2576
+ };
2577
+ loaded.session.append("room/member-removed", { name: request.name });
2578
+ await this.ctx.sessions.flush(loaded.session);
2579
+ return {
2580
+ ok: true,
2581
+ value: { name: request.name }
2582
+ };
2583
+ }
2584
+ /**
2585
+ * Post a human @-message into the room: leading `@name` tokens address
2586
+ * members, and so does the request's `targets` list (the composer's
2587
+ * mention-menu picks — explicit addressing wherever the `@name` sits); the
2588
+ * two union before the roster check (a `room/dispatch` journal record,
2589
+ * executed by the engine, and one auto-opened in_progress task per target).
2590
+ * The human's raw text is
2591
+ * FIRST appended as a standard `user/message` (source kind 'user' — the
2592
+ * human typed it; the official messageDefinition classifies any other kind
2593
+ * as a collapsed context-injection row, not the user bubble), so the words
2594
+ * render as the official user bubble and enter the main agent's
2595
+ * model-visible history. The append wakes nothing: turns start from
2596
+ * prompt/followup calls, never from log appends (the agent is the log's
2597
+ * writer, not a watcher). The `room/dispatch` record stays as pure
2598
+ * bookkeeping (task board, dispatch cursors, replay) — it no longer drives
2599
+ * any chat node. A BARE message is a structured `no-targets` rejection —
2600
+ * defense only: the room composer releases bare messages to the official
2601
+ * submit path (a normal main-agent turn) and never calls this Remote
2602
+ * without addressing.
2603
+ * @param request - room session, raw composer text, and the menu-picked addressees.
2604
+ * @returns the parse receipt, or a rejection.
2605
+ */
2606
+ async postMessage(request) {
2607
+ const loaded = await this.ensureLive(request.sessionId);
2608
+ if (!loaded.ok) return {
2609
+ ok: false,
2610
+ error: loaded.error
2611
+ };
2612
+ if (request.text.trim() === "") return {
2613
+ ok: false,
2614
+ error: { code: "empty-text" }
2615
+ };
2616
+ const requestSignature = createHash("sha256").update(JSON.stringify({
2617
+ text: request.text.trim(),
2618
+ targets: [...new Set(request.targets ?? [])].sort()
2619
+ })).digest("hex");
2620
+ if (request.requestId !== void 0) {
2621
+ if (request.requestId.trim() === "" || request.requestId.length > 128) return {
2622
+ ok: false,
2623
+ error: { code: "request-conflict" }
2624
+ };
2625
+ const previous = loaded.session.snapshotEvents().find((event) => event.type === "room/dispatch" && event.data.id === request.requestId);
2626
+ if (previous?.type === "room/dispatch") {
2627
+ const original = previous.data;
2628
+ const parsedRetry = parseMentions(request.text);
2629
+ const retryTargets = [.../* @__PURE__ */ new Set([...parsedRetry.targets, ...request.targets ?? []])].sort();
2630
+ const legacyMatch = original.origin === "human" && original.text === (parsedRetry.targets.length > 0 ? parsedRetry.text : request.text.trim()) && (retryTargets.length === 0 || JSON.stringify(retryTargets) === JSON.stringify([...original.targets].sort()));
2631
+ if (original.requestSignature === void 0 ? !legacyMatch : original.requestSignature !== requestSignature) return {
2632
+ ok: false,
2633
+ error: { code: "request-conflict" }
2634
+ };
2635
+ await this.ctx.sessions.flush(loaded.session);
2636
+ for (const target of original.targetIds ?? []) {
2637
+ const member = replay(loaded.session.snapshotEvents()).members.find((member) => memberId(loaded.session.snapshotEvents(), member) === target);
2638
+ if (member !== void 0) this.engine.dispatch(loaded.session, member.name, original.text, {
2639
+ dispatchSeq: previous.seq,
2640
+ targetId: target
2641
+ });
2642
+ }
2643
+ return {
2644
+ ok: true,
2645
+ value: {
2646
+ parsed: {
2647
+ targets: original.targets,
2648
+ text: original.text
2649
+ },
2650
+ seq: previous.seq
2651
+ }
2652
+ };
2653
+ }
2654
+ }
2655
+ const parsed = parseMentions(request.text);
2656
+ const targets = [.../* @__PURE__ */ new Set([...parsed.targets, ...request.targets ?? []])];
2657
+ if (this.handoffs.has(request.sessionId)) return {
2658
+ ok: false,
2659
+ error: { code: "coordinator-busy" }
2660
+ };
2661
+ if (targets.length === 0) {
2662
+ const selected = coordinatorMember(loaded.state, loaded.session.snapshotEvents());
2663
+ if (selected === void 0) return {
2664
+ ok: false,
2665
+ error: { code: "member-not-found" }
2666
+ };
2667
+ targets.push(selected.name);
2668
+ }
2669
+ const text = parsed.targets.length > 0 ? parsed.text : request.text.trim();
2670
+ if (text === "") return {
2671
+ ok: false,
2672
+ error: { code: "empty-text" }
2673
+ };
2674
+ const roster = new Set(loaded.state.members.map((member) => member.name));
2675
+ const unknown = targets.filter((target) => !roster.has(target));
2676
+ if (unknown.length > 0) return {
2677
+ ok: false,
2678
+ error: {
2679
+ code: "unknown-targets",
2680
+ names: unknown
2681
+ }
2682
+ };
2683
+ loaded.session.append("user/message", createUserMessage({
2684
+ content: [{
2685
+ type: "text",
2686
+ text: request.text.trim()
2687
+ }],
2688
+ source: { kind: "user" }
2689
+ }), { surfaceOp: "append" });
2690
+ const dispatch = loaded.session.append("room/dispatch", {
2691
+ id: request.requestId ?? randomUUID(),
2692
+ requestSignature,
2693
+ targets,
2694
+ text,
2695
+ origin: "human",
2696
+ targetIds: targets.map((name) => memberId(loaded.session.snapshotEvents(), loaded.state.members.find((member) => member.name === name)))
2697
+ });
2698
+ for (const target of targets) loaded.session.append("room/task-added", {
2699
+ id: randomUUID(),
2700
+ member: target,
2701
+ title: taskTitle(text),
2702
+ status: "in_progress",
2703
+ deliveryId: `${dispatch.seq}:${memberId(loaded.session.snapshotEvents(), loaded.state.members.find((member) => member.name === target))}`
2704
+ });
2705
+ await this.ctx.sessions.flush(loaded.session);
2706
+ for (const target of targets) this.engine.dispatch(loaded.session, target, text, { dispatchSeq: dispatch.seq });
2707
+ return {
2708
+ ok: true,
2709
+ value: {
2710
+ parsed: {
2711
+ targets,
2712
+ text
2713
+ },
2714
+ seq: dispatch.seq
2715
+ }
2716
+ };
2717
+ }
2718
+ /**
2719
+ * Dispatch one message to one member (host-only — the `room_message` tool's
2720
+ * path; the model-facing equivalent of the human's `@member text`). Same
2721
+ * dispatch internals as postMessage minus the user/message bubble: the
2722
+ * caller is the room's own main agent, so the text must NOT wear the human
2723
+ * bubble (source kind 'user' would misattribute it). The dispatch record
2724
+ * and the auto-opened in_progress task journal as usual.
2725
+ * @param request - room session, addressee, text.
2726
+ * @returns the dispatch receipt, or a rejection.
2727
+ */
2728
+ async messageMember(request, actorChildSessionId) {
2729
+ const loaded = await this.ensureRoom(request.sessionId);
2730
+ if (!loaded.ok) return {
2731
+ ok: false,
2732
+ error: loaded.error
2733
+ };
2734
+ if (!this.isCurrentCoordinator(loaded.session, actorChildSessionId)) return {
2735
+ ok: false,
2736
+ error: { code: "not-coordinator" }
2737
+ };
2738
+ if (this.handoffs.has(request.sessionId)) return {
2739
+ ok: false,
2740
+ error: { code: "coordinator-busy" }
2741
+ };
2742
+ if (!loaded.state.members.some((member) => member.name === request.member)) return {
2743
+ ok: false,
2744
+ error: { code: "member-not-found" }
2745
+ };
2746
+ const text = request.text.trim();
2747
+ if (text === "") return {
2748
+ ok: false,
2749
+ error: { code: "empty-text" }
2750
+ };
2751
+ const coordinator = coordinatorMember(loaded.state, loaded.session.snapshotEvents());
2752
+ const target = loaded.state.members.find((member) => member.name === request.member);
2753
+ const dispatch = loaded.session.append("room/dispatch", {
2754
+ id: randomUUID(),
2755
+ targets: [request.member],
2756
+ targetIds: [memberId(loaded.session.snapshotEvents(), target)],
2757
+ text,
2758
+ origin: "coordinator",
2759
+ ...coordinator === void 0 || coordinator.name === request.member ? {} : { replyTo: memberId(loaded.session.snapshotEvents(), coordinator) }
2760
+ });
2761
+ loaded.session.append("room/task-added", {
2762
+ id: randomUUID(),
2763
+ member: request.member,
2764
+ title: taskTitle(text),
2765
+ status: "in_progress",
2766
+ deliveryId: `${dispatch.seq}:${memberId(loaded.session.snapshotEvents(), target)}`
2767
+ });
2768
+ await this.ctx.sessions.flush(loaded.session);
2769
+ this.engine.dispatch(loaded.session, request.member, text, { dispatchSeq: dispatch.seq });
2770
+ return {
2771
+ ok: true,
2772
+ value: { member: request.member }
2773
+ };
2774
+ }
2775
+ /** Host-authenticated actor; model tool arguments never provide the room or sender. */
2776
+ isCurrentCoordinator(session, actorChildSessionId) {
2777
+ const events = session.snapshotEvents();
2778
+ const coordinator = coordinatorMember(replay(events), events);
2779
+ return actorChildSessionId === void 0 ? coordinator?.kind === "main-agent" : coordinator?.childSessionId === actorChildSessionId;
2780
+ }
2781
+ /** Bounded shared room context for native and external model tools; reading never wakes an agent. */
2782
+ async readRoomContext(sessionId) {
2783
+ const state = await this.getState({ sessionId: SessionId(sessionId) });
2784
+ if (!state.ok) throw new Error(`Room unavailable: ${state.error.code}`);
2785
+ const recent = (this.ctx.sessions.get(SessionId(sessionId))?.snapshotEvents() ?? []).filter((event) => event.type === "room/speech").slice(-12).map((event) => event.type === "room/speech" ? {
2786
+ member: event.data.member,
2787
+ text: event.data.text.slice(0, 2e3)
2788
+ } : null);
2789
+ return JSON.stringify({
2790
+ planCommands: planCommandContract(),
2791
+ coordinator: coordinatorMember(state.value)?.name,
2792
+ state: {
2793
+ ...state.value,
2794
+ deliveries: state.value.deliveries?.slice(-30),
2795
+ tasks: state.value.tasks.slice(-100),
2796
+ relays: state.value.relays.slice(-30)
2797
+ },
2798
+ providers: await this.listProviders({}),
2799
+ recent
2800
+ });
2801
+ }
2802
+ /** Human UI and native/external tools share the same durable transition service. */
2803
+ async planCommand(request) {
2804
+ try {
2805
+ const loaded = await this.ensureLive(request.sessionId);
2806
+ if (!loaded.ok) return {
2807
+ ok: false,
2808
+ message: loaded.error.code
2809
+ };
2810
+ if (this.handoffs.has(request.sessionId)) return {
2811
+ ok: false,
2812
+ message: "Coordinator handoff is in progress"
2813
+ };
2814
+ await this.plans.command(loaded.session, request.command, {
2815
+ kind: "human",
2816
+ memberId: "human"
2817
+ });
2818
+ return { ok: true };
2819
+ } catch (error) {
2820
+ return {
2821
+ ok: false,
2822
+ message: String(error)
2823
+ };
2824
+ }
2825
+ }
2826
+ /** The actor is derived from the calling native agent or authenticated bridge. */
2827
+ async commandPlan(sessionId, command, actorChildSessionId) {
2828
+ const loaded = await this.ensureLive(SessionId(sessionId));
2829
+ if (!loaded.ok) throw new Error(loaded.error.code);
2830
+ if (this.handoffs.has(sessionId)) throw new Error("Coordinator handoff is in progress");
2831
+ const events = loaded.session.snapshotEvents();
2832
+ const state = replay(events);
2833
+ const member = state.members.find((member) => actorChildSessionId === void 0 ? member.kind === "main-agent" : member.childSessionId === actorChildSessionId);
2834
+ if (member === void 0) throw new Error("Calling member is not in this room");
2835
+ const id = memberId(events, member);
2836
+ const { requests: _requests, ...view } = await this.plans.command(loaded.session, command, {
2837
+ kind: coordinatorMember(state, events)?.name === member.name ? "coordinator" : "worker",
2838
+ memberId: id
2839
+ });
2840
+ return JSON.stringify(view);
2841
+ }
2842
+ /** Shared backend for external MCP room tools. Not exposed as a browser Remote. */
2843
+ async receiveMemberCommand(actor, command) {
2844
+ try {
2845
+ const loaded = await this.ensureLive(SessionId(actor.parentSessionId));
2846
+ if (!loaded.ok) return {
2847
+ ok: false,
2848
+ error: `Room unavailable: ${loaded.error.code}`
2849
+ };
2850
+ const sender = loaded.state.members.find((member) => member.childSessionId === actor.childSessionId && member.provider === actor.provider);
2851
+ if (sender === void 0) return {
2852
+ ok: false,
2853
+ error: "The authenticated member is not in this room roster"
2854
+ };
2855
+ if (command === null || typeof command !== "object" || command.arguments === null || typeof command.arguments !== "object" || Array.isArray(command.arguments)) return {
2856
+ ok: false,
2857
+ error: "Malformed room command"
2858
+ };
2859
+ const args = command.arguments;
2860
+ const keys = {
2861
+ room_plan: ["command"],
2862
+ room_read: [],
2863
+ room_invite: [
2864
+ "provider",
2865
+ "name",
2866
+ "instructions",
2867
+ "cwd",
2868
+ "model",
2869
+ "firstTask"
2870
+ ],
2871
+ room_message: ["member", "text"]
2872
+ }[command.name];
2873
+ if (keys === void 0 || Object.keys(args).some((key) => !keys.includes(key))) return {
2874
+ ok: false,
2875
+ error: "Unknown room command or argument; room and actor identity are host-owned"
2876
+ };
2877
+ const required = (key) => {
2878
+ const value = args[key];
2879
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`A non-empty ${key} is required`);
2880
+ return value;
2881
+ };
2882
+ const optional = (key) => args[key] === void 0 ? void 0 : required(key);
2883
+ if (command.name === "room_read") return {
2884
+ ok: true,
2885
+ receipt: JSON.stringify({
2886
+ self: sender.name,
2887
+ ...JSON.parse(await this.readRoomContext(loaded.session.id))
2888
+ })
2889
+ };
2890
+ if (command.name === "room_plan") return {
2891
+ ok: true,
2892
+ receipt: await this.commandPlan(loaded.session.id, required("command"), actor.childSessionId)
2893
+ };
2894
+ if (!this.isCurrentCoordinator(loaded.session, actor.childSessionId)) return {
2895
+ ok: false,
2896
+ error: "Only the current coordinator may invite members or dispatch work"
2897
+ };
2898
+ const result = command.name === "room_invite" ? await this.inviteMember({
2899
+ sessionId: loaded.session.id,
2900
+ provider: required("provider"),
2901
+ name: required("name"),
2902
+ ...optional("instructions") === void 0 ? {} : { instructions: optional("instructions") },
2903
+ ...optional("cwd") === void 0 ? {} : { cwd: optional("cwd") },
2904
+ ...optional("model") === void 0 ? {} : { model: optional("model") },
2905
+ ...optional("firstTask") === void 0 ? {} : { firstTask: optional("firstTask") }
2906
+ }, "agent", actor.childSessionId) : await this.messageMember({
2907
+ sessionId: loaded.session.id,
2908
+ member: required("member"),
2909
+ text: required("text")
2910
+ }, actor.childSessionId);
2911
+ return result.ok ? {
2912
+ ok: true,
2913
+ receipt: JSON.stringify(result.value)
2914
+ } : {
2915
+ ok: false,
2916
+ error: result.error.code
2917
+ };
2918
+ } catch (error) {
2919
+ return {
2920
+ ok: false,
2921
+ error: String(error)
2922
+ };
2923
+ }
2924
+ }
2925
+ /**
2926
+ * The notification gate entry, verbatim-frozen contract for the local-agent
2927
+ * family bridge (`ctx.get('room')` + duck-typed call). The call crosses an
2928
+ * untyped package boundary, so the message shape is validated at runtime:
2929
+ * a malformed message throws (the bridge treats a rejection as "room does
2930
+ * not claim this" and delivers directly) instead of poisoning the journal.
2931
+ * `from`/`to` arrive as roster names OR member child session ids (the
2932
+ * bridge cannot speak names for the sender — only room owns the roster);
2933
+ * both are normalized to roster names when resolvable, so the pending card
2934
+ * and the delivery text (`{from} 给你的通知: …`) read as names. Phase 1's
2935
+ * gate is ALWAYS human confirmation: the relay is journaled pending and
2936
+ * the sender receives 'pending-confirm' (never 'sent' — the bridge keeps
2937
+ * the member's conclusion honest). A non-room parent session throws.
2938
+ * @param request - sender, recipient, content, the shared parent session, provenance.
2939
+ * @returns the gate receipt.
2940
+ */
2941
+ async receiveMemberMessage(request) {
2942
+ if (typeof request?.from !== "string" || typeof request.to !== "string" || typeof request.content !== "string" || typeof request.parentSessionId !== "string") throw new Error("room: malformed member message (from/to/content/parentSessionId must be strings)");
2943
+ const loaded = await this.ensureLive(request.parentSessionId);
2944
+ if (!loaded.ok) throw new Error(`room: not a room session (${loaded.error.code})`);
2945
+ if (loaded.state.members.find((member) => member.childSessionId === request.from && member.provider === request.provenance?.provider) !== void 0 && this.isCurrentCoordinator(loaded.session, request.from)) {
2946
+ const target = loaded.state.members.find((member) => member.name === request.to || member.childSessionId === request.to);
2947
+ if (target === void 0) throw new Error("Unknown room member");
2948
+ const result = await this.messageMember({
2949
+ sessionId: loaded.session.id,
2950
+ member: target.name,
2951
+ text: request.content
2952
+ }, request.from);
2953
+ if (!result.ok) throw new Error(result.error.code);
2954
+ return "sent";
2955
+ }
2956
+ const resolveName = (endpoint) => {
2957
+ const byName = loaded.state.members.find((member) => member.name === endpoint);
2958
+ if (byName !== void 0) return byName.name;
2959
+ return loaded.state.members.find((member) => member.childSessionId === endpoint)?.name ?? endpoint;
2960
+ };
2961
+ loaded.session.append("room/relay", {
2962
+ id: randomUUID(),
2963
+ from: resolveName(request.from),
2964
+ to: resolveName(request.to),
2965
+ content: request.content,
2966
+ ...request.provenance === void 0 ? {} : { provenance: request.provenance }
2967
+ });
2968
+ await this.ctx.sessions.flush(loaded.session);
2969
+ return "pending-confirm";
2970
+ }
2971
+ /**
2972
+ * Confirm a pending relay: journal the confirmed edge and dispatch the
2973
+ * notification to the recipient as a continuation (`{from} 给你的通知:
2974
+ * {content}`); the engine marks the relay sent once the recipient's own
2975
+ * session receives the prompt.
2976
+ * @param request - room session, relay id.
2977
+ * @returns the resolution receipt, or a rejection.
2978
+ */
2979
+ async confirmRelay(request) {
2980
+ const loaded = await this.ensureLive(request.sessionId);
2981
+ if (!loaded.ok) return {
2982
+ ok: false,
2983
+ error: loaded.error
2984
+ };
2985
+ const relay = loaded.state.relays.find((entry) => entry.id === request.relayId);
2986
+ if (relay === void 0) return {
2987
+ ok: false,
2988
+ error: { code: "relay-not-found" }
2989
+ };
2990
+ if (relay.state !== "pending") return {
2991
+ ok: false,
2992
+ error: { code: "relay-not-pending" }
2993
+ };
2994
+ if (!loaded.state.members.some((member) => member.name === relay.to)) return {
2995
+ ok: false,
2996
+ error: { code: "member-not-found" }
2997
+ };
2998
+ loaded.session.append("room/relay-resolved", {
2999
+ id: relay.id,
3000
+ state: "confirmed"
3001
+ });
3002
+ await this.ctx.sessions.flush(loaded.session);
3003
+ this.engine.dispatch(loaded.session, relay.to, `${relay.from} 给你的通知: ${relay.content}`, { relayIds: [relay.id] });
3004
+ return {
3005
+ ok: true,
3006
+ value: { relayId: relay.id }
3007
+ };
3008
+ }
3009
+ /**
3010
+ * Dismiss a pending relay: the notification never reaches anyone.
3011
+ * @param request - room session, relay id.
3012
+ * @returns the resolution receipt, or a rejection.
3013
+ */
3014
+ async dismissRelay(request) {
3015
+ const loaded = await this.ensureLive(request.sessionId);
3016
+ if (!loaded.ok) return {
3017
+ ok: false,
3018
+ error: loaded.error
3019
+ };
3020
+ const relay = loaded.state.relays.find((entry) => entry.id === request.relayId);
3021
+ if (relay === void 0) return {
3022
+ ok: false,
3023
+ error: { code: "relay-not-found" }
3024
+ };
3025
+ if (relay.state !== "pending") return {
3026
+ ok: false,
3027
+ error: { code: "relay-not-pending" }
3028
+ };
3029
+ loaded.session.append("room/relay-resolved", {
3030
+ id: relay.id,
3031
+ state: "dismissed"
3032
+ });
3033
+ await this.ctx.sessions.flush(loaded.session);
3034
+ return {
3035
+ ok: true,
3036
+ value: { relayId: relay.id }
3037
+ };
3038
+ }
3039
+ /**
3040
+ * Add a task to a member's board lane (the human's management entry; @-
3041
+ * dispatches auto-open in_progress tasks, this opens a pending one).
3042
+ * `blockedBy` names the member the task waits on (display only — it never
3043
+ * dispatches anything); it must resolve against the roster.
3044
+ * @param request - room session, member, title, optional blockedBy.
3045
+ * @returns the new task's id, or a rejection.
3046
+ */
3047
+ async addTask(request) {
3048
+ const loaded = await this.ensureLive(request.sessionId);
3049
+ if (!loaded.ok) return {
3050
+ ok: false,
3051
+ error: loaded.error
3052
+ };
3053
+ if (!loaded.state.members.some((member) => member.name === request.member)) return {
3054
+ ok: false,
3055
+ error: { code: "member-not-found" }
3056
+ };
3057
+ if (request.title.trim() === "") return {
3058
+ ok: false,
3059
+ error: { code: "empty-text" }
3060
+ };
3061
+ if (request.blockedBy !== void 0 && !loaded.state.members.some((member) => member.name === request.blockedBy)) return {
3062
+ ok: false,
3063
+ error: { code: "member-not-found" }
3064
+ };
3065
+ const id = randomUUID();
3066
+ loaded.session.append("room/task-added", {
3067
+ id,
3068
+ member: request.member,
3069
+ title: request.title.trim(),
3070
+ status: "pending",
3071
+ ...request.blockedBy === void 0 ? {} : { blockedBy: request.blockedBy }
3072
+ });
3073
+ await this.ctx.sessions.flush(loaded.session);
3074
+ return {
3075
+ ok: true,
3076
+ value: { id }
3077
+ };
3078
+ }
3079
+ /**
3080
+ * Close an open task (done by default, or cancelled). A failed task is
3081
+ * still open for closing — that is the human's dismiss path after a
3082
+ * failed run (the board's [关闭] passes 'cancelled').
3083
+ * @param request - room session, task id, optional closing status.
3084
+ * @returns the closed task's id, or a rejection.
3085
+ */
3086
+ async closeTask(request) {
3087
+ const loaded = await this.ensureLive(request.sessionId);
3088
+ if (!loaded.ok) return {
3089
+ ok: false,
3090
+ error: loaded.error
3091
+ };
3092
+ const task = loaded.state.tasks.find((entry) => entry.id === request.taskId);
3093
+ if (task === void 0) return {
3094
+ ok: false,
3095
+ error: { code: "task-not-found" }
3096
+ };
3097
+ if (task.status === "done" || task.status === "cancelled") return {
3098
+ ok: false,
3099
+ error: { code: "task-closed" }
3100
+ };
3101
+ loaded.session.append("room/task-updated", {
3102
+ id: task.id,
3103
+ status: request.status ?? "done"
3104
+ });
3105
+ await this.ctx.sessions.flush(loaded.session);
3106
+ return {
3107
+ ok: true,
3108
+ value: { id: task.id }
3109
+ };
3110
+ }
3111
+ /**
3112
+ * Edit an open task's title and/or blockedBy (the `room_task` tool's update
3113
+ * action; journaled as `room/task-edited`, status changes stay on
3114
+ * closeTask). Host method only — the capsule UI edits nothing but the goal.
3115
+ * `blockedBy: null` clears the wait; a string must resolve against the
3116
+ * roster. A closed task takes no edits.
3117
+ * @param request - room session, task id, and at least one edited field.
3118
+ * @returns the edited task's id, or a rejection.
3119
+ */
3120
+ async updateTask(request) {
3121
+ const loaded = await this.ensureLive(request.sessionId);
3122
+ if (!loaded.ok) return {
3123
+ ok: false,
3124
+ error: loaded.error
3125
+ };
3126
+ const task = loaded.state.tasks.find((entry) => entry.id === request.taskId);
3127
+ if (task === void 0) return {
3128
+ ok: false,
3129
+ error: { code: "task-not-found" }
3130
+ };
3131
+ if (task.status === "done" || task.status === "cancelled") return {
3132
+ ok: false,
3133
+ error: { code: "task-closed" }
3134
+ };
3135
+ if (request.title === void 0 && request.blockedBy === void 0) return {
3136
+ ok: false,
3137
+ error: { code: "nothing-to-update" }
3138
+ };
3139
+ if (request.title !== void 0 && request.title.trim() === "") return {
3140
+ ok: false,
3141
+ error: { code: "empty-text" }
3142
+ };
3143
+ if (typeof request.blockedBy === "string" && !loaded.state.members.some((member) => member.name === request.blockedBy)) return {
3144
+ ok: false,
3145
+ error: { code: "member-not-found" }
3146
+ };
3147
+ loaded.session.append("room/task-edited", {
3148
+ id: task.id,
3149
+ ...request.title === void 0 ? {} : { title: request.title.trim() },
3150
+ ...request.blockedBy === void 0 ? {} : { blockedBy: request.blockedBy }
3151
+ });
3152
+ await this.ctx.sessions.flush(loaded.session);
3153
+ return {
3154
+ ok: true,
3155
+ value: { id: task.id }
3156
+ };
3157
+ }
3158
+ /**
3159
+ * Set (or, with a blank text, clear) the room's goal. The goal is one
3160
+ * journaled line — the latest `room/goal` event wins; it rides the top of
3161
+ * the roster section in every member prompt and drives the dock's goal
3162
+ * capsule. Pure display/coordination text: it never dispatches anything.
3163
+ * @param request - room session and goal text.
3164
+ * @returns the now-current goal (absent after a clear), or a rejection.
3165
+ */
3166
+ async setGoal(request) {
3167
+ const loaded = await this.ensureLive(request.sessionId);
3168
+ if (!loaded.ok) return {
3169
+ ok: false,
3170
+ error: loaded.error
3171
+ };
3172
+ const text = request.text.trim();
3173
+ loaded.session.append("room/goal", { text });
3174
+ await this.ctx.sessions.flush(loaded.session);
3175
+ return {
3176
+ ok: true,
3177
+ value: text === "" ? {} : { goal: text }
3178
+ };
3179
+ }
3180
+ /**
3181
+ * Cancel a member's in-flight run through the local-agent facade. A hit
3182
+ * journals the terminal `cancelled` edge immediately (the engine's own
3183
+ * settle for that run then no-ops); a miss — no facade, no delegation
3184
+ * handle, or nothing in flight — journals nothing and reports
3185
+ * `cancelled: false`.
3186
+ * @param request - room session, member name.
3187
+ * @returns the cancellation receipt, or a rejection.
3188
+ */
3189
+ async cancel(request) {
3190
+ const loaded = await this.ensureLive(request.sessionId);
3191
+ if (!loaded.ok) return {
3192
+ ok: false,
3193
+ error: loaded.error
3194
+ };
3195
+ const member = loaded.state.members.find((entry) => entry.name === request.name);
3196
+ if (member === void 0) return {
3197
+ ok: false,
3198
+ error: { code: "member-not-found" }
3199
+ };
3200
+ const facade = probeLocalAgent(this.ctx);
3201
+ if (facade !== void 0 && member.childSessionId !== void 0 ? facade.cancel(member.childSessionId) : false) {
3202
+ const running = loaded.state.runs.find((entry) => entry.member === request.name && entry.state === "running");
3203
+ if (running === void 0) return {
3204
+ ok: true,
3205
+ value: { cancelled: true }
3206
+ };
3207
+ loaded.session.append("room/run-state", {
3208
+ member: request.name,
3209
+ state: "cancelled",
3210
+ startedAt: running.startedAt,
3211
+ ...running.runId === void 0 ? {} : { runId: running.runId }
3212
+ });
3213
+ for (const task of loaded.state.tasks) if (task.member === request.name && task.status === "in_progress" && (running.runId === void 0 || task.deliveryId === running.runId)) loaded.session.append("room/task-updated", {
3214
+ id: task.id,
3215
+ status: "cancelled"
3216
+ });
3217
+ await this.ctx.sessions.flush(loaded.session);
3218
+ return {
3219
+ ok: true,
3220
+ value: { cancelled: true }
3221
+ };
3222
+ }
3223
+ return {
3224
+ ok: true,
3225
+ value: { cancelled: false }
3226
+ };
3227
+ }
3228
+ };
3229
+ })();
3230
+ //#endregion
3231
+ export { MAIN_AGENT_MEMBER, ROOM_EVENT_TYPES, RoomService, RoomService as default };