@axiom-lattice/gateway 4.7.0 → 4.8.0

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 (54) hide show
  1. package/dist/{OrphanRecoveryRuntime-MQDEJX3M.mjs → OrphanRecoveryRuntime-BEFILJMU.mjs} +2 -2
  2. package/dist/{chunk-2POW27EI.mjs → chunk-7CG57LEF.mjs} +2 -2
  3. package/dist/{chunk-WKNZ7GBC.mjs → chunk-EXPERMZR.mjs} +1 -1
  4. package/dist/chunk-EXPERMZR.mjs.map +1 -0
  5. package/dist/{chunk-52SUWMOX.mjs → chunk-JUA3VJE7.mjs} +3 -2
  6. package/dist/chunk-JUA3VJE7.mjs.map +1 -0
  7. package/dist/{chunk-TJAUXMYJ.mjs → chunk-URXZPI2F.mjs} +8 -1
  8. package/dist/chunk-URXZPI2F.mjs.map +1 -0
  9. package/dist/{chunk-KDQKTU3N.mjs → chunk-ZBTG3T7K.mjs} +44 -8
  10. package/dist/chunk-ZBTG3T7K.mjs.map +1 -0
  11. package/dist/embed/{embed-page-AgentDetailSideApp-2C2ZR62Q.js → embed-page-AgentDetailSideApp-NTLAFO5C.js} +1 -1
  12. package/dist/embed/{embed-page-AgentGovernanceWorkbench-GG4ONSTS.js → embed-page-AgentGovernanceWorkbench-QPVBOMLH.js} +1 -1
  13. package/dist/embed/{embed-page-MessageGraph-WMYTVUUC.js → embed-page-MessageGraph-JAJGMYKH.js} +1 -1
  14. package/dist/embed/{embed-page-ReactChart-7XCMDMH6.js → embed-page-ReactChart-WUG6TEAZ.js} +1 -1
  15. package/dist/embed/{embed-page-SkillFlow-XMMHEHQL.js → embed-page-SkillFlow-TSBZ4V2P.js} +1 -1
  16. package/dist/embed/{embed-page-TasksSideApp-4ZJ4Q4CL.js → embed-page-TasksSideApp-TJI22YGD.js} +1 -1
  17. package/dist/embed/{embed-page-TeamWorkspace-H5OOEN3K.js → embed-page-TeamWorkspace-OJJLN5VI.js} +1 -1
  18. package/dist/embed/{embed-page-TopologyRuntimeView-QMXWERVC.js → embed-page-TopologyRuntimeView-ZFU4N6TV.js} +1 -1
  19. package/dist/embed/{embed-page-WorkflowAutomationView-L2OEGCA5.js → embed-page-WorkflowAutomationView-ALXV7VQQ.js} +1 -1
  20. package/dist/embed/{embed-page-chunk-CZE4IEB3.js → embed-page-chunk-DZQBVJ7Z.js} +1 -1
  21. package/dist/embed/{embed-page-chunk-N2ISCAS7.js → embed-page-chunk-IPIJ2SG6.js} +1 -1
  22. package/dist/embed/{embed-page-chunk-SUMYB6ZA.js → embed-page-chunk-N22EGQAJ.js} +1 -1
  23. package/dist/embed/{embed-page-chunk-OHDEKFTU.js → embed-page-chunk-O72XBKQH.js} +2 -2
  24. package/dist/embed/{embed-page-chunk-XIXSRYJY.js → embed-page-chunk-QMVIL4BJ.js} +190 -185
  25. package/dist/embed/{embed-page-chunk-2G54EIW2.js → embed-page-chunk-UT3WYBZH.js} +1 -1
  26. package/dist/embed/embed-page-chunk-YPNSOZYW.js +8 -0
  27. package/dist/embed/embed-page.js +1 -1
  28. package/dist/index.d.mts +146 -17
  29. package/dist/index.d.ts +146 -17
  30. package/dist/index.js +1190 -328
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +649 -97
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/{open-STFMNRO5.mjs → open-UVKMONVE.mjs} +5 -4
  35. package/dist/open-UVKMONVE.mjs.map +1 -0
  36. package/dist/{open-files-NPNQQ6TY.mjs → open-files-X3HXVEEM.mjs} +4 -4
  37. package/dist/{open-session-IHDGAXL3.mjs → open-session-2Y7XJJW2.mjs} +3 -3
  38. package/dist/{resources-DXPXAXEZ.mjs → resources-2L3KSUUU.mjs} +2 -2
  39. package/dist/sandboxOpenCredentialProvider-USIU7Z7H.mjs +275 -0
  40. package/dist/sandboxOpenCredentialProvider-USIU7Z7H.mjs.map +1 -0
  41. package/dist/{sandboxProvider-I3P66VXT.mjs → sandboxProvider-DUYWHYJN.mjs} +2 -2
  42. package/package.json +14 -8
  43. package/dist/chunk-52SUWMOX.mjs.map +0 -1
  44. package/dist/chunk-KDQKTU3N.mjs.map +0 -1
  45. package/dist/chunk-TJAUXMYJ.mjs.map +0 -1
  46. package/dist/chunk-WKNZ7GBC.mjs.map +0 -1
  47. package/dist/embed/embed-page-chunk-HEMNMC4X.js +0 -7
  48. package/dist/open-STFMNRO5.mjs.map +0 -1
  49. /package/dist/{OrphanRecoveryRuntime-MQDEJX3M.mjs.map → OrphanRecoveryRuntime-BEFILJMU.mjs.map} +0 -0
  50. /package/dist/{chunk-2POW27EI.mjs.map → chunk-7CG57LEF.mjs.map} +0 -0
  51. /package/dist/{open-files-NPNQQ6TY.mjs.map → open-files-X3HXVEEM.mjs.map} +0 -0
  52. /package/dist/{open-session-IHDGAXL3.mjs.map → open-session-2Y7XJJW2.mjs.map} +0 -0
  53. /package/dist/{resources-DXPXAXEZ.mjs.map → resources-2L3KSUUU.mjs.map} +0 -0
  54. /package/dist/{sandboxProvider-I3P66VXT.mjs.map → sandboxProvider-DUYWHYJN.mjs.map} +0 -0
@@ -1,4 +1,4 @@
1
- import{b as f}from"./embed-page-chunk-OHDEKFTU.js";import"./embed-page-chunk-XIXSRYJY.js";import"./embed-page-chunk-HEMNMC4X.js";import{a as b}from"./embed-page-chunk-MB65DZAX.js";import"./embed-page-chunk-VW3NF5NT.js";import"./embed-page-chunk-5JDQ456X.js";import"./embed-page-chunk-Z7USDLHI.js";import{a as g}from"./embed-page-chunk-O4QK5LWZ.js";import"./embed-page-chunk-4BKXE5ON.js";import"./embed-page-chunk-IX4WLWCY.js";import"./embed-page-chunk-EUOOO2LS.js";import{b as U,e as s}from"./embed-page-chunk-6Y3UYJ6Z.js";var h=U(d=>{"use strict";var E=g();d.createRoot=E.createRoot,d.hydrateRoot=E.hydrateRoot;var ee});var T=s(h()),k=s(g());var y="axiom-web-app-embed";var x=["axiom:hello","axiom:identity","axiom:send","axiom:config"];function w(e,t){return{source:y,version:1,type:e,payload:t}}var N=6,z=40,B=500;function H(e){if(typeof e!="object"||e===null||Array.isArray(e))return!1;let t=e,n=Object.keys(t);return n.length!==2||!n.includes("label")||!n.includes("text")?!1:typeof t.label=="string"&&t.label.length>=1&&t.label.length<=z&&typeof t.text=="string"&&t.text.length>=1&&t.text.length<=B}function Q(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(t.length>=N)break;H(n)&&t.push({label:n.label,text:n.text})}return t}function _(e){if(typeof e!="object"||e===null)return null;let t=e,n={};if(typeof t.welcome=="string"&&t.welcome.length>0&&(n.welcome=t.welcome),Array.isArray(t.quicks)){let r=Q(t.quicks);(r.length>0||t.quicks.length===0)&&(n.quicks=r)}return n.welcome===void 0&&n.quicks===void 0?null:n}function R(e,t){if(typeof e!="object"||e===null)return null;let n=e;return n.source!==y||n.version!==1||typeof n.type!="string"||!t.includes(n.type)?null:{type:n.type,payload:n.payload}}var p=s(b()),P=/^[A-Za-z0-9._:@-]{1,128}$/,q="Open this chat from the host page embed to start chatting.",v="axiom-embed-root",C="axiom-embed-style",A=32768,G=`
1
+ import{b as f}from"./embed-page-chunk-O72XBKQH.js";import"./embed-page-chunk-QMVIL4BJ.js";import"./embed-page-chunk-YPNSOZYW.js";import{a as b}from"./embed-page-chunk-MB65DZAX.js";import"./embed-page-chunk-VW3NF5NT.js";import"./embed-page-chunk-5JDQ456X.js";import"./embed-page-chunk-Z7USDLHI.js";import{a as g}from"./embed-page-chunk-O4QK5LWZ.js";import"./embed-page-chunk-4BKXE5ON.js";import"./embed-page-chunk-IX4WLWCY.js";import"./embed-page-chunk-EUOOO2LS.js";import{b as U,e as s}from"./embed-page-chunk-6Y3UYJ6Z.js";var h=U(d=>{"use strict";var E=g();d.createRoot=E.createRoot,d.hydrateRoot=E.hydrateRoot;var ee});var T=s(h()),k=s(g());var y="axiom-web-app-embed";var x=["axiom:hello","axiom:identity","axiom:send","axiom:config"];function w(e,t){return{source:y,version:1,type:e,payload:t}}var N=6,z=40,B=500;function H(e){if(typeof e!="object"||e===null||Array.isArray(e))return!1;let t=e,n=Object.keys(t);return n.length!==2||!n.includes("label")||!n.includes("text")?!1:typeof t.label=="string"&&t.label.length>=1&&t.label.length<=z&&typeof t.text=="string"&&t.text.length>=1&&t.text.length<=B}function Q(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(t.length>=N)break;H(n)&&t.push({label:n.label,text:n.text})}return t}function _(e){if(typeof e!="object"||e===null)return null;let t=e,n={};if(typeof t.welcome=="string"&&t.welcome.length>0&&(n.welcome=t.welcome),Array.isArray(t.quicks)){let r=Q(t.quicks);(r.length>0||t.quicks.length===0)&&(n.quicks=r)}return n.welcome===void 0&&n.quicks===void 0?null:n}function R(e,t){if(typeof e!="object"||e===null)return null;let n=e;return n.source!==y||n.version!==1||typeof n.type!="string"||!t.includes(n.type)?null:{type:n.type,payload:n.payload}}var p=s(b()),P=/^[A-Za-z0-9._:@-]{1,128}$/,q="Open this chat from the host page embed to start chatting.",v="axiom-embed-root",C="axiom-embed-style",A=32768,G=`
2
2
  html, body, #${v} { height: 100%; margin: 0; }
3
3
  .axiom-embed-fill { height: 100% !important; min-height: 0 !important; }
4
4
  .axiom-embed-card { display: flex; align-items: center; justify-content: center; height: 100%; margin: 0; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; color: #64748b; padding: 24px; box-sizing: border-box; text-align: center; }
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as http from 'http';
2
- import { ProjectRoomMessage, ProjectBotMembership, ProjectMembership, TaskItem, ChannelAdapter, InboundMessage, Binding, BindingRegistry, ChannelInstallationStore, DispatchResult, ProjectRoomEventScope, ProjectRoomBusinessEventDraft, ProjectRoomScopedBusinessEvent, ProjectRoomBusinessEvent, ProjectRoomEventSubscription, TaskStore, ProjectRoomStore, ProjectBotMembershipStore, ThreadStore, ProjectRoom, Thread, ProjectStore, TaskWorkItemStore, TenantStore, LoggerType, PinoFileOptions } from '@axiom-lattice/protocols';
2
+ import { ProjectRoomMessage, ProjectBotMembership, ProjectMembership, TaskItem, ChannelAdapter, InboundMessage, Binding, BindingRegistry, ChannelInstallationStore, DispatchResult, ProjectRoomEventScope, ProjectRoomBusinessEventDraft, ProjectRoomScopedBusinessEvent, ProjectRoomBusinessEvent, ProjectRoomEventSubscription, TaskStore, ProjectRoomStore, ProjectBotMembershipStore, ThreadStore, ProjectRoom, Thread, ProjectStore, TaskWorkItemStore, TenantStore, OutboxStore, LoggerType, PinoFileOptions } from '@axiom-lattice/protocols';
3
3
  import { TrustedAgentMessageDispatcher, TrustedMessageOptions, TaskLifecycleListener, TaskLifecycleService, IMessageQueueStore, Agent, TaskRunRegistry } from '@axiom-lattice/core';
4
4
  import * as fastify from 'fastify';
5
5
 
@@ -177,6 +177,7 @@ interface ProjectRoomEventBrokerOptions {
177
177
  maxGlobalEvents?: number;
178
178
  maxGlobalBytes?: number;
179
179
  initialSequence?: number;
180
+ sourceKeyLimit?: number;
180
181
  log?: {
181
182
  warn(message: string, context: Record<string, unknown>): void;
182
183
  };
@@ -195,22 +196,53 @@ declare class ProjectRoomEventBroker {
195
196
  private readonly maxRooms;
196
197
  private readonly maxGlobalEvents;
197
198
  private readonly maxGlobalBytes;
199
+ private readonly sourceKeyLimit;
198
200
  private readonly log;
199
201
  private readonly rooms;
200
202
  private readonly sequenceScopes;
203
+ private readonly seenSourceKeys;
201
204
  private sequence;
202
205
  private eventCount;
203
206
  private byteCount;
204
207
  private subscriberCount;
205
208
  private usageClock;
206
209
  private closed;
210
+ private broadcast;
207
211
  constructor(options?: ProjectRoomEventBrokerOptions);
208
212
  /** Number of currently registered subscriptions across all rooms. */
209
213
  get activeSubscriberCount(): number;
210
214
  /** Exact UTF-8 byte count of all retained serialized scoped events. */
211
215
  get retainedBytes(): number;
212
- /** Atomically stores an immutable event before queueing detached listener callbacks. */
213
- publish(scope: ProjectRoomEventScope, draft: ProjectRoomBusinessEventDraft): ProjectRoomScopedBusinessEvent;
216
+ /**
217
+ * Installs (or clears) a hook invoked after a locally published event is stored
218
+ * and queued to local subscribers. Used to mirror events to other replicas via
219
+ * a shared transport. Best-effort: a throwing hook never affects `publish`.
220
+ */
221
+ setBroadcastHook(hook: ((scope: ProjectRoomEventScope, serializedEvent: string, sourceKey?: string) => void) | undefined): void;
222
+ /**
223
+ * Atomically stores an immutable event before queueing detached listener callbacks.
224
+ *
225
+ * @param sourceKey Optional caller-owned fact key; when present it is retained
226
+ * for bounded cross-replica duplicate suppression so a later foreign copy of the
227
+ * same fact (or a local publish) is dropped by {@link ingestForeignEvent}.
228
+ */
229
+ publish(scope: ProjectRoomEventScope, draft: ProjectRoomBusinessEventDraft, sourceKey?: string): ProjectRoomScopedBusinessEvent;
230
+ /**
231
+ * Delivers one event published by ANOTHER replica to this process's local
232
+ * subscribers for its room, without adding it to the local replay ring or
233
+ * consuming a local sequence (its id carries the origin replica's epoch).
234
+ *
235
+ * @param serializedEvent A serialized scoped business event produced by a peer.
236
+ * @param sourceKey Optional caller-owned fact key shared by every replica's copy
237
+ * of the same committed fact. When it was already seen (locally published or
238
+ * ingested from a peer) the event is dropped to avoid N× duplicate delivery.
239
+ * @returns True when it was queued to at least one local subscription.
240
+ * @remarks Source-key deduplication is process-local and bounded, so it caps
241
+ * duplicate fan-out within the retention window without replacing REST, which
242
+ * remains the authority. A malformed payload or a room with no local
243
+ * subscribers is ignored.
244
+ */
245
+ ingestForeignEvent(serializedEvent: string, sourceKey?: string): boolean;
214
246
  /** Atomically resolves replay and registers a distinct subscription for live events. */
215
247
  subscribe(scope: ProjectRoomEventScope, afterEventId: string | undefined, listener: Listener): ProjectRoomEventSubscription;
216
248
  /** Clears retained state, marks every subscription closed, and permanently closes the broker. */
@@ -232,6 +264,7 @@ declare class ProjectRoomEventBroker {
232
264
  private assertOpen;
233
265
  private createRoom;
234
266
  private touch;
267
+ private rememberSourceKey;
235
268
  }
236
269
 
237
270
  /**
@@ -307,11 +340,18 @@ interface ProjectTaskMachineScope {
307
340
  /**
308
341
  * Serializes Project task machine operations by tenant, Assistant, and Project.
309
342
  *
310
- * This mutex is process-local: callers must share one instance, and deployments
311
- * with multiple Gateway processes need an external lease for cross-process
312
- * exclusion. Acquisition is FIFO and intentionally non-reentrant; an operation
313
- * must not await another acquisition of its own machine key. When an operation
314
- * also needs the Project mutation mutex, it must acquire this machine mutex first.
343
+ * The local FIFO lock is process-local: callers must share one instance.
344
+ * Acquisition is FIFO and intentionally non-reentrant; an operation must not
345
+ * await another acquisition of its own machine key. When an operation also needs
346
+ * the Project mutation mutex, it must acquire this machine mutex first.
347
+ *
348
+ * A coordination lease store may be installed process-wide
349
+ * ({@link configureCoordinationLeaseStore}). When present, each critical section
350
+ * additionally holds a bounded distributed lease
351
+ * (`project-task-machine:<tenant>:<assistant>:<project>`) so operations on the
352
+ * same machine serialize across replicas, not just within this process. The local
353
+ * FIFO lock is always retained; the lease only adds cross-replica exclusion. When
354
+ * no store is configured (single process, tests), behavior is unchanged.
315
355
  */
316
356
  declare class ProjectTaskExecutionMutex {
317
357
  private readonly queues;
@@ -397,6 +437,11 @@ type ProjectTaskRunOutcome = {
397
437
  task: TaskItem;
398
438
  thread: Thread;
399
439
  messageId: string;
440
+ } | {
441
+ kind: "deferred";
442
+ task: TaskItem;
443
+ thread: Thread;
444
+ messageId: string;
400
445
  };
401
446
  /** Stable classification of the queue fact used to recover an in-progress Project Task. */
402
447
  type ProjectTaskRecoveryClassification = {
@@ -764,6 +809,37 @@ declare class AgentTaskConsumer {
764
809
  trigger_agent_task(taskRequest: AgentTaskRequest): void;
765
810
  }
766
811
 
812
+ /** Cross-replica liveness probe: resolves true when a peer still owns the task's run. */
813
+ type TaskLivenessProbe = (task: TaskItem) => Promise<boolean>;
814
+ /**
815
+ * Builds a cross-replica task-liveness probe from the registered `"lease"` store.
816
+ *
817
+ * @returns A {@link TaskLivenessProbe}, or `undefined` only when no `lease`
818
+ * store is registered (a test-only path); with the default in-memory lease
819
+ * store a probe is always returned (single-process safe because the in-memory
820
+ * store has no peer).
821
+ * @remarks
822
+ * A task is considered live when EITHER lease is currently held:
823
+ * - `task-run:<taskId>` — the immediate executor's run-lifetime lease, or
824
+ * - `thread-run:<tenantId>:<ownerId>:<thread_id>` — the queue processor
825
+ * executing the task's run thread (dispatched workers run through this).
826
+ *
827
+ * Fail-safe: any lease-store error resolves `true`, so an unknown liveness
828
+ * state never causes a live run to be converged as an orphan.
829
+ *
830
+ * The probe is advisory: there is a benign race between the probe read and
831
+ * `failTask` (a run could acquire its lease in that window), and no
832
+ * fencing/heartbeat-expiry detection is provided yet — consistent with the
833
+ * framework's current no-lease posture.
834
+ *
835
+ * @example
836
+ * ```ts
837
+ * const probe = createTaskRunLiveness();
838
+ * if (probe && await probe(task)) return; // a peer replica owns it
839
+ * ```
840
+ */
841
+ declare function createTaskRunLiveness(): TaskLivenessProbe | undefined;
842
+
767
843
  /** Minimal caller-graph client surface used by recovery. */
768
844
  interface RecoveryCallerClient {
769
845
  getState(config: unknown): Promise<unknown>;
@@ -789,6 +865,12 @@ interface TopCallerRecoveryDeps {
789
865
  registry?: {
790
866
  isLive(taskId: string): boolean;
791
867
  };
868
+ /**
869
+ * Optional cross-replica liveness probe consulted by Guard B, so a run held
870
+ * by a peer replica is not converged/backfilled. Mutually additive with the
871
+ * process-local `registry`.
872
+ */
873
+ isTaskLive?: TaskLivenessProbe;
792
874
  /** Resume the continuation (tests can call client.invoke(null, config)). */
793
875
  resume(client: RecoveryCallerClient, config: unknown, agent?: unknown): Promise<void>;
794
876
  }
@@ -808,6 +890,12 @@ interface RecoverTaskDeps {
808
890
  * immediate (frozen) caller. Defaults to `[converged]` when omitted.
809
891
  */
810
892
  orphans?: TaskItem[];
893
+ /**
894
+ * Optional cross-replica liveness probe consulted by Guard B, so a run held
895
+ * by a peer replica is not converged. Forwarded to
896
+ * {@link reconcileStaleTaskRuns} and to {@link recoverTopCaller}.
897
+ */
898
+ isTaskLive?: TaskLivenessProbe;
811
899
  /** Resolve a caller's agent client + config from the resolved top caller. */
812
900
  callerFor: TopCallerRecoveryDeps["callerFor"];
813
901
  /** Resume the continuation (tests can call client.invoke(null, config)). */
@@ -862,9 +950,10 @@ declare function recoverTopCaller(task: TaskItem, top: {
862
950
  * being mistaken for a direct child of its frozen immediate caller.
863
951
  *
864
952
  * This is an **offline/maintenance** entry point (the `task:recover` CLI): run
865
- * it when the Gateway is stopped, or from within the Gateway process. The
866
- * `live` guard reads the process-local {@link taskRunRegistry}, so it is
867
- * meaningful only in-process and cannot detect a run held by another process.
953
+ * it when the Gateway is stopped, or from within the Gateway process. Guard B
954
+ * reads the process-local {@link taskRunRegistry}; when the optional
955
+ * `isTaskLive` cross-replica probe is supplied (the gateway's lease-backed
956
+ * probe), it also detects a run held by a peer replica.
868
957
  *
869
958
  * The convergence actor is `system:on-demand-recovery` (distinct from the
870
959
  * startup sweep's `system:orphan-recovery`), so on-demand convergences are
@@ -891,6 +980,7 @@ declare function runOrphanRecovery(deps: {
891
980
  }>;
892
981
  callerFor: TopCallerRecoveryDeps["callerFor"];
893
982
  resume: TopCallerRecoveryDeps["resume"];
983
+ isTaskLive?: TaskLivenessProbe;
894
984
  log: {
895
985
  info(msg: string, ctx: Record<string, unknown>): void;
896
986
  warn(msg: string, ctx: Record<string, unknown>): void;
@@ -925,6 +1015,11 @@ interface OrphanRecoveryRuntimeDeps {
925
1015
  callerFor: TopCallerRecoveryDeps["callerFor"];
926
1016
  /** Resume the continuation (tests can call client.invoke(null, config)). */
927
1017
  resume: TopCallerRecoveryDeps["resume"];
1018
+ /**
1019
+ * Cross-replica liveness probe; defaults to the shared cross-replica probe
1020
+ * ({@link createTaskRunLiveness()}); inherit it unless a test injects one.
1021
+ */
1022
+ isTaskLive?: TaskLivenessProbe;
928
1023
  /** Structured logger. */
929
1024
  log: {
930
1025
  info(msg: string, ctx: Record<string, unknown>): void;
@@ -942,9 +1037,11 @@ interface OrphanRecoveryRuntime {
942
1037
  * On-demand recovery of exactly one task within one tenant.
943
1038
  *
944
1039
  * @remarks Offline/maintenance: run when the Gateway is stopped, or from the
945
- * Gateway process. The `live` guard reads the process-local task registry and
946
- * cannot see a run held by another process. Governed Project tasks are refused
947
- * (`skipped:project_task`) so this never bypasses their managed runtime.
1040
+ * Gateway process. The `live` guard reads the process-local task registry and,
1041
+ * when a lease store is registered, also consults the cross-replica liveness
1042
+ * probe so a run held by another process is not converged. Governed Project
1043
+ * tasks are refused (`skipped:project_task`) so this never bypasses their
1044
+ * managed runtime.
948
1045
  */
949
1046
  recoverTask(input: {
950
1047
  tenantId: string;
@@ -962,8 +1059,12 @@ interface OrphanRecoveryRuntime {
962
1059
  * observer-based reconciliation) and from `recoverTask()`
963
1060
  * (`skipped:project_task`), so the on-demand entry can never bypass the managed
964
1061
  * Project runtime. `recoverTask()` delegates to {@link recoverSingleTask} with
965
- * the shared lifecycle and the process-local live-run registry, so it never
966
- * converges a task a live run still holds — in-process only.
1062
+ * the shared lifecycle, the process-local live-run registry, and the
1063
+ * cross-replica liveness probe (built from the shared `"lease"` store unless
1064
+ * `isTaskLive` is supplied), so it does not converge a task a live run still
1065
+ * holds — in this process or a peer replica. The liveness probe is advisory: a
1066
+ * benign race exists between the probe read and `failTask` (no
1067
+ * fencing/heartbeat support yet).
967
1068
  *
968
1069
  * @example
969
1070
  * ```ts
@@ -987,6 +1088,34 @@ declare function createOrphanRecoveryRuntime(deps: OrphanRecoveryRuntimeDeps): O
987
1088
  */
988
1089
  declare function createRecoveryCallerDeps(): Pick<TopCallerRecoveryDeps, "callerFor" | "resume">;
989
1090
 
1091
+ /** Outbox topic carrying cross-replica Project Room realtime events. */
1092
+ declare const ROOM_EVENT_TOPIC = "room-event";
1093
+ /**
1094
+ * Mirrors one locally published Project Room event to the shared outbox.
1095
+ *
1096
+ * @param outbox Shared append-only outbox log.
1097
+ * @param owner Origin replica id, so its own poller skips the row.
1098
+ * @param scope Room scope the event was published for.
1099
+ * @param serialized Canonical serialized scoped event.
1100
+ * @param sourceKey Optional caller-owned fact key carried to peer replicas for
1101
+ * bounded duplicate suppression at their broker.
1102
+ * @returns A promise that settles once the append attempt completes.
1103
+ * @remarks Best-effort: an append failure is swallowed because REST remains the
1104
+ * authority and clients refetch on reconnect.
1105
+ */
1106
+ declare function publishRoomEvent(outbox: OutboxStore, owner: string, scope: ProjectRoomEventScope, serialized: string, sourceKey?: string): Promise<void>;
1107
+ /**
1108
+ * Delivers one foreign outbox payload to the local broker's subscribers.
1109
+ *
1110
+ * @param payload Raw outbox payload; expected `{ serialized: string, sourceKey?: string }`.
1111
+ * @param broker The process-local Project Room event broker.
1112
+ * @returns True when the broker queued the event to at least one local subscription.
1113
+ * @remarks Malformed payloads are ignored. The broker drops events whose
1114
+ * source key it already saw; the poller additionally skips rows authored by this
1115
+ * replica.
1116
+ */
1117
+ declare function ingestRoomEvent(payload: unknown, broker: ProjectRoomEventBroker): boolean;
1118
+
990
1119
  /**
991
1120
  * Logger configuration for gateway
992
1121
  */
@@ -1017,4 +1146,4 @@ declare const LatticeGateway: {
1017
1146
  AgentTaskConsumer: typeof AgentTaskConsumer;
1018
1147
  };
1019
1148
 
1020
- export { type GatewayLoggerConfig, LatticeGateway, type LatticeGatewayConfig, type OrphanRecoveryRuntime, type OrphanRecoveryRuntimeDeps, type RecoverTaskDeps, type RecoverTaskResult, createOrphanRecoveryRuntime, createRecoveryCallerDeps, recoverTask, recoverTopCaller, resolveTopCaller, runOrphanRecovery };
1149
+ export { type GatewayLoggerConfig, LatticeGateway, type LatticeGatewayConfig, type OrphanRecoveryRuntime, type OrphanRecoveryRuntimeDeps, ProjectRoomEventBroker, ROOM_EVENT_TOPIC, type RecoverTaskDeps, type RecoverTaskResult, type TaskLivenessProbe, createOrphanRecoveryRuntime, createRecoveryCallerDeps, createTaskRunLiveness, ingestRoomEvent, publishRoomEvent, recoverTask, recoverTopCaller, resolveTopCaller, runOrphanRecovery };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as http from 'http';
2
- import { ProjectRoomMessage, ProjectBotMembership, ProjectMembership, TaskItem, ChannelAdapter, InboundMessage, Binding, BindingRegistry, ChannelInstallationStore, DispatchResult, ProjectRoomEventScope, ProjectRoomBusinessEventDraft, ProjectRoomScopedBusinessEvent, ProjectRoomBusinessEvent, ProjectRoomEventSubscription, TaskStore, ProjectRoomStore, ProjectBotMembershipStore, ThreadStore, ProjectRoom, Thread, ProjectStore, TaskWorkItemStore, TenantStore, LoggerType, PinoFileOptions } from '@axiom-lattice/protocols';
2
+ import { ProjectRoomMessage, ProjectBotMembership, ProjectMembership, TaskItem, ChannelAdapter, InboundMessage, Binding, BindingRegistry, ChannelInstallationStore, DispatchResult, ProjectRoomEventScope, ProjectRoomBusinessEventDraft, ProjectRoomScopedBusinessEvent, ProjectRoomBusinessEvent, ProjectRoomEventSubscription, TaskStore, ProjectRoomStore, ProjectBotMembershipStore, ThreadStore, ProjectRoom, Thread, ProjectStore, TaskWorkItemStore, TenantStore, OutboxStore, LoggerType, PinoFileOptions } from '@axiom-lattice/protocols';
3
3
  import { TrustedAgentMessageDispatcher, TrustedMessageOptions, TaskLifecycleListener, TaskLifecycleService, IMessageQueueStore, Agent, TaskRunRegistry } from '@axiom-lattice/core';
4
4
  import * as fastify from 'fastify';
5
5
 
@@ -177,6 +177,7 @@ interface ProjectRoomEventBrokerOptions {
177
177
  maxGlobalEvents?: number;
178
178
  maxGlobalBytes?: number;
179
179
  initialSequence?: number;
180
+ sourceKeyLimit?: number;
180
181
  log?: {
181
182
  warn(message: string, context: Record<string, unknown>): void;
182
183
  };
@@ -195,22 +196,53 @@ declare class ProjectRoomEventBroker {
195
196
  private readonly maxRooms;
196
197
  private readonly maxGlobalEvents;
197
198
  private readonly maxGlobalBytes;
199
+ private readonly sourceKeyLimit;
198
200
  private readonly log;
199
201
  private readonly rooms;
200
202
  private readonly sequenceScopes;
203
+ private readonly seenSourceKeys;
201
204
  private sequence;
202
205
  private eventCount;
203
206
  private byteCount;
204
207
  private subscriberCount;
205
208
  private usageClock;
206
209
  private closed;
210
+ private broadcast;
207
211
  constructor(options?: ProjectRoomEventBrokerOptions);
208
212
  /** Number of currently registered subscriptions across all rooms. */
209
213
  get activeSubscriberCount(): number;
210
214
  /** Exact UTF-8 byte count of all retained serialized scoped events. */
211
215
  get retainedBytes(): number;
212
- /** Atomically stores an immutable event before queueing detached listener callbacks. */
213
- publish(scope: ProjectRoomEventScope, draft: ProjectRoomBusinessEventDraft): ProjectRoomScopedBusinessEvent;
216
+ /**
217
+ * Installs (or clears) a hook invoked after a locally published event is stored
218
+ * and queued to local subscribers. Used to mirror events to other replicas via
219
+ * a shared transport. Best-effort: a throwing hook never affects `publish`.
220
+ */
221
+ setBroadcastHook(hook: ((scope: ProjectRoomEventScope, serializedEvent: string, sourceKey?: string) => void) | undefined): void;
222
+ /**
223
+ * Atomically stores an immutable event before queueing detached listener callbacks.
224
+ *
225
+ * @param sourceKey Optional caller-owned fact key; when present it is retained
226
+ * for bounded cross-replica duplicate suppression so a later foreign copy of the
227
+ * same fact (or a local publish) is dropped by {@link ingestForeignEvent}.
228
+ */
229
+ publish(scope: ProjectRoomEventScope, draft: ProjectRoomBusinessEventDraft, sourceKey?: string): ProjectRoomScopedBusinessEvent;
230
+ /**
231
+ * Delivers one event published by ANOTHER replica to this process's local
232
+ * subscribers for its room, without adding it to the local replay ring or
233
+ * consuming a local sequence (its id carries the origin replica's epoch).
234
+ *
235
+ * @param serializedEvent A serialized scoped business event produced by a peer.
236
+ * @param sourceKey Optional caller-owned fact key shared by every replica's copy
237
+ * of the same committed fact. When it was already seen (locally published or
238
+ * ingested from a peer) the event is dropped to avoid N× duplicate delivery.
239
+ * @returns True when it was queued to at least one local subscription.
240
+ * @remarks Source-key deduplication is process-local and bounded, so it caps
241
+ * duplicate fan-out within the retention window without replacing REST, which
242
+ * remains the authority. A malformed payload or a room with no local
243
+ * subscribers is ignored.
244
+ */
245
+ ingestForeignEvent(serializedEvent: string, sourceKey?: string): boolean;
214
246
  /** Atomically resolves replay and registers a distinct subscription for live events. */
215
247
  subscribe(scope: ProjectRoomEventScope, afterEventId: string | undefined, listener: Listener): ProjectRoomEventSubscription;
216
248
  /** Clears retained state, marks every subscription closed, and permanently closes the broker. */
@@ -232,6 +264,7 @@ declare class ProjectRoomEventBroker {
232
264
  private assertOpen;
233
265
  private createRoom;
234
266
  private touch;
267
+ private rememberSourceKey;
235
268
  }
236
269
 
237
270
  /**
@@ -307,11 +340,18 @@ interface ProjectTaskMachineScope {
307
340
  /**
308
341
  * Serializes Project task machine operations by tenant, Assistant, and Project.
309
342
  *
310
- * This mutex is process-local: callers must share one instance, and deployments
311
- * with multiple Gateway processes need an external lease for cross-process
312
- * exclusion. Acquisition is FIFO and intentionally non-reentrant; an operation
313
- * must not await another acquisition of its own machine key. When an operation
314
- * also needs the Project mutation mutex, it must acquire this machine mutex first.
343
+ * The local FIFO lock is process-local: callers must share one instance.
344
+ * Acquisition is FIFO and intentionally non-reentrant; an operation must not
345
+ * await another acquisition of its own machine key. When an operation also needs
346
+ * the Project mutation mutex, it must acquire this machine mutex first.
347
+ *
348
+ * A coordination lease store may be installed process-wide
349
+ * ({@link configureCoordinationLeaseStore}). When present, each critical section
350
+ * additionally holds a bounded distributed lease
351
+ * (`project-task-machine:<tenant>:<assistant>:<project>`) so operations on the
352
+ * same machine serialize across replicas, not just within this process. The local
353
+ * FIFO lock is always retained; the lease only adds cross-replica exclusion. When
354
+ * no store is configured (single process, tests), behavior is unchanged.
315
355
  */
316
356
  declare class ProjectTaskExecutionMutex {
317
357
  private readonly queues;
@@ -397,6 +437,11 @@ type ProjectTaskRunOutcome = {
397
437
  task: TaskItem;
398
438
  thread: Thread;
399
439
  messageId: string;
440
+ } | {
441
+ kind: "deferred";
442
+ task: TaskItem;
443
+ thread: Thread;
444
+ messageId: string;
400
445
  };
401
446
  /** Stable classification of the queue fact used to recover an in-progress Project Task. */
402
447
  type ProjectTaskRecoveryClassification = {
@@ -764,6 +809,37 @@ declare class AgentTaskConsumer {
764
809
  trigger_agent_task(taskRequest: AgentTaskRequest): void;
765
810
  }
766
811
 
812
+ /** Cross-replica liveness probe: resolves true when a peer still owns the task's run. */
813
+ type TaskLivenessProbe = (task: TaskItem) => Promise<boolean>;
814
+ /**
815
+ * Builds a cross-replica task-liveness probe from the registered `"lease"` store.
816
+ *
817
+ * @returns A {@link TaskLivenessProbe}, or `undefined` only when no `lease`
818
+ * store is registered (a test-only path); with the default in-memory lease
819
+ * store a probe is always returned (single-process safe because the in-memory
820
+ * store has no peer).
821
+ * @remarks
822
+ * A task is considered live when EITHER lease is currently held:
823
+ * - `task-run:<taskId>` — the immediate executor's run-lifetime lease, or
824
+ * - `thread-run:<tenantId>:<ownerId>:<thread_id>` — the queue processor
825
+ * executing the task's run thread (dispatched workers run through this).
826
+ *
827
+ * Fail-safe: any lease-store error resolves `true`, so an unknown liveness
828
+ * state never causes a live run to be converged as an orphan.
829
+ *
830
+ * The probe is advisory: there is a benign race between the probe read and
831
+ * `failTask` (a run could acquire its lease in that window), and no
832
+ * fencing/heartbeat-expiry detection is provided yet — consistent with the
833
+ * framework's current no-lease posture.
834
+ *
835
+ * @example
836
+ * ```ts
837
+ * const probe = createTaskRunLiveness();
838
+ * if (probe && await probe(task)) return; // a peer replica owns it
839
+ * ```
840
+ */
841
+ declare function createTaskRunLiveness(): TaskLivenessProbe | undefined;
842
+
767
843
  /** Minimal caller-graph client surface used by recovery. */
768
844
  interface RecoveryCallerClient {
769
845
  getState(config: unknown): Promise<unknown>;
@@ -789,6 +865,12 @@ interface TopCallerRecoveryDeps {
789
865
  registry?: {
790
866
  isLive(taskId: string): boolean;
791
867
  };
868
+ /**
869
+ * Optional cross-replica liveness probe consulted by Guard B, so a run held
870
+ * by a peer replica is not converged/backfilled. Mutually additive with the
871
+ * process-local `registry`.
872
+ */
873
+ isTaskLive?: TaskLivenessProbe;
792
874
  /** Resume the continuation (tests can call client.invoke(null, config)). */
793
875
  resume(client: RecoveryCallerClient, config: unknown, agent?: unknown): Promise<void>;
794
876
  }
@@ -808,6 +890,12 @@ interface RecoverTaskDeps {
808
890
  * immediate (frozen) caller. Defaults to `[converged]` when omitted.
809
891
  */
810
892
  orphans?: TaskItem[];
893
+ /**
894
+ * Optional cross-replica liveness probe consulted by Guard B, so a run held
895
+ * by a peer replica is not converged. Forwarded to
896
+ * {@link reconcileStaleTaskRuns} and to {@link recoverTopCaller}.
897
+ */
898
+ isTaskLive?: TaskLivenessProbe;
811
899
  /** Resolve a caller's agent client + config from the resolved top caller. */
812
900
  callerFor: TopCallerRecoveryDeps["callerFor"];
813
901
  /** Resume the continuation (tests can call client.invoke(null, config)). */
@@ -862,9 +950,10 @@ declare function recoverTopCaller(task: TaskItem, top: {
862
950
  * being mistaken for a direct child of its frozen immediate caller.
863
951
  *
864
952
  * This is an **offline/maintenance** entry point (the `task:recover` CLI): run
865
- * it when the Gateway is stopped, or from within the Gateway process. The
866
- * `live` guard reads the process-local {@link taskRunRegistry}, so it is
867
- * meaningful only in-process and cannot detect a run held by another process.
953
+ * it when the Gateway is stopped, or from within the Gateway process. Guard B
954
+ * reads the process-local {@link taskRunRegistry}; when the optional
955
+ * `isTaskLive` cross-replica probe is supplied (the gateway's lease-backed
956
+ * probe), it also detects a run held by a peer replica.
868
957
  *
869
958
  * The convergence actor is `system:on-demand-recovery` (distinct from the
870
959
  * startup sweep's `system:orphan-recovery`), so on-demand convergences are
@@ -891,6 +980,7 @@ declare function runOrphanRecovery(deps: {
891
980
  }>;
892
981
  callerFor: TopCallerRecoveryDeps["callerFor"];
893
982
  resume: TopCallerRecoveryDeps["resume"];
983
+ isTaskLive?: TaskLivenessProbe;
894
984
  log: {
895
985
  info(msg: string, ctx: Record<string, unknown>): void;
896
986
  warn(msg: string, ctx: Record<string, unknown>): void;
@@ -925,6 +1015,11 @@ interface OrphanRecoveryRuntimeDeps {
925
1015
  callerFor: TopCallerRecoveryDeps["callerFor"];
926
1016
  /** Resume the continuation (tests can call client.invoke(null, config)). */
927
1017
  resume: TopCallerRecoveryDeps["resume"];
1018
+ /**
1019
+ * Cross-replica liveness probe; defaults to the shared cross-replica probe
1020
+ * ({@link createTaskRunLiveness()}); inherit it unless a test injects one.
1021
+ */
1022
+ isTaskLive?: TaskLivenessProbe;
928
1023
  /** Structured logger. */
929
1024
  log: {
930
1025
  info(msg: string, ctx: Record<string, unknown>): void;
@@ -942,9 +1037,11 @@ interface OrphanRecoveryRuntime {
942
1037
  * On-demand recovery of exactly one task within one tenant.
943
1038
  *
944
1039
  * @remarks Offline/maintenance: run when the Gateway is stopped, or from the
945
- * Gateway process. The `live` guard reads the process-local task registry and
946
- * cannot see a run held by another process. Governed Project tasks are refused
947
- * (`skipped:project_task`) so this never bypasses their managed runtime.
1040
+ * Gateway process. The `live` guard reads the process-local task registry and,
1041
+ * when a lease store is registered, also consults the cross-replica liveness
1042
+ * probe so a run held by another process is not converged. Governed Project
1043
+ * tasks are refused (`skipped:project_task`) so this never bypasses their
1044
+ * managed runtime.
948
1045
  */
949
1046
  recoverTask(input: {
950
1047
  tenantId: string;
@@ -962,8 +1059,12 @@ interface OrphanRecoveryRuntime {
962
1059
  * observer-based reconciliation) and from `recoverTask()`
963
1060
  * (`skipped:project_task`), so the on-demand entry can never bypass the managed
964
1061
  * Project runtime. `recoverTask()` delegates to {@link recoverSingleTask} with
965
- * the shared lifecycle and the process-local live-run registry, so it never
966
- * converges a task a live run still holds — in-process only.
1062
+ * the shared lifecycle, the process-local live-run registry, and the
1063
+ * cross-replica liveness probe (built from the shared `"lease"` store unless
1064
+ * `isTaskLive` is supplied), so it does not converge a task a live run still
1065
+ * holds — in this process or a peer replica. The liveness probe is advisory: a
1066
+ * benign race exists between the probe read and `failTask` (no
1067
+ * fencing/heartbeat support yet).
967
1068
  *
968
1069
  * @example
969
1070
  * ```ts
@@ -987,6 +1088,34 @@ declare function createOrphanRecoveryRuntime(deps: OrphanRecoveryRuntimeDeps): O
987
1088
  */
988
1089
  declare function createRecoveryCallerDeps(): Pick<TopCallerRecoveryDeps, "callerFor" | "resume">;
989
1090
 
1091
+ /** Outbox topic carrying cross-replica Project Room realtime events. */
1092
+ declare const ROOM_EVENT_TOPIC = "room-event";
1093
+ /**
1094
+ * Mirrors one locally published Project Room event to the shared outbox.
1095
+ *
1096
+ * @param outbox Shared append-only outbox log.
1097
+ * @param owner Origin replica id, so its own poller skips the row.
1098
+ * @param scope Room scope the event was published for.
1099
+ * @param serialized Canonical serialized scoped event.
1100
+ * @param sourceKey Optional caller-owned fact key carried to peer replicas for
1101
+ * bounded duplicate suppression at their broker.
1102
+ * @returns A promise that settles once the append attempt completes.
1103
+ * @remarks Best-effort: an append failure is swallowed because REST remains the
1104
+ * authority and clients refetch on reconnect.
1105
+ */
1106
+ declare function publishRoomEvent(outbox: OutboxStore, owner: string, scope: ProjectRoomEventScope, serialized: string, sourceKey?: string): Promise<void>;
1107
+ /**
1108
+ * Delivers one foreign outbox payload to the local broker's subscribers.
1109
+ *
1110
+ * @param payload Raw outbox payload; expected `{ serialized: string, sourceKey?: string }`.
1111
+ * @param broker The process-local Project Room event broker.
1112
+ * @returns True when the broker queued the event to at least one local subscription.
1113
+ * @remarks Malformed payloads are ignored. The broker drops events whose
1114
+ * source key it already saw; the poller additionally skips rows authored by this
1115
+ * replica.
1116
+ */
1117
+ declare function ingestRoomEvent(payload: unknown, broker: ProjectRoomEventBroker): boolean;
1118
+
990
1119
  /**
991
1120
  * Logger configuration for gateway
992
1121
  */
@@ -1017,4 +1146,4 @@ declare const LatticeGateway: {
1017
1146
  AgentTaskConsumer: typeof AgentTaskConsumer;
1018
1147
  };
1019
1148
 
1020
- export { type GatewayLoggerConfig, LatticeGateway, type LatticeGatewayConfig, type OrphanRecoveryRuntime, type OrphanRecoveryRuntimeDeps, type RecoverTaskDeps, type RecoverTaskResult, createOrphanRecoveryRuntime, createRecoveryCallerDeps, recoverTask, recoverTopCaller, resolveTopCaller, runOrphanRecovery };
1149
+ export { type GatewayLoggerConfig, LatticeGateway, type LatticeGatewayConfig, type OrphanRecoveryRuntime, type OrphanRecoveryRuntimeDeps, ProjectRoomEventBroker, ROOM_EVENT_TOPIC, type RecoverTaskDeps, type RecoverTaskResult, type TaskLivenessProbe, createOrphanRecoveryRuntime, createRecoveryCallerDeps, createTaskRunLiveness, ingestRoomEvent, publishRoomEvent, recoverTask, recoverTopCaller, resolveTopCaller, runOrphanRecovery };