@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.
Files changed (38) hide show
  1. package/dist/a2a-BHBLACJV.mjs +17 -0
  2. package/dist/a2a-BHBLACJV.mjs.map +1 -0
  3. package/dist/{chunk-NPHK4SZ6.mjs → chunk-FGSFOXIX.mjs} +2 -2
  4. package/dist/{a2a-FGYX2HH3.mjs → chunk-HYYLYIMH.mjs} +59 -15
  5. package/dist/chunk-HYYLYIMH.mjs.map +1 -0
  6. package/dist/{chunk-73QZQQOT.mjs → chunk-MWWLJMYN.mjs} +3 -2
  7. package/dist/chunk-MWWLJMYN.mjs.map +1 -0
  8. package/dist/embed/{embed-page-AgentDetailSideApp-S5N76UPU.js → embed-page-AgentDetailSideApp-UMITWVYQ.js} +1 -1
  9. package/dist/embed/{embed-page-AgentGovernanceWorkbench-QFMP2M73.js → embed-page-AgentGovernanceWorkbench-UD7RE7PT.js} +1 -1
  10. package/dist/embed/{embed-page-MessageGraph-H3HPZKOH.js → embed-page-MessageGraph-XQWKYDUB.js} +1 -1
  11. package/dist/embed/{embed-page-ReactChart-ZFMANJCT.js → embed-page-ReactChart-LW3GWMY2.js} +1 -1
  12. package/dist/embed/{embed-page-SkillFlow-HY7PD43P.js → embed-page-SkillFlow-X3D346DE.js} +1 -1
  13. package/dist/embed/{embed-page-TasksSideApp-QKEXMHCT.js → embed-page-TasksSideApp-SUMX2GJF.js} +1 -1
  14. package/dist/embed/{embed-page-TeamWorkspace-ZFVAFZXZ.js → embed-page-TeamWorkspace-QMIX2TP2.js} +1 -1
  15. package/dist/embed/{embed-page-TopologyRuntimeView-2BS4OYEZ.js → embed-page-TopologyRuntimeView-ZP6NWNS7.js} +1 -1
  16. package/dist/embed/{embed-page-WorkflowAutomationView-WUI7BSQP.js → embed-page-WorkflowAutomationView-4HD3ISK6.js} +1 -1
  17. package/dist/embed/{embed-page-chunk-4T4QMOUS.js → embed-page-chunk-3VIEL6KL.js} +1 -1
  18. package/dist/embed/{embed-page-chunk-5WYDCL3B.js → embed-page-chunk-MFFCCHGW.js} +158 -158
  19. package/dist/embed/{embed-page-chunk-6HM7ZERK.js → embed-page-chunk-QCKYIPFY.js} +1 -1
  20. package/dist/embed/{embed-page-chunk-XKQ6YPUW.js → embed-page-chunk-QGAGZMX2.js} +1 -1
  21. package/dist/embed/{embed-page-chunk-VJTO6SG6.js → embed-page-chunk-SX7YO5JA.js} +1 -1
  22. package/dist/embed/{embed-page-chunk-7ZBZKXBU.js → embed-page-chunk-T7XXE35M.js} +4 -4
  23. package/dist/embed/{embed-page-chunk-MSHEVUK3.js → embed-page-chunk-YAJUWBF2.js} +1 -1
  24. package/dist/embed/embed-page.js +1 -1
  25. package/dist/index.d.mts +431 -5
  26. package/dist/index.d.ts +431 -5
  27. package/dist/index.js +2939 -1294
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +1811 -211
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{open-UVKMONVE.mjs → open-FJ3BY3CL.mjs} +3 -3
  32. package/dist/{open-exposure-VU227IO3.mjs → open-exposure-OX5NKWDG.mjs} +6 -6
  33. package/package.json +7 -7
  34. package/dist/a2a-FGYX2HH3.mjs.map +0 -1
  35. package/dist/chunk-73QZQQOT.mjs.map +0 -1
  36. /package/dist/{chunk-NPHK4SZ6.mjs.map → chunk-FGSFOXIX.mjs.map} +0 -0
  37. /package/dist/{open-UVKMONVE.mjs.map → open-FJ3BY3CL.mjs.map} +0 -0
  38. /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-73QZQQOT.mjs";
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, errorMessage4) {
5126
- const encodedError = encodeURIComponent(errorMessage4);
5127
- return ERROR_HTML.replace("{assistantId}", assistantId).replace("{threadId}", threadId).replace("{vmIsolation}", vmIsolation).replace("{errorMessage}", errorMessage4);
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
- const { modelLatticeManager: modelLatticeManager4 } = await import("@axiom-lattice/core");
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 { modelLatticeManager: modelLatticeManager4 } = await import("@axiom-lattice/core");
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
- const { modelLatticeManager: modelLatticeManager4 } = await import("@axiom-lattice/core");
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 { getStoreLattice: getStoreLattice30 } = await import("@axiom-lattice/core");
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 errorMessage4 = error instanceof Error ? error.message.toLowerCase() : "";
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 (errorMessage4.includes("duplicate") || errorMessage4.includes("unique constraint") || errorCode === "23505") {
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 getStoreLattice15 } from "@axiom-lattice/core";
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 getStoreLattice15("default", "channelInstallation").store;
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 getStoreLattice16 } from "@axiom-lattice/core";
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 = getStoreLattice16("default", "assistant");
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 = getStoreLattice16("default", "assistant");
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
- getStoreLattice as getStoreLattice17,
11919
+ getReplicaId as getReplicaId6,
11920
+ getStoreLattice as getStoreLattice18,
11903
11921
  ManagedTaskStartError,
11904
- modelLatticeManager as modelLatticeManager3,
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
- }, { input: { id: messageId, message: buildTaskPrompt(task, mode) } }, {
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
- return this.toPublicEntry(created, 0);
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 — a synthetic active bot per available tenant agent.
17218
- * @remarks Eligibility is a project property: `kind === "public"` treats every
17219
- * available agent as an implicit member; every other kind returns the stored
17220
- * roster unchanged. The synthetic records are never persisted.
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
- return agents.map((agent) => publicProjectBot(project, room.id, agent, now));
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 && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
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
- this.requireCurrent(target, input.expectedUpdatedAt);
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" }, input.expectedUpdatedAt);
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
- this.requireCurrent(target, expected);
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, expected);
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 && ["ProjectBotMembershipIdConflictError", "DuplicateProjectBotMembershipError"].includes(error.name)) throw new ProjectRosterServiceError("ROSTER_CONFLICT", 409, "Roster membership conflict");
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
- const target = await this.deps.membershipStore.findById(actor3.tenantId, id3);
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 publicProjectBot(project, roomId, agent, now) {
17456
- const id3 = `public-project-bot:${project.id}:${agent.assistantId}`;
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: agent.name,
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, snapshot, room) {
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
- const membership = await this.deps.resolveMembership(input);
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
- membershipId: id2,
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, membershipId: body.membershipId },
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 = getStoreLattice17("default", "agentWebApp").store;
22375
- const assistantStore = getStoreLattice17("default", "assistant").store;
22376
- const projectStore = getStoreLattice17("default", "project").store;
22377
- const projectMembershipStore = getStoreLattice17("default", "projectMembership").store;
22378
- const userTenantLinkStore = getStoreLattice17("default", "userTenantLink").store;
22379
- const projectRoomStore = getStoreLattice17("default", "projectRoom").store;
22380
- const projectBotMembershipStore = getStoreLattice17("default", "projectBotMembership").store;
22381
- const projectRoomMessageStore = getStoreLattice17("default", "projectRoomMessage").store;
22382
- const projectRoomReadStateStore = getStoreLattice17("default", "projectRoomReadState").store;
22383
- const userPushSubscriptionStore = getStoreLattice17("default", "userPushSubscription").store;
22384
- const threadStore = getStoreLattice17("default", "thread").store;
22385
- const taskStore = getStoreLattice17("default", "task").store;
22386
- const taskWorkItemStore = getStoreLattice17("default", "taskWorkItem").store;
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 getStoreLattice17("default", "tenant").store.getTenantById(tenantId);
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
- ...coordinatorWakeNotifier === void 0 ? {} : { coordinatorWake: coordinatorWakeNotifier }
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) => getStoreLattice17("default", "user").store.getUserById(id3).then((u) => u ? { name: u.name } : null)
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) => getStoreLattice17("default", "workspace").store.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) => getStoreLattice17("default", "user").store.getUserById(id3).then((u) => u ? { id: u.id, name: u.name, email: u.email } : null)
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: getStoreLattice17("default", "capabilityBundle").store,
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: getStoreLattice17("default", "modelProvider").store,
23183
+ providerStore: getStoreLattice18("default", "modelProvider").store,
22667
23184
  userTenantLinkStore,
22668
23185
  registry: {
22669
- register: (tenantId, modelKey2, config) => modelLatticeManager3.registerLatticeWithTenant(tenantId, modelKey2, config),
22670
- has: (tenantId, modelKey2) => modelLatticeManager3.hasLatticeWithTenant(tenantId, modelKey2),
22671
- remove: (tenantId, modelKey2) => modelLatticeManager3.removeLatticeWithTenant(tenantId, modelKey2),
22672
- listRegistered: (tenantId) => modelLatticeManager3.getLatticeKeysByTenant(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) => modelLatticeManager3.hasLattice(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: getStoreLattice17("default", "thread").store,
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) => modelLatticeManager3.hasLattice(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: getStoreLattice17("default", "tenant").store,
23153
- workspaces: getStoreLattice17("default", "workspace").store,
23154
- queue: getStoreLattice17("default", "threadMessageQueue").store
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
- tasks: input.taskStore,
23205
- projects: input.projectStore,
23206
- rooms: input.projectRoomStore,
23207
- bots: input.projectBotMembershipStore,
23208
- threads,
23209
- lifecycle,
23210
- dispatcher: createTrustedAgentMessageDispatcher((scope) => agentInstanceManager6.getAgent(scope)),
23211
- queue: stores.queue,
23212
- agentFor
23213
- }, {
23214
- settlementTimeoutMs: PROJECT_TASK_SETTLEMENT_TIMEOUT_MS,
23215
- lifecycleSettlementTimeoutMs: PROJECT_TASK_LIFECYCLE_SETTLEMENT_TIMEOUT_MS
23216
- });
23217
- const observer = new ProjectTaskObserver({
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 getStoreLattice18 } from "@axiom-lattice/core";
24056
+ import { getStoreLattice as getStoreLattice19 } from "@axiom-lattice/core";
23533
24057
  function getStore2() {
23534
- return getStoreLattice18("default", "assistant").store;
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 getStoreLattice19 } from "@axiom-lattice/core";
24138
+ import { getStoreLattice as getStoreLattice20 } from "@axiom-lattice/core";
23615
24139
  function getStore3() {
23616
- return getStoreLattice19("default", "database").store;
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 getStoreLattice20 } from "@axiom-lattice/core";
24220
+ import { getStoreLattice as getStoreLattice21 } from "@axiom-lattice/core";
23697
24221
  function getStore4() {
23698
- return getStoreLattice20("default", "metrics").store;
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 getStoreLattice21 } from "@axiom-lattice/core";
24302
+ import { getStoreLattice as getStoreLattice22 } from "@axiom-lattice/core";
23779
24303
  function getStore5() {
23780
- return getStoreLattice21("default", "mcp").store;
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 getStoreLattice22 } from "@axiom-lattice/core";
24387
+ import { getStoreLattice as getStoreLattice23 } from "@axiom-lattice/core";
23864
24388
  function getStore6() {
23865
- return getStoreLattice22("default", "connection").store;
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 getStoreLattice23 } from "@axiom-lattice/core";
24474
+ import { getStoreLattice as getStoreLattice24 } from "@axiom-lattice/core";
23951
24475
  function getStore7() {
23952
- return getStoreLattice23("default", "collection").store;
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 getStoreLattice24 } from "@axiom-lattice/core";
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 getStoreLattice24("default", "channelInstallation").store;
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 getStoreLattice25 } from "@axiom-lattice/core";
24715
+ import { getStoreLattice as getStoreLattice26 } from "@axiom-lattice/core";
24192
24716
  function getStore9() {
24193
- return getStoreLattice25("default", "menu").store;
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 getStoreLattice26 } from "@axiom-lattice/core";
24795
+ import { getStoreLattice as getStoreLattice27 } from "@axiom-lattice/core";
24272
24796
  function getStore10() {
24273
- return getStoreLattice26("default", "eval").store;
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 getStoreLattice29,
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 getReplicaId6,
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 getStoreLattice27,
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 = getStoreLattice27("default", "thread").store;
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 = getStoreLattice27("default", "thread").store;
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
- const metadata = exactStrings(envelope?.metadata, ["inputMessageId"]);
25719
- if (!targetEnvelope || targetEnvelope.adapterChannel !== "room" || targetEnvelope.channelInstallationId !== installation.id || !target || !metadata || typeof envelope?.text !== "string") throw new Error("Invalid room interrupt");
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 getStoreLattice28 } from "@axiom-lattice/core";
25789
- var SHARED_STORE_KINDS = ["lease", "outbox", "threadMessageQueue", "task", "taskWorkItem"];
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 getStoreLattice28("default", kind).store.constructor?.name;
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 = getStoreLattice29("default", "mcp");
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: getStoreLattice29("default", "task").store,
26135
- rooms: getStoreLattice29("default", "projectRoom").store,
26136
- memberships: getStoreLattice29("default", "projectBotMembership").store,
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) => getStore11("default", kind).store),
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 = getStoreLattice29("default", "menu").store;
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-FGYX2HH3.mjs");
26197
- const a2aKeyStore = getStoreLattice29("default", "a2aApiKey").store;
26198
- const assistantStore = getStoreLattice29("default", "assistant").store;
26199
- const projectStore = getStoreLattice29("default", "project").store;
26200
- const taskStore = getStoreLattice29("default", "task").store;
26201
- const authService = new A2AAuthService({
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: parseEnvKeys(process.env.A2A_API_KEYS || ""),
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-UVKMONVE.mjs");
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-VU227IO3.mjs");
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 = getStoreLattice29("default", "thread").store;
26240
- const assistantStore = getStoreLattice29("default", "assistant").store;
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 = getStoreLattice29("default", "sharedResource").store;
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
- await connectAllChannels(
26299
- (channel) => adapterRegistry.get(channel),
26300
- { deps: { router: channelDeps.router } }
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(getStoreLattice29("default", "lease").store);
26310
- const outboxStore = getStoreLattice29("default", "outbox").store;
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: getReplicaId6() });
27874
+ configurePluginCatalogSync({ outbox: outboxStore, owner: getReplicaId7() });
26314
27875
  bridgeAssistantEvents(
26315
27876
  (event, cb) => eventBus4.subscribe(event, cb),
26316
27877
  outboxStore,
26317
- getReplicaId6()
27878
+ getReplicaId7()
26318
27879
  );
26319
27880
  if (projectRoomRealtimeRuntime) {
26320
27881
  projectRoomRealtimeRuntime.broker.setBroadcastHook((scope, serialized, sourceKey) => {
26321
- void publishRoomEvent(outboxStore, getReplicaId6(), scope, serialized, sourceKey);
27882
+ void publishRoomEvent(outboxStore, getReplicaId7(), scope, serialized, sourceKey);
26322
27883
  });
26323
27884
  }
26324
27885
  const outboxPoller = new OutboxPoller({
26325
27886
  store: outboxStore,
26326
- owner: getReplicaId6(),
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: getStoreLattice29("default", "mcp").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 = getStoreLattice29("default", "lease").store;
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: getStoreLattice29("default", "task").store,
26369
- workItemStore: getStoreLattice29("default", "taskWorkItem").store,
26370
- tenantStore: getStoreLattice29("default", "tenant").store,
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
- const agentTaskConsumer = new AgentTaskConsumer(target_port);
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,