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