@axiom-lattice/gateway 4.2.2 → 4.2.4

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-DTDBMB2U.js → embed-page-AgentDetailSideApp-3LBCEKFU.js} +1 -1
  7. package/dist/embed/{embed-page-AgentGovernanceWorkbench-JUP6SNBX.js → embed-page-AgentGovernanceWorkbench-K3JUQNBW.js} +1 -1
  8. package/dist/embed/{embed-page-MessageGraph-H5S7SARE.js → embed-page-MessageGraph-3X4ROEN6.js} +1 -1
  9. package/dist/embed/{embed-page-ReactChart-FWXMXO7F.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-Q6HTW6S6.js → embed-page-TasksSideApp-3WRDBZU3.js} +1 -1
  12. package/dist/embed/{embed-page-TeamWorkspace-6KFWCIKW.js → embed-page-TeamWorkspace-6JSMZYUF.js} +1 -1
  13. package/dist/embed/{embed-page-TopologyRuntimeView-MASWKWXA.js → embed-page-TopologyRuntimeView-YDFENLHH.js} +1 -1
  14. package/dist/embed/{embed-page-WorkflowAutomationView-6UOT36GW.js → embed-page-WorkflowAutomationView-MP5B7Q2Q.js} +1 -1
  15. package/dist/embed/{embed-page-chunk-AVZ7FKIO.js → embed-page-chunk-B2E2XOH2.js} +10 -10
  16. package/dist/embed/{embed-page-chunk-X4DK4JRG.js → embed-page-chunk-FFO7CAYL.js} +1 -1
  17. package/dist/embed/{embed-page-chunk-NHOWQIND.js → embed-page-chunk-GDTBDOUE.js} +284 -204
  18. package/dist/embed/{embed-page-chunk-NFLB3VUZ.js → embed-page-chunk-IRP6JSSX.js} +1 -1
  19. package/dist/embed/{embed-page-chunk-3EUETTEM.js → embed-page-chunk-PAJ5NG2Z.js} +1 -1
  20. package/dist/embed/{embed-page-chunk-FJ3SFKJY.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-2WCPJYBC.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-7SO4HHS4.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 +1720 -872
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +1209 -430
  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
  }
@@ -13519,31 +13689,72 @@ function registerCapabilityBundleRoutes(app2, controller) {
13519
13689
  }
13520
13690
 
13521
13691
  // src/services/collaboration/ProjectRoomService.ts
13522
- import { randomUUID as randomUUID12 } from "crypto";
13692
+ import { randomUUID as randomUUID13 } from "crypto";
13523
13693
  var ProjectRoomService = class {
13524
13694
  /** Creates a project room service from explicitly injected collaborators. */
13525
13695
  constructor(deps) {
13526
13696
  this.deps = deps;
13527
- this.idFactory = deps.idFactory ?? randomUUID12;
13697
+ this.idFactory = deps.idFactory ?? randomUUID13;
13528
13698
  }
13529
- /** 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
+ */
13530
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) {
13531
13741
  const { project } = await this.deps.accessService.requireProjectAccess({
13532
13742
  ...input,
13533
13743
  capability: "read"
13534
13744
  });
13535
- return this.deps.roomStore.ensureMainRoom({
13745
+ const room = await this.deps.roomStore.ensureMainRoom({
13536
13746
  id: this.idFactory(),
13537
13747
  tenantId: input.tenantId,
13538
13748
  workspaceId: project.workspaceId,
13539
13749
  projectId: project.id,
13540
13750
  name: project.name
13541
13751
  });
13752
+ return { project, room };
13542
13753
  }
13543
13754
  };
13544
13755
 
13545
13756
  // src/services/collaboration/ProjectMembershipService.ts
13546
- import { randomUUID as randomUUID13 } from "crypto";
13757
+ import { randomUUID as randomUUID14 } from "crypto";
13547
13758
  var ProjectMembershipServiceError = class extends Error {
13548
13759
  constructor(code, statusCode, message) {
13549
13760
  super(message);
@@ -13559,30 +13770,30 @@ function isMembershipConflict(error) {
13559
13770
  var ProjectMembershipService = class {
13560
13771
  constructor(deps) {
13561
13772
  this.deps = deps;
13562
- this.idFactory = deps.idFactory ?? randomUUID13;
13773
+ this.idFactory = deps.idFactory ?? randomUUID14;
13563
13774
  this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
13564
13775
  }
13565
- async list(actor2) {
13566
- await this.deps.accessService.requireRosterAccess(actor2);
13567
- 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);
13568
13779
  if (!membership || membership.status !== "active") this.accessDenied();
13569
- return this.deps.membershipStore.list(actor2.tenantId, actor2.projectId);
13780
+ return this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
13570
13781
  }
13571
- async add(actor2, input) {
13572
- return this.withProjectLock(actor2, async () => {
13573
- const access = await this.authorize(actor2);
13782
+ async add(actor3, input) {
13783
+ return this.withProjectLock(actor3, async () => {
13784
+ const access = await this.authorize(actor3);
13574
13785
  if (input.role === "owner" && access.role !== "owner") this.accessDenied();
13575
- if (!await this.deps.userTenantLinkStore.getLink(input.userId, actor2.tenantId)) {
13786
+ if (!await this.deps.userTenantLinkStore.getLink(input.userId, actor3.tenantId)) {
13576
13787
  throw new ProjectMembershipServiceError("TARGET_USER_NOT_IN_TENANT", 400, "Target user is not in the tenant");
13577
13788
  }
13578
- 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)) {
13579
13790
  throw new ProjectMembershipServiceError("MEMBERSHIP_EXISTS", 409, "Project membership already exists");
13580
13791
  }
13581
13792
  try {
13582
13793
  const created = await this.deps.membershipStore.create({
13583
13794
  id: this.idFactory(),
13584
- tenantId: actor2.tenantId,
13585
- projectId: actor2.projectId,
13795
+ tenantId: actor3.tenantId,
13796
+ projectId: actor3.projectId,
13586
13797
  userId: input.userId,
13587
13798
  role: input.role,
13588
13799
  status: "active"
@@ -13600,36 +13811,36 @@ var ProjectMembershipService = class {
13600
13811
  }
13601
13812
  });
13602
13813
  }
13603
- async updateRole(actor2, input) {
13604
- return this.withProjectLock(actor2, async () => {
13605
- const access = await this.authorize(actor2);
13606
- 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);
13607
13818
  this.requireActiveAndCurrent(target, input.expectedUpdatedAt);
13608
13819
  if ((target.role === "owner" || input.role === "owner") && access.role !== "owner") this.accessDenied();
13609
- 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 }));
13610
13821
  this.publish(updated, "role_changed", `membership:${updated.id}:role:${updated.role}:${input.expectedUpdatedAt.toISOString()}`);
13611
13822
  return updated;
13612
13823
  });
13613
13824
  }
13614
- async remove(actor2, input) {
13615
- return this.withProjectLock(actor2, async () => {
13616
- const access = await this.authorize(actor2);
13617
- 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);
13618
13829
  this.requireActiveAndCurrent(target, input.expectedUpdatedAt);
13619
13830
  if (target.role === "owner" && access.role !== "owner") this.accessDenied();
13620
- 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 }));
13621
13832
  this.publish(removed, "removed", `membership:${removed.id}:remove:${input.expectedUpdatedAt.toISOString()}`);
13622
13833
  return removed;
13623
13834
  });
13624
13835
  }
13625
- async authorize(actor2) {
13626
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "manage" });
13627
- 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);
13628
13839
  if (!membership || membership.status !== "active") this.accessDenied();
13629
13840
  return membership;
13630
13841
  }
13631
- async target(actor2, id3) {
13632
- 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);
13633
13844
  if (!target) throw new ProjectMembershipServiceError("MEMBERSHIP_NOT_FOUND", 404, "Project membership not found");
13634
13845
  return target;
13635
13846
  }
@@ -13684,16 +13895,16 @@ var ProjectMembershipService = class {
13684
13895
  accessDenied() {
13685
13896
  throw new ProjectMembershipServiceError("PROJECT_ACCESS_DENIED", 403, "Project access denied");
13686
13897
  }
13687
- async withProjectLock(actor2, operation) {
13898
+ async withProjectLock(actor3, operation) {
13688
13899
  return this.mutationMutex.runExclusive(
13689
- { tenantId: actor2.tenantId, projectId: actor2.projectId },
13900
+ { tenantId: actor3.tenantId, projectId: actor3.projectId },
13690
13901
  operation
13691
13902
  );
13692
13903
  }
13693
13904
  };
13694
13905
 
13695
13906
  // src/services/collaboration/ProjectRosterService.ts
13696
- import { randomUUID as randomUUID14 } from "crypto";
13907
+ import { randomUUID as randomUUID15 } from "crypto";
13697
13908
  var ProjectRosterServiceError = class extends Error {
13698
13909
  /** Creates a stable roster service error. */
13699
13910
  constructor(code, statusCode, message) {
@@ -13703,35 +13914,38 @@ var ProjectRosterServiceError = class extends Error {
13703
13914
  this.name = "ProjectRosterServiceError";
13704
13915
  }
13705
13916
  };
13917
+ function coordinatorProtected(message) {
13918
+ return new ProjectRosterServiceError("ROOM_COORDINATOR_PROTECTED", 409, message);
13919
+ }
13706
13920
  var activeStatuses = ["active", "paused"];
13707
13921
  var activeTaskStatuses = ["pending", "in_progress", "interrupted"];
13708
- var metadataKeys = ["source", "kind", "workspaceId", "projectId", "roomId", "membershipId", "assistantId"];
13709
13922
  var ProjectRosterService = class {
13710
13923
  /** Creates a roster service from explicitly injected collaborators. */
13711
13924
  constructor(deps) {
13712
13925
  this.deps = deps;
13713
- this.idFactory = deps.idFactory ?? randomUUID14;
13926
+ this.idFactory = deps.idFactory ?? randomUUID15;
13714
13927
  this.mutationMutex = deps.projectMutationMutex ?? projectMutationMutex;
13715
13928
  }
13716
13929
  /** Lists all bot memberships visible to a project reader. */
13717
- async list(actor2) {
13718
- await this.authorize(actor2, "read");
13719
- 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);
13720
13933
  }
13721
13934
  /** Adds or reactivates a bot and ensures its room-presence thread exists. */
13722
- async add(actor2, input) {
13723
- return this.withLock(actor2, async () => {
13724
- await this.authorize(actor2, "manage");
13725
- const room = await this.deps.roomService.ensureMainRoom(actor2);
13726
- const assistant = await this.deps.assistantStore.getAssistantById(actor2.tenantId, input.assistantId);
13727
- 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");
13728
13942
  const normalized = this.validateInput(input);
13729
- const existing = await this.deps.membershipStore.findByAssistant(actor2.tenantId, actor2.projectId, input.assistantId);
13730
- if (existing) this.validateMembership(existing, actor2, room, input.assistantId);
13731
- 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);
13732
13946
  for (let attempt = 0; attempt < 3; attempt += 1) {
13733
13947
  try {
13734
- await this.ensureThread(actor2, room, input.assistantId, ids.membershipId, ids.threadId);
13948
+ await this.ensureThread(actor3, room, input.assistantId, ids.membershipId, ids.threadId);
13735
13949
  break;
13736
13950
  } catch (error) {
13737
13951
  if (!(error instanceof ProjectRosterServiceError) || error.code !== "THREAD_CONFLICT" || existing) throw error;
@@ -13739,65 +13953,67 @@ var ProjectRosterService = class {
13739
13953
  if (attempt === 2) throw error;
13740
13954
  }
13741
13955
  }
13742
- await this.authorize(actor2, "manage");
13956
+ await this.authorize(actor3, "manage");
13743
13957
  let result;
13744
13958
  try {
13745
- 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 });
13746
13960
  } catch (error) {
13747
13961
  if (error instanceof Error && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
13748
13962
  throw error;
13749
13963
  }
13750
13964
  if (result.kind !== "created" && result.kind !== "updated" && result.kind !== "reactivated") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
13751
- this.validateMembership(result.membership, actor2, room, input.assistantId, ids.threadId);
13965
+ this.validateMembership(result.membership, actor3, room, input.assistantId, ids.threadId);
13752
13966
  await this.deps.bindingLifecycle.ensure(result.membership);
13753
13967
  this.publish(result.membership, result.kind === "created" ? "added" : result.kind === "reactivated" ? "resumed" : "updated", `roster:${result.membership.id}:${result.membership.updatedAt.toISOString()}`);
13754
13968
  return result.membership;
13755
13969
  });
13756
13970
  }
13757
13971
  /** Updates mutable bot presentation fields using optimistic concurrency. */
13758
- async update(actor2, input) {
13972
+ async update(actor3, input) {
13759
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) } };
13760
- return this.mutate(actor2, input.membershipId, input.expectedUpdatedAt, patch);
13974
+ return this.mutate(actor3, input.membershipId, input.expectedUpdatedAt, patch);
13761
13975
  }
13762
13976
  /** Pauses a bot without deleting its presence thread or history. */
13763
- async pause(actor2, input) {
13764
- 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");
13765
13979
  }
13766
13980
  /** Resumes a bot without replacing its presence thread or history. */
13767
- async resume(actor2, input) {
13768
- 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");
13769
13983
  }
13770
13984
  /** Removes a bot after checking every active task status independently. */
13771
- async remove(actor2, input) {
13772
- return this.withLock(actor2, async () => {
13773
- await this.authorize(actor2, "manage");
13774
- 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);
13775
13989
  this.requireCurrent(target, input.expectedUpdatedAt);
13990
+ if (target.role === "coordinator") throw coordinatorProtected("The built-in room coordinator cannot be removed");
13776
13991
  for (const status of activeTaskStatuses) {
13777
- 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 });
13778
13993
  if (tasks.length > 0) throw new ProjectRosterServiceError("BOT_HAS_ACTIVE_TASKS", 409, "Bot has active tasks");
13779
13994
  }
13780
- await this.authorize(actor2, "manage");
13995
+ await this.authorize(actor3, "manage");
13781
13996
  await this.deps.bindingLifecycle.remove(target);
13782
13997
  try {
13783
- const removed = await this.applyUpdate(actor2, target, { status: "removed" }, input.expectedUpdatedAt);
13998
+ const removed = await this.applyUpdate(actor3, target, { status: "removed" }, input.expectedUpdatedAt);
13784
13999
  this.publish(removed, "removed", `roster:${removed.id}:${removed.updatedAt.toISOString()}`);
13785
14000
  return removed;
13786
14001
  } catch (error) {
13787
- const current = await this.deps.membershipStore.findById(actor2.tenantId, target.id);
13788
- 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;
13789
14004
  await this.applyLifecycle(current);
13790
14005
  throw error;
13791
14006
  }
13792
14007
  });
13793
14008
  }
13794
- async mutate(actor2, id3, expected, patch, lifecycle) {
13795
- return this.withLock(actor2, async () => {
13796
- await this.authorize(actor2, "manage");
13797
- 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);
13798
14013
  this.requireCurrent(target, expected);
13799
- await this.authorize(actor2, "manage");
13800
- 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);
13801
14017
  if (lifecycle === "pause") await this.deps.bindingLifecycle.pause(updated);
13802
14018
  else if (lifecycle === "resume") await this.deps.bindingLifecycle.resume(updated);
13803
14019
  else await this.applyLifecycle(updated);
@@ -13806,6 +14022,18 @@ var ProjectRosterService = class {
13806
14022
  return updated;
13807
14023
  });
13808
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
+ }
13809
14037
  publish(membership, change, sourceKey) {
13810
14038
  if (!this.deps.realtime) return;
13811
14039
  Promise.resolve().then(() => this.deps.realtime.rosterChanged({ tenantId: membership.tenantId, roomId: membership.roomId, membership, change, sourceKey })).catch(() => {
@@ -13826,36 +14054,36 @@ var ProjectRosterService = class {
13826
14054
  }
13827
14055
  await this.deps.bindingLifecycle.ensure(membership);
13828
14056
  }
13829
- async applyUpdate(actor2, target, patch, expectedUpdatedAt2) {
14057
+ async applyUpdate(actor3, target, patch, expectedUpdatedAt2) {
13830
14058
  let result;
13831
14059
  try {
13832
- 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 });
13833
14061
  } catch (error) {
13834
14062
  if (error instanceof Error && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
13835
14063
  throw error;
13836
14064
  }
13837
14065
  if (result.kind === "updated") {
13838
- 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);
13839
14067
  return result.membership;
13840
14068
  }
13841
14069
  if (result.kind === "not_found") throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
13842
14070
  if (result.kind === "conflict") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster member changed; reload before saving");
13843
14071
  throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
13844
14072
  }
13845
- async authorize(actor2, capability2) {
13846
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: capability2 });
14073
+ async authorize(actor3, capability2) {
14074
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: capability2 });
13847
14075
  }
13848
- async target(actor2, id3) {
13849
- const target = await this.deps.membershipStore.findById(actor2.tenantId, id3);
13850
- if (!target || target.tenantId !== actor2.tenantId || target.projectId !== actor2.projectId) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
13851
- const room = await this.deps.roomService.ensureMainRoom(actor2);
13852
- const assistant = await this.deps.assistantStore.getAssistantById(actor2.tenantId, target.assistantId);
13853
- 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");
13854
14082
  if (!target.roomThreadId) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster member immutable scope mismatch");
13855
- this.validateMembership(target, actor2, room, target.assistantId, target.roomThreadId);
13856
- 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);
13857
14085
  if (!thread) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread not found");
13858
- 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);
13859
14087
  return target;
13860
14088
  }
13861
14089
  requireCurrent(target, expected) {
@@ -13881,56 +14109,55 @@ var ProjectRosterService = class {
13881
14109
  return normalized;
13882
14110
  }
13883
14111
  metadata(metadata, expected) {
13884
- if (!metadata || Object.keys(metadata).length !== metadataKeys.length || metadataKeys.some((key) => !Object.prototype.hasOwnProperty.call(metadata, key))) return false;
13885
- 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);
13886
14113
  }
13887
- expectedMetadata(room, actor2, membershipId, assistantId) {
13888
- 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 });
13889
14116
  }
13890
- validateMembership(value, actor2, room, assistantId, threadId) {
13891
- 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");
13892
14119
  }
13893
- async findSafePresence(actor2, room, assistantId) {
13894
- 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 });
13895
- 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);
13896
14123
  for (const thread of threads) {
13897
14124
  const metadata = thread.metadata;
13898
- 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;
13899
14126
  const membershipId = metadata.membershipId;
13900
- const owner = await this.deps.membershipStore.findById(actor2.tenantId, membershipId);
13901
- 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;
13902
14129
  if (memberships.some((item) => item.roomThreadId === thread.id)) continue;
13903
14130
  return { membershipId, threadId: thread.id };
13904
14131
  }
13905
14132
  return { membershipId: this.idFactory(), threadId: this.idFactory() };
13906
14133
  }
13907
- async ensureThread(actor2, room, assistantId, membershipId, threadId) {
13908
- const expected = this.expectedMetadata(room, actor2, membershipId, assistantId);
13909
- 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);
13910
14137
  if (existing) {
13911
- this.validateThread(existing, actor2.tenantId, expected, threadId);
14138
+ this.validateThread(existing, actor3.tenantId, expected, threadId);
13912
14139
  return;
13913
14140
  }
13914
- const created = await this.deps.threadStore.createThread(actor2.tenantId, assistantId, threadId, { metadata: expected });
13915
- this.validateThread(created, actor2.tenantId, expected, threadId);
13916
- 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);
13917
14144
  if (!reread) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread was not persisted");
13918
- this.validateThread(reread, actor2.tenantId, expected, threadId);
14145
+ this.validateThread(reread, actor3.tenantId, expected, threadId);
13919
14146
  }
13920
14147
  validateThread(thread, tenantId, expected, threadId) {
13921
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");
13922
14149
  }
13923
14150
  /** Serializes roster writes with task ownership mutations in this process. */
13924
- withLock(actor2, operation) {
14151
+ withLock(actor3, operation) {
13925
14152
  return this.mutationMutex.runExclusive(
13926
- { tenantId: actor2.tenantId, projectId: actor2.projectId },
14153
+ { tenantId: actor3.tenantId, projectId: actor3.projectId },
13927
14154
  operation
13928
14155
  );
13929
14156
  }
13930
14157
  };
13931
14158
 
13932
14159
  // src/services/collaboration/RoomMessageService.ts
13933
- import { createHash as createHash4, randomUUID as randomUUID15 } from "crypto";
14160
+ import { createHash as createHash4, randomUUID as randomUUID16 } from "crypto";
13934
14161
 
13935
14162
  // src/services/collaboration/ProjectBotMembershipSnapshot.ts
13936
14163
  import { snapshotExactRecord as snapshotExactRecord4 } from "@axiom-lattice/protocols";
@@ -14019,54 +14246,54 @@ var RoomMessageService = class {
14019
14246
  constructor(deps, options = {}) {
14020
14247
  this.deps = deps;
14021
14248
  this.idempotentOutcomes = /* @__PURE__ */ new Map();
14022
- this.idFactory = deps.idFactory ?? randomUUID15;
14249
+ this.idFactory = deps.idFactory ?? randomUUID16;
14023
14250
  const limit = options.idempotentOutcomeLimit ?? 1e3;
14024
14251
  if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("idempotentOutcomeLimit must be a positive safe integer");
14025
14252
  this.idempotentOutcomeLimit = limit;
14026
14253
  }
14027
14254
  /** Lists newest-first messages strictly before an optional cursor. */
14028
- async list(actor2, input = {}) {
14255
+ async list(actor3, input = {}) {
14029
14256
  const snapshot = this.snapshotList(input);
14030
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "read" });
14031
- const room = await this.room(actor2, access.project);
14032
- const messages = await this.deps.messageStore.list({ tenantId: actor2.tenantId, roomId: room.id, ...snapshot.before === void 0 ? {} : { before: snapshot.before }, limit: snapshot.limit });
14033
- 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));
14034
14261
  }
14035
14262
  /**
14036
14263
  * Validates and persists a human-authored text message without dispatching it.
14037
14264
  * @deprecated This persistence primitive remains for the Phase 1 route boundary; the later
14038
14265
  * route task will switch POST handling to createAndDispatch.
14039
14266
  */
14040
- async createHumanMessage(actor2, input) {
14041
- return (await this.persistHumanMessage(actor2, input, false)).message;
14267
+ async createHumanMessage(actor3, input) {
14268
+ return (await this.persistHumanMessage(actor3, input, false)).message;
14042
14269
  }
14043
- async persistHumanMessage(actor2, input, allowIdempotencyKey) {
14044
- const snapshot = this.snapshotCreate(actor2, input, allowIdempotencyKey);
14045
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14046
- const room = await this.room(actor2, access.project);
14047
- 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);
14048
14275
  }
14049
- async persistNormalizedHumanMessage(actor2, snapshot, room) {
14050
- 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);
14051
14278
  let reply = null;
14052
14279
  if (snapshot.replyToMessageId !== void 0) {
14053
- reply = await this.deps.messageStore.findById(actor2.tenantId, snapshot.replyToMessageId);
14054
- 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");
14055
14282
  }
14056
- for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor2, room, membershipId);
14057
- 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" });
14058
14285
  const id3 = this.idFactory();
14059
14286
  let result;
14060
- 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 } };
14061
14288
  try {
14062
14289
  result = snapshot.idempotencyKey === void 0 ? await this.deps.messageStore.create(candidate) : await this.deps.messageStore.createIdempotent({ ...candidate, idempotencyKey: snapshot.idempotencyKey });
14063
14290
  } catch (error) {
14064
14291
  if (error instanceof Error && /Message.*Conflict|Conflict.*Message|DuplicateProjectRoomMessage/.test(error.name)) throw new RoomMessageServiceError("MESSAGE_CONFLICT", 409, "Message conflict");
14065
14292
  throw error;
14066
14293
  }
14067
- 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);
14068
14295
  const replayed = result.id !== id3;
14069
- 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");
14070
14297
  if (!matchesNormalizedPayload(validated, snapshot)) {
14071
14298
  if (replayed && snapshot.idempotencyKey !== void 0) throw new RoomMessageServiceError("IDEMPOTENCY_KEY_REUSED", 409, "Idempotency key reused with different message input");
14072
14299
  throw new RoomMessageServiceError("INVALID_STORED_MESSAGE", 500, "Invalid created message");
@@ -14078,11 +14305,11 @@ var RoomMessageService = class {
14078
14305
  * Idempotent outcomes are single-flight and retained only in this service process; a
14079
14306
  * store replay after restart reports `DISPATCH_STATUS_UNKNOWN` until explicitly retried.
14080
14307
  */
14081
- async createAndDispatch(actor2, input) {
14082
- const normalized = this.snapshotCreate(actor2, input, true);
14083
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14084
- const room = await this.room(actor2, access.project);
14085
- 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);
14086
14313
  const requestFingerprint = fingerprintRequest(normalized);
14087
14314
  if (key !== void 0) {
14088
14315
  const existing = this.idempotentOutcomes.get(key);
@@ -14092,7 +14319,7 @@ var RoomMessageService = class {
14092
14319
  return this.resultForCaller(await existing.promise, true);
14093
14320
  }
14094
14321
  this.admitIdempotentEntry();
14095
- const promise = this.createAndDispatchOutcome(actor2, normalized, room);
14322
+ const promise = this.createAndDispatchOutcome(actor3, normalized, room);
14096
14323
  const entry = { requestFingerprint, promise, settled: false };
14097
14324
  this.idempotentOutcomes.set(key, entry);
14098
14325
  promise.then(() => this.settleIdempotentOutcome(key, entry), () => {
@@ -14101,16 +14328,16 @@ var RoomMessageService = class {
14101
14328
  const resolved = await promise;
14102
14329
  return this.resultForCaller(resolved, resolved.replayed);
14103
14330
  }
14104
- const outcome = await this.createAndDispatchOutcome(actor2, normalized, room);
14331
+ const outcome = await this.createAndDispatchOutcome(actor3, normalized, room);
14105
14332
  return this.resultForCaller(outcome, outcome.replayed);
14106
14333
  }
14107
- async createAndDispatchOutcome(actor2, input, room) {
14108
- const persisted = await this.persistNormalizedHumanMessage(actor2, input, room);
14334
+ async createAndDispatchOutcome(actor3, input, room) {
14335
+ const persisted = await this.persistNormalizedHumanMessage(actor3, input, room);
14109
14336
  if (persisted.replayed) return { message: persisted.message, dispatch: [{ success: false, errorCode: "DISPATCH_STATUS_UNKNOWN" }], replayed: true };
14110
14337
  const message = persisted.message;
14111
14338
  this.notifyMessageCreated(message);
14112
14339
  try {
14113
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14340
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14114
14341
  } catch (error) {
14115
14342
  return { message, dispatch: [{ success: false, errorCode: error instanceof ProjectCollaborationError && error.code === "PROJECT_ACCESS_DENIED" ? "AUTHORIZATION_REVOKED" : "DISPATCH_FAILED" }], replayed: false };
14116
14343
  }
@@ -14129,8 +14356,8 @@ var RoomMessageService = class {
14129
14356
  });
14130
14357
  });
14131
14358
  }
14132
- idempotencyMapKey(actor2, room, input) {
14133
- 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}`;
14134
14361
  }
14135
14362
  settleIdempotentOutcome(key, entry) {
14136
14363
  if (this.idempotentOutcomes.get(key) !== entry) return;
@@ -14155,14 +14382,14 @@ var RoomMessageService = class {
14155
14382
  return { message: cloneRoomMessage(outcome.message), dispatch: outcome.dispatch.map((entry) => ({ ...entry })), replayed };
14156
14383
  }
14157
14384
  /** Retries an existing human message using the current roster without creating a copy. */
14158
- async retry(actor2, messageId) {
14159
- const access = await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14160
- const message = await this.deps.messageStore.findById(actor2.tenantId, messageId);
14161
- const room = await this.room(actor2, access.project);
14162
- 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") {
14163
14390
  throw new RoomMessageServiceError("INVALID_RETRY_SOURCE", 404, "Retry source not found");
14164
14391
  }
14165
- await this.deps.accessService.requireProjectAccess({ ...actor2, capability: "send" });
14392
+ await this.deps.accessService.requireProjectAccess({ ...actor3, capability: "send" });
14166
14393
  return this.safeDispatch(message);
14167
14394
  }
14168
14395
  async safeDispatch(message) {
@@ -14180,7 +14407,7 @@ var RoomMessageService = class {
14180
14407
  if (typeof limit !== "number" || !Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid message limit");
14181
14408
  return { ...before === void 0 ? {} : { before }, limit: Math.min(100, limit) };
14182
14409
  }
14183
- snapshotCreate(actor2, input, allowIdempotencyKey = false) {
14410
+ snapshotCreate(actor3, input, allowIdempotencyKey = false) {
14184
14411
  const allowed = allowIdempotencyKey ? ["text", "mentions", "replyToMessageId", "requestIdempotencyKey"] : ["text", "mentions", "replyToMessageId"];
14185
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");
14186
14413
  const value = input;
@@ -14200,35 +14427,35 @@ var RoomMessageService = class {
14200
14427
  let idempotencyKey;
14201
14428
  if (value.requestIdempotencyKey !== void 0) {
14202
14429
  if (typeof value.requestIdempotencyKey !== "string" || !/^[\x21-\x7e]{1,200}$/.test(value.requestIdempotencyKey.trim())) throw new RoomMessageServiceError("INVALID_MESSAGE", 400, "Invalid idempotency key");
14203
- 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")}`;
14204
14431
  }
14205
14432
  return { text, mentions, botIds, ...value.replyToMessageId === void 0 ? {} : { replyToMessageId: value.replyToMessageId }, ...idempotencyKey === void 0 ? {} : { idempotencyKey } };
14206
14433
  }
14207
- async room(actor2, project) {
14208
- 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");
14209
- const room = await this.deps.roomService.ensureMainRoom(actor2);
14210
- 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");
14211
14438
  return room;
14212
14439
  }
14213
- async validateMentionIdentity(actor2, room, id3) {
14214
- 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);
14215
14442
  const snapshot = snapshotProjectBotMembership(membership);
14216
- 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");
14217
14444
  }
14218
- matchesRoom(membership, actor2, room) {
14219
- 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;
14220
14447
  }
14221
- isMessage(message, actor2, room) {
14448
+ isMessage(message, actor3, room) {
14222
14449
  try {
14223
- this.validateMessage(message, actor2, room);
14450
+ this.validateMessage(message, actor3, room);
14224
14451
  return true;
14225
14452
  } catch {
14226
14453
  return false;
14227
14454
  }
14228
14455
  }
14229
- validateMessage(message, actor2, room, expectedId) {
14456
+ validateMessage(message, actor3, room, expectedId) {
14230
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");
14231
- 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");
14232
14459
  const mentions = message.mentions.map((mention) => {
14233
14460
  if (exact(mention, ["type"]) && mention.type === "team") return { type: "team" };
14234
14461
  if (exact(mention, ["membershipId", "type"]) && mention.type === "bot" && nonempty(mention.membershipId)) return { type: "bot", membershipId: mention.membershipId };
@@ -14271,6 +14498,111 @@ function matchesNormalizedPayload(message, input) {
14271
14498
  import { ZodError as ZodError3 } from "zod";
14272
14499
  import { toProjectRoomPublicMessage } from "@axiom-lattice/protocols";
14273
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
+
14274
14606
  // src/schemas/project-room.ts
14275
14607
  import { z as z9 } from "zod";
14276
14608
  var id2 = z9.string().trim().min(1).max(128);
@@ -14311,6 +14643,15 @@ var updateProjectBotSchema = z9.object({
14311
14643
  mentionName: z9.string().optional(),
14312
14644
  expectedUpdatedAt: isoDate
14313
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();
14314
14655
 
14315
14656
  // src/routes/project-rooms.ts
14316
14657
  var ProjectRoomAuthError = class extends Error {
@@ -14321,7 +14662,11 @@ var ProjectRoomAuthError = class extends Error {
14321
14662
  }
14322
14663
  };
14323
14664
  function registerProjectRoomRoutes(app2, deps) {
14324
- 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
+ }));
14325
14670
  app2.get("/api/projects/:projectId/room/messages", async (request, reply) => run(reply, async () => {
14326
14671
  const params = projectRoomParamsSchema.parse(request.params);
14327
14672
  const query = roomMessagesQuerySchema.parse(request.query);
@@ -14353,6 +14698,16 @@ function registerProjectRoomRoutes(app2, deps) {
14353
14698
  await deps.readStateService.mark(actor(request, params.projectId));
14354
14699
  return { data: { marked: true } };
14355
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
+ }));
14356
14711
  app2.get("/api/projects/:projectId/members", async (request, reply) => run(reply, async () => {
14357
14712
  const memberships = await deps.membershipService.list(actor(request, projectRoomParamsSchema.parse(request.params).projectId));
14358
14713
  const enriched = await Promise.all(memberships.map(async (membership) => {
@@ -14393,6 +14748,7 @@ async function run(reply, operation) {
14393
14748
  return reply.code(result.statusCode ?? 200).send({ success: true, data: result.data });
14394
14749
  } catch (error) {
14395
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 } });
14396
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 } });
14397
14753
  return reply.code(500).send({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error", retryable: false } });
14398
14754
  }
@@ -14446,7 +14802,7 @@ async function execute(reply, operation) {
14446
14802
  }
14447
14803
 
14448
14804
  // src/routes/cospace.ts
14449
- import { randomUUID as randomUUID16 } from "crypto";
14805
+ import { randomUUID as randomUUID18 } from "crypto";
14450
14806
  import { ZodError as ZodError5 } from "zod";
14451
14807
  import {
14452
14808
  generateDirectHumanProjectId,
@@ -14480,16 +14836,16 @@ async function computeUnread(deps, tenantId, userId, roomId, lastMessage) {
14480
14836
  });
14481
14837
  }
14482
14838
  function registerCoSpaceRoutes(app2, deps) {
14483
- const idFactory = deps.idFactory ?? (() => randomUUID16());
14839
+ const idFactory = deps.idFactory ?? (() => randomUUID18());
14484
14840
  app2.get(
14485
14841
  "/api/workspaces/:workspaceId/rooms",
14486
14842
  async (request, reply) => {
14487
14843
  return run2(reply, async () => {
14488
- const actor2 = getActor2(request);
14844
+ const actor3 = getActor2(request);
14489
14845
  const params = workspaceParamsSchema.parse(request.params);
14490
14846
  const query = workspaceRoomsQuerySchema.parse(request.query);
14491
14847
  const workspace = await deps.workspaceStore.getWorkspaceById(
14492
- actor2.tenantId,
14848
+ actor3.tenantId,
14493
14849
  params.workspaceId
14494
14850
  );
14495
14851
  if (!workspace) {
@@ -14500,16 +14856,16 @@ function registerCoSpaceRoutes(app2, deps) {
14500
14856
  );
14501
14857
  }
14502
14858
  const allProjects = await deps.projectStore.getProjectsByWorkspace(
14503
- actor2.tenantId,
14859
+ actor3.tenantId,
14504
14860
  params.workspaceId
14505
14861
  );
14506
14862
  const accessibleProjects = [];
14507
14863
  for (const project of allProjects) {
14508
14864
  if (project.kind === "public") continue;
14509
14865
  const membership = await deps.membershipStore.findByUser(
14510
- actor2.tenantId,
14866
+ actor3.tenantId,
14511
14867
  project.id,
14512
- actor2.userId
14868
+ actor3.userId
14513
14869
  );
14514
14870
  if (membership && membership.status === "active") {
14515
14871
  accessibleProjects.push(project);
@@ -14519,19 +14875,19 @@ function registerCoSpaceRoutes(app2, deps) {
14519
14875
  for (const project of accessibleProjects) {
14520
14876
  try {
14521
14877
  const room = await deps.roomStore.getMainRoom(
14522
- actor2.tenantId,
14878
+ actor3.tenantId,
14523
14879
  project.id
14524
14880
  );
14525
14881
  if (!room) continue;
14526
14882
  const messages = await deps.messageStore.list({
14527
- tenantId: actor2.tenantId,
14883
+ tenantId: actor3.tenantId,
14528
14884
  roomId: room.id,
14529
14885
  limit: 1
14530
14886
  });
14531
14887
  const lastMessage = messages[0];
14532
14888
  const publicLastMessage = lastMessage === void 0 ? void 0 : toProjectRoomPublicMessage2(lastMessage);
14533
14889
  const memberships = await deps.membershipStore.list(
14534
- actor2.tenantId,
14890
+ actor3.tenantId,
14535
14891
  project.id
14536
14892
  );
14537
14893
  const participantNames = [];
@@ -14541,7 +14897,7 @@ function registerCoSpaceRoutes(app2, deps) {
14541
14897
  const user = await deps.userStore.getUserById(m.userId);
14542
14898
  if (user) {
14543
14899
  participantNames.push(user.name);
14544
- if (m.userId !== actor2.userId) otherHumanNames.push(user.name);
14900
+ if (m.userId !== actor3.userId) otherHumanNames.push(user.name);
14545
14901
  }
14546
14902
  }
14547
14903
  const displayName = project.kind === "personal" && otherHumanNames.length === 1 ? otherHumanNames[0] : void 0;
@@ -14556,8 +14912,8 @@ function registerCoSpaceRoutes(app2, deps) {
14556
14912
  ...displayName === void 0 ? {} : { displayName },
14557
14913
  unreadCount: await computeUnread(
14558
14914
  deps,
14559
- actor2.tenantId,
14560
- actor2.userId,
14915
+ actor3.tenantId,
14916
+ actor3.userId,
14561
14917
  room.id,
14562
14918
  lastMessage
14563
14919
  )
@@ -14572,16 +14928,16 @@ function registerCoSpaceRoutes(app2, deps) {
14572
14928
  return bTime - aTime;
14573
14929
  });
14574
14930
  try {
14575
- const publicRoom = await deps.publicRoom.ensure(actor2.tenantId, params.workspaceId);
14931
+ const publicRoom = await deps.publicRoom.ensure(actor3.tenantId, params.workspaceId);
14576
14932
  if (publicRoom) {
14577
14933
  const publicMessages = await deps.messageStore.list({
14578
- tenantId: actor2.tenantId,
14934
+ tenantId: actor3.tenantId,
14579
14935
  roomId: publicRoom.room.id,
14580
14936
  limit: 1
14581
14937
  });
14582
14938
  const publicLastMessage = publicMessages[0];
14583
14939
  const publicMemberships = await deps.membershipStore.list(
14584
- actor2.tenantId,
14940
+ actor3.tenantId,
14585
14941
  publicRoom.project.id
14586
14942
  );
14587
14943
  const publicParticipantNames = [];
@@ -14601,8 +14957,8 @@ function registerCoSpaceRoutes(app2, deps) {
14601
14957
  isPublic: true,
14602
14958
  unreadCount: await computeUnread(
14603
14959
  deps,
14604
- actor2.tenantId,
14605
- actor2.userId,
14960
+ actor3.tenantId,
14961
+ actor3.userId,
14606
14962
  publicRoom.room.id,
14607
14963
  publicLastMessage
14608
14964
  )
@@ -14619,11 +14975,11 @@ function registerCoSpaceRoutes(app2, deps) {
14619
14975
  "/api/workspaces/:workspaceId/direct",
14620
14976
  async (request, reply) => {
14621
14977
  return run2(reply, async () => {
14622
- const actor2 = getActor2(request);
14978
+ const actor3 = getActor2(request);
14623
14979
  const params = workspaceParamsSchema.parse(request.params);
14624
14980
  const body = createDirectRoomSchema.parse(request.body);
14625
14981
  const workspace = await deps.workspaceStore.getWorkspaceById(
14626
- actor2.tenantId,
14982
+ actor3.tenantId,
14627
14983
  params.workspaceId
14628
14984
  );
14629
14985
  if (!workspace) {
@@ -14636,21 +14992,21 @@ function registerCoSpaceRoutes(app2, deps) {
14636
14992
  let projectId;
14637
14993
  if (body.type === "human") {
14638
14994
  projectId = generateDirectHumanProjectId(
14639
- actor2.tenantId,
14995
+ actor3.tenantId,
14640
14996
  params.workspaceId,
14641
- actor2.userId,
14997
+ actor3.userId,
14642
14998
  body.targetUserId
14643
14999
  );
14644
15000
  } else {
14645
15001
  projectId = generateDirectBotProjectId(
14646
- actor2.tenantId,
15002
+ actor3.tenantId,
14647
15003
  params.workspaceId,
14648
- actor2.userId,
15004
+ actor3.userId,
14649
15005
  body.assistantId
14650
15006
  );
14651
15007
  }
14652
15008
  let project = await deps.projectStore.getProjectById(
14653
- actor2.tenantId,
15009
+ actor3.tenantId,
14654
15010
  projectId
14655
15011
  );
14656
15012
  if (!project) {
@@ -14660,14 +15016,14 @@ function registerCoSpaceRoutes(app2, deps) {
14660
15016
  displayName = targetUser?.name ?? body.targetUserId;
14661
15017
  } else {
14662
15018
  const targetAssistant = await deps.assistantStore.getAssistantById(
14663
- actor2.tenantId,
15019
+ actor3.tenantId,
14664
15020
  body.assistantId
14665
15021
  );
14666
15022
  displayName = targetAssistant?.name ?? body.assistantId;
14667
15023
  }
14668
15024
  const name = body.type === "human" ? `Direct with ${displayName}` : `Chat with ${displayName}`;
14669
15025
  project = await deps.projectStore.createProject(
14670
- actor2.tenantId,
15026
+ actor3.tenantId,
14671
15027
  params.workspaceId,
14672
15028
  projectId,
14673
15029
  {
@@ -14678,45 +15034,45 @@ function registerCoSpaceRoutes(app2, deps) {
14678
15034
  }
14679
15035
  const room = await deps.roomStore.ensureMainRoom({
14680
15036
  id: idFactory(),
14681
- tenantId: actor2.tenantId,
15037
+ tenantId: actor3.tenantId,
14682
15038
  workspaceId: params.workspaceId,
14683
15039
  projectId: project.id,
14684
15040
  name: project.name
14685
15041
  });
14686
15042
  let membership = await deps.membershipStore.findByUser(
14687
- actor2.tenantId,
15043
+ actor3.tenantId,
14688
15044
  project.id,
14689
- actor2.userId
15045
+ actor3.userId
14690
15046
  );
14691
15047
  if (!membership) {
14692
15048
  membership = await deps.membershipStore.create({
14693
15049
  id: idFactory(),
14694
- tenantId: actor2.tenantId,
15050
+ tenantId: actor3.tenantId,
14695
15051
  projectId: project.id,
14696
- userId: actor2.userId,
15052
+ userId: actor3.userId,
14697
15053
  role: "owner",
14698
15054
  status: "active"
14699
15055
  });
14700
15056
  } else if (membership.status === "removed") {
14701
15057
  membership = await deps.membershipStore.create({
14702
15058
  id: idFactory(),
14703
- tenantId: actor2.tenantId,
15059
+ tenantId: actor3.tenantId,
14704
15060
  projectId: project.id,
14705
- userId: actor2.userId,
15061
+ userId: actor3.userId,
14706
15062
  role: membership.role,
14707
15063
  status: "active"
14708
15064
  });
14709
15065
  }
14710
15066
  if (body.type === "human" && body.targetUserId) {
14711
15067
  const targetMembership = await deps.membershipStore.findByUser(
14712
- actor2.tenantId,
15068
+ actor3.tenantId,
14713
15069
  project.id,
14714
15070
  body.targetUserId
14715
15071
  );
14716
15072
  if (!targetMembership) {
14717
15073
  await deps.membershipStore.create({
14718
15074
  id: idFactory(),
14719
- tenantId: actor2.tenantId,
15075
+ tenantId: actor3.tenantId,
14720
15076
  projectId: project.id,
14721
15077
  userId: body.targetUserId,
14722
15078
  role: "member",
@@ -14725,7 +15081,7 @@ function registerCoSpaceRoutes(app2, deps) {
14725
15081
  } else if (targetMembership.status === "removed") {
14726
15082
  await deps.membershipStore.create({
14727
15083
  id: idFactory(),
14728
- tenantId: actor2.tenantId,
15084
+ tenantId: actor3.tenantId,
14729
15085
  projectId: project.id,
14730
15086
  userId: body.targetUserId,
14731
15087
  role: targetMembership.role,
@@ -14735,19 +15091,19 @@ function registerCoSpaceRoutes(app2, deps) {
14735
15091
  }
14736
15092
  if (body.type === "bot" && body.assistantId) {
14737
15093
  const existingBotMembership = await deps.botMembershipStore.findByAssistant(
14738
- actor2.tenantId,
15094
+ actor3.tenantId,
14739
15095
  project.id,
14740
15096
  body.assistantId
14741
15097
  );
14742
15098
  if (!existingBotMembership || existingBotMembership.status === "removed") {
14743
15099
  const assistant = await deps.assistantStore.getAssistantById(
14744
- actor2.tenantId,
15100
+ actor3.tenantId,
14745
15101
  body.assistantId
14746
15102
  );
14747
15103
  const title = assistant?.name || "Assistant";
14748
15104
  await deps.botMembershipStore.save({
14749
15105
  id: idFactory(),
14750
- tenantId: actor2.tenantId,
15106
+ tenantId: actor3.tenantId,
14751
15107
  workspaceId: params.workspaceId,
14752
15108
  projectId: project.id,
14753
15109
  roomId: room.id,
@@ -14777,11 +15133,11 @@ function registerCoSpaceRoutes(app2, deps) {
14777
15133
  "/api/workspaces/:workspaceId/groups",
14778
15134
  async (request, reply) => {
14779
15135
  return run2(reply, async () => {
14780
- const actor2 = getActor2(request);
15136
+ const actor3 = getActor2(request);
14781
15137
  const params = workspaceParamsSchema.parse(request.params);
14782
15138
  const body = createGroupRoomSchema.parse(request.body);
14783
15139
  const workspace = await deps.workspaceStore.getWorkspaceById(
14784
- actor2.tenantId,
15140
+ actor3.tenantId,
14785
15141
  params.workspaceId
14786
15142
  );
14787
15143
  if (!workspace) {
@@ -14792,34 +15148,35 @@ function registerCoSpaceRoutes(app2, deps) {
14792
15148
  );
14793
15149
  }
14794
15150
  const project = await deps.projectStore.createProject(
14795
- actor2.tenantId,
15151
+ actor3.tenantId,
14796
15152
  params.workspaceId,
14797
15153
  idFactory(),
14798
15154
  { name: body.name, kind: "business" }
14799
15155
  );
14800
15156
  const room = await deps.roomStore.ensureMainRoom({
14801
15157
  id: idFactory(),
14802
- tenantId: actor2.tenantId,
15158
+ tenantId: actor3.tenantId,
14803
15159
  workspaceId: params.workspaceId,
14804
15160
  projectId: project.id,
14805
15161
  name: project.name
14806
15162
  });
14807
15163
  await deps.membershipStore.create({
14808
15164
  id: idFactory(),
14809
- tenantId: actor2.tenantId,
15165
+ tenantId: actor3.tenantId,
14810
15166
  projectId: project.id,
14811
- userId: actor2.userId,
15167
+ userId: actor3.userId,
14812
15168
  role: "owner",
14813
15169
  status: "active"
14814
15170
  });
14815
- 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;
14816
15173
  await deps.rosterService.addBot(
14817
- { tenantId: actor2.tenantId, userId: actor2.userId, projectId: project.id },
15174
+ { tenantId: actor3.tenantId, userId: actor3.userId, projectId: project.id },
14818
15175
  {
14819
15176
  assistantId: coordinator.id,
14820
15177
  role: "coordinator",
14821
15178
  title: coordinator.name,
14822
- mentionName: ROOM_COORDINATOR_KEY
15179
+ mentionName
14823
15180
  }
14824
15181
  );
14825
15182
  return { statusCode: 201, data: { project, room } };
@@ -14830,10 +15187,10 @@ function registerCoSpaceRoutes(app2, deps) {
14830
15187
  "/api/workspaces/:workspaceId/orgs/bots",
14831
15188
  async (request, reply) => {
14832
15189
  return run2(reply, async () => {
14833
- const actor2 = getActor2(request);
15190
+ const actor3 = getActor2(request);
14834
15191
  const params = workspaceParamsSchema.parse(request.params);
14835
15192
  const workspace = await deps.workspaceStore.getWorkspaceById(
14836
- actor2.tenantId,
15193
+ actor3.tenantId,
14837
15194
  params.workspaceId
14838
15195
  );
14839
15196
  if (!workspace) {
@@ -14844,18 +15201,18 @@ function registerCoSpaceRoutes(app2, deps) {
14844
15201
  );
14845
15202
  }
14846
15203
  const assistants = await deps.assistantStore.getAllAssistants(
14847
- actor2.tenantId
15204
+ actor3.tenantId
14848
15205
  );
14849
15206
  const bots = [];
14850
15207
  for (const assistant of assistants) {
14851
15208
  const directProjectId = generateDirectBotProjectId(
14852
- actor2.tenantId,
15209
+ actor3.tenantId,
14853
15210
  params.workspaceId,
14854
- actor2.userId,
15211
+ actor3.userId,
14855
15212
  assistant.id
14856
15213
  );
14857
15214
  const existingProject = await deps.projectStore.getProjectById(
14858
- actor2.tenantId,
15215
+ actor3.tenantId,
14859
15216
  directProjectId
14860
15217
  );
14861
15218
  const bot = {
@@ -14867,7 +15224,7 @@ function registerCoSpaceRoutes(app2, deps) {
14867
15224
  if (assistant.createdAt) bot.createdAt = assistant.createdAt.toISOString();
14868
15225
  if (existingProject) {
14869
15226
  const membership = await deps.botMembershipStore.findByAssistant(
14870
- actor2.tenantId,
15227
+ actor3.tenantId,
14871
15228
  directProjectId,
14872
15229
  assistant.id
14873
15230
  );
@@ -14889,10 +15246,10 @@ function registerCoSpaceRoutes(app2, deps) {
14889
15246
  "/api/workspaces/:workspaceId/orgs/members",
14890
15247
  async (request, reply) => {
14891
15248
  return run2(reply, async () => {
14892
- const actor2 = getActor2(request);
15249
+ const actor3 = getActor2(request);
14893
15250
  const params = workspaceParamsSchema.parse(request.params);
14894
15251
  const workspace = await deps.workspaceStore.getWorkspaceById(
14895
- actor2.tenantId,
15252
+ actor3.tenantId,
14896
15253
  params.workspaceId
14897
15254
  );
14898
15255
  if (!workspace) {
@@ -14903,21 +15260,21 @@ function registerCoSpaceRoutes(app2, deps) {
14903
15260
  );
14904
15261
  }
14905
15262
  const userTenantLinks = await deps.userTenantLinkStore.getUsersByTenant(
14906
- actor2.tenantId
15263
+ actor3.tenantId
14907
15264
  );
14908
15265
  const members = [];
14909
15266
  for (const link of userTenantLinks) {
14910
- if (link.userId === actor2.userId) continue;
15267
+ if (link.userId === actor3.userId) continue;
14911
15268
  const user = await deps.userStore.getUserById(link.userId);
14912
15269
  if (!user) continue;
14913
15270
  const directProjectId = generateDirectHumanProjectId(
14914
- actor2.tenantId,
15271
+ actor3.tenantId,
14915
15272
  params.workspaceId,
14916
- actor2.userId,
15273
+ actor3.userId,
14917
15274
  link.userId
14918
15275
  );
14919
15276
  const existingProject = await deps.projectStore.getProjectById(
14920
- actor2.tenantId,
15277
+ actor3.tenantId,
14921
15278
  directProjectId
14922
15279
  );
14923
15280
  const member = {
@@ -14938,11 +15295,11 @@ function registerCoSpaceRoutes(app2, deps) {
14938
15295
  "/api/workspaces/:workspaceId/rooms/:roomId/messages",
14939
15296
  async (request, reply) => {
14940
15297
  return run2(reply, async () => {
14941
- const actor2 = getActor2(request);
15298
+ const actor3 = getActor2(request);
14942
15299
  const params = workspaceRoomParamsSchema.parse(request.params);
14943
15300
  const body = sendMessageSchema.parse(request.body);
14944
15301
  const workspace = await deps.workspaceStore.getWorkspaceById(
14945
- actor2.tenantId,
15302
+ actor3.tenantId,
14946
15303
  params.workspaceId
14947
15304
  );
14948
15305
  if (!workspace) {
@@ -14953,14 +15310,14 @@ function registerCoSpaceRoutes(app2, deps) {
14953
15310
  );
14954
15311
  }
14955
15312
  const projects = await deps.projectStore.getProjectsByWorkspace(
14956
- actor2.tenantId,
15313
+ actor3.tenantId,
14957
15314
  params.workspaceId
14958
15315
  );
14959
15316
  let targetRoom = null;
14960
15317
  let targetProject = null;
14961
15318
  for (const project of projects) {
14962
15319
  const room = await deps.roomStore.getMainRoom(
14963
- actor2.tenantId,
15320
+ actor3.tenantId,
14964
15321
  project.id
14965
15322
  );
14966
15323
  if (room && room.id === params.roomId) {
@@ -14978,11 +15335,11 @@ function registerCoSpaceRoutes(app2, deps) {
14978
15335
  }
14979
15336
  const message = await deps.messageStore.create({
14980
15337
  id: idFactory(),
14981
- tenantId: actor2.tenantId,
15338
+ tenantId: actor3.tenantId,
14982
15339
  workspaceId: params.workspaceId,
14983
15340
  projectId: targetProject.id,
14984
15341
  roomId: targetRoom.id,
14985
- author: { type: "human", userId: actor2.userId },
15342
+ author: { type: "human", userId: actor3.userId },
14986
15343
  content: { type: "text", text: body.text },
14987
15344
  mentions: [],
14988
15345
  source: "user"
@@ -15031,6 +15388,59 @@ function registerCoSpaceRoutes(app2, deps) {
15031
15388
  }
15032
15389
  }
15033
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
+
15034
15444
  // src/services/collaboration/InternalRoomInstallationService.ts
15035
15445
  import { v5 as uuidv5 } from "uuid";
15036
15446
  var ROOM_INSTALLATION_NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341";
@@ -15083,22 +15493,6 @@ var InternalRoomInstallationService = class {
15083
15493
  }
15084
15494
  };
15085
15495
 
15086
- // src/services/collaboration/RoomBindingLifecycle.ts
15087
- var NoopRoomBindingLifecycle = class {
15088
- /** Performs no binding work. */
15089
- async ensure(_membership) {
15090
- }
15091
- /** Performs no binding work. */
15092
- async pause(_membership) {
15093
- }
15094
- /** Performs no binding work. */
15095
- async resume(_membership) {
15096
- }
15097
- /** Performs no binding work. */
15098
- async remove(_membership) {
15099
- }
15100
- };
15101
-
15102
15496
  // src/services/collaboration/RoomBindingProvisioner.ts
15103
15497
  var RoomBindingProvisionerError = class extends Error {
15104
15498
  /** Creates a binding provisioning failure with a stable machine-readable code. */
@@ -15285,6 +15679,7 @@ var RoomDispatchService = class {
15285
15679
  async dispatch(message) {
15286
15680
  const source = snapshotDispatchSource(message);
15287
15681
  if (!source) throw new Error("Invalid project room dispatch message");
15682
+ const anchorMessageId = source.sourceId ?? source.id;
15288
15683
  const entries = await this.deps.resolver.resolve(source);
15289
15684
  const results = [];
15290
15685
  for (const entry of entries) {
@@ -15304,7 +15699,7 @@ var RoomDispatchService = class {
15304
15699
  content: { text: source.content.text },
15305
15700
  conversation: { id: membership.roomId, type: "group" },
15306
15701
  replyTarget: { adapterChannel: "room", channelInstallationId: installation.id, rawTarget: {
15307
- sourceRoomMessageId: source.id,
15702
+ sourceRoomMessageId: anchorMessageId,
15308
15703
  targetMembershipId: membership.id,
15309
15704
  inputMessageId: "pending-router-input"
15310
15705
  } }
@@ -15318,7 +15713,7 @@ var RoomDispatchService = class {
15318
15713
  workspaceId: membership.workspaceId,
15319
15714
  projectId: membership.projectId,
15320
15715
  roomId: membership.roomId,
15321
- sourceRoomMessageId: source.id,
15716
+ sourceRoomMessageId: anchorMessageId,
15322
15717
  membershipId: membership.id,
15323
15718
  assistantId: membership.assistantId,
15324
15719
  inputMessageId: "pending-router-input",
@@ -15354,6 +15749,7 @@ function snapshotDispatchSource(value) {
15354
15749
  }
15355
15750
  const mentions = snapshotMentions(values.mentions);
15356
15751
  if (!mentions) return void 0;
15752
+ if (values.sourceId !== void 0 && (typeof values.sourceId !== "string" || values.sourceId.length === 0)) return void 0;
15357
15753
  return {
15358
15754
  id: values.id,
15359
15755
  tenantId: values.tenantId,
@@ -15364,6 +15760,7 @@ function snapshotDispatchSource(value) {
15364
15760
  content: { type: "text", text: content.text },
15365
15761
  mentions,
15366
15762
  source: "user",
15763
+ ...values.sourceId === void 0 ? {} : { sourceId: values.sourceId },
15367
15764
  createdAt
15368
15765
  };
15369
15766
  }
@@ -15393,7 +15790,7 @@ function exactValues(value, required, optional = []) {
15393
15790
  }
15394
15791
 
15395
15792
  // src/services/collaboration/CoordinatorWakeNotifier.ts
15396
- import { randomUUID as randomUUID17 } from "crypto";
15793
+ import { randomUUID as randomUUID19 } from "crypto";
15397
15794
  import { isProjectLifecycleTask as isProjectLifecycleTask2 } from "@axiom-lattice/core";
15398
15795
  import {
15399
15796
  snapshotExactRecord as snapshotExactRecord6
@@ -15410,10 +15807,10 @@ var CoordinatorWakeNotifier = class {
15410
15807
  this.accepted = /* @__PURE__ */ new Set();
15411
15808
  this.maxPending = deps.maxPending ?? 1024;
15412
15809
  if (!Number.isSafeInteger(this.maxPending) || this.maxPending < 1) throw new RangeError("maxPending must be a positive safe integer");
15413
- this.idFactory = deps.idFactory ?? randomUUID17;
15810
+ this.idFactory = deps.idFactory ?? randomUUID19;
15414
15811
  }
15415
15812
  /** Admits one notification without awaiting any store or dispatch operation. */
15416
- handle(value) {
15813
+ handle(value, anchorMessageId) {
15417
15814
  const notification = this.parse(value);
15418
15815
  if (!notification) {
15419
15816
  this.warn("Project task wake notification rejected", { code: "INVALID_COORDINATOR_WAKE_EVENT" });
@@ -15432,7 +15829,7 @@ var CoordinatorWakeNotifier = class {
15432
15829
  if (queue.keys.has(eventKey)) return queue.tail;
15433
15830
  if (this.pendingCount >= this.maxPending) return this.overflow(notification.taskId);
15434
15831
  queue.keys.add(eventKey);
15435
- queue.events.push(notification);
15832
+ queue.events.push({ notification, ...anchorMessageId === void 0 ? {} : { anchorMessageId } });
15436
15833
  this.pendingCount += 1;
15437
15834
  const previousTail = queue.tail;
15438
15835
  queue.tail = previousTail.then(() => this.processQueue(scopeKey3, queue)).catch((error) => {
@@ -15468,17 +15865,17 @@ var CoordinatorWakeNotifier = class {
15468
15865
  }
15469
15866
  async processQueue(scopeKey3, queue) {
15470
15867
  while (queue.events.length > 0) {
15471
- const notification = queue.events.shift();
15868
+ const entry = queue.events.shift();
15472
15869
  try {
15473
- if (!this.closed) await this.wake(notification);
15870
+ if (!this.closed) await this.wake(entry.notification, entry.anchorMessageId);
15474
15871
  } finally {
15475
- queue.keys.delete(notification.eventKey);
15872
+ queue.keys.delete(entry.notification.eventKey);
15476
15873
  this.pendingCount -= 1;
15477
15874
  }
15478
15875
  }
15479
15876
  if (queue.events.length === 0 && this.queues.get(scopeKey3) === queue) this.queues.delete(scopeKey3);
15480
15877
  }
15481
- async wake(notification) {
15878
+ async wake(notification, anchorMessageId) {
15482
15879
  try {
15483
15880
  const task = snapshotTaskItem(await this.deps.tasks.getById(notification.tenantId, notification.taskId));
15484
15881
  if (this.closed) return;
@@ -15500,7 +15897,8 @@ var CoordinatorWakeNotifier = class {
15500
15897
  const coordinator = await this.findActiveCoordinator(room, notification.tenantId, notification.projectId);
15501
15898
  if (this.closed) return;
15502
15899
  if (!coordinator) return;
15503
- 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);
15504
15902
  } catch (error) {
15505
15903
  this.warn("project-room:coordinator-wake-failed", { code: "COORDINATOR_WAKE_FAILED", taskId: safeId(notification.taskId), error: error instanceof Error ? error.name : "UnknownError" });
15506
15904
  }
@@ -15516,7 +15914,7 @@ var CoordinatorWakeNotifier = class {
15516
15914
  }
15517
15915
  return void 0;
15518
15916
  }
15519
- async dispatchWake(notification, task, room, owner, coordinator) {
15917
+ async dispatchWake(notification, task, room, owner, coordinator, anchorMessageId) {
15520
15918
  const message = {
15521
15919
  id: this.idFactory(),
15522
15920
  tenantId: room.tenantId,
@@ -15527,6 +15925,7 @@ var CoordinatorWakeNotifier = class {
15527
15925
  content: { type: "text", text: coordinatorWakeText(task, owner, notification.status) },
15528
15926
  mentions: [{ type: "bot", membershipId: coordinator.id }],
15529
15927
  source: "user",
15928
+ ...anchorMessageId === void 0 ? {} : { sourceId: anchorMessageId },
15530
15929
  createdAt: /* @__PURE__ */ new Date()
15531
15930
  };
15532
15931
  let results;
@@ -15587,6 +15986,10 @@ Failure: ${task.failureReason}` : "";
15587
15986
  function safeId(value) {
15588
15987
  return typeof value === "string" && value.length > 0 ? value : void 0;
15589
15988
  }
15989
+ function readSourceMessageId(context) {
15990
+ const value = context?.source_message_id;
15991
+ return typeof value === "string" && value.length > 0 ? value : void 0;
15992
+ }
15590
15993
 
15591
15994
  // src/services/collaboration/ProjectTaskExecutionMutex.ts
15592
15995
  import { snapshotExactRecord as snapshotExactRecord7 } from "@axiom-lattice/protocols";
@@ -15667,6 +16070,7 @@ var ProjectTaskExecutionMutex = class {
15667
16070
  };
15668
16071
 
15669
16072
  // src/services/collaboration/ProjectTaskExecutionService.ts
16073
+ import { randomUUID as randomUUID20 } from "crypto";
15670
16074
  import {
15671
16075
  ThreadStatus as ThreadStatus3,
15672
16076
  isProjectLifecycleTask as isProjectLifecycleTask3,
@@ -15934,7 +16338,7 @@ var ProjectTaskExecutionService = class {
15934
16338
  return current;
15935
16339
  }
15936
16340
  async dispatchRun(task, thread, mode, signal) {
15937
- 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)();
15938
16342
  if (signal?.aborted) return this.compensateCancellation(task, thread, messageId, false);
15939
16343
  const settlement = this.armSettlement(task, thread, messageId);
15940
16344
  try {
@@ -16018,7 +16422,7 @@ var ProjectTaskExecutionService = class {
16018
16422
  if (event.messageId !== messageId || !tryClaim()) return;
16019
16423
  cleanup();
16020
16424
  const error = "Project task execution interrupted without durable lifecycle transition";
16021
- void this.withLifecycleTimeout(this.settleInterruptedEvent(task, thread, messageId), {
16425
+ void this.withLifecycleTimeout(this.settleInterruptedEvent(task, thread, messageId, event.state), {
16022
16426
  kind: "failed",
16023
16427
  task,
16024
16428
  thread,
@@ -16090,11 +16494,18 @@ var ProjectTaskExecutionService = class {
16090
16494
  return void 0;
16091
16495
  }
16092
16496
  }
16093
- async settleInterruptedEvent(task, thread, messageId) {
16497
+ async settleInterruptedEvent(task, thread, messageId, state) {
16094
16498
  const current = await this.withLifecycleTimeout(this.reloadTask(task), void 0);
16095
16499
  if (current?.status === "interrupted" && this.matchesExecution(current, task, thread)) {
16096
16500
  return { kind: "interrupted", task: current, thread, messageId };
16097
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
+ }
16098
16509
  return this.failSettledTaskAwaited(
16099
16510
  task,
16100
16511
  thread,
@@ -16103,6 +16514,35 @@ var ProjectTaskExecutionService = class {
16103
16514
  "interruption_not_durable"
16104
16515
  );
16105
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
+ }
16106
16546
  async settleEndedRecovery(input, error) {
16107
16547
  const current = await this.withLifecycleTimeout(this.reloadTask(input.task), void 0);
16108
16548
  if (current && this.matchesExecution(current, input.task, input.thread)) {
@@ -16285,6 +16725,9 @@ import {
16285
16725
  isProjectLifecycleTask as isProjectLifecycleTask4,
16286
16726
  parseExternalDependency as parseExternalDependency2
16287
16727
  } from "@axiom-lattice/core";
16728
+ import {
16729
+ snapshotExactRecord as snapshotExactRecord9
16730
+ } from "@axiom-lattice/protocols";
16288
16731
  var SYSTEM_ACTOR2 = "system";
16289
16732
  var TERMINAL_ACTIONS = ["completed", "failed", "interrupted", "cancelled"];
16290
16733
  var MAX_LEARNING_ROUND_ANCESTORS = 8;
@@ -16355,6 +16798,10 @@ var ProjectTaskObserver = class {
16355
16798
  if (signal?.aborted) continue;
16356
16799
  const task = snapshotTaskItem(await this.deps.tasks.getById(hint.event.tenantId, hint.event.taskId));
16357
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;
16358
16805
  const eventTime = Date.parse(hint.event.updatedAt);
16359
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) {
16360
16807
  await this.reconcileTask(task, signal);
@@ -16365,14 +16812,21 @@ var ProjectTaskObserver = class {
16365
16812
  async notify(event) {
16366
16813
  const task = snapshotTaskItem(await this.deps.tasks.getById(event.tenantId, event.taskId));
16367
16814
  const item = await this.deps.workItems.findByEventKey(event.tenantId, event.taskId, event.eventKey);
16368
- 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;
16369
- 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)) {
16370
16819
  try {
16371
- 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;
16372
16822
  } catch (error) {
16373
16823
  await this.safeWarning(task, "projection", error);
16374
16824
  }
16375
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;
16376
16830
  if (event.action === "completed" && task.status === "completed") await this.wakeParent(task);
16377
16831
  if (TERMINAL_ACTIONS.includes(event.action) && isTerminal(task.status)) await this.reevaluateDependents(task);
16378
16832
  }
@@ -16458,6 +16912,34 @@ var ProjectTaskObserver = class {
16458
16912
  await this.resumeIfReady(task, signal);
16459
16913
  }
16460
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
+ }
16461
16943
  // SCOPE-COMPAT: learning rounds (metadata.trainingRound) and their subtrees execute in the
16462
16944
  // architect's builder thread (canvas CopilotPanel), not the Project Room runtime. Exempting
16463
16945
  // them here keeps admission hints, startup reconciliation, and dependent re-evaluation from
@@ -16980,6 +17462,14 @@ function isTerminal(status) {
16980
17462
  function errorMessage3(error) {
16981
17463
  return error instanceof Error ? error.message : String(error);
16982
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
+ }
16983
17473
  function combineSignals(left, right) {
16984
17474
  const signals = [left, right].filter((signal) => signal !== void 0);
16985
17475
  if (signals.length === 0) return void 0;
@@ -16991,10 +17481,22 @@ function combineSignals(left, right) {
16991
17481
  import { createHash as createHash6 } from "crypto";
16992
17482
  import { isProjectLifecycleTask as isProjectLifecycleTask5 } from "@axiom-lattice/core";
16993
17483
  import {
17484
+ PROJECT_ROOM_TASK_DELEGATED_LABEL,
17485
+ PROJECT_ROOM_TASK_DELEGATED_SEPARATOR,
16994
17486
  PROJECT_TASK_LIFECYCLE_ACTIONS,
16995
17487
  snapshotExactArray as snapshotExactArray2,
16996
- snapshotExactRecord as snapshotExactRecord9
17488
+ snapshotExactRecord as snapshotExactRecord10
16997
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
+ }
16998
17500
  var RoomProjectionService = class {
16999
17501
  /** @param deps Message, lifecycle-event, Task, warning, logging, and ID dependencies. */
17000
17502
  constructor(deps) {
@@ -17010,6 +17512,7 @@ var RoomProjectionService = class {
17010
17512
  const parsed = parseProjectionInput(input.task, input.workItem);
17011
17513
  const room = snapshotProjectRoom(input.room);
17012
17514
  const scope = room ? { tenantId: room.tenantId, workspaceId: room.workspaceId, projectId: room.projectId } : void 0;
17515
+ if (!parsed.ok && parsed.routine) return { kind: "skipped" };
17013
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) {
17014
17517
  return this.warning(
17015
17518
  "INVALID_PROJECTABLE_EVENT",
@@ -17040,6 +17543,42 @@ var RoomProjectionService = class {
17040
17543
  return this.warning("PROJECTION_WRITE_FAILED", { taskId: task.id, eventKey: item.eventKey }, scope);
17041
17544
  }
17042
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
+ }
17043
17582
  /**
17044
17583
  * Replays bounded raw lifecycle pages for one exact Project scope.
17045
17584
  *
@@ -17054,7 +17593,7 @@ var RoomProjectionService = class {
17054
17593
  tenantId: input.tenantId,
17055
17594
  workspaceId: input.workspaceId,
17056
17595
  projectId: input.projectId,
17057
- actions: [...PROJECT_TASK_LIFECYCLE_ACTIONS],
17596
+ actions: [...ROOM_PROJECTABLE_ACTIONS],
17058
17597
  before,
17059
17598
  limit: input.pageSize
17060
17599
  });
@@ -17169,6 +17708,7 @@ function parseProjectionInput(taskValue, itemValue) {
17169
17708
  try {
17170
17709
  const task = snapshotProjectTask(taskValue);
17171
17710
  const item = snapshotProjectableWorkItem(itemValue);
17711
+ if (item && !isRoomProjectableAction(item.action)) return { ok: false, routine: true };
17172
17712
  const projection = item ? parseProjection(item) : void 0;
17173
17713
  return task && item && projection ? { ok: true, task, item, projection } : { ok: false, ...safeIdentity ? { safeIdentity } : {} };
17174
17714
  } catch {
@@ -17176,7 +17716,7 @@ function parseProjectionInput(taskValue, itemValue) {
17176
17716
  }
17177
17717
  }
17178
17718
  function snapshotProjectableWorkItem(value) {
17179
- const row = snapshotExactRecord9(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17719
+ const row = snapshotExactRecord10(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17180
17720
  "workspaceId",
17181
17721
  "projectId",
17182
17722
  "threadId",
@@ -17213,11 +17753,11 @@ function parseIso(value) {
17213
17753
  return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
17214
17754
  }
17215
17755
  function parseInterruption(value) {
17216
- const common = snapshotExactRecord9(value, ["type", "summary", "requestedAt"]);
17756
+ const common = snapshotExactRecord10(value, ["type", "summary", "requestedAt"]);
17217
17757
  if (common && (common.type === "missing_input" || common.type === "user_decision") && typeof common.summary === "string" && common.summary.length > 0 && parseIso(common.requestedAt)) {
17218
17758
  return { type: common.type, summary: common.summary, requestedAt: common.requestedAt };
17219
17759
  }
17220
- const external = snapshotExactRecord9(value, ["type", "summary", "requestedAt", "dependencyTaskIds"]);
17760
+ const external = snapshotExactRecord10(value, ["type", "summary", "requestedAt", "dependencyTaskIds"]);
17221
17761
  const ids = snapshotExactArray2(external?.dependencyTaskIds);
17222
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) {
17223
17763
  return {
@@ -17227,7 +17767,7 @@ function parseInterruption(value) {
17227
17767
  dependencyTaskIds: [...ids]
17228
17768
  };
17229
17769
  }
17230
- const review = snapshotExactRecord9(value, ["type", "evidenceId"]);
17770
+ const review = snapshotExactRecord10(value, ["type", "evidenceId"]);
17231
17771
  return review?.type === "review_required" && typeof review.evidenceId === "string" && review.evidenceId.length > 0 ? { type: "review_required", evidenceId: review.evidenceId } : void 0;
17232
17772
  }
17233
17773
  function parseBeliefImpact(value) {
@@ -17235,50 +17775,36 @@ function parseBeliefImpact(value) {
17235
17775
  if (!rows) return void 0;
17236
17776
  const impacts = [];
17237
17777
  for (const row of rows) {
17238
- const entry = snapshotExactRecord9(row, ["key", "after", "basis"]);
17778
+ const entry = snapshotExactRecord10(row, ["key", "after", "basis"]);
17239
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;
17240
17780
  impacts.push({ key: entry.key, after: entry.after, basis: entry.basis });
17241
17781
  }
17242
17782
  return impacts;
17243
17783
  }
17244
17784
  function parseCompletion(item) {
17245
- const row = snapshotExactRecord9(item.detail, ["evidenceId", "eventKey", "result", "beliefImpact"]);
17785
+ const row = snapshotExactRecord10(item.detail, ["evidenceId", "eventKey", "result", "beliefImpact"]);
17246
17786
  const beliefImpact = parseBeliefImpact(row?.beliefImpact);
17247
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;
17248
17788
  return { evidenceId: row.evidenceId, eventKey: row.eventKey, result: row.result, beliefImpact };
17249
17789
  }
17250
17790
  function parseProjection(item) {
17251
- if (item.action === "in_progress") {
17252
- const started = snapshotExactRecord9(item.detail, ["previousStatus"]);
17253
- if (started?.previousStatus === "pending") return { kind: "started", text: "Task started" };
17254
- const resumed = snapshotExactRecord9(item.detail, ["type", "previousInterruption"]);
17255
- if (resumed?.type === "interruption_resume" && parseInterruption(resumed.previousInterruption)) {
17256
- return { kind: "resumed", text: "Task resumed" };
17257
- }
17258
- const rejected = snapshotExactRecord9(item.detail, ["type", "evidenceId", "note"]);
17259
- if (rejected?.type === "review_rejected" && typeof rejected.evidenceId === "string" && rejected.evidenceId && typeof rejected.note === "string" && rejected.note) {
17260
- return { kind: "resumed", text: "Task resumed after review rejection" };
17261
- }
17262
- const stale = snapshotExactRecord9(item.detail, ["type", "evidenceId"]);
17263
- return stale?.type === "review_stale" && typeof stale.evidenceId === "string" && stale.evidenceId ? { kind: "resumed", text: "Task resumed after stale review" } : void 0;
17264
- }
17265
17791
  if (item.action === "interrupted") {
17266
17792
  return parseInterruption(item.detail) ? { kind: "interrupted", text: "Task interrupted" } : void 0;
17267
17793
  }
17268
17794
  if (item.action === "failed") {
17269
- const row = snapshotExactRecord9(item.detail, ["failureReason"]);
17795
+ const row = snapshotExactRecord10(item.detail, ["failureReason"]);
17270
17796
  return typeof row?.failureReason === "string" && row.failureReason ? { kind: "failed", text: "Task failed" } : void 0;
17271
17797
  }
17272
17798
  if (item.action === "completed") {
17273
17799
  return parseCompletion(item) ? { kind: "completed", text: "Task completed" } : void 0;
17274
17800
  }
17275
17801
  if (item.action === "cancelled") {
17276
- const empty = snapshotExactRecord9(item.detail, []);
17277
- const summarized = snapshotExactRecord9(item.detail, ["summary"]);
17802
+ const empty = snapshotExactRecord10(item.detail, []);
17803
+ const summarized = snapshotExactRecord10(item.detail, ["summary"]);
17278
17804
  return empty || typeof summarized?.summary === "string" && summarized.summary ? { kind: "cancelled", text: "Task cancelled" } : void 0;
17279
17805
  }
17280
17806
  if (item.action === "reassigned") {
17281
- const row = snapshotExactRecord9(
17807
+ const row = snapshotExactRecord10(
17282
17808
  item.detail,
17283
17809
  ["previousOwnerId", "previousThreadId", "ownerId", "membershipId", "threadId"]
17284
17810
  );
@@ -17287,7 +17813,7 @@ function parseProjection(item) {
17287
17813
  return void 0;
17288
17814
  }
17289
17815
  function assertCanonicalProjection(value, expected) {
17290
- const row = snapshotExactRecord9(value, [
17816
+ const row = snapshotExactRecord10(value, [
17291
17817
  "id",
17292
17818
  "tenantId",
17293
17819
  "workspaceId",
@@ -17301,8 +17827,8 @@ function assertCanonicalProjection(value, expected) {
17301
17827
  "idempotencyKey",
17302
17828
  "createdAt"
17303
17829
  ]);
17304
- const author = snapshotExactRecord9(row?.author, ["type"]);
17305
- const content = snapshotExactRecord9(row?.content, ["type", "text"]);
17830
+ const author = snapshotExactRecord10(row?.author, ["type"]);
17831
+ const content = snapshotExactRecord10(row?.content, ["type", "text"]);
17306
17832
  const mentions = snapshotExactArray2(row?.mentions);
17307
17833
  const createdAt = cloneExactDate(row?.createdAt);
17308
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) {
@@ -17324,7 +17850,7 @@ function assertCanonicalProjection(value, expected) {
17324
17850
  };
17325
17851
  }
17326
17852
  function snapshotLifecycleCursor(value) {
17327
- const row = snapshotExactRecord9(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17853
+ const row = snapshotExactRecord10(value, ["id", "taskId", "tenantId", "action", "actor", "createdAt"], [
17328
17854
  "workspaceId",
17329
17855
  "projectId",
17330
17856
  "threadId",
@@ -17344,7 +17870,7 @@ function isRecord3(value) {
17344
17870
  import { taskLifecycleNotifier } from "@axiom-lattice/core";
17345
17871
 
17346
17872
  // src/services/collaboration/ProjectRoomEventBroker.ts
17347
- import { randomUUID as randomUUID18 } from "crypto";
17873
+ import { randomUUID as randomUUID21 } from "crypto";
17348
17874
  import {
17349
17875
  parseProjectRoomEventId,
17350
17876
  ProjectRoomBrokerCapacityError,
@@ -17373,7 +17899,7 @@ var ProjectRoomEventBroker = class {
17373
17899
  this.subscriberCount = 0;
17374
17900
  this.usageClock = 0;
17375
17901
  this.closed = false;
17376
- this.epoch = options.epoch ?? randomUUID18();
17902
+ this.epoch = options.epoch ?? randomUUID21();
17377
17903
  if (!UUID_PATTERN.test(this.epoch)) {
17378
17904
  throw new TypeError("Project Room event epoch must be a lowercase canonical UUID");
17379
17905
  }
@@ -17694,7 +18220,7 @@ function snapshotJson(value, ancestors = /* @__PURE__ */ new Set()) {
17694
18220
  import { createHash as createHash7 } from "crypto";
17695
18221
  import {
17696
18222
  projectRoomUserEventScope,
17697
- snapshotExactRecord as snapshotExactRecord10,
18223
+ snapshotExactRecord as snapshotExactRecord11,
17698
18224
  snapshotProjectRoomBotMembershipRealtime,
17699
18225
  snapshotProjectRoomMembershipRealtime,
17700
18226
  snapshotProjectRoomMessageRealtime
@@ -17771,7 +18297,7 @@ var BrokerProjectRoomRealtimePublisher = class {
17771
18297
  /** Publishes a membership notice onto the affected user's channel scope. */
17772
18298
  membershipAffected(input) {
17773
18299
  this.publish("membership.affected", () => {
17774
- const value = snapshotExactRecord10(input, ["tenantId", "userId", "change", "projectId", "roomId", "sourceKey"]);
18300
+ const value = snapshotExactRecord11(input, ["tenantId", "userId", "change", "projectId", "roomId", "sourceKey"]);
17775
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)) {
17776
18302
  throw new TypeError("Invalid membership input");
17777
18303
  }
@@ -17791,7 +18317,7 @@ var BrokerProjectRoomRealtimePublisher = class {
17791
18317
  /** Publishes a read-marker update onto the acting user's channel scope. */
17792
18318
  readChanged(input) {
17793
18319
  this.publish("read.changed", () => {
17794
- const value = snapshotExactRecord10(input, ["tenantId", "userId", "projectId", "roomId", "lastReadAt", "sourceKey"]);
18320
+ const value = snapshotExactRecord11(input, ["tenantId", "userId", "projectId", "roomId", "lastReadAt", "sourceKey"]);
17795
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))) {
17796
18322
  throw new TypeError("Invalid read input");
17797
18323
  }
@@ -17836,13 +18362,13 @@ var BrokerProjectRoomRealtimePublisher = class {
17836
18362
  };
17837
18363
  function snapshotInput(input, kind) {
17838
18364
  const keys2 = kind === "message" ? ["tenantId", "roomId", "message", "sourceKey"] : kind === "roster" ? ["tenantId", "roomId", "membership", "sourceKey"] : ["tenantId", "roomId", "membership", "sourceKey"];
17839
- const value = snapshotExactRecord10(input, [...keys2, "change"]);
18365
+ const value = snapshotExactRecord11(input, [...keys2, "change"]);
17840
18366
  const validChange = kind === "roster" ? ["added", "updated", "paused", "resumed", "removed"].includes(value?.change) : ["added", "role_changed", "removed"].includes(value?.change);
17841
18367
  if (!value || !safeId2(value.tenantId) || !safeId2(value.roomId) || !safeSourceKey(value.sourceKey) || !validChange) throw new TypeError(`Invalid ${kind} input`);
17842
18368
  return value;
17843
18369
  }
17844
18370
  function snapshotTaskInput(input) {
17845
- 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"]);
17846
18372
  if (!value || !safeId2(value.tenantId) || !safeId2(value.roomId) || !safeSourceKey(value.sourceKey)) throw new TypeError("Invalid task input");
17847
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");
17848
18374
  if (typeof value.updatedAt !== "string") throw new TypeError("Invalid task timestamp");
@@ -17868,7 +18394,7 @@ function safeErrorCode(error) {
17868
18394
 
17869
18395
  // src/services/collaboration/ProjectRoomTaskEventSubscriber.ts
17870
18396
  import { isProjectLifecycleTask as isProjectLifecycleTask6 } from "@axiom-lattice/core";
17871
- import { snapshotExactRecord as snapshotExactRecord11 } from "@axiom-lattice/protocols";
18397
+ import { snapshotExactRecord as snapshotExactRecord12 } from "@axiom-lattice/protocols";
17872
18398
  var ProjectRoomTaskEventSubscriber = class {
17873
18399
  /** Creates a bounded Project task lifecycle observer. */
17874
18400
  constructor(deps) {
@@ -17978,7 +18504,7 @@ var ProjectRoomTaskEventSubscriber = class {
17978
18504
  }
17979
18505
  }
17980
18506
  parse(value) {
17981
- 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"]);
17982
18508
  const actions = ["in_progress", "interrupted", "failed", "completed", "cancelled", "reassigned"];
17983
18509
  const statuses = ["pending", "in_progress", "review", "failed", "interrupted", "completed", "cancelled"];
17984
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;
@@ -18041,7 +18567,8 @@ var ProjectRoomRealtimeRuntime = class {
18041
18567
  };
18042
18568
  function createProjectRoomRealtimeRuntime(deps) {
18043
18569
  const broker = new ProjectRoomEventBroker({ log: deps.log });
18044
- 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;
18045
18572
  const subscriber = new ProjectRoomTaskEventSubscriber({
18046
18573
  tasks: deps.tasks,
18047
18574
  rooms: deps.rooms,
@@ -18058,6 +18585,20 @@ function createProjectRoomRealtimeRuntime(deps) {
18058
18585
  shutdownTimeoutMs: deps.shutdownTimeoutMs
18059
18586
  });
18060
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
+ }
18061
18602
  async function boundedAllSettled(promises, timeoutMs) {
18062
18603
  if (promises.length === 0) return true;
18063
18604
  let timer;
@@ -18070,6 +18611,94 @@ async function boundedAllSettled(promises, timeoutMs) {
18070
18611
  return result;
18071
18612
  }
18072
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
+
18073
18702
  // src/services/collaboration/WorkspaceRoomEventsService.ts
18074
18703
  import { PROJECT_ROOM_USER_CHANNEL_PROJECT, projectRoomUserEventScope as projectRoomUserEventScope2 } from "@axiom-lattice/protocols";
18075
18704
 
@@ -18306,7 +18935,7 @@ var WorkspaceRoomEventsService = class {
18306
18935
  for (const active of [...this.connections.values()]) for (const connection of [...active]) connection.close();
18307
18936
  this.connections.clear();
18308
18937
  }
18309
- buildConnection(actor2, workspaceId, raw) {
18938
+ buildConnection(actor3, workspaceId, raw) {
18310
18939
  let phase = "initializing";
18311
18940
  const subscriptions = /* @__PURE__ */ new Map();
18312
18941
  let heartbeatTimer;
@@ -18319,7 +18948,7 @@ var WorkspaceRoomEventsService = class {
18319
18948
  const clear = (timer) => {
18320
18949
  if (timer !== void 0) clearTimeout(timer);
18321
18950
  };
18322
- const userScope = projectRoomUserEventScope2(actor2.tenantId, actor2.userId);
18951
+ const userScope = projectRoomUserEventScope2(actor3.tenantId, actor3.userId);
18323
18952
  const userScopeKey = JSON.stringify([userScope.tenantId, userScope.roomId, userScope.projectId]);
18324
18953
  const roomScopeKey = (scope) => JSON.stringify([scope.tenantId, scope.roomId, scope.projectId]);
18325
18954
  const serializeScoped = (scope, event) => scope === null ? `id: ${event.id}
@@ -18343,7 +18972,7 @@ data: ${JSON.stringify({ ...event, scope })}
18343
18972
  for (const entry of subscriptions.values()) entry.unsubscribe();
18344
18973
  subscriptions.clear();
18345
18974
  writer.close();
18346
- const userKey = JSON.stringify([actor2.tenantId, actor2.userId]);
18975
+ const userKey = JSON.stringify([actor3.tenantId, actor3.userId]);
18347
18976
  const active = this.connections.get(userKey);
18348
18977
  if (active && recordRef.current !== void 0) {
18349
18978
  active.delete(recordRef.current);
@@ -18362,11 +18991,11 @@ data: ${JSON.stringify({ ...event, scope })}
18362
18991
  recordRef.current = record;
18363
18992
  const rebuildSubscriptions = async () => {
18364
18993
  try {
18365
- 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 });
18366
18995
  if (closed) return true;
18367
18996
  const wanted = /* @__PURE__ */ new Map();
18368
18997
  for (const room of rooms) {
18369
- const scope = { tenantId: actor2.tenantId, roomId: room.roomId, projectId: room.projectId };
18998
+ const scope = { tenantId: actor3.tenantId, roomId: room.roomId, projectId: room.projectId };
18370
18999
  wanted.set(roomScopeKey(scope), scope);
18371
19000
  }
18372
19001
  for (const [key, entry] of [...subscriptions]) {
@@ -18403,7 +19032,7 @@ data: ${JSON.stringify({ ...event, scope })}
18403
19032
  };
18404
19033
  const scheduleExpiry = () => {
18405
19034
  if (closed) return;
18406
- const delay2 = actor2.tokenExpiresAt - this.now();
19035
+ const delay2 = actor3.tokenExpiresAt - this.now();
18407
19036
  if (delay2 <= 0) {
18408
19037
  revoke("TOKEN_EXPIRED");
18409
19038
  return;
@@ -18430,7 +19059,7 @@ data: ${JSON.stringify({ ...event, scope })}
18430
19059
  accessTimer = void 0;
18431
19060
  if (closed) return;
18432
19061
  const projectIds = [...subscriptions.values()].map((entry) => entry.scope.projectId).filter((projectId) => projectId !== PROJECT_ROOM_USER_CHANNEL_PROJECT);
18433
- 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(() => {
18434
19063
  if (!closed) scheduleAccessCheck();
18435
19064
  }, () => {
18436
19065
  if (!closed) revoke("PROJECT_ACCESS_REVOKED");
@@ -18472,7 +19101,7 @@ function createWorkspaceMemberRoomLister(deps) {
18472
19101
  }
18473
19102
 
18474
19103
  // src/routes/workspace-room-events.ts
18475
- import { snapshotExactRecord as snapshotExactRecord12 } from "@axiom-lattice/protocols";
19104
+ import { snapshotExactRecord as snapshotExactRecord13 } from "@axiom-lattice/protocols";
18476
19105
  function registerWorkspaceRoomEventRoutes(app2, deps) {
18477
19106
  app2.get("/api/workspaces/:workspaceId/room-events", async (request, reply) => {
18478
19107
  try {
@@ -18480,7 +19109,7 @@ function registerWorkspaceRoomEventRoutes(app2, deps) {
18480
19109
  const headers = strictHeaders(request);
18481
19110
  if (request.body !== void 0 || Object.keys(request.query).length !== 0) return invalid(reply);
18482
19111
  const session = verifyConsoleSessionAuthHeader(headers.authorization, deps.tokenSecret);
18483
- if (!session?.tenantId) return unauthorized(reply);
19112
+ if (!session?.tenantId) return unauthorized2(reply);
18484
19113
  reply.hijack();
18485
19114
  reply.raw.writeHead(200, {
18486
19115
  "Content-Type": "text/event-stream",
@@ -18500,7 +19129,7 @@ function registerWorkspaceRoomEventRoutes(app2, deps) {
18500
19129
  });
18501
19130
  }
18502
19131
  function exactParams(params) {
18503
- const value = snapshotExactRecord12(params, ["workspaceId"]);
19132
+ const value = snapshotExactRecord13(params, ["workspaceId"]);
18504
19133
  if (!value || typeof value.workspaceId !== "string" || !value.workspaceId.trim()) throw new InvalidRealtimeRequestError();
18505
19134
  return { workspaceId: value.workspaceId };
18506
19135
  }
@@ -18523,7 +19152,7 @@ var InvalidRealtimeRequestError = class extends Error {
18523
19152
  function invalid(reply) {
18524
19153
  return reply.code(400).send({ success: false, error: { code: "INVALID_REQUEST", message: "Invalid request", retryable: false } });
18525
19154
  }
18526
- function unauthorized(reply) {
19155
+ function unauthorized2(reply) {
18527
19156
  return reply.code(401).send({ success: false, error: { code: "UNAUTHORIZED", message: "Unauthorized", retryable: false } });
18528
19157
  }
18529
19158
  function routeError(reply, error) {
@@ -18643,7 +19272,7 @@ function isPublicRoomAddress(message) {
18643
19272
  }
18644
19273
  async function validateReassignmentThread(threads, input) {
18645
19274
  const thread = snapshotThread(await threads.getThreadById(input.tenantId, input.newThreadId));
18646
- const metadata = snapshotExactRecord13(thread?.metadata, [
19275
+ const metadata = snapshotExactRecord14(thread?.metadata, [
18647
19276
  "source",
18648
19277
  "kind",
18649
19278
  "workspaceId",
@@ -18667,6 +19296,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18667
19296
  const projectBotMembershipStore = getStoreLattice16("default", "projectBotMembership").store;
18668
19297
  const projectRoomMessageStore = getStoreLattice16("default", "projectRoomMessage").store;
18669
19298
  const projectRoomReadStateStore = getStoreLattice16("default", "projectRoomReadState").store;
19299
+ const userPushSubscriptionStore = getStoreLattice16("default", "userPushSubscription").store;
18670
19300
  const threadStore = getStoreLattice16("default", "thread").store;
18671
19301
  const taskStore = getStoreLattice16("default", "task").store;
18672
19302
  const taskWorkItemStore = getStoreLattice16("default", "taskWorkItem").store;
@@ -18674,10 +19304,22 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18674
19304
  const accessService = new ProjectAccessService({ projectStore, membershipStore: projectMembershipStore, userTenantLinkStore });
18675
19305
  const realtimeMode = resolveProjectRoomRealtimeMode();
18676
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
+ });
18677
19318
  const projectRoomRealtimeRuntime = realtimeMode === "single-gateway" ? options.projectRoomRealtimeRuntime ?? createProjectRoomRealtimeRuntime({
18678
19319
  tasks: taskStore,
18679
19320
  rooms: projectRoomStore,
18680
19321
  memberships: projectBotMembershipStore,
19322
+ pushNotifier: roomPushNotifier,
18681
19323
  log: realtimeLog
18682
19324
  }) : void 0;
18683
19325
  if (projectRoomRealtimeRuntime) {
@@ -18706,10 +19348,53 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18706
19348
  });
18707
19349
  }
18708
19350
  const realtime = projectRoomRealtimeRuntime?.publisher;
18709
- const roomService = new ProjectRoomService({ accessService, roomStore: projectRoomStore });
18710
- 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 });
18711
19351
  const installation = channelDeps ? new InternalRoomInstallationService(channelDeps.installationStore) : void 0;
18712
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 });
18713
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 });
18714
19399
  const roomAdapterAvailable = channelDeps?.adapterRegistry.get("room")?.channel === "room";
18715
19400
  const dispatcher = channelDeps && installation && bindingLifecycle instanceof RoomBindingProvisioner && roomAdapterAvailable ? new RoomDispatchService({
@@ -18726,11 +19411,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18726
19411
  log: realtimeLog
18727
19412
  }) : void 0;
18728
19413
  if (coordinatorWakeNotifier) {
18729
- const unsubscribeCoordinatorWake = taskLifecycleNotifier2.subscribe(((event) => {
18730
- void coordinatorWakeNotifier.handle(event);
18731
- }));
18732
19414
  app2.addHook("onClose", async () => {
18733
- unsubscribeCoordinatorWake();
18734
19415
  await coordinatorWakeNotifier.dispose();
18735
19416
  });
18736
19417
  }
@@ -18755,7 +19436,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18755
19436
  projectRoomMessageStore,
18756
19437
  threadStore,
18757
19438
  realtime,
18758
- log: options.projectTaskLog
19439
+ log: options.projectTaskLog,
19440
+ ...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier }
18759
19441
  }) : { reason: void 0 };
18760
19442
  const projectTaskComposition = "composition" in projectTaskRuntime ? projectTaskRuntime.composition : void 0;
18761
19443
  const projectTaskUnavailableReason = "reason" in projectTaskRuntime ? projectTaskRuntime.reason : void 0;
@@ -18765,23 +19447,17 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18765
19447
  });
18766
19448
  }
18767
19449
  if (projectTaskComposition) app2.addHook("onClose", async () => projectTaskComposition.lease.release());
18768
- 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 });
18769
19451
  const taskLifecycle = projectTaskComposition?.lease.runtime.lifecycle ?? createTaskLifecycleService({ taskStore, workItemStore: taskWorkItemStore });
18770
19452
  const reassignmentAuthorizer = createProjectTaskReassignmentAuthorizer(
18771
19453
  new ProjectTaskOwnerPolicy(projectBotMembershipStore),
18772
19454
  (input) => validateReassignmentThread(threadStore, input)
18773
19455
  );
18774
- const coordinatorProvisioner = new RoomCoordinatorProvisioner({
18775
- getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
18776
- createAssistant: assistantStore.createAssistant.bind(assistantStore)
18777
- });
18778
19456
  const publicRoomService = new WorkspacePublicRoomService({
18779
19457
  getProjectById: projectStore.getProjectById.bind(projectStore),
18780
19458
  createProject: projectStore.createProject.bind(projectStore),
18781
19459
  ensureMainRoom: projectRoomStore.ensureMainRoom.bind(projectRoomStore),
18782
- findBotByAssistant: projectBotMembershipStore.findByAssistant.bind(projectBotMembershipStore),
18783
- saveBot: projectBotMembershipStore.save.bind(projectBotMembershipStore),
18784
- ensureCoordinator: (tenantId) => coordinatorProvisioner.ensure(tenantId)
19460
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope)
18785
19461
  });
18786
19462
  const readStateService = {
18787
19463
  mark: async (actorRef) => {
@@ -18805,6 +19481,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18805
19481
  membershipService,
18806
19482
  messageService,
18807
19483
  readStateService,
19484
+ interruptService: roomInterruptService,
18808
19485
  userDirectory: {
18809
19486
  getUserById: (id3) => getStoreLattice16("default", "user").store.getUserById(id3).then((u) => u ? { name: u.name } : null)
18810
19487
  }
@@ -18851,9 +19528,10 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18851
19528
  save: projectBotMembershipStore.save.bind(projectBotMembershipStore)
18852
19529
  },
18853
19530
  coordinatorProvisioner,
19531
+ getCoordinatorMentionName: resolveCoordinatorMentionName,
18854
19532
  rosterService: {
18855
- addBot: async (actor2, input) => {
18856
- await rosterService.add(actor2, input);
19533
+ addBot: async (actor3, input) => {
19534
+ await rosterService.add(actor3, input);
18857
19535
  }
18858
19536
  },
18859
19537
  publicRoom: {
@@ -18880,7 +19558,9 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
18880
19558
  workspaceController: new WorkspaceController(createProjectCollaborationInitializer({
18881
19559
  membershipStore: projectMembershipStore,
18882
19560
  roomStore: projectRoomStore,
18883
- userTenantLinkStore
19561
+ userTenantLinkStore,
19562
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope),
19563
+ log: realtimeLog
18884
19564
  })),
18885
19565
  taskController: createTaskController({
18886
19566
  access: {
@@ -19142,7 +19822,9 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
19142
19822
  registerWorkspaceRoutes(app2, createProjectCollaborationInitializer({
19143
19823
  membershipStore: projectMembershipStore,
19144
19824
  roomStore: projectRoomStore,
19145
- userTenantLinkStore
19825
+ userTenantLinkStore,
19826
+ ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope),
19827
+ log: realtimeLog
19146
19828
  }));
19147
19829
  registerDatabaseConfigRoutes(app2);
19148
19830
  registerMetricsServerConfigRoutes(app2);
@@ -19154,6 +19836,10 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
19154
19836
  executeDataQuery
19155
19837
  );
19156
19838
  registerMcpServerConfigRoutes(app2);
19839
+ registerPushNotificationRoutes(app2, {
19840
+ subscriptionStore: userPushSubscriptionStore,
19841
+ vapidPublicKey: process.env.VAPID_PUBLIC_KEY || null
19842
+ });
19157
19843
  registerUserRoutes(app2);
19158
19844
  registerTenantRoutes(app2);
19159
19845
  registerAuthRoutes(app2, {
@@ -19339,7 +20025,7 @@ function tryCreateProjectTaskRuntime(input) {
19339
20025
  function createRuntime(input, stores, log2) {
19340
20026
  const controller = new AbortController();
19341
20027
  const mutex = new ProjectTaskExecutionMutex();
19342
- const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID19 });
20028
+ const threads = new ProjectTaskThreadService({ threads: input.threadStore, idFactory: randomUUID22 });
19343
20029
  const lifecycle = createTaskLifecycleService({ taskStore: input.taskStore, workItemStore: input.taskWorkItemStore });
19344
20030
  const projection = new RoomProjectionService({
19345
20031
  messages: input.projectRoomMessageStore,
@@ -19348,7 +20034,7 @@ function createRuntime(input, stores, log2) {
19348
20034
  warnings: input.taskWorkItemStore,
19349
20035
  log: log2,
19350
20036
  realtime: input.realtime,
19351
- idFactory: randomUUID19
20037
+ idFactory: randomUUID22
19352
20038
  });
19353
20039
  const agentFor = (task, thread) => agentInstanceManager6.getAgent({
19354
20040
  tenant_id: task.tenantId,
@@ -19385,7 +20071,8 @@ function createRuntime(input, stores, log2) {
19385
20071
  mutex,
19386
20072
  projection,
19387
20073
  log: log2,
19388
- signal: controller.signal
20074
+ signal: controller.signal,
20075
+ ...input.coordinatorWake === void 0 ? {} : { coordinatorWake: input.coordinatorWake }
19389
20076
  }, PROJECT_TASK_OBSERVER_OPTIONS);
19390
20077
  const taskLifecycleListener = ((event) => {
19391
20078
  void observer.listener(event);
@@ -20173,7 +20860,7 @@ var collectionRegistration = {
20173
20860
 
20174
20861
  // src/export_registrations/channel-installation.registration.ts
20175
20862
  import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
20176
- import { randomUUID as randomUUID20 } from "crypto";
20863
+ import { randomUUID as randomUUID23 } from "crypto";
20177
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;
20178
20865
  var PUBLIC_CHANNELS = /* @__PURE__ */ new Set(["lark", "email", "slack", "wechat"]);
20179
20866
  function isPublicChannel(channel) {
@@ -20332,7 +21019,7 @@ var channelInstallationRegistration = {
20332
21019
  }
20333
21020
  id3 = sourceId;
20334
21021
  } else {
20335
- id3 = randomUUID20();
21022
+ id3 = randomUUID23();
20336
21023
  }
20337
21024
  await store.createInstallation(tenantId, id3, createInput);
20338
21025
  return { newId: id3 };
@@ -21218,9 +21905,9 @@ import {
21218
21905
  } from "@axiom-lattice/core";
21219
21906
  import {
21220
21907
  snapshotExactArray as snapshotExactArray3,
21221
- snapshotExactRecord as snapshotExactRecord14
21908
+ snapshotExactRecord as snapshotExactRecord15
21222
21909
  } from "@axiom-lattice/protocols";
21223
- import { randomUUID as randomUUID21 } from "crypto";
21910
+ import { randomUUID as randomUUID24 } from "crypto";
21224
21911
  var BindingNotFoundError = class extends Error {
21225
21912
  constructor(message) {
21226
21913
  super(message);
@@ -21248,8 +21935,13 @@ var CORRELATED_REPLY_REQUIRED_KEYS = [
21248
21935
  "outputText",
21249
21936
  "state"
21250
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
+ }
21251
21943
  function exactDataValues(value, required, optional = []) {
21252
- return snapshotExactRecord14(value, required, optional);
21944
+ return snapshotExactRecord15(value, required, optional);
21253
21945
  }
21254
21946
  function uniqueNonemptyStrings(value) {
21255
21947
  const snapshot = snapshotExactArray3(value);
@@ -21328,6 +22020,8 @@ function correlatedRoomReplyTarget(replyTarget, inputMessageId) {
21328
22020
  var MessageRouter = class {
21329
22021
  constructor(config) {
21330
22022
  this.replySubscriptions = /* @__PURE__ */ new Set();
22023
+ /** Recent answer-by-message resumes per presence thread (first answer wins). */
22024
+ this.recentResumes = /* @__PURE__ */ new Map();
21331
22025
  this.middlewares = [...config.middlewares];
21332
22026
  this.bindingRegistry = config.bindingRegistry;
21333
22027
  this.adapterRegistry = config.adapterRegistry;
@@ -21535,7 +22229,7 @@ var MessageRouter = class {
21535
22229
  }
21536
22230
  if (!threadId) {
21537
22231
  const threadStore = getStoreLattice26("default", "thread").store;
21538
- const newThreadId = randomUUID21();
22232
+ const newThreadId = randomUUID24();
21539
22233
  console.log({
21540
22234
  event: "dispatch:thread:create",
21541
22235
  agentId,
@@ -21581,19 +22275,51 @@ var MessageRouter = class {
21581
22275
  workspace_id: binding?.workspaceId || "",
21582
22276
  project_id: binding?.projectId || ""
21583
22277
  });
21584
- const inputMessageId = randomUUID21();
22278
+ const inputMessageId = randomUUID24();
21585
22279
  const replyTarget = internalRoom ? correlatedRoomReplyTarget(message.replyTarget, inputMessageId) : message.replyTarget;
21586
22280
  const replyAdapter = replyTarget ? this.adapterRegistry.get(replyTarget.adapterChannel) : void 0;
21587
22281
  if (options.trustedMessageOptions && (!this.trustedDispatcher || !replyTarget || !replyAdapter)) {
21588
22282
  throw new Error("Trusted dispatch requires reply target/adapter");
21589
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
+ }
21590
22314
  const queueMessage = {
21591
22315
  input: { id: inputMessageId, message: message.content.text },
21592
- custom_run_config: message.content.metadata
22316
+ custom_run_config: message.content.metadata,
22317
+ ...resumeCommand ? { command: resumeCommand } : {}
21593
22318
  };
21594
22319
  let cleanupReplySubscription;
21595
22320
  if (replyTarget && replyAdapter) {
21596
22321
  let cleaned = false;
22322
+ let onInterrupted;
21597
22323
  const callback = (value) => {
21598
22324
  const event = parseCorrelatedReplyReadyEvent(value);
21599
22325
  if (!event) return;
@@ -21619,12 +22345,26 @@ var MessageRouter = class {
21619
22345
  cleaned = true;
21620
22346
  clearTimeout(timer);
21621
22347
  agent.unsubscribe("reply:ready", callback);
22348
+ if (onInterrupted !== void 0) agent.unsubscribe("message:interrupted", onInterrupted);
21622
22349
  this.replySubscriptions.delete(subscription);
21623
22350
  };
21624
22351
  const subscription = { inputMessageId, timer, cleanup };
21625
22352
  cleanupReplySubscription = cleanup;
21626
22353
  this.replySubscriptions.add(subscription);
21627
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);
21628
22368
  }
21629
22369
  let addResult;
21630
22370
  try {
@@ -21686,12 +22426,34 @@ var MessageRouter = class {
21686
22426
  };
21687
22427
  return dispatch(0);
21688
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
+ }
21689
22451
  };
21690
22452
 
21691
22453
  // src/channels/room/RoomChannelAdapter.ts
21692
- import { z as z10 } from "zod";
21693
- import { snapshotExactRecord as snapshotExactRecord15 } from "@axiom-lattice/protocols";
21694
- 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();
21695
22457
  function createRoomChannelAdapter(writer) {
21696
22458
  return {
21697
22459
  channel: "room",
@@ -21719,6 +22481,23 @@ function createRoomChannelAdapter(writer) {
21719
22481
  sourceId: metadata.outputMessageId,
21720
22482
  idempotencyKey: `room-reply:${target.sourceRoomMessageId}:${target.targetMembershipId}:${metadata.outputMessageId}`
21721
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
+ });
21722
22501
  }
21723
22502
  };
21724
22503
  }
@@ -21734,7 +22513,7 @@ function exactStrings(value, keys2) {
21734
22513
  function exactValues2(value, required, optional = []) {
21735
22514
  try {
21736
22515
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
21737
- return snapshotExactRecord15(value, required, optional);
22516
+ return snapshotExactRecord16(value, required, optional);
21738
22517
  } catch {
21739
22518
  return void 0;
21740
22519
  }
@@ -22023,7 +22802,7 @@ var start = async (config) => {
22023
22802
  });
22024
22803
  try {
22025
22804
  const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-7UGQ7GMW.mjs");
22026
- const { registerA2AStandardRoutes } = await import("./a2a-standard-VEQBXBMM.mjs");
22805
+ const { registerA2AStandardRoutes } = await import("./a2a-standard-ALXWP7V6.mjs");
22027
22806
  const a2a = await import("./a2a-FGYX2HH3.mjs");
22028
22807
  const a2aKeyStore = getStoreLattice27("default", "a2aApiKey").store;
22029
22808
  const assistantStore = getStoreLattice27("default", "assistant").store;
@@ -22096,7 +22875,7 @@ var start = async (config) => {
22096
22875
  logger5.info("Registered sandbox manager from env configuration");
22097
22876
  }
22098
22877
  try {
22099
- const { ResourceController } = await import("./resources-SRQRKW5G.mjs");
22878
+ const { ResourceController } = await import("./resources-JFKAPS45.mjs");
22100
22879
  const sharedResourceStore = getStoreLattice27("default", "sharedResource").store;
22101
22880
  const cache = new TokenCache();
22102
22881
  const resourceController = new ResourceController({