@axiom-lattice/gateway 4.2.1 → 4.2.3

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 (39) hide show
  1. package/dist/{a2a-standard-VEQBXBMM.mjs → a2a-standard-ALXWP7V6.mjs} +2 -2
  2. package/dist/chunk-EMYRCJOI.mjs +99 -0
  3. package/dist/chunk-EMYRCJOI.mjs.map +1 -0
  4. package/dist/{chunk-J4BK77PA.mjs → chunk-YKTXW6F6.mjs} +11 -1
  5. package/dist/chunk-YKTXW6F6.mjs.map +1 -0
  6. package/dist/embed/{embed-page-AgentDetailSideApp-KGYKTMZR.js → embed-page-AgentDetailSideApp-3LBCEKFU.js} +1 -1
  7. package/dist/embed/{embed-page-AgentGovernanceWorkbench-F42AJH77.js → embed-page-AgentGovernanceWorkbench-K3JUQNBW.js} +1 -1
  8. package/dist/embed/{embed-page-MessageGraph-ACHQORG7.js → embed-page-MessageGraph-3X4ROEN6.js} +1 -1
  9. package/dist/embed/{embed-page-ReactChart-FGUHXBED.js → embed-page-ReactChart-F6VYCIYY.js} +1 -1
  10. package/dist/embed/{embed-page-SkillFlow-SFZ5HLMK.js → embed-page-SkillFlow-3CF7R3CW.js} +1 -1
  11. package/dist/embed/{embed-page-TasksSideApp-4GY5YBTN.js → embed-page-TasksSideApp-3WRDBZU3.js} +1 -1
  12. package/dist/embed/{embed-page-TeamWorkspace-YWE6ZTLB.js → embed-page-TeamWorkspace-6JSMZYUF.js} +1 -1
  13. package/dist/embed/{embed-page-TopologyRuntimeView-MKFPCHT3.js → embed-page-TopologyRuntimeView-YDFENLHH.js} +1 -1
  14. package/dist/embed/{embed-page-WorkflowAutomationView-WT6VH5HD.js → embed-page-WorkflowAutomationView-MP5B7Q2Q.js} +1 -1
  15. package/dist/embed/{embed-page-chunk-67U2KZFL.js → embed-page-chunk-B2E2XOH2.js} +10 -10
  16. package/dist/embed/{embed-page-chunk-FU2KMTSK.js → embed-page-chunk-FFO7CAYL.js} +1 -1
  17. package/dist/embed/{embed-page-chunk-MGZK7ELC.js → embed-page-chunk-GDTBDOUE.js} +354 -187
  18. package/dist/embed/{embed-page-chunk-3WXEO6CG.js → embed-page-chunk-IRP6JSSX.js} +1 -1
  19. package/dist/embed/{embed-page-chunk-3PY2YGBU.js → embed-page-chunk-PAJ5NG2Z.js} +1 -1
  20. package/dist/embed/{embed-page-chunk-HS5VQFWH.js → embed-page-chunk-QEYWJHEK.js} +1 -1
  21. package/dist/embed/{embed-page-chunk-N46WJ2FS.js → embed-page-chunk-RVXX5HKF.js} +1 -1
  22. package/dist/embed/{embed-page-chunk-FDID7LZP.js → embed-page-chunk-UOL6CUAL.js} +1 -1
  23. package/dist/embed/{embed-page-chunk-74BPRLSV.js → embed-page-chunk-URNZX66S.js} +5 -5
  24. package/dist/embed/{embed-page-chunk-42S7N5Y5.js → embed-page-chunk-YMJGPA3F.js} +163 -145
  25. package/dist/embed/embed-page.js +1 -1
  26. package/dist/index.d.mts +10 -0
  27. package/dist/index.d.ts +10 -0
  28. package/dist/index.js +1748 -873
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +1238 -432
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{resources-SRQRKW5G.mjs → resources-JFKAPS45.mjs} +16 -10
  33. package/dist/resources-JFKAPS45.mjs.map +1 -0
  34. package/package.json +9 -7
  35. package/dist/chunk-J4BK77PA.mjs.map +0 -1
  36. package/dist/chunk-LHQY46YB.mjs +0 -47
  37. package/dist/chunk-LHQY46YB.mjs.map +0 -1
  38. package/dist/resources-SRQRKW5G.mjs.map +0 -1
  39. /package/dist/{a2a-standard-VEQBXBMM.mjs.map → a2a-standard-ALXWP7V6.mjs.map} +0 -0
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  getContentTypeFromFilename,
3
- getFilenameFromPath
4
- } from "./chunk-LHQY46YB.mjs";
3
+ getFilenameFromPath,
4
+ sendBufferWithRange
5
+ } from "./chunk-EMYRCJOI.mjs";
5
6
  import {
6
7
  buildPreviewTokenScript
7
8
  } from "./chunk-2POW27EI.mjs";
@@ -26,7 +27,7 @@ import {
26
27
  isPathWithinRoot,
27
28
  resolvePathWithinRoot,
28
29
  saveProjectFile
29
- } from "./chunk-J4BK77PA.mjs";
30
+ } from "./chunk-YKTXW6F6.mjs";
30
31
  import "./chunk-NFEGQTCC.mjs";
31
32
 
32
33
  // src/index.ts
@@ -40,10 +41,10 @@ import staticPlugin from "@fastify/static";
40
41
  import path4 from "path";
41
42
 
42
43
  // src/routes/index.ts
43
- import { randomUUID as randomUUID19 } from "crypto";
44
+ import { randomUUID as randomUUID22 } from "crypto";
44
45
  import {
45
46
  descriptorDataValue as descriptorDataValue3,
46
- snapshotExactRecord as snapshotExactRecord13,
47
+ snapshotExactRecord as snapshotExactRecord14,
47
48
  ProjectTaskStoreUnsupportedError,
48
49
  requireProjectTaskWorkItemStore
49
50
  } from "@axiom-lattice/protocols";
@@ -1006,7 +1007,8 @@ var createRun = async (request, reply) => {
1006
1007
  type: "error",
1007
1008
  data: {
1008
1009
  id: v4(),
1009
- content: error instanceof Error ? error.message : "Stream processing error"
1010
+ content: error instanceof Error ? error.message : "Stream processing error",
1011
+ code: error?.code ?? "stream_error"
1010
1012
  }
1011
1013
  }]
1012
1014
  });
@@ -1077,7 +1079,8 @@ var resumeStream = async (request, reply) => {
1077
1079
  type: "error",
1078
1080
  data: {
1079
1081
  id: v4(),
1080
- content: error instanceof Error ? error.message : "Resume stream processing error"
1082
+ content: error instanceof Error ? error.message : "Resume stream processing error",
1083
+ code: error?.code ?? "stream_error"
1081
1084
  }
1082
1085
  }]
1083
1086
  });
@@ -5103,8 +5106,9 @@ function registerSandboxProxyRoutes(app2) {
5103
5106
  try {
5104
5107
  const buf = await sandbox.file.downloadFile({ file: resolvedPath });
5105
5108
  const contentDisposition = `inline; filename="${filename.replace(/"/g, '\\"')}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
5106
- reply = reply.status(200).type(inferredContentType).header("Content-Disposition", contentDisposition).send(buf);
5107
- return reply;
5109
+ return sendBufferWithRange(reply, buf, inferredContentType, request.headers.range, {
5110
+ "Content-Disposition": contentDisposition
5111
+ });
5108
5112
  } catch (err) {
5109
5113
  reply.status(500).send({ error: String(err) });
5110
5114
  return;
@@ -5128,7 +5132,19 @@ import { getSandBoxManager as getSandBoxManager3 } from "@axiom-lattice/core";
5128
5132
  import { v4 as uuidv4 } from "uuid";
5129
5133
 
5130
5134
  // src/services/collaboration/RoomCoordinatorProvisioner.ts
5135
+ import { roomCoordinatorConfig } from "@axiom-lattice/core";
5131
5136
  var ROOM_COORDINATOR_KEY = "room-coordinator";
5137
+ var ROOM_COORDINATOR_ASSISTANT_INPUT = {
5138
+ name: "Room Coordinator",
5139
+ description: "Coordinates project room conversations and organizes room work.",
5140
+ graphDefinition: roomCoordinatorConfig
5141
+ };
5142
+ function needsConfigRepair(assistant) {
5143
+ const graph = assistant.graphDefinition;
5144
+ if (typeof graph !== "object" || graph === null || Array.isArray(graph)) return true;
5145
+ const record = graph;
5146
+ return typeof record.type !== "string" || !Array.isArray(record.middleware);
5147
+ }
5132
5148
  var RoomCoordinatorProvisioner = class {
5133
5149
  /** Creates a provisioner over the tenant-scoped assistant store operations. */
5134
5150
  constructor(deps) {
@@ -5143,13 +5159,17 @@ var RoomCoordinatorProvisioner = class {
5143
5159
  */
5144
5160
  async ensure(tenantId) {
5145
5161
  const existing = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
5146
- if (existing) return existing;
5147
- try {
5148
- return await this.deps.createAssistant(tenantId, ROOM_COORDINATOR_KEY, {
5149
- name: "Room Coordinator",
5150
- description: "Coordinates project room conversations and organizes room work.",
5151
- graphDefinition: {}
5162
+ if (existing) {
5163
+ if (!needsConfigRepair(existing)) return existing;
5164
+ const repaired = await this.deps.updateAssistant(tenantId, ROOM_COORDINATOR_KEY, {
5165
+ name: existing.name ?? ROOM_COORDINATOR_ASSISTANT_INPUT.name,
5166
+ description: existing.description ?? ROOM_COORDINATOR_ASSISTANT_INPUT.description,
5167
+ graphDefinition: ROOM_COORDINATOR_ASSISTANT_INPUT.graphDefinition
5152
5168
  });
5169
+ return repaired ?? existing;
5170
+ }
5171
+ try {
5172
+ return await this.deps.createAssistant(tenantId, ROOM_COORDINATOR_KEY, ROOM_COORDINATOR_ASSISTANT_INPUT);
5153
5173
  } catch (error) {
5154
5174
  const winner = await this.deps.getAssistantById(tenantId, ROOM_COORDINATOR_KEY);
5155
5175
  if (winner) return winner;
@@ -5158,9 +5178,134 @@ var RoomCoordinatorProvisioner = class {
5158
5178
  }
5159
5179
  };
5160
5180
 
5181
+ // src/services/collaboration/RoomCoordinatorEnsurer.ts
5182
+ import { randomUUID as randomUUID5 } from "crypto";
5183
+
5184
+ // src/services/collaboration/ProjectMutationMutex.ts
5185
+ import {
5186
+ ProjectMutationMutex,
5187
+ projectMutationMutex
5188
+ } from "@axiom-lattice/core";
5189
+
5190
+ // src/services/collaboration/RoomPresenceThread.ts
5191
+ var ROOM_PRESENCE_METADATA_KEYS = ["source", "kind", "workspaceId", "projectId", "roomId", "membershipId", "assistantId"];
5192
+ function roomPresenceMetadata(input) {
5193
+ return {
5194
+ source: "project_room",
5195
+ kind: "room_presence",
5196
+ workspaceId: input.workspaceId,
5197
+ projectId: input.projectId,
5198
+ roomId: input.roomId,
5199
+ membershipId: input.membershipId,
5200
+ assistantId: input.assistantId
5201
+ };
5202
+ }
5203
+ function isRoomPresenceMetadata(metadata, expected) {
5204
+ if (!metadata || Object.keys(metadata).length !== ROOM_PRESENCE_METADATA_KEYS.length) return false;
5205
+ if (ROOM_PRESENCE_METADATA_KEYS.some((key) => !Object.prototype.hasOwnProperty.call(metadata, key))) return false;
5206
+ return metadata.source === expected.source && metadata.kind === expected.kind && metadata.workspaceId === expected.workspaceId && metadata.projectId === expected.projectId && metadata.roomId === expected.roomId && metadata.membershipId === expected.membershipId && metadata.assistantId === expected.assistantId;
5207
+ }
5208
+
5209
+ // src/services/collaboration/RoomCoordinatorEnsurer.ts
5210
+ var RoomCoordinatorEnsureError = class extends Error {
5211
+ /** Creates a typed ensure failure with the underlying store outcome. */
5212
+ constructor(outcome) {
5213
+ super(`Room coordinator could not be ensured: ${outcome}`);
5214
+ this.outcome = outcome;
5215
+ this.name = "RoomCoordinatorEnsureError";
5216
+ }
5217
+ };
5218
+ var RoomCoordinatorEnsurer = class {
5219
+ /** Creates the ensurer from explicitly injected stores and provisioner. */
5220
+ constructor(deps) {
5221
+ this.deps = deps;
5222
+ this.mutex = deps.mutationMutex ?? projectMutationMutex;
5223
+ this.idFactory = deps.idFactory ?? randomUUID5;
5224
+ }
5225
+ /**
5226
+ * Ensures the coordinator membership for one room.
5227
+ *
5228
+ * @param scope Exact tenant/workspace/project/room scope.
5229
+ * @returns The existing or newly created coordinator membership.
5230
+ */
5231
+ async ensure(scope) {
5232
+ return this.mutex.runExclusive({ tenantId: scope.tenantId, projectId: scope.projectId }, async () => {
5233
+ const assistant = await this.deps.provisioner.ensure(scope.tenantId);
5234
+ const existing = await this.deps.membershipStore.findByAssistant(scope.tenantId, scope.projectId, ROOM_COORDINATOR_KEY);
5235
+ if (existing) return existing;
5236
+ const membershipId = this.idFactory();
5237
+ const threadId = this.idFactory();
5238
+ const metadata = roomPresenceMetadata({
5239
+ workspaceId: scope.workspaceId,
5240
+ projectId: scope.projectId,
5241
+ roomId: scope.roomId,
5242
+ membershipId,
5243
+ assistantId: assistant.id
5244
+ });
5245
+ await this.ensureThread(scope.tenantId, assistant.id, threadId, metadata);
5246
+ const mentionName = await this.deps.resolveMentionName?.(scope.tenantId) ?? ROOM_COORDINATOR_KEY;
5247
+ try {
5248
+ const saved = await this.deps.membershipStore.save({
5249
+ id: membershipId,
5250
+ tenantId: scope.tenantId,
5251
+ workspaceId: scope.workspaceId,
5252
+ projectId: scope.projectId,
5253
+ roomId: scope.roomId,
5254
+ assistantId: assistant.id,
5255
+ role: "coordinator",
5256
+ title: assistant.name,
5257
+ mentionName,
5258
+ status: "active",
5259
+ roomThreadId: threadId
5260
+ });
5261
+ if (saved.kind !== "created" && saved.kind !== "updated" && saved.kind !== "reactivated") {
5262
+ const winner = await this.deps.membershipStore.findByAssistant(scope.tenantId, scope.projectId, ROOM_COORDINATOR_KEY);
5263
+ if (winner) return winner;
5264
+ throw new RoomCoordinatorEnsureError(saved.kind);
5265
+ }
5266
+ await this.deps.bindingLifecycle.ensure(saved.membership);
5267
+ return saved.membership;
5268
+ } catch (error) {
5269
+ const winner = await this.deps.membershipStore.findByAssistant(scope.tenantId, scope.projectId, ROOM_COORDINATOR_KEY);
5270
+ if (winner) return winner;
5271
+ throw error;
5272
+ }
5273
+ });
5274
+ }
5275
+ async ensureThread(tenantId, assistantId, threadId, metadata) {
5276
+ const existing = await this.deps.threadStore.getThreadById(tenantId, threadId);
5277
+ if (existing) {
5278
+ if (existing.id !== threadId || existing.tenantId !== tenantId || existing.assistantId !== assistantId || !isRoomPresenceMetadata(existing.metadata, metadata)) {
5279
+ throw new RoomCoordinatorEnsureError("thread_conflict");
5280
+ }
5281
+ return;
5282
+ }
5283
+ const created = await this.deps.threadStore.createThread(tenantId, assistantId, threadId, { metadata });
5284
+ if (!created || created.id !== threadId || created.tenantId !== tenantId || created.assistantId !== assistantId || !isRoomPresenceMetadata(created.metadata, metadata)) {
5285
+ throw new RoomCoordinatorEnsureError("thread_conflict");
5286
+ }
5287
+ }
5288
+ };
5289
+
5290
+ // src/services/collaboration/RoomBindingLifecycle.ts
5291
+ var NoopRoomBindingLifecycle = class {
5292
+ /** Performs no binding work. */
5293
+ async ensure(_membership) {
5294
+ }
5295
+ /** Performs no binding work. */
5296
+ async pause(_membership) {
5297
+ }
5298
+ /** Performs no binding work. */
5299
+ async resume(_membership) {
5300
+ }
5301
+ /** Performs no binding work. */
5302
+ async remove(_membership) {
5303
+ }
5304
+ };
5305
+
5161
5306
  // src/services/collaboration/WorkspacePublicRoomService.ts
5162
5307
  import { createHash } from "crypto";
5163
- import { randomUUID as randomUUID5 } from "crypto";
5308
+ import { randomUUID as randomUUID6 } from "crypto";
5164
5309
  var WORKSPACE_PUBLIC_ROOM_PROJECT_PREFIX = "workspace_public_";
5165
5310
  var PUBLIC_ROOM_PROJECT_NAME = "General";
5166
5311
  var PUBLIC_ROOM_PROJECT_DESCRIPTION = "Public room for everyone in the workspace";
@@ -5172,7 +5317,7 @@ var WorkspacePublicRoomService = class {
5172
5317
  /** Creates the service from explicitly injected stores and provisioners. */
5173
5318
  constructor(deps) {
5174
5319
  this.deps = deps;
5175
- this.idFactory = deps.idFactory ?? randomUUID5;
5320
+ this.idFactory = deps.idFactory ?? randomUUID6;
5176
5321
  }
5177
5322
  /**
5178
5323
  * Returns the workspace's public room project and room, creating them on first call.
@@ -5191,7 +5336,7 @@ var WorkspacePublicRoomService = class {
5191
5336
  projectId,
5192
5337
  name: project.name
5193
5338
  });
5194
- await this.ensureCoordinatorBot(tenantId, workspaceId, projectId, room.id);
5339
+ await this.deps.ensureCoordinator({ tenantId, workspaceId, projectId, roomId: room.id });
5195
5340
  return { project, room };
5196
5341
  }
5197
5342
  async ensureProject(tenantId, workspaceId, projectId) {
@@ -5209,27 +5354,6 @@ var WorkspacePublicRoomService = class {
5209
5354
  throw Object.assign(new Error("Workspace public room project could not be ensured"), { cause: error });
5210
5355
  }
5211
5356
  }
5212
- async ensureCoordinatorBot(tenantId, workspaceId, projectId, roomId) {
5213
- const coordinator = await this.deps.ensureCoordinator(tenantId);
5214
- const existing = await this.deps.findBotByAssistant(tenantId, projectId, coordinator.id);
5215
- if (existing && existing.status !== "removed") return;
5216
- const saved = await this.deps.saveBot({
5217
- id: this.idFactory(),
5218
- tenantId,
5219
- workspaceId,
5220
- projectId,
5221
- roomId,
5222
- assistantId: coordinator.id,
5223
- role: "coordinator",
5224
- title: coordinator.name,
5225
- mentionName: ROOM_COORDINATOR_KEY,
5226
- status: "active",
5227
- roomThreadId: this.idFactory()
5228
- });
5229
- if (saved.kind === "coordinator_conflict" || saved.kind === "mention_conflict") {
5230
- throw new Error(`Workspace public room coordinator bot could not be ensured: ${saved.kind}`);
5231
- }
5232
- }
5233
5357
  };
5234
5358
 
5235
5359
  // src/controllers/workspace.ts
@@ -5348,8 +5472,9 @@ function createProjectCollaborationInitializer(deps) {
5348
5472
  } catch {
5349
5473
  throw new ProjectCollaborationInitializationError("owner");
5350
5474
  }
5475
+ let room;
5351
5476
  try {
5352
- await deps.roomStore.ensureMainRoom({
5477
+ room = await deps.roomStore.ensureMainRoom({
5353
5478
  id: idFactory(),
5354
5479
  tenantId,
5355
5480
  workspaceId: project.workspaceId,
@@ -5359,6 +5484,22 @@ function createProjectCollaborationInitializer(deps) {
5359
5484
  } catch {
5360
5485
  throw new ProjectCollaborationInitializationError("room");
5361
5486
  }
5487
+ if (project.kind === "business" && deps.ensureCoordinator) {
5488
+ try {
5489
+ await deps.ensureCoordinator({
5490
+ tenantId,
5491
+ workspaceId: project.workspaceId,
5492
+ projectId: project.id,
5493
+ roomId: room.id
5494
+ });
5495
+ } catch (error) {
5496
+ deps.log?.warn("project:coordinator-initialization-failed", {
5497
+ tenantId,
5498
+ projectId: project.id,
5499
+ errorName: error instanceof Error ? error.name : "UnknownError"
5500
+ });
5501
+ }
5502
+ }
5362
5503
  }
5363
5504
  };
5364
5505
  }
@@ -5370,17 +5511,29 @@ var WorkspaceController = class {
5370
5511
  const roomStore = getStoreLattice6("default", "projectRoom").store;
5371
5512
  const botMembershipStore = getStoreLattice6("default", "projectBotMembership").store;
5372
5513
  const assistantStore = getStoreLattice6("default", "assistant").store;
5514
+ const threadStore = getStoreLattice6("default", "thread").store;
5515
+ const tenantStore = getStoreLattice6("default", "tenant").store;
5373
5516
  const coordinatorProvisioner = new RoomCoordinatorProvisioner({
5374
5517
  getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
5375
- createAssistant: assistantStore.createAssistant.bind(assistantStore)
5518
+ createAssistant: assistantStore.createAssistant.bind(assistantStore),
5519
+ updateAssistant: assistantStore.updateAssistant.bind(assistantStore)
5520
+ });
5521
+ const coordinatorEnsurer = new RoomCoordinatorEnsurer({
5522
+ provisioner: coordinatorProvisioner,
5523
+ membershipStore: botMembershipStore,
5524
+ threadStore,
5525
+ bindingLifecycle: new NoopRoomBindingLifecycle(),
5526
+ resolveMentionName: async (tenantId) => {
5527
+ const tenant2 = await tenantStore.getTenantById(tenantId);
5528
+ const value = tenant2?.metadata?.coordinatorMentionName;
5529
+ return typeof value === "string" && value.length > 0 ? value : void 0;
5530
+ }
5376
5531
  });
5377
5532
  this.publicRoom = new WorkspacePublicRoomService({
5378
5533
  getProjectById: this.projectStore.getProjectById.bind(this.projectStore),
5379
5534
  createProject: this.projectStore.createProject.bind(this.projectStore),
5380
5535
  ensureMainRoom: roomStore.ensureMainRoom.bind(roomStore),
5381
- findBotByAssistant: botMembershipStore.findByAssistant.bind(botMembershipStore),
5382
- saveBot: botMembershipStore.save.bind(botMembershipStore),
5383
- ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
5536
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope)
5384
5537
  });
5385
5538
  }
5386
5539
  getTenantId(request) {
@@ -6089,15 +6242,19 @@ var WorkspaceController = class {
6089
6242
  }
6090
6243
  outputBuf = Buffer.from(content2, "utf-8");
6091
6244
  }
6092
- return reply.status(200).type(contentType).header("Content-Disposition", "inline").send(outputBuf);
6245
+ return sendBufferWithRange(reply, outputBuf, contentType, request.headers.range);
6093
6246
  } catch (err) {
6094
6247
  return reply.status(502).send({ success: false, error: String(err) });
6095
6248
  }
6096
6249
  }
6097
6250
  const { backend } = await this.getBackend(tenantId, workspaceId, projectId, assistantId, resolvedPath);
6098
- const content = await backend.read(resolvedPath, 0, Infinity);
6099
6251
  const filename = getFilenameFromPath(resolvedPath);
6100
6252
  const mimeType = getContentTypeFromFilename(filename);
6253
+ if (isBinaryContentType(filename) && backend.readBinary) {
6254
+ const buffer2 = await backend.readBinary(resolvedPath);
6255
+ return sendBufferWithRange(reply, buffer2, mimeType, request.headers.range);
6256
+ }
6257
+ const content = await backend.read(resolvedPath, 0, Infinity);
6101
6258
  let finalContent = content;
6102
6259
  const isHtmlFs = mimeType?.toLowerCase().includes("text/html") || filename.toLowerCase().endsWith(".html") || filename.toLowerCase().endsWith(".htm");
6103
6260
  if (isHtmlFs) {
@@ -6120,7 +6277,7 @@ var WorkspaceController = class {
6120
6277
  }
6121
6278
  }
6122
6279
  const buffer = Buffer.from(finalContent, "utf-8");
6123
- return reply.status(200).type(mimeType).header("Content-Disposition", "inline").send(buffer);
6280
+ return sendBufferWithRange(reply, buffer, mimeType, request.headers.range);
6124
6281
  } catch (error) {
6125
6282
  const message = error instanceof Error ? error.message : String(error);
6126
6283
  return reply.status(502).send({ success: false, error: `View proxy error: ${message}` });
@@ -6203,6 +6360,10 @@ var WorkspaceController = class {
6203
6360
  if (error instanceof ProjectFileValidationError) {
6204
6361
  return reply.status(400).send({ success: false, error: error.message });
6205
6362
  }
6363
+ const code = error?.code;
6364
+ if (code === "FST_REQ_FILE_TOO_LARGE" || error instanceof Error && /too large/i.test(error.message)) {
6365
+ return reply.status(413).send({ success: false, error: "File is too large" });
6366
+ }
6206
6367
  const message = error instanceof Error ? error.message : String(error);
6207
6368
  return reply.status(500).send({
6208
6369
  success: false,
@@ -6333,7 +6494,7 @@ function registerWorkspaceRoutes(app2, projectCollaboration) {
6333
6494
  }
6334
6495
 
6335
6496
  // src/services/collaboration/ProjectAccessService.ts
6336
- import { randomUUID as randomUUID6 } from "crypto";
6497
+ import { randomUUID as randomUUID7 } from "crypto";
6337
6498
  var ProjectCollaborationError = class extends Error {
6338
6499
  /** Creates a project collaboration error. */
6339
6500
  constructor(code, statusCode, message) {
@@ -6359,7 +6520,7 @@ var ProjectAccessService = class {
6359
6520
  /** Creates a project access service from explicitly injected stores. */
6360
6521
  constructor(deps) {
6361
6522
  this.deps = deps;
6362
- this.idFactory = deps.idFactory ?? randomUUID6;
6523
+ this.idFactory = deps.idFactory ?? randomUUID7;
6363
6524
  }
6364
6525
  /** Returns the project and active membership when the user has the requested capability. */
6365
6526
  async requireProjectAccess(input) {
@@ -6528,7 +6689,7 @@ import {
6528
6689
  getStoreLattice as getStoreLattice7,
6529
6690
  sqlDatabaseManager
6530
6691
  } from "@axiom-lattice/core";
6531
- import { randomUUID as randomUUID7 } from "crypto";
6692
+ import { randomUUID as randomUUID8 } from "crypto";
6532
6693
  function getTenantId8(request) {
6533
6694
  const userTenantId = request.user?.tenantId;
6534
6695
  if (userTenantId) {
@@ -6606,7 +6767,7 @@ async function createDatabaseConfig(request, reply) {
6606
6767
  message: "Database configuration with this key already exists"
6607
6768
  };
6608
6769
  }
6609
- const id3 = body.id || randomUUID7();
6770
+ const id3 = body.id || randomUUID8();
6610
6771
  const config = await store.createConfig(tenantId, id3, body);
6611
6772
  try {
6612
6773
  sqlDatabaseManager.registerDatabase(tenantId, config.key, config.config);
@@ -6811,7 +6972,7 @@ import {
6811
6972
  metricsServerManager as metricsServerManager2,
6812
6973
  SemanticMetricsClient as SemanticMetricsClient2
6813
6974
  } from "@axiom-lattice/core";
6814
- import { randomUUID as randomUUID8 } from "crypto";
6975
+ import { randomUUID as randomUUID9 } from "crypto";
6815
6976
  function getTenantId9(request) {
6816
6977
  const userTenantId = request.user?.tenantId;
6817
6978
  if (userTenantId) {
@@ -6892,7 +7053,7 @@ async function createMetricsServerConfig(request, reply) {
6892
7053
  message: "selectedDataSources is required for semantic metrics servers"
6893
7054
  };
6894
7055
  }
6895
- const id3 = body.id || randomUUID8();
7056
+ const id3 = body.id || randomUUID9();
6896
7057
  const configData = {
6897
7058
  key: body.key,
6898
7059
  name: body.name,
@@ -9410,7 +9571,7 @@ function registerChannelRoutes(app2, dependencies) {
9410
9571
  }
9411
9572
 
9412
9573
  // src/controllers/channel-installations.ts
9413
- import { randomUUID as randomUUID9 } from "crypto";
9574
+ import { randomUUID as randomUUID10 } from "crypto";
9414
9575
  import {
9415
9576
  descriptorDataValue
9416
9577
  } from "@axiom-lattice/protocols";
@@ -9614,7 +9775,7 @@ async function createChannelInstallation(request, reply) {
9614
9775
  };
9615
9776
  }
9616
9777
  const store = await getInstallationStore();
9617
- const installationId = randomUUID9();
9778
+ const installationId = randomUUID10();
9618
9779
  const createInput = {
9619
9780
  channel: body.channel,
9620
9781
  name: body.name,
@@ -10574,7 +10735,7 @@ async function getLocalA2ATemplates(_request, reply) {
10574
10735
  }
10575
10736
 
10576
10737
  // src/routes/a2a-test-connection.ts
10577
- import { randomUUID as randomUUID10 } from "crypto";
10738
+ import { randomUUID as randomUUID11 } from "crypto";
10578
10739
  function describeTestConnectionError(error, agentCardUrl) {
10579
10740
  if (!(error instanceof Error)) {
10580
10741
  return `Failed to fetch agent card from ${agentCardUrl}: ${String(error)}`;
@@ -10614,7 +10775,7 @@ async function ensureLocalRuntimeForConnectionTest(localRuntime, agentCardUrl) {
10614
10775
  return { error: portValidation.error };
10615
10776
  }
10616
10777
  const state = await localA2AService.startProvider(
10617
- `test-${randomUUID10()}`,
10778
+ `test-${randomUUID11()}`,
10618
10779
  testRuntime
10619
10780
  );
10620
10781
  if (state.status === "running") return { runtimeId: state.runtimeId };
@@ -11020,12 +11181,6 @@ function snapshotEnsureInput(input) {
11020
11181
  return { task, room, membership, mode: row.mode };
11021
11182
  }
11022
11183
 
11023
- // src/services/collaboration/ProjectMutationMutex.ts
11024
- import {
11025
- ProjectMutationMutex,
11026
- projectMutationMutex
11027
- } from "@axiom-lattice/core";
11028
-
11029
11184
  // src/controllers/tasks.ts
11030
11185
  var HttpTaskError = class extends Error {
11031
11186
  /** Creates a stable Task HTTP error. */
@@ -11934,7 +12089,7 @@ import { createHmac as createHmac3 } from "crypto";
11934
12089
  import { z as z5, ZodError as ZodError2 } from "zod";
11935
12090
 
11936
12091
  // src/services/agent-web-app/AgentWebAppRuntimeService.ts
11937
- import { createHash as createHash2, randomUUID as randomUUID11 } from "crypto";
12092
+ import { createHash as createHash2, randomUUID as randomUUID12 } from "crypto";
11938
12093
  import { MessageChunkTypes as MessageChunkTypes3, parseAgentWebAppGenUIBlock } from "@axiom-lattice/protocols";
11939
12094
  function isRecord2(value) {
11940
12095
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -12131,7 +12286,7 @@ ${formatFileRefsSection(refs)}` : input.content;
12131
12286
  const userDigest = createHash2("sha256").update(userId, "utf8").digest("hex");
12132
12287
  const contentDigest = createHash2("sha256").update(file.bytes).digest("hex");
12133
12288
  const displayName = file.name;
12134
- const physicalName = `${randomUUID11()}-${sanitizePhysicalFilename(file.name)}`;
12289
+ const physicalName = `${randomUUID12()}-${sanitizePhysicalFilename(file.name)}`;
12135
12290
  const saved = await this.deps.saveProjectFile({
12136
12291
  tenantId: runtime.webApp.tenantId,
12137
12292
  workspaceId: runtime.project.workspaceId,
@@ -12283,7 +12438,7 @@ ${formatFileRefsSection(refs)}` : input.content;
12283
12438
  const thread = await this.deps.threadStore.createThread(
12284
12439
  runtime.webApp.tenantId,
12285
12440
  runtime.webApp.assistantId,
12286
- randomUUID11(),
12441
+ randomUUID12(),
12287
12442
  { metadata: this.metadata(runtime, userId, normalizeLabel(rawLabel)) }
12288
12443
  );
12289
12444
  return toRuntimeThread(thread);
@@ -12919,17 +13074,32 @@ async function pipeAgentWebAppStream(raw, stream, options = {}) {
12919
13074
  const projection = createAgentWebAppStreamProjectionContext(options);
12920
13075
  await pipeAsyncIterableToSse(raw, stream, {
12921
13076
  project: (chunk) => projectAgentWebAppChunk(chunk, projection),
12922
- onError: () => [
12923
- { type: "error", error: { code: "STREAM_FAILED", message: "Stream failed", retryable: true } },
12924
- { type: "stream.completed" }
12925
- ]
13077
+ onError: (error) => {
13078
+ const code = error?.code;
13079
+ if (code === "aborted" || code === "superseded") return [{ type: "stream.completed" }];
13080
+ return [
13081
+ {
13082
+ type: "error",
13083
+ error: {
13084
+ code: "STREAM_FAILED",
13085
+ message: error instanceof Error && error.message ? error.message : "Stream failed",
13086
+ retryable: false
13087
+ }
13088
+ },
13089
+ { type: "stream.completed" }
13090
+ ];
13091
+ }
12926
13092
  });
12927
13093
  }
12928
13094
  async function pipeNativeWebAppStream(raw, stream) {
12929
13095
  await pipeAsyncIterableToSse(raw, stream, {
12930
- onError: () => [{
13096
+ onError: (error) => [{
12931
13097
  type: "error",
12932
- data: { id: "", content: "Stream failed" }
13098
+ data: {
13099
+ id: "",
13100
+ content: error instanceof Error && error.message ? error.message : "Stream failed",
13101
+ code: error?.code ?? "stream_error"
13102
+ }
12933
13103
  }]
12934
13104
  });
12935
13105
  }
@@ -12965,6 +13135,7 @@ function isMultipartRequestError(error) {
12965
13135
  import { existsSync as existsSync2 } from "fs";
12966
13136
  import { readFile, readdir as readdir2 } from "fs/promises";
12967
13137
  import path3 from "path";
13138
+ import { fileURLToPath } from "url";
12968
13139
  import { z as z6 } from "zod";
12969
13140
  var idSchema2 = z6.string().min(1).max(128).regex(/^[A-Za-z0-9._:@-]+$/);
12970
13141
  var JS_CONTENT_TYPE = "application/javascript; charset=utf-8";
@@ -12994,10 +13165,36 @@ async function listChunkCssFiles(assetsDir) {
12994
13165
  }
12995
13166
  }
12996
13167
  var BUNDLED_MARKER = "agent-chat-loader.js";
13168
+ function selfModuleDir() {
13169
+ try {
13170
+ const target = {};
13171
+ const originalPrepare = Error.prepareStackTrace;
13172
+ Error.captureStackTrace(target, selfModuleDir);
13173
+ Error.prepareStackTrace = (error, frames2) => frames2;
13174
+ const frames = target.stack;
13175
+ Error.prepareStackTrace = originalPrepare;
13176
+ if (!Array.isArray(frames)) return null;
13177
+ for (const frame of frames) {
13178
+ const callSite = frame;
13179
+ if (typeof callSite.getFileName !== "function") continue;
13180
+ const fileName = callSite.getFileName();
13181
+ if (typeof fileName !== "string" || fileName.length === 0) continue;
13182
+ const filePath = fileName.startsWith("file://") ? fileURLToPath(fileName) : fileName;
13183
+ return path3.dirname(filePath);
13184
+ }
13185
+ } catch {
13186
+ }
13187
+ return null;
13188
+ }
12997
13189
  function resolveBundledEmbedDir() {
13190
+ const candidates = [];
12998
13191
  if (typeof __dirname === "string") {
12999
- const compiled = path3.resolve(__dirname, "embed");
13000
- if (existsSync2(path3.join(compiled, BUNDLED_MARKER))) return compiled;
13192
+ candidates.push(path3.resolve(__dirname, "embed"));
13193
+ }
13194
+ const selfDir = selfModuleDir();
13195
+ if (selfDir !== null) candidates.push(path3.resolve(selfDir, "embed"));
13196
+ for (const candidate of candidates) {
13197
+ if (existsSync2(path3.join(candidate, BUNDLED_MARKER))) return candidate;
13001
13198
  }
13002
13199
  let dir = path3.dirname(path3.resolve(process.argv[1] ?? process.cwd()));
13003
13200
  for (let depth = 0; depth < 8; depth += 1) {
@@ -13492,31 +13689,72 @@ function registerCapabilityBundleRoutes(app2, controller) {
13492
13689
  }
13493
13690
 
13494
13691
  // src/services/collaboration/ProjectRoomService.ts
13495
- import { randomUUID as randomUUID12 } from "crypto";
13692
+ import { randomUUID as randomUUID13 } from "crypto";
13496
13693
  var ProjectRoomService = class {
13497
13694
  /** Creates a project room service from explicitly injected collaborators. */
13498
13695
  constructor(deps) {
13499
13696
  this.deps = deps;
13500
- this.idFactory = deps.idFactory ?? randomUUID12;
13697
+ this.idFactory = deps.idFactory ?? randomUUID13;
13501
13698
  }
13502
- /** Authorizes read access and atomically ensures the project's main room. */
13699
+ /**
13700
+ * Authorizes read access and ensures the project's main room.
13701
+ *
13702
+ * @remarks
13703
+ * This method is safe to call while holding the process-wide Project
13704
+ * mutation mutex: it never re-enters it. Coordinator backfill is a separate
13705
+ * entry point ({@link ensureMainRoomWithCoordinator}) because it acquires
13706
+ * that mutex internally.
13707
+ */
13503
13708
  async ensureMainRoom(input) {
13709
+ const { room } = await this.authorizedRoom(input);
13710
+ return room;
13711
+ }
13712
+ /**
13713
+ * Authorizes read access, ensures the main room, and lazily backfills the
13714
+ * built-in coordinator for business projects.
13715
+ *
13716
+ * @remarks
13717
+ * Only call this outside the Project mutation mutex (room open / bootstrap
13718
+ * paths); calling it under the mutex deadlocks on the coordinator ensurer.
13719
+ */
13720
+ async ensureMainRoomWithCoordinator(input) {
13721
+ const { project, room } = await this.authorizedRoom(input);
13722
+ if (project.kind === "business" && this.deps.ensureCoordinator) {
13723
+ try {
13724
+ await this.deps.ensureCoordinator({
13725
+ tenantId: input.tenantId,
13726
+ workspaceId: project.workspaceId,
13727
+ projectId: project.id,
13728
+ roomId: room.id
13729
+ });
13730
+ } catch (error) {
13731
+ this.deps.log?.warn("project-room:coordinator-ensure-failed", {
13732
+ tenantId: input.tenantId,
13733
+ projectId: project.id,
13734
+ errorName: error instanceof Error ? error.name : "UnknownError"
13735
+ });
13736
+ }
13737
+ }
13738
+ return room;
13739
+ }
13740
+ async authorizedRoom(input) {
13504
13741
  const { project } = await this.deps.accessService.requireProjectAccess({
13505
13742
  ...input,
13506
13743
  capability: "read"
13507
13744
  });
13508
- return this.deps.roomStore.ensureMainRoom({
13745
+ const room = await this.deps.roomStore.ensureMainRoom({
13509
13746
  id: this.idFactory(),
13510
13747
  tenantId: input.tenantId,
13511
13748
  workspaceId: project.workspaceId,
13512
13749
  projectId: project.id,
13513
13750
  name: project.name
13514
13751
  });
13752
+ return { project, room };
13515
13753
  }
13516
13754
  };
13517
13755
 
13518
13756
  // src/services/collaboration/ProjectMembershipService.ts
13519
- import { randomUUID as randomUUID13 } from "crypto";
13757
+ import { randomUUID as randomUUID14 } from "crypto";
13520
13758
  var ProjectMembershipServiceError = class extends Error {
13521
13759
  constructor(code, statusCode, message) {
13522
13760
  super(message);
@@ -13532,30 +13770,30 @@ function isMembershipConflict(error) {
13532
13770
  var ProjectMembershipService = class {
13533
13771
  constructor(deps) {
13534
13772
  this.deps = deps;
13535
- this.idFactory = deps.idFactory ?? randomUUID13;
13773
+ this.idFactory = deps.idFactory ?? randomUUID14;
13536
13774
  this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
13537
13775
  }
13538
- async list(actor2) {
13539
- await this.deps.accessService.requireRosterAccess(actor2);
13540
- const membership = await this.deps.membershipStore.findByUser(actor2.tenantId, actor2.projectId, actor2.userId);
13776
+ async list(actor3) {
13777
+ await this.deps.accessService.requireRosterAccess(actor3);
13778
+ const membership = await this.deps.membershipStore.findByUser(actor3.tenantId, actor3.projectId, actor3.userId);
13541
13779
  if (!membership || membership.status !== "active") this.accessDenied();
13542
- return this.deps.membershipStore.list(actor2.tenantId, actor2.projectId);
13780
+ return this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
13543
13781
  }
13544
- async add(actor2, input) {
13545
- return this.withProjectLock(actor2, async () => {
13546
- const access = await this.authorize(actor2);
13782
+ async add(actor3, input) {
13783
+ return this.withProjectLock(actor3, async () => {
13784
+ const access = await this.authorize(actor3);
13547
13785
  if (input.role === "owner" && access.role !== "owner") this.accessDenied();
13548
- if (!await this.deps.userTenantLinkStore.getLink(input.userId, actor2.tenantId)) {
13786
+ if (!await this.deps.userTenantLinkStore.getLink(input.userId, actor3.tenantId)) {
13549
13787
  throw new ProjectMembershipServiceError("TARGET_USER_NOT_IN_TENANT", 400, "Target user is not in the tenant");
13550
13788
  }
13551
- if (await this.deps.membershipStore.findByUser(actor2.tenantId, actor2.projectId, input.userId)) {
13789
+ if (await this.deps.membershipStore.findByUser(actor3.tenantId, actor3.projectId, input.userId)) {
13552
13790
  throw new ProjectMembershipServiceError("MEMBERSHIP_EXISTS", 409, "Project membership already exists");
13553
13791
  }
13554
13792
  try {
13555
13793
  const created = await this.deps.membershipStore.create({
13556
13794
  id: this.idFactory(),
13557
- tenantId: actor2.tenantId,
13558
- projectId: actor2.projectId,
13795
+ tenantId: actor3.tenantId,
13796
+ projectId: actor3.projectId,
13559
13797
  userId: input.userId,
13560
13798
  role: input.role,
13561
13799
  status: "active"
@@ -13573,36 +13811,36 @@ var ProjectMembershipService = class {
13573
13811
  }
13574
13812
  });
13575
13813
  }
13576
- async updateRole(actor2, input) {
13577
- return this.withProjectLock(actor2, async () => {
13578
- const access = await this.authorize(actor2);
13579
- const target = await this.target(actor2, input.membershipId);
13814
+ async updateRole(actor3, input) {
13815
+ return this.withProjectLock(actor3, async () => {
13816
+ const access = await this.authorize(actor3);
13817
+ const target = await this.target(actor3, input.membershipId);
13580
13818
  this.requireActiveAndCurrent(target, input.expectedUpdatedAt);
13581
13819
  if ((target.role === "owner" || input.role === "owner") && access.role !== "owner") this.accessDenied();
13582
- const updated = await this.mapMutation(await this.deps.membershipStore.updateRole({ tenantId: actor2.tenantId, id: input.membershipId, role: input.role, expectedUpdatedAt: input.expectedUpdatedAt }));
13820
+ const updated = await this.mapMutation(await this.deps.membershipStore.updateRole({ tenantId: actor3.tenantId, id: input.membershipId, role: input.role, expectedUpdatedAt: input.expectedUpdatedAt }));
13583
13821
  this.publish(updated, "role_changed", `membership:${updated.id}:role:${updated.role}:${input.expectedUpdatedAt.toISOString()}`);
13584
13822
  return updated;
13585
13823
  });
13586
13824
  }
13587
- async remove(actor2, input) {
13588
- return this.withProjectLock(actor2, async () => {
13589
- const access = await this.authorize(actor2);
13590
- const target = await this.target(actor2, input.membershipId);
13825
+ async remove(actor3, input) {
13826
+ return this.withProjectLock(actor3, async () => {
13827
+ const access = await this.authorize(actor3);
13828
+ const target = await this.target(actor3, input.membershipId);
13591
13829
  this.requireActiveAndCurrent(target, input.expectedUpdatedAt);
13592
13830
  if (target.role === "owner" && access.role !== "owner") this.accessDenied();
13593
- const removed = await this.mapMutation(await this.deps.membershipStore.remove({ tenantId: actor2.tenantId, id: input.membershipId, expectedUpdatedAt: input.expectedUpdatedAt }));
13831
+ const removed = await this.mapMutation(await this.deps.membershipStore.remove({ tenantId: actor3.tenantId, id: input.membershipId, expectedUpdatedAt: input.expectedUpdatedAt }));
13594
13832
  this.publish(removed, "removed", `membership:${removed.id}:remove:${input.expectedUpdatedAt.toISOString()}`);
13595
13833
  return removed;
13596
13834
  });
13597
13835
  }
13598
- async authorize(actor2) {
13599
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "manage" });
13600
- const membership = await this.deps.membershipStore.findByUser(actor2.tenantId, actor2.projectId, actor2.userId);
13836
+ async authorize(actor3) {
13837
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "manage" });
13838
+ const membership = await this.deps.membershipStore.findByUser(actor3.tenantId, actor3.projectId, actor3.userId);
13601
13839
  if (!membership || membership.status !== "active") this.accessDenied();
13602
13840
  return membership;
13603
13841
  }
13604
- async target(actor2, id3) {
13605
- const target = (await this.deps.membershipStore.list(actor2.tenantId, actor2.projectId)).find((item) => item.id === id3);
13842
+ async target(actor3, id3) {
13843
+ const target = (await this.deps.membershipStore.list(actor3.tenantId, actor3.projectId)).find((item) => item.id === id3);
13606
13844
  if (!target) throw new ProjectMembershipServiceError("MEMBERSHIP_NOT_FOUND", 404, "Project membership not found");
13607
13845
  return target;
13608
13846
  }
@@ -13657,16 +13895,16 @@ var ProjectMembershipService = class {
13657
13895
  accessDenied() {
13658
13896
  throw new ProjectMembershipServiceError("PROJECT_ACCESS_DENIED", 403, "Project access denied");
13659
13897
  }
13660
- async withProjectLock(actor2, operation) {
13898
+ async withProjectLock(actor3, operation) {
13661
13899
  return this.mutationMutex.runExclusive(
13662
- { tenantId: actor2.tenantId, projectId: actor2.projectId },
13900
+ { tenantId: actor3.tenantId, projectId: actor3.projectId },
13663
13901
  operation
13664
13902
  );
13665
13903
  }
13666
13904
  };
13667
13905
 
13668
13906
  // src/services/collaboration/ProjectRosterService.ts
13669
- import { randomUUID as randomUUID14 } from "crypto";
13907
+ import { randomUUID as randomUUID15 } from "crypto";
13670
13908
  var ProjectRosterServiceError = class extends Error {
13671
13909
  /** Creates a stable roster service error. */
13672
13910
  constructor(code, statusCode, message) {
@@ -13676,35 +13914,38 @@ var ProjectRosterServiceError = class extends Error {
13676
13914
  this.name = "ProjectRosterServiceError";
13677
13915
  }
13678
13916
  };
13917
+ function coordinatorProtected(message) {
13918
+ return new ProjectRosterServiceError("ROOM_COORDINATOR_PROTECTED", 409, message);
13919
+ }
13679
13920
  var activeStatuses = ["active", "paused"];
13680
13921
  var activeTaskStatuses = ["pending", "in_progress", "interrupted"];
13681
- var metadataKeys = ["source", "kind", "workspaceId", "projectId", "roomId", "membershipId", "assistantId"];
13682
13922
  var ProjectRosterService = class {
13683
13923
  /** Creates a roster service from explicitly injected collaborators. */
13684
13924
  constructor(deps) {
13685
13925
  this.deps = deps;
13686
- this.idFactory = deps.idFactory ?? randomUUID14;
13926
+ this.idFactory = deps.idFactory ?? randomUUID15;
13687
13927
  this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
13688
13928
  }
13689
13929
  /** Lists all bot memberships visible to a project reader. */
13690
- async list(actor2) {
13691
- await this.authorize(actor2, "read");
13692
- return this.deps.membershipStore.list(actor2.tenantId, actor2.projectId);
13930
+ async list(actor3) {
13931
+ await this.authorize(actor3, "read");
13932
+ return this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
13693
13933
  }
13694
13934
  /** Adds or reactivates a bot and ensures its room-presence thread exists. */
13695
- async add(actor2, input) {
13696
- return this.withLock(actor2, async () => {
13697
- await this.authorize(actor2, "manage");
13698
- const room = await this.deps.roomService.ensureMainRoom(actor2);
13699
- const assistant = await this.deps.assistantStore.getAssistantById(actor2.tenantId, input.assistantId);
13700
- if (!assistant || assistant.tenantId !== actor2.tenantId) throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Assistant not found");
13935
+ async add(actor3, input) {
13936
+ this.assertAssignableRole(input);
13937
+ return this.withLock(actor3, async () => {
13938
+ await this.authorize(actor3, "manage");
13939
+ const room = await this.deps.roomService.ensureMainRoom(actor3);
13940
+ const assistant = await this.deps.assistantStore.getAssistantById(actor3.tenantId, input.assistantId);
13941
+ if (!assistant || assistant.tenantId !== actor3.tenantId) throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Assistant not found");
13701
13942
  const normalized = this.validateInput(input);
13702
- const existing = await this.deps.membershipStore.findByAssistant(actor2.tenantId, actor2.projectId, input.assistantId);
13703
- if (existing) this.validateMembership(existing, actor2, room, input.assistantId);
13704
- let ids = existing ? { membershipId: existing.id, threadId: existing.roomThreadId } : await this.findSafePresence(actor2, room, input.assistantId);
13943
+ const existing = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, input.assistantId);
13944
+ if (existing) this.validateMembership(existing, actor3, room, input.assistantId);
13945
+ let ids = existing ? { membershipId: existing.id, threadId: existing.roomThreadId } : await this.findSafePresence(actor3, room, input.assistantId);
13705
13946
  for (let attempt = 0; attempt < 3; attempt += 1) {
13706
13947
  try {
13707
- await this.ensureThread(actor2, room, input.assistantId, ids.membershipId, ids.threadId);
13948
+ await this.ensureThread(actor3, room, input.assistantId, ids.membershipId, ids.threadId);
13708
13949
  break;
13709
13950
  } catch (error) {
13710
13951
  if (!(error instanceof ProjectRosterServiceError) || error.code !== "THREAD_CONFLICT" || existing) throw error;
@@ -13712,65 +13953,67 @@ var ProjectRosterService = class {
13712
13953
  if (attempt === 2) throw error;
13713
13954
  }
13714
13955
  }
13715
- await this.authorize(actor2, "manage");
13956
+ await this.authorize(actor3, "manage");
13716
13957
  let result;
13717
13958
  try {
13718
- result = await this.deps.membershipStore.save({ id: ids.membershipId, tenantId: actor2.tenantId, workspaceId: room.workspaceId, projectId: actor2.projectId, roomId: room.id, assistantId: input.assistantId, role: input.role, title: normalized.title, ...normalized.responsibility === void 0 ? {} : { responsibility: normalized.responsibility }, mentionName: normalized.mentionName, status: "active", roomThreadId: ids.threadId });
13959
+ result = await this.deps.membershipStore.save({ id: ids.membershipId, tenantId: actor3.tenantId, workspaceId: room.workspaceId, projectId: actor3.projectId, roomId: room.id, assistantId: input.assistantId, role: input.role, title: normalized.title, ...normalized.responsibility === void 0 ? {} : { responsibility: normalized.responsibility }, mentionName: normalized.mentionName, status: "active", roomThreadId: ids.threadId });
13719
13960
  } catch (error) {
13720
13961
  if (error instanceof Error && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
13721
13962
  throw error;
13722
13963
  }
13723
13964
  if (result.kind !== "created" && result.kind !== "updated" && result.kind !== "reactivated") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
13724
- this.validateMembership(result.membership, actor2, room, input.assistantId, ids.threadId);
13965
+ this.validateMembership(result.membership, actor3, room, input.assistantId, ids.threadId);
13725
13966
  await this.deps.bindingLifecycle.ensure(result.membership);
13726
13967
  this.publish(result.membership, result.kind === "created" ? "added" : result.kind === "reactivated" ? "resumed" : "updated", `roster:${result.membership.id}:${result.membership.updatedAt.toISOString()}`);
13727
13968
  return result.membership;
13728
13969
  });
13729
13970
  }
13730
13971
  /** Updates mutable bot presentation fields using optimistic concurrency. */
13731
- async update(actor2, input) {
13972
+ async update(actor3, input) {
13732
13973
  const patch = { ...input.role === void 0 ? {} : { role: input.role }, ...input.title === void 0 ? {} : { title: this.validateTitle(input.title) }, ...input.responsibility === void 0 ? {} : { responsibility: this.validateResponsibility(input.responsibility) }, ...input.mentionName === void 0 ? {} : { mentionName: this.validateMention(input.mentionName) } };
13733
- return this.mutate(actor2, input.membershipId, input.expectedUpdatedAt, patch);
13974
+ return this.mutate(actor3, input.membershipId, input.expectedUpdatedAt, patch);
13734
13975
  }
13735
13976
  /** Pauses a bot without deleting its presence thread or history. */
13736
- async pause(actor2, input) {
13737
- return this.mutate(actor2, input.membershipId, input.expectedUpdatedAt, { status: "paused" }, "pause");
13977
+ async pause(actor3, input) {
13978
+ return this.mutate(actor3, input.membershipId, input.expectedUpdatedAt, { status: "paused" }, "pause");
13738
13979
  }
13739
13980
  /** Resumes a bot without replacing its presence thread or history. */
13740
- async resume(actor2, input) {
13741
- return this.mutate(actor2, input.membershipId, input.expectedUpdatedAt, { status: "active" }, "resume");
13981
+ async resume(actor3, input) {
13982
+ return this.mutate(actor3, input.membershipId, input.expectedUpdatedAt, { status: "active" }, "resume");
13742
13983
  }
13743
13984
  /** Removes a bot after checking every active task status independently. */
13744
- async remove(actor2, input) {
13745
- return this.withLock(actor2, async () => {
13746
- await this.authorize(actor2, "manage");
13747
- const target = await this.target(actor2, input.membershipId);
13985
+ async remove(actor3, input) {
13986
+ return this.withLock(actor3, async () => {
13987
+ await this.authorize(actor3, "manage");
13988
+ const target = await this.target(actor3, input.membershipId);
13748
13989
  this.requireCurrent(target, input.expectedUpdatedAt);
13990
+ if (target.role === "coordinator") throw coordinatorProtected("The built-in room coordinator cannot be removed");
13749
13991
  for (const status of activeTaskStatuses) {
13750
- const tasks = await this.deps.taskStore.list({ tenantId: actor2.tenantId, projectId: actor2.projectId, ownerType: "agent", ownerId: target.assistantId, status, limit: 1 });
13992
+ const tasks = await this.deps.taskStore.list({ tenantId: actor3.tenantId, projectId: actor3.projectId, ownerType: "agent", ownerId: target.assistantId, status, limit: 1 });
13751
13993
  if (tasks.length > 0) throw new ProjectRosterServiceError("BOT_HAS_ACTIVE_TASKS", 409, "Bot has active tasks");
13752
13994
  }
13753
- await this.authorize(actor2, "manage");
13995
+ await this.authorize(actor3, "manage");
13754
13996
  await this.deps.bindingLifecycle.remove(target);
13755
13997
  try {
13756
- const removed = await this.applyUpdate(actor2, target, { status: "removed" }, input.expectedUpdatedAt);
13998
+ const removed = await this.applyUpdate(actor3, target, { status: "removed" }, input.expectedUpdatedAt);
13757
13999
  this.publish(removed, "removed", `roster:${removed.id}:${removed.updatedAt.toISOString()}`);
13758
14000
  return removed;
13759
14001
  } catch (error) {
13760
- const current = await this.deps.membershipStore.findById(actor2.tenantId, target.id);
13761
- if (!current || current.tenantId !== actor2.tenantId || current.projectId !== actor2.projectId) throw error;
14002
+ const current = await this.deps.membershipStore.findById(actor3.tenantId, target.id);
14003
+ if (!current || current.tenantId !== actor3.tenantId || current.projectId !== actor3.projectId) throw error;
13762
14004
  await this.applyLifecycle(current);
13763
14005
  throw error;
13764
14006
  }
13765
14007
  });
13766
14008
  }
13767
- async mutate(actor2, id3, expected, patch, lifecycle) {
13768
- return this.withLock(actor2, async () => {
13769
- await this.authorize(actor2, "manage");
13770
- const target = await this.target(actor2, id3);
14009
+ async mutate(actor3, id3, expected, patch, lifecycle) {
14010
+ return this.withLock(actor3, async () => {
14011
+ await this.authorize(actor3, "manage");
14012
+ const target = await this.target(actor3, id3);
13771
14013
  this.requireCurrent(target, expected);
13772
- await this.authorize(actor2, "manage");
13773
- const updated = await this.applyUpdate(actor2, target, patch, expected);
14014
+ this.assertCoordinatorPolicy(target, patch, lifecycle);
14015
+ await this.authorize(actor3, "manage");
14016
+ const updated = await this.applyUpdate(actor3, target, patch, expected);
13774
14017
  if (lifecycle === "pause") await this.deps.bindingLifecycle.pause(updated);
13775
14018
  else if (lifecycle === "resume") await this.deps.bindingLifecycle.resume(updated);
13776
14019
  else await this.applyLifecycle(updated);
@@ -13779,6 +14022,18 @@ var ProjectRosterService = class {
13779
14022
  return updated;
13780
14023
  });
13781
14024
  }
14025
+ assertAssignableRole(input) {
14026
+ if (input.role === "coordinator" && input.assistantId !== ROOM_COORDINATOR_KEY) throw coordinatorProtected("Only the built-in room coordinator can hold the coordinator role");
14027
+ if (input.assistantId === ROOM_COORDINATOR_KEY && input.role !== "coordinator") throw coordinatorProtected("The built-in room coordinator cannot be added as a specialist");
14028
+ }
14029
+ assertCoordinatorPolicy(target, patch, lifecycle) {
14030
+ if (target.role === "coordinator") {
14031
+ if (lifecycle !== void 0) throw coordinatorProtected(`The built-in room coordinator cannot be ${lifecycle === "pause" ? "paused" : "resumed"}`);
14032
+ if (patch.role !== void 0 && patch.role !== "coordinator") throw coordinatorProtected("The built-in room coordinator role cannot be changed");
14033
+ return;
14034
+ }
14035
+ if (patch.role === "coordinator") throw coordinatorProtected("Only the built-in room coordinator can hold the coordinator role");
14036
+ }
13782
14037
  publish(membership, change, sourceKey) {
13783
14038
  if (!this.deps.realtime) return;
13784
14039
  Promise.resolve().then(() => this.deps.realtime.rosterChanged({ tenantId: membership.tenantId, roomId: membership.roomId, membership, change, sourceKey })).catch(() => {
@@ -13799,36 +14054,36 @@ var ProjectRosterService = class {
13799
14054
  }
13800
14055
  await this.deps.bindingLifecycle.ensure(membership);
13801
14056
  }
13802
- async applyUpdate(actor2, target, patch, expectedUpdatedAt2) {
14057
+ async applyUpdate(actor3, target, patch, expectedUpdatedAt2) {
13803
14058
  let result;
13804
14059
  try {
13805
- result = await this.deps.membershipStore.update({ tenantId: actor2.tenantId, id: target.id, patch, expectedUpdatedAt: expectedUpdatedAt2 });
14060
+ result = await this.deps.membershipStore.update({ tenantId: actor3.tenantId, id: target.id, patch, expectedUpdatedAt: expectedUpdatedAt2 });
13806
14061
  } catch (error) {
13807
14062
  if (error instanceof Error && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
13808
14063
  throw error;
13809
14064
  }
13810
14065
  if (result.kind === "updated") {
13811
- this.validateMembership(result.membership, actor2, { id: target.roomId, workspaceId: target.workspaceId }, target.assistantId, target.roomThreadId);
14066
+ this.validateMembership(result.membership, actor3, { id: target.roomId, workspaceId: target.workspaceId }, target.assistantId, target.roomThreadId);
13812
14067
  return result.membership;
13813
14068
  }
13814
14069
  if (result.kind === "not_found") throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
13815
14070
  if (result.kind === "conflict") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster member changed; reload before saving");
13816
14071
  throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
13817
14072
  }
13818
- async authorize(actor2, capability2) {
13819
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: capability2 });
14073
+ async authorize(actor3, capability2) {
14074
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: capability2 });
13820
14075
  }
13821
- async target(actor2, id3) {
13822
- const target = await this.deps.membershipStore.findById(actor2.tenantId, id3);
13823
- if (!target || target.tenantId !== actor2.tenantId || target.projectId !== actor2.projectId) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
13824
- const room = await this.deps.roomService.ensureMainRoom(actor2);
13825
- const assistant = await this.deps.assistantStore.getAssistantById(actor2.tenantId, target.assistantId);
13826
- if (!assistant || assistant.tenantId !== actor2.tenantId) throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Assistant not found");
14076
+ async target(actor3, id3) {
14077
+ const target = await this.deps.membershipStore.findById(actor3.tenantId, id3);
14078
+ if (!target || target.tenantId !== actor3.tenantId || target.projectId !== actor3.projectId) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
14079
+ const room = await this.deps.roomService.ensureMainRoom(actor3);
14080
+ const assistant = await this.deps.assistantStore.getAssistantById(actor3.tenantId, target.assistantId);
14081
+ if (!assistant || assistant.tenantId !== actor3.tenantId) throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Assistant not found");
13827
14082
  if (!target.roomThreadId) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster member immutable scope mismatch");
13828
- this.validateMembership(target, actor2, room, target.assistantId, target.roomThreadId);
13829
- const thread = await this.deps.threadStore.getThreadById(actor2.tenantId, target.roomThreadId);
14083
+ this.validateMembership(target, actor3, room, target.assistantId, target.roomThreadId);
14084
+ const thread = await this.deps.threadStore.getThreadById(actor3.tenantId, target.roomThreadId);
13830
14085
  if (!thread) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread not found");
13831
- this.validateThread(thread, actor2.tenantId, this.expectedMetadata(room, actor2, target.id, target.assistantId), target.roomThreadId);
14086
+ this.validateThread(thread, actor3.tenantId, this.expectedMetadata(room, actor3, target.id, target.assistantId), target.roomThreadId);
13832
14087
  return target;
13833
14088
  }
13834
14089
  requireCurrent(target, expected) {
@@ -13854,56 +14109,55 @@ var ProjectRosterService = class {
13854
14109
  return normalized;
13855
14110
  }
13856
14111
  metadata(metadata, expected) {
13857
- if (!metadata || Object.keys(metadata).length !== metadataKeys.length || metadataKeys.some((key) => !Object.prototype.hasOwnProperty.call(metadata, key))) return false;
13858
- return metadata.source === expected.source && metadata.kind === expected.kind && metadata.workspaceId === expected.workspaceId && metadata.projectId === expected.projectId && metadata.roomId === expected.roomId && metadata.membershipId === expected.membershipId && metadata.assistantId === expected.assistantId;
14112
+ return isRoomPresenceMetadata(metadata, expected);
13859
14113
  }
13860
- expectedMetadata(room, actor2, membershipId, assistantId) {
13861
- return { source: "project_room", kind: "room_presence", workspaceId: room.workspaceId, projectId: actor2.projectId, roomId: room.id, membershipId, assistantId };
14114
+ expectedMetadata(room, actor3, membershipId, assistantId) {
14115
+ return roomPresenceMetadata({ workspaceId: room.workspaceId, projectId: actor3.projectId, roomId: room.id, membershipId, assistantId });
13862
14116
  }
13863
- validateMembership(value, actor2, room, assistantId, threadId) {
13864
- if (value.tenantId !== actor2.tenantId || value.projectId !== actor2.projectId || value.workspaceId !== room.workspaceId || value.roomId !== room.id || value.assistantId !== assistantId || threadId !== void 0 && value.roomThreadId !== threadId) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership immutable scope mismatch");
14117
+ validateMembership(value, actor3, room, assistantId, threadId) {
14118
+ if (value.tenantId !== actor3.tenantId || value.projectId !== actor3.projectId || value.workspaceId !== room.workspaceId || value.roomId !== room.id || value.assistantId !== assistantId || threadId !== void 0 && value.roomThreadId !== threadId) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership immutable scope mismatch");
13865
14119
  }
13866
- async findSafePresence(actor2, room, assistantId) {
13867
- const threads = await this.deps.threadStore.getThreadsByAssistantId(actor2.tenantId, assistantId, { source: "project_room", kind: "room_presence", workspaceId: room.workspaceId, projectId: actor2.projectId, roomId: room.id, assistantId });
13868
- const memberships = await this.deps.membershipStore.list(actor2.tenantId, actor2.projectId);
14120
+ async findSafePresence(actor3, room, assistantId) {
14121
+ const threads = await this.deps.threadStore.getThreadsByAssistantId(actor3.tenantId, assistantId, { source: "project_room", kind: "room_presence", workspaceId: room.workspaceId, projectId: actor3.projectId, roomId: room.id, assistantId });
14122
+ const memberships = await this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
13869
14123
  for (const thread of threads) {
13870
14124
  const metadata = thread.metadata;
13871
- if (thread.tenantId !== actor2.tenantId || thread.assistantId !== assistantId || typeof metadata?.membershipId !== "string" || !this.metadata(metadata, this.expectedMetadata(room, actor2, metadata.membershipId, assistantId))) continue;
14125
+ if (thread.tenantId !== actor3.tenantId || thread.assistantId !== assistantId || typeof metadata?.membershipId !== "string" || !this.metadata(metadata, this.expectedMetadata(room, actor3, metadata.membershipId, assistantId))) continue;
13872
14126
  const membershipId = metadata.membershipId;
13873
- const owner = await this.deps.membershipStore.findById(actor2.tenantId, membershipId);
13874
- if (owner && (owner.tenantId !== actor2.tenantId || owner.workspaceId !== room.workspaceId || owner.projectId !== actor2.projectId || owner.roomId !== room.id || owner.roomThreadId !== thread.id || owner.assistantId !== assistantId || owner.id !== membershipId)) continue;
14127
+ const owner = await this.deps.membershipStore.findById(actor3.tenantId, membershipId);
14128
+ if (owner && (owner.tenantId !== actor3.tenantId || owner.workspaceId !== room.workspaceId || owner.projectId !== actor3.projectId || owner.roomId !== room.id || owner.roomThreadId !== thread.id || owner.assistantId !== assistantId || owner.id !== membershipId)) continue;
13875
14129
  if (memberships.some((item) => item.roomThreadId === thread.id)) continue;
13876
14130
  return { membershipId, threadId: thread.id };
13877
14131
  }
13878
14132
  return { membershipId: this.idFactory(), threadId: this.idFactory() };
13879
14133
  }
13880
- async ensureThread(actor2, room, assistantId, membershipId, threadId) {
13881
- const expected = this.expectedMetadata(room, actor2, membershipId, assistantId);
13882
- const existing = await this.deps.threadStore.getThreadById(actor2.tenantId, threadId);
14134
+ async ensureThread(actor3, room, assistantId, membershipId, threadId) {
14135
+ const expected = this.expectedMetadata(room, actor3, membershipId, assistantId);
14136
+ const existing = await this.deps.threadStore.getThreadById(actor3.tenantId, threadId);
13883
14137
  if (existing) {
13884
- this.validateThread(existing, actor2.tenantId, expected, threadId);
14138
+ this.validateThread(existing, actor3.tenantId, expected, threadId);
13885
14139
  return;
13886
14140
  }
13887
- const created = await this.deps.threadStore.createThread(actor2.tenantId, assistantId, threadId, { metadata: expected });
13888
- this.validateThread(created, actor2.tenantId, expected, threadId);
13889
- const reread = await this.deps.threadStore.getThreadById(actor2.tenantId, threadId);
14141
+ const created = await this.deps.threadStore.createThread(actor3.tenantId, assistantId, threadId, { metadata: expected });
14142
+ this.validateThread(created, actor3.tenantId, expected, threadId);
14143
+ const reread = await this.deps.threadStore.getThreadById(actor3.tenantId, threadId);
13890
14144
  if (!reread) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread was not persisted");
13891
- this.validateThread(reread, actor2.tenantId, expected, threadId);
14145
+ this.validateThread(reread, actor3.tenantId, expected, threadId);
13892
14146
  }
13893
14147
  validateThread(thread, tenantId, expected, threadId) {
13894
14148
  if (thread.id !== threadId || thread.tenantId !== tenantId || thread.assistantId !== expected.assistantId || !this.metadata(thread.metadata, expected)) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread identity mismatch");
13895
14149
  }
13896
14150
  /** Serializes roster writes with task ownership mutations in this process. */
13897
- withLock(actor2, operation) {
14151
+ withLock(actor3, operation) {
13898
14152
  return this.mutationMutex.runExclusive(
13899
- { tenantId: actor2.tenantId, projectId: actor2.projectId },
14153
+ { tenantId: actor3.tenantId, projectId: actor3.projectId },
13900
14154
  operation
13901
14155
  );
13902
14156
  }
13903
14157
  };
13904
14158
 
13905
14159
  // src/services/collaboration/RoomMessageService.ts
13906
- import { createHash as createHash4, randomUUID as randomUUID15 } from "crypto";
14160
+ import { createHash as createHash4, randomUUID as randomUUID16 } from "crypto";
13907
14161
 
13908
14162
  // src/services/collaboration/ProjectBotMembershipSnapshot.ts
13909
14163
  import { snapshotExactRecord as snapshotExactRecord4 } from "@axiom-lattice/protocols";
@@ -13992,54 +14246,54 @@ var RoomMessageService = class {
13992
14246
  constructor(deps, options = {}) {
13993
14247
  this.deps = deps;
13994
14248
  this.idempotentOutcomes = /* @__PURE__ */ new Map();
13995
- this.idFactory = deps.idFactory ?? randomUUID15;
14249
+ this.idFactory = deps.idFactory ?? randomUUID16;
13996
14250
  const limit = options.idempotentOutcomeLimit ?? 1e3;
13997
14251
  if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("idempotentOutcomeLimit must be a positive safe integer");
13998
14252
  this.idempotentOutcomeLimit = limit;
13999
14253
  }
14000
14254
  /** Lists newest-first messages strictly before an optional cursor. */
14001
- async list(actor2, input = {}) {
14255
+ async list(actor3, input = {}) {
14002
14256
  const snapshot = this.snapshotList(input);
14003
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "read" });
14004
- const room = await this.room(actor2, access.project);
14005
- const messages = await this.deps.messageStore.list({ tenantId: actor2.tenantId, roomId: room.id, ...snapshot.before === void 0 ? {} : { before: snapshot.before }, limit: snapshot.limit });
14006
- return messages.map((message) => this.validateMessage(message, actor2, room));
14257
+ const access = await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "read" });
14258
+ const room = await this.room(actor3, access.project);
14259
+ const messages = await this.deps.messageStore.list({ tenantId: actor3.tenantId, roomId: room.id, ...snapshot.before === void 0 ? {} : { before: snapshot.before }, limit: snapshot.limit });
14260
+ return messages.map((message) => this.validateMessage(message, actor3, room));
14007
14261
  }
14008
14262
  /**
14009
14263
  * Validates and persists a human-authored text message without dispatching it.
14010
14264
  * @deprecated This persistence primitive remains for the Phase 1 route boundary; the later
14011
14265
  * route task will switch POST handling to createAndDispatch.
14012
14266
  */
14013
- async createHumanMessage(actor2, input) {
14014
- return (await this.persistHumanMessage(actor2, input, false)).message;
14267
+ async createHumanMessage(actor3, input) {
14268
+ return (await this.persistHumanMessage(actor3, input, false)).message;
14015
14269
  }
14016
- async persistHumanMessage(actor2, input, allowIdempotencyKey) {
14017
- const snapshot = this.snapshotCreate(actor2, input, allowIdempotencyKey);
14018
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14019
- const room = await this.room(actor2, access.project);
14020
- return this.persistNormalizedHumanMessage(actor2, snapshot, room);
14270
+ async persistHumanMessage(actor3, input, allowIdempotencyKey) {
14271
+ const snapshot = this.snapshotCreate(actor3, input, allowIdempotencyKey);
14272
+ const access = await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14273
+ const room = await this.room(actor3, access.project);
14274
+ return this.persistNormalizedHumanMessage(actor3, snapshot, room);
14021
14275
  }
14022
- async persistNormalizedHumanMessage(actor2, snapshot, room) {
14023
- for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor2, room, membershipId);
14276
+ async persistNormalizedHumanMessage(actor3, snapshot, room) {
14277
+ for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor3, room, membershipId);
14024
14278
  let reply = null;
14025
14279
  if (snapshot.replyToMessageId !== void 0) {
14026
- reply = await this.deps.messageStore.findById(actor2.tenantId, snapshot.replyToMessageId);
14027
- if (!reply || reply.id !== snapshot.replyToMessageId || !this.isMessage(reply, actor2, room)) throw new RoomMessageServiceError("INVALID_REPLY_TARGET", 404, "Reply target not found");
14280
+ reply = await this.deps.messageStore.findById(actor3.tenantId, snapshot.replyToMessageId);
14281
+ if (!reply || reply.id !== snapshot.replyToMessageId || !this.isMessage(reply, actor3, room)) throw new RoomMessageServiceError("INVALID_REPLY_TARGET", 404, "Reply target not found");
14028
14282
  }
14029
- for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor2, room, membershipId);
14030
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14283
+ for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor3, room, membershipId);
14284
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14031
14285
  const id3 = this.idFactory();
14032
14286
  let result;
14033
- const candidate = { id: id3, tenantId: actor2.tenantId, workspaceId: room.workspaceId, projectId: actor2.projectId, roomId: room.id, author: { type: "human", userId: actor2.userId }, content: { type: "text", text: snapshot.text }, mentions: snapshot.mentions, ...snapshot.replyToMessageId === void 0 ? {} : { replyToMessageId: snapshot.replyToMessageId }, source: "user", ...snapshot.idempotencyKey === void 0 ? {} : { idempotencyKey: snapshot.idempotencyKey } };
14287
+ const candidate = { id: id3, tenantId: actor3.tenantId, workspaceId: room.workspaceId, projectId: actor3.projectId, roomId: room.id, author: { type: "human", userId: actor3.userId }, content: { type: "text", text: snapshot.text }, mentions: snapshot.mentions, ...snapshot.replyToMessageId === void 0 ? {} : { replyToMessageId: snapshot.replyToMessageId }, source: "user", ...snapshot.idempotencyKey === void 0 ? {} : { idempotencyKey: snapshot.idempotencyKey } };
14034
14288
  try {
14035
14289
  result = snapshot.idempotencyKey === void 0 ? await this.deps.messageStore.create(candidate) : await this.deps.messageStore.createIdempotent({ ...candidate, idempotencyKey: snapshot.idempotencyKey });
14036
14290
  } catch (error) {
14037
14291
  if (error instanceof Error && /Message.*Conflict|Conflict.*Message|DuplicateProjectRoomMessage/.test(error.name)) throw new RoomMessageServiceError("MESSAGE_CONFLICT", 409, "Message conflict");
14038
14292
  throw error;
14039
14293
  }
14040
- const validated = this.validateMessage(result, actor2, room, result.id === id3 ? id3 : void 0);
14294
+ const validated = this.validateMessage(result, actor3, room, result.id === id3 ? id3 : void 0);
14041
14295
  const replayed = result.id !== id3;
14042
- if (validated.author.type !== "human" || validated.author.userId !== actor2.userId || validated.source !== "user" || validated.sourceId !== void 0 || snapshot.idempotencyKey === void 0 && validated.idempotencyKey !== void 0 || snapshot.idempotencyKey !== void 0 && validated.idempotencyKey !== snapshot.idempotencyKey) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid created message");
14296
+ if (validated.author.type !== "human" || validated.author.userId !== actor3.userId || validated.source !== "user" || validated.sourceId !== void 0 || snapshot.idempotencyKey === void 0 && validated.idempotencyKey !== void 0 || snapshot.idempotencyKey !== void 0 && validated.idempotencyKey !== snapshot.idempotencyKey) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid created message");
14043
14297
  if (!matchesNormalizedPayload(validated, snapshot)) {
14044
14298
  if (replayed && snapshot.idempotencyKey !== void 0) throw new RoomMessageServiceError("IDEMPOTENCY_KEY_REUSED", 409, "Idempotency key reused with different message input");
14045
14299
  throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid created message");
@@ -14051,11 +14305,11 @@ var RoomMessageService = class {
14051
14305
  * Idempotent outcomes are single-flight and retained only in this service process; a
14052
14306
  * store replay after restart reports `DISPATCH_STATUS_UNKNOWN` until explicitly retried.
14053
14307
  */
14054
- async createAndDispatch(actor2, input) {
14055
- const normalized = this.snapshotCreate(actor2, input, true);
14056
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14057
- const room = await this.room(actor2, access.project);
14058
- const key = this.idempotencyMapKey(actor2, room, normalized);
14308
+ async createAndDispatch(actor3, input) {
14309
+ const normalized = this.snapshotCreate(actor3, input, true);
14310
+ const access = await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14311
+ const room = await this.room(actor3, access.project);
14312
+ const key = this.idempotencyMapKey(actor3, room, normalized);
14059
14313
  const requestFingerprint = fingerprintRequest(normalized);
14060
14314
  if (key !== void 0) {
14061
14315
  const existing = this.idempotentOutcomes.get(key);
@@ -14065,7 +14319,7 @@ var RoomMessageService = class {
14065
14319
  return this.resultForCaller(await existing.promise, true);
14066
14320
  }
14067
14321
  this.admitIdempotentEntry();
14068
- const promise = this.createAndDispatchOutcome(actor2, normalized, room);
14322
+ const promise = this.createAndDispatchOutcome(actor3, normalized, room);
14069
14323
  const entry = { requestFingerprint, promise, settled: false };
14070
14324
  this.idempotentOutcomes.set(key, entry);
14071
14325
  promise.then(() => this.settleIdempotentOutcome(key, entry), () => {
@@ -14074,16 +14328,16 @@ var RoomMessageService = class {
14074
14328
  const resolved = await promise;
14075
14329
  return this.resultForCaller(resolved, resolved.replayed);
14076
14330
  }
14077
- const outcome = await this.createAndDispatchOutcome(actor2, normalized, room);
14331
+ const outcome = await this.createAndDispatchOutcome(actor3, normalized, room);
14078
14332
  return this.resultForCaller(outcome, outcome.replayed);
14079
14333
  }
14080
- async createAndDispatchOutcome(actor2, input, room) {
14081
- const persisted = await this.persistNormalizedHumanMessage(actor2, input, room);
14334
+ async createAndDispatchOutcome(actor3, input, room) {
14335
+ const persisted = await this.persistNormalizedHumanMessage(actor3, input, room);
14082
14336
  if (persisted.replayed) return { message: persisted.message, dispatch: [{ success: false, errorCode: "DISPATCH_STATUS_UNKNOWN" }], replayed: true };
14083
14337
  const message = persisted.message;
14084
14338
  this.notifyMessageCreated(message);
14085
14339
  try {
14086
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14340
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14087
14341
  } catch (error) {
14088
14342
  return { message, dispatch: [{ success: false, errorCode: error instanceof ProjectCollaborationError && error.code === "PROJECT_ACCESS_DENIED" ? "AUTHORIZATION_REVOKED" : "DISPATCH_FAILED" }], replayed: false };
14089
14343
  }
@@ -14102,8 +14356,8 @@ var RoomMessageService = class {
14102
14356
  });
14103
14357
  });
14104
14358
  }
14105
- idempotencyMapKey(actor2, room, input) {
14106
- return input.idempotencyKey === void 0 ? void 0 : `${actor2.tenantId}:${room.id}:${input.idempotencyKey}`;
14359
+ idempotencyMapKey(actor3, room, input) {
14360
+ return input.idempotencyKey === void 0 ? void 0 : `${actor3.tenantId}:${room.id}:${input.idempotencyKey}`;
14107
14361
  }
14108
14362
  settleIdempotentOutcome(key, entry) {
14109
14363
  if (this.idempotentOutcomes.get(key) !== entry) return;
@@ -14128,14 +14382,14 @@ var RoomMessageService = class {
14128
14382
  return { message: cloneRoomMessage(outcome.message), dispatch: outcome.dispatch.map((entry) => ({ ...entry })), replayed };
14129
14383
  }
14130
14384
  /** Retries an existing human message using the current roster without creating a copy. */
14131
- async retry(actor2, messageId) {
14132
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14133
- const message = await this.deps.messageStore.findById(actor2.tenantId, messageId);
14134
- const room = await this.room(actor2, access.project);
14135
- if (!message || message.id !== messageId || !this.isMessage(message, actor2, room) || message.author.type !== "human" || message.source !== "user") {
14385
+ async retry(actor3, messageId) {
14386
+ const access = await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14387
+ const message = await this.deps.messageStore.findById(actor3.tenantId, messageId);
14388
+ const room = await this.room(actor3, access.project);
14389
+ if (!message || message.id !== messageId || !this.isMessage(message, actor3, room) || message.author.type !== "human" || message.source !== "user") {
14136
14390
  throw new RoomMessageServiceError("INVALID_RETRY_SOURCE", 404, "Retry source not found");
14137
14391
  }
14138
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14392
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14139
14393
  return this.safeDispatch(message);
14140
14394
  }
14141
14395
  async safeDispatch(message) {
@@ -14153,7 +14407,7 @@ var RoomMessageService = class {
14153
14407
  if (typeof limit !== "number" || !Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid message limit");
14154
14408
  return { ...before === void 0 ? {} : { before }, limit: Math.min(100, limit) };
14155
14409
  }
14156
- snapshotCreate(actor2, input, allowIdempotencyKey = false) {
14410
+ snapshotCreate(actor3, input, allowIdempotencyKey = false) {
14157
14411
  const allowed = allowIdempotencyKey ? ["text", "mentions", "replyToMessageId", "requestIdempotencyKey"] : ["text", "mentions", "replyToMessageId"];
14158
14412
  if (typeof input !== "object" || input === null || Array.isArray(input) || Object.keys(input).some((key) => !allowed.includes(key)) || !Object.prototype.hasOwnProperty.call(input, "text")) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid message input");
14159
14413
  const value = input;
@@ -14173,35 +14427,35 @@ var RoomMessageService = class {
14173
14427
  let idempotencyKey;
14174
14428
  if (value.requestIdempotencyKey !== void 0) {
14175
14429
  if (typeof value.requestIdempotencyKey !== "string" || !/^[\x21-\x7e]{1,200}$/.test(value.requestIdempotencyKey.trim())) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid idempotency key");
14176
- idempotencyKey = `room-human:${actor2.userId}:${createHash4("sha256").update(value.requestIdempotencyKey.trim(), "utf8").digest("hex")}`;
14430
+ idempotencyKey = `room-human:${actor3.userId}:${createHash4("sha256").update(value.requestIdempotencyKey.trim(), "utf8").digest("hex")}`;
14177
14431
  }
14178
14432
  return { text, mentions, botIds, ...value.replyToMessageId === void 0 ? {} : { replyToMessageId: value.replyToMessageId }, ...idempotencyKey === void 0 ? {} : { idempotencyKey } };
14179
14433
  }
14180
- async room(actor2, project) {
14181
- if (!project || project.tenantId !== actor2.tenantId || project.id !== actor2.projectId || !nonempty(project.id) || !nonempty(project.workspaceId)) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid authorized project");
14182
- const room = await this.deps.roomService.ensureMainRoom(actor2);
14183
- if (!exact(room, ["createdAt", "id", "name", "projectId", "tenantId", "type", "updatedAt", "workspaceId"]) || room.tenantId !== actor2.tenantId || room.projectId !== actor2.projectId || room.workspaceId !== project.workspaceId || room.type !== "main" || !nonempty(room.id) || !nonempty(room.name) || !validDate(room.createdAt) || !validDate(room.updatedAt)) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid room result");
14434
+ async room(actor3, project) {
14435
+ if (!project || project.tenantId !== actor3.tenantId || project.id !== actor3.projectId || !nonempty(project.id) || !nonempty(project.workspaceId)) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid authorized project");
14436
+ const room = await this.deps.roomService.ensureMainRoom(actor3);
14437
+ if (!exact(room, ["createdAt", "id", "name", "projectId", "tenantId", "type", "updatedAt", "workspaceId"]) || room.tenantId !== actor3.tenantId || room.projectId !== actor3.projectId || room.workspaceId !== project.workspaceId || room.type !== "main" || !nonempty(room.id) || !nonempty(room.name) || !validDate(room.createdAt) || !validDate(room.updatedAt)) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid room result");
14184
14438
  return room;
14185
14439
  }
14186
- async validateMentionIdentity(actor2, room, id3) {
14187
- const membership = await this.deps.botStore.findById(actor2.tenantId, id3);
14440
+ async validateMentionIdentity(actor3, room, id3) {
14441
+ const membership = await this.deps.botStore.findById(actor3.tenantId, id3);
14188
14442
  const snapshot = snapshotProjectBotMembership(membership);
14189
- if (!snapshot || snapshot.id !== id3 || !this.matchesRoom(snapshot, actor2, room)) throw new RoomMessageServiceError("UNKNOWN_MENTION", 404, "Mentioned bot membership not found");
14443
+ if (!snapshot || snapshot.id !== id3 || !this.matchesRoom(snapshot, actor3, room)) throw new RoomMessageServiceError("UNKNOWN_MENTION", 404, "Mentioned bot membership not found");
14190
14444
  }
14191
- matchesRoom(membership, actor2, room) {
14192
- return membership.tenantId === actor2.tenantId && membership.projectId === actor2.projectId && membership.workspaceId === room.workspaceId && membership.roomId === room.id;
14445
+ matchesRoom(membership, actor3, room) {
14446
+ return membership.tenantId === actor3.tenantId && membership.projectId === actor3.projectId && membership.workspaceId === room.workspaceId && membership.roomId === room.id;
14193
14447
  }
14194
- isMessage(message, actor2, room) {
14448
+ isMessage(message, actor3, room) {
14195
14449
  try {
14196
- this.validateMessage(message, actor2, room);
14450
+ this.validateMessage(message, actor3, room);
14197
14451
  return true;
14198
14452
  } catch {
14199
14453
  return false;
14200
14454
  }
14201
14455
  }
14202
- validateMessage(message, actor2, room, expectedId) {
14456
+ validateMessage(message, actor3, room, expectedId) {
14203
14457
  if (!exactFields(message, ["author", "content", "createdAt", "id", "mentions", "projectId", "roomId", "source", "tenantId", "workspaceId"], ["replyToMessageId", "sourceId", "idempotencyKey"])) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid stored message");
14204
- if (!nonempty(message.id) || expectedId !== void 0 && message.id !== expectedId || message.tenantId !== actor2.tenantId || message.workspaceId !== room.workspaceId || message.projectId !== actor2.projectId || message.roomId !== room.id || !validDate(message.createdAt) || !nonempty(message.content?.text) || !exact(message.content, ["text", "type"]) || message.content.type !== "text" || !Array.isArray(message.mentions) || !this.validAuthor(message.author) || !["user", "agent", "task", "routine", "system"].includes(message.source) || message.replyToMessageId !== void 0 && !nonempty(message.replyToMessageId) || message.sourceId !== void 0 && !nonempty(message.sourceId) || message.idempotencyKey !== void 0 && !nonempty(message.idempotencyKey)) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid stored message");
14458
+ if (!nonempty(message.id) || expectedId !== void 0 && message.id !== expectedId || message.tenantId !== actor3.tenantId || message.workspaceId !== room.workspaceId || message.projectId !== actor3.projectId || message.roomId !== room.id || !validDate(message.createdAt) || !nonempty(message.content?.text) || !exact(message.content, ["text", "type"]) || message.content.type !== "text" || !Array.isArray(message.mentions) || !this.validAuthor(message.author) || !["user", "agent", "task", "routine", "system"].includes(message.source) || message.replyToMessageId !== void 0 && !nonempty(message.replyToMessageId) || message.sourceId !== void 0 && !nonempty(message.sourceId) || message.idempotencyKey !== void 0 && !nonempty(message.idempotencyKey)) throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid stored message");
14205
14459
  const mentions = message.mentions.map((mention) => {
14206
14460
  if (exact(mention, ["type"]) && mention.type === "team") return { type: "team" };
14207
14461
  if (exact(mention, ["membershipId", "type"]) && mention.type === "bot" && nonempty(mention.membershipId)) return { type: "bot", membershipId: mention.membershipId };
@@ -14244,6 +14498,111 @@ function matchesNormalizedPayload(message, input) {
14244
14498
  import { ZodError as ZodError3 } from "zod";
14245
14499
  import { toProjectRoomPublicMessage } from "@axiom-lattice/protocols";
14246
14500
 
14501
+ // src/services/collaboration/RoomInterruptService.ts
14502
+ import { randomUUID as randomUUID17 } from "crypto";
14503
+
14504
+ // src/router/roomInterrupt.ts
14505
+ var CLARIFY_FENCE = /^```clarify\n([\s\S]*?)\n```$/;
14506
+ function readClarifyInterrupt(state) {
14507
+ if (typeof state !== "object" || state === null) return void 0;
14508
+ const tasks = state.tasks;
14509
+ if (!Array.isArray(tasks)) return void 0;
14510
+ for (const task of tasks) {
14511
+ const interrupts = typeof task === "object" && task !== null ? task.interrupts : void 0;
14512
+ if (!Array.isArray(interrupts)) continue;
14513
+ for (const interrupt of interrupts) {
14514
+ const value = typeof interrupt === "object" && interrupt !== null ? interrupt.value : void 0;
14515
+ if (typeof value !== "string") continue;
14516
+ const match = CLARIFY_FENCE.exec(value.trim());
14517
+ if (!match) continue;
14518
+ try {
14519
+ const parsed = JSON.parse(match[1]);
14520
+ if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.questions)) {
14521
+ return parsed;
14522
+ }
14523
+ } catch {
14524
+ }
14525
+ }
14526
+ }
14527
+ return void 0;
14528
+ }
14529
+ function buildClarifyResumeCommand(text) {
14530
+ return { resume: { data: { answers: [{ questionIndex: 0, otherText: text }] } } };
14531
+ }
14532
+ function buildClarifySkipCommand() {
14533
+ return { resume: { data: { answers: [] } } };
14534
+ }
14535
+
14536
+ // src/services/collaboration/RoomInterruptService.ts
14537
+ var RoomInterruptServiceError = class extends Error {
14538
+ /** Creates a stable room interrupt error. */
14539
+ constructor(code, statusCode, message) {
14540
+ super(message);
14541
+ this.code = code;
14542
+ this.statusCode = statusCode;
14543
+ this.name = "RoomInterruptServiceError";
14544
+ }
14545
+ };
14546
+ var RoomInterruptService = class {
14547
+ /** Creates the service from explicitly injected stores and dispatcher. */
14548
+ constructor(deps) {
14549
+ this.deps = deps;
14550
+ this.idFactory = deps.idFactory ?? randomUUID17;
14551
+ }
14552
+ /** Resolves a pending clarify interrupt with no answers. */
14553
+ async skip(input) {
14554
+ await this.resume(input, buildClarifySkipCommand());
14555
+ }
14556
+ /** Resolves a pending interrupt with an explicit resume command. */
14557
+ async resume(input, command) {
14558
+ await this.deps.requireAccess({ tenantId: input.tenantId, userId: input.userId, projectId: input.projectId });
14559
+ const membership = await this.deps.resolveMembership(input);
14560
+ if (!membership || membership.status !== "active" || !membership.roomThreadId || membership.projectId !== input.projectId) {
14561
+ throw new RoomInterruptServiceError("ROOM_INTERRUPT_TARGET_UNAVAILABLE", 404, "Room interrupt target is unavailable");
14562
+ }
14563
+ const scope = {
14564
+ tenantId: membership.tenantId,
14565
+ assistantId: membership.assistantId,
14566
+ threadId: membership.roomThreadId,
14567
+ workspaceId: membership.workspaceId,
14568
+ projectId: membership.projectId
14569
+ };
14570
+ const status = await this.deps.getThreadStatus(scope);
14571
+ if (String(status) !== "interrupted") {
14572
+ throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
14573
+ }
14574
+ const inputMessageId = this.idFactory();
14575
+ await this.deps.dispatcher.dispatch(
14576
+ {
14577
+ tenant_id: scope.tenantId,
14578
+ assistant_id: scope.assistantId,
14579
+ thread_id: scope.threadId,
14580
+ workspace_id: scope.workspaceId,
14581
+ project_id: scope.projectId
14582
+ },
14583
+ { input: { id: inputMessageId, message: "Clarification answers submitted" }, command },
14584
+ {
14585
+ executionMode: "followup",
14586
+ trustedRunContext: {
14587
+ projectRoom: {
14588
+ tenantId: membership.tenantId,
14589
+ workspaceId: membership.workspaceId,
14590
+ projectId: membership.projectId,
14591
+ roomId: membership.roomId,
14592
+ sourceRoomMessageId: `interrupt:${membership.id}`,
14593
+ membershipId: membership.id,
14594
+ assistantId: membership.assistantId,
14595
+ inputMessageId,
14596
+ role: membership.role,
14597
+ title: membership.title,
14598
+ ...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
14599
+ }
14600
+ }
14601
+ }
14602
+ );
14603
+ }
14604
+ };
14605
+
14247
14606
  // src/schemas/project-room.ts
14248
14607
  import { z as z9 } from "zod";
14249
14608
  var id2 = z9.string().trim().min(1).max(128);
@@ -14284,6 +14643,15 @@ var updateProjectBotSchema = z9.object({
14284
14643
  mentionName: z9.string().optional(),
14285
14644
  expectedUpdatedAt: isoDate
14286
14645
  }).strict().refine((value) => value.role !== void 0 || value.title !== void 0 || value.responsibility !== void 0 || value.mentionName !== void 0, { message: "At least one mutable field is required" });
14646
+ var resumeInterruptSchema = z9.object({
14647
+ membershipId: id2,
14648
+ answers: z9.array(z9.object({
14649
+ questionIndex: z9.number().int().nonnegative(),
14650
+ otherText: z9.string().optional(),
14651
+ selectedOptions: z9.array(z9.string()).optional(),
14652
+ filePath: z9.string().optional()
14653
+ }).strict()).optional()
14654
+ }).strict();
14287
14655
 
14288
14656
  // src/routes/project-rooms.ts
14289
14657
  var ProjectRoomAuthError = class extends Error {
@@ -14294,7 +14662,11 @@ var ProjectRoomAuthError = class extends Error {
14294
14662
  }
14295
14663
  };
14296
14664
  function registerProjectRoomRoutes(app2, deps) {
14297
- app2.get("/api/projects/:projectId/room", async (request, reply) => run(reply, async () => ({ data: await deps.roomService.ensureMainRoom(actor(request, request.params.projectId)) })));
14665
+ app2.get("/api/projects/:projectId/room", async (request, reply) => run(reply, async () => {
14666
+ const requestActor = actor(request, request.params.projectId);
14667
+ const room = deps.roomService.ensureMainRoomWithCoordinator ? await deps.roomService.ensureMainRoomWithCoordinator(requestActor) : await deps.roomService.ensureMainRoom(requestActor);
14668
+ return { data: room };
14669
+ }));
14298
14670
  app2.get("/api/projects/:projectId/room/messages", async (request, reply) => run(reply, async () => {
14299
14671
  const params = projectRoomParamsSchema.parse(request.params);
14300
14672
  const query = roomMessagesQuerySchema.parse(request.query);
@@ -14326,6 +14698,16 @@ function registerProjectRoomRoutes(app2, deps) {
14326
14698
  await deps.readStateService.mark(actor(request, params.projectId));
14327
14699
  return { data: { marked: true } };
14328
14700
  }));
14701
+ app2.post("/api/projects/:projectId/room/interrupts/resume", async (request, reply) => run(reply, async () => {
14702
+ const params = projectRoomParamsSchema.parse(request.params);
14703
+ const requestActor = actor(request, params.projectId);
14704
+ const body = resumeInterruptSchema.parse(request.body ?? {});
14705
+ await deps.interruptService.resume(
14706
+ { tenantId: requestActor.tenantId, projectId: requestActor.projectId, userId: requestActor.userId, membershipId: body.membershipId },
14707
+ { resume: { data: { answers: body.answers ?? [] } } }
14708
+ );
14709
+ return { data: { resolved: true } };
14710
+ }));
14329
14711
  app2.get("/api/projects/:projectId/members", async (request, reply) => run(reply, async () => {
14330
14712
  const memberships = await deps.membershipService.list(actor(request, projectRoomParamsSchema.parse(request.params).projectId));
14331
14713
  const enriched = await Promise.all(memberships.map(async (membership) => {
@@ -14366,6 +14748,7 @@ async function run(reply, operation) {
14366
14748
  return reply.code(result.statusCode ?? 200).send({ success: true, data: result.data });
14367
14749
  } catch (error) {
14368
14750
  if (error instanceof ZodError3) return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
14751
+ if (error instanceof RoomInterruptServiceError) return reply.code(error.statusCode).send({ success: false, error: { code: error.code, message: error.message, retryable: false } });
14369
14752
  if (error instanceof ProjectRoomAuthError || error instanceof ProjectCollaborationError || error instanceof ProjectMembershipServiceError || error instanceof ProjectRosterServiceError || error instanceof RoomMessageServiceError) return reply.code(error.statusCode).send({ success: false, error: { code: error.code, message: error.message, retryable: error instanceof RoomMessageServiceError && error.retryable } });
14370
14753
  return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
14371
14754
  }
@@ -14419,7 +14802,7 @@ async function execute(reply, operation) {
14419
14802
  }
14420
14803
 
14421
14804
  // src/routes/cospace.ts
14422
- import { randomUUID as randomUUID16 } from "crypto";
14805
+ import { randomUUID as randomUUID18 } from "crypto";
14423
14806
  import { ZodError as ZodError5 } from "zod";
14424
14807
  import {
14425
14808
  generateDirectHumanProjectId,
@@ -14453,16 +14836,16 @@ async function computeUnread(deps, tenantId, userId, roomId, lastMessage) {
14453
14836
  });
14454
14837
  }
14455
14838
  function registerCoSpaceRoutes(app2, deps) {
14456
- const idFactory = deps.idFactory ?? (() => randomUUID16());
14839
+ const idFactory = deps.idFactory ?? (() => randomUUID18());
14457
14840
  app2.get(
14458
14841
  "/api/workspaces/:workspaceId/rooms",
14459
14842
  async (request, reply) => {
14460
14843
  return run2(reply, async () => {
14461
- const actor2 = getActor2(request);
14844
+ const actor3 = getActor2(request);
14462
14845
  const params = workspaceParamsSchema.parse(request.params);
14463
14846
  const query = workspaceRoomsQuerySchema.parse(request.query);
14464
14847
  const workspace = await deps.workspaceStore.getWorkspaceById(
14465
- actor2.tenantId,
14848
+ actor3.tenantId,
14466
14849
  params.workspaceId
14467
14850
  );
14468
14851
  if (!workspace) {
@@ -14473,16 +14856,16 @@ function registerCoSpaceRoutes(app2, deps) {
14473
14856
  );
14474
14857
  }
14475
14858
  const allProjects = await deps.projectStore.getProjectsByWorkspace(
14476
- actor2.tenantId,
14859
+ actor3.tenantId,
14477
14860
  params.workspaceId
14478
14861
  );
14479
14862
  const accessibleProjects = [];
14480
14863
  for (const project of allProjects) {
14481
14864
  if (project.kind === "public") continue;
14482
14865
  const membership = await deps.membershipStore.findByUser(
14483
- actor2.tenantId,
14866
+ actor3.tenantId,
14484
14867
  project.id,
14485
- actor2.userId
14868
+ actor3.userId
14486
14869
  );
14487
14870
  if (membership && membership.status === "active") {
14488
14871
  accessibleProjects.push(project);
@@ -14492,19 +14875,19 @@ function registerCoSpaceRoutes(app2, deps) {
14492
14875
  for (const project of accessibleProjects) {
14493
14876
  try {
14494
14877
  const room = await deps.roomStore.getMainRoom(
14495
- actor2.tenantId,
14878
+ actor3.tenantId,
14496
14879
  project.id
14497
14880
  );
14498
14881
  if (!room) continue;
14499
14882
  const messages = await deps.messageStore.list({
14500
- tenantId: actor2.tenantId,
14883
+ tenantId: actor3.tenantId,
14501
14884
  roomId: room.id,
14502
14885
  limit: 1
14503
14886
  });
14504
14887
  const lastMessage = messages[0];
14505
14888
  const publicLastMessage = lastMessage === void 0 ? void 0 : toProjectRoomPublicMessage2(lastMessage);
14506
14889
  const memberships = await deps.membershipStore.list(
14507
- actor2.tenantId,
14890
+ actor3.tenantId,
14508
14891
  project.id
14509
14892
  );
14510
14893
  const participantNames = [];
@@ -14514,7 +14897,7 @@ function registerCoSpaceRoutes(app2, deps) {
14514
14897
  const user = await deps.userStore.getUserById(m.userId);
14515
14898
  if (user) {
14516
14899
  participantNames.push(user.name);
14517
- if (m.userId !== actor2.userId) otherHumanNames.push(user.name);
14900
+ if (m.userId !== actor3.userId) otherHumanNames.push(user.name);
14518
14901
  }
14519
14902
  }
14520
14903
  const displayName = project.kind === "personal" && otherHumanNames.length === 1 ? otherHumanNames[0] : void 0;
@@ -14529,8 +14912,8 @@ function registerCoSpaceRoutes(app2, deps) {
14529
14912
  ...displayName === void 0 ? {} : { displayName },
14530
14913
  unreadCount: await computeUnread(
14531
14914
  deps,
14532
- actor2.tenantId,
14533
- actor2.userId,
14915
+ actor3.tenantId,
14916
+ actor3.userId,
14534
14917
  room.id,
14535
14918
  lastMessage
14536
14919
  )
@@ -14545,16 +14928,16 @@ function registerCoSpaceRoutes(app2, deps) {
14545
14928
  return bTime - aTime;
14546
14929
  });
14547
14930
  try {
14548
- const publicRoom = await deps.publicRoom.ensure(actor2.tenantId, params.workspaceId);
14931
+ const publicRoom = await deps.publicRoom.ensure(actor3.tenantId, params.workspaceId);
14549
14932
  if (publicRoom) {
14550
14933
  const publicMessages = await deps.messageStore.list({
14551
- tenantId: actor2.tenantId,
14934
+ tenantId: actor3.tenantId,
14552
14935
  roomId: publicRoom.room.id,
14553
14936
  limit: 1
14554
14937
  });
14555
14938
  const publicLastMessage = publicMessages[0];
14556
14939
  const publicMemberships = await deps.membershipStore.list(
14557
- actor2.tenantId,
14940
+ actor3.tenantId,
14558
14941
  publicRoom.project.id
14559
14942
  );
14560
14943
  const publicParticipantNames = [];
@@ -14574,8 +14957,8 @@ function registerCoSpaceRoutes(app2, deps) {
14574
14957
  isPublic: true,
14575
14958
  unreadCount: await computeUnread(
14576
14959
  deps,
14577
- actor2.tenantId,
14578
- actor2.userId,
14960
+ actor3.tenantId,
14961
+ actor3.userId,
14579
14962
  publicRoom.room.id,
14580
14963
  publicLastMessage
14581
14964
  )
@@ -14592,11 +14975,11 @@ function registerCoSpaceRoutes(app2, deps) {
14592
14975
  "/api/workspaces/:workspaceId/direct",
14593
14976
  async (request, reply) => {
14594
14977
  return run2(reply, async () => {
14595
- const actor2 = getActor2(request);
14978
+ const actor3 = getActor2(request);
14596
14979
  const params = workspaceParamsSchema.parse(request.params);
14597
14980
  const body = createDirectRoomSchema.parse(request.body);
14598
14981
  const workspace = await deps.workspaceStore.getWorkspaceById(
14599
- actor2.tenantId,
14982
+ actor3.tenantId,
14600
14983
  params.workspaceId
14601
14984
  );
14602
14985
  if (!workspace) {
@@ -14609,21 +14992,21 @@ function registerCoSpaceRoutes(app2, deps) {
14609
14992
  let projectId;
14610
14993
  if (body.type === "human") {
14611
14994
  projectId = generateDirectHumanProjectId(
14612
- actor2.tenantId,
14995
+ actor3.tenantId,
14613
14996
  params.workspaceId,
14614
- actor2.userId,
14997
+ actor3.userId,
14615
14998
  body.targetUserId
14616
14999
  );
14617
15000
  } else {
14618
15001
  projectId = generateDirectBotProjectId(
14619
- actor2.tenantId,
15002
+ actor3.tenantId,
14620
15003
  params.workspaceId,
14621
- actor2.userId,
15004
+ actor3.userId,
14622
15005
  body.assistantId
14623
15006
  );
14624
15007
  }
14625
15008
  let project = await deps.projectStore.getProjectById(
14626
- actor2.tenantId,
15009
+ actor3.tenantId,
14627
15010
  projectId
14628
15011
  );
14629
15012
  if (!project) {
@@ -14633,14 +15016,14 @@ function registerCoSpaceRoutes(app2, deps) {
14633
15016
  displayName = targetUser?.name ?? body.targetUserId;
14634
15017
  } else {
14635
15018
  const targetAssistant = await deps.assistantStore.getAssistantById(
14636
- actor2.tenantId,
15019
+ actor3.tenantId,
14637
15020
  body.assistantId
14638
15021
  );
14639
15022
  displayName = targetAssistant?.name ?? body.assistantId;
14640
15023
  }
14641
15024
  const name = body.type === "human" ? `Direct with ${displayName}` : `Chat with ${displayName}`;
14642
15025
  project = await deps.projectStore.createProject(
14643
- actor2.tenantId,
15026
+ actor3.tenantId,
14644
15027
  params.workspaceId,
14645
15028
  projectId,
14646
15029
  {
@@ -14651,45 +15034,45 @@ function registerCoSpaceRoutes(app2, deps) {
14651
15034
  }
14652
15035
  const room = await deps.roomStore.ensureMainRoom({
14653
15036
  id: idFactory(),
14654
- tenantId: actor2.tenantId,
15037
+ tenantId: actor3.tenantId,
14655
15038
  workspaceId: params.workspaceId,
14656
15039
  projectId: project.id,
14657
15040
  name: project.name
14658
15041
  });
14659
15042
  let membership = await deps.membershipStore.findByUser(
14660
- actor2.tenantId,
15043
+ actor3.tenantId,
14661
15044
  project.id,
14662
- actor2.userId
15045
+ actor3.userId
14663
15046
  );
14664
15047
  if (!membership) {
14665
15048
  membership = await deps.membershipStore.create({
14666
15049
  id: idFactory(),
14667
- tenantId: actor2.tenantId,
15050
+ tenantId: actor3.tenantId,
14668
15051
  projectId: project.id,
14669
- userId: actor2.userId,
15052
+ userId: actor3.userId,
14670
15053
  role: "owner",
14671
15054
  status: "active"
14672
15055
  });
14673
15056
  } else if (membership.status === "removed") {
14674
15057
  membership = await deps.membershipStore.create({
14675
15058
  id: idFactory(),
14676
- tenantId: actor2.tenantId,
15059
+ tenantId: actor3.tenantId,
14677
15060
  projectId: project.id,
14678
- userId: actor2.userId,
15061
+ userId: actor3.userId,
14679
15062
  role: membership.role,
14680
15063
  status: "active"
14681
15064
  });
14682
15065
  }
14683
15066
  if (body.type === "human" && body.targetUserId) {
14684
15067
  const targetMembership = await deps.membershipStore.findByUser(
14685
- actor2.tenantId,
15068
+ actor3.tenantId,
14686
15069
  project.id,
14687
15070
  body.targetUserId
14688
15071
  );
14689
15072
  if (!targetMembership) {
14690
15073
  await deps.membershipStore.create({
14691
15074
  id: idFactory(),
14692
- tenantId: actor2.tenantId,
15075
+ tenantId: actor3.tenantId,
14693
15076
  projectId: project.id,
14694
15077
  userId: body.targetUserId,
14695
15078
  role: "member",
@@ -14698,7 +15081,7 @@ function registerCoSpaceRoutes(app2, deps) {
14698
15081
  } else if (targetMembership.status === "removed") {
14699
15082
  await deps.membershipStore.create({
14700
15083
  id: idFactory(),
14701
- tenantId: actor2.tenantId,
15084
+ tenantId: actor3.tenantId,
14702
15085
  projectId: project.id,
14703
15086
  userId: body.targetUserId,
14704
15087
  role: targetMembership.role,
@@ -14708,19 +15091,19 @@ function registerCoSpaceRoutes(app2, deps) {
14708
15091
  }
14709
15092
  if (body.type === "bot" && body.assistantId) {
14710
15093
  const existingBotMembership = await deps.botMembershipStore.findByAssistant(
14711
- actor2.tenantId,
15094
+ actor3.tenantId,
14712
15095
  project.id,
14713
15096
  body.assistantId
14714
15097
  );
14715
15098
  if (!existingBotMembership || existingBotMembership.status === "removed") {
14716
15099
  const assistant = await deps.assistantStore.getAssistantById(
14717
- actor2.tenantId,
15100
+ actor3.tenantId,
14718
15101
  body.assistantId
14719
15102
  );
14720
15103
  const title = assistant?.name || "Assistant";
14721
15104
  await deps.botMembershipStore.save({
14722
15105
  id: idFactory(),
14723
- tenantId: actor2.tenantId,
15106
+ tenantId: actor3.tenantId,
14724
15107
  workspaceId: params.workspaceId,
14725
15108
  projectId: project.id,
14726
15109
  roomId: room.id,
@@ -14750,11 +15133,11 @@ function registerCoSpaceRoutes(app2, deps) {
14750
15133
  "/api/workspaces/:workspaceId/groups",
14751
15134
  async (request, reply) => {
14752
15135
  return run2(reply, async () => {
14753
- const actor2 = getActor2(request);
15136
+ const actor3 = getActor2(request);
14754
15137
  const params = workspaceParamsSchema.parse(request.params);
14755
15138
  const body = createGroupRoomSchema.parse(request.body);
14756
15139
  const workspace = await deps.workspaceStore.getWorkspaceById(
14757
- actor2.tenantId,
15140
+ actor3.tenantId,
14758
15141
  params.workspaceId
14759
15142
  );
14760
15143
  if (!workspace) {
@@ -14765,34 +15148,35 @@ function registerCoSpaceRoutes(app2, deps) {
14765
15148
  );
14766
15149
  }
14767
15150
  const project = await deps.projectStore.createProject(
14768
- actor2.tenantId,
15151
+ actor3.tenantId,
14769
15152
  params.workspaceId,
14770
15153
  idFactory(),
14771
15154
  { name: body.name, kind: "business" }
14772
15155
  );
14773
15156
  const room = await deps.roomStore.ensureMainRoom({
14774
15157
  id: idFactory(),
14775
- tenantId: actor2.tenantId,
15158
+ tenantId: actor3.tenantId,
14776
15159
  workspaceId: params.workspaceId,
14777
15160
  projectId: project.id,
14778
15161
  name: project.name
14779
15162
  });
14780
15163
  await deps.membershipStore.create({
14781
15164
  id: idFactory(),
14782
- tenantId: actor2.tenantId,
15165
+ tenantId: actor3.tenantId,
14783
15166
  projectId: project.id,
14784
- userId: actor2.userId,
15167
+ userId: actor3.userId,
14785
15168
  role: "owner",
14786
15169
  status: "active"
14787
15170
  });
14788
- const coordinator = await deps.coordinatorProvisioner.ensure(actor2.tenantId);
15171
+ const coordinator = await deps.coordinatorProvisioner.ensure(actor3.tenantId);
15172
+ const mentionName = await deps.getCoordinatorMentionName?.(actor3.tenantId) ?? ROOM_COORDINATOR_KEY;
14789
15173
  await deps.rosterService.addBot(
14790
- { tenantId: actor2.tenantId, userId: actor2.userId, projectId: project.id },
15174
+ { tenantId: actor3.tenantId, userId: actor3.userId, projectId: project.id },
14791
15175
  {
14792
15176
  assistantId: coordinator.id,
14793
15177
  role: "coordinator",
14794
15178
  title: coordinator.name,
14795
- mentionName: ROOM_COORDINATOR_KEY
15179
+ mentionName
14796
15180
  }
14797
15181
  );
14798
15182
  return { statusCode: 201, data: { project, room } };
@@ -14803,10 +15187,10 @@ function registerCoSpaceRoutes(app2, deps) {
14803
15187
  "/api/workspaces/:workspaceId/orgs/bots",
14804
15188
  async (request, reply) => {
14805
15189
  return run2(reply, async () => {
14806
- const actor2 = getActor2(request);
15190
+ const actor3 = getActor2(request);
14807
15191
  const params = workspaceParamsSchema.parse(request.params);
14808
15192
  const workspace = await deps.workspaceStore.getWorkspaceById(
14809
- actor2.tenantId,
15193
+ actor3.tenantId,
14810
15194
  params.workspaceId
14811
15195
  );
14812
15196
  if (!workspace) {
@@ -14817,18 +15201,18 @@ function registerCoSpaceRoutes(app2, deps) {
14817
15201
  );
14818
15202
  }
14819
15203
  const assistants = await deps.assistantStore.getAllAssistants(
14820
- actor2.tenantId
15204
+ actor3.tenantId
14821
15205
  );
14822
15206
  const bots = [];
14823
15207
  for (const assistant of assistants) {
14824
15208
  const directProjectId = generateDirectBotProjectId(
14825
- actor2.tenantId,
15209
+ actor3.tenantId,
14826
15210
  params.workspaceId,
14827
- actor2.userId,
15211
+ actor3.userId,
14828
15212
  assistant.id
14829
15213
  );
14830
15214
  const existingProject = await deps.projectStore.getProjectById(
14831
- actor2.tenantId,
15215
+ actor3.tenantId,
14832
15216
  directProjectId
14833
15217
  );
14834
15218
  const bot = {
@@ -14840,7 +15224,7 @@ function registerCoSpaceRoutes(app2, deps) {
14840
15224
  if (assistant.createdAt) bot.createdAt = assistant.createdAt.toISOString();
14841
15225
  if (existingProject) {
14842
15226
  const membership = await deps.botMembershipStore.findByAssistant(
14843
- actor2.tenantId,
15227
+ actor3.tenantId,
14844
15228
  directProjectId,
14845
15229
  assistant.id
14846
15230
  );
@@ -14862,10 +15246,10 @@ function registerCoSpaceRoutes(app2, deps) {
14862
15246
  "/api/workspaces/:workspaceId/orgs/members",
14863
15247
  async (request, reply) => {
14864
15248
  return run2(reply, async () => {
14865
- const actor2 = getActor2(request);
15249
+ const actor3 = getActor2(request);
14866
15250
  const params = workspaceParamsSchema.parse(request.params);
14867
15251
  const workspace = await deps.workspaceStore.getWorkspaceById(
14868
- actor2.tenantId,
15252
+ actor3.tenantId,
14869
15253
  params.workspaceId
14870
15254
  );
14871
15255
  if (!workspace) {
@@ -14876,21 +15260,21 @@ function registerCoSpaceRoutes(app2, deps) {
14876
15260
  );
14877
15261
  }
14878
15262
  const userTenantLinks = await deps.userTenantLinkStore.getUsersByTenant(
14879
- actor2.tenantId
15263
+ actor3.tenantId
14880
15264
  );
14881
15265
  const members = [];
14882
15266
  for (const link of userTenantLinks) {
14883
- if (link.userId === actor2.userId) continue;
15267
+ if (link.userId === actor3.userId) continue;
14884
15268
  const user = await deps.userStore.getUserById(link.userId);
14885
15269
  if (!user) continue;
14886
15270
  const directProjectId = generateDirectHumanProjectId(
14887
- actor2.tenantId,
15271
+ actor3.tenantId,
14888
15272
  params.workspaceId,
14889
- actor2.userId,
15273
+ actor3.userId,
14890
15274
  link.userId
14891
15275
  );
14892
15276
  const existingProject = await deps.projectStore.getProjectById(
14893
- actor2.tenantId,
15277
+ actor3.tenantId,
14894
15278
  directProjectId
14895
15279
  );
14896
15280
  const member = {
@@ -14911,11 +15295,11 @@ function registerCoSpaceRoutes(app2, deps) {
14911
15295
  "/api/workspaces/:workspaceId/rooms/:roomId/messages",
14912
15296
  async (request, reply) => {
14913
15297
  return run2(reply, async () => {
14914
- const actor2 = getActor2(request);
15298
+ const actor3 = getActor2(request);
14915
15299
  const params = workspaceRoomParamsSchema.parse(request.params);
14916
15300
  const body = sendMessageSchema.parse(request.body);
14917
15301
  const workspace = await deps.workspaceStore.getWorkspaceById(
14918
- actor2.tenantId,
15302
+ actor3.tenantId,
14919
15303
  params.workspaceId
14920
15304
  );
14921
15305
  if (!workspace) {
@@ -14926,14 +15310,14 @@ function registerCoSpaceRoutes(app2, deps) {
14926
15310
  );
14927
15311
  }
14928
15312
  const projects = await deps.projectStore.getProjectsByWorkspace(
14929
- actor2.tenantId,
15313
+ actor3.tenantId,
14930
15314
  params.workspaceId
14931
15315
  );
14932
15316
  let targetRoom = null;
14933
15317
  let targetProject = null;
14934
15318
  for (const project of projects) {
14935
15319
  const room = await deps.roomStore.getMainRoom(
14936
- actor2.tenantId,
15320
+ actor3.tenantId,
14937
15321
  project.id
14938
15322
  );
14939
15323
  if (room && room.id === params.roomId) {
@@ -14951,11 +15335,11 @@ function registerCoSpaceRoutes(app2, deps) {
14951
15335
  }
14952
15336
  const message = await deps.messageStore.create({
14953
15337
  id: idFactory(),
14954
- tenantId: actor2.tenantId,
15338
+ tenantId: actor3.tenantId,
14955
15339
  workspaceId: params.workspaceId,
14956
15340
  projectId: targetProject.id,
14957
15341
  roomId: targetRoom.id,
14958
- author: { type: "human", userId: actor2.userId },
15342
+ author: { type: "human", userId: actor3.userId },
14959
15343
  content: { type: "text", text: body.text },
14960
15344
  mentions: [],
14961
15345
  source: "user"
@@ -15004,6 +15388,59 @@ function registerCoSpaceRoutes(app2, deps) {
15004
15388
  }
15005
15389
  }
15006
15390
 
15391
+ // src/routes/push-notifications.ts
15392
+ import { ZodError as ZodError6, z as z10 } from "zod";
15393
+ var subscriptionSchema = z10.object({ endpoint: z10.string().min(1), keys: z10.object({ p256dh: z10.string().min(1), auth: z10.string().min(1) }).strict() }).strict();
15394
+ var deleteSchema = z10.object({ endpoint: z10.string().min(1) }).strict();
15395
+ function actor2(request) {
15396
+ const user = request.user;
15397
+ return user?.id && user.tenantId ? { tenantId: user.tenantId, userId: user.id } : null;
15398
+ }
15399
+ function unauthorized() {
15400
+ return { statusCode: 401, body: { success: false, error: { code: "UNAUTHORIZED", message: "Console session required", retryable: false } } };
15401
+ }
15402
+ function registerPushNotificationRoutes(app2, deps) {
15403
+ app2.get("/api/push/vapid-public-key", async (request, reply) => {
15404
+ if (actor2(request) === null) return reply.code(401).send(unauthorized().body);
15405
+ return reply.send({ success: true, data: { publicKey: deps.vapidPublicKey } });
15406
+ });
15407
+ app2.post("/api/push/subscriptions", async (request, reply) => {
15408
+ const current = actor2(request);
15409
+ if (current === null) return reply.code(401).send(unauthorized().body);
15410
+ try {
15411
+ const body = subscriptionSchema.parse(request.body);
15412
+ const userAgent = request.headers["user-agent"];
15413
+ await deps.subscriptionStore.save({
15414
+ tenantId: current.tenantId,
15415
+ userId: current.userId,
15416
+ endpoint: body.endpoint,
15417
+ keys: body.keys,
15418
+ ...typeof userAgent === "string" && userAgent.length > 0 ? { userAgent } : {}
15419
+ });
15420
+ return reply.code(204).send();
15421
+ } catch (error) {
15422
+ if (error instanceof ZodError6) {
15423
+ return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
15424
+ }
15425
+ return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
15426
+ }
15427
+ });
15428
+ app2.delete("/api/push/subscriptions", async (request, reply) => {
15429
+ const current = actor2(request);
15430
+ if (current === null) return reply.code(401).send(unauthorized().body);
15431
+ try {
15432
+ const body = deleteSchema.parse(request.body);
15433
+ await deps.subscriptionStore.delete(current.tenantId, current.userId, body.endpoint);
15434
+ return reply.code(204).send();
15435
+ } catch (error) {
15436
+ if (error instanceof ZodError6) {
15437
+ return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
15438
+ }
15439
+ return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
15440
+ }
15441
+ });
15442
+ }
15443
+
15007
15444
  // src/services/collaboration/InternalRoomInstallationService.ts
15008
15445
  import { v5 as uuidv5 } from "uuid";
15009
15446
  var ROOM_INSTALLATION_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
@@ -15056,22 +15493,6 @@ var InternalRoomInstallationService = class {
15056
15493
  }
15057
15494
  };
15058
15495
 
15059
- // src/services/collaboration/RoomBindingLifecycle.ts
15060
- var NoopRoomBindingLifecycle = class {
15061
- /** Performs no binding work. */
15062
- async ensure(_membership) {
15063
- }
15064
- /** Performs no binding work. */
15065
- async pause(_membership) {
15066
- }
15067
- /** Performs no binding work. */
15068
- async resume(_membership) {
15069
- }
15070
- /** Performs no binding work. */
15071
- async remove(_membership) {
15072
- }
15073
- };
15074
-
15075
15496
  // src/services/collaboration/RoomBindingProvisioner.ts
15076
15497
  var RoomBindingProvisionerError = class extends Error {
15077
15498
  /** Creates a binding provisioning failure with a stable machine-readable code. */
@@ -15258,6 +15679,7 @@ var RoomDispatchService = class {
15258
15679
  async dispatch(message) {
15259
15680
  const source = snapshotDispatchSource(message);
15260
15681
  if (!source) throw new Error("Invalid project room dispatch message");
15682
+ const anchorMessageId = source.sourceId ?? source.id;
15261
15683
  const entries = await this.deps.resolver.resolve(source);
15262
15684
  const results = [];
15263
15685
  for (const entry of entries) {
@@ -15277,7 +15699,7 @@ var RoomDispatchService = class {
15277
15699
  content: { text: source.content.text },
15278
15700
  conversation: { id: membership.roomId, type: "group" },
15279
15701
  replyTarget: { adapterChannel: "room", channelInstallationId: installation.id, rawTarget: {
15280
- sourceRoomMessageId: source.id,
15702
+ sourceRoomMessageId: anchorMessageId,
15281
15703
  targetMembershipId: membership.id,
15282
15704
  inputMessageId: "pending-router-input"
15283
15705
  } }
@@ -15291,7 +15713,7 @@ var RoomDispatchService = class {
15291
15713
  workspaceId: membership.workspaceId,
15292
15714
  projectId: membership.projectId,
15293
15715
  roomId: membership.roomId,
15294
- sourceRoomMessageId: source.id,
15716
+ sourceRoomMessageId: anchorMessageId,
15295
15717
  membershipId: membership.id,
15296
15718
  assistantId: membership.assistantId,
15297
15719
  inputMessageId: "pending-router-input",
@@ -15327,6 +15749,7 @@ function snapshotDispatchSource(value) {
15327
15749
  }
15328
15750
  const mentions = snapshotMentions(values.mentions);
15329
15751
  if (!mentions) return void 0;
15752
+ if (values.sourceId !== void 0 && (typeof values.sourceId !== "string" || values.sourceId.length === 0)) return void 0;
15330
15753
  return {
15331
15754
  id: values.id,
15332
15755
  tenantId: values.tenantId,
@@ -15337,6 +15760,7 @@ function snapshotDispatchSource(value) {
15337
15760
  content: { type: "text", text: content.text },
15338
15761
  mentions,
15339
15762
  source: "user",
15763
+ ...values.sourceId === void 0 ? {} : { sourceId: values.sourceId },
15340
15764
  createdAt
15341
15765
  };
15342
15766
  }
@@ -15366,7 +15790,7 @@ function exactValues(value, required, optional = []) {
15366
15790
  }
15367
15791
 
15368
15792
  // src/services/collaboration/CoordinatorWakeNotifier.ts
15369
- import { randomUUID as randomUUID17 } from "crypto";
15793
+ import { randomUUID as randomUUID19 } from "crypto";
15370
15794
  import { isProjectLifecycleTask as isProjectLifecycleTask2 } from "@axiom-lattice/core";
15371
15795
  import {
15372
15796
  snapshotExactRecord as snapshotExactRecord6
@@ -15383,10 +15807,10 @@ var CoordinatorWakeNotifier = class {
15383
15807
  this.accepted = /* @__PURE__ */ new Set();
15384
15808
  this.maxPending = deps.maxPending ?? 1024;
15385
15809
  if (!Number.isSafeInteger(this.maxPending) || this.maxPending < 1) throw new RangeError("maxPending must be a positive safe integer");
15386
- this.idFactory = deps.idFactory ?? randomUUID17;
15810
+ this.idFactory = deps.idFactory ?? randomUUID19;
15387
15811
  }
15388
15812
  /** Admits one notification without awaiting any store or dispatch operation. */
15389
- handle(value) {
15813
+ handle(value, anchorMessageId) {
15390
15814
  const notification = this.parse(value);
15391
15815
  if (!notification) {
15392
15816
  this.warn("Project task wake notification rejected", { code: "INVALID_COORDINATOR_WAKE_EVENT" });
@@ -15405,7 +15829,7 @@ var CoordinatorWakeNotifier = class {
15405
15829
  if (queue.keys.has(eventKey)) return queue.tail;
15406
15830
  if (this.pendingCount >= this.maxPending) return this.overflow(notification.taskId);
15407
15831
  queue.keys.add(eventKey);
15408
- queue.events.push(notification);
15832
+ queue.events.push({ notification, ...anchorMessageId === void 0 ? {} : { anchorMessageId } });
15409
15833
  this.pendingCount += 1;
15410
15834
  const previousTail = queue.tail;
15411
15835
  queue.tail = previousTail.then(() => this.processQueue(scopeKey3, queue)).catch((error) => {
@@ -15441,17 +15865,17 @@ var CoordinatorWakeNotifier = class {
15441
15865
  }
15442
15866
  async processQueue(scopeKey3, queue) {
15443
15867
  while (queue.events.length > 0) {
15444
- const notification = queue.events.shift();
15868
+ const entry = queue.events.shift();
15445
15869
  try {
15446
- if (!this.closed) await this.wake(notification);
15870
+ if (!this.closed) await this.wake(entry.notification, entry.anchorMessageId);
15447
15871
  } finally {
15448
- queue.keys.delete(notification.eventKey);
15872
+ queue.keys.delete(entry.notification.eventKey);
15449
15873
  this.pendingCount -= 1;
15450
15874
  }
15451
15875
  }
15452
15876
  if (queue.events.length === 0 && this.queues.get(scopeKey3) === queue) this.queues.delete(scopeKey3);
15453
15877
  }
15454
- async wake(notification) {
15878
+ async wake(notification, anchorMessageId) {
15455
15879
  try {
15456
15880
  const task = snapshotTaskItem(await this.deps.tasks.getById(notification.tenantId, notification.taskId));
15457
15881
  if (this.closed) return;
@@ -15473,7 +15897,8 @@ var CoordinatorWakeNotifier = class {
15473
15897
  const coordinator = await this.findActiveCoordinator(room, notification.tenantId, notification.projectId);
15474
15898
  if (this.closed) return;
15475
15899
  if (!coordinator) return;
15476
- await this.dispatchWake(notification, task, room, owner, coordinator);
15900
+ const anchor = anchorMessageId ?? readSourceMessageId(task.context);
15901
+ await this.dispatchWake(notification, task, room, owner, coordinator, anchor);
15477
15902
  } catch (error) {
15478
15903
  this.warn("project-room:coordinator-wake-failed", { code: "COORDINATOR_WAKE_FAILED", taskId: safeId(notification.taskId), error: error instanceof Error ? error.name : "UnknownError" });
15479
15904
  }
@@ -15489,7 +15914,7 @@ var CoordinatorWakeNotifier = class {
15489
15914
  }
15490
15915
  return void 0;
15491
15916
  }
15492
- async dispatchWake(notification, task, room, owner, coordinator) {
15917
+ async dispatchWake(notification, task, room, owner, coordinator, anchorMessageId) {
15493
15918
  const message = {
15494
15919
  id: this.idFactory(),
15495
15920
  tenantId: room.tenantId,
@@ -15500,6 +15925,7 @@ var CoordinatorWakeNotifier = class {
15500
15925
  content: { type: "text", text: coordinatorWakeText(task, owner, notification.status) },
15501
15926
  mentions: [{ type: "bot", membershipId: coordinator.id }],
15502
15927
  source: "user",
15928
+ ...anchorMessageId === void 0 ? {} : { sourceId: anchorMessageId },
15503
15929
  createdAt: /* @__PURE__ */ new Date()
15504
15930
  };
15505
15931
  let results;
@@ -15560,6 +15986,10 @@ Failure: ${task.failureReason}` : "";
15560
15986
  function safeId(value) {
15561
15987
  return typeof value === "string" && value.length > 0 ? value : void 0;
15562
15988
  }
15989
+ function readSourceMessageId(context) {
15990
+ const value = context?.source_message_id;
15991
+ return typeof value === "string" && value.length > 0 ? value : void 0;
15992
+ }
15563
15993
 
15564
15994
  // src/services/collaboration/ProjectTaskExecutionMutex.ts
15565
15995
  import { snapshotExactRecord as snapshotExactRecord7 } from "@axiom-lattice/protocols";
@@ -15640,6 +16070,7 @@ var ProjectTaskExecutionMutex = class {
15640
16070
  };
15641
16071
 
15642
16072
  // src/services/collaboration/ProjectTaskExecutionService.ts
16073
+ import { randomUUID as randomUUID20 } from "crypto";
15643
16074
  import {
15644
16075
  ThreadStatus as ThreadStatus3,
15645
16076
  isProjectLifecycleTask as isProjectLifecycleTask3,
@@ -15907,7 +16338,7 @@ var ProjectTaskExecutionService = class {
15907
16338
  return current;
15908
16339
  }
15909
16340
  async dispatchRun(task, thread, mode, signal) {
15910
- const messageId = mode === "start" ? `project-task:${task.id}:${thread.id}:start` : `project-task:${task.id}:${thread.id}:resume:${task.updatedAt.toISOString()}`;
16341
+ const messageId = (this.options.idFactory ?? randomUUID20)();
15911
16342
  if (signal?.aborted) return this.compensateCancellation(task, thread, messageId, false);
15912
16343
  const settlement = this.armSettlement(task, thread, messageId);
15913
16344
  try {
@@ -15991,7 +16422,7 @@ var ProjectTaskExecutionService = class {
15991
16422
  if (event.messageId !== messageId || !tryClaim()) return;
15992
16423
  cleanup();
15993
16424
  const error = "Project task execution interrupted without durable lifecycle transition";
15994
- void this.withLifecycleTimeout(this.settleInterruptedEvent(task, thread, messageId), {
16425
+ void this.withLifecycleTimeout(this.settleInterruptedEvent(task, thread, messageId, event.state), {
15995
16426
  kind: "failed",
15996
16427
  task,
15997
16428
  thread,
@@ -16063,11 +16494,18 @@ var ProjectTaskExecutionService = class {
16063
16494
  return void 0;
16064
16495
  }
16065
16496
  }
16066
- async settleInterruptedEvent(task, thread, messageId) {
16497
+ async settleInterruptedEvent(task, thread, messageId, state) {
16067
16498
  const current = await this.withLifecycleTimeout(this.reloadTask(task), void 0);
16068
16499
  if (current?.status === "interrupted" && this.matchesExecution(current, task, thread)) {
16069
16500
  return { kind: "interrupted", task: current, thread, messageId };
16070
16501
  }
16502
+ const clarify = readClarifyInterrupt(state ?? await this.readInterruptedThreadState(task, thread));
16503
+ if (clarify !== void 0) {
16504
+ const interrupted = await this.interruptForClarify(current ?? task, thread, clarify);
16505
+ if (interrupted?.status === "interrupted" && this.matchesExecution(interrupted, task, thread)) {
16506
+ return { kind: "interrupted", task: interrupted, thread, messageId };
16507
+ }
16508
+ }
16071
16509
  return this.failSettledTaskAwaited(
16072
16510
  task,
16073
16511
  thread,
@@ -16076,6 +16514,35 @@ var ProjectTaskExecutionService = class {
16076
16514
  "interruption_not_durable"
16077
16515
  );
16078
16516
  }
16517
+ /** Reads the paused thread snapshot when no interrupt event payload is available (recovery). */
16518
+ async readInterruptedThreadState(task, thread) {
16519
+ try {
16520
+ return await this.deps.agentFor(task, thread).getCurrentState();
16521
+ } catch {
16522
+ return void 0;
16523
+ }
16524
+ }
16525
+ /**
16526
+ * Persists a durable `missing_input` interruption for a paused clarify question.
16527
+ *
16528
+ * @remarks The task must survive the pause so the room can answer it; the
16529
+ * agent resumes the same run once the answer arrives.
16530
+ */
16531
+ async interruptForClarify(task, thread, clarify) {
16532
+ const summary = clarify.questions.find(
16533
+ (question) => typeof question.question === "string" && question.question.trim().length > 0
16534
+ )?.question.trim() ?? "Waiting for clarification from the room";
16535
+ const result = await this.withLifecycleTimeout(this.deps.lifecycle.interruptTask({
16536
+ tenantId: task.tenantId,
16537
+ taskId: task.id,
16538
+ type: "missing_input",
16539
+ summary,
16540
+ actor: SYSTEM_ACTOR,
16541
+ threadId: thread.id
16542
+ }), void 0);
16543
+ if (!result || !result.success) return void 0;
16544
+ return result.task ?? await this.reloadTask(task);
16545
+ }
16079
16546
  async settleEndedRecovery(input, error) {
16080
16547
  const current = await this.withLifecycleTimeout(this.reloadTask(input.task), void 0);
16081
16548
  if (current && this.matchesExecution(current, input.task, input.thread)) {
@@ -16258,6 +16725,9 @@ import {
16258
16725
  isProjectLifecycleTask as isProjectLifecycleTask4,
16259
16726
  parseExternalDependency as parseExternalDependency2
16260
16727
  } from "@axiom-lattice/core";
16728
+ import {
16729
+ snapshotExactRecord as snapshotExactRecord9
16730
+ } from "@axiom-lattice/protocols";
16261
16731
  var SYSTEM_ACTOR2 = "system";
16262
16732
  var TERMINAL_ACTIONS = ["completed", "failed", "interrupted", "cancelled"];
16263
16733
  var MAX_LEARNING_ROUND_ANCESTORS = 8;
@@ -16328,6 +16798,10 @@ var ProjectTaskObserver = class {
16328
16798
  if (signal?.aborted) continue;
16329
16799
  const task = snapshotTaskItem(await this.deps.tasks.getById(hint.event.tenantId, hint.event.taskId));
16330
16800
  if (signal?.aborted) continue;
16801
+ if (task && task.workspaceId === hint.event.workspaceId && task.projectId === hint.event.projectId) {
16802
+ await this.projectDelegation(task);
16803
+ }
16804
+ if (signal?.aborted) continue;
16331
16805
  const eventTime = Date.parse(hint.event.updatedAt);
16332
16806
  if (task && task.status === "pending" && task.ownerType === "agent" && task.workspaceId === hint.event.workspaceId && task.projectId === hint.event.projectId && task.ownerId === hint.event.ownerId && task.updatedAt.getTime() >= eventTime) {
16333
16807
  await this.reconcileTask(task, signal);
@@ -16338,14 +16812,21 @@ var ProjectTaskObserver = class {
16338
16812
  async notify(event) {
16339
16813
  const task = snapshotTaskItem(await this.deps.tasks.getById(event.tenantId, event.taskId));
16340
16814
  const item = await this.deps.workItems.findByEventKey(event.tenantId, event.taskId, event.eventKey);
16341
- if (!task || !item || item.eventKey !== event.eventKey || item.action !== event.action || item.taskId !== event.taskId || item.tenantId !== event.tenantId || item.workspaceId !== event.workspaceId || item.projectId !== event.projectId || item.threadId !== event.threadId || task.workspaceId !== event.workspaceId || task.projectId !== event.projectId) return;
16342
- if (task.status === event.status && task.updatedAt.toISOString() === event.updatedAt && (event.threadId === void 0 || task.context?.thread_id === event.threadId)) {
16815
+ if (!task || task.workspaceId !== event.workspaceId || task.projectId !== event.projectId) return;
16816
+ const matches = item !== null && item !== void 0 && item.eventKey === event.eventKey && item.action === event.action && item.taskId === event.taskId && item.tenantId === event.tenantId && item.workspaceId === event.workspaceId && item.projectId === event.projectId && item.threadId === event.threadId;
16817
+ let anchorMessageId;
16818
+ if (matches && task.status === event.status && task.updatedAt.toISOString() === event.updatedAt && (event.threadId === void 0 || task.context?.thread_id === event.threadId)) {
16343
16819
  try {
16344
- await this.deps.projection.project({ task, room: await this.exactMainRoom(task), workItem: item });
16820
+ const result = await this.deps.projection.project({ task, room: await this.exactMainRoom(task), workItem: item });
16821
+ if (result.kind === "projected") anchorMessageId = result.message.id;
16345
16822
  } catch (error) {
16346
16823
  await this.safeWarning(task, "projection", error);
16347
16824
  }
16348
16825
  }
16826
+ if ((event.status === "completed" || event.status === "failed") && task.status === event.status) {
16827
+ void this.deps.coordinatorWake?.handle(event, anchorMessageId);
16828
+ }
16829
+ if (!matches) return;
16349
16830
  if (event.action === "completed" && task.status === "completed") await this.wakeParent(task);
16350
16831
  if (TERMINAL_ACTIONS.includes(event.action) && isTerminal(task.status)) await this.reevaluateDependents(task);
16351
16832
  }
@@ -16431,6 +16912,34 @@ var ProjectTaskObserver = class {
16431
16912
  await this.resumeIfReady(task, signal);
16432
16913
  }
16433
16914
  }
16915
+ /**
16916
+ * Projects the one-time delegation event for an Agent-created task assigned to another Agent.
16917
+ *
16918
+ * @param task Exact newly admitted Project task snapshot.
16919
+ * @remarks Admission hints are the creation signal; idempotency lives in the projection key.
16920
+ * Learning-round subtrees stay out of the room, matching admission and wake exemptions.
16921
+ */
16922
+ async projectDelegation(task) {
16923
+ const createdBy = readCreatedBy(task.context?.created_by);
16924
+ if (!createdBy || task.ownerType !== "agent" || createdBy.assistantId === task.ownerId) return;
16925
+ try {
16926
+ if (await this.belongsToLearningRound(task)) return;
16927
+ const room = await this.exactMainRoom(task);
16928
+ const from = await this.deps.bots.findById(task.tenantId, createdBy.membershipId) ?? await this.deps.bots.findByAssistant(task.tenantId, task.projectId, createdBy.assistantId);
16929
+ const to = await this.deps.bots.findByAssistant(task.tenantId, task.projectId, task.ownerId);
16930
+ const result = await this.deps.projection.projectDelegation({
16931
+ task,
16932
+ room,
16933
+ fromLabel: `@${from?.mentionName ?? createdBy.assistantId}`,
16934
+ toLabel: `@${to?.mentionName ?? task.ownerId}`
16935
+ });
16936
+ if (result.kind === "warning") {
16937
+ this.deps.log.warn("Project task delegation projection failed", { taskId: task.id, code: result.warning.code });
16938
+ }
16939
+ } catch (error) {
16940
+ await this.safeWarning(task, "delegation", error);
16941
+ }
16942
+ }
16434
16943
  // SCOPE-COMPAT: learning rounds (metadata.trainingRound) and their subtrees execute in the
16435
16944
  // architect's builder thread (canvas CopilotPanel), not the Project Room runtime. Exempting
16436
16945
  // them here keeps admission hints, startup reconciliation, and dependent re-evaluation from
@@ -16953,6 +17462,14 @@ function isTerminal(status) {
16953
17462
  function errorMessage3(error) {
16954
17463
  return error instanceof Error ? error.message : String(error);
16955
17464
  }
17465
+ function readCreatedBy(value) {
17466
+ try {
17467
+ const row = snapshotExactRecord9(value, ["assistantId", "membershipId"]);
17468
+ return row && typeof row.assistantId === "string" && row.assistantId.length > 0 && typeof row.membershipId === "string" && row.membershipId.length > 0 ? { assistantId: row.assistantId, membershipId: row.membershipId } : void 0;
17469
+ } catch {
17470
+ return void 0;
17471
+ }
17472
+ }
16956
17473
  function combineSignals(left, right) {
16957
17474
  const signals = [left, right].filter((signal) => signal !== void 0);
16958
17475
  if (signals.length === 0) return void 0;
@@ -16964,10 +17481,22 @@ function combineSignals(left, right) {
16964
17481
  import { createHash as createHash6 } from "crypto";
16965
17482
  import { isProjectLifecycleTask as isProjectLifecycleTask5 } from "@axiom-lattice/core";
16966
17483
  import {
17484
+ PROJECT_ROOM_TASK_DELEGATED_LABEL,
17485
+ PROJECT_ROOM_TASK_DELEGATED_SEPARATOR,
16967
17486
  PROJECT_TASK_LIFECYCLE_ACTIONS,
16968
17487
  snapshotExactArray as snapshotExactArray2,
16969
- snapshotExactRecord as snapshotExactRecord9
17488
+ snapshotExactRecord as snapshotExactRecord10
16970
17489
  } from "@axiom-lattice/protocols";
17490
+ var ROOM_PROJECTABLE_ACTIONS = [
17491
+ "interrupted",
17492
+ "failed",
17493
+ "completed",
17494
+ "cancelled",
17495
+ "reassigned"
17496
+ ];
17497
+ function isRoomProjectableAction(action) {
17498
+ return ROOM_PROJECTABLE_ACTIONS.includes(action);
17499
+ }
16971
17500
  var RoomProjectionService = class {
16972
17501
  /** @param deps Message, lifecycle-event, Task, warning, logging, and ID dependencies. */
16973
17502
  constructor(deps) {
@@ -16983,6 +17512,7 @@ var RoomProjectionService = class {
16983
17512
  const parsed = parseProjectionInput(input.task, input.workItem);
16984
17513
  const room = snapshotProjectRoom(input.room);
16985
17514
  const scope = room ? { tenantId: room.tenantId, workspaceId: room.workspaceId, projectId: room.projectId } : void 0;
17515
+ if (!parsed.ok && parsed.routine) return { kind: "skipped" };
16986
17516
  if (!parsed.ok || !room || parsed.task.tenantId !== room.tenantId || parsed.task.workspaceId !== room.workspaceId || parsed.task.projectId !== room.projectId || parsed.item.tenantId !== parsed.task.tenantId || parsed.item.workspaceId !== parsed.task.workspaceId || parsed.item.projectId !== parsed.task.projectId || parsed.item.taskId !== parsed.task.id) {
16987
17517
  return this.warning(
16988
17518
  "INVALID_PROJECTABLE_EVENT",
@@ -17013,6 +17543,42 @@ var RoomProjectionService = class {
17013
17543
  return this.warning("PROJECTION_WRITE_FAILED", { taskId: task.id, eventKey: item.eventKey }, scope);
17014
17544
  }
17015
17545
  }
17546
+ /**
17547
+ * Projects the one-time delegation event for an Agent-created task assigned to another Agent.
17548
+ *
17549
+ * @param input Untrusted Task and main room candidates plus resolved actor labels.
17550
+ * @returns The canonical stored message or a recoverable warning result.
17551
+ * @remarks Idempotent per task: repeated admission hints or replays never duplicate the row.
17552
+ */
17553
+ async projectDelegation(input) {
17554
+ const task = snapshotTaskItem(input.task);
17555
+ const room = snapshotProjectRoom(input.room);
17556
+ if (!task || !room || task.tenantId !== room.tenantId || task.workspaceId !== room.workspaceId || task.projectId !== room.projectId) {
17557
+ return { kind: "warning", warning: { code: "ROOM_TASK_PROJECTION_FAILED", ...task ? { taskId: task.id } : {} } };
17558
+ }
17559
+ const scope = { tenantId: task.tenantId, workspaceId: task.workspaceId, projectId: task.projectId };
17560
+ try {
17561
+ const candidate = {
17562
+ id: this.deps.idFactory(),
17563
+ tenantId: task.tenantId,
17564
+ workspaceId: task.workspaceId,
17565
+ projectId: task.projectId,
17566
+ roomId: room.id,
17567
+ author: { type: "system" },
17568
+ content: { type: "text", text: `${PROJECT_ROOM_TASK_DELEGATED_LABEL}${PROJECT_ROOM_TASK_DELEGATED_SEPARATOR}${input.fromLabel} \u2192 ${input.toLabel}` },
17569
+ mentions: [],
17570
+ source: "task",
17571
+ sourceId: task.id,
17572
+ idempotencyKey: tupleKey("task-delegation", [task.id])
17573
+ };
17574
+ const canonical = await this.deps.messages.createIdempotent(candidate);
17575
+ const message = assertCanonicalProjection(canonical, candidate);
17576
+ void Promise.resolve().then(() => this.deps.realtime?.messageCreated(message)).catch(() => void 0);
17577
+ return { kind: "projected", message };
17578
+ } catch {
17579
+ return this.warning("PROJECTION_WRITE_FAILED", { taskId: task.id, eventKey: "delegation" }, scope);
17580
+ }
17581
+ }
17016
17582
  /**
17017
17583
  * Replays bounded raw lifecycle pages for one exact Project scope.
17018
17584
  *
@@ -17027,7 +17593,7 @@ var RoomProjectionService = class {
17027
17593
  tenantId: input.tenantId,
17028
17594
  workspaceId: input.workspaceId,
17029
17595
  projectId: input.projectId,
17030
- actions: [...PROJECT_TASK_LIFECYCLE_ACTIONS],
17596
+ actions: [...ROOM_PROJECTABLE_ACTIONS],
17031
17597
  before,
17032
17598
  limit: input.pageSize
17033
17599
  });
@@ -17142,6 +17708,7 @@ function parseProjectionInput(taskValue, itemValue) {
17142
17708
  try {
17143
17709
  const task = snapshotProjectTask(taskValue);
17144
17710
  const item = snapshotProjectableWorkItem(itemValue);
17711
+ if (item && !isRoomProjectableAction(item.action)) return { ok: false, routine: true };
17145
17712
  const projection = item ? parseProjection(item) : void 0;
17146
17713
  return task && item && projection ? { ok: true, task, item, projection } : { ok: false, ...safeIdentity ? { safeIdentity } : {} };
17147
17714
  } catch {
@@ -17149,7 +17716,7 @@ function parseProjectionInput(taskValue, itemValue) {
17149
17716
  }
17150
17717
  }
17151
17718
  function snapshotProjectableWorkItem(value) {
17152
- const row = snapshotExactRecord9(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17719
+ const row = snapshotExactRecord10(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17153
17720
  "workspaceId",
17154
17721
  "projectId",
17155
17722
  "threadId",
@@ -17186,11 +17753,11 @@ function parseIso(value) {
17186
17753
  return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
17187
17754
  }
17188
17755
  function parseInterruption(value) {
17189
- const common = snapshotExactRecord9(value, ["type", "summary", "requestedAt"]);
17756
+ const common = snapshotExactRecord10(value, ["type", "summary", "requestedAt"]);
17190
17757
  if (common && (common.type === "missing_input" || common.type === "user_decision") && typeof common.summary === "string" && common.summary.length > 0 && parseIso(common.requestedAt)) {
17191
17758
  return { type: common.type, summary: common.summary, requestedAt: common.requestedAt };
17192
17759
  }
17193
- const external = snapshotExactRecord9(value, ["type", "summary", "requestedAt", "dependencyTaskIds"]);
17760
+ const external = snapshotExactRecord10(value, ["type", "summary", "requestedAt", "dependencyTaskIds"]);
17194
17761
  const ids = snapshotExactArray2(external?.dependencyTaskIds);
17195
17762
  if (external?.type === "external_dependency" && typeof external.summary === "string" && external.summary.length > 0 && parseIso(external.requestedAt) && ids && ids.length > 0 && ids.every((id3) => typeof id3 === "string" && id3.length > 0) && new Set(ids).size === ids.length) {
17196
17763
  return {
@@ -17200,7 +17767,7 @@ function parseInterruption(value) {
17200
17767
  dependencyTaskIds: [...ids]
17201
17768
  };
17202
17769
  }
17203
- const review = snapshotExactRecord9(value, ["type", "evidenceId"]);
17770
+ const review = snapshotExactRecord10(value, ["type", "evidenceId"]);
17204
17771
  return review?.type === "review_required" && typeof review.evidenceId === "string" && review.evidenceId.length > 0 ? { type: "review_required", evidenceId: review.evidenceId } : void 0;
17205
17772
  }
17206
17773
  function parseBeliefImpact(value) {
@@ -17208,50 +17775,36 @@ function parseBeliefImpact(value) {
17208
17775
  if (!rows) return void 0;
17209
17776
  const impacts = [];
17210
17777
  for (const row of rows) {
17211
- const entry = snapshotExactRecord9(row, ["key", "after", "basis"]);
17778
+ const entry = snapshotExactRecord10(row, ["key", "after", "basis"]);
17212
17779
  if (!entry || typeof entry.key !== "string" || !entry.key || typeof entry.after !== "number" || !Number.isFinite(entry.after) || typeof entry.basis !== "string" || !entry.basis) return void 0;
17213
17780
  impacts.push({ key: entry.key, after: entry.after, basis: entry.basis });
17214
17781
  }
17215
17782
  return impacts;
17216
17783
  }
17217
17784
  function parseCompletion(item) {
17218
- const row = snapshotExactRecord9(item.detail, ["evidenceId", "eventKey", "result", "beliefImpact"]);
17785
+ const row = snapshotExactRecord10(item.detail, ["evidenceId", "eventKey", "result", "beliefImpact"]);
17219
17786
  const beliefImpact = parseBeliefImpact(row?.beliefImpact);
17220
17787
  if (!row || typeof row.evidenceId !== "string" || !row.evidenceId || typeof row.eventKey !== "string" || !row.eventKey || item.eventKey !== `${row.eventKey}:completed` || typeof row.result !== "string" || !beliefImpact) return void 0;
17221
17788
  return { evidenceId: row.evidenceId, eventKey: row.eventKey, result: row.result, beliefImpact };
17222
17789
  }
17223
17790
  function parseProjection(item) {
17224
- if (item.action === "in_progress") {
17225
- const started = snapshotExactRecord9(item.detail, ["previousStatus"]);
17226
- if (started?.previousStatus === "pending") return { kind: "started", text: "Task started" };
17227
- const resumed = snapshotExactRecord9(item.detail, ["type", "previousInterruption"]);
17228
- if (resumed?.type === "interruption_resume" && parseInterruption(resumed.previousInterruption)) {
17229
- return { kind: "resumed", text: "Task resumed" };
17230
- }
17231
- const rejected = snapshotExactRecord9(item.detail, ["type", "evidenceId", "note"]);
17232
- if (rejected?.type === "review_rejected" && typeof rejected.evidenceId === "string" && rejected.evidenceId && typeof rejected.note === "string" && rejected.note) {
17233
- return { kind: "resumed", text: "Task resumed after review rejection" };
17234
- }
17235
- const stale = snapshotExactRecord9(item.detail, ["type", "evidenceId"]);
17236
- return stale?.type === "review_stale" && typeof stale.evidenceId === "string" && stale.evidenceId ? { kind: "resumed", text: "Task resumed after stale review" } : void 0;
17237
- }
17238
17791
  if (item.action === "interrupted") {
17239
17792
  return parseInterruption(item.detail) ? { kind: "interrupted", text: "Task interrupted" } : void 0;
17240
17793
  }
17241
17794
  if (item.action === "failed") {
17242
- const row = snapshotExactRecord9(item.detail, ["failureReason"]);
17795
+ const row = snapshotExactRecord10(item.detail, ["failureReason"]);
17243
17796
  return typeof row?.failureReason === "string" && row.failureReason ? { kind: "failed", text: "Task failed" } : void 0;
17244
17797
  }
17245
17798
  if (item.action === "completed") {
17246
17799
  return parseCompletion(item) ? { kind: "completed", text: "Task completed" } : void 0;
17247
17800
  }
17248
17801
  if (item.action === "cancelled") {
17249
- const empty = snapshotExactRecord9(item.detail, []);
17250
- const summarized = snapshotExactRecord9(item.detail, ["summary"]);
17802
+ const empty = snapshotExactRecord10(item.detail, []);
17803
+ const summarized = snapshotExactRecord10(item.detail, ["summary"]);
17251
17804
  return empty || typeof summarized?.summary === "string" && summarized.summary ? { kind: "cancelled", text: "Task cancelled" } : void 0;
17252
17805
  }
17253
17806
  if (item.action === "reassigned") {
17254
- const row = snapshotExactRecord9(
17807
+ const row = snapshotExactRecord10(
17255
17808
  item.detail,
17256
17809
  ["previousOwnerId", "previousThreadId", "ownerId", "membershipId", "threadId"]
17257
17810
  );
@@ -17260,7 +17813,7 @@ function parseProjection(item) {
17260
17813
  return void 0;
17261
17814
  }
17262
17815
  function assertCanonicalProjection(value, expected) {
17263
- const row = snapshotExactRecord9(value, [
17816
+ const row = snapshotExactRecord10(value, [
17264
17817
  "id",
17265
17818
  "tenantId",
17266
17819
  "workspaceId",
@@ -17274,8 +17827,8 @@ function assertCanonicalProjection(value, expected) {
17274
17827
  "idempotencyKey",
17275
17828
  "createdAt"
17276
17829
  ]);
17277
- const author = snapshotExactRecord9(row?.author, ["type"]);
17278
- const content = snapshotExactRecord9(row?.content, ["type", "text"]);
17830
+ const author = snapshotExactRecord10(row?.author, ["type"]);
17831
+ const content = snapshotExactRecord10(row?.content, ["type", "text"]);
17279
17832
  const mentions = snapshotExactArray2(row?.mentions);
17280
17833
  const createdAt = cloneExactDate(row?.createdAt);
17281
17834
  if (!row || !createdAt || author?.type !== "system" || content?.type !== "text" || content.text !== expected.content.text || !mentions || mentions.length !== 0 || typeof row.id !== "string" || !row.id || row.tenantId !== expected.tenantId || row.workspaceId !== expected.workspaceId || row.projectId !== expected.projectId || row.roomId !== expected.roomId || row.source !== "task" || row.sourceId !== expected.sourceId || row.idempotencyKey !== expected.idempotencyKey) {
@@ -17297,7 +17850,7 @@ function assertCanonicalProjection(value, expected) {
17297
17850
  };
17298
17851
  }
17299
17852
  function snapshotLifecycleCursor(value) {
17300
- const row = snapshotExactRecord9(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17853
+ const row = snapshotExactRecord10(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17301
17854
  "workspaceId",
17302
17855
  "projectId",
17303
17856
  "threadId",
@@ -17317,7 +17870,7 @@ function isRecord3(value) {
17317
17870
  import { taskLifecycleNotifier } from "@axiom-lattice/core";
17318
17871
 
17319
17872
  // src/services/collaboration/ProjectRoomEventBroker.ts
17320
- import { randomUUID as randomUUID18 } from "crypto";
17873
+ import { randomUUID as randomUUID21 } from "crypto";
17321
17874
  import {
17322
17875
  parseProjectRoomEventId,
17323
17876
  ProjectRoomBrokerCapacityError,
@@ -17346,7 +17899,7 @@ var ProjectRoomEventBroker = class {
17346
17899
  this.subscriberCount = 0;
17347
17900
  this.usageClock = 0;
17348
17901
  this.closed = false;
17349
- this.epoch = options.epoch ?? randomUUID18();
17902
+ this.epoch = options.epoch ?? randomUUID21();
17350
17903
  if (!UUID_PATTERN.test(this.epoch)) {
17351
17904
  throw new TypeError("Project Room event epoch must be a lowercase canonical UUID");
17352
17905
  }
@@ -17667,7 +18220,7 @@ function snapshotJson(value, ancestors = /* @__PURE__ */ new Set()) {
17667
18220
  import { createHash as createHash7 } from "crypto";
17668
18221
  import {
17669
18222
  projectRoomUserEventScope,
17670
- snapshotExactRecord as snapshotExactRecord10,
18223
+ snapshotExactRecord as snapshotExactRecord11,
17671
18224
  snapshotProjectRoomBotMembershipRealtime,
17672
18225
  snapshotProjectRoomMembershipRealtime,
17673
18226
  snapshotProjectRoomMessageRealtime
@@ -17744,7 +18297,7 @@ var BrokerProjectRoomRealtimePublisher = class {
17744
18297
  /** Publishes a membership notice onto the affected user's channel scope. */
17745
18298
  membershipAffected(input) {
17746
18299
  this.publish("membership.affected", () => {
17747
- const value = snapshotExactRecord10(input, ["tenantId", "userId", "change", "projectId", "roomId", "sourceKey"]);
18300
+ const value = snapshotExactRecord11(input, ["tenantId", "userId", "change", "projectId", "roomId", "sourceKey"]);
17748
18301
  if (!value || !safeId2(value.tenantId) || !safeId2(value.userId) || !safeId2(value.projectId) || !safeId2(value.roomId) || !safeSourceKey(value.sourceKey) || !["added", "removed", "role_changed"].includes(value.change)) {
17749
18302
  throw new TypeError("Invalid membership input");
17750
18303
  }
@@ -17764,7 +18317,7 @@ var BrokerProjectRoomRealtimePublisher = class {
17764
18317
  /** Publishes a read-marker update onto the acting user's channel scope. */
17765
18318
  readChanged(input) {
17766
18319
  this.publish("read.changed", () => {
17767
- const value = snapshotExactRecord10(input, ["tenantId", "userId", "projectId", "roomId", "lastReadAt", "sourceKey"]);
18320
+ const value = snapshotExactRecord11(input, ["tenantId", "userId", "projectId", "roomId", "lastReadAt", "sourceKey"]);
17768
18321
  if (!value || !safeId2(value.tenantId) || !safeId2(value.userId) || !safeId2(value.projectId) || !safeId2(value.roomId) || !safeSourceKey(value.sourceKey) || typeof value.lastReadAt !== "string" || !Number.isFinite(Date.parse(value.lastReadAt))) {
17769
18322
  throw new TypeError("Invalid read input");
17770
18323
  }
@@ -17809,13 +18362,13 @@ var BrokerProjectRoomRealtimePublisher = class {
17809
18362
  };
17810
18363
  function snapshotInput(input, kind) {
17811
18364
  const keys2 = kind === "message" ? ["tenantId", "roomId", "message", "sourceKey"] : kind === "roster" ? ["tenantId", "roomId", "membership", "sourceKey"] : ["tenantId", "roomId", "membership", "sourceKey"];
17812
- const value = snapshotExactRecord10(input, [...keys2, "change"]);
18365
+ const value = snapshotExactRecord11(input, [...keys2, "change"]);
17813
18366
  const validChange = kind === "roster" ? ["added", "updated", "paused", "resumed", "removed"].includes(value?.change) : ["added", "role_changed", "removed"].includes(value?.change);
17814
18367
  if (!value || !safeId2(value.tenantId) || !safeId2(value.roomId) || !safeSourceKey(value.sourceKey) || !validChange) throw new TypeError(`Invalid ${kind} input`);
17815
18368
  return value;
17816
18369
  }
17817
18370
  function snapshotTaskInput(input) {
17818
- const value = snapshotExactRecord10(input, ["tenantId", "roomId", "projectId", "taskId", "status", "ownerMembershipId", "updatedAt", "sourceKey"]);
18371
+ const value = snapshotExactRecord11(input, ["tenantId", "roomId", "projectId", "taskId", "status", "ownerMembershipId", "updatedAt", "sourceKey"]);
17819
18372
  if (!value || !safeId2(value.tenantId) || !safeId2(value.roomId) || !safeSourceKey(value.sourceKey)) throw new TypeError("Invalid task input");
17820
18373
  if (!safeId2(value.projectId) || !safeId2(value.taskId) || !safeId2(value.ownerMembershipId) || !["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"].includes(value.status)) throw new TypeError("Invalid task input");
17821
18374
  if (typeof value.updatedAt !== "string") throw new TypeError("Invalid task timestamp");
@@ -17841,7 +18394,7 @@ function safeErrorCode(error) {
17841
18394
 
17842
18395
  // src/services/collaboration/ProjectRoomTaskEventSubscriber.ts
17843
18396
  import { isProjectLifecycleTask as isProjectLifecycleTask6 } from "@axiom-lattice/core";
17844
- import { snapshotExactRecord as snapshotExactRecord11 } from "@axiom-lattice/protocols";
18397
+ import { snapshotExactRecord as snapshotExactRecord12 } from "@axiom-lattice/protocols";
17845
18398
  var ProjectRoomTaskEventSubscriber = class {
17846
18399
  /** Creates a bounded Project task lifecycle observer. */
17847
18400
  constructor(deps) {
@@ -17951,7 +18504,7 @@ var ProjectRoomTaskEventSubscriber = class {
17951
18504
  }
17952
18505
  }
17953
18506
  parse(value) {
17954
- const row = snapshotExactRecord11(value, ["tenantId", "workspaceId", "projectId", "taskId", "status", "action", "eventKey", "updatedAt"], ["threadId"]);
18507
+ const row = snapshotExactRecord12(value, ["tenantId", "workspaceId", "projectId", "taskId", "status", "action", "eventKey", "updatedAt"], ["threadId"]);
17955
18508
  const actions = ["in_progress", "interrupted", "failed", "completed", "cancelled", "reassigned"];
17956
18509
  const statuses = ["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"];
17957
18510
  if (!row || !actions.includes(String(row.action)) || !statuses.includes(String(row.status)) || !["tenantId", "workspaceId", "projectId", "taskId", "eventKey", "updatedAt"].every((key) => typeof row[key] === "string" && row[key].length > 0) || !Number.isFinite(Date.parse(row.updatedAt)) || row.threadId !== void 0 && typeof row.threadId !== "string") return void 0;
@@ -18014,7 +18567,8 @@ var ProjectRoomRealtimeRuntime = class {
18014
18567
  };
18015
18568
  function createProjectRoomRealtimeRuntime(deps) {
18016
18569
  const broker = new ProjectRoomEventBroker({ log: deps.log });
18017
- const publisher = new BrokerProjectRoomRealtimePublisher({ broker, log: deps.log });
18570
+ const basePublisher = new BrokerProjectRoomRealtimePublisher({ broker, log: deps.log });
18571
+ const publisher = deps.pushNotifier ? withPushNotifier(basePublisher, deps.pushNotifier) : basePublisher;
18018
18572
  const subscriber = new ProjectRoomTaskEventSubscriber({
18019
18573
  tasks: deps.tasks,
18020
18574
  rooms: deps.rooms,
@@ -18031,6 +18585,20 @@ function createProjectRoomRealtimeRuntime(deps) {
18031
18585
  shutdownTimeoutMs: deps.shutdownTimeoutMs
18032
18586
  });
18033
18587
  }
18588
+ function withPushNotifier(target, notifier) {
18589
+ return new Proxy(target, {
18590
+ get(inner, prop, receiver) {
18591
+ if (prop === "messageCreated") {
18592
+ return (message) => {
18593
+ inner.messageCreated(message);
18594
+ void notifier.notifyMessage(message).catch(() => void 0);
18595
+ };
18596
+ }
18597
+ const value = Reflect.get(inner, prop, receiver);
18598
+ return typeof value === "function" ? value.bind(inner) : value;
18599
+ }
18600
+ });
18601
+ }
18034
18602
  async function boundedAllSettled(promises, timeoutMs) {
18035
18603
  if (promises.length === 0) return true;
18036
18604
  let timer;
@@ -18043,6 +18611,94 @@ async function boundedAllSettled(promises, timeoutMs) {
18043
18611
  return result;
18044
18612
  }
18045
18613
 
18614
+ // src/services/notifications/createRoomPushNotifier.ts
18615
+ import webpush from "web-push";
18616
+
18617
+ // src/services/notifications/RoomPushNotifier.ts
18618
+ function isSubscriptionGone(error) {
18619
+ if (typeof error !== "object" || error === null) return false;
18620
+ const statusCode = error.statusCode;
18621
+ return statusCode === 404 || statusCode === 410;
18622
+ }
18623
+ var MAX_BODY = 140;
18624
+ var RoomPushNotifier = class {
18625
+ /** Creates a notifier from explicitly injected collaborators. */
18626
+ constructor(deps) {
18627
+ this.deps = deps;
18628
+ }
18629
+ /**
18630
+ * Notifies the room's active human members (excluding the author) about a message.
18631
+ *
18632
+ * @param message - The persisted, canonical room message.
18633
+ */
18634
+ async notifyMessage(message) {
18635
+ const authorId = message.author.type === "human" ? message.author.userId : void 0;
18636
+ let memberships;
18637
+ try {
18638
+ memberships = await this.deps.membershipStore.list(message.tenantId, message.projectId);
18639
+ } catch (error) {
18640
+ this.log("membership lookup failed", message, error);
18641
+ return;
18642
+ }
18643
+ const recipients = memberships.filter((membership) => membership.status === "active" && membership.userId !== authorId).map((membership) => membership.userId);
18644
+ const title = await this.deps.resolveTitle?.(message) ?? message.roomId;
18645
+ const payload = {
18646
+ title,
18647
+ body: message.content.text.slice(0, MAX_BODY),
18648
+ tag: message.projectId,
18649
+ url: this.deps.buildUrl?.(message) ?? "/"
18650
+ };
18651
+ for (const userId of recipients) {
18652
+ let subscriptions;
18653
+ try {
18654
+ subscriptions = await this.deps.subscriptionStore.list(message.tenantId, userId);
18655
+ } catch (error) {
18656
+ this.log("subscription lookup failed", message, error);
18657
+ continue;
18658
+ }
18659
+ for (const subscription of subscriptions) {
18660
+ try {
18661
+ await this.deps.send(subscription, payload);
18662
+ } catch (error) {
18663
+ if ((this.deps.isGone ?? isSubscriptionGone)(error)) {
18664
+ await this.deps.subscriptionStore.delete(message.tenantId, userId, subscription.endpoint).catch(() => void 0);
18665
+ } else {
18666
+ this.log("push send failed", message, error);
18667
+ }
18668
+ }
18669
+ }
18670
+ }
18671
+ }
18672
+ log(message, source, error) {
18673
+ this.deps.log?.(`[RoomPushNotifier] ${message}`, {
18674
+ tenantId: source.tenantId,
18675
+ projectId: source.projectId,
18676
+ roomId: source.roomId,
18677
+ messageId: source.id,
18678
+ error: error instanceof Error ? error.message : String(error)
18679
+ });
18680
+ }
18681
+ };
18682
+
18683
+ // src/services/notifications/createRoomPushNotifier.ts
18684
+ function createRoomPushNotifier(input) {
18685
+ if (input.vapid === null) return null;
18686
+ webpush.setVapidDetails(input.vapid.subject, input.vapid.publicKey, input.vapid.privateKey);
18687
+ return new RoomPushNotifier({
18688
+ membershipStore: input.membershipStore,
18689
+ subscriptionStore: input.subscriptionStore,
18690
+ ...input.resolveTitle === void 0 ? {} : { resolveTitle: input.resolveTitle },
18691
+ ...input.buildUrl === void 0 ? {} : { buildUrl: input.buildUrl },
18692
+ ...input.log === void 0 ? {} : { log: input.log },
18693
+ send: async (subscription, payload) => {
18694
+ await webpush.sendNotification(
18695
+ { endpoint: subscription.endpoint, keys: subscription.keys },
18696
+ JSON.stringify(payload)
18697
+ );
18698
+ }
18699
+ });
18700
+ }
18701
+
18046
18702
  // src/services/collaboration/WorkspaceRoomEventsService.ts
18047
18703
  import { PROJECT_ROOM_USER_CHANNEL_PROJECT, projectRoomUserEventScope as projectRoomUserEventScope2 } from "@axiom-lattice/protocols";
18048
18704
 
@@ -18279,7 +18935,7 @@ var WorkspaceRoomEventsService = class {
18279
18935
  for (const active of [...this.connections.values()]) for (const connection of [...active]) connection.close();
18280
18936
  this.connections.clear();
18281
18937
  }
18282
- buildConnection(actor2, workspaceId, raw) {
18938
+ buildConnection(actor3, workspaceId, raw) {
18283
18939
  let phase = "initializing";
18284
18940
  const subscriptions = /* @__PURE__ */ new Map();
18285
18941
  let heartbeatTimer;
@@ -18292,7 +18948,7 @@ var WorkspaceRoomEventsService = class {
18292
18948
  const clear = (timer) => {
18293
18949
  if (timer !== void 0) clearTimeout(timer);
18294
18950
  };
18295
- const userScope = projectRoomUserEventScope2(actor2.tenantId, actor2.userId);
18951
+ const userScope = projectRoomUserEventScope2(actor3.tenantId, actor3.userId);
18296
18952
  const userScopeKey = JSON.stringify([userScope.tenantId, userScope.roomId, userScope.projectId]);
18297
18953
  const roomScopeKey = (scope) => JSON.stringify([scope.tenantId, scope.roomId, scope.projectId]);
18298
18954
  const serializeScoped = (scope, event) => scope === null ? `id: ${event.id}
@@ -18316,7 +18972,7 @@ data: ${JSON.stringify({ ...event, scope })}
18316
18972
  for (const entry of subscriptions.values()) entry.unsubscribe();
18317
18973
  subscriptions.clear();
18318
18974
  writer.close();
18319
- const userKey = JSON.stringify([actor2.tenantId, actor2.userId]);
18975
+ const userKey = JSON.stringify([actor3.tenantId, actor3.userId]);
18320
18976
  const active = this.connections.get(userKey);
18321
18977
  if (active && recordRef.current !== void 0) {
18322
18978
  active.delete(recordRef.current);
@@ -18335,11 +18991,11 @@ data: ${JSON.stringify({ ...event, scope })}
18335
18991
  recordRef.current = record;
18336
18992
  const rebuildSubscriptions = async () => {
18337
18993
  try {
18338
- const rooms = await this.deps.listMemberRooms({ tenantId: actor2.tenantId, userId: actor2.userId, workspaceId });
18994
+ const rooms = await this.deps.listMemberRooms({ tenantId: actor3.tenantId, userId: actor3.userId, workspaceId });
18339
18995
  if (closed) return true;
18340
18996
  const wanted = /* @__PURE__ */ new Map();
18341
18997
  for (const room of rooms) {
18342
- const scope = { tenantId: actor2.tenantId, roomId: room.roomId, projectId: room.projectId };
18998
+ const scope = { tenantId: actor3.tenantId, roomId: room.roomId, projectId: room.projectId };
18343
18999
  wanted.set(roomScopeKey(scope), scope);
18344
19000
  }
18345
19001
  for (const [key, entry] of [...subscriptions]) {
@@ -18376,7 +19032,7 @@ data: ${JSON.stringify({ ...event, scope })}
18376
19032
  };
18377
19033
  const scheduleExpiry = () => {
18378
19034
  if (closed) return;
18379
- const delay2 = actor2.tokenExpiresAt - this.now();
19035
+ const delay2 = actor3.tokenExpiresAt - this.now();
18380
19036
  if (delay2 <= 0) {
18381
19037
  revoke("TOKEN_EXPIRED");
18382
19038
  return;
@@ -18403,7 +19059,7 @@ data: ${JSON.stringify({ ...event, scope })}
18403
19059
  accessTimer = void 0;
18404
19060
  if (closed) return;
18405
19061
  const projectIds = [...subscriptions.values()].map((entry) => entry.scope.projectId).filter((projectId) => projectId !== PROJECT_ROOM_USER_CHANNEL_PROJECT);
18406
- Promise.all(projectIds.map((projectId) => Promise.resolve().then(() => this.deps.requireReadAccess({ tenantId: actor2.tenantId, userId: actor2.userId, projectId })))).then(() => {
19062
+ Promise.all(projectIds.map((projectId) => Promise.resolve().then(() => this.deps.requireReadAccess({ tenantId: actor3.tenantId, userId: actor3.userId, projectId })))).then(() => {
18407
19063
  if (!closed) scheduleAccessCheck();
18408
19064
  }, () => {
18409
19065
  if (!closed) revoke("PROJECT_ACCESS_REVOKED");
@@ -18445,7 +19101,7 @@ function createWorkspaceMemberRoomLister(deps) {
18445
19101
  }
18446
19102
 
18447
19103
  // src/routes/workspace-room-events.ts
18448
- import { snapshotExactRecord as snapshotExactRecord12 } from "@axiom-lattice/protocols";
19104
+ import { snapshotExactRecord as snapshotExactRecord13 } from "@axiom-lattice/protocols";
18449
19105
  function registerWorkspaceRoomEventRoutes(app2, deps) {
18450
19106
  app2.get("/api/workspaces/:workspaceId/room-events", async (request, reply) => {
18451
19107
  try {
@@ -18453,7 +19109,7 @@ function registerWorkspaceRoomEventRoutes(app2, deps) {
18453
19109
  const headers = strictHeaders(request);
18454
19110
  if (request.body !== void 0 || Object.keys(request.query).length !== 0) return invalid(reply);
18455
19111
  const session = verifyConsoleSessionAuthHeader(headers.authorization, deps.tokenSecret);
18456
- if (!session?.tenantId) return unauthorized(reply);
19112
+ if (!session?.tenantId) return unauthorized2(reply);
18457
19113
  reply.hijack();
18458
19114
  reply.raw.writeHead(200, {
18459
19115
  "Content-Type": "text/event-stream",
@@ -18473,7 +19129,7 @@ function registerWorkspaceRoomEventRoutes(app2, deps) {
18473
19129
  });
18474
19130
  }
18475
19131
  function exactParams(params) {
18476
- const value = snapshotExactRecord12(params, ["workspaceId"]);
19132
+ const value = snapshotExactRecord13(params, ["workspaceId"]);
18477
19133
  if (!value || typeof value.workspaceId !== "string" || !value.workspaceId.trim()) throw new InvalidRealtimeRequestError();
18478
19134
  return { workspaceId: value.workspaceId };
18479
19135
  }
@@ -18496,7 +19152,7 @@ var InvalidRealtimeRequestError = class extends Error {
18496
19152
  function invalid(reply) {
18497
19153
  return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
18498
19154
  }
18499
- function unauthorized(reply) {
19155
+ function unauthorized2(reply) {
18500
19156
  return reply.code(401).send({ success: false, error: { code: "UNAUTHORIZED", message: "Unauthorized", retryable: false } });
18501
19157
  }
18502
19158
  function routeError(reply, error) {
@@ -18616,7 +19272,7 @@ function isPublicRoomAddress(message) {
18616
19272
  }
18617
19273
  async function validateReassignmentThread(threads, input) {
18618
19274
  const thread = snapshotThread(await threads.getThreadById(input.tenantId, input.newThreadId));
18619
- const metadata = snapshotExactRecord13(thread?.metadata, [
19275
+ const metadata = snapshotExactRecord14(thread?.metadata, [
18620
19276
  "source",
18621
19277
  "kind",
18622
19278
  "workspaceId",
@@ -18640,6 +19296,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18640
19296
  const projectBotMembershipStore = getStoreLattice16("default", "projectBotMembership").store;
18641
19297
  const projectRoomMessageStore = getStoreLattice16("default", "projectRoomMessage").store;
18642
19298
  const projectRoomReadStateStore = getStoreLattice16("default", "projectRoomReadState").store;
19299
+ const userPushSubscriptionStore = getStoreLattice16("default", "userPushSubscription").store;
18643
19300
  const threadStore = getStoreLattice16("default", "thread").store;
18644
19301
  const taskStore = getStoreLattice16("default", "task").store;
18645
19302
  const taskWorkItemStore = getStoreLattice16("default", "taskWorkItem").store;
@@ -18647,10 +19304,22 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18647
19304
  const accessService = new ProjectAccessService({ projectStore, membershipStore: projectMembershipStore, userTenantLinkStore });
18648
19305
  const realtimeMode = resolveProjectRoomRealtimeMode();
18649
19306
  const realtimeLog = options.projectTaskLog ?? { warn: () => void 0 };
19307
+ const roomPushNotifier = createRoomPushNotifier({
19308
+ subscriptionStore: userPushSubscriptionStore,
19309
+ membershipStore: projectMembershipStore,
19310
+ vapid: process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY && process.env.VAPID_SUBJECT ? { publicKey: process.env.VAPID_PUBLIC_KEY, privateKey: process.env.VAPID_PRIVATE_KEY, subject: process.env.VAPID_SUBJECT } : null,
19311
+ resolveTitle: async (message) => {
19312
+ const project = await projectStore.getProjectById(message.tenantId, message.projectId);
19313
+ return project?.name;
19314
+ },
19315
+ buildUrl: (message) => `/?room=${encodeURIComponent(message.projectId)}`,
19316
+ log: (message, context) => realtimeLog.warn(message, context ?? {})
19317
+ });
18650
19318
  const projectRoomRealtimeRuntime = realtimeMode === "single-gateway" ? options.projectRoomRealtimeRuntime ?? createProjectRoomRealtimeRuntime({
18651
19319
  tasks: taskStore,
18652
19320
  rooms: projectRoomStore,
18653
19321
  memberships: projectBotMembershipStore,
19322
+ pushNotifier: roomPushNotifier,
18654
19323
  log: realtimeLog
18655
19324
  }) : void 0;
18656
19325
  if (projectRoomRealtimeRuntime) {
@@ -18679,10 +19348,53 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18679
19348
  });
18680
19349
  }
18681
19350
  const realtime = projectRoomRealtimeRuntime?.publisher;
18682
- const roomService = new ProjectRoomService({ accessService, roomStore: projectRoomStore });
18683
- const membershipService = new ProjectMembershipService({ membershipStore: projectMembershipStore, userTenantLinkStore, accessService, projectMutationMutex: projectMutationMutex2, roomStore: projectRoomStore, realtime, onRealtimeFailure: realtime && channelDeps?.warn ? ({ code, membershipId }) => channelDeps.warn?.("project-room:membership-realtime-failed", { code, membershipId }) : void 0 });
18684
19351
  const installation = channelDeps ? new InternalRoomInstallationService(channelDeps.installationStore) : void 0;
18685
19352
  const bindingLifecycle = channelDeps && installation ? new RoomBindingProvisioner({ installation, registry: channelDeps.bindingRegistry }) : new NoopRoomBindingLifecycle();
19353
+ const coordinatorProvisioner = new RoomCoordinatorProvisioner({
19354
+ getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
19355
+ createAssistant: assistantStore.createAssistant.bind(assistantStore),
19356
+ updateAssistant: assistantStore.updateAssistant.bind(assistantStore)
19357
+ });
19358
+ const resolveCoordinatorMentionName = async (tenantId) => {
19359
+ const tenant2 = await getStoreLattice16("default", "tenant").store.getTenantById(tenantId);
19360
+ const value = tenant2?.metadata?.coordinatorMentionName;
19361
+ return typeof value === "string" && value.length > 0 ? value : void 0;
19362
+ };
19363
+ const coordinatorEnsurer = new RoomCoordinatorEnsurer({
19364
+ provisioner: coordinatorProvisioner,
19365
+ membershipStore: projectBotMembershipStore,
19366
+ threadStore,
19367
+ bindingLifecycle,
19368
+ resolveMentionName: resolveCoordinatorMentionName,
19369
+ log: realtimeLog
19370
+ });
19371
+ const roomService = new ProjectRoomService({
19372
+ accessService,
19373
+ roomStore: projectRoomStore,
19374
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope),
19375
+ log: realtimeLog
19376
+ });
19377
+ const roomInterruptService = new RoomInterruptService({
19378
+ requireAccess: async ({ tenantId, userId, projectId }) => {
19379
+ await accessService.requireProjectAccess({ tenantId, userId, projectId, capability: "send" });
19380
+ },
19381
+ resolveMembership: async ({ tenantId, projectId, membershipId }) => {
19382
+ const membership = await projectBotMembershipStore.findById(tenantId, membershipId);
19383
+ return membership !== null && membership.projectId === projectId ? membership : null;
19384
+ },
19385
+ getThreadStatus: async ({ tenantId, assistantId, threadId, workspaceId, projectId }) => {
19386
+ const agent = agentInstanceManager6.getAgent({
19387
+ tenant_id: tenantId,
19388
+ assistant_id: assistantId,
19389
+ thread_id: threadId,
19390
+ workspace_id: workspaceId,
19391
+ project_id: projectId
19392
+ });
19393
+ return String(await agent.getRunStatus());
19394
+ },
19395
+ dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope))
19396
+ });
19397
+ const membershipService = new ProjectMembershipService({ membershipStore: projectMembershipStore, userTenantLinkStore, accessService, projectMutationMutex: projectMutationMutex2, roomStore: projectRoomStore, realtime, onRealtimeFailure: realtime && channelDeps?.warn ? ({ code, membershipId }) => channelDeps.warn?.("project-room:membership-realtime-failed", { code, membershipId }) : void 0 });
18686
19398
  const rosterService = new ProjectRosterService({ accessService, roomService, membershipStore: projectBotMembershipStore, threadStore, assistantStore, taskStore, bindingLifecycle, projectMutationMutex: projectMutationMutex2, realtime, onRealtimeFailure: realtime && channelDeps?.warn ? ({ code, membershipId }) => channelDeps.warn?.("project-room:roster-realtime-failed", { code, membershipId }) : void 0 });
18687
19399
  const roomAdapterAvailable = channelDeps?.adapterRegistry.get("room")?.channel === "room";
18688
19400
  const dispatcher = channelDeps && installation && bindingLifecycle instanceof RoomBindingProvisioner && roomAdapterAvailable ? new RoomDispatchService({
@@ -18699,11 +19411,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18699
19411
  log: realtimeLog
18700
19412
  }) : void 0;
18701
19413
  if (coordinatorWakeNotifier) {
18702
- const unsubscribeCoordinatorWake = taskLifecycleNotifier2.subscribe(((event) => {
18703
- void coordinatorWakeNotifier.handle(event);
18704
- }));
18705
19414
  app2.addHook("onClose", async () => {
18706
- unsubscribeCoordinatorWake();
18707
19415
  await coordinatorWakeNotifier.dispose();
18708
19416
  });
18709
19417
  }
@@ -18728,7 +19436,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18728
19436
  projectRoomMessageStore,
18729
19437
  threadStore,
18730
19438
  realtime,
18731
- log: options.projectTaskLog
19439
+ log: options.projectTaskLog,
19440
+ ...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier }
18732
19441
  }) : { reason: void 0 };
18733
19442
  const projectTaskComposition = "composition" in projectTaskRuntime ? projectTaskRuntime.composition : void 0;
18734
19443
  const projectTaskUnavailableReason = "reason" in projectTaskRuntime ? projectTaskRuntime.reason : void 0;
@@ -18738,23 +19447,17 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18738
19447
  });
18739
19448
  }
18740
19449
  if (projectTaskComposition) app2.addHook("onClose", async () => projectTaskComposition.lease.release());
18741
- const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: randomUUID19 });
19450
+ const taskThreadService = projectTaskComposition?.lease.runtime.threads ?? new ProjectTaskThreadService({ threads: threadStore, idFactory: randomUUID22 });
18742
19451
  const taskLifecycle = projectTaskComposition?.lease.runtime.lifecycle ?? createTaskLifecycleService({ taskStore, workItemStore: taskWorkItemStore });
18743
19452
  const reassignmentAuthorizer = createProjectTaskReassignmentAuthorizer(
18744
19453
  new ProjectTaskOwnerPolicy(projectBotMembershipStore),
18745
19454
  (input) => validateReassignmentThread(threadStore, input)
18746
19455
  );
18747
- const coordinatorProvisioner = new RoomCoordinatorProvisioner({
18748
- getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
18749
- createAssistant: assistantStore.createAssistant.bind(assistantStore)
18750
- });
18751
19456
  const publicRoomService = new WorkspacePublicRoomService({
18752
19457
  getProjectById: projectStore.getProjectById.bind(projectStore),
18753
19458
  createProject: projectStore.createProject.bind(projectStore),
18754
19459
  ensureMainRoom: projectRoomStore.ensureMainRoom.bind(projectRoomStore),
18755
- findBotByAssistant: projectBotMembershipStore.findByAssistant.bind(projectBotMembershipStore),
18756
- saveBot: projectBotMembershipStore.save.bind(projectBotMembershipStore),
18757
- ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
19460
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope)
18758
19461
  });
18759
19462
  const readStateService = {
18760
19463
  mark: async (actorRef) => {
@@ -18778,6 +19481,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18778
19481
  membershipService,
18779
19482
  messageService,
18780
19483
  readStateService,
19484
+ interruptService: roomInterruptService,
18781
19485
  userDirectory: {
18782
19486
  getUserById: (id3) => getStoreLattice16("default", "user").store.getUserById(id3).then((u) => u ? { name: u.name } : null)
18783
19487
  }
@@ -18824,9 +19528,10 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18824
19528
  save: projectBotMembershipStore.save.bind(projectBotMembershipStore)
18825
19529
  },
18826
19530
  coordinatorProvisioner,
19531
+ getCoordinatorMentionName: resolveCoordinatorMentionName,
18827
19532
  rosterService: {
18828
- addBot: async (actor2, input) => {
18829
- await rosterService.add(actor2, input);
19533
+ addBot: async (actor3, input) => {
19534
+ await rosterService.add(actor3, input);
18830
19535
  }
18831
19536
  },
18832
19537
  publicRoom: {
@@ -18853,7 +19558,9 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18853
19558
  workspaceController: new WorkspaceController(createProjectCollaborationInitializer({
18854
19559
  membershipStore: projectMembershipStore,
18855
19560
  roomStore: projectRoomStore,
18856
- userTenantLinkStore
19561
+ userTenantLinkStore,
19562
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope),
19563
+ log: realtimeLog
18857
19564
  })),
18858
19565
  taskController: createTaskController({
18859
19566
  access: {
@@ -19115,7 +19822,9 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
19115
19822
  registerWorkspaceRoutes(app2, createProjectCollaborationInitializer({
19116
19823
  membershipStore: projectMembershipStore,
19117
19824
  roomStore: projectRoomStore,
19118
- userTenantLinkStore
19825
+ userTenantLinkStore,
19826
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope),
19827
+ log: realtimeLog
19119
19828
  }));
19120
19829
  registerDatabaseConfigRoutes(app2);
19121
19830
  registerMetricsServerConfigRoutes(app2);
@@ -19127,6 +19836,10 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
19127
19836
  executeDataQuery
19128
19837
  );
19129
19838
  registerMcpServerConfigRoutes(app2);
19839
+ registerPushNotificationRoutes(app2, {
19840
+ subscriptionStore: userPushSubscriptionStore,
19841
+ vapidPublicKey: process.env.VAPID_PUBLIC_KEY || null
19842
+ });
19130
19843
  registerUserRoutes(app2);
19131
19844
  registerTenantRoutes(app2);
19132
19845
  registerAuthRoutes(app2, {
@@ -19312,7 +20025,7 @@ function tryCreateProjectTaskRuntime(input) {
19312
20025
  function createRuntime(input, stores, log2) {
19313
20026
  const controller = new AbortController();
19314
20027
  const mutex = new ProjectTaskExecutionMutex();
19315
- const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID19 });
20028
+ const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID22 });
19316
20029
  const lifecycle = createTaskLifecycleService({ taskStore: input.taskStore, workItemStore: input.taskWorkItemStore });
19317
20030
  const projection = new RoomProjectionService({
19318
20031
  messages: input.projectRoomMessageStore,
@@ -19321,7 +20034,7 @@ function createRuntime(input, stores, log2) {
19321
20034
  warnings: input.taskWorkItemStore,
19322
20035
  log: log2,
19323
20036
  realtime: input.realtime,
19324
- idFactory: randomUUID19
20037
+ idFactory: randomUUID22
19325
20038
  });
19326
20039
  const agentFor = (task, thread) => agentInstanceManager6.getAgent({
19327
20040
  tenant_id: task.tenantId,
@@ -19358,7 +20071,8 @@ function createRuntime(input, stores, log2) {
19358
20071
  mutex,
19359
20072
  projection,
19360
20073
  log: log2,
19361
- signal: controller.signal
20074
+ signal: controller.signal,
20075
+ ...input.coordinatorWake === void 0 ? {} : { coordinatorWake: input.coordinatorWake }
19362
20076
  }, PROJECT_TASK_OBSERVER_OPTIONS);
19363
20077
  const taskLifecycleListener = ((event) => {
19364
20078
  void observer.listener(event);
@@ -20146,7 +20860,7 @@ var collectionRegistration = {
20146
20860
 
20147
20861
  // src/export_registrations/channel-installation.registration.ts
20148
20862
  import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
20149
- import { randomUUID as randomUUID20 } from "crypto";
20863
+ import { randomUUID as randomUUID23 } from "crypto";
20150
20864
  var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
20151
20865
  var PUBLIC_CHANNELS = /* @__PURE__ */ new Set(["lark", "email", "slack", "wechat"]);
20152
20866
  function isPublicChannel(channel) {
@@ -20305,7 +21019,7 @@ var channelInstallationRegistration = {
20305
21019
  }
20306
21020
  id3 = sourceId;
20307
21021
  } else {
20308
- id3 = randomUUID20();
21022
+ id3 = randomUUID23();
20309
21023
  }
20310
21024
  await store.createInstallation(tenantId, id3, createInput);
20311
21025
  return { newId: id3 };
@@ -21191,9 +21905,9 @@ import {
21191
21905
  } from "@axiom-lattice/core";
21192
21906
  import {
21193
21907
  snapshotExactArray as snapshotExactArray3,
21194
- snapshotExactRecord as snapshotExactRecord14
21908
+ snapshotExactRecord as snapshotExactRecord15
21195
21909
  } from "@axiom-lattice/protocols";
21196
- import { randomUUID as randomUUID21 } from "crypto";
21910
+ import { randomUUID as randomUUID24 } from "crypto";
21197
21911
  var BindingNotFoundError = class extends Error {
21198
21912
  constructor(message) {
21199
21913
  super(message);
@@ -21221,8 +21935,13 @@ var CORRELATED_REPLY_REQUIRED_KEYS = [
21221
21935
  "outputText",
21222
21936
  "state"
21223
21937
  ];
21938
+ var INTERRUPT_PAUSED_NOTICE = "This assistant is paused waiting for an approval or input and will continue once it is resolved.";
21939
+ var RESUME_DEDUPE_WINDOW_MS = 3e4;
21940
+ function clarifyText(clarify) {
21941
+ return ["```clarify", JSON.stringify({ questions: clarify.questions }), "```"].join("\n");
21942
+ }
21224
21943
  function exactDataValues(value, required, optional = []) {
21225
- return snapshotExactRecord14(value, required, optional);
21944
+ return snapshotExactRecord15(value, required, optional);
21226
21945
  }
21227
21946
  function uniqueNonemptyStrings(value) {
21228
21947
  const snapshot = snapshotExactArray3(value);
@@ -21301,6 +22020,8 @@ function correlatedRoomReplyTarget(replyTarget, inputMessageId) {
21301
22020
  var MessageRouter = class {
21302
22021
  constructor(config) {
21303
22022
  this.replySubscriptions = /* @__PURE__ */ new Set();
22023
+ /** Recent answer-by-message resumes per presence thread (first answer wins). */
22024
+ this.recentResumes = /* @__PURE__ */ new Map();
21304
22025
  this.middlewares = [...config.middlewares];
21305
22026
  this.bindingRegistry = config.bindingRegistry;
21306
22027
  this.adapterRegistry = config.adapterRegistry;
@@ -21508,7 +22229,7 @@ var MessageRouter = class {
21508
22229
  }
21509
22230
  if (!threadId) {
21510
22231
  const threadStore = getStoreLattice26("default", "thread").store;
21511
- const newThreadId = randomUUID21();
22232
+ const newThreadId = randomUUID24();
21512
22233
  console.log({
21513
22234
  event: "dispatch:thread:create",
21514
22235
  agentId,
@@ -21554,19 +22275,51 @@ var MessageRouter = class {
21554
22275
  workspace_id: binding?.workspaceId || "",
21555
22276
  project_id: binding?.projectId || ""
21556
22277
  });
21557
- const inputMessageId = randomUUID21();
22278
+ const inputMessageId = randomUUID24();
21558
22279
  const replyTarget = internalRoom ? correlatedRoomReplyTarget(message.replyTarget, inputMessageId) : message.replyTarget;
21559
22280
  const replyAdapter = replyTarget ? this.adapterRegistry.get(replyTarget.adapterChannel) : void 0;
21560
22281
  if (options.trustedMessageOptions && (!this.trustedDispatcher || !replyTarget || !replyAdapter)) {
21561
22282
  throw new Error("Trusted dispatch requires reply target/adapter");
21562
22283
  }
22284
+ let resumeCommand;
22285
+ let interruptNotice;
22286
+ if (internalRoom) {
22287
+ try {
22288
+ const status = await agent.getRunStatus();
22289
+ if (String(status) === "interrupted") {
22290
+ const clarify = readClarifyInterrupt(await agent.getCurrentState());
22291
+ const lastResume = this.recentResumes.get(threadId);
22292
+ const resumeInFlight = lastResume !== void 0 && Date.now() - lastResume < RESUME_DEDUPE_WINDOW_MS;
22293
+ if (clarify && !resumeInFlight) {
22294
+ await this.projectRoomInterrupt(replyTarget, replyAdapter, installation, inputMessageId, clarifyText(clarify));
22295
+ this.recentResumes.set(threadId, Date.now());
22296
+ if (this.recentResumes.size > 256) {
22297
+ for (const [key, at] of this.recentResumes) {
22298
+ if (Date.now() - at >= RESUME_DEDUPE_WINDOW_MS) this.recentResumes.delete(key);
22299
+ }
22300
+ }
22301
+ resumeCommand = buildClarifyResumeCommand(message.content.text);
22302
+ } else if (!clarify) {
22303
+ interruptNotice = INTERRUPT_PAUSED_NOTICE;
22304
+ }
22305
+ }
22306
+ } catch (error) {
22307
+ console.warn({
22308
+ event: "dispatch:interrupt:status-failed",
22309
+ threadId,
22310
+ error: error instanceof Error ? error.message : String(error)
22311
+ });
22312
+ }
22313
+ }
21563
22314
  const queueMessage = {
21564
22315
  input: { id: inputMessageId, message: message.content.text },
21565
- custom_run_config: message.content.metadata
22316
+ custom_run_config: message.content.metadata,
22317
+ ...resumeCommand ? { command: resumeCommand } : {}
21566
22318
  };
21567
22319
  let cleanupReplySubscription;
21568
22320
  if (replyTarget && replyAdapter) {
21569
22321
  let cleaned = false;
22322
+ let onInterrupted;
21570
22323
  const callback = (value) => {
21571
22324
  const event = parseCorrelatedReplyReadyEvent(value);
21572
22325
  if (!event) return;
@@ -21592,12 +22345,26 @@ var MessageRouter = class {
21592
22345
  cleaned = true;
21593
22346
  clearTimeout(timer);
21594
22347
  agent.unsubscribe("reply:ready", callback);
22348
+ if (onInterrupted !== void 0) agent.unsubscribe("message:interrupted", onInterrupted);
21595
22349
  this.replySubscriptions.delete(subscription);
21596
22350
  };
21597
22351
  const subscription = { inputMessageId, timer, cleanup };
21598
22352
  cleanupReplySubscription = cleanup;
21599
22353
  this.replySubscriptions.add(subscription);
21600
22354
  agent.subscribe("reply:ready", callback);
22355
+ if (internalRoom) {
22356
+ onInterrupted = (value) => {
22357
+ if (cleaned) return;
22358
+ const event = typeof value === "object" && value !== null ? value : void 0;
22359
+ if (event?.type !== "message:interrupted" || event.messageId !== inputMessageId) return;
22360
+ const clarify = readClarifyInterrupt(event.state);
22361
+ void this.projectRoomInterrupt(replyTarget, replyAdapter, installation, inputMessageId, clarify ? clarifyText(clarify) : INTERRUPT_PAUSED_NOTICE);
22362
+ };
22363
+ agent.subscribe("message:interrupted", onInterrupted);
22364
+ }
22365
+ }
22366
+ if (interruptNotice !== void 0 && replyTarget && replyAdapter) {
22367
+ await this.projectRoomInterrupt(replyTarget, replyAdapter, installation, inputMessageId, interruptNotice);
21601
22368
  }
21602
22369
  let addResult;
21603
22370
  try {
@@ -21659,12 +22426,34 @@ var MessageRouter = class {
21659
22426
  };
21660
22427
  return dispatch(0);
21661
22428
  }
22429
+ /**
22430
+ * Projects a pending HITL interrupt into the room as an independent bot message.
22431
+ *
22432
+ * @remarks
22433
+ * Best-effort: adapters without interrupt support and persistence failures are
22434
+ * logged and never block dispatch or reply correlation.
22435
+ */
22436
+ async projectRoomInterrupt(replyTarget, replyAdapter, installation, inputMessageId, text) {
22437
+ if (!replyTarget || !replyAdapter?.sendInterrupt || !installation) {
22438
+ console.warn({ event: "dispatch:interrupt:unsupported", inputMessageId });
22439
+ return;
22440
+ }
22441
+ try {
22442
+ await replyAdapter.sendInterrupt(replyTarget, { text, metadata: { inputMessageId } }, installation);
22443
+ } catch (error) {
22444
+ console.error({
22445
+ event: "dispatch:interrupt:failed",
22446
+ inputMessageId,
22447
+ error: error instanceof Error ? error.message : String(error)
22448
+ });
22449
+ }
22450
+ }
21662
22451
  };
21663
22452
 
21664
22453
  // src/channels/room/RoomChannelAdapter.ts
21665
- import { z as z10 } from "zod";
21666
- import { snapshotExactRecord as snapshotExactRecord15 } from "@axiom-lattice/protocols";
21667
- var roomConfigSchema = z10.object({}).strict();
22454
+ import { z as z11 } from "zod";
22455
+ import { snapshotExactRecord as snapshotExactRecord16 } from "@axiom-lattice/protocols";
22456
+ var roomConfigSchema = z11.object({}).strict();
21668
22457
  function createRoomChannelAdapter(writer) {
21669
22458
  return {
21670
22459
  channel: "room",
@@ -21692,6 +22481,23 @@ function createRoomChannelAdapter(writer) {
21692
22481
  sourceId: metadata.outputMessageId,
21693
22482
  idempotencyKey: `room-reply:${target.sourceRoomMessageId}:${target.targetMembershipId}:${metadata.outputMessageId}`
21694
22483
  });
22484
+ },
22485
+ async sendInterrupt(replyTarget, message, installation) {
22486
+ const installed = installationValues(installation);
22487
+ if (!installed) throw new Error("Invalid room installation");
22488
+ const targetEnvelope = exactValues2(replyTarget, ["adapterChannel", "channelInstallationId", "rawTarget"]);
22489
+ const target = exactStrings(targetEnvelope?.rawTarget, ["sourceRoomMessageId", "targetMembershipId", "inputMessageId"]);
22490
+ const envelope = exactValues2(message, ["text", "metadata"]);
22491
+ const metadata = exactStrings(envelope?.metadata, ["inputMessageId"]);
22492
+ if (!targetEnvelope || targetEnvelope.adapterChannel !== "room" || targetEnvelope.channelInstallationId !== installation.id || !target || !metadata || typeof envelope?.text !== "string") throw new Error("Invalid room interrupt");
22493
+ if (target.inputMessageId !== metadata.inputMessageId) throw new Error("Invalid room interrupt correlation");
22494
+ await writer.postInterrupt({
22495
+ tenantId: installed.tenantId,
22496
+ membershipId: target.targetMembershipId,
22497
+ text: envelope.text,
22498
+ sourceRoomMessageId: target.sourceRoomMessageId,
22499
+ inputMessageId: metadata.inputMessageId
22500
+ });
21695
22501
  }
21696
22502
  };
21697
22503
  }
@@ -21707,7 +22513,7 @@ function exactStrings(value, keys2) {
21707
22513
  function exactValues2(value, required, optional = []) {
21708
22514
  try {
21709
22515
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
21710
- return snapshotExactRecord15(value, required, optional);
22516
+ return snapshotExactRecord16(value, required, optional);
21711
22517
  } catch {
21712
22518
  return void 0;
21713
22519
  }
@@ -21996,7 +22802,7 @@ var start = async (config) => {
21996
22802
  });
21997
22803
  try {
21998
22804
  const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-7UGQ7GMW.mjs");
21999
- const { registerA2AStandardRoutes } = await import("./a2a-standard-VEQBXBMM.mjs");
22805
+ const { registerA2AStandardRoutes } = await import("./a2a-standard-ALXWP7V6.mjs");
22000
22806
  const a2a = await import("./a2a-FGYX2HH3.mjs");
22001
22807
  const a2aKeyStore = getStoreLattice27("default", "a2aApiKey").store;
22002
22808
  const assistantStore = getStoreLattice27("default", "assistant").store;
@@ -22069,7 +22875,7 @@ var start = async (config) => {
22069
22875
  logger5.info("Registered sandbox manager from env configuration");
22070
22876
  }
22071
22877
  try {
22072
- const { ResourceController } = await import("./resources-SRQRKW5G.mjs");
22878
+ const { ResourceController } = await import("./resources-JFKAPS45.mjs");
22073
22879
  const sharedResourceStore = getStoreLattice27("default", "sharedResource").store;
22074
22880
  const cache = new TokenCache();
22075
22881
  const resourceController = new ResourceController({