@axiom-lattice/gateway 4.8.4 → 4.9.1
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-BHBLACJV.mjs +17 -0
- package/dist/a2a-BHBLACJV.mjs.map +1 -0
- package/dist/{chunk-NPHK4SZ6.mjs → chunk-FGSFOXIX.mjs} +2 -2
- package/dist/{a2a-FGYX2HH3.mjs → chunk-HYYLYIMH.mjs} +59 -15
- package/dist/chunk-HYYLYIMH.mjs.map +1 -0
- package/dist/{chunk-73QZQQOT.mjs → chunk-MWWLJMYN.mjs} +3 -2
- package/dist/chunk-MWWLJMYN.mjs.map +1 -0
- package/dist/embed/{embed-page-AgentDetailSideApp-S5N76UPU.js → embed-page-AgentDetailSideApp-UMITWVYQ.js} +1 -1
- package/dist/embed/{embed-page-AgentGovernanceWorkbench-QFMP2M73.js → embed-page-AgentGovernanceWorkbench-UD7RE7PT.js} +1 -1
- package/dist/embed/{embed-page-MessageGraph-H3HPZKOH.js → embed-page-MessageGraph-XQWKYDUB.js} +1 -1
- package/dist/embed/{embed-page-ReactChart-ZFMANJCT.js → embed-page-ReactChart-LW3GWMY2.js} +1 -1
- package/dist/embed/{embed-page-SkillFlow-HY7PD43P.js → embed-page-SkillFlow-X3D346DE.js} +1 -1
- package/dist/embed/{embed-page-TasksSideApp-QKEXMHCT.js → embed-page-TasksSideApp-SUMX2GJF.js} +1 -1
- package/dist/embed/{embed-page-TeamWorkspace-ZFVAFZXZ.js → embed-page-TeamWorkspace-QMIX2TP2.js} +1 -1
- package/dist/embed/{embed-page-TopologyRuntimeView-2BS4OYEZ.js → embed-page-TopologyRuntimeView-ZP6NWNS7.js} +1 -1
- package/dist/embed/{embed-page-WorkflowAutomationView-WUI7BSQP.js → embed-page-WorkflowAutomationView-4HD3ISK6.js} +1 -1
- package/dist/embed/{embed-page-chunk-4T4QMOUS.js → embed-page-chunk-3VIEL6KL.js} +1 -1
- package/dist/embed/{embed-page-chunk-5WYDCL3B.js → embed-page-chunk-MFFCCHGW.js} +158 -158
- package/dist/embed/{embed-page-chunk-6HM7ZERK.js → embed-page-chunk-QCKYIPFY.js} +1 -1
- package/dist/embed/{embed-page-chunk-XKQ6YPUW.js → embed-page-chunk-QGAGZMX2.js} +1 -1
- package/dist/embed/{embed-page-chunk-VJTO6SG6.js → embed-page-chunk-SX7YO5JA.js} +1 -1
- package/dist/embed/{embed-page-chunk-7ZBZKXBU.js → embed-page-chunk-T7XXE35M.js} +4 -4
- package/dist/embed/{embed-page-chunk-MSHEVUK3.js → embed-page-chunk-YAJUWBF2.js} +1 -1
- package/dist/embed/embed-page.js +1 -1
- package/dist/index.d.mts +431 -5
- package/dist/index.d.ts +431 -5
- package/dist/index.js +2939 -1294
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1811 -211
- package/dist/index.mjs.map +1 -1
- package/dist/{open-UVKMONVE.mjs → open-FJ3BY3CL.mjs} +3 -3
- package/dist/{open-exposure-VU227IO3.mjs → open-exposure-OX5NKWDG.mjs} +6 -6
- package/package.json +7 -7
- package/dist/a2a-FGYX2HH3.mjs.map +0 -1
- package/dist/chunk-73QZQQOT.mjs.map +0 -1
- /package/dist/{chunk-NPHK4SZ6.mjs.map → chunk-FGSFOXIX.mjs.map} +0 -0
- /package/dist/{open-UVKMONVE.mjs.map → open-FJ3BY3CL.mjs.map} +0 -0
- /package/dist/{open-exposure-VU227IO3.mjs.map → open-exposure-OX5NKWDG.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -3,10 +3,20 @@ import {
|
|
|
3
3
|
getFilenameFromPath,
|
|
4
4
|
sendBufferWithRange
|
|
5
5
|
} from "./chunk-EMYRCJOI.mjs";
|
|
6
|
+
import {
|
|
7
|
+
PathOutsideRootError,
|
|
8
|
+
ProjectFileValidationError,
|
|
9
|
+
filesystemRootDirectory,
|
|
10
|
+
formatFileRefsSection,
|
|
11
|
+
isBinaryContentType,
|
|
12
|
+
isPathWithinRoot,
|
|
13
|
+
resolvePathWithinRoot,
|
|
14
|
+
saveProjectFile
|
|
15
|
+
} from "./chunk-YKTXW6F6.mjs";
|
|
6
16
|
import {
|
|
7
17
|
PluginToolScopeError,
|
|
8
18
|
invokePluginTool
|
|
9
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-MWWLJMYN.mjs";
|
|
10
20
|
import {
|
|
11
21
|
buildPreviewTokenScript
|
|
12
22
|
} from "./chunk-7CG57LEF.mjs";
|
|
@@ -14,6 +24,16 @@ import "./chunk-EXPERMZR.mjs";
|
|
|
14
24
|
import {
|
|
15
25
|
CollectionService
|
|
16
26
|
} from "./chunk-PWHUWFXL.mjs";
|
|
27
|
+
import {
|
|
28
|
+
A2A_KEY_CHANGED_TOPIC,
|
|
29
|
+
handleA2AKeyChanged,
|
|
30
|
+
publishA2AKeyChanged,
|
|
31
|
+
refreshStoreKeyMap
|
|
32
|
+
} from "./chunk-HYYLYIMH.mjs";
|
|
33
|
+
import {
|
|
34
|
+
A2AAuthService,
|
|
35
|
+
parseEnvKeys
|
|
36
|
+
} from "./chunk-GPHOYR3X.mjs";
|
|
17
37
|
import {
|
|
18
38
|
createOrphanRecoveryRuntime,
|
|
19
39
|
createTaskRunLiveness,
|
|
@@ -33,16 +53,6 @@ import {
|
|
|
33
53
|
getQrCode,
|
|
34
54
|
getQrCodeStatus
|
|
35
55
|
} from "./chunk-6CUQGDJI.mjs";
|
|
36
|
-
import {
|
|
37
|
-
PathOutsideRootError,
|
|
38
|
-
ProjectFileValidationError,
|
|
39
|
-
filesystemRootDirectory,
|
|
40
|
-
formatFileRefsSection,
|
|
41
|
-
isBinaryContentType,
|
|
42
|
-
isPathWithinRoot,
|
|
43
|
-
resolvePathWithinRoot,
|
|
44
|
-
saveProjectFile
|
|
45
|
-
} from "./chunk-YKTXW6F6.mjs";
|
|
46
56
|
import "./chunk-NFEGQTCC.mjs";
|
|
47
57
|
|
|
48
58
|
// src/index.ts
|
|
@@ -5122,9 +5132,9 @@ var SandboxService = class {
|
|
|
5122
5132
|
);
|
|
5123
5133
|
return rewritten;
|
|
5124
5134
|
}
|
|
5125
|
-
generateErrorHtml(assistantId, threadId, vmIsolation,
|
|
5126
|
-
const encodedError = encodeURIComponent(
|
|
5127
|
-
return ERROR_HTML.replace("{assistantId}", assistantId).replace("{threadId}", threadId).replace("{vmIsolation}", vmIsolation).replace("{errorMessage}",
|
|
5135
|
+
generateErrorHtml(assistantId, threadId, vmIsolation, errorMessage6) {
|
|
5136
|
+
const encodedError = encodeURIComponent(errorMessage6);
|
|
5137
|
+
return ERROR_HTML.replace("{assistantId}", assistantId).replace("{threadId}", threadId).replace("{vmIsolation}", vmIsolation).replace("{errorMessage}", errorMessage6);
|
|
5128
5138
|
}
|
|
5129
5139
|
};
|
|
5130
5140
|
var sandboxService = new SandboxService();
|
|
@@ -8642,6 +8652,16 @@ async function fetchPluginUiResource(backend, tenantId, pluginType, resource) {
|
|
|
8642
8652
|
if (!plugin) throw new Error(`plugin "${pluginType}" not found`);
|
|
8643
8653
|
const tool = (plugin.meta.tools ?? []).find((t2) => t2.ui?.resource === resource);
|
|
8644
8654
|
if (!tool?.ui) throw new Error(`plugin "${pluginType}" has no tool declaring ui resource "${resource}"`);
|
|
8655
|
+
const inline = plugin.meta.uiResources?.[resource];
|
|
8656
|
+
if (inline) {
|
|
8657
|
+
return {
|
|
8658
|
+
mimeType: inline.mimeType ?? "text/html;profile=mcp-app",
|
|
8659
|
+
html: inline.html,
|
|
8660
|
+
csp: inline.csp ?? tool.ui.csp,
|
|
8661
|
+
permissions: tool.ui.permissions,
|
|
8662
|
+
prefersBorder: tool.ui.prefersBorder
|
|
8663
|
+
};
|
|
8664
|
+
}
|
|
8645
8665
|
const html = await backend.catalog.readPluginFile(tenantId, pluginType, resource);
|
|
8646
8666
|
return {
|
|
8647
8667
|
mimeType: "text/html",
|
|
@@ -8721,7 +8741,7 @@ function registerMcpAppRoutes(fastify2) {
|
|
|
8721
8741
|
}
|
|
8722
8742
|
|
|
8723
8743
|
// src/controllers/eval.ts
|
|
8724
|
-
import { getStoreLattice as getStoreLattice11 } from "@axiom-lattice/core";
|
|
8744
|
+
import { getStoreLattice as getStoreLattice11, modelLatticeManager as modelLatticeManager3 } from "@axiom-lattice/core";
|
|
8725
8745
|
import { v4 as uuidv43 } from "uuid";
|
|
8726
8746
|
|
|
8727
8747
|
// src/services/eval-runner.ts
|
|
@@ -9105,8 +9125,7 @@ async function createProject(request, reply) {
|
|
|
9105
9125
|
const judgeCfg = data.judgeModelConfig || {};
|
|
9106
9126
|
const modelKey2 = judgeCfg.modelKey || "";
|
|
9107
9127
|
if (modelKey2) {
|
|
9108
|
-
|
|
9109
|
-
if (!modelLatticeManager4.hasLattice(modelKey2)) {
|
|
9128
|
+
if (!modelLatticeManager3.hasLattice(modelKey2)) {
|
|
9110
9129
|
return reply.status(400).send({ success: false, message: `Judge model "${modelKey2}" is not registered` });
|
|
9111
9130
|
}
|
|
9112
9131
|
}
|
|
@@ -9152,8 +9171,7 @@ async function listProjects(request, reply) {
|
|
|
9152
9171
|
let projects = await store.getProjectsByTenant(tenantId);
|
|
9153
9172
|
if (projects.length === 0) {
|
|
9154
9173
|
try {
|
|
9155
|
-
const
|
|
9156
|
-
const models = modelLatticeManager4.getAllLattices();
|
|
9174
|
+
const models = modelLatticeManager3.getAllLattices();
|
|
9157
9175
|
const first = models[0];
|
|
9158
9176
|
const judgeModel = first ? { modelKey: first.key } : {};
|
|
9159
9177
|
const host = request.hostname || "localhost";
|
|
@@ -9221,8 +9239,7 @@ async function updateProject(request, reply) {
|
|
|
9221
9239
|
const judgeCfg = body.judgeModelConfig || {};
|
|
9222
9240
|
const modelKey2 = judgeCfg.modelKey || "";
|
|
9223
9241
|
if (modelKey2) {
|
|
9224
|
-
|
|
9225
|
-
if (!modelLatticeManager4.hasLattice(modelKey2)) {
|
|
9242
|
+
if (!modelLatticeManager3.hasLattice(modelKey2)) {
|
|
9226
9243
|
return reply.status(400).send({ success: false, message: `Judge model "${modelKey2}" is not registered` });
|
|
9227
9244
|
}
|
|
9228
9245
|
}
|
|
@@ -10621,6 +10638,7 @@ import { randomUUID as randomUUID11 } from "crypto";
|
|
|
10621
10638
|
import {
|
|
10622
10639
|
descriptorDataValue
|
|
10623
10640
|
} from "@axiom-lattice/protocols";
|
|
10641
|
+
import { getStoreLattice as getStoreLattice15 } from "@axiom-lattice/core";
|
|
10624
10642
|
var _adapterRegistry;
|
|
10625
10643
|
var _router;
|
|
10626
10644
|
function setChannelControllerDeps(deps) {
|
|
@@ -10744,8 +10762,7 @@ function snapshotPlainObject(value) {
|
|
|
10744
10762
|
}
|
|
10745
10763
|
}
|
|
10746
10764
|
async function getInstallationStore() {
|
|
10747
|
-
const
|
|
10748
|
-
const store = getStoreLattice30("default", "channelInstallation").store;
|
|
10765
|
+
const store = getStoreLattice15("default", "channelInstallation").store;
|
|
10749
10766
|
if (store) return store;
|
|
10750
10767
|
const { PostgreSQLChannelInstallationStore } = await import("@axiom-lattice/pg-stores");
|
|
10751
10768
|
const databaseUrl = process.env.DATABASE_URL;
|
|
@@ -10853,9 +10870,9 @@ async function createChannelInstallation(request, reply) {
|
|
|
10853
10870
|
};
|
|
10854
10871
|
} catch (error) {
|
|
10855
10872
|
console.error("Failed to create channel installation:", error);
|
|
10856
|
-
const
|
|
10873
|
+
const errorMessage6 = error instanceof Error ? error.message.toLowerCase() : "";
|
|
10857
10874
|
const errorCode = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
10858
|
-
if (
|
|
10875
|
+
if (errorMessage6.includes("duplicate") || errorMessage6.includes("unique constraint") || errorCode === "23505") {
|
|
10859
10876
|
reply.code(409);
|
|
10860
10877
|
return {
|
|
10861
10878
|
success: false,
|
|
@@ -11064,7 +11081,7 @@ import {
|
|
|
11064
11081
|
DuplicateChannelBindingSubjectError,
|
|
11065
11082
|
descriptorDataValue as descriptorDataValue2
|
|
11066
11083
|
} from "@axiom-lattice/protocols";
|
|
11067
|
-
import { getBindingRegistry, getStoreLattice as
|
|
11084
|
+
import { getBindingRegistry, getStoreLattice as getStoreLattice16 } from "@axiom-lattice/core";
|
|
11068
11085
|
function getTenantId13(request) {
|
|
11069
11086
|
const userTenantId = request.user?.tenantId;
|
|
11070
11087
|
if (typeof userTenantId === "string" && userTenantId) return userTenantId;
|
|
@@ -11122,7 +11139,7 @@ function snapshotPublicUpdate(value) {
|
|
|
11122
11139
|
return body;
|
|
11123
11140
|
}
|
|
11124
11141
|
function getInstallationStore2() {
|
|
11125
|
-
return
|
|
11142
|
+
return getStoreLattice16("default", "channelInstallation").store;
|
|
11126
11143
|
}
|
|
11127
11144
|
async function isValidPublicInstallation(tenantId, channelInstallationId, channel) {
|
|
11128
11145
|
const installation = await getInstallationStore2().getInstallationById(channelInstallationId);
|
|
@@ -11622,7 +11639,7 @@ var registerSTTRoutes = (app2) => {
|
|
|
11622
11639
|
};
|
|
11623
11640
|
|
|
11624
11641
|
// src/controllers/local-a2a.ts
|
|
11625
|
-
import { getStoreLattice as
|
|
11642
|
+
import { getStoreLattice as getStoreLattice17 } from "@axiom-lattice/core";
|
|
11626
11643
|
import {
|
|
11627
11644
|
AgentType as AgentType2
|
|
11628
11645
|
} from "@axiom-lattice/protocols";
|
|
@@ -11638,7 +11655,7 @@ function getLocalRuntime(assistant) {
|
|
|
11638
11655
|
return graphDefinition.localRuntime;
|
|
11639
11656
|
}
|
|
11640
11657
|
async function getLocalA2AAssistants(tenantId) {
|
|
11641
|
-
const storeLattice =
|
|
11658
|
+
const storeLattice = getStoreLattice17("default", "assistant");
|
|
11642
11659
|
const assistants = await storeLattice.store.getAllAssistants(tenantId);
|
|
11643
11660
|
const results = [];
|
|
11644
11661
|
for (const assistant of assistants) {
|
|
@@ -11650,7 +11667,7 @@ async function getLocalA2AAssistants(tenantId) {
|
|
|
11650
11667
|
return results;
|
|
11651
11668
|
}
|
|
11652
11669
|
async function getLocalA2AAssistant(tenantId, assistantId) {
|
|
11653
|
-
const storeLattice =
|
|
11670
|
+
const storeLattice = getStoreLattice17("default", "assistant");
|
|
11654
11671
|
const assistant = await storeLattice.store.getAssistantById(tenantId, assistantId);
|
|
11655
11672
|
if (!assistant) return null;
|
|
11656
11673
|
const runtime = getLocalRuntime(assistant);
|
|
@@ -11899,9 +11916,10 @@ import {
|
|
|
11899
11916
|
createTaskLifecycleService,
|
|
11900
11917
|
createTrustedAgentMessageDispatcher,
|
|
11901
11918
|
executionAuthorityRegistry,
|
|
11902
|
-
|
|
11919
|
+
getReplicaId as getReplicaId6,
|
|
11920
|
+
getStoreLattice as getStoreLattice18,
|
|
11903
11921
|
ManagedTaskStartError,
|
|
11904
|
-
modelLatticeManager as
|
|
11922
|
+
modelLatticeManager as modelLatticeManager4,
|
|
11905
11923
|
ProjectTaskOwnerPolicy,
|
|
11906
11924
|
ProjectTaskOwnerPolicyError as ProjectTaskOwnerPolicyError2,
|
|
11907
11925
|
projectMutationMutex as projectMutationMutex2,
|
|
@@ -12353,10 +12371,10 @@ var ProjectTaskExecutionService = class {
|
|
|
12353
12371
|
return { kind: "ready", task: current, thread: ensured.thread, rosterExempt: validated.rosterExempt };
|
|
12354
12372
|
}
|
|
12355
12373
|
/** Resumes an interrupted Project Task on its canonical existing Thread. */
|
|
12356
|
-
async resume(input, signal) {
|
|
12374
|
+
async resume(input, signal, options) {
|
|
12357
12375
|
const prepared = await this.prepareResume(input, signal);
|
|
12358
12376
|
if (prepared.kind === "aborted") return prepared.outcome;
|
|
12359
|
-
return this.dispatchRun(prepared.task, prepared.thread, "resume", prepared.rosterExempt, signal);
|
|
12377
|
+
return this.dispatchRun(prepared.task, prepared.thread, "resume", prepared.rosterExempt, signal, options);
|
|
12360
12378
|
}
|
|
12361
12379
|
/** Clears the durable interruption and loads the exact resumed task. */
|
|
12362
12380
|
async prepareResume(input, signal) {
|
|
@@ -12389,6 +12407,7 @@ var ProjectTaskExecutionService = class {
|
|
|
12389
12407
|
*
|
|
12390
12408
|
* @param input Tenant, task identity, and optional command actor.
|
|
12391
12409
|
* @param signal Optional cancellation signal for the armed settlement.
|
|
12410
|
+
* @param options Optional resume command carried into the queued run message.
|
|
12392
12411
|
* @returns The settled run outcome.
|
|
12393
12412
|
* @remarks `start` semantics: make this task run. `pending` starts,
|
|
12394
12413
|
* `interrupted` resumes, `failed` retries, and `completed`/`cancelled`
|
|
@@ -12396,14 +12415,15 @@ var ProjectTaskExecutionService = class {
|
|
|
12396
12415
|
* conflict. This is a record transition plus an application-layer side
|
|
12397
12416
|
* effect; the kernel only records the status change. `actor` attributes the
|
|
12398
12417
|
* start/resume/retry/reopen transition and defaults to the system actor.
|
|
12418
|
+
* `options.command` is only carried on an interrupted-task resume.
|
|
12399
12419
|
*/
|
|
12400
|
-
async dispatch(input, signal) {
|
|
12420
|
+
async dispatch(input, signal, options) {
|
|
12401
12421
|
const task = await this.requireTask(input);
|
|
12402
12422
|
if (task.ownerType !== "agent") {
|
|
12403
12423
|
throw new ProjectTaskExecutionError("PROJECT_TASK_NOT_READY", "Project task is not agent-owned");
|
|
12404
12424
|
}
|
|
12405
12425
|
if (task.status === "pending") return this.start(input, signal);
|
|
12406
|
-
if (task.status === "interrupted") return this.resume(input, signal);
|
|
12426
|
+
if (task.status === "interrupted") return this.resume(input, signal, options);
|
|
12407
12427
|
if (task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
|
|
12408
12428
|
return this.dispatchRestartable(task, input.actor ?? SYSTEM_ACTOR, signal);
|
|
12409
12429
|
}
|
|
@@ -12414,6 +12434,7 @@ var ProjectTaskExecutionService = class {
|
|
|
12414
12434
|
*
|
|
12415
12435
|
* @param input Tenant, task identity, and optional command actor.
|
|
12416
12436
|
* @param signal Optional cancellation signal for the pre-run transition.
|
|
12437
|
+
* @param options Optional resume command carried into the queued run message.
|
|
12417
12438
|
* @returns The `in_progress` task snapshot and whether the run was queued.
|
|
12418
12439
|
* @remarks Intended for command HTTP handlers that must release the Project
|
|
12419
12440
|
* mutation mutex immediately: the armed settlement keeps running in the
|
|
@@ -12421,20 +12442,25 @@ var ProjectTaskExecutionService = class {
|
|
|
12421
12442
|
* awaits it. Enqueue failure converges the task exactly like the awaited
|
|
12422
12443
|
* path before throwing, so no phantom run is left behind.
|
|
12423
12444
|
*/
|
|
12424
|
-
async dispatchDetached(input, signal) {
|
|
12445
|
+
async dispatchDetached(input, signal, options) {
|
|
12425
12446
|
const task = await this.requireTask(input);
|
|
12447
|
+
if (options?.expectedStatus !== void 0 && task.status !== options.expectedStatus) {
|
|
12448
|
+
throw new ProjectTaskExecutionError("PROJECT_TASK_NOT_RESUMABLE", "Project task is not in the expected status");
|
|
12449
|
+
}
|
|
12426
12450
|
if (task.ownerType !== "agent") {
|
|
12427
12451
|
throw new ProjectTaskExecutionError("PROJECT_TASK_NOT_READY", "Project task is not agent-owned");
|
|
12428
12452
|
}
|
|
12429
12453
|
const actor3 = input.actor ?? SYSTEM_ACTOR;
|
|
12430
12454
|
let prepared;
|
|
12431
12455
|
let mode;
|
|
12456
|
+
let resumeOptions;
|
|
12432
12457
|
if (task.status === "pending") {
|
|
12433
12458
|
prepared = await this.prepareStart(input, signal);
|
|
12434
12459
|
mode = "start";
|
|
12435
12460
|
} else if (task.status === "interrupted") {
|
|
12436
12461
|
prepared = await this.prepareResume(input, signal);
|
|
12437
12462
|
mode = "resume";
|
|
12463
|
+
resumeOptions = options;
|
|
12438
12464
|
} else if (task.status === "failed" || task.status === "completed" || task.status === "cancelled") {
|
|
12439
12465
|
prepared = await this.prepareRestartable(task, actor3, signal);
|
|
12440
12466
|
mode = "resume";
|
|
@@ -12442,7 +12468,7 @@ var ProjectTaskExecutionService = class {
|
|
|
12442
12468
|
throw new ProjectTaskExecutionError("PROJECT_TASK_NOT_READY", `Project task is '${task.status}'`);
|
|
12443
12469
|
}
|
|
12444
12470
|
if (prepared.kind === "aborted") return { task: prepared.outcome.task, queued: false };
|
|
12445
|
-
return this.enqueueDetached(prepared.task, prepared.thread, mode, prepared.rosterExempt);
|
|
12471
|
+
return this.enqueueDetached(prepared.task, prepared.thread, mode, prepared.rosterExempt, resumeOptions);
|
|
12446
12472
|
}
|
|
12447
12473
|
/** Recovers one exact pending or processing queue row without dispatching a duplicate input. */
|
|
12448
12474
|
async recover(input, signal) {
|
|
@@ -12663,6 +12689,12 @@ var ProjectTaskExecutionService = class {
|
|
|
12663
12689
|
return room;
|
|
12664
12690
|
}
|
|
12665
12691
|
async requireMembership(task, project, room) {
|
|
12692
|
+
if (isRosterExemptProject(project) && this.deps.ensurePublicOwner) {
|
|
12693
|
+
try {
|
|
12694
|
+
await this.deps.ensurePublicOwner({ tenantId: task.tenantId, projectId: task.projectId, assistantId: task.ownerId });
|
|
12695
|
+
} catch {
|
|
12696
|
+
}
|
|
12697
|
+
}
|
|
12666
12698
|
const found = snapshotBotMembership(await this.deps.bots.findByAssistant(
|
|
12667
12699
|
task.tenantId,
|
|
12668
12700
|
task.projectId,
|
|
@@ -12681,10 +12713,10 @@ var ProjectTaskExecutionService = class {
|
|
|
12681
12713
|
}
|
|
12682
12714
|
return current;
|
|
12683
12715
|
}
|
|
12684
|
-
async dispatchRun(task, thread, mode, rosterExempt, signal) {
|
|
12716
|
+
async dispatchRun(task, thread, mode, rosterExempt, signal, options) {
|
|
12685
12717
|
const messageId = (this.options.idFactory ?? randomUUID13)();
|
|
12686
12718
|
if (signal?.aborted) return this.compensateCancellation(task, thread, messageId, false);
|
|
12687
|
-
const armed = await this.armAndEnqueue(task, thread, mode, messageId, rosterExempt, signal);
|
|
12719
|
+
const armed = await this.armAndEnqueue(task, thread, mode, messageId, rosterExempt, signal, options);
|
|
12688
12720
|
if (armed.cancelled) return this.cancelSettlement(armed.settlement, task, thread, messageId, false);
|
|
12689
12721
|
if (signal?.aborted) return this.cancelSettlement(armed.settlement, task, thread, messageId, armed.queued);
|
|
12690
12722
|
if (!armed.queued) {
|
|
@@ -12699,7 +12731,7 @@ var ProjectTaskExecutionService = class {
|
|
|
12699
12731
|
* enqueue yields `queued: false` with a stable reason; the caller decides
|
|
12700
12732
|
* whether to converge only (awaited path) or converge and throw (detached).
|
|
12701
12733
|
*/
|
|
12702
|
-
async armAndEnqueue(task, thread, mode, messageId, rosterExempt, signal) {
|
|
12734
|
+
async armAndEnqueue(task, thread, mode, messageId, rosterExempt, signal, options) {
|
|
12703
12735
|
const settlement = this.armSettlement(task, thread, messageId);
|
|
12704
12736
|
if (signal?.aborted) return { settlement, queued: false, error: "", cancelled: true };
|
|
12705
12737
|
try {
|
|
@@ -12709,7 +12741,10 @@ var ProjectTaskExecutionService = class {
|
|
|
12709
12741
|
project_id: task.projectId,
|
|
12710
12742
|
assistant_id: task.ownerId,
|
|
12711
12743
|
thread_id: thread.id
|
|
12712
|
-
}, {
|
|
12744
|
+
}, {
|
|
12745
|
+
input: { id: messageId, message: buildTaskPrompt(task, mode) },
|
|
12746
|
+
...mode === "resume" && options?.command ? { command: options.command } : {}
|
|
12747
|
+
}, {
|
|
12713
12748
|
executionMode: "followup",
|
|
12714
12749
|
trustedRunContext: { projectTask: buildTrustedTaskContext(task, thread, messageId, rosterExempt) }
|
|
12715
12750
|
});
|
|
@@ -12735,9 +12770,9 @@ var ProjectTaskExecutionService = class {
|
|
|
12735
12770
|
* task is converged exactly like the awaited path before throwing, so no
|
|
12736
12771
|
* phantom in-progress run is left behind.
|
|
12737
12772
|
*/
|
|
12738
|
-
async enqueueDetached(task, thread, mode, rosterExempt) {
|
|
12773
|
+
async enqueueDetached(task, thread, mode, rosterExempt, options) {
|
|
12739
12774
|
const messageId = (this.options.idFactory ?? randomUUID13)();
|
|
12740
|
-
const armed = await this.armAndEnqueue(task, thread, mode, messageId, rosterExempt);
|
|
12775
|
+
const armed = await this.armAndEnqueue(task, thread, mode, messageId, rosterExempt, void 0, options);
|
|
12741
12776
|
void armed.settlement.promise.catch(() => void 0);
|
|
12742
12777
|
if (!armed.queued) {
|
|
12743
12778
|
await this.claimDispatchFailure(armed.settlement, task, thread, messageId, armed.error);
|
|
@@ -12868,12 +12903,14 @@ var ProjectTaskExecutionService = class {
|
|
|
12868
12903
|
async settleInterruptedEvent(task, thread, messageId, state) {
|
|
12869
12904
|
const current = await this.withLifecycleTimeout(this.reloadTask(task), void 0);
|
|
12870
12905
|
if (current?.status === "interrupted" && this.matchesExecution(current, task, thread)) {
|
|
12906
|
+
this.emitInterrupted(current, thread, messageId, state);
|
|
12871
12907
|
return { kind: "interrupted", task: current, thread, messageId };
|
|
12872
12908
|
}
|
|
12873
12909
|
const clarify = readClarifyInterrupt(state ?? await this.readInterruptedThreadState(task, thread));
|
|
12874
12910
|
if (clarify !== void 0) {
|
|
12875
12911
|
const interrupted = await this.interruptForClarify(current ?? task, thread, clarify);
|
|
12876
12912
|
if (interrupted?.status === "interrupted" && this.matchesExecution(interrupted, task, thread)) {
|
|
12913
|
+
this.emitInterrupted(interrupted, thread, messageId, state);
|
|
12877
12914
|
return { kind: "interrupted", task: interrupted, thread, messageId };
|
|
12878
12915
|
}
|
|
12879
12916
|
}
|
|
@@ -12885,6 +12922,22 @@ var ProjectTaskExecutionService = class {
|
|
|
12885
12922
|
"interruption_not_durable"
|
|
12886
12923
|
);
|
|
12887
12924
|
}
|
|
12925
|
+
/**
|
|
12926
|
+
* Fire-and-forget best-effort projection of a durable interruption.
|
|
12927
|
+
*
|
|
12928
|
+
* @remarks Never awaited and never allowed to affect settlement. The state is
|
|
12929
|
+
* resolved lazily so a missing/undefined event state only triggers a thread
|
|
12930
|
+
* read when the hook is actually wired.
|
|
12931
|
+
*/
|
|
12932
|
+
emitInterrupted(task, thread, messageId, state) {
|
|
12933
|
+
const hook = this.deps.onTaskInterrupted;
|
|
12934
|
+
if (!hook) return;
|
|
12935
|
+
void Promise.resolve().then(async () => {
|
|
12936
|
+
const resolved = state ?? await this.readInterruptedThreadState(task, thread);
|
|
12937
|
+
await hook({ task, thread, messageId, state: resolved });
|
|
12938
|
+
}).catch(() => {
|
|
12939
|
+
});
|
|
12940
|
+
}
|
|
12888
12941
|
/** Reads the paused thread snapshot when no interrupt event payload is available (recovery). */
|
|
12889
12942
|
async readInterruptedThreadState(task, thread) {
|
|
12890
12943
|
try {
|
|
@@ -16141,6 +16194,42 @@ var ModelProviderService = class {
|
|
|
16141
16194
|
}
|
|
16142
16195
|
return registered;
|
|
16143
16196
|
}
|
|
16197
|
+
/**
|
|
16198
|
+
* Bring this replica's registry in line with one provider's current state.
|
|
16199
|
+
*
|
|
16200
|
+
* @param tenantId Tenant scope (only the default tenant registers).
|
|
16201
|
+
* @param providerId Provider to reconcile; omit to re-hydrate the tenant.
|
|
16202
|
+
* @remarks A missing provider removes every registry key under its
|
|
16203
|
+
* `${providerId}/` prefix; legacy keys registered through `/api/models` are
|
|
16204
|
+
* left untouched.
|
|
16205
|
+
*/
|
|
16206
|
+
async reconcileProvider(tenantId, providerId2) {
|
|
16207
|
+
if (!this.shouldRegister(tenantId)) {
|
|
16208
|
+
return;
|
|
16209
|
+
}
|
|
16210
|
+
if (!providerId2) {
|
|
16211
|
+
await this.hydrateRegistry();
|
|
16212
|
+
return;
|
|
16213
|
+
}
|
|
16214
|
+
const provider = await this.deps.providerStore.getProviderById(tenantId, providerId2);
|
|
16215
|
+
if (!provider) {
|
|
16216
|
+
const prefix = `${providerId2}/`;
|
|
16217
|
+
for (const modelKey2 of this.deps.registry.listRegistered(tenantId)) {
|
|
16218
|
+
if (modelKey2.startsWith(prefix)) {
|
|
16219
|
+
this.deps.registry.remove(tenantId, modelKey2);
|
|
16220
|
+
}
|
|
16221
|
+
}
|
|
16222
|
+
return;
|
|
16223
|
+
}
|
|
16224
|
+
const models = await this.deps.providerStore.listModels(tenantId, providerId2);
|
|
16225
|
+
const current = new Set(models.map((model) => toModelKey(providerId2, model.modelId)));
|
|
16226
|
+
for (const modelKey2 of this.deps.registry.listRegistered(tenantId)) {
|
|
16227
|
+
if (modelKey2.startsWith(`${providerId2}/`) && !current.has(modelKey2)) {
|
|
16228
|
+
this.deps.registry.remove(tenantId, modelKey2);
|
|
16229
|
+
}
|
|
16230
|
+
}
|
|
16231
|
+
this.syncRegistry(tenantId, provider, models);
|
|
16232
|
+
}
|
|
16144
16233
|
/**
|
|
16145
16234
|
* Create a provider configuration. Does not run discovery.
|
|
16146
16235
|
*/
|
|
@@ -16173,7 +16262,9 @@ var ModelProviderService = class {
|
|
|
16173
16262
|
id3,
|
|
16174
16263
|
{ ...input, enabled: input.enabled ?? true }
|
|
16175
16264
|
);
|
|
16176
|
-
|
|
16265
|
+
const entry = this.toPublicEntry(created, 0);
|
|
16266
|
+
this.notifyChanged(tenantId, id3);
|
|
16267
|
+
return entry;
|
|
16177
16268
|
} catch (error) {
|
|
16178
16269
|
throw this.mapStoreError(error);
|
|
16179
16270
|
}
|
|
@@ -16226,6 +16317,7 @@ var ModelProviderService = class {
|
|
|
16226
16317
|
}
|
|
16227
16318
|
const models = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16228
16319
|
this.syncRegistry(tenantId, updated, models);
|
|
16320
|
+
this.notifyChanged(tenantId, id3);
|
|
16229
16321
|
return this.toPublicEntry(updated, models.length);
|
|
16230
16322
|
} catch (error) {
|
|
16231
16323
|
throw this.mapStoreError(error);
|
|
@@ -16243,6 +16335,7 @@ var ModelProviderService = class {
|
|
|
16243
16335
|
for (const model of snapshot) {
|
|
16244
16336
|
this.deps.registry.remove(tenantId, toModelKey(id3, model.modelId));
|
|
16245
16337
|
}
|
|
16338
|
+
this.notifyChanged(tenantId, id3);
|
|
16246
16339
|
}
|
|
16247
16340
|
/**
|
|
16248
16341
|
* Discover the provider's models and replace its snapshot.
|
|
@@ -16320,6 +16413,7 @@ var ModelProviderService = class {
|
|
|
16320
16413
|
}
|
|
16321
16414
|
registered.push(...this.syncRegistry(tenantId, provider, snapshot));
|
|
16322
16415
|
}
|
|
16416
|
+
this.notifyChanged(tenantId, id3);
|
|
16323
16417
|
return {
|
|
16324
16418
|
discovered,
|
|
16325
16419
|
registered,
|
|
@@ -16436,6 +16530,7 @@ var ModelProviderService = class {
|
|
|
16436
16530
|
}
|
|
16437
16531
|
}
|
|
16438
16532
|
this.syncRegistry(tenantId, provider, models);
|
|
16533
|
+
this.notifyChanged(tenantId, id3);
|
|
16439
16534
|
return this.toPublicEntry(provider, models.length);
|
|
16440
16535
|
}
|
|
16441
16536
|
syncRegistry(tenantId, provider, models) {
|
|
@@ -16458,6 +16553,13 @@ var ModelProviderService = class {
|
|
|
16458
16553
|
}
|
|
16459
16554
|
return registered;
|
|
16460
16555
|
}
|
|
16556
|
+
/** Best-effort local mutation notification; never affects the operation. */
|
|
16557
|
+
notifyChanged(tenantId, providerId2) {
|
|
16558
|
+
try {
|
|
16559
|
+
this.deps.onChanged?.(tenantId, providerId2);
|
|
16560
|
+
} catch {
|
|
16561
|
+
}
|
|
16562
|
+
}
|
|
16461
16563
|
/**
|
|
16462
16564
|
* Enable or disable a single discovered model.
|
|
16463
16565
|
*/
|
|
@@ -16479,6 +16581,7 @@ var ModelProviderService = class {
|
|
|
16479
16581
|
}
|
|
16480
16582
|
const models = await this.deps.providerStore.listModels(tenantId, id3);
|
|
16481
16583
|
this.syncRegistry(tenantId, provider, models);
|
|
16584
|
+
this.notifyChanged(tenantId, id3);
|
|
16482
16585
|
return this.toPublicEntry(provider, models.length);
|
|
16483
16586
|
}
|
|
16484
16587
|
/**
|
|
@@ -16800,6 +16903,10 @@ var ModelProviderController = class {
|
|
|
16800
16903
|
async hydrateRegistry() {
|
|
16801
16904
|
return this.service.hydrateRegistry();
|
|
16802
16905
|
}
|
|
16906
|
+
/** Reconciles one provider (or the tenant) after a foreign change. */
|
|
16907
|
+
async reconcileProvider(tenantId, providerId2) {
|
|
16908
|
+
return this.service.reconcileProvider(tenantId, providerId2);
|
|
16909
|
+
}
|
|
16803
16910
|
/** GET /api/model-providers */
|
|
16804
16911
|
async listProviders(request, reply) {
|
|
16805
16912
|
try {
|
|
@@ -16971,6 +17078,27 @@ function registerModelProviderRoutes(app2, controller) {
|
|
|
16971
17078
|
);
|
|
16972
17079
|
}
|
|
16973
17080
|
|
|
17081
|
+
// src/services/modelProviderSync.ts
|
|
17082
|
+
var MODEL_PROVIDER_CHANGED_TOPIC = "model-provider-changed";
|
|
17083
|
+
async function publishModelProviderChanged(outbox, owner, tenantId, providerId2) {
|
|
17084
|
+
if (!tenantId) return;
|
|
17085
|
+
try {
|
|
17086
|
+
await outbox.append({
|
|
17087
|
+
topic: MODEL_PROVIDER_CHANGED_TOPIC,
|
|
17088
|
+
payload: { tenantId, providerId: providerId2 },
|
|
17089
|
+
originReplicaId: owner
|
|
17090
|
+
});
|
|
17091
|
+
} catch {
|
|
17092
|
+
}
|
|
17093
|
+
}
|
|
17094
|
+
async function handleModelProviderChanged(payload, reconcile) {
|
|
17095
|
+
const parsed = payload;
|
|
17096
|
+
const tenantId = parsed?.tenantId;
|
|
17097
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) return;
|
|
17098
|
+
const providerId2 = typeof parsed?.providerId === "string" && parsed.providerId.length > 0 ? parsed.providerId : void 0;
|
|
17099
|
+
await reconcile(tenantId, providerId2);
|
|
17100
|
+
}
|
|
17101
|
+
|
|
16974
17102
|
// src/services/collaboration/ProjectRoomService.ts
|
|
16975
17103
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
16976
17104
|
var ProjectRoomService = class {
|
|
@@ -17214,19 +17342,155 @@ var ProjectRosterService = class {
|
|
|
17214
17342
|
*
|
|
17215
17343
|
* @param actor Authenticated tenant, user, and project scope.
|
|
17216
17344
|
* @returns Stored memberships, or — for a public (general) project, which has
|
|
17217
|
-
* no authored roster —
|
|
17218
|
-
*
|
|
17219
|
-
*
|
|
17220
|
-
*
|
|
17345
|
+
* no authored roster — stored memberships plus a **virtual** entry per
|
|
17346
|
+
* remaining available tenant agent.
|
|
17347
|
+
* @remarks Read-only. Eligibility is a project property: `kind === "public"`
|
|
17348
|
+
* derives the member set dynamically from the tenant's agents (excluding the
|
|
17349
|
+
* built-in coordinator and personal assistants). Persisted rows (built-in
|
|
17350
|
+
* coordinator + any agent already interacted with) are returned as-is; every
|
|
17351
|
+
* other eligible agent is a virtual entry whose id materializes
|
|
17352
|
+
* just-in-time on the first @mention / assignment / management action
|
|
17353
|
+
* ({@link ensurePublicProjectBot}). Virtual entries are never persisted.
|
|
17221
17354
|
*/
|
|
17222
17355
|
async list(actor3) {
|
|
17223
17356
|
await this.authorize(actor3, "read");
|
|
17224
17357
|
const project = await this.deps.projects.getProjectById(actor3.tenantId, actor3.projectId);
|
|
17225
17358
|
if (project?.kind !== "public") return this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
|
|
17226
17359
|
const room = await this.deps.roomService.ensureMainRoom(actor3);
|
|
17360
|
+
const persisted = await this.deps.membershipStore.list(actor3.tenantId, actor3.projectId);
|
|
17361
|
+
const known = new Set(persisted.map((item) => item.assistantId));
|
|
17362
|
+
const taken = new Set(persisted.map((item) => item.mentionName));
|
|
17227
17363
|
const agents = await this.deps.agents.list(actor3.tenantId, actor3.userId);
|
|
17228
17364
|
const now = /* @__PURE__ */ new Date();
|
|
17229
|
-
|
|
17365
|
+
const entries = [...persisted];
|
|
17366
|
+
for (const agent of agents) {
|
|
17367
|
+
if (agent.assistantId === ROOM_COORDINATOR_KEY) continue;
|
|
17368
|
+
if (agent.ownerUserId !== void 0) continue;
|
|
17369
|
+
if (known.has(agent.assistantId)) continue;
|
|
17370
|
+
const mentionName = uniqueMentionName(agent.name, taken);
|
|
17371
|
+
taken.add(mentionName);
|
|
17372
|
+
entries.push(virtualPublicProjectBot(project, room, agent, mentionName, now));
|
|
17373
|
+
}
|
|
17374
|
+
return entries;
|
|
17375
|
+
}
|
|
17376
|
+
/**
|
|
17377
|
+
* Just-in-time materializes one public-project agent into a real membership,
|
|
17378
|
+
* presence thread, and binding.
|
|
17379
|
+
*
|
|
17380
|
+
* @param actor Authenticated tenant, user, and public-project scope.
|
|
17381
|
+
* @param assistantId Eligible tenant agent to materialize.
|
|
17382
|
+
* @returns The existing membership, or the newly materialized one.
|
|
17383
|
+
* @remarks Serialized under the Project mutation mutex so concurrent touches
|
|
17384
|
+
* converge on one row; bounded to a single agent (unlike an eager sweep).
|
|
17385
|
+
* Returns an existing row (any status) unchanged, so an explicitly removed
|
|
17386
|
+
* agent is not resurrected.
|
|
17387
|
+
*/
|
|
17388
|
+
async ensurePublicProjectBot(actor3, assistantId) {
|
|
17389
|
+
await this.authorize(actor3, "read");
|
|
17390
|
+
return this.withLock(actor3, () => this.materializePublicBot(actor3, assistantId));
|
|
17391
|
+
}
|
|
17392
|
+
/**
|
|
17393
|
+
* Resolves a roster membership id to a real membership, materializing a
|
|
17394
|
+
* public-project virtual entry just-in-time.
|
|
17395
|
+
*
|
|
17396
|
+
* @param actor Authenticated tenant, user, and project scope.
|
|
17397
|
+
* @param membershipId A real membership id or a public virtual entry id.
|
|
17398
|
+
* @returns The real membership (with a real presence thread).
|
|
17399
|
+
* @remarks Read-capability path used by roster interactions that need an
|
|
17400
|
+
* addressable thread (e.g. "View agent work" on a not-yet-touched agent).
|
|
17401
|
+
* It performs only idempotent writes (materializing/repairing the anchor for
|
|
17402
|
+
* an entry the caller can already read), scoped to this project.
|
|
17403
|
+
*/
|
|
17404
|
+
async ensureBotMembership(actor3, membershipId) {
|
|
17405
|
+
await this.authorize(actor3, "read");
|
|
17406
|
+
return this.withLock(actor3, async () => {
|
|
17407
|
+
const existing = await this.deps.membershipStore.findById(actor3.tenantId, membershipId);
|
|
17408
|
+
if (existing && existing.tenantId === actor3.tenantId && existing.projectId === actor3.projectId) {
|
|
17409
|
+
await this.healBinding(existing);
|
|
17410
|
+
return existing;
|
|
17411
|
+
}
|
|
17412
|
+
const assistantId = publicProjectBotAssistantId(actor3.projectId, membershipId);
|
|
17413
|
+
if (assistantId !== void 0) return this.materializePublicBot(actor3, assistantId);
|
|
17414
|
+
throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
|
|
17415
|
+
});
|
|
17416
|
+
}
|
|
17417
|
+
/**
|
|
17418
|
+
* System (trusted) just-in-time materialization of a public-project agent.
|
|
17419
|
+
*
|
|
17420
|
+
* @param input Exact tenant, project, and eligible shared agent.
|
|
17421
|
+
* @returns The existing or newly materialized membership.
|
|
17422
|
+
* @remarks For trusted gateway paths that are not a human capability check
|
|
17423
|
+
* (e.g. starting a public-project task owned by this agent, so it has a room
|
|
17424
|
+
* anchor to post/reply). No human access check is performed; the owner must
|
|
17425
|
+
* be a shared tenant agent (personal assistants are rejected).
|
|
17426
|
+
*/
|
|
17427
|
+
async ensurePublicProjectAgent(input) {
|
|
17428
|
+
const actor3 = { tenantId: input.tenantId, userId: "", projectId: input.projectId };
|
|
17429
|
+
return this.withLock(actor3, () => this.materializePublicBot(actor3, input.assistantId));
|
|
17430
|
+
}
|
|
17431
|
+
/** Unlocked single-agent materialization; callers must hold the Project lock. */
|
|
17432
|
+
async materializePublicBot(actor3, assistantId) {
|
|
17433
|
+
const project = await this.deps.projects.getProjectById(actor3.tenantId, actor3.projectId);
|
|
17434
|
+
if (project?.kind !== "public") throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Public project not found");
|
|
17435
|
+
const existing = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, assistantId);
|
|
17436
|
+
if (existing) {
|
|
17437
|
+
this.validateMembership(existing, actor3, { id: existing.roomId, workspaceId: existing.workspaceId }, assistantId, existing.roomThreadId);
|
|
17438
|
+
await this.healBinding(existing);
|
|
17439
|
+
return existing;
|
|
17440
|
+
}
|
|
17441
|
+
const agent = (await this.deps.agents.list(actor3.tenantId, actor3.userId)).find((item) => item.assistantId === assistantId);
|
|
17442
|
+
if (!agent || agent.ownerUserId !== void 0) throw new ProjectRosterServiceError("ASSISTANT_NOT_FOUND", 404, "Agent not found");
|
|
17443
|
+
const room = await this.deps.roomStore.ensureMainRoom({
|
|
17444
|
+
id: randomUUID18(),
|
|
17445
|
+
tenantId: actor3.tenantId,
|
|
17446
|
+
workspaceId: project.workspaceId,
|
|
17447
|
+
projectId: actor3.projectId,
|
|
17448
|
+
name: project.name
|
|
17449
|
+
});
|
|
17450
|
+
const taken = new Set((await this.deps.membershipStore.list(actor3.tenantId, actor3.projectId)).map((item) => item.mentionName));
|
|
17451
|
+
const mentionName = uniqueMentionName(agent.name, taken);
|
|
17452
|
+
let ids = await this.findSafePresence(actor3, room, assistantId);
|
|
17453
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
17454
|
+
try {
|
|
17455
|
+
await this.ensureThread(actor3, room, assistantId, ids.membershipId, ids.threadId);
|
|
17456
|
+
break;
|
|
17457
|
+
} catch (error) {
|
|
17458
|
+
if (!(error instanceof ProjectRosterServiceError) || error.code !== "THREAD_CONFLICT") throw error;
|
|
17459
|
+
ids = { membershipId: this.idFactory(), threadId: this.idFactory() };
|
|
17460
|
+
if (attempt === 2) throw error;
|
|
17461
|
+
}
|
|
17462
|
+
}
|
|
17463
|
+
let result;
|
|
17464
|
+
try {
|
|
17465
|
+
result = await this.deps.membershipStore.save({
|
|
17466
|
+
id: ids.membershipId,
|
|
17467
|
+
tenantId: actor3.tenantId,
|
|
17468
|
+
workspaceId: room.workspaceId,
|
|
17469
|
+
projectId: actor3.projectId,
|
|
17470
|
+
roomId: room.id,
|
|
17471
|
+
assistantId,
|
|
17472
|
+
role: "specialist",
|
|
17473
|
+
title: publicBotTitle(agent.name, assistantId),
|
|
17474
|
+
mentionName,
|
|
17475
|
+
status: "active",
|
|
17476
|
+
roomThreadId: ids.threadId
|
|
17477
|
+
});
|
|
17478
|
+
} catch (error) {
|
|
17479
|
+
if (error instanceof Error && error.name === "ProjectBotMembershipIdConflictError") {
|
|
17480
|
+
const raced = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, assistantId);
|
|
17481
|
+
if (raced) return raced;
|
|
17482
|
+
}
|
|
17483
|
+
throw error;
|
|
17484
|
+
}
|
|
17485
|
+
if (result.kind !== "created" && result.kind !== "updated" && result.kind !== "reactivated") {
|
|
17486
|
+
const raced = await this.deps.membershipStore.findByAssistant(actor3.tenantId, actor3.projectId, assistantId);
|
|
17487
|
+
if (raced) return raced;
|
|
17488
|
+
throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
|
|
17489
|
+
}
|
|
17490
|
+
this.validateMembership(result.membership, actor3, room, assistantId, ids.threadId);
|
|
17491
|
+
await this.deps.bindingLifecycle.ensure(result.membership);
|
|
17492
|
+
this.publish(result.membership, result.kind === "created" ? "added" : "updated", `roster:${result.membership.id}:${result.membership.updatedAt.toISOString()}`);
|
|
17493
|
+
return result.membership;
|
|
17230
17494
|
}
|
|
17231
17495
|
/** Adds or reactivates a bot and ensures its room-presence thread exists. */
|
|
17232
17496
|
async add(actor3, input) {
|
|
@@ -17255,7 +17519,7 @@ var ProjectRosterService = class {
|
|
|
17255
17519
|
try {
|
|
17256
17520
|
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 });
|
|
17257
17521
|
} catch (error) {
|
|
17258
|
-
if (error instanceof Error &&
|
|
17522
|
+
if (error instanceof Error && error.name === "ProjectBotMembershipIdConflictError") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
|
|
17259
17523
|
throw error;
|
|
17260
17524
|
}
|
|
17261
17525
|
if (result.kind !== "created" && result.kind !== "updated" && result.kind !== "reactivated") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, `Roster ${result.kind.replace("_", " ")}`);
|
|
@@ -17282,8 +17546,9 @@ var ProjectRosterService = class {
|
|
|
17282
17546
|
async remove(actor3, input) {
|
|
17283
17547
|
return this.withLock(actor3, async () => {
|
|
17284
17548
|
await this.authorize(actor3, "manage");
|
|
17285
|
-
const target = await this.target(actor3, input.membershipId);
|
|
17286
|
-
|
|
17549
|
+
const { membership: target, materialized } = await this.target(actor3, input.membershipId);
|
|
17550
|
+
const expected = materialized ? target.updatedAt : input.expectedUpdatedAt;
|
|
17551
|
+
this.requireCurrent(target, expected);
|
|
17287
17552
|
if (target.role === "coordinator") throw coordinatorProtected("The built-in room coordinator cannot be removed");
|
|
17288
17553
|
for (const status of activeTaskStatuses) {
|
|
17289
17554
|
const tasks = await this.deps.taskStore.list({ tenantId: actor3.tenantId, projectId: actor3.projectId, ownerType: "agent", ownerId: target.assistantId, status, limit: 1 });
|
|
@@ -17292,7 +17557,7 @@ var ProjectRosterService = class {
|
|
|
17292
17557
|
await this.authorize(actor3, "manage");
|
|
17293
17558
|
await this.deps.bindingLifecycle.remove(target);
|
|
17294
17559
|
try {
|
|
17295
|
-
const removed = await this.applyUpdate(actor3, target, { status: "removed" },
|
|
17560
|
+
const removed = await this.applyUpdate(actor3, target, { status: "removed" }, expected);
|
|
17296
17561
|
this.publish(removed, "removed", `roster:${removed.id}:${removed.updatedAt.toISOString()}`);
|
|
17297
17562
|
return removed;
|
|
17298
17563
|
} catch (error) {
|
|
@@ -17306,11 +17571,12 @@ var ProjectRosterService = class {
|
|
|
17306
17571
|
async mutate(actor3, id3, expected, patch, lifecycle) {
|
|
17307
17572
|
return this.withLock(actor3, async () => {
|
|
17308
17573
|
await this.authorize(actor3, "manage");
|
|
17309
|
-
const target = await this.target(actor3, id3);
|
|
17310
|
-
|
|
17574
|
+
const { membership: target, materialized } = await this.target(actor3, id3);
|
|
17575
|
+
const expectedAt = materialized ? target.updatedAt : expected;
|
|
17576
|
+
this.requireCurrent(target, expectedAt);
|
|
17311
17577
|
this.assertCoordinatorPolicy(target, patch, lifecycle);
|
|
17312
17578
|
await this.authorize(actor3, "manage");
|
|
17313
|
-
const updated = await this.applyUpdate(actor3, target, patch,
|
|
17579
|
+
const updated = await this.applyUpdate(actor3, target, patch, expectedAt);
|
|
17314
17580
|
if (lifecycle === "pause") await this.deps.bindingLifecycle.pause(updated);
|
|
17315
17581
|
else if (lifecycle === "resume") await this.deps.bindingLifecycle.resume(updated);
|
|
17316
17582
|
else await this.applyLifecycle(updated);
|
|
@@ -17351,12 +17617,20 @@ var ProjectRosterService = class {
|
|
|
17351
17617
|
}
|
|
17352
17618
|
await this.deps.bindingLifecycle.ensure(membership);
|
|
17353
17619
|
}
|
|
17620
|
+
/** Best-effort, idempotent binding repair for an already-materialized active member. */
|
|
17621
|
+
async healBinding(membership) {
|
|
17622
|
+
if (membership.status !== "active") return;
|
|
17623
|
+
try {
|
|
17624
|
+
await this.deps.bindingLifecycle.ensure(membership);
|
|
17625
|
+
} catch {
|
|
17626
|
+
}
|
|
17627
|
+
}
|
|
17354
17628
|
async applyUpdate(actor3, target, patch, expectedUpdatedAt2) {
|
|
17355
17629
|
let result;
|
|
17356
17630
|
try {
|
|
17357
17631
|
result = await this.deps.membershipStore.update({ tenantId: actor3.tenantId, id: target.id, patch, expectedUpdatedAt: expectedUpdatedAt2 });
|
|
17358
17632
|
} catch (error) {
|
|
17359
|
-
if (error instanceof Error &&
|
|
17633
|
+
if (error instanceof Error && error.name === "ProjectBotMembershipIdConflictError") throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
|
|
17360
17634
|
throw error;
|
|
17361
17635
|
}
|
|
17362
17636
|
if (result.kind === "updated") {
|
|
@@ -17371,7 +17645,18 @@ var ProjectRosterService = class {
|
|
|
17371
17645
|
await this.deps.accessService.requireProjectAccess({ ...actor3, capability: capability2 });
|
|
17372
17646
|
}
|
|
17373
17647
|
async target(actor3, id3) {
|
|
17374
|
-
|
|
17648
|
+
let target = await this.deps.membershipStore.findById(actor3.tenantId, id3);
|
|
17649
|
+
let materialized = false;
|
|
17650
|
+
if (!target || target.tenantId !== actor3.tenantId || target.projectId !== actor3.projectId) {
|
|
17651
|
+
const assistantId = publicProjectBotAssistantId(actor3.projectId, id3);
|
|
17652
|
+
if (assistantId !== void 0) {
|
|
17653
|
+
const project = await this.deps.projects.getProjectById(actor3.tenantId, actor3.projectId);
|
|
17654
|
+
if (project?.kind === "public") {
|
|
17655
|
+
target = await this.materializePublicBot(actor3, assistantId);
|
|
17656
|
+
materialized = true;
|
|
17657
|
+
}
|
|
17658
|
+
}
|
|
17659
|
+
}
|
|
17375
17660
|
if (!target || target.tenantId !== actor3.tenantId || target.projectId !== actor3.projectId) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
|
|
17376
17661
|
const room = await this.deps.roomService.ensureMainRoom(actor3);
|
|
17377
17662
|
const assistant = await this.deps.assistantStore.getAssistantById(actor3.tenantId, target.assistantId);
|
|
@@ -17381,7 +17666,7 @@ var ProjectRosterService = class {
|
|
|
17381
17666
|
const thread = await this.deps.threadStore.getThreadById(actor3.tenantId, target.roomThreadId);
|
|
17382
17667
|
if (!thread) throw new ProjectRosterServiceError("THREAD_CONFLICT", 409, "Presence thread not found");
|
|
17383
17668
|
this.validateThread(thread, actor3.tenantId, this.expectedMetadata(room, actor3, target.id, target.assistantId), target.roomThreadId);
|
|
17384
|
-
return target;
|
|
17669
|
+
return { membership: target, materialized };
|
|
17385
17670
|
}
|
|
17386
17671
|
requireCurrent(target, expected) {
|
|
17387
17672
|
if (!activeStatuses.includes(target.status)) throw new ProjectRosterServiceError("ROSTER_MEMBER_NOT_FOUND", 404, "Roster member not found");
|
|
@@ -17452,22 +17737,50 @@ var ProjectRosterService = class {
|
|
|
17452
17737
|
);
|
|
17453
17738
|
}
|
|
17454
17739
|
};
|
|
17455
|
-
function
|
|
17456
|
-
|
|
17740
|
+
function uniqueMentionName(name, taken) {
|
|
17741
|
+
let base = (name || "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
17742
|
+
if (!/^[a-z]/.test(base)) base = `agent-${base}`.replace(/^agent-$/, "agent");
|
|
17743
|
+
base = base.slice(0, 32);
|
|
17744
|
+
if (base.length < 2) base = "agent";
|
|
17745
|
+
let candidate = base;
|
|
17746
|
+
let suffix = 2;
|
|
17747
|
+
while (taken.has(candidate)) {
|
|
17748
|
+
const tail = `-${suffix}`;
|
|
17749
|
+
candidate = `${base.slice(0, 32 - tail.length)}${tail}`;
|
|
17750
|
+
suffix += 1;
|
|
17751
|
+
}
|
|
17752
|
+
return candidate;
|
|
17753
|
+
}
|
|
17754
|
+
function publicBotTitle(name, fallbackId) {
|
|
17755
|
+
const trimmed = (name || "").trim();
|
|
17756
|
+
if (trimmed.length === 0) return fallbackId.slice(0, 80);
|
|
17757
|
+
return trimmed.slice(0, 80);
|
|
17758
|
+
}
|
|
17759
|
+
var PUBLIC_PROJECT_BOT_PREFIX = "public-project-bot:";
|
|
17760
|
+
function publicProjectBotId(projectId, assistantId) {
|
|
17761
|
+
return `${PUBLIC_PROJECT_BOT_PREFIX}${projectId}:${assistantId}`;
|
|
17762
|
+
}
|
|
17763
|
+
function publicProjectBotAssistantId(projectId, membershipId) {
|
|
17764
|
+
const prefix = `${PUBLIC_PROJECT_BOT_PREFIX}${projectId}:`;
|
|
17765
|
+
return membershipId.startsWith(prefix) && membershipId.length > prefix.length ? membershipId.slice(prefix.length) : void 0;
|
|
17766
|
+
}
|
|
17767
|
+
function virtualPublicProjectBot(project, room, agent, mentionName, now) {
|
|
17768
|
+
const id3 = publicProjectBotId(project.id, agent.assistantId);
|
|
17457
17769
|
return {
|
|
17458
17770
|
id: id3,
|
|
17459
17771
|
tenantId: project.tenantId,
|
|
17460
17772
|
workspaceId: project.workspaceId,
|
|
17461
17773
|
projectId: project.id,
|
|
17462
|
-
roomId,
|
|
17774
|
+
roomId: room.id,
|
|
17463
17775
|
assistantId: agent.assistantId,
|
|
17464
17776
|
role: "specialist",
|
|
17465
|
-
title: agent.name,
|
|
17466
|
-
mentionName
|
|
17777
|
+
title: publicBotTitle(agent.name, agent.assistantId),
|
|
17778
|
+
mentionName,
|
|
17467
17779
|
status: "active",
|
|
17468
17780
|
roomThreadId: id3,
|
|
17469
17781
|
joinedAt: now,
|
|
17470
|
-
updatedAt: now
|
|
17782
|
+
updatedAt: now,
|
|
17783
|
+
provisional: true
|
|
17471
17784
|
};
|
|
17472
17785
|
}
|
|
17473
17786
|
|
|
@@ -17588,7 +17901,8 @@ var RoomMessageService = class {
|
|
|
17588
17901
|
const room = await this.room(actor3, access.project);
|
|
17589
17902
|
return this.persistNormalizedHumanMessage(actor3, snapshot, room);
|
|
17590
17903
|
}
|
|
17591
|
-
async persistNormalizedHumanMessage(actor3,
|
|
17904
|
+
async persistNormalizedHumanMessage(actor3, input, room) {
|
|
17905
|
+
const snapshot = await this.resolvePublicMentions(actor3, input);
|
|
17592
17906
|
for (const membershipId of snapshot.botIds) await this.validateMentionIdentity(actor3, room, membershipId);
|
|
17593
17907
|
let reply = null;
|
|
17594
17908
|
if (snapshot.replyToMessageId !== void 0) {
|
|
@@ -17615,6 +17929,38 @@ var RoomMessageService = class {
|
|
|
17615
17929
|
}
|
|
17616
17930
|
return { message: validated, replayed };
|
|
17617
17931
|
}
|
|
17932
|
+
/**
|
|
17933
|
+
* Rewrites public-project virtual mention ids to their real membership ids,
|
|
17934
|
+
* materializing each mentioned agent just-in-time.
|
|
17935
|
+
*
|
|
17936
|
+
* @param actor Authenticated sender.
|
|
17937
|
+
* @param snapshot Normalized message with possibly-virtual bot mention ids.
|
|
17938
|
+
* @returns The snapshot unchanged, or one whose bot mentions carry real ids.
|
|
17939
|
+
*/
|
|
17940
|
+
async resolvePublicMentions(actor3, snapshot) {
|
|
17941
|
+
const resolver = this.deps.resolveMentionTarget;
|
|
17942
|
+
if (!resolver || snapshot.botIds.length === 0) return snapshot;
|
|
17943
|
+
let changed = false;
|
|
17944
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17945
|
+
const mentions = [];
|
|
17946
|
+
for (const mention of snapshot.mentions) {
|
|
17947
|
+
if (mention.type !== "bot") {
|
|
17948
|
+
mentions.push(mention);
|
|
17949
|
+
continue;
|
|
17950
|
+
}
|
|
17951
|
+
const real = await resolver(actor3, actor3.projectId, mention.membershipId);
|
|
17952
|
+
if (real !== mention.membershipId) changed = true;
|
|
17953
|
+
if (seen.has(real)) {
|
|
17954
|
+
changed = true;
|
|
17955
|
+
continue;
|
|
17956
|
+
}
|
|
17957
|
+
seen.add(real);
|
|
17958
|
+
mentions.push({ type: "bot", membershipId: real });
|
|
17959
|
+
}
|
|
17960
|
+
if (!changed) return snapshot;
|
|
17961
|
+
const botIds = mentions.filter((mention) => mention.type === "bot").map((mention) => mention.membershipId);
|
|
17962
|
+
return { ...snapshot, mentions, botIds };
|
|
17963
|
+
}
|
|
17618
17964
|
/**
|
|
17619
17965
|
* Persists a human message, then resolves and dispatches its current targets.
|
|
17620
17966
|
* Idempotent outcomes are single-flight and retained only in this service process; a
|
|
@@ -17815,6 +18161,13 @@ import { toProjectRoomPublicMessage } from "@axiom-lattice/protocols";
|
|
|
17815
18161
|
|
|
17816
18162
|
// src/services/collaboration/RoomInterruptService.ts
|
|
17817
18163
|
import { randomUUID as randomUUID20 } from "crypto";
|
|
18164
|
+
var RESUMABLE_TASK_INTERRUPT_CODES = /* @__PURE__ */ new Set([
|
|
18165
|
+
"PROJECT_TASK_NOT_RESUMABLE",
|
|
18166
|
+
"PROJECT_TASK_NOT_READY",
|
|
18167
|
+
"PROJECT_TASK_START_CONFLICT",
|
|
18168
|
+
"TASK_STATUS_CONFLICT",
|
|
18169
|
+
"REVIEW_APPROVAL_REQUIRED"
|
|
18170
|
+
]);
|
|
17818
18171
|
var RoomInterruptServiceError = class extends Error {
|
|
17819
18172
|
/** Creates a stable room interrupt error. */
|
|
17820
18173
|
constructor(code, statusCode, message) {
|
|
@@ -17837,7 +18190,16 @@ var RoomInterruptService = class {
|
|
|
17837
18190
|
/** Resolves a pending interrupt with an explicit resume command. */
|
|
17838
18191
|
async resume(input, command) {
|
|
17839
18192
|
await this.deps.requireAccess({ tenantId: input.tenantId, userId: input.userId, projectId: input.projectId });
|
|
17840
|
-
|
|
18193
|
+
if (input.ref.kind === "task") {
|
|
18194
|
+
await this.resumeTask(input, command);
|
|
18195
|
+
return;
|
|
18196
|
+
}
|
|
18197
|
+
await this.resumePresence(input, command);
|
|
18198
|
+
}
|
|
18199
|
+
/** Resolves a bot presence-thread interrupt by dispatching a follow-up resume. */
|
|
18200
|
+
async resumePresence(input, command) {
|
|
18201
|
+
const membershipId = input.ref.kind === "presence" ? input.ref.membershipId : "";
|
|
18202
|
+
const membership = await this.deps.resolveMembership({ tenantId: input.tenantId, projectId: input.projectId, membershipId });
|
|
17841
18203
|
if (!membership || membership.status !== "active" || !membership.roomThreadId || membership.projectId !== input.projectId) {
|
|
17842
18204
|
throw new RoomInterruptServiceError("ROOM_INTERRUPT_TARGET_UNAVAILABLE", 404, "Room interrupt target is unavailable");
|
|
17843
18205
|
}
|
|
@@ -17853,6 +18215,12 @@ var RoomInterruptService = class {
|
|
|
17853
18215
|
throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
|
|
17854
18216
|
}
|
|
17855
18217
|
const inputMessageId = this.idFactory();
|
|
18218
|
+
let rosterExempt = false;
|
|
18219
|
+
try {
|
|
18220
|
+
rosterExempt = isRosterExemptProject(await this.deps.projects.getProjectById(membership.tenantId, membership.projectId));
|
|
18221
|
+
} catch {
|
|
18222
|
+
rosterExempt = false;
|
|
18223
|
+
}
|
|
17856
18224
|
await this.deps.dispatcher.dispatch(
|
|
17857
18225
|
{
|
|
17858
18226
|
tenant_id: scope.tenantId,
|
|
@@ -17877,12 +18245,54 @@ var RoomInterruptService = class {
|
|
|
17877
18245
|
role: membership.role,
|
|
17878
18246
|
title: membership.title,
|
|
17879
18247
|
source: "project_room",
|
|
17880
|
-
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
|
|
18248
|
+
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
|
|
18249
|
+
...rosterExempt ? { rosterExempt: true } : {}
|
|
17881
18250
|
}
|
|
17882
18251
|
}
|
|
17883
18252
|
}
|
|
17884
18253
|
);
|
|
17885
18254
|
}
|
|
18255
|
+
/** Resolves a Project Task interrupt by re-reading the task thread and resuming its durable run. */
|
|
18256
|
+
async resumeTask(input, command) {
|
|
18257
|
+
if (!this.deps.tasks || !this.deps.execution) throw new RoomInterruptServiceError("ROOM_INTERRUPT_TARGET_UNAVAILABLE", 404, "Project task runtime is unavailable");
|
|
18258
|
+
const taskId = input.ref.kind === "task" ? input.ref.taskId : "";
|
|
18259
|
+
const task = await this.deps.tasks.getById(input.tenantId, taskId);
|
|
18260
|
+
if (!task || task.tenantId !== input.tenantId || task.projectId !== input.projectId || task.status !== "interrupted" || typeof task.context?.thread_id !== "string") {
|
|
18261
|
+
throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
|
|
18262
|
+
}
|
|
18263
|
+
const status = await this.deps.getThreadStatus({
|
|
18264
|
+
tenantId: task.tenantId,
|
|
18265
|
+
assistantId: task.ownerId,
|
|
18266
|
+
threadId: task.context.thread_id,
|
|
18267
|
+
workspaceId: task.workspaceId ?? "",
|
|
18268
|
+
projectId: task.projectId ?? ""
|
|
18269
|
+
});
|
|
18270
|
+
if (String(status) !== "interrupted") throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
|
|
18271
|
+
if (this.deps.getThreadState) {
|
|
18272
|
+
const state = await this.deps.getThreadState({
|
|
18273
|
+
tenantId: task.tenantId,
|
|
18274
|
+
assistantId: task.ownerId,
|
|
18275
|
+
threadId: task.context.thread_id,
|
|
18276
|
+
workspaceId: task.workspaceId ?? "",
|
|
18277
|
+
projectId: task.projectId ?? ""
|
|
18278
|
+
});
|
|
18279
|
+
if (readClarifyInterrupt(state) === void 0) {
|
|
18280
|
+
throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, "Room interrupt is no longer pending");
|
|
18281
|
+
}
|
|
18282
|
+
}
|
|
18283
|
+
try {
|
|
18284
|
+
await this.deps.execution.dispatchDetached(
|
|
18285
|
+
{ tenantId: task.tenantId, taskId: task.id, actor: `user:${input.userId}` },
|
|
18286
|
+
void 0,
|
|
18287
|
+
{ command, expectedStatus: "interrupted" }
|
|
18288
|
+
);
|
|
18289
|
+
} catch (error) {
|
|
18290
|
+
if (error instanceof ProjectTaskExecutionError && RESUMABLE_TASK_INTERRUPT_CODES.has(error.code)) {
|
|
18291
|
+
throw new RoomInterruptServiceError("ROOM_INTERRUPT_NOT_PENDING", 409, error.message);
|
|
18292
|
+
}
|
|
18293
|
+
throw error;
|
|
18294
|
+
}
|
|
18295
|
+
}
|
|
17886
18296
|
};
|
|
17887
18297
|
|
|
17888
18298
|
// src/schemas/project-room.ts
|
|
@@ -17902,7 +18312,7 @@ var createRoomMessageSchema = z10.object({
|
|
|
17902
18312
|
mentions: z10.array(z10.union([
|
|
17903
18313
|
z10.object({ type: z10.literal("team") }).strict(),
|
|
17904
18314
|
z10.object({ type: z10.literal("bot"), membershipId: id2 }).strict()
|
|
17905
|
-
])).optional(),
|
|
18315
|
+
])).max(50).optional(),
|
|
17906
18316
|
replyToMessageId: id2.optional()
|
|
17907
18317
|
}).strict();
|
|
17908
18318
|
var addProjectMemberSchema = z10.object({ userId: id2, role: z10.enum(["owner", "admin", "member", "viewer"]) }).strict();
|
|
@@ -17926,14 +18336,18 @@ var updateProjectBotSchema = z10.object({
|
|
|
17926
18336
|
expectedUpdatedAt: isoDate
|
|
17927
18337
|
}).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" });
|
|
17928
18338
|
var resumeInterruptSchema = z10.object({
|
|
17929
|
-
|
|
18339
|
+
ref: z10.discriminatedUnion("kind", [
|
|
18340
|
+
z10.object({ kind: z10.literal("presence"), membershipId: id2 }).strict(),
|
|
18341
|
+
z10.object({ kind: z10.literal("task"), taskId: id2 }).strict()
|
|
18342
|
+
]).optional(),
|
|
18343
|
+
membershipId: id2.optional(),
|
|
17930
18344
|
answers: z10.array(z10.object({
|
|
17931
18345
|
questionIndex: z10.number().int().nonnegative(),
|
|
17932
18346
|
otherText: z10.string().optional(),
|
|
17933
18347
|
selectedOptions: z10.array(z10.string()).optional(),
|
|
17934
18348
|
filePath: z10.string().optional()
|
|
17935
18349
|
}).strict()).optional()
|
|
17936
|
-
}).strict();
|
|
18350
|
+
}).strict().refine((v) => v.ref !== void 0 || v.membershipId !== void 0, { message: "ref or membershipId is required" });
|
|
17937
18351
|
|
|
17938
18352
|
// src/routes/project-rooms.ts
|
|
17939
18353
|
var ProjectRoomAuthError = class extends Error {
|
|
@@ -17984,8 +18398,9 @@ function registerProjectRoomRoutes(app2, deps) {
|
|
|
17984
18398
|
const params = projectRoomParamsSchema.parse(request.params);
|
|
17985
18399
|
const requestActor = actor(request, params.projectId);
|
|
17986
18400
|
const body = resumeInterruptSchema.parse(request.body ?? {});
|
|
18401
|
+
const ref = body.ref ?? { kind: "presence", membershipId: body.membershipId };
|
|
17987
18402
|
await deps.interruptService.resume(
|
|
17988
|
-
{ tenantId: requestActor.tenantId, projectId: requestActor.projectId, userId: requestActor.userId,
|
|
18403
|
+
{ tenantId: requestActor.tenantId, projectId: requestActor.projectId, userId: requestActor.userId, ref },
|
|
17989
18404
|
{ resume: { data: { answers: body.answers ?? [] } } }
|
|
17990
18405
|
);
|
|
17991
18406
|
return { data: { resolved: true } };
|
|
@@ -18061,6 +18476,12 @@ function registerProjectRosterRoutes(app2, deps) {
|
|
|
18061
18476
|
const body = mutationExpectedUpdatedAtSchema.parse(request.body);
|
|
18062
18477
|
return { data: await method.call(deps.service, getActor(request, p.projectId), { membershipId: p.membershipId, expectedUpdatedAt: body.expectedUpdatedAt }) };
|
|
18063
18478
|
}));
|
|
18479
|
+
app2.post("/api/projects/:projectId/bots/:membershipId/ensure", async (request, reply) => execute(reply, async () => {
|
|
18480
|
+
const p = membershipParamsSchema.parse(request.params);
|
|
18481
|
+
emptyObjectSchema.parse(request.body ?? {});
|
|
18482
|
+
emptyObjectSchema.parse(request.query ?? {});
|
|
18483
|
+
return { data: await deps.service.ensureBotMembership(getActor(request, p.projectId), p.membershipId) };
|
|
18484
|
+
}));
|
|
18064
18485
|
app2.delete("/api/projects/:projectId/bots/:membershipId", async (request, reply) => execute(reply, async () => {
|
|
18065
18486
|
const p = membershipParamsSchema.parse(request.params);
|
|
18066
18487
|
const q = mutationExpectedUpdatedAtSchema.parse(request.query);
|
|
@@ -18968,6 +19389,12 @@ var RoomDispatchService = class {
|
|
|
18968
19389
|
if (!source) throw new Error("Invalid project room dispatch message");
|
|
18969
19390
|
const anchorMessageId = source.sourceId ?? source.id;
|
|
18970
19391
|
const entries = await this.deps.resolver.resolve(source);
|
|
19392
|
+
let rosterExempt = false;
|
|
19393
|
+
try {
|
|
19394
|
+
rosterExempt = isRosterExemptProject(await this.deps.projects.getProjectById(source.tenantId, source.projectId));
|
|
19395
|
+
} catch {
|
|
19396
|
+
rosterExempt = false;
|
|
19397
|
+
}
|
|
18971
19398
|
const results = [];
|
|
18972
19399
|
for (const entry of entries) {
|
|
18973
19400
|
if (entry.kind === "failure") {
|
|
@@ -19007,7 +19434,8 @@ var RoomDispatchService = class {
|
|
|
19007
19434
|
role: membership.role,
|
|
19008
19435
|
title: membership.title,
|
|
19009
19436
|
source: "project_room",
|
|
19010
|
-
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility }
|
|
19437
|
+
...membership.responsibility === void 0 ? {} : { responsibility: membership.responsibility },
|
|
19438
|
+
...rosterExempt ? { rosterExempt: true } : {}
|
|
19011
19439
|
} }
|
|
19012
19440
|
}
|
|
19013
19441
|
});
|
|
@@ -19453,6 +19881,54 @@ var ProjectTaskExecutionMutex = class {
|
|
|
19453
19881
|
}
|
|
19454
19882
|
};
|
|
19455
19883
|
|
|
19884
|
+
// src/services/collaboration/TaskInterruptCardProjector.ts
|
|
19885
|
+
function clarifyFence(data) {
|
|
19886
|
+
return ["```clarify", JSON.stringify(data), "```"].join("\n");
|
|
19887
|
+
}
|
|
19888
|
+
var TaskInterruptCardProjector = class {
|
|
19889
|
+
constructor(deps) {
|
|
19890
|
+
this.deps = deps;
|
|
19891
|
+
}
|
|
19892
|
+
/** Projects the interruption as a correlated room card when it is a clarify. */
|
|
19893
|
+
async project(input) {
|
|
19894
|
+
const clarify = readClarifyInterrupt(input.state);
|
|
19895
|
+
if (!clarify) return;
|
|
19896
|
+
const { task, messageId } = input;
|
|
19897
|
+
const workspaceId = task.workspaceId;
|
|
19898
|
+
const projectId = task.projectId;
|
|
19899
|
+
if (!workspaceId || !projectId) return;
|
|
19900
|
+
const adapter = this.deps.adapter;
|
|
19901
|
+
const sendInterrupt = adapter.sendInterrupt;
|
|
19902
|
+
if (!sendInterrupt) {
|
|
19903
|
+
this.safeWarn("project-room:task-interrupt-card-unsupported", { taskId: task.id });
|
|
19904
|
+
return;
|
|
19905
|
+
}
|
|
19906
|
+
try {
|
|
19907
|
+
const room = snapshotProjectRoom(await this.deps.rooms.getMainRoom(task.tenantId, projectId));
|
|
19908
|
+
if (!room || !room.id || room.tenantId !== task.tenantId || room.workspaceId !== workspaceId || room.projectId !== projectId) return;
|
|
19909
|
+
const membership = snapshotBotMembership(await this.deps.bots.findByAssistant(task.tenantId, projectId, task.ownerId));
|
|
19910
|
+
if (!membership || membership.tenantId !== task.tenantId || membership.workspaceId !== workspaceId || membership.projectId !== projectId || membership.roomId !== room.id || membership.assistantId !== task.ownerId || membership.status !== "active") return;
|
|
19911
|
+
const installation = await this.deps.installation.ensure(task.tenantId);
|
|
19912
|
+
const text = clarifyFence({ taskId: task.id, questions: clarify.questions });
|
|
19913
|
+
await sendInterrupt.call(
|
|
19914
|
+
adapter,
|
|
19915
|
+
{ adapterChannel: "room", channelInstallationId: installation.id, rawTarget: { targetMembershipId: membership.id } },
|
|
19916
|
+
{ text, metadata: { correlationKey: `task:${task.id}:${messageId}` } },
|
|
19917
|
+
installation
|
|
19918
|
+
);
|
|
19919
|
+
} catch (error) {
|
|
19920
|
+
this.safeWarn("project-room:task-interrupt-card-failed", { taskId: task.id, error: error instanceof Error ? error.message : String(error) });
|
|
19921
|
+
}
|
|
19922
|
+
}
|
|
19923
|
+
/** Logs without ever letting a failing logger reject `project()`. */
|
|
19924
|
+
safeWarn(message, context) {
|
|
19925
|
+
try {
|
|
19926
|
+
this.deps.log.warn(message, context);
|
|
19927
|
+
} catch {
|
|
19928
|
+
}
|
|
19929
|
+
}
|
|
19930
|
+
};
|
|
19931
|
+
|
|
19456
19932
|
// src/services/collaboration/ProjectTaskObserver.ts
|
|
19457
19933
|
import { createHash as createHash7 } from "crypto";
|
|
19458
19934
|
import {
|
|
@@ -22370,20 +22846,28 @@ async function transferGovernedDispatchOwner(task, options, deps) {
|
|
|
22370
22846
|
return result.task;
|
|
22371
22847
|
});
|
|
22372
22848
|
}
|
|
22849
|
+
function broadcastModelProviderChanged(tenantId, providerId2) {
|
|
22850
|
+
if (!tenantId) return;
|
|
22851
|
+
try {
|
|
22852
|
+
const outbox = getStoreLattice18("default", "outbox").store;
|
|
22853
|
+
void publishModelProviderChanged(outbox, getReplicaId6(), tenantId, providerId2);
|
|
22854
|
+
} catch {
|
|
22855
|
+
}
|
|
22856
|
+
}
|
|
22373
22857
|
var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
22374
|
-
const webAppStore =
|
|
22375
|
-
const assistantStore =
|
|
22376
|
-
const projectStore =
|
|
22377
|
-
const projectMembershipStore =
|
|
22378
|
-
const userTenantLinkStore =
|
|
22379
|
-
const projectRoomStore =
|
|
22380
|
-
const projectBotMembershipStore =
|
|
22381
|
-
const projectRoomMessageStore =
|
|
22382
|
-
const projectRoomReadStateStore =
|
|
22383
|
-
const userPushSubscriptionStore =
|
|
22384
|
-
const threadStore =
|
|
22385
|
-
const taskStore =
|
|
22386
|
-
const taskWorkItemStore =
|
|
22858
|
+
const webAppStore = getStoreLattice18("default", "agentWebApp").store;
|
|
22859
|
+
const assistantStore = getStoreLattice18("default", "assistant").store;
|
|
22860
|
+
const projectStore = getStoreLattice18("default", "project").store;
|
|
22861
|
+
const projectMembershipStore = getStoreLattice18("default", "projectMembership").store;
|
|
22862
|
+
const userTenantLinkStore = getStoreLattice18("default", "userTenantLink").store;
|
|
22863
|
+
const projectRoomStore = getStoreLattice18("default", "projectRoom").store;
|
|
22864
|
+
const projectBotMembershipStore = getStoreLattice18("default", "projectBotMembership").store;
|
|
22865
|
+
const projectRoomMessageStore = getStoreLattice18("default", "projectRoomMessage").store;
|
|
22866
|
+
const projectRoomReadStateStore = getStoreLattice18("default", "projectRoomReadState").store;
|
|
22867
|
+
const userPushSubscriptionStore = getStoreLattice18("default", "userPushSubscription").store;
|
|
22868
|
+
const threadStore = getStoreLattice18("default", "thread").store;
|
|
22869
|
+
const taskStore = getStoreLattice18("default", "task").store;
|
|
22870
|
+
const taskWorkItemStore = getStoreLattice18("default", "taskWorkItem").store;
|
|
22387
22871
|
const projectTaskEnabled = options.projectTaskEnabled ?? process.env.AXIOM_PROJECT_TASK_EXECUTION_ENABLED !== "false";
|
|
22388
22872
|
const accessService = new ProjectAccessService({ projectStore, membershipStore: projectMembershipStore, userTenantLinkStore });
|
|
22389
22873
|
const realtimeMode = resolveProjectRoomRealtimeMode();
|
|
@@ -22449,7 +22933,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22449
22933
|
updateAssistant: assistantStore.updateAssistant.bind(assistantStore)
|
|
22450
22934
|
});
|
|
22451
22935
|
const resolveCoordinatorMentionName = async (tenantId) => {
|
|
22452
|
-
const tenant2 = await
|
|
22936
|
+
const tenant2 = await getStoreLattice18("default", "tenant").store.getTenantById(tenantId);
|
|
22453
22937
|
const value = tenant2?.metadata?.coordinatorMentionName;
|
|
22454
22938
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
22455
22939
|
};
|
|
@@ -22467,33 +22951,14 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22467
22951
|
ensureCoordinator: (scope) => coordinatorEnsurer.ensure(scope),
|
|
22468
22952
|
log: realtimeLog
|
|
22469
22953
|
});
|
|
22470
|
-
const roomInterruptService = new RoomInterruptService({
|
|
22471
|
-
requireAccess: async ({ tenantId, userId, projectId }) => {
|
|
22472
|
-
await accessService.requireProjectAccess({ tenantId, userId, projectId, capability: "send" });
|
|
22473
|
-
},
|
|
22474
|
-
resolveMembership: async ({ tenantId, projectId, membershipId }) => {
|
|
22475
|
-
const membership = await projectBotMembershipStore.findById(tenantId, membershipId);
|
|
22476
|
-
return membership !== null && membership.projectId === projectId ? membership : null;
|
|
22477
|
-
},
|
|
22478
|
-
getThreadStatus: async ({ tenantId, assistantId, threadId, workspaceId, projectId }) => {
|
|
22479
|
-
const agent = agentInstanceManager6.getAgent({
|
|
22480
|
-
tenant_id: tenantId,
|
|
22481
|
-
assistant_id: assistantId,
|
|
22482
|
-
thread_id: threadId,
|
|
22483
|
-
workspace_id: workspaceId,
|
|
22484
|
-
project_id: projectId
|
|
22485
|
-
});
|
|
22486
|
-
return String(await agent.getRunStatus());
|
|
22487
|
-
},
|
|
22488
|
-
dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope))
|
|
22489
|
-
});
|
|
22490
22954
|
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 });
|
|
22491
22955
|
const rosterService = new ProjectRosterService({
|
|
22492
22956
|
accessService,
|
|
22493
22957
|
roomService,
|
|
22958
|
+
roomStore: projectRoomStore,
|
|
22494
22959
|
membershipStore: projectBotMembershipStore,
|
|
22495
22960
|
projects: projectStore,
|
|
22496
|
-
agents: { list: async (tenantId, userId) => (await collectTenantAssistants(tenantId, userId)).map((assistant) => ({ assistantId: assistant.id, name: assistant.name })) },
|
|
22961
|
+
agents: { list: async (tenantId, userId) => (await collectTenantAssistants(tenantId, userId)).map((assistant) => ({ assistantId: assistant.id, name: assistant.name, ...assistant.ownerUserId === void 0 ? {} : { ownerUserId: assistant.ownerUserId } })) },
|
|
22497
22962
|
threadStore,
|
|
22498
22963
|
assistantStore,
|
|
22499
22964
|
taskStore,
|
|
@@ -22507,7 +22972,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22507
22972
|
resolver: new RoomTargetResolver({ botStore: projectBotMembershipStore }),
|
|
22508
22973
|
installation,
|
|
22509
22974
|
bindings: bindingLifecycle,
|
|
22510
|
-
router: channelDeps.router
|
|
22975
|
+
router: channelDeps.router,
|
|
22976
|
+
projects: projectStore
|
|
22511
22977
|
}) : new UnavailableRoomDispatcher();
|
|
22512
22978
|
const coordinatorWakeNotifier = dispatcher instanceof RoomDispatchService ? new CoordinatorWakeNotifier({
|
|
22513
22979
|
tasks: taskStore,
|
|
@@ -22527,12 +22993,26 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22527
22993
|
botStore: projectBotMembershipStore,
|
|
22528
22994
|
messageStore: projectRoomMessageStore,
|
|
22529
22995
|
dispatcher,
|
|
22996
|
+
resolveMentionTarget: async (actor3, projectId, membershipId) => {
|
|
22997
|
+
const assistantId = publicProjectBotAssistantId(projectId, membershipId);
|
|
22998
|
+
if (assistantId === void 0) return membershipId;
|
|
22999
|
+
const membership = await rosterService.ensurePublicProjectBot({ tenantId: actor3.tenantId, userId: actor3.userId, projectId }, assistantId);
|
|
23000
|
+
return membership.id;
|
|
23001
|
+
},
|
|
22530
23002
|
realtime,
|
|
22531
23003
|
onRealtimeFailure: realtime && channelDeps?.warn ? (event) => channelDeps.warn?.("project-room:message-realtime-callback-failed", {
|
|
22532
23004
|
code: event.code,
|
|
22533
23005
|
messageId: event.messageId
|
|
22534
23006
|
}) : void 0
|
|
22535
23007
|
});
|
|
23008
|
+
const roomAdapter = channelDeps?.adapterRegistry.get("room");
|
|
23009
|
+
const taskInterruptProjector = installation && roomAdapter ? new TaskInterruptCardProjector({
|
|
23010
|
+
rooms: projectRoomStore,
|
|
23011
|
+
bots: projectBotMembershipStore,
|
|
23012
|
+
installation,
|
|
23013
|
+
adapter: roomAdapter,
|
|
23014
|
+
log: realtimeLog
|
|
23015
|
+
}) : void 0;
|
|
22536
23016
|
const projectTaskRuntime = projectTaskEnabled ? tryCreateProjectTaskRuntime({
|
|
22537
23017
|
taskStore,
|
|
22538
23018
|
taskWorkItemStore,
|
|
@@ -22543,7 +23023,11 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22543
23023
|
threadStore,
|
|
22544
23024
|
realtime,
|
|
22545
23025
|
log: options.projectTaskLog,
|
|
22546
|
-
|
|
23026
|
+
ensurePublicOwner: (scope) => rosterService.ensurePublicProjectAgent(scope),
|
|
23027
|
+
...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier },
|
|
23028
|
+
...taskInterruptProjector === void 0 ? {} : {
|
|
23029
|
+
onTaskInterrupted: (input) => taskInterruptProjector.project(input)
|
|
23030
|
+
}
|
|
22547
23031
|
}) : { reason: void 0 };
|
|
22548
23032
|
const projectTaskComposition = "composition" in projectTaskRuntime ? projectTaskRuntime.composition : void 0;
|
|
22549
23033
|
const projectTaskUnavailableReason = "reason" in projectTaskRuntime ? projectTaskRuntime.reason : void 0;
|
|
@@ -22573,6 +23057,39 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22573
23057
|
}),
|
|
22574
23058
|
reassign: unsupportedHumanReassign
|
|
22575
23059
|
});
|
|
23060
|
+
const roomInterruptService = new RoomInterruptService({
|
|
23061
|
+
requireAccess: async ({ tenantId, userId, projectId }) => {
|
|
23062
|
+
await accessService.requireProjectAccess({ tenantId, userId, projectId, capability: "send" });
|
|
23063
|
+
},
|
|
23064
|
+
resolveMembership: async ({ tenantId, projectId, membershipId }) => {
|
|
23065
|
+
const membership = await projectBotMembershipStore.findById(tenantId, membershipId);
|
|
23066
|
+
return membership !== null && membership.projectId === projectId ? membership : null;
|
|
23067
|
+
},
|
|
23068
|
+
getThreadStatus: async ({ tenantId, assistantId, threadId, workspaceId, projectId }) => {
|
|
23069
|
+
const agent = agentInstanceManager6.getAgent({
|
|
23070
|
+
tenant_id: tenantId,
|
|
23071
|
+
assistant_id: assistantId,
|
|
23072
|
+
thread_id: threadId,
|
|
23073
|
+
workspace_id: workspaceId,
|
|
23074
|
+
project_id: projectId
|
|
23075
|
+
});
|
|
23076
|
+
return String(await agent.getRunStatus());
|
|
23077
|
+
},
|
|
23078
|
+
getThreadState: async ({ tenantId, assistantId, threadId, workspaceId, projectId }) => {
|
|
23079
|
+
const agent = agentInstanceManager6.getAgent({
|
|
23080
|
+
tenant_id: tenantId,
|
|
23081
|
+
assistant_id: assistantId,
|
|
23082
|
+
thread_id: threadId,
|
|
23083
|
+
workspace_id: workspaceId,
|
|
23084
|
+
project_id: projectId
|
|
23085
|
+
});
|
|
23086
|
+
return agent.getCurrentState();
|
|
23087
|
+
},
|
|
23088
|
+
dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
|
|
23089
|
+
projects: projectStore,
|
|
23090
|
+
tasks: taskStore,
|
|
23091
|
+
execution: projectTaskComposition?.lease.runtime.execution
|
|
23092
|
+
});
|
|
22576
23093
|
const publicRoomService = new WorkspacePublicRoomService({
|
|
22577
23094
|
getProjectById: projectStore.getProjectById.bind(projectStore),
|
|
22578
23095
|
createProject: projectStore.createProject.bind(projectStore),
|
|
@@ -22603,7 +23120,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22603
23120
|
readStateService,
|
|
22604
23121
|
interruptService: roomInterruptService,
|
|
22605
23122
|
userDirectory: {
|
|
22606
|
-
getUserById: (id3) =>
|
|
23123
|
+
getUserById: (id3) => getStoreLattice18("default", "user").store.getUserById(id3).then((u) => u ? { name: u.name } : null)
|
|
22607
23124
|
}
|
|
22608
23125
|
});
|
|
22609
23126
|
registerProjectRosterRoutes(app2, { service: rosterService });
|
|
@@ -22614,7 +23131,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22614
23131
|
createProject: projectStore.createProject.bind(projectStore)
|
|
22615
23132
|
},
|
|
22616
23133
|
workspaceStore: {
|
|
22617
|
-
getWorkspaceById: (tenantId, id3) =>
|
|
23134
|
+
getWorkspaceById: (tenantId, id3) => getStoreLattice18("default", "workspace").store.getWorkspaceById(tenantId, id3)
|
|
22618
23135
|
},
|
|
22619
23136
|
membershipStore: {
|
|
22620
23137
|
list: projectMembershipStore.list.bind(projectMembershipStore),
|
|
@@ -22637,7 +23154,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22637
23154
|
getUsersByTenant: userTenantLinkStore.getUsersByTenant.bind(userTenantLinkStore)
|
|
22638
23155
|
},
|
|
22639
23156
|
userStore: {
|
|
22640
|
-
getUserById: (id3) =>
|
|
23157
|
+
getUserById: (id3) => getStoreLattice18("default", "user").store.getUserById(id3).then((u) => u ? { id: u.id, name: u.name, email: u.email } : null)
|
|
22641
23158
|
},
|
|
22642
23159
|
assistantStore: {
|
|
22643
23160
|
getAssistantById: assistantStore.getAssistantById.bind(assistantStore),
|
|
@@ -22659,18 +23176,19 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22659
23176
|
}
|
|
22660
23177
|
});
|
|
22661
23178
|
registerCapabilityBundleRoutes(app2, new CapabilityBundleController({
|
|
22662
|
-
bundleStore:
|
|
23179
|
+
bundleStore: getStoreLattice18("default", "capabilityBundle").store,
|
|
22663
23180
|
projectStore
|
|
22664
23181
|
}, { authRequired: process.env.AUTH_REQUIRED === "true" }));
|
|
22665
23182
|
const modelProviderController = new ModelProviderController({
|
|
22666
|
-
providerStore:
|
|
23183
|
+
providerStore: getStoreLattice18("default", "modelProvider").store,
|
|
22667
23184
|
userTenantLinkStore,
|
|
22668
23185
|
registry: {
|
|
22669
|
-
register: (tenantId, modelKey2, config) =>
|
|
22670
|
-
has: (tenantId, modelKey2) =>
|
|
22671
|
-
remove: (tenantId, modelKey2) =>
|
|
22672
|
-
listRegistered: (tenantId) =>
|
|
22673
|
-
}
|
|
23186
|
+
register: (tenantId, modelKey2, config) => modelLatticeManager4.registerLatticeWithTenant(tenantId, modelKey2, config),
|
|
23187
|
+
has: (tenantId, modelKey2) => modelLatticeManager4.hasLatticeWithTenant(tenantId, modelKey2),
|
|
23188
|
+
remove: (tenantId, modelKey2) => modelLatticeManager4.removeLatticeWithTenant(tenantId, modelKey2),
|
|
23189
|
+
listRegistered: (tenantId) => modelLatticeManager4.getLatticeKeysByTenant(tenantId)
|
|
23190
|
+
},
|
|
23191
|
+
onChanged: (tenantId, providerId2) => broadcastModelProviderChanged(tenantId, providerId2)
|
|
22674
23192
|
}, { authRequired: process.env.AUTH_REQUIRED === "true" });
|
|
22675
23193
|
registerModelProviderRoutes(app2, modelProviderController);
|
|
22676
23194
|
void modelProviderController.hydrateRegistry().catch(() => void 0);
|
|
@@ -22679,7 +23197,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22679
23197
|
webAppStore,
|
|
22680
23198
|
assistantStore,
|
|
22681
23199
|
projectStore,
|
|
22682
|
-
modelRegistry: { has: (key) =>
|
|
23200
|
+
modelRegistry: { has: (key) => modelLatticeManager4.hasLattice(key) },
|
|
22683
23201
|
configuredAgentResolver: createConfiguredAgentResolver({
|
|
22684
23202
|
hydrate: (tenantId) => agentLatticeManager4.initializeStoredAssistantsForTenant(tenantId),
|
|
22685
23203
|
has: (tenantId, assistantId) => agentLatticeManager4.hasAgentLatticeWithTenant(tenantId, assistantId)
|
|
@@ -22724,7 +23242,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22724
23242
|
}),
|
|
22725
23243
|
service: new AgentWebAppRuntimeService({
|
|
22726
23244
|
webAppStore,
|
|
22727
|
-
threadStore:
|
|
23245
|
+
threadStore: getStoreLattice18("default", "thread").store,
|
|
22728
23246
|
projectStore,
|
|
22729
23247
|
assistantResolver: {
|
|
22730
23248
|
resolve: async (tenantId, assistantId) => {
|
|
@@ -22736,7 +23254,7 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
22736
23254
|
}
|
|
22737
23255
|
},
|
|
22738
23256
|
modelRegistry: {
|
|
22739
|
-
has: (key) =>
|
|
23257
|
+
has: (key) => modelLatticeManager4.hasLattice(key),
|
|
22740
23258
|
label: (key) => key
|
|
22741
23259
|
},
|
|
22742
23260
|
messageReader: {
|
|
@@ -23129,7 +23647,8 @@ var registerLatticeRoutes = (app2, channelDeps, options = {}) => {
|
|
|
23129
23647
|
return {
|
|
23130
23648
|
...projectTaskComposition ? { projectTaskRuntime: projectTaskComposition.lease } : {},
|
|
23131
23649
|
...projectTaskUnavailableReason ? { projectTaskUnavailableReason } : {},
|
|
23132
|
-
...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {}
|
|
23650
|
+
...projectRoomRealtimeRuntime ? { projectRoomRealtimeRuntime } : {},
|
|
23651
|
+
modelProviderController
|
|
23133
23652
|
};
|
|
23134
23653
|
};
|
|
23135
23654
|
function tryCreateProjectTaskRuntime(input) {
|
|
@@ -23149,9 +23668,9 @@ function tryCreateProjectTaskRuntime(input) {
|
|
|
23149
23668
|
let stores;
|
|
23150
23669
|
try {
|
|
23151
23670
|
stores = {
|
|
23152
|
-
tenants:
|
|
23153
|
-
workspaces:
|
|
23154
|
-
queue:
|
|
23671
|
+
tenants: getStoreLattice18("default", "tenant").store,
|
|
23672
|
+
workspaces: getStoreLattice18("default", "workspace").store,
|
|
23673
|
+
queue: getStoreLattice18("default", "threadMessageQueue").store
|
|
23155
23674
|
};
|
|
23156
23675
|
const missing = missingRuntimeMethod(input, stores);
|
|
23157
23676
|
if (missing) return { reason: missing };
|
|
@@ -23200,21 +23719,26 @@ function createRuntime(input, stores, log2) {
|
|
|
23200
23719
|
workspace_id: task.workspaceId,
|
|
23201
23720
|
project_id: task.projectId
|
|
23202
23721
|
});
|
|
23203
|
-
const execution = new ProjectTaskExecutionService(
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
|
|
23212
|
-
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23722
|
+
const execution = new ProjectTaskExecutionService(
|
|
23723
|
+
{
|
|
23724
|
+
tasks: input.taskStore,
|
|
23725
|
+
projects: input.projectStore,
|
|
23726
|
+
rooms: input.projectRoomStore,
|
|
23727
|
+
bots: input.projectBotMembershipStore,
|
|
23728
|
+
threads,
|
|
23729
|
+
lifecycle,
|
|
23730
|
+
dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
|
|
23731
|
+
queue: stores.queue,
|
|
23732
|
+
agentFor,
|
|
23733
|
+
...input.ensurePublicOwner === void 0 ? {} : { ensurePublicOwner: input.ensurePublicOwner },
|
|
23734
|
+
...input.onTaskInterrupted === void 0 ? {} : { onTaskInterrupted: input.onTaskInterrupted }
|
|
23735
|
+
},
|
|
23736
|
+
{
|
|
23737
|
+
settlementTimeoutMs: PROJECT_TASK_SETTLEMENT_TIMEOUT_MS,
|
|
23738
|
+
lifecycleSettlementTimeoutMs: PROJECT_TASK_LIFECYCLE_SETTLEMENT_TIMEOUT_MS
|
|
23739
|
+
}
|
|
23740
|
+
);
|
|
23741
|
+
const observer = new ProjectTaskObserver({
|
|
23218
23742
|
tasks: input.taskStore,
|
|
23219
23743
|
workItems: input.taskWorkItemStore,
|
|
23220
23744
|
tenants: stores.tenants,
|
|
@@ -23529,9 +24053,9 @@ var skillRegistration = {
|
|
|
23529
24053
|
};
|
|
23530
24054
|
|
|
23531
24055
|
// src/export_registrations/agent.registration.ts
|
|
23532
|
-
import { getStoreLattice as
|
|
24056
|
+
import { getStoreLattice as getStoreLattice19 } from "@axiom-lattice/core";
|
|
23533
24057
|
function getStore2() {
|
|
23534
|
-
return
|
|
24058
|
+
return getStoreLattice19("default", "assistant").store;
|
|
23535
24059
|
}
|
|
23536
24060
|
var agentRegistration = {
|
|
23537
24061
|
entityType: "agent",
|
|
@@ -23611,9 +24135,9 @@ var agentRegistration = {
|
|
|
23611
24135
|
};
|
|
23612
24136
|
|
|
23613
24137
|
// src/export_registrations/database-config.registration.ts
|
|
23614
|
-
import { getStoreLattice as
|
|
24138
|
+
import { getStoreLattice as getStoreLattice20 } from "@axiom-lattice/core";
|
|
23615
24139
|
function getStore3() {
|
|
23616
|
-
return
|
|
24140
|
+
return getStoreLattice20("default", "database").store;
|
|
23617
24141
|
}
|
|
23618
24142
|
var databaseConfigRegistration = {
|
|
23619
24143
|
entityType: "database_config",
|
|
@@ -23693,9 +24217,9 @@ var databaseConfigRegistration = {
|
|
|
23693
24217
|
};
|
|
23694
24218
|
|
|
23695
24219
|
// src/export_registrations/metrics-config.registration.ts
|
|
23696
|
-
import { getStoreLattice as
|
|
24220
|
+
import { getStoreLattice as getStoreLattice21 } from "@axiom-lattice/core";
|
|
23697
24221
|
function getStore4() {
|
|
23698
|
-
return
|
|
24222
|
+
return getStoreLattice21("default", "metrics").store;
|
|
23699
24223
|
}
|
|
23700
24224
|
var metricsConfigRegistration = {
|
|
23701
24225
|
entityType: "metrics_config",
|
|
@@ -23775,9 +24299,9 @@ var metricsConfigRegistration = {
|
|
|
23775
24299
|
};
|
|
23776
24300
|
|
|
23777
24301
|
// src/export_registrations/mcp-config.registration.ts
|
|
23778
|
-
import { getStoreLattice as
|
|
24302
|
+
import { getStoreLattice as getStoreLattice22 } from "@axiom-lattice/core";
|
|
23779
24303
|
function getStore5() {
|
|
23780
|
-
return
|
|
24304
|
+
return getStoreLattice22("default", "mcp").store;
|
|
23781
24305
|
}
|
|
23782
24306
|
var mcpConfigRegistration = {
|
|
23783
24307
|
entityType: "mcp_config",
|
|
@@ -23860,9 +24384,9 @@ var mcpConfigRegistration = {
|
|
|
23860
24384
|
};
|
|
23861
24385
|
|
|
23862
24386
|
// src/export_registrations/connection.registration.ts
|
|
23863
|
-
import { getStoreLattice as
|
|
24387
|
+
import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
|
|
23864
24388
|
function getStore6() {
|
|
23865
|
-
return
|
|
24389
|
+
return getStoreLattice23("default", "connection").store;
|
|
23866
24390
|
}
|
|
23867
24391
|
async function listAllConnections(tenantId) {
|
|
23868
24392
|
const store = getStore6();
|
|
@@ -23947,9 +24471,9 @@ var connectionRegistration = {
|
|
|
23947
24471
|
};
|
|
23948
24472
|
|
|
23949
24473
|
// src/export_registrations/collection.registration.ts
|
|
23950
|
-
import { getStoreLattice as
|
|
24474
|
+
import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
|
|
23951
24475
|
function getStore7() {
|
|
23952
|
-
return
|
|
24476
|
+
return getStoreLattice24("default", "collection").store;
|
|
23953
24477
|
}
|
|
23954
24478
|
var collectionRegistration = {
|
|
23955
24479
|
entityType: "collection",
|
|
@@ -24020,7 +24544,7 @@ var collectionRegistration = {
|
|
|
24020
24544
|
};
|
|
24021
24545
|
|
|
24022
24546
|
// src/export_registrations/channel-installation.registration.ts
|
|
24023
|
-
import { getStoreLattice as
|
|
24547
|
+
import { getStoreLattice as getStoreLattice25 } from "@axiom-lattice/core";
|
|
24024
24548
|
import { randomUUID as randomUUID25 } from "crypto";
|
|
24025
24549
|
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;
|
|
24026
24550
|
var PUBLIC_CHANNELS = /* @__PURE__ */ new Set(["lark", "email", "slack", "wechat"]);
|
|
@@ -24054,7 +24578,7 @@ function validateImportData(data) {
|
|
|
24054
24578
|
};
|
|
24055
24579
|
}
|
|
24056
24580
|
function getStore8() {
|
|
24057
|
-
return
|
|
24581
|
+
return getStoreLattice25("default", "channelInstallation").store;
|
|
24058
24582
|
}
|
|
24059
24583
|
function getOverwriteError(tenantId, incomingChannel, existing) {
|
|
24060
24584
|
if (!existing) return "Channel installation overwrite target must exist";
|
|
@@ -24188,9 +24712,9 @@ var channelInstallationRegistration = {
|
|
|
24188
24712
|
};
|
|
24189
24713
|
|
|
24190
24714
|
// src/export_registrations/menu.registration.ts
|
|
24191
|
-
import { getStoreLattice as
|
|
24715
|
+
import { getStoreLattice as getStoreLattice26 } from "@axiom-lattice/core";
|
|
24192
24716
|
function getStore9() {
|
|
24193
|
-
return
|
|
24717
|
+
return getStoreLattice26("default", "menu").store;
|
|
24194
24718
|
}
|
|
24195
24719
|
var menuRegistration = {
|
|
24196
24720
|
entityType: "menu",
|
|
@@ -24268,9 +24792,9 @@ var menuRegistration = {
|
|
|
24268
24792
|
};
|
|
24269
24793
|
|
|
24270
24794
|
// src/export_registrations/eval.registration.ts
|
|
24271
|
-
import { getStoreLattice as
|
|
24795
|
+
import { getStoreLattice as getStoreLattice27 } from "@axiom-lattice/core";
|
|
24272
24796
|
function getStore10() {
|
|
24273
|
-
return
|
|
24797
|
+
return getStoreLattice27("default", "eval").store;
|
|
24274
24798
|
}
|
|
24275
24799
|
var evalRegistration = {
|
|
24276
24800
|
entityType: "eval",
|
|
@@ -25091,22 +25615,59 @@ var _AgentTaskConsumer = class _AgentTaskConsumer {
|
|
|
25091
25615
|
_AgentTaskConsumer.agent_run_endpoint = "http://localhost:4001/api/runs";
|
|
25092
25616
|
var AgentTaskConsumer = _AgentTaskConsumer;
|
|
25093
25617
|
|
|
25618
|
+
// src/services/gracefulShutdown.ts
|
|
25619
|
+
function createGracefulShutdown(options) {
|
|
25620
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
25621
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
25622
|
+
let started = false;
|
|
25623
|
+
let exited = false;
|
|
25624
|
+
const doExit = (code) => {
|
|
25625
|
+
if (exited) return;
|
|
25626
|
+
exited = true;
|
|
25627
|
+
exit(code);
|
|
25628
|
+
};
|
|
25629
|
+
return async (signal) => {
|
|
25630
|
+
if (started) return;
|
|
25631
|
+
started = true;
|
|
25632
|
+
options.log?.info(`Received ${signal}; shutting down gracefully`, { signal });
|
|
25633
|
+
const timer = setTimeout(() => {
|
|
25634
|
+
options.log?.warn("Graceful shutdown timed out; forcing exit", { signal, timeoutMs });
|
|
25635
|
+
doExit(1);
|
|
25636
|
+
}, timeoutMs);
|
|
25637
|
+
timer.unref?.();
|
|
25638
|
+
for (const step of options.steps) {
|
|
25639
|
+
try {
|
|
25640
|
+
await step();
|
|
25641
|
+
} catch (error) {
|
|
25642
|
+
options.log?.warn("Graceful shutdown step failed", {
|
|
25643
|
+
error: error instanceof Error ? error.message : String(error)
|
|
25644
|
+
});
|
|
25645
|
+
}
|
|
25646
|
+
}
|
|
25647
|
+
clearTimeout(timer);
|
|
25648
|
+
doExit(0);
|
|
25649
|
+
};
|
|
25650
|
+
}
|
|
25651
|
+
|
|
25094
25652
|
// src/index.ts
|
|
25095
25653
|
import {
|
|
25096
25654
|
registerLoggerLattice,
|
|
25097
25655
|
getLoggerLattice,
|
|
25098
25656
|
loggerLatticeManager,
|
|
25099
25657
|
sandboxLatticeManager as sandboxLatticeManager2,
|
|
25100
|
-
getStoreLattice as
|
|
25658
|
+
getStoreLattice as getStoreLattice30,
|
|
25101
25659
|
agentInstanceManager as agentInstanceManager9,
|
|
25102
25660
|
createSandboxProvider,
|
|
25103
25661
|
TokenCache,
|
|
25104
25662
|
mcpManager as mcpManager3,
|
|
25105
25663
|
convertToConnection as convertToConnection2,
|
|
25106
25664
|
createTrustedAgentMessageDispatcher as createTrustedAgentMessageDispatcher2,
|
|
25665
|
+
connectAllChannels,
|
|
25666
|
+
disconnectAllChannels,
|
|
25667
|
+
ChannelConnectionLeader,
|
|
25107
25668
|
runAsLeader,
|
|
25108
25669
|
configureCoordinationLeaseStore,
|
|
25109
|
-
getReplicaId as
|
|
25670
|
+
getReplicaId as getReplicaId7,
|
|
25110
25671
|
taskLifecycleNotifier as taskLifecycleNotifier3,
|
|
25111
25672
|
projectTaskAdmissionNotifier as projectTaskAdmissionNotifier2,
|
|
25112
25673
|
scheduleLatticeManager as scheduleLatticeManager2,
|
|
@@ -25115,7 +25676,9 @@ import {
|
|
|
25115
25676
|
agentLatticeManager as agentLatticeManager5,
|
|
25116
25677
|
configurePluginCatalogSync,
|
|
25117
25678
|
PLUGIN_CATALOG_CHANGED_TOPIC,
|
|
25118
|
-
getSandboxPluginCatalog as getSandboxPluginCatalog3
|
|
25679
|
+
getSandboxPluginCatalog as getSandboxPluginCatalog3,
|
|
25680
|
+
getSandBoxManager as getSandBoxManager4,
|
|
25681
|
+
installPluginHost
|
|
25119
25682
|
} from "@axiom-lattice/core";
|
|
25120
25683
|
|
|
25121
25684
|
// src/channels/ChannelInfrastructure.ts
|
|
@@ -25123,7 +25686,7 @@ import { RoomAgentMessageService } from "@axiom-lattice/core";
|
|
|
25123
25686
|
|
|
25124
25687
|
// src/router/MessageRouter.ts
|
|
25125
25688
|
import {
|
|
25126
|
-
getStoreLattice as
|
|
25689
|
+
getStoreLattice as getStoreLattice28,
|
|
25127
25690
|
agentInstanceManager as agentInstanceManager8
|
|
25128
25691
|
} from "@axiom-lattice/core";
|
|
25129
25692
|
import {
|
|
@@ -25223,7 +25786,7 @@ function validateRoomTrustedDispatch(message, options, binding, agentId) {
|
|
|
25223
25786
|
"inputMessageId",
|
|
25224
25787
|
"role",
|
|
25225
25788
|
"title"
|
|
25226
|
-
], ["responsibility", "source"]);
|
|
25789
|
+
], ["responsibility", "source", "rosterExempt"]);
|
|
25227
25790
|
if (!binding || !targetEnvelope || !target || !context || !authority || typeof authority.expectedRoomThreadId !== "string" || authority.expectedRoomThreadId.length === 0 || trusted?.executionMode !== "followup") return false;
|
|
25228
25791
|
const expectedSender = `room:${context.roomId}:membership:${context.membershipId}`;
|
|
25229
25792
|
return targetEnvelope.adapterChannel === "room" && targetEnvelope.channelInstallationId === message.channelInstallationId && target.sourceRoomMessageId === context.sourceRoomMessageId && target.targetMembershipId === context.membershipId && target.inputMessageId === context.inputMessageId && message.tenantId === context.tenantId && message.sender.id === expectedSender && message.conversation?.id === context.roomId && message.conversation?.type === "group" && binding.tenantId === context.tenantId && binding.channel === "room" && binding.channelInstallationId === message.channelInstallationId && binding.senderId === expectedSender && binding.agentId === context.assistantId && binding.workspaceId === context.workspaceId && binding.projectId === context.projectId && binding.threadId === authority.expectedRoomThreadId && binding.threadMode === "fixed" && agentId === context.assistantId;
|
|
@@ -25414,7 +25977,7 @@ var MessageRouter = class {
|
|
|
25414
25977
|
channel: message.channel,
|
|
25415
25978
|
adapterChannel: adapter.channel
|
|
25416
25979
|
}, "Thread resolved by adapter strategy");
|
|
25417
|
-
const threadStore =
|
|
25980
|
+
const threadStore = getStoreLattice28("default", "thread").store;
|
|
25418
25981
|
try {
|
|
25419
25982
|
await threadStore.createThread(
|
|
25420
25983
|
tenantId,
|
|
@@ -25451,7 +26014,7 @@ var MessageRouter = class {
|
|
|
25451
26014
|
}
|
|
25452
26015
|
}
|
|
25453
26016
|
if (!threadId) {
|
|
25454
|
-
const threadStore =
|
|
26017
|
+
const threadStore = getStoreLattice28("default", "thread").store;
|
|
25455
26018
|
const newThreadId = randomUUID27();
|
|
25456
26019
|
console.log({
|
|
25457
26020
|
event: "dispatch:thread:create",
|
|
@@ -25713,10 +26276,23 @@ function createRoomChannelAdapter(writer) {
|
|
|
25713
26276
|
const installed = installationValues(installation);
|
|
25714
26277
|
if (!installed) throw new Error("Invalid room installation");
|
|
25715
26278
|
const targetEnvelope = exactValues2(replyTarget, ["adapterChannel", "channelInstallationId", "rawTarget"]);
|
|
25716
|
-
const target = exactStrings(targetEnvelope?.rawTarget, ["sourceRoomMessageId", "targetMembershipId", "inputMessageId"]);
|
|
25717
26279
|
const envelope = exactValues2(message, ["text", "metadata"]);
|
|
25718
|
-
|
|
25719
|
-
|
|
26280
|
+
if (!targetEnvelope || targetEnvelope.adapterChannel !== "room" || targetEnvelope.channelInstallationId !== installation.id || typeof envelope?.text !== "string") throw new Error("Invalid room interrupt");
|
|
26281
|
+
const correlationKey = exactStrings(envelope.metadata, ["correlationKey"])?.correlationKey;
|
|
26282
|
+
if (correlationKey) {
|
|
26283
|
+
const correlatedTarget = exactStrings(targetEnvelope.rawTarget, ["targetMembershipId"]);
|
|
26284
|
+
if (!correlatedTarget) throw new Error("Invalid room interrupt correlation");
|
|
26285
|
+
await writer.postCorrelatedInterrupt({
|
|
26286
|
+
tenantId: installed.tenantId,
|
|
26287
|
+
membershipId: correlatedTarget.targetMembershipId,
|
|
26288
|
+
correlationKey,
|
|
26289
|
+
text: envelope.text
|
|
26290
|
+
});
|
|
26291
|
+
return;
|
|
26292
|
+
}
|
|
26293
|
+
const target = exactStrings(targetEnvelope.rawTarget, ["sourceRoomMessageId", "targetMembershipId", "inputMessageId"]);
|
|
26294
|
+
const metadata = exactStrings(envelope.metadata, ["inputMessageId"]);
|
|
26295
|
+
if (!target || !metadata) throw new Error("Invalid room interrupt");
|
|
25720
26296
|
if (target.inputMessageId !== metadata.inputMessageId) throw new Error("Invalid room interrupt correlation");
|
|
25721
26297
|
await writer.postInterrupt({
|
|
25722
26298
|
tenantId: installed.tenantId,
|
|
@@ -25784,12 +26360,945 @@ async function prepareProjectTaskRuntime(runtime, log2) {
|
|
|
25784
26360
|
}
|
|
25785
26361
|
}
|
|
25786
26362
|
|
|
26363
|
+
// src/services/plugins/PluginDeploymentRuntimeStartup.ts
|
|
26364
|
+
import { setPluginDeploymentService } from "@axiom-lattice/core";
|
|
26365
|
+
|
|
26366
|
+
// src/services/plugins/PluginDeploymentService.ts
|
|
26367
|
+
import { randomUUID as randomUUID28 } from "crypto";
|
|
26368
|
+
import {
|
|
26369
|
+
PLUGIN_HOST_DIR,
|
|
26370
|
+
PluginDeploymentRuntimeRegistry
|
|
26371
|
+
} from "@axiom-lattice/core";
|
|
26372
|
+
|
|
26373
|
+
// src/services/plugins/pluginDeploymentNaming.ts
|
|
26374
|
+
import { createHash as createHash10 } from "crypto";
|
|
26375
|
+
var PLUGIN_GUEST_PORT = 8080;
|
|
26376
|
+
var SANDBOX_NAME_HASH_LENGTH = 16;
|
|
26377
|
+
function pluginSandboxName(tenantId, pluginType) {
|
|
26378
|
+
if (!tenantId || typeof tenantId !== "string") {
|
|
26379
|
+
throw new Error("pluginSandboxName: tenantId must be a non-empty string");
|
|
26380
|
+
}
|
|
26381
|
+
if (!pluginType || typeof pluginType !== "string") {
|
|
26382
|
+
throw new Error("pluginSandboxName: pluginType must be a non-empty string");
|
|
26383
|
+
}
|
|
26384
|
+
const hash = createHash10("sha256").update(`${tenantId}\0${pluginType}`).digest("hex").slice(0, SANDBOX_NAME_HASH_LENGTH);
|
|
26385
|
+
return `plugin-${hash}`;
|
|
26386
|
+
}
|
|
26387
|
+
|
|
26388
|
+
// src/services/plugins/PluginDeploymentService.ts
|
|
26389
|
+
var PLUGIN_HOST_SERVER_PATH = `${PLUGIN_HOST_DIR}/server.js`;
|
|
26390
|
+
var DEFAULT_PLUGIN_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
26391
|
+
var DEFAULT_PLUGIN_HEALTH_TIMEOUT_MS = 3e4;
|
|
26392
|
+
var DEFAULT_PLUGIN_HEALTH_POLL_INTERVAL_MS = 500;
|
|
26393
|
+
var DEFAULT_PLUGIN_HEALTH_PROBE_TIMEOUT_MS = 5e3;
|
|
26394
|
+
var PLUGIN_DEPLOYMENT_LEASE_KEY = "plugin-deployment:reconcile";
|
|
26395
|
+
var PLUGIN_DEPLOYMENT_LEASE_TTL_MS = 6e4;
|
|
26396
|
+
var PLUGIN_TYPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
26397
|
+
var PluginDeploymentError = class extends Error {
|
|
26398
|
+
/**
|
|
26399
|
+
* @param code - Machine-readable failure code.
|
|
26400
|
+
* @param message - Human-readable description.
|
|
26401
|
+
*/
|
|
26402
|
+
constructor(code, message) {
|
|
26403
|
+
super(message);
|
|
26404
|
+
this.name = "PluginDeploymentError";
|
|
26405
|
+
this.code = code;
|
|
26406
|
+
}
|
|
26407
|
+
};
|
|
26408
|
+
function isRecord4(value) {
|
|
26409
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26410
|
+
}
|
|
26411
|
+
function buildBootstrapEntrypoint(pluginType) {
|
|
26412
|
+
return [
|
|
26413
|
+
"sh",
|
|
26414
|
+
"-c",
|
|
26415
|
+
`while [ ! -f ${PLUGIN_HOST_SERVER_PATH} ]; do sleep 0.2; done; exec node ${PLUGIN_HOST_SERVER_PATH} --type ${pluginType} --port ${PLUGIN_GUEST_PORT}`
|
|
26416
|
+
];
|
|
26417
|
+
}
|
|
26418
|
+
var PluginDeploymentService = class {
|
|
26419
|
+
/**
|
|
26420
|
+
* @param deps - Injected store, sandbox manager, catalog, installer, lease, and seams.
|
|
26421
|
+
*/
|
|
26422
|
+
constructor(deps) {
|
|
26423
|
+
this.store = deps.store;
|
|
26424
|
+
this.sandboxManager = deps.sandboxManager;
|
|
26425
|
+
this.catalog = deps.catalog;
|
|
26426
|
+
this.installPluginHost = deps.installPluginHost;
|
|
26427
|
+
this.lease = deps.lease;
|
|
26428
|
+
this.registry = deps.registry ?? new PluginDeploymentRuntimeRegistry({ now: deps.clock ?? Date.now });
|
|
26429
|
+
this.now = deps.clock ?? Date.now;
|
|
26430
|
+
this.logger = deps.logger ?? { info: () => void 0 };
|
|
26431
|
+
this.limits = deps.limits ?? {};
|
|
26432
|
+
this.serviceBaseUrl = (deps.serviceBaseUrl ?? process.env.MICROSANDBOX_SERVICE_BASE_URL ?? "").replace(
|
|
26433
|
+
/\/$/,
|
|
26434
|
+
""
|
|
26435
|
+
);
|
|
26436
|
+
this.serviceKey = deps.serviceKey;
|
|
26437
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
26438
|
+
this.sleep = deps.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
26439
|
+
this.defaultMode = deps.defaultMode ?? "onDemand";
|
|
26440
|
+
this.defaultIdleTimeoutMs = deps.defaultIdleTimeoutMs ?? DEFAULT_PLUGIN_IDLE_TIMEOUT_MS;
|
|
26441
|
+
this.providerName = deps.providerName ?? "default";
|
|
26442
|
+
this.image = deps.image;
|
|
26443
|
+
this.healthTimeoutMs = deps.healthTimeoutMs ?? DEFAULT_PLUGIN_HEALTH_TIMEOUT_MS;
|
|
26444
|
+
this.healthPollIntervalMs = deps.healthPollIntervalMs ?? DEFAULT_PLUGIN_HEALTH_POLL_INTERVAL_MS;
|
|
26445
|
+
this.healthProbeTimeoutMs = deps.healthProbeTimeoutMs ?? DEFAULT_PLUGIN_HEALTH_PROBE_TIMEOUT_MS;
|
|
26446
|
+
this.replicaId = deps.replicaId ?? String(process.pid);
|
|
26447
|
+
this.listAllDeployments = deps.listAllDeployments;
|
|
26448
|
+
}
|
|
26449
|
+
/**
|
|
26450
|
+
* Provision a resident sandbox for a published tenant plugin.
|
|
26451
|
+
*
|
|
26452
|
+
* Validates the plugin against the tenant catalog, enforces limits, resolves a
|
|
26453
|
+
* deterministic sandbox with the bootstrap entrypoint and exposed guest port,
|
|
26454
|
+
* installs the host server, waits for `/health`, and upserts a `running`
|
|
26455
|
+
* {@link PluginDeployment}. Re-deploying an existing deployment reuses the same
|
|
26456
|
+
* sandbox name and record (idempotent).
|
|
26457
|
+
*
|
|
26458
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26459
|
+
* @param pluginType - Plugin type (directory key).
|
|
26460
|
+
* @param options - Lifecycle mode and overrides.
|
|
26461
|
+
* @returns The stored deployment record.
|
|
26462
|
+
* @throws PluginDeploymentError with `PLUGIN_NOT_FOUND`, `INVALID_PLUGIN_TYPE`,
|
|
26463
|
+
* `LIMIT_EXCEEDED`, `BACKEND_UNSUPPORTED`, `SANDBOX_CREATE_FAILED`, or
|
|
26464
|
+
* `HEALTH_TIMEOUT`.
|
|
26465
|
+
*/
|
|
26466
|
+
async deploy(tenantId, pluginType, options = {}) {
|
|
26467
|
+
const snapshot = await this.catalog.ensureLoaded(tenantId);
|
|
26468
|
+
const manifest = snapshot.manifests[pluginType];
|
|
26469
|
+
if (!manifest) {
|
|
26470
|
+
throw new PluginDeploymentError(
|
|
26471
|
+
"PLUGIN_NOT_FOUND",
|
|
26472
|
+
`plugin "${pluginType}" is not published for tenant "${tenantId}"`
|
|
26473
|
+
);
|
|
26474
|
+
}
|
|
26475
|
+
if (!PLUGIN_TYPE_RE.test(pluginType)) {
|
|
26476
|
+
throw new PluginDeploymentError(
|
|
26477
|
+
"INVALID_PLUGIN_TYPE",
|
|
26478
|
+
`plugin type "${pluginType}" is not a valid kebab-case identifier`
|
|
26479
|
+
);
|
|
26480
|
+
}
|
|
26481
|
+
const existing = await this.store.get(tenantId, pluginType);
|
|
26482
|
+
if (!existing) {
|
|
26483
|
+
await this.assertWithinLimits(tenantId);
|
|
26484
|
+
}
|
|
26485
|
+
const mode = options.mode ?? existing?.mode ?? this.defaultMode;
|
|
26486
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? existing?.idleTimeoutMs ?? this.defaultIdleTimeoutMs;
|
|
26487
|
+
const name = pluginSandboxName(tenantId, pluginType);
|
|
26488
|
+
const config = this.buildSandboxConfig(
|
|
26489
|
+
tenantId,
|
|
26490
|
+
pluginType,
|
|
26491
|
+
mode,
|
|
26492
|
+
idleTimeoutMs,
|
|
26493
|
+
options.image ?? this.image
|
|
26494
|
+
);
|
|
26495
|
+
let sandbox;
|
|
26496
|
+
try {
|
|
26497
|
+
sandbox = await this.sandboxManager.createSandbox(name, config);
|
|
26498
|
+
await this.installPluginHost(sandbox);
|
|
26499
|
+
} catch (error) {
|
|
26500
|
+
throw new PluginDeploymentError(
|
|
26501
|
+
"SANDBOX_CREATE_FAILED",
|
|
26502
|
+
`failed to provision sandbox "${name}": ${errorMessage4(error)}`
|
|
26503
|
+
);
|
|
26504
|
+
}
|
|
26505
|
+
const endpoint = await this.resolveEndpoint(sandbox);
|
|
26506
|
+
const health = await this.waitForHealth(endpoint);
|
|
26507
|
+
if (!health.ok) {
|
|
26508
|
+
const failedAt = this.nowIso();
|
|
26509
|
+
await this.store.upsert(
|
|
26510
|
+
this.buildRecord({
|
|
26511
|
+
id: existing?.id ?? randomUUID28(),
|
|
26512
|
+
tenantId,
|
|
26513
|
+
pluginType,
|
|
26514
|
+
pluginVersion: manifest.version,
|
|
26515
|
+
mode,
|
|
26516
|
+
status: "failed",
|
|
26517
|
+
sandboxName: name,
|
|
26518
|
+
endpoint,
|
|
26519
|
+
idleTimeoutMs,
|
|
26520
|
+
provider: options.provider ?? this.providerName,
|
|
26521
|
+
image: options.image ?? this.image,
|
|
26522
|
+
lastHealthAt: failedAt,
|
|
26523
|
+
lastError: health.error,
|
|
26524
|
+
createdAt: existing?.createdAt
|
|
26525
|
+
})
|
|
26526
|
+
);
|
|
26527
|
+
throw new PluginDeploymentError(
|
|
26528
|
+
"HEALTH_TIMEOUT",
|
|
26529
|
+
`resident host for plugin "${pluginType}" did not become healthy: ${health.error ?? "unknown"}`
|
|
26530
|
+
);
|
|
26531
|
+
}
|
|
26532
|
+
const record = this.buildRecord({
|
|
26533
|
+
id: existing?.id ?? randomUUID28(),
|
|
26534
|
+
tenantId,
|
|
26535
|
+
pluginType,
|
|
26536
|
+
pluginVersion: manifest.version,
|
|
26537
|
+
mode,
|
|
26538
|
+
status: "running",
|
|
26539
|
+
sandboxName: name,
|
|
26540
|
+
endpoint,
|
|
26541
|
+
idleTimeoutMs,
|
|
26542
|
+
provider: options.provider ?? this.providerName,
|
|
26543
|
+
image: options.image ?? this.image,
|
|
26544
|
+
lastHealthAt: this.nowIso(),
|
|
26545
|
+
createdAt: existing?.createdAt
|
|
26546
|
+
});
|
|
26547
|
+
const stored = await this.store.upsert(record);
|
|
26548
|
+
const key = this.registryKey(tenantId, pluginType);
|
|
26549
|
+
this.registry.setEndpoint(key, endpoint);
|
|
26550
|
+
this.registry.markHealthy(key);
|
|
26551
|
+
this.logger.info(
|
|
26552
|
+
{ event: "plugin.deployment.deployed", tenantId, pluginType, mode, endpoint },
|
|
26553
|
+
"plugin deployment running"
|
|
26554
|
+
);
|
|
26555
|
+
return stored;
|
|
26556
|
+
}
|
|
26557
|
+
/**
|
|
26558
|
+
* Delete a deployment's sandbox and record.
|
|
26559
|
+
*
|
|
26560
|
+
* Sandbox deletion is best-effort: a provider failure is logged but does not
|
|
26561
|
+
* prevent the record from being removed.
|
|
26562
|
+
*
|
|
26563
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26564
|
+
* @param pluginType - Plugin type.
|
|
26565
|
+
* @returns Whether a deployment record was removed.
|
|
26566
|
+
*/
|
|
26567
|
+
async undeploy(tenantId, pluginType) {
|
|
26568
|
+
const name = pluginSandboxName(tenantId, pluginType);
|
|
26569
|
+
try {
|
|
26570
|
+
await this.sandboxManager.deleteSandbox(name);
|
|
26571
|
+
} catch (error) {
|
|
26572
|
+
this.logger.warn?.(
|
|
26573
|
+
{ event: "plugin.deployment.undeploy.sandbox_failed", tenantId, pluginType, error: errorMessage4(error) },
|
|
26574
|
+
"best-effort sandbox delete failed"
|
|
26575
|
+
);
|
|
26576
|
+
}
|
|
26577
|
+
const undeployed = await this.store.delete(tenantId, pluginType);
|
|
26578
|
+
this.registry.clear(this.registryKey(tenantId, pluginType));
|
|
26579
|
+
this.logger.info(
|
|
26580
|
+
{ event: "plugin.deployment.undeployed", tenantId, pluginType, undeployed },
|
|
26581
|
+
"plugin deployment removed"
|
|
26582
|
+
);
|
|
26583
|
+
return { undeployed };
|
|
26584
|
+
}
|
|
26585
|
+
/**
|
|
26586
|
+
* Read the durable deployment status and refresh it with a live `/health`.
|
|
26587
|
+
*
|
|
26588
|
+
* When a `running` record exists, a single `/health` probe updates
|
|
26589
|
+
* `lastHealthAt`/`lastError`; the probe never throws.
|
|
26590
|
+
*
|
|
26591
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26592
|
+
* @param pluginType - Plugin type.
|
|
26593
|
+
* @returns The status view (with `status: "absent"` when no record exists).
|
|
26594
|
+
*/
|
|
26595
|
+
async status(tenantId, pluginType) {
|
|
26596
|
+
const record = await this.store.get(tenantId, pluginType);
|
|
26597
|
+
if (!record) {
|
|
26598
|
+
return { status: "absent" };
|
|
26599
|
+
}
|
|
26600
|
+
let lastHealthAt = record.lastHealthAt;
|
|
26601
|
+
let lastError = record.lastError;
|
|
26602
|
+
if (record.status === "running" && record.endpoint) {
|
|
26603
|
+
const health = await this.probeHealth(record.endpoint);
|
|
26604
|
+
const key = this.registryKey(tenantId, pluginType);
|
|
26605
|
+
if (health.ok) {
|
|
26606
|
+
this.registry.markHealthy(key);
|
|
26607
|
+
lastHealthAt = this.nowIso();
|
|
26608
|
+
lastError = void 0;
|
|
26609
|
+
} else {
|
|
26610
|
+
this.registry.markUnhealthy(key, health.error ?? "health probe failed");
|
|
26611
|
+
lastHealthAt = this.nowIso();
|
|
26612
|
+
lastError = health.error ?? "health probe failed";
|
|
26613
|
+
}
|
|
26614
|
+
await this.store.updateStatus(tenantId, pluginType, { lastHealthAt, lastError });
|
|
26615
|
+
}
|
|
26616
|
+
return {
|
|
26617
|
+
status: record.status,
|
|
26618
|
+
endpoint: record.endpoint,
|
|
26619
|
+
mode: record.mode,
|
|
26620
|
+
lastHealthAt,
|
|
26621
|
+
...lastError === void 0 ? {} : { lastError },
|
|
26622
|
+
sandboxName: record.sandboxName,
|
|
26623
|
+
port: record.port
|
|
26624
|
+
};
|
|
26625
|
+
}
|
|
26626
|
+
/**
|
|
26627
|
+
* List every resident deployment for a tenant.
|
|
26628
|
+
*
|
|
26629
|
+
* Reads the durable store directly (no `/health` probe) and maps each record
|
|
26630
|
+
* to the minimal view the read-only `manage_plugin deployments` MCP App needs.
|
|
26631
|
+
*
|
|
26632
|
+
* @param tenantId - Tenant whose deployments are listed.
|
|
26633
|
+
* @returns One minimal entry per stored deployment.
|
|
26634
|
+
*/
|
|
26635
|
+
async list(tenantId) {
|
|
26636
|
+
const records = await this.store.listByTenant(tenantId);
|
|
26637
|
+
return records.map((record) => ({
|
|
26638
|
+
pluginType: record.pluginType,
|
|
26639
|
+
status: record.status,
|
|
26640
|
+
mode: record.mode,
|
|
26641
|
+
...record.endpoint === void 0 ? {} : { endpoint: record.endpoint },
|
|
26642
|
+
...record.lastHealthAt === void 0 ? {} : { lastHealthAt: record.lastHealthAt },
|
|
26643
|
+
...record.lastError === void 0 ? {} : { lastError: record.lastError }
|
|
26644
|
+
}));
|
|
26645
|
+
}
|
|
26646
|
+
/**
|
|
26647
|
+
* Read a deployment's stored record without probing its host.
|
|
26648
|
+
*
|
|
26649
|
+
* The catalog executor uses this to decide whether a plugin tool call should
|
|
26650
|
+
* route to the resident service. Unlike {@link status} it never issues a
|
|
26651
|
+
* `/health` probe and resolves `undefined` when no record exists.
|
|
26652
|
+
*
|
|
26653
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26654
|
+
* @param pluginType - Plugin type.
|
|
26655
|
+
* @returns The minimal `{ status, endpoint, mode }` view, or `undefined`.
|
|
26656
|
+
*/
|
|
26657
|
+
async getDeployment(tenantId, pluginType) {
|
|
26658
|
+
const record = await this.store.get(tenantId, pluginType);
|
|
26659
|
+
if (!record) return void 0;
|
|
26660
|
+
return { status: record.status, endpoint: record.endpoint, mode: record.mode };
|
|
26661
|
+
}
|
|
26662
|
+
/**
|
|
26663
|
+
* Ensure an existing on-demand deployment is running and healthy.
|
|
26664
|
+
*
|
|
26665
|
+
* Paused/stopped deployments are resumed (or re-provisioned when the sandbox
|
|
26666
|
+
* is gone) and their status is refreshed. Concurrent calls for the same plugin
|
|
26667
|
+
* are coalesced by the runtime registry's single-flight.
|
|
26668
|
+
*
|
|
26669
|
+
* @param tenantId - Tenant that owns the plugin.
|
|
26670
|
+
* @param pluginType - Plugin type.
|
|
26671
|
+
*/
|
|
26672
|
+
async ensureRunning(tenantId, pluginType) {
|
|
26673
|
+
const record = await this.store.get(tenantId, pluginType);
|
|
26674
|
+
if (!record) return;
|
|
26675
|
+
if (record.status === "running" && record.endpoint) return;
|
|
26676
|
+
const key = this.registryKey(tenantId, pluginType);
|
|
26677
|
+
await this.registry.ensureRunning(key, async () => {
|
|
26678
|
+
const config = this.buildSandboxConfig(
|
|
26679
|
+
tenantId,
|
|
26680
|
+
pluginType,
|
|
26681
|
+
record.mode,
|
|
26682
|
+
record.idleTimeoutMs,
|
|
26683
|
+
record.imageRef ?? this.image
|
|
26684
|
+
);
|
|
26685
|
+
let sandbox;
|
|
26686
|
+
try {
|
|
26687
|
+
sandbox = await this.sandboxManager.getSandbox(record.sandboxName);
|
|
26688
|
+
await sandbox.start();
|
|
26689
|
+
} catch {
|
|
26690
|
+
try {
|
|
26691
|
+
sandbox = await this.sandboxManager.createSandbox(record.sandboxName, config);
|
|
26692
|
+
await this.installPluginHost(sandbox);
|
|
26693
|
+
} catch (error) {
|
|
26694
|
+
throw new PluginDeploymentError(
|
|
26695
|
+
"SANDBOX_CREATE_FAILED",
|
|
26696
|
+
`failed to provision sandbox "${record.sandboxName}": ${errorMessage4(error)}`
|
|
26697
|
+
);
|
|
26698
|
+
}
|
|
26699
|
+
}
|
|
26700
|
+
const endpoint = await this.resolveEndpoint(sandbox);
|
|
26701
|
+
const health = await this.waitForHealth(endpoint);
|
|
26702
|
+
if (!health.ok) {
|
|
26703
|
+
await this.store.updateStatus(tenantId, pluginType, {
|
|
26704
|
+
status: "failed",
|
|
26705
|
+
endpoint,
|
|
26706
|
+
lastHealthAt: this.nowIso(),
|
|
26707
|
+
lastError: health.error ?? "health probe failed"
|
|
26708
|
+
});
|
|
26709
|
+
throw new PluginDeploymentError(
|
|
26710
|
+
"HEALTH_TIMEOUT",
|
|
26711
|
+
`resident host for plugin "${pluginType}" did not become healthy: ${health.error ?? "unknown"}`
|
|
26712
|
+
);
|
|
26713
|
+
}
|
|
26714
|
+
await this.store.updateStatus(tenantId, pluginType, {
|
|
26715
|
+
status: "running",
|
|
26716
|
+
endpoint,
|
|
26717
|
+
lastHealthAt: this.nowIso(),
|
|
26718
|
+
lastError: void 0
|
|
26719
|
+
});
|
|
26720
|
+
this.registry.setEndpoint(key, endpoint);
|
|
26721
|
+
this.registry.markHealthy(key);
|
|
26722
|
+
});
|
|
26723
|
+
}
|
|
26724
|
+
/**
|
|
26725
|
+
* Reconcile the store against the provider at startup.
|
|
26726
|
+
*
|
|
26727
|
+
* Lease-gated and never throws: for each stored deployment it verifies the
|
|
26728
|
+
* sandbox still exists, refreshes the endpoint, and marks missing/unreadable
|
|
26729
|
+
* sandboxes `failed` with a diagnostic. Runs are best-effort.
|
|
26730
|
+
*
|
|
26731
|
+
* @returns A summary of the pass.
|
|
26732
|
+
*/
|
|
26733
|
+
async startupReconcile() {
|
|
26734
|
+
if (!await this.acquireLease()) {
|
|
26735
|
+
return { ran: false, checked: 0, refreshed: 0, failed: 0 };
|
|
26736
|
+
}
|
|
26737
|
+
const records = await this.listAll();
|
|
26738
|
+
let refreshed = 0;
|
|
26739
|
+
let failed = 0;
|
|
26740
|
+
for (const record of records) {
|
|
26741
|
+
try {
|
|
26742
|
+
const sandbox = await this.sandboxManager.getSandbox(record.sandboxName);
|
|
26743
|
+
const sandboxStatus = await sandbox.getStatus();
|
|
26744
|
+
if (sandboxStatus === "unknown") {
|
|
26745
|
+
throw new Error("sandbox not found");
|
|
26746
|
+
}
|
|
26747
|
+
const endpoint = typeof sandbox.getEndpoint === "function" ? (await sandbox.getEndpoint(record.port || PLUGIN_GUEST_PORT)).endpoint : record.endpoint;
|
|
26748
|
+
await this.store.updateStatus(record.tenantId, record.pluginType, {
|
|
26749
|
+
status: sandboxStatus === "running" ? "running" : record.status,
|
|
26750
|
+
...endpoint === void 0 ? {} : { endpoint },
|
|
26751
|
+
lastError: void 0
|
|
26752
|
+
});
|
|
26753
|
+
if (endpoint !== void 0) {
|
|
26754
|
+
this.registry.setEndpoint(this.registryKey(record.tenantId, record.pluginType), endpoint);
|
|
26755
|
+
}
|
|
26756
|
+
refreshed += 1;
|
|
26757
|
+
} catch (error) {
|
|
26758
|
+
failed += 1;
|
|
26759
|
+
this.logger.warn?.(
|
|
26760
|
+
{
|
|
26761
|
+
event: "plugin.deployment.reconcile.failed",
|
|
26762
|
+
tenantId: record.tenantId,
|
|
26763
|
+
pluginType: record.pluginType,
|
|
26764
|
+
sandboxName: record.sandboxName,
|
|
26765
|
+
error: errorMessage4(error)
|
|
26766
|
+
},
|
|
26767
|
+
"stored plugin deployment has no reachable sandbox"
|
|
26768
|
+
);
|
|
26769
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26770
|
+
status: "failed",
|
|
26771
|
+
lastError: `startup reconcile: ${errorMessage4(error)}`
|
|
26772
|
+
});
|
|
26773
|
+
}
|
|
26774
|
+
}
|
|
26775
|
+
this.logger.info(
|
|
26776
|
+
{ event: "plugin.deployment.reconcile", checked: records.length, refreshed, failed },
|
|
26777
|
+
"plugin deployment startup reconcile complete"
|
|
26778
|
+
);
|
|
26779
|
+
return { ran: true, checked: records.length, refreshed, failed };
|
|
26780
|
+
}
|
|
26781
|
+
/**
|
|
26782
|
+
* Probe every running deployment's `/health` and refresh its health record.
|
|
26783
|
+
*
|
|
26784
|
+
* Lease-gated and never throws. Unhealthy `alwaysOn` deployments are
|
|
26785
|
+
* re-provisioned best-effort.
|
|
26786
|
+
*
|
|
26787
|
+
* @returns A summary of the pass.
|
|
26788
|
+
*/
|
|
26789
|
+
async healthWatchdog() {
|
|
26790
|
+
if (!await this.acquireLease()) {
|
|
26791
|
+
return { ran: false, checked: 0, healthy: 0, unhealthy: 0 };
|
|
26792
|
+
}
|
|
26793
|
+
const records = await this.listAll();
|
|
26794
|
+
let healthy = 0;
|
|
26795
|
+
let unhealthy = 0;
|
|
26796
|
+
for (const record of records) {
|
|
26797
|
+
if (record.status !== "running" || !record.endpoint) continue;
|
|
26798
|
+
const key = this.registryKey(record.tenantId, record.pluginType);
|
|
26799
|
+
const health = await this.probeHealth(record.endpoint);
|
|
26800
|
+
if (health.ok) {
|
|
26801
|
+
healthy += 1;
|
|
26802
|
+
this.registry.markHealthy(key);
|
|
26803
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26804
|
+
lastHealthAt: this.nowIso(),
|
|
26805
|
+
lastError: void 0
|
|
26806
|
+
});
|
|
26807
|
+
continue;
|
|
26808
|
+
}
|
|
26809
|
+
unhealthy += 1;
|
|
26810
|
+
this.registry.markUnhealthy(key, health.error ?? "health probe failed");
|
|
26811
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26812
|
+
lastHealthAt: this.nowIso(),
|
|
26813
|
+
lastError: health.error ?? "health probe failed"
|
|
26814
|
+
});
|
|
26815
|
+
if (record.mode === "alwaysOn") {
|
|
26816
|
+
await this.restartBestEffort(record, key);
|
|
26817
|
+
}
|
|
26818
|
+
}
|
|
26819
|
+
this.logger.info(
|
|
26820
|
+
{ event: "plugin.deployment.watchdog", checked: healthy + unhealthy, healthy, unhealthy },
|
|
26821
|
+
"plugin deployment health watchdog complete"
|
|
26822
|
+
);
|
|
26823
|
+
return { ran: true, checked: healthy + unhealthy, healthy, unhealthy };
|
|
26824
|
+
}
|
|
26825
|
+
/**
|
|
26826
|
+
* Best-effort re-provision and health-check of an unhealthy `alwaysOn`
|
|
26827
|
+
* deployment.
|
|
26828
|
+
*
|
|
26829
|
+
* @param record - Deployment to restart.
|
|
26830
|
+
* @param key - Runtime registry key.
|
|
26831
|
+
*/
|
|
26832
|
+
async restartBestEffort(record, key) {
|
|
26833
|
+
try {
|
|
26834
|
+
const config = this.buildSandboxConfig(
|
|
26835
|
+
record.tenantId,
|
|
26836
|
+
record.pluginType,
|
|
26837
|
+
record.mode,
|
|
26838
|
+
record.idleTimeoutMs,
|
|
26839
|
+
record.imageRef ?? this.image
|
|
26840
|
+
);
|
|
26841
|
+
let sandbox;
|
|
26842
|
+
try {
|
|
26843
|
+
sandbox = await this.sandboxManager.createSandbox(record.sandboxName, config);
|
|
26844
|
+
await sandbox.start().catch(() => void 0);
|
|
26845
|
+
await this.installPluginHost(sandbox);
|
|
26846
|
+
} catch (error) {
|
|
26847
|
+
throw new PluginDeploymentError(
|
|
26848
|
+
"SANDBOX_CREATE_FAILED",
|
|
26849
|
+
`failed to provision sandbox "${record.sandboxName}": ${errorMessage4(error)}`
|
|
26850
|
+
);
|
|
26851
|
+
}
|
|
26852
|
+
const endpoint = await this.resolveEndpoint(sandbox);
|
|
26853
|
+
const health = await this.waitForHealth(endpoint);
|
|
26854
|
+
if (!health.ok) {
|
|
26855
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26856
|
+
endpoint,
|
|
26857
|
+
lastError: health.error ?? "health probe failed"
|
|
26858
|
+
});
|
|
26859
|
+
return;
|
|
26860
|
+
}
|
|
26861
|
+
await this.safeUpdateStatus(record.tenantId, record.pluginType, {
|
|
26862
|
+
status: "running",
|
|
26863
|
+
endpoint,
|
|
26864
|
+
lastHealthAt: this.nowIso(),
|
|
26865
|
+
lastError: void 0
|
|
26866
|
+
});
|
|
26867
|
+
this.registry.setEndpoint(key, endpoint);
|
|
26868
|
+
this.registry.markHealthy(key);
|
|
26869
|
+
} catch (error) {
|
|
26870
|
+
this.logger.warn?.(
|
|
26871
|
+
{
|
|
26872
|
+
event: "plugin.deployment.watchdog.restart_failed",
|
|
26873
|
+
tenantId: record.tenantId,
|
|
26874
|
+
pluginType: record.pluginType,
|
|
26875
|
+
error: errorMessage4(error)
|
|
26876
|
+
},
|
|
26877
|
+
"best-effort alwaysOn restart failed"
|
|
26878
|
+
);
|
|
26879
|
+
}
|
|
26880
|
+
}
|
|
26881
|
+
/**
|
|
26882
|
+
* Build the sandbox creation config for a plugin deployment.
|
|
26883
|
+
*
|
|
26884
|
+
* @param tenantId - Tenant id.
|
|
26885
|
+
* @param pluginType - Plugin type.
|
|
26886
|
+
* @param mode - Lifecycle strategy.
|
|
26887
|
+
* @param idleTimeoutMs - On-demand idle timeout in milliseconds.
|
|
26888
|
+
* @param image - Optional image override for this provisioning.
|
|
26889
|
+
* @returns The provider-facing config.
|
|
26890
|
+
*/
|
|
26891
|
+
buildSandboxConfig(tenantId, pluginType, mode, idleTimeoutMs, image = this.image) {
|
|
26892
|
+
return {
|
|
26893
|
+
assistant_id: "",
|
|
26894
|
+
thread_id: pluginSandboxName(tenantId, pluginType),
|
|
26895
|
+
tenantId,
|
|
26896
|
+
entrypoint: buildBootstrapEntrypoint(pluginType),
|
|
26897
|
+
exposePorts: [PLUGIN_GUEST_PORT],
|
|
26898
|
+
// `alwaysOn` deployments never expire; on-demand deployments are
|
|
26899
|
+
// time-boxed by the service (finalized with the lifecycle work in
|
|
26900
|
+
// Plan 4 Task 6). The provider expects seconds, not milliseconds.
|
|
26901
|
+
timeoutSeconds: mode === "alwaysOn" ? null : Math.max(1, Math.round(idleTimeoutMs / 1e3)),
|
|
26902
|
+
...image === void 0 ? {} : { image }
|
|
26903
|
+
};
|
|
26904
|
+
}
|
|
26905
|
+
/**
|
|
26906
|
+
* Resolve the routable endpoint for the published guest port.
|
|
26907
|
+
*
|
|
26908
|
+
* @param sandbox - Provisioned sandbox instance.
|
|
26909
|
+
* @returns The relative proxy path or scheme-less host endpoint.
|
|
26910
|
+
* @throws PluginDeploymentError with `BACKEND_UNSUPPORTED` when the provider
|
|
26911
|
+
* does not implement `getEndpoint`.
|
|
26912
|
+
*/
|
|
26913
|
+
async resolveEndpoint(sandbox) {
|
|
26914
|
+
if (typeof sandbox.getEndpoint !== "function") {
|
|
26915
|
+
throw new PluginDeploymentError(
|
|
26916
|
+
"BACKEND_UNSUPPORTED",
|
|
26917
|
+
"sandbox provider does not support resident plugin services (missing getEndpoint)"
|
|
26918
|
+
);
|
|
26919
|
+
}
|
|
26920
|
+
const { endpoint } = await sandbox.getEndpoint(PLUGIN_GUEST_PORT);
|
|
26921
|
+
return endpoint;
|
|
26922
|
+
}
|
|
26923
|
+
/**
|
|
26924
|
+
* Poll `/health` until the host answers healthy or the deadline passes.
|
|
26925
|
+
*
|
|
26926
|
+
* @param endpoint - Resolved endpoint route.
|
|
26927
|
+
* @returns The final probe outcome.
|
|
26928
|
+
*/
|
|
26929
|
+
async waitForHealth(endpoint) {
|
|
26930
|
+
const deadline = Date.now() + this.healthTimeoutMs;
|
|
26931
|
+
let last = { ok: false, error: "health not attempted" };
|
|
26932
|
+
do {
|
|
26933
|
+
last = await this.probeHealth(endpoint);
|
|
26934
|
+
if (last.ok) return last;
|
|
26935
|
+
if (Date.now() >= deadline) break;
|
|
26936
|
+
await this.sleep(this.healthPollIntervalMs);
|
|
26937
|
+
} while (Date.now() < deadline);
|
|
26938
|
+
return last;
|
|
26939
|
+
}
|
|
26940
|
+
/**
|
|
26941
|
+
* Issue a single `GET ${baseUrl}${endpoint}/health` request.
|
|
26942
|
+
*
|
|
26943
|
+
* Never throws; network/timeout/non-2xx/parse failures resolve `{ ok: false }`.
|
|
26944
|
+
*
|
|
26945
|
+
* @param endpoint - Resolved endpoint route.
|
|
26946
|
+
* @returns The probe outcome.
|
|
26947
|
+
*/
|
|
26948
|
+
async probeHealth(endpoint) {
|
|
26949
|
+
const url = `${this.serviceBaseUrl}${endpoint}/health`;
|
|
26950
|
+
const controller = new AbortController();
|
|
26951
|
+
const timer = setTimeout(() => controller.abort(), this.healthProbeTimeoutMs);
|
|
26952
|
+
try {
|
|
26953
|
+
const response = await this.fetchImpl(url, {
|
|
26954
|
+
method: "GET",
|
|
26955
|
+
...this.serviceKey === void 0 ? {} : { headers: { Authorization: `Bearer ${this.serviceKey}` } },
|
|
26956
|
+
signal: controller.signal
|
|
26957
|
+
});
|
|
26958
|
+
if (!response.ok) {
|
|
26959
|
+
return { ok: false, error: `health responded with HTTP ${response.status}` };
|
|
26960
|
+
}
|
|
26961
|
+
const body = await response.json().catch(() => void 0);
|
|
26962
|
+
if (isRecord4(body) && body.ok === true) {
|
|
26963
|
+
return { ok: true };
|
|
26964
|
+
}
|
|
26965
|
+
return { ok: false, error: "health returned an invalid envelope" };
|
|
26966
|
+
} catch (error) {
|
|
26967
|
+
return { ok: false, error: errorMessage4(error) };
|
|
26968
|
+
} finally {
|
|
26969
|
+
clearTimeout(timer);
|
|
26970
|
+
}
|
|
26971
|
+
}
|
|
26972
|
+
/**
|
|
26973
|
+
* Enforce per-tenant and global deployment caps.
|
|
26974
|
+
*
|
|
26975
|
+
* @param tenantId - Tenant attempting to deploy.
|
|
26976
|
+
* @throws PluginDeploymentError with `LIMIT_EXCEEDED`.
|
|
26977
|
+
*/
|
|
26978
|
+
async assertWithinLimits(tenantId) {
|
|
26979
|
+
const { maxPerTenant, maxGlobal } = this.limits;
|
|
26980
|
+
if (maxPerTenant !== void 0) {
|
|
26981
|
+
const tenantRecords = await this.store.listByTenant(tenantId);
|
|
26982
|
+
if (tenantRecords.length >= maxPerTenant) {
|
|
26983
|
+
throw new PluginDeploymentError(
|
|
26984
|
+
"LIMIT_EXCEEDED",
|
|
26985
|
+
`tenant "${tenantId}" reached the resident plugin limit (${maxPerTenant})`
|
|
26986
|
+
);
|
|
26987
|
+
}
|
|
26988
|
+
}
|
|
26989
|
+
if (maxGlobal !== void 0 && this.listAllDeployments) {
|
|
26990
|
+
const all = await this.listAllDeployments();
|
|
26991
|
+
if (all.length >= maxGlobal) {
|
|
26992
|
+
throw new PluginDeploymentError(
|
|
26993
|
+
"LIMIT_EXCEEDED",
|
|
26994
|
+
`global resident plugin limit reached (${maxGlobal})`
|
|
26995
|
+
);
|
|
26996
|
+
}
|
|
26997
|
+
}
|
|
26998
|
+
}
|
|
26999
|
+
/**
|
|
27000
|
+
* Acquire the reconcile lease for this pass.
|
|
27001
|
+
*
|
|
27002
|
+
* @returns True when this replica may run the pass.
|
|
27003
|
+
*/
|
|
27004
|
+
async acquireLease() {
|
|
27005
|
+
try {
|
|
27006
|
+
return await this.lease.tryAcquire(
|
|
27007
|
+
PLUGIN_DEPLOYMENT_LEASE_KEY,
|
|
27008
|
+
this.replicaId,
|
|
27009
|
+
PLUGIN_DEPLOYMENT_LEASE_TTL_MS
|
|
27010
|
+
);
|
|
27011
|
+
} catch (error) {
|
|
27012
|
+
this.logger.warn?.(
|
|
27013
|
+
{ event: "plugin.deployment.lease.failed", error: errorMessage4(error) },
|
|
27014
|
+
"could not acquire reconcile lease; skipping pass"
|
|
27015
|
+
);
|
|
27016
|
+
return false;
|
|
27017
|
+
}
|
|
27018
|
+
}
|
|
27019
|
+
/**
|
|
27020
|
+
* List deployments for reconcile/watchdog, degrading to an empty list.
|
|
27021
|
+
*
|
|
27022
|
+
* @returns Stored deployments, or `[]` when no global lister is wired.
|
|
27023
|
+
*/
|
|
27024
|
+
async listAll() {
|
|
27025
|
+
if (!this.listAllDeployments) return [];
|
|
27026
|
+
try {
|
|
27027
|
+
return await this.listAllDeployments();
|
|
27028
|
+
} catch (error) {
|
|
27029
|
+
this.logger.warn?.(
|
|
27030
|
+
{ event: "plugin.deployment.list.failed", error: errorMessage4(error) },
|
|
27031
|
+
"could not list plugin deployments"
|
|
27032
|
+
);
|
|
27033
|
+
return [];
|
|
27034
|
+
}
|
|
27035
|
+
}
|
|
27036
|
+
/**
|
|
27037
|
+
* Apply a status patch, swallowing store errors (used on the soft paths).
|
|
27038
|
+
*
|
|
27039
|
+
* @param tenantId - Tenant id.
|
|
27040
|
+
* @param pluginType - Plugin type.
|
|
27041
|
+
* @param patch - Partial deployment update.
|
|
27042
|
+
*/
|
|
27043
|
+
async safeUpdateStatus(tenantId, pluginType, patch) {
|
|
27044
|
+
try {
|
|
27045
|
+
await this.store.updateStatus(tenantId, pluginType, patch);
|
|
27046
|
+
} catch (error) {
|
|
27047
|
+
this.logger.warn?.(
|
|
27048
|
+
{
|
|
27049
|
+
event: "plugin.deployment.store_update_failed",
|
|
27050
|
+
tenantId,
|
|
27051
|
+
pluginType,
|
|
27052
|
+
error: errorMessage4(error)
|
|
27053
|
+
},
|
|
27054
|
+
"could not update plugin deployment status"
|
|
27055
|
+
);
|
|
27056
|
+
}
|
|
27057
|
+
}
|
|
27058
|
+
/**
|
|
27059
|
+
* Assemble a durable deployment record.
|
|
27060
|
+
*
|
|
27061
|
+
* @param input - Record fields (optional fields fall back to now/undefined).
|
|
27062
|
+
* @returns A fully populated deployment.
|
|
27063
|
+
*/
|
|
27064
|
+
buildRecord(input) {
|
|
27065
|
+
const now = this.nowIso();
|
|
27066
|
+
return {
|
|
27067
|
+
id: input.id,
|
|
27068
|
+
tenantId: input.tenantId,
|
|
27069
|
+
pluginType: input.pluginType,
|
|
27070
|
+
pluginVersion: input.pluginVersion,
|
|
27071
|
+
mode: input.mode,
|
|
27072
|
+
status: input.status,
|
|
27073
|
+
provider: input.provider,
|
|
27074
|
+
sandboxName: input.sandboxName,
|
|
27075
|
+
port: PLUGIN_GUEST_PORT,
|
|
27076
|
+
endpoint: input.endpoint,
|
|
27077
|
+
...input.image === void 0 ? {} : { imageRef: input.image },
|
|
27078
|
+
idleTimeoutMs: input.idleTimeoutMs,
|
|
27079
|
+
...input.lastHealthAt === void 0 ? {} : { lastHealthAt: input.lastHealthAt },
|
|
27080
|
+
...input.lastError === void 0 ? {} : { lastError: input.lastError },
|
|
27081
|
+
createdAt: input.createdAt ?? now,
|
|
27082
|
+
updatedAt: now
|
|
27083
|
+
};
|
|
27084
|
+
}
|
|
27085
|
+
/**
|
|
27086
|
+
* Compose the runtime registry key for a deployment.
|
|
27087
|
+
*
|
|
27088
|
+
* @param tenantId - Tenant id.
|
|
27089
|
+
* @param pluginType - Plugin type.
|
|
27090
|
+
* @returns `${tenantId}:${pluginType}`.
|
|
27091
|
+
*/
|
|
27092
|
+
registryKey(tenantId, pluginType) {
|
|
27093
|
+
return `${tenantId}:${pluginType}`;
|
|
27094
|
+
}
|
|
27095
|
+
/**
|
|
27096
|
+
* Current time as an ISO string from the injected clock.
|
|
27097
|
+
*
|
|
27098
|
+
* @returns ISO-8601 timestamp.
|
|
27099
|
+
*/
|
|
27100
|
+
nowIso() {
|
|
27101
|
+
return new Date(this.now()).toISOString();
|
|
27102
|
+
}
|
|
27103
|
+
};
|
|
27104
|
+
function errorMessage4(error) {
|
|
27105
|
+
return error instanceof Error ? error.message : String(error);
|
|
27106
|
+
}
|
|
27107
|
+
|
|
27108
|
+
// src/services/plugins/PluginDeploymentRuntimeStartup.ts
|
|
27109
|
+
var DEFAULT_PLUGIN_WATCHDOG_INTERVAL_MS = 3e4;
|
|
27110
|
+
var DEFAULT_PLUGIN_RECONCILE_DEADLINE_MS = 3e4;
|
|
27111
|
+
var DEFAULT_PLUGIN_MAX_PER_TENANT = 10;
|
|
27112
|
+
var DEFAULT_PLUGIN_MAX_GLOBAL = 100;
|
|
27113
|
+
var PLUGIN_DEPLOYMENT_RUNTIME_FLAG = "AXIOM_PLUGIN_DEPLOYMENT_RUNTIME";
|
|
27114
|
+
var PLUGIN_DEPLOYMENT_MAX_PER_TENANT_ENV = "PLUGIN_DEPLOYMENT_MAX_PER_TENANT";
|
|
27115
|
+
var PLUGIN_DEPLOYMENT_MAX_GLOBAL_ENV = "PLUGIN_DEPLOYMENT_MAX_GLOBAL";
|
|
27116
|
+
function isPluginDeploymentRuntimeEnabled(env = process.env) {
|
|
27117
|
+
const flag = env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG];
|
|
27118
|
+
if (flag === "false" || flag === "0") return false;
|
|
27119
|
+
if (flag === "true" || flag === "1") return true;
|
|
27120
|
+
return Boolean(env.MICROSANDBOX_SERVICE_BASE_URL);
|
|
27121
|
+
}
|
|
27122
|
+
function readPluginDeploymentLimits(env = process.env) {
|
|
27123
|
+
return {
|
|
27124
|
+
maxPerTenant: parsePositiveInt(
|
|
27125
|
+
env[PLUGIN_DEPLOYMENT_MAX_PER_TENANT_ENV],
|
|
27126
|
+
DEFAULT_PLUGIN_MAX_PER_TENANT
|
|
27127
|
+
),
|
|
27128
|
+
maxGlobal: parsePositiveInt(
|
|
27129
|
+
env[PLUGIN_DEPLOYMENT_MAX_GLOBAL_ENV],
|
|
27130
|
+
DEFAULT_PLUGIN_MAX_GLOBAL
|
|
27131
|
+
)
|
|
27132
|
+
};
|
|
27133
|
+
}
|
|
27134
|
+
function startPluginDeploymentRuntime(options) {
|
|
27135
|
+
const { lease, holder, logger: logger7 } = options;
|
|
27136
|
+
const forcedOn = process.env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG] === "true" || process.env[PLUGIN_DEPLOYMENT_RUNTIME_FLAG] === "1";
|
|
27137
|
+
const effectiveServiceBaseUrl = options.wiring.serviceBaseUrl ?? process.env.MICROSANDBOX_SERVICE_BASE_URL;
|
|
27138
|
+
if (forcedOn && !effectiveServiceBaseUrl) {
|
|
27139
|
+
logger7.warn(
|
|
27140
|
+
`${PLUGIN_DEPLOYMENT_RUNTIME_FLAG} is enabled but MICROSANDBOX_SERVICE_BASE_URL is empty; skipping plugin deployment runtime`,
|
|
27141
|
+
{ holder }
|
|
27142
|
+
);
|
|
27143
|
+
return { reconcile: Promise.resolve(), dispose: async () => void 0 };
|
|
27144
|
+
}
|
|
27145
|
+
const register = options.register ?? ((service3) => setPluginDeploymentService(service3));
|
|
27146
|
+
const setIntervalFn = options.setIntervalFn ?? setInterval;
|
|
27147
|
+
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
|
|
27148
|
+
const watchdogIntervalMs = options.watchdogIntervalMs ?? DEFAULT_PLUGIN_WATCHDOG_INTERVAL_MS;
|
|
27149
|
+
const reconcileDeadlineMs = options.reconcileDeadlineMs ?? DEFAULT_PLUGIN_RECONCILE_DEADLINE_MS;
|
|
27150
|
+
const createService = options.createService ?? ((deps2) => new PluginDeploymentService(deps2));
|
|
27151
|
+
const deps = {
|
|
27152
|
+
...options.wiring,
|
|
27153
|
+
lease,
|
|
27154
|
+
replicaId: holder
|
|
27155
|
+
};
|
|
27156
|
+
const service2 = createService(deps);
|
|
27157
|
+
register(service2);
|
|
27158
|
+
logger7.info("plugin deployment runtime started", { holder });
|
|
27159
|
+
const reconcile = runStartupReconcile(service2, reconcileDeadlineMs, logger7, holder);
|
|
27160
|
+
let held = false;
|
|
27161
|
+
let inFlight = false;
|
|
27162
|
+
const tick = async () => {
|
|
27163
|
+
if (inFlight) return;
|
|
27164
|
+
inFlight = true;
|
|
27165
|
+
try {
|
|
27166
|
+
if (held) {
|
|
27167
|
+
held = await renewLease(lease, holder, logger7);
|
|
27168
|
+
if (!held) {
|
|
27169
|
+
logger7.warn("plugin deployment lease lost; watchdog paused", { holder });
|
|
27170
|
+
}
|
|
27171
|
+
} else {
|
|
27172
|
+
held = await acquireLease(lease, holder, logger7);
|
|
27173
|
+
if (held) {
|
|
27174
|
+
logger7.info("plugin deployment lease acquired", { holder });
|
|
27175
|
+
}
|
|
27176
|
+
}
|
|
27177
|
+
if (!held) return;
|
|
27178
|
+
await runWatchdog(service2, holder, logger7);
|
|
27179
|
+
} finally {
|
|
27180
|
+
inFlight = false;
|
|
27181
|
+
}
|
|
27182
|
+
};
|
|
27183
|
+
const interval = setIntervalFn(() => {
|
|
27184
|
+
void tick();
|
|
27185
|
+
}, watchdogIntervalMs);
|
|
27186
|
+
return {
|
|
27187
|
+
reconcile,
|
|
27188
|
+
async dispose() {
|
|
27189
|
+
clearIntervalFn(interval);
|
|
27190
|
+
if (held) {
|
|
27191
|
+
try {
|
|
27192
|
+
await lease.release(PLUGIN_DEPLOYMENT_LEASE_KEY, holder);
|
|
27193
|
+
} catch (error) {
|
|
27194
|
+
logger7.warn("plugin deployment lease release failed", {
|
|
27195
|
+
holder,
|
|
27196
|
+
error: errorMessage5(error)
|
|
27197
|
+
});
|
|
27198
|
+
}
|
|
27199
|
+
held = false;
|
|
27200
|
+
}
|
|
27201
|
+
register(void 0);
|
|
27202
|
+
logger7.info("plugin deployment runtime stopped", { holder });
|
|
27203
|
+
}
|
|
27204
|
+
};
|
|
27205
|
+
}
|
|
27206
|
+
async function runStartupReconcile(service2, deadlineMs, logger7, holder) {
|
|
27207
|
+
try {
|
|
27208
|
+
await withDeadline(service2.startupReconcile(), deadlineMs);
|
|
27209
|
+
logger7.info("plugin deployment startup reconcile complete", { holder });
|
|
27210
|
+
} catch (error) {
|
|
27211
|
+
logger7.warn("plugin deployment startup reconcile failed", {
|
|
27212
|
+
holder,
|
|
27213
|
+
error: errorMessage5(error)
|
|
27214
|
+
});
|
|
27215
|
+
}
|
|
27216
|
+
}
|
|
27217
|
+
async function withDeadline(work, deadlineMs) {
|
|
27218
|
+
const settled = work.then(() => void 0);
|
|
27219
|
+
if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {
|
|
27220
|
+
await settled;
|
|
27221
|
+
return;
|
|
27222
|
+
}
|
|
27223
|
+
let timer;
|
|
27224
|
+
const deadline = new Promise((resolve3) => {
|
|
27225
|
+
timer = setTimeout(resolve3, deadlineMs);
|
|
27226
|
+
unrefTimer(timer);
|
|
27227
|
+
});
|
|
27228
|
+
try {
|
|
27229
|
+
await Promise.race([settled, deadline]);
|
|
27230
|
+
} finally {
|
|
27231
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
27232
|
+
}
|
|
27233
|
+
}
|
|
27234
|
+
async function acquireLease(lease, holder, logger7) {
|
|
27235
|
+
try {
|
|
27236
|
+
return await lease.tryAcquire(
|
|
27237
|
+
PLUGIN_DEPLOYMENT_LEASE_KEY,
|
|
27238
|
+
holder,
|
|
27239
|
+
PLUGIN_DEPLOYMENT_LEASE_TTL_MS
|
|
27240
|
+
);
|
|
27241
|
+
} catch (error) {
|
|
27242
|
+
logger7.warn("plugin deployment lease acquire failed", {
|
|
27243
|
+
holder,
|
|
27244
|
+
error: errorMessage5(error)
|
|
27245
|
+
});
|
|
27246
|
+
return false;
|
|
27247
|
+
}
|
|
27248
|
+
}
|
|
27249
|
+
async function renewLease(lease, holder, logger7) {
|
|
27250
|
+
try {
|
|
27251
|
+
return await lease.renew(
|
|
27252
|
+
PLUGIN_DEPLOYMENT_LEASE_KEY,
|
|
27253
|
+
holder,
|
|
27254
|
+
PLUGIN_DEPLOYMENT_LEASE_TTL_MS
|
|
27255
|
+
);
|
|
27256
|
+
} catch (error) {
|
|
27257
|
+
logger7.warn("plugin deployment lease renew failed", {
|
|
27258
|
+
holder,
|
|
27259
|
+
error: errorMessage5(error)
|
|
27260
|
+
});
|
|
27261
|
+
return false;
|
|
27262
|
+
}
|
|
27263
|
+
}
|
|
27264
|
+
async function runWatchdog(service2, holder, logger7) {
|
|
27265
|
+
try {
|
|
27266
|
+
await service2.healthWatchdog();
|
|
27267
|
+
} catch (error) {
|
|
27268
|
+
logger7.warn("plugin deployment health watchdog failed", {
|
|
27269
|
+
holder,
|
|
27270
|
+
error: errorMessage5(error)
|
|
27271
|
+
});
|
|
27272
|
+
}
|
|
27273
|
+
}
|
|
27274
|
+
function unrefTimer(timer) {
|
|
27275
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
27276
|
+
timer.unref?.();
|
|
27277
|
+
}
|
|
27278
|
+
}
|
|
27279
|
+
function parsePositiveInt(value, fallback) {
|
|
27280
|
+
if (value === void 0 || value.trim() === "") return fallback;
|
|
27281
|
+
const parsed = Number(value);
|
|
27282
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
27283
|
+
return Math.floor(parsed);
|
|
27284
|
+
}
|
|
27285
|
+
function errorMessage5(error) {
|
|
27286
|
+
return error instanceof Error ? error.message : String(error);
|
|
27287
|
+
}
|
|
27288
|
+
|
|
25787
27289
|
// src/services/multiReplicaReadiness.ts
|
|
25788
|
-
import { getStoreLattice as
|
|
25789
|
-
var SHARED_STORE_KINDS = [
|
|
27290
|
+
import { getStoreLattice as getStoreLattice29 } from "@axiom-lattice/core";
|
|
27291
|
+
var SHARED_STORE_KINDS = [
|
|
27292
|
+
"lease",
|
|
27293
|
+
"outbox",
|
|
27294
|
+
"threadMessageQueue",
|
|
27295
|
+
"task",
|
|
27296
|
+
"taskWorkItem",
|
|
27297
|
+
"channelInstallation"
|
|
27298
|
+
];
|
|
25790
27299
|
function storeClassName(kind) {
|
|
25791
27300
|
try {
|
|
25792
|
-
return
|
|
27301
|
+
return getStoreLattice29("default", kind).store.constructor?.name;
|
|
25793
27302
|
} catch {
|
|
25794
27303
|
return void 0;
|
|
25795
27304
|
}
|
|
@@ -26058,6 +27567,7 @@ app.setErrorHandler((error, request, reply) => {
|
|
|
26058
27567
|
error: error.message || "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"
|
|
26059
27568
|
});
|
|
26060
27569
|
});
|
|
27570
|
+
var shutdownHandlersInstalled = false;
|
|
26061
27571
|
function getConfiguredSandboxProvider() {
|
|
26062
27572
|
const sandboxProviderType = process.env.SANDBOX_PROVIDER_TYPE || "microsandbox-remote";
|
|
26063
27573
|
return createSandboxProvider({
|
|
@@ -26076,7 +27586,7 @@ function getConfiguredSandboxProvider() {
|
|
|
26076
27586
|
}
|
|
26077
27587
|
async function restoreMcpConnections() {
|
|
26078
27588
|
try {
|
|
26079
|
-
const storeLattice =
|
|
27589
|
+
const storeLattice = getStoreLattice30("default", "mcp");
|
|
26080
27590
|
const store = storeLattice.store;
|
|
26081
27591
|
if (!store) {
|
|
26082
27592
|
logger6.info("MCP store not configured, skipping connection restoration");
|
|
@@ -26131,9 +27641,9 @@ var start = async (config) => {
|
|
|
26131
27641
|
let projectRoomRealtimeRuntime;
|
|
26132
27642
|
if (projectRoomRealtimeMode === "single-gateway") {
|
|
26133
27643
|
projectRoomRealtimeRuntime = createProjectRoomRealtimeRuntime({
|
|
26134
|
-
tasks:
|
|
26135
|
-
rooms:
|
|
26136
|
-
memberships:
|
|
27644
|
+
tasks: getStoreLattice30("default", "task").store,
|
|
27645
|
+
rooms: getStoreLattice30("default", "projectRoom").store,
|
|
27646
|
+
memberships: getStoreLattice30("default", "projectBotMembership").store,
|
|
26137
27647
|
log: { warn: (message, context) => logger6.warn(message, context) }
|
|
26138
27648
|
});
|
|
26139
27649
|
}
|
|
@@ -26143,10 +27653,9 @@ var start = async (config) => {
|
|
|
26143
27653
|
const { wechatChannelAdapter } = await import("./WechatChannelAdapter-7WHE5UGY.mjs");
|
|
26144
27654
|
adapterRegistry.register(wechatChannelAdapter);
|
|
26145
27655
|
try {
|
|
26146
|
-
const { getStoreLattice: getStore11 } = await import("@axiom-lattice/core");
|
|
26147
27656
|
const infrastructure = createChannelInfrastructure({
|
|
26148
27657
|
adapterRegistry,
|
|
26149
|
-
getStore: ((kind) =>
|
|
27658
|
+
getStore: ((kind) => getStoreLattice30("default", kind).store),
|
|
26150
27659
|
trustedDispatcher: createTrustedAgentMessageDispatcher2((scope) => agentInstanceManager9.getAgent(scope)),
|
|
26151
27660
|
middlewares: [
|
|
26152
27661
|
createDeduplicationMiddleware(),
|
|
@@ -26176,7 +27685,7 @@ var start = async (config) => {
|
|
|
26176
27685
|
}
|
|
26177
27686
|
setEvalRunService(evalRunner);
|
|
26178
27687
|
try {
|
|
26179
|
-
const menuStore =
|
|
27688
|
+
const menuStore = getStoreLattice30("default", "menu").store;
|
|
26180
27689
|
setMenuRegistry(menuStore);
|
|
26181
27690
|
logger6.info("Menu registry initialized");
|
|
26182
27691
|
} catch {
|
|
@@ -26191,18 +27700,18 @@ var start = async (config) => {
|
|
|
26191
27700
|
projectRoomRealtimeRuntime
|
|
26192
27701
|
});
|
|
26193
27702
|
try {
|
|
26194
|
-
const { A2AAuthService, parseEnvKeys } = await import("./A2AAuthService-7UGQ7GMW.mjs");
|
|
27703
|
+
const { A2AAuthService: A2AAuthService2, parseEnvKeys: parseEnvKeys2 } = await import("./A2AAuthService-7UGQ7GMW.mjs");
|
|
26195
27704
|
const { registerA2AStandardRoutes } = await import("./a2a-standard-PBYPNHOA.mjs");
|
|
26196
|
-
const a2a = await import("./a2a-
|
|
26197
|
-
const a2aKeyStore =
|
|
26198
|
-
const assistantStore =
|
|
26199
|
-
const projectStore =
|
|
26200
|
-
const taskStore =
|
|
26201
|
-
const authService = new
|
|
27705
|
+
const a2a = await import("./a2a-BHBLACJV.mjs");
|
|
27706
|
+
const a2aKeyStore = getStoreLattice30("default", "a2aApiKey").store;
|
|
27707
|
+
const assistantStore = getStoreLattice30("default", "assistant").store;
|
|
27708
|
+
const projectStore = getStoreLattice30("default", "project").store;
|
|
27709
|
+
const taskStore = getStoreLattice30("default", "task").store;
|
|
27710
|
+
const authService = new A2AAuthService2({
|
|
26202
27711
|
keyStore: a2aKeyStore,
|
|
26203
27712
|
assistantStore,
|
|
26204
27713
|
projectStore,
|
|
26205
|
-
envKeyMap:
|
|
27714
|
+
envKeyMap: parseEnvKeys2(process.env.A2A_API_KEYS || ""),
|
|
26206
27715
|
allowUnauthenticated: process.env.A2A_ALLOW_UNAUTHENTICATED === "true"
|
|
26207
27716
|
});
|
|
26208
27717
|
await authService.refresh();
|
|
@@ -26222,12 +27731,12 @@ var start = async (config) => {
|
|
|
26222
27731
|
});
|
|
26223
27732
|
}
|
|
26224
27733
|
try {
|
|
26225
|
-
const { registerOpenRoutes } = await import("./open-
|
|
27734
|
+
const { registerOpenRoutes } = await import("./open-FJ3BY3CL.mjs");
|
|
26226
27735
|
const { registerOpenSessionRoutes } = await import("./open-session-2Y7XJJW2.mjs");
|
|
26227
27736
|
const { registerOpenCatalogRoutes } = await import("./open-catalog-SZQ435AY.mjs");
|
|
26228
27737
|
const { registerOpenFilesRoutes } = await import("./open-files-X3HXVEEM.mjs");
|
|
26229
27738
|
const { registerOpenAuditRoutes } = await import("./open-audit-UJ3NXCGW.mjs");
|
|
26230
|
-
const { registerOpenExposureRoutes } = await import("./open-exposure-
|
|
27739
|
+
const { registerOpenExposureRoutes } = await import("./open-exposure-OX5NKWDG.mjs");
|
|
26231
27740
|
const { openLattice } = await import("./openLattice-Q6M5S6CY.mjs");
|
|
26232
27741
|
const { createKbProvider } = await import("./kbProvider-2D6DZ34N.mjs");
|
|
26233
27742
|
const { createOpenSandboxProvider } = await import("./sandboxProvider-DUYWHYJN.mjs");
|
|
@@ -26236,8 +27745,8 @@ var start = async (config) => {
|
|
|
26236
27745
|
const { CollectionService: CollectionService2 } = await import("./collection_service-RTRJEQFL.mjs");
|
|
26237
27746
|
openLattice.register(createKbProvider(new CollectionService2()));
|
|
26238
27747
|
openLattice.register(createOpenSandboxProvider());
|
|
26239
|
-
const threadStore =
|
|
26240
|
-
const assistantStore =
|
|
27748
|
+
const threadStore = getStoreLattice30("default", "thread").store;
|
|
27749
|
+
const assistantStore = getStoreLattice30("default", "assistant").store;
|
|
26241
27750
|
openLattice.register(
|
|
26242
27751
|
createAgentProvider({
|
|
26243
27752
|
threadStore,
|
|
@@ -26275,9 +27784,53 @@ var start = async (config) => {
|
|
|
26275
27784
|
sandboxLatticeManager2.registerLattice("default", getConfiguredSandboxProvider());
|
|
26276
27785
|
logger6.info("Registered sandbox manager from env configuration");
|
|
26277
27786
|
}
|
|
27787
|
+
if (isPluginDeploymentRuntimeEnabled()) {
|
|
27788
|
+
try {
|
|
27789
|
+
const pluginDeploymentStore = getStoreLattice30("default", "pluginDeployment").store;
|
|
27790
|
+
const tenantStore = getStoreLattice30("default", "tenant").store;
|
|
27791
|
+
const leaseStore2 = getStoreLattice30("default", "lease").store;
|
|
27792
|
+
const listAllDeployments = async () => {
|
|
27793
|
+
const tenants = await tenantStore.getAllTenants();
|
|
27794
|
+
const lists = await Promise.all(
|
|
27795
|
+
tenants.map(
|
|
27796
|
+
(tenant2) => pluginDeploymentStore.listByTenant(tenant2.id).catch(() => [])
|
|
27797
|
+
)
|
|
27798
|
+
);
|
|
27799
|
+
return lists.flat();
|
|
27800
|
+
};
|
|
27801
|
+
const pluginDeploymentRuntime = startPluginDeploymentRuntime({
|
|
27802
|
+
wiring: {
|
|
27803
|
+
store: pluginDeploymentStore,
|
|
27804
|
+
sandboxManager: getSandBoxManager4(),
|
|
27805
|
+
catalog: getSandboxPluginCatalog3(),
|
|
27806
|
+
installPluginHost,
|
|
27807
|
+
limits: readPluginDeploymentLimits(),
|
|
27808
|
+
serviceKey: process.env.MICROSANDBOX_API_KEY,
|
|
27809
|
+
listAllDeployments,
|
|
27810
|
+
// The service logs fields-first; the gateway logger is message-first.
|
|
27811
|
+
logger: {
|
|
27812
|
+
info: (fields, message) => logger6.info(message ?? "plugin deployment", fields),
|
|
27813
|
+
warn: (fields, message) => logger6.warn(message ?? "plugin deployment", fields),
|
|
27814
|
+
error: (fields, message) => logger6.error(message ?? "plugin deployment", fields)
|
|
27815
|
+
}
|
|
27816
|
+
},
|
|
27817
|
+
lease: leaseStore2,
|
|
27818
|
+
holder: getReplicaId7(),
|
|
27819
|
+
logger: logger6
|
|
27820
|
+
});
|
|
27821
|
+
app.addHook("onClose", async () => {
|
|
27822
|
+
await pluginDeploymentRuntime.dispose();
|
|
27823
|
+
});
|
|
27824
|
+
logger6.info("Plugin deployment runtime started");
|
|
27825
|
+
} catch (err) {
|
|
27826
|
+
logger6.warn("Plugin deployment runtime unavailable", {
|
|
27827
|
+
error: err instanceof Error ? err.message : String(err)
|
|
27828
|
+
});
|
|
27829
|
+
}
|
|
27830
|
+
}
|
|
26278
27831
|
try {
|
|
26279
27832
|
const { ResourceController } = await import("./resources-2L3KSUUU.mjs");
|
|
26280
|
-
const sharedResourceStore =
|
|
27833
|
+
const sharedResourceStore = getStoreLattice30("default", "sharedResource").store;
|
|
26281
27834
|
const cache = new TokenCache();
|
|
26282
27835
|
const resourceController = new ResourceController({
|
|
26283
27836
|
store: sharedResourceStore,
|
|
@@ -26293,12 +27846,20 @@ var start = async (config) => {
|
|
|
26293
27846
|
});
|
|
26294
27847
|
}
|
|
26295
27848
|
if (channelDeps && process.env.CHANNELS_ENABLED !== "false") {
|
|
26296
|
-
const { connectAllChannels } = await import("@axiom-lattice/core");
|
|
26297
27849
|
try {
|
|
26298
|
-
|
|
26299
|
-
(
|
|
26300
|
-
|
|
26301
|
-
|
|
27850
|
+
const channelLeader = new ChannelConnectionLeader({
|
|
27851
|
+
leaseStore: getStoreLattice30("default", "lease").store,
|
|
27852
|
+
owner: getReplicaId7(),
|
|
27853
|
+
connect: () => connectAllChannels((channel) => adapterRegistry.get(channel), {
|
|
27854
|
+
deps: { router: channelDeps.router }
|
|
27855
|
+
}),
|
|
27856
|
+
disconnect: () => disconnectAllChannels((channel) => adapterRegistry.get(channel)),
|
|
27857
|
+
log: { warn: (message, context) => logger6.warn(message, context) }
|
|
27858
|
+
});
|
|
27859
|
+
app.addHook("onClose", async () => {
|
|
27860
|
+
await channelLeader.stop();
|
|
27861
|
+
});
|
|
27862
|
+
channelLeader.start();
|
|
26302
27863
|
} catch (err) {
|
|
26303
27864
|
logger6.error("Failed to start channel connections", {
|
|
26304
27865
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -26306,24 +27867,24 @@ var start = async (config) => {
|
|
|
26306
27867
|
}
|
|
26307
27868
|
}
|
|
26308
27869
|
const target_port = config?.port || Number(process.env.PORT) || 4001;
|
|
26309
|
-
configureCoordinationLeaseStore(
|
|
26310
|
-
const outboxStore =
|
|
27870
|
+
configureCoordinationLeaseStore(getStoreLattice30("default", "lease").store);
|
|
27871
|
+
const outboxStore = getStoreLattice30("default", "outbox").store;
|
|
26311
27872
|
taskLifecycleNotifier3.setOutbox(outboxStore);
|
|
26312
27873
|
projectTaskAdmissionNotifier2.setOutbox(outboxStore);
|
|
26313
|
-
configurePluginCatalogSync({ outbox: outboxStore, owner:
|
|
27874
|
+
configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId7() });
|
|
26314
27875
|
bridgeAssistantEvents(
|
|
26315
27876
|
(event, cb) => eventBus4.subscribe(event, cb),
|
|
26316
27877
|
outboxStore,
|
|
26317
|
-
|
|
27878
|
+
getReplicaId7()
|
|
26318
27879
|
);
|
|
26319
27880
|
if (projectRoomRealtimeRuntime) {
|
|
26320
27881
|
projectRoomRealtimeRuntime.broker.setBroadcastHook((scope, serialized, sourceKey) => {
|
|
26321
|
-
void publishRoomEvent(outboxStore,
|
|
27882
|
+
void publishRoomEvent(outboxStore, getReplicaId7(), scope, serialized, sourceKey);
|
|
26322
27883
|
});
|
|
26323
27884
|
}
|
|
26324
27885
|
const outboxPoller = new OutboxPoller({
|
|
26325
27886
|
store: outboxStore,
|
|
26326
|
-
owner:
|
|
27887
|
+
owner: getReplicaId7(),
|
|
26327
27888
|
handlers: {
|
|
26328
27889
|
"task-lifecycle": (payload) => taskLifecycleNotifier3.deliverFromOutbox(payload),
|
|
26329
27890
|
"project-task-admission": (payload) => projectTaskAdmissionNotifier2.deliverFromOutbox(payload),
|
|
@@ -26331,14 +27892,20 @@ var start = async (config) => {
|
|
|
26331
27892
|
if (projectRoomRealtimeRuntime) ingestRoomEvent(payload, projectRoomRealtimeRuntime.broker);
|
|
26332
27893
|
},
|
|
26333
27894
|
"assistant-changed": (payload) => handleAssistantChanged(payload, (tenantId) => agentLatticeManager5.initializeStoredAssistantsForTenant(tenantId)),
|
|
27895
|
+
[A2A_KEY_CHANGED_TOPIC]: (payload) => handleA2AKeyChanged(payload, () => refreshStoreKeyMap()),
|
|
26334
27896
|
[MCP_CONFIG_CHANGED_TOPIC]: (payload) => reconcileMcpConfigChange(payload, {
|
|
26335
|
-
store:
|
|
27897
|
+
store: getStoreLattice30("default", "mcp").store,
|
|
26336
27898
|
manager: mcpManager3,
|
|
26337
27899
|
convertToConnection: convertToConnection2
|
|
26338
27900
|
}),
|
|
26339
27901
|
[PLUGIN_CATALOG_CHANGED_TOPIC]: async (payload) => {
|
|
26340
27902
|
const tenantId = payload?.tenantId;
|
|
26341
27903
|
if (typeof tenantId === "string" && tenantId.length > 0) getSandboxPluginCatalog3().invalidate(tenantId);
|
|
27904
|
+
},
|
|
27905
|
+
[MODEL_PROVIDER_CHANGED_TOPIC]: async (payload) => {
|
|
27906
|
+
const controller = routes.modelProviderController;
|
|
27907
|
+
if (!controller) return;
|
|
27908
|
+
await handleModelProviderChanged(payload, (tenantId, providerId2) => controller.reconcileProvider(tenantId, providerId2));
|
|
26342
27909
|
}
|
|
26343
27910
|
},
|
|
26344
27911
|
log: logger6
|
|
@@ -26348,7 +27915,7 @@ var start = async (config) => {
|
|
|
26348
27915
|
} catch (err) {
|
|
26349
27916
|
logger6.warn("Outbox poller failed to start", { error: err instanceof Error ? err.message : String(err) });
|
|
26350
27917
|
}
|
|
26351
|
-
const leaseStore =
|
|
27918
|
+
const leaseStore = getStoreLattice30("default", "lease").store;
|
|
26352
27919
|
if (leaseStore.constructor?.name === "InMemoryLeaseStore") {
|
|
26353
27920
|
logger6.warn(
|
|
26354
27921
|
"Startup recovery leader gate uses a non-distributed lease store (per-process only); configure a shared 'lease' store for multi-replica deployments",
|
|
@@ -26365,9 +27932,9 @@ var start = async (config) => {
|
|
|
26365
27932
|
const { createOrphanRecoveryRuntime: createOrphanRecoveryRuntime2 } = await import("./OrphanRecoveryRuntime-BEFILJMU.mjs");
|
|
26366
27933
|
const { createRecoveryCallerDeps: createRecoveryCallerDeps2 } = await import("./RecoveryCallerDeps-6SJUYJ5S.mjs");
|
|
26367
27934
|
const runtime = createOrphanRecoveryRuntime2({
|
|
26368
|
-
taskStore:
|
|
26369
|
-
workItemStore:
|
|
26370
|
-
tenantStore:
|
|
27935
|
+
taskStore: getStoreLattice30("default", "task").store,
|
|
27936
|
+
workItemStore: getStoreLattice30("default", "taskWorkItem").store,
|
|
27937
|
+
tenantStore: getStoreLattice30("default", "tenant").store,
|
|
26371
27938
|
...createRecoveryCallerDeps2(),
|
|
26372
27939
|
log: logger6
|
|
26373
27940
|
});
|
|
@@ -26398,17 +27965,41 @@ var start = async (config) => {
|
|
|
26398
27965
|
log: logger6
|
|
26399
27966
|
});
|
|
26400
27967
|
outboxRetention.start();
|
|
27968
|
+
let agentTaskConsumer;
|
|
26401
27969
|
const queueServiceConfig = config?.queueServiceConfig;
|
|
26402
27970
|
if (queueServiceConfig) {
|
|
26403
27971
|
setQueueServiceType(queueServiceConfig.type);
|
|
26404
27972
|
if (queueServiceConfig.defaultStartPollingQueue) {
|
|
26405
|
-
|
|
27973
|
+
agentTaskConsumer = new AgentTaskConsumer(target_port);
|
|
26406
27974
|
agentTaskConsumer.startPollingQueue();
|
|
26407
27975
|
}
|
|
26408
27976
|
}
|
|
26409
27977
|
restoreMcpConnections().catch((error) => {
|
|
26410
27978
|
logger6.error("MCP connection restoration failed", { error });
|
|
26411
27979
|
});
|
|
27980
|
+
if (config?.gracefulShutdown !== false && !shutdownHandlersInstalled) {
|
|
27981
|
+
shutdownHandlersInstalled = true;
|
|
27982
|
+
const gracefulShutdown = createGracefulShutdown({
|
|
27983
|
+
timeoutMs: config?.gracefulShutdownTimeoutMs ?? 1e4,
|
|
27984
|
+
log: {
|
|
27985
|
+
info: (message, context) => logger6.info(message, context),
|
|
27986
|
+
warn: (message, context) => logger6.warn(message, context)
|
|
27987
|
+
},
|
|
27988
|
+
steps: [
|
|
27989
|
+
async () => {
|
|
27990
|
+
await Promise.all(
|
|
27991
|
+
agentInstanceManager9.getAllAgents().map((agent) => agent.abort().catch(() => void 0))
|
|
27992
|
+
);
|
|
27993
|
+
},
|
|
27994
|
+
() => agentTaskConsumer?.stopPollingQueue(),
|
|
27995
|
+
() => outboxPoller.stop(),
|
|
27996
|
+
() => outboxRetention.stop(),
|
|
27997
|
+
() => app.close()
|
|
27998
|
+
]
|
|
27999
|
+
});
|
|
28000
|
+
process.once("SIGTERM", () => void gracefulShutdown("SIGTERM"));
|
|
28001
|
+
process.once("SIGINT", () => void gracefulShutdown("SIGINT"));
|
|
28002
|
+
}
|
|
26412
28003
|
} catch (err) {
|
|
26413
28004
|
logger6.error("Server start failed", { error: err });
|
|
26414
28005
|
process.exit(1);
|
|
@@ -26422,13 +28013,22 @@ var LatticeGateway = {
|
|
|
26422
28013
|
AgentTaskConsumer
|
|
26423
28014
|
};
|
|
26424
28015
|
export {
|
|
28016
|
+
A2AAuthService,
|
|
28017
|
+
A2A_KEY_CHANGED_TOPIC,
|
|
26425
28018
|
LatticeGateway,
|
|
28019
|
+
MODEL_PROVIDER_CHANGED_TOPIC,
|
|
28020
|
+
ModelProviderService,
|
|
26426
28021
|
ProjectRoomEventBroker,
|
|
26427
28022
|
ROOM_EVENT_TOPIC,
|
|
26428
28023
|
createOrphanRecoveryRuntime,
|
|
26429
28024
|
createRecoveryCallerDeps,
|
|
26430
28025
|
createTaskRunLiveness,
|
|
28026
|
+
handleA2AKeyChanged,
|
|
28027
|
+
handleModelProviderChanged,
|
|
26431
28028
|
ingestRoomEvent,
|
|
28029
|
+
parseEnvKeys,
|
|
28030
|
+
publishA2AKeyChanged,
|
|
28031
|
+
publishModelProviderChanged,
|
|
26432
28032
|
publishRoomEvent,
|
|
26433
28033
|
recoverTask,
|
|
26434
28034
|
recoverTopCaller,
|