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