@mastra/code-sdk 1.15.0-alpha.2 → 1.15.0-alpha.4

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 (77) hide show
  1. package/dist/acp/agent.d.ts.map +1 -1
  2. package/dist/acp/agent.js +1 -1
  3. package/dist/acp/agent.js.map +1 -1
  4. package/dist/acp/event-mapper.d.ts.map +1 -1
  5. package/dist/acp/event-mapper.js +1 -1
  6. package/dist/acp/event-mapper.js.map +1 -1
  7. package/dist/agent-connections/signal-provider.d.ts +1 -1
  8. package/dist/agent-connections/signal-provider.d.ts.map +1 -1
  9. package/dist/agent-connections/signal-provider.js.map +1 -1
  10. package/dist/agent-connections/state-processor.d.ts +2 -2
  11. package/dist/agent-connections/state-processor.d.ts.map +1 -1
  12. package/dist/agent-connections/state-processor.js.map +1 -1
  13. package/dist/agent-connections/thread-state.d.ts +1 -1
  14. package/dist/agent-connections/thread-state.d.ts.map +1 -1
  15. package/dist/agent-connections/thread-state.js.map +1 -1
  16. package/dist/agent-connections/tools.d.ts.map +1 -1
  17. package/dist/agent-connections/tools.js +1 -1
  18. package/dist/agent-connections/tools.js.map +1 -1
  19. package/dist/agents/credential-resolver.d.ts +6 -0
  20. package/dist/agents/credential-resolver.d.ts.map +1 -1
  21. package/dist/agents/credential-resolver.js +13 -4
  22. package/dist/agents/credential-resolver.js.map +1 -1
  23. package/dist/agents/sandbox-filesystem.d.ts.map +1 -1
  24. package/dist/agents/sandbox-filesystem.js +17 -6
  25. package/dist/agents/sandbox-filesystem.js.map +1 -1
  26. package/dist/experimental-agent.d.ts +1 -1
  27. package/dist/experimental-agent.d.ts.map +1 -1
  28. package/dist/experimental-agent.js.map +1 -1
  29. package/dist/factory/client.d.ts +41 -0
  30. package/dist/factory/client.d.ts.map +1 -0
  31. package/dist/factory/client.js +98 -0
  32. package/dist/factory/client.js.map +1 -0
  33. package/dist/factory/remote-controller.d.ts +34 -0
  34. package/dist/factory/remote-controller.d.ts.map +1 -0
  35. package/dist/factory/remote-controller.js +43 -0
  36. package/dist/factory/remote-controller.js.map +1 -0
  37. package/dist/factory/remote-session.d.ts +210 -0
  38. package/dist/factory/remote-session.d.ts.map +1 -0
  39. package/dist/factory/remote-session.js +416 -0
  40. package/dist/factory/remote-session.js.map +1 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +34 -15
  43. package/dist/index.js.map +1 -1
  44. package/dist/onboarding/pack-activation.d.ts +2 -11
  45. package/dist/onboarding/pack-activation.d.ts.map +1 -1
  46. package/dist/onboarding/pack-activation.js.map +1 -1
  47. package/dist/onboarding/settings.d.ts +18 -0
  48. package/dist/onboarding/settings.d.ts.map +1 -1
  49. package/dist/onboarding/settings.js +28 -3
  50. package/dist/onboarding/settings.js.map +1 -1
  51. package/dist/platform/index.d.ts +74 -0
  52. package/dist/platform/index.d.ts.map +1 -0
  53. package/dist/platform/index.js +200 -0
  54. package/dist/platform/index.js.map +1 -0
  55. package/dist/process-memory-diagnostics.d.ts +2 -1
  56. package/dist/process-memory-diagnostics.d.ts.map +1 -1
  57. package/dist/process-memory-diagnostics.js.map +1 -1
  58. package/dist/tools/web-search.d.ts +1 -1
  59. package/dist/tools/web-search.d.ts.map +1 -1
  60. package/dist/tools/web-search.js.map +1 -1
  61. package/dist/utils/local-observability.d.ts +19 -0
  62. package/dist/utils/local-observability.d.ts.map +1 -0
  63. package/dist/utils/local-observability.js +57 -0
  64. package/dist/utils/local-observability.js.map +1 -0
  65. package/dist/utils/notification-dispatch.d.ts +27 -12
  66. package/dist/utils/notification-dispatch.d.ts.map +1 -1
  67. package/dist/utils/notification-dispatch.js +46 -24
  68. package/dist/utils/notification-dispatch.js.map +1 -1
  69. package/dist/utils/signals-pubsub.d.ts +5 -3
  70. package/dist/utils/signals-pubsub.d.ts.map +1 -1
  71. package/dist/utils/signals-pubsub.js +8 -6
  72. package/dist/utils/signals-pubsub.js.map +1 -1
  73. package/dist/utils/wake-session.d.ts +44 -0
  74. package/dist/utils/wake-session.d.ts.map +1 -0
  75. package/dist/utils/wake-session.js +79 -0
  76. package/dist/utils/wake-session.js.map +1 -0
  77. package/package.json +18 -19
@@ -2,34 +2,48 @@ import type { LeaseProvider } from '@mastra/core/events';
2
2
  import type { Mastra } from '@mastra/core/mastra';
3
3
  /**
4
4
  * Whether this call runs inside this process's own dispatch pass for
5
- * `resourceId`, as opposed to the dispatch schedule every process sharing the
6
- * database takes turns running.
5
+ * `resourceId`, holding the lease for threads of `scope` (or with no `scope`,
6
+ * for threads that carry none), as opposed to the dispatch schedule every
7
+ * process sharing the database takes turns running.
7
8
  */
8
- export declare function isDispatchingNotificationsFor(resourceId: string): boolean;
9
+ export declare function isDispatchingNotificationsFor(resourceId: string, scope?: string): boolean;
9
10
  /**
10
11
  * Whether a delivery-time policy should leave a due notification pending
11
12
  * (`hold`). Deliveries happen from this process's own dispatch pass for the
12
- * record's resource. Anything else holds: a thread no session here owns
13
- * belongs to another process sharing the database, and once this process's
14
- * dispatcher runs, its own resources are delivered by whichever process holds
15
- * their dispatch lease, never by the shared dispatch schedule.
13
+ * record's resource, under the lease that covers the record's thread. Anything
14
+ * else holds: a thread no session here owns belongs to another process sharing
15
+ * the database, and once this process's dispatcher runs, its own resources are
16
+ * delivered by whichever process holds their dispatch lease, never by the
17
+ * shared dispatch schedule.
16
18
  */
17
- export declare function shouldHoldNotificationDelivery({ resourceId, ownedHere, dispatcher, }: {
19
+ export declare function shouldHoldNotificationDelivery({ resourceId, scope, ownedHere, dispatcher, }: {
18
20
  resourceId: string;
21
+ /** The thread's scope (`WakeSession.scope`), unset for a thread that carries none. */
22
+ scope?: string;
19
23
  /** Whether a session in this process owns the resource. */
20
24
  ownedHere: boolean;
21
25
  dispatcher: {
22
26
  readonly running: boolean;
23
27
  };
24
28
  }): boolean;
29
+ type SessionScope = {
30
+ resourceId: string;
31
+ scope?: string;
32
+ };
25
33
  export type ResourceNotificationDispatcherOptions = {
26
34
  getMastra: () => Mastra | undefined;
27
- /** The resources this process runs sessions for. */
28
- getResourceIds: () => Iterable<string>;
35
+ /**
36
+ * The resources this process runs sessions for, each with the thread scope
37
+ * (`threadScopeKey`) of a session serving it, unset when it has none.
38
+ */
39
+ getSessionScopes: () => Iterable<SessionScope> | Promise<Iterable<SessionScope>>;
29
40
  /**
30
41
  * Cross-process leases. When set, a resource's notifications are dispatched
31
- * only by the process holding its dispatch lease, so two processes serving
32
- * the same resource (worktrees of one repo) never both deliver them.
42
+ * only by the processes holding its dispatch leases, so two processes
43
+ * serving the same resource never both deliver one: the threads of each
44
+ * scope (each project, for processes in different directories) go to the
45
+ * process holding that scope's lease, and threads with no scope to the one
46
+ * holding the resource's own.
33
47
  */
34
48
  leases?: LeaseProvider;
35
49
  /** Unique to this process. */
@@ -56,4 +70,5 @@ export declare function createResourceNotificationDispatcher(options: ResourceNo
56
70
  tick: () => Promise<void>;
57
71
  };
58
72
  export type ResourceNotificationDispatcher = ReturnType<typeof createResourceNotificationDispatcher>;
73
+ export {};
59
74
  //# sourceMappingURL=notification-dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-dispatch.d.ts","sourceRoot":"","sources":["../../src/utils/notification-dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAWlD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEzE;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,EAC7C,UAAU,EACV,SAAS,EACT,UAAU,GACX,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3C,GAAG,OAAO,CAGV;AAED,MAAM,MAAM,qCAAqC,GAAG;IAClD,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACpC,oDAAoD;IACpD,cAAc,EAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvC;;;;OAIG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,qCAAqC;sBA2DhF,OAAO;aAGb,IAAI;YAMC,OAAO,CAAC,IAAI,CAAC;IAM3B,iEAAiE;gBA1BlD,OAAO,CAAC,IAAI,CAAC;EA6B/B;AAED,MAAM,MAAM,8BAA8B,GAAG,UAAU,CAAC,OAAO,oCAAoC,CAAC,CAAC"}
1
+ {"version":3,"file":"notification-dispatch.d.ts","sourceRoot":"","sources":["../../src/utils/notification-dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAoBlD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAIzF;AAED;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAAC,EAC7C,UAAU,EACV,KAAK,EACL,SAAS,EACT,UAAU,GACX,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3C,GAAG,OAAO,CAGV;AAED,KAAK,YAAY,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,qCAAqC,GAAG;IAClD,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACpC;;;OAGG;IACH,gBAAgB,EAAE,MAAM,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACjF;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,qCAAqC;sBA2EhF,OAAO;aAGb,IAAI;YAMC,OAAO,CAAC,IAAI,CAAC;IAM3B,iEAAiE;gBA1BlD,OAAO,CAAC,IAAI,CAAC;EA6B/B;AAED,MAAM,MAAM,8BAA8B,GAAG,UAAU,CAAC,OAAO,oCAAoC,CAAC,CAAC"}
@@ -4,26 +4,33 @@ import { AsyncLocalStorage } from "async_hooks";
4
4
  //#region src/utils/notification-dispatch.ts
5
5
  const DEFAULT_INTERVAL_MS = 1e4;
6
6
  const DEFAULT_LEASE_TTL_MS = 6e4;
7
- /** The resource whose notifications the current async context is dispatching. */
8
- const dispatchingFor = new AsyncLocalStorage();
7
+ /**
8
+ * The resource the current async context is dispatching notifications for,
9
+ * and which of its dispatch leases this pass holds.
10
+ */
11
+ const dispatching = new AsyncLocalStorage();
9
12
  /**
10
13
  * Whether this call runs inside this process's own dispatch pass for
11
- * `resourceId`, as opposed to the dispatch schedule every process sharing the
12
- * database takes turns running.
14
+ * `resourceId`, holding the lease for threads of `scope` (or with no `scope`,
15
+ * for threads that carry none), as opposed to the dispatch schedule every
16
+ * process sharing the database takes turns running.
13
17
  */
14
- function isDispatchingNotificationsFor(resourceId) {
15
- return dispatchingFor.getStore() === resourceId;
18
+ function isDispatchingNotificationsFor(resourceId, scope) {
19
+ const pass = dispatching.getStore();
20
+ if (pass?.resourceId !== resourceId) return false;
21
+ return scope === void 0 ? pass.unscoped : pass.scopes.has(scope);
16
22
  }
17
23
  /**
18
24
  * Whether a delivery-time policy should leave a due notification pending
19
25
  * (`hold`). Deliveries happen from this process's own dispatch pass for the
20
- * record's resource. Anything else holds: a thread no session here owns
21
- * belongs to another process sharing the database, and once this process's
22
- * dispatcher runs, its own resources are delivered by whichever process holds
23
- * their dispatch lease, never by the shared dispatch schedule.
26
+ * record's resource, under the lease that covers the record's thread. Anything
27
+ * else holds: a thread no session here owns belongs to another process sharing
28
+ * the database, and once this process's dispatcher runs, its own resources are
29
+ * delivered by whichever process holds their dispatch lease, never by the
30
+ * shared dispatch schedule.
24
31
  */
25
- function shouldHoldNotificationDelivery({ resourceId, ownedHere, dispatcher }) {
26
- if (isDispatchingNotificationsFor(resourceId)) return false;
32
+ function shouldHoldNotificationDelivery({ resourceId, scope, ownedHere, dispatcher }) {
33
+ if (isDispatchingNotificationsFor(resourceId, scope)) return false;
27
34
  return !ownedHere || dispatcher.running;
28
35
  }
29
36
  /**
@@ -39,19 +46,20 @@ function shouldHoldNotificationDelivery({ resourceId, ownedHere, dispatcher }) {
39
46
  function createResourceNotificationDispatcher(options) {
40
47
  const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
41
48
  const leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
49
+ /** The dispatch lease keys this process holds. */
42
50
  const held = /* @__PURE__ */ new Set();
43
51
  let timer;
44
52
  let ticking;
45
- const release = async (resourceId) => {
46
- held.delete(resourceId);
47
- await options.leases?.releaseLease(notificationDispatchLeaseKey(resourceId), options.owner).catch(options.onError);
53
+ const release = async (key) => {
54
+ held.delete(key);
55
+ await options.leases?.releaseLease(key, options.owner).catch(options.onError);
48
56
  };
49
- const claim = async (resourceId) => {
57
+ const claim = async (key) => {
50
58
  if (!options.leases) return true;
51
59
  try {
52
- const { acquired } = await options.leases.acquireLease(notificationDispatchLeaseKey(resourceId), options.owner, leaseTtlMs);
53
- if (acquired) held.add(resourceId);
54
- else held.delete(resourceId);
60
+ const { acquired } = await options.leases.acquireLease(key, options.owner, leaseTtlMs);
61
+ if (acquired) held.add(key);
62
+ else held.delete(key);
55
63
  return acquired;
56
64
  } catch (error) {
57
65
  options.onError?.(error);
@@ -62,12 +70,26 @@ function createResourceNotificationDispatcher(options) {
62
70
  const mastra = options.getMastra();
63
71
  const storage = await mastra?.getStorage()?.getStore("notifications");
64
72
  if (!mastra || !storage) return;
65
- const resourceIds = new Set(options.getResourceIds());
66
- for (const resourceId of [...held]) if (!resourceIds.has(resourceId)) await release(resourceId);
67
- for (const resourceId of resourceIds) {
68
- if (!timer || !await claim(resourceId)) continue;
73
+ const scopesByResource = /* @__PURE__ */ new Map();
74
+ for (const { resourceId, scope } of await options.getSessionScopes()) {
75
+ const scopes = scopesByResource.get(resourceId) ?? /* @__PURE__ */ new Set();
76
+ if (scope !== void 0) scopes.add(scope);
77
+ scopesByResource.set(resourceId, scopes);
78
+ }
79
+ const wanted = new Set([...scopesByResource].flatMap(([resourceId, scopes]) => [notificationDispatchLeaseKey(resourceId), ...[...scopes].map((scope) => notificationDispatchLeaseKey(resourceId, scope))]));
80
+ for (const key of [...held]) if (!wanted.has(key)) await release(key);
81
+ for (const [resourceId, scopes] of scopesByResource) {
82
+ if (!timer) continue;
83
+ const unscoped = await claim(notificationDispatchLeaseKey(resourceId));
84
+ const heldScopes = /* @__PURE__ */ new Set();
85
+ for (const scope of scopes) if (await claim(notificationDispatchLeaseKey(resourceId, scope))) heldScopes.add(scope);
86
+ if (!unscoped && heldScopes.size === 0) continue;
69
87
  try {
70
- await dispatchingFor.run(resourceId, () => dispatchDueNotifications({
88
+ await dispatching.run({
89
+ resourceId,
90
+ unscoped,
91
+ scopes: heldScopes
92
+ }, () => dispatchDueNotifications({
71
93
  mastra,
72
94
  storage,
73
95
  resourceId
@@ -1 +1 @@
1
- {"version":3,"file":"notification-dispatch.js","names":[],"sources":["../../src/utils/notification-dispatch.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\n\nimport type { LeaseProvider } from '@mastra/core/events';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { dispatchDueNotifications } from '@mastra/core/notifications';\n\nimport { notificationDispatchLeaseKey } from './signals-pubsub.js';\n\nconst DEFAULT_INTERVAL_MS = 10_000;\nconst DEFAULT_LEASE_TTL_MS = 60_000;\n\n/** The resource whose notifications the current async context is dispatching. */\nconst dispatchingFor = new AsyncLocalStorage<string>();\n\n/**\n * Whether this call runs inside this process's own dispatch pass for\n * `resourceId`, as opposed to the dispatch schedule every process sharing the\n * database takes turns running.\n */\nexport function isDispatchingNotificationsFor(resourceId: string): boolean {\n return dispatchingFor.getStore() === resourceId;\n}\n\n/**\n * Whether a delivery-time policy should leave a due notification pending\n * (`hold`). Deliveries happen from this process's own dispatch pass for the\n * record's resource. Anything else holds: a thread no session here owns\n * belongs to another process sharing the database, and once this process's\n * dispatcher runs, its own resources are delivered by whichever process holds\n * their dispatch lease, never by the shared dispatch schedule.\n */\nexport function shouldHoldNotificationDelivery({\n resourceId,\n ownedHere,\n dispatcher,\n}: {\n resourceId: string;\n /** Whether a session in this process owns the resource. */\n ownedHere: boolean;\n dispatcher: { readonly running: boolean };\n}): boolean {\n if (isDispatchingNotificationsFor(resourceId)) return false;\n return !ownedHere || dispatcher.running;\n}\n\nexport type ResourceNotificationDispatcherOptions = {\n getMastra: () => Mastra | undefined;\n /** The resources this process runs sessions for. */\n getResourceIds: () => Iterable<string>;\n /**\n * Cross-process leases. When set, a resource's notifications are dispatched\n * only by the process holding its dispatch lease, so two processes serving\n * the same resource (worktrees of one repo) never both deliver them.\n */\n leases?: LeaseProvider;\n /** Unique to this process. */\n owner: string;\n intervalMs?: number;\n leaseTtlMs?: number;\n onError?: (error: unknown) => void;\n};\n\n/**\n * Delivers the due notifications of the resources this process serves, on a\n * timer of its own.\n *\n * Every Mastra Code process shares one database, so one notification store and\n * one dispatch schedule, and the scheduler gives each fire to whichever process\n * claims it first, usually the same one for as long as it runs. That process\n * cannot run another project's thread, so its delivery policy holds those\n * records. Each process therefore dispatches its own resources here.\n */\nexport function createResourceNotificationDispatcher(options: ResourceNotificationDispatcherOptions) {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;\n const held = new Set<string>();\n let timer: ReturnType<typeof setInterval> | undefined;\n let ticking: Promise<void> | undefined;\n\n const release = async (resourceId: string) => {\n held.delete(resourceId);\n await options.leases?.releaseLease(notificationDispatchLeaseKey(resourceId), options.owner).catch(options.onError);\n };\n\n const claim = async (resourceId: string): Promise<boolean> => {\n if (!options.leases) return true;\n try {\n // Acquiring a lease this process already holds renews it.\n const { acquired } = await options.leases.acquireLease(\n notificationDispatchLeaseKey(resourceId),\n options.owner,\n leaseTtlMs,\n );\n if (acquired) held.add(resourceId);\n else held.delete(resourceId);\n return acquired;\n } catch (error) {\n options.onError?.(error);\n return false;\n }\n };\n\n const runTick = async () => {\n const mastra = options.getMastra();\n const storage = await mastra?.getStorage()?.getStore('notifications');\n if (!mastra || !storage) return;\n const resourceIds = new Set(options.getResourceIds());\n for (const resourceId of [...held]) {\n if (!resourceIds.has(resourceId)) await release(resourceId);\n }\n for (const resourceId of resourceIds) {\n if (!timer || !(await claim(resourceId))) continue;\n try {\n await dispatchingFor.run(resourceId, () => dispatchDueNotifications({ mastra, storage, resourceId }));\n } catch (error) {\n options.onError?.(error);\n }\n }\n };\n\n const tick = (): Promise<void> => {\n // Ticks run from a timer, so a failure must be reported, never left as an unhandled rejection.\n ticking ??= runTick()\n .catch(error => options.onError?.(error))\n .finally(() => {\n ticking = undefined;\n });\n return ticking;\n };\n\n return {\n get running(): boolean {\n return timer !== undefined;\n },\n start(): void {\n if (timer) return;\n timer = setInterval(() => void tick(), intervalMs);\n timer.unref?.();\n void tick();\n },\n async stop(): Promise<void> {\n if (timer) clearInterval(timer);\n timer = undefined;\n await ticking;\n await Promise.all([...held].map(release));\n },\n /** Runs one dispatch pass now. Only dispatches while started. */\n tick,\n };\n}\n\nexport type ResourceNotificationDispatcher = ReturnType<typeof createResourceNotificationDispatcher>;\n"],"mappings":";;;;AAQA,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;;AAG7B,MAAM,iBAAiB,IAAI,kBAA0B;;;;;;AAOrD,SAAgB,8BAA8B,YAA6B;CACzE,OAAO,eAAe,SAAS,MAAM;AACvC;;;;;;;;;AAUA,SAAgB,+BAA+B,EAC7C,YACA,WACA,cAMU;CACV,IAAI,8BAA8B,UAAU,GAAG,OAAO;CACtD,OAAO,CAAC,aAAa,WAAW;AAClC;;;;;;;;;;;AA6BA,SAAgB,qCAAqC,SAAgD;CACnG,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI;CACJ,IAAI;CAEJ,MAAM,UAAU,OAAO,eAAuB;EAC5C,KAAK,OAAO,UAAU;EACtB,MAAM,QAAQ,QAAQ,aAAa,6BAA6B,UAAU,GAAG,QAAQ,KAAK,CAAC,CAAC,MAAM,QAAQ,OAAO;CACnH;CAEA,MAAM,QAAQ,OAAO,eAAyC;EAC5D,IAAI,CAAC,QAAQ,QAAQ,OAAO;EAC5B,IAAI;GAEF,MAAM,EAAE,aAAa,MAAM,QAAQ,OAAO,aACxC,6BAA6B,UAAU,GACvC,QAAQ,OACR,UACF;GACA,IAAI,UAAU,KAAK,IAAI,UAAU;QAC5B,KAAK,OAAO,UAAU;GAC3B,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,UAAU,KAAK;GACvB,OAAO;EACT;CACF;CAEA,MAAM,UAAU,YAAY;EAC1B,MAAM,SAAS,QAAQ,UAAU;EACjC,MAAM,UAAU,MAAM,QAAQ,WAAW,CAAC,EAAE,SAAS,eAAe;EACpE,IAAI,CAAC,UAAU,CAAC,SAAS;EACzB,MAAM,cAAc,IAAI,IAAI,QAAQ,eAAe,CAAC;EACpD,KAAK,MAAM,cAAc,CAAC,GAAG,IAAI,GAC/B,IAAI,CAAC,YAAY,IAAI,UAAU,GAAG,MAAM,QAAQ,UAAU;EAE5D,KAAK,MAAM,cAAc,aAAa;GACpC,IAAI,CAAC,SAAS,CAAE,MAAM,MAAM,UAAU,GAAI;GAC1C,IAAI;IACF,MAAM,eAAe,IAAI,kBAAkB,yBAAyB;KAAE;KAAQ;KAAS;IAAW,CAAC,CAAC;GACtG,SAAS,OAAO;IACd,QAAQ,UAAU,KAAK;GACzB;EACF;CACF;CAEA,MAAM,aAA4B;EAEhC,YAAY,QAAQ,CAAC,CAClB,OAAM,UAAS,QAAQ,UAAU,KAAK,CAAC,CAAC,CACxC,cAAc;GACb,UAAU,KAAA;EACZ,CAAC;EACH,OAAO;CACT;CAEA,OAAO;EACL,IAAI,UAAmB;GACrB,OAAO,UAAU,KAAA;EACnB;EACA,QAAc;GACZ,IAAI,OAAO;GACX,QAAQ,kBAAkB,KAAK,KAAK,GAAG,UAAU;GACjD,MAAM,QAAQ;GACd,KAAU;EACZ;EACA,MAAM,OAAsB;GAC1B,IAAI,OAAO,cAAc,KAAK;GAC9B,QAAQ,KAAA;GACR,MAAM;GACN,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC;EAC1C;;EAEA;CACF;AACF"}
1
+ {"version":3,"file":"notification-dispatch.js","names":[],"sources":["../../src/utils/notification-dispatch.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\n\nimport type { LeaseProvider } from '@mastra/core/events';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { dispatchDueNotifications } from '@mastra/core/notifications';\n\nimport { notificationDispatchLeaseKey } from './signals-pubsub.js';\n\nconst DEFAULT_INTERVAL_MS = 10_000;\nconst DEFAULT_LEASE_TTL_MS = 60_000;\n\n/**\n * The resource the current async context is dispatching notifications for,\n * and which of its dispatch leases this pass holds.\n */\nconst dispatching = new AsyncLocalStorage<{\n resourceId: string;\n /** The resource's own lease: its threads that carry no scope. */\n unscoped: boolean;\n /** The thread scopes whose leases this pass holds. */\n scopes: ReadonlySet<string>;\n}>();\n\n/**\n * Whether this call runs inside this process's own dispatch pass for\n * `resourceId`, holding the lease for threads of `scope` (or with no `scope`,\n * for threads that carry none), as opposed to the dispatch schedule every\n * process sharing the database takes turns running.\n */\nexport function isDispatchingNotificationsFor(resourceId: string, scope?: string): boolean {\n const pass = dispatching.getStore();\n if (pass?.resourceId !== resourceId) return false;\n return scope === undefined ? pass.unscoped : pass.scopes.has(scope);\n}\n\n/**\n * Whether a delivery-time policy should leave a due notification pending\n * (`hold`). Deliveries happen from this process's own dispatch pass for the\n * record's resource, under the lease that covers the record's thread. Anything\n * else holds: a thread no session here owns belongs to another process sharing\n * the database, and once this process's dispatcher runs, its own resources are\n * delivered by whichever process holds their dispatch lease, never by the\n * shared dispatch schedule.\n */\nexport function shouldHoldNotificationDelivery({\n resourceId,\n scope,\n ownedHere,\n dispatcher,\n}: {\n resourceId: string;\n /** The thread's scope (`WakeSession.scope`), unset for a thread that carries none. */\n scope?: string;\n /** Whether a session in this process owns the resource. */\n ownedHere: boolean;\n dispatcher: { readonly running: boolean };\n}): boolean {\n if (isDispatchingNotificationsFor(resourceId, scope)) return false;\n return !ownedHere || dispatcher.running;\n}\n\ntype SessionScope = { resourceId: string; scope?: string };\n\nexport type ResourceNotificationDispatcherOptions = {\n getMastra: () => Mastra | undefined;\n /**\n * The resources this process runs sessions for, each with the thread scope\n * (`threadScopeKey`) of a session serving it, unset when it has none.\n */\n getSessionScopes: () => Iterable<SessionScope> | Promise<Iterable<SessionScope>>;\n /**\n * Cross-process leases. When set, a resource's notifications are dispatched\n * only by the processes holding its dispatch leases, so two processes\n * serving the same resource never both deliver one: the threads of each\n * scope (each project, for processes in different directories) go to the\n * process holding that scope's lease, and threads with no scope to the one\n * holding the resource's own.\n */\n leases?: LeaseProvider;\n /** Unique to this process. */\n owner: string;\n intervalMs?: number;\n leaseTtlMs?: number;\n onError?: (error: unknown) => void;\n};\n\n/**\n * Delivers the due notifications of the resources this process serves, on a\n * timer of its own.\n *\n * Every Mastra Code process shares one database, so one notification store and\n * one dispatch schedule, and the scheduler gives each fire to whichever process\n * claims it first, usually the same one for as long as it runs. That process\n * cannot run another project's thread, so its delivery policy holds those\n * records. Each process therefore dispatches its own resources here.\n */\nexport function createResourceNotificationDispatcher(options: ResourceNotificationDispatcherOptions) {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;\n /** The dispatch lease keys this process holds. */\n const held = new Set<string>();\n let timer: ReturnType<typeof setInterval> | undefined;\n let ticking: Promise<void> | undefined;\n\n const release = async (key: string) => {\n held.delete(key);\n await options.leases?.releaseLease(key, options.owner).catch(options.onError);\n };\n\n const claim = async (key: string): Promise<boolean> => {\n if (!options.leases) return true;\n try {\n // Acquiring a lease this process already holds renews it.\n const { acquired } = await options.leases.acquireLease(key, options.owner, leaseTtlMs);\n if (acquired) held.add(key);\n else held.delete(key);\n return acquired;\n } catch (error) {\n options.onError?.(error);\n return false;\n }\n };\n\n const runTick = async () => {\n const mastra = options.getMastra();\n const storage = await mastra?.getStorage()?.getStore('notifications');\n if (!mastra || !storage) return;\n const scopesByResource = new Map<string, Set<string>>();\n for (const { resourceId, scope } of await options.getSessionScopes()) {\n const scopes = scopesByResource.get(resourceId) ?? new Set<string>();\n if (scope !== undefined) scopes.add(scope);\n scopesByResource.set(resourceId, scopes);\n }\n const wanted = new Set(\n [...scopesByResource].flatMap(([resourceId, scopes]) => [\n notificationDispatchLeaseKey(resourceId),\n ...[...scopes].map(scope => notificationDispatchLeaseKey(resourceId, scope)),\n ]),\n );\n for (const key of [...held]) {\n if (!wanted.has(key)) await release(key);\n }\n for (const [resourceId, scopes] of scopesByResource) {\n if (!timer) continue;\n const unscoped = await claim(notificationDispatchLeaseKey(resourceId));\n const heldScopes = new Set<string>();\n for (const scope of scopes) {\n if (await claim(notificationDispatchLeaseKey(resourceId, scope))) heldScopes.add(scope);\n }\n if (!unscoped && heldScopes.size === 0) continue;\n try {\n await dispatching.run({ resourceId, unscoped, scopes: heldScopes }, () =>\n dispatchDueNotifications({ mastra, storage, resourceId }),\n );\n } catch (error) {\n options.onError?.(error);\n }\n }\n };\n\n const tick = (): Promise<void> => {\n // Ticks run from a timer, so a failure must be reported, never left as an unhandled rejection.\n ticking ??= runTick()\n .catch(error => options.onError?.(error))\n .finally(() => {\n ticking = undefined;\n });\n return ticking;\n };\n\n return {\n get running(): boolean {\n return timer !== undefined;\n },\n start(): void {\n if (timer) return;\n timer = setInterval(() => void tick(), intervalMs);\n timer.unref?.();\n void tick();\n },\n async stop(): Promise<void> {\n if (timer) clearInterval(timer);\n timer = undefined;\n await ticking;\n await Promise.all([...held].map(release));\n },\n /** Runs one dispatch pass now. Only dispatches while started. */\n tick,\n };\n}\n\nexport type ResourceNotificationDispatcher = ReturnType<typeof createResourceNotificationDispatcher>;\n"],"mappings":"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"}
@@ -15,10 +15,12 @@ export type SignalsPubSubOptions = {
15
15
  };
16
16
  /**
17
17
  * The lease that lets exactly one process dispatch a resource's due
18
- * notifications. Held in that resource's directory, so every process serving
19
- * the resource contends for it whichever project it was started in.
18
+ * notifications: those of the resource's threads that carry `scope` (a
19
+ * `threadScopeKey`, one project's threads), or with no `scope`, those of its
20
+ * threads that carry none. Held in that resource's directory, so every process
21
+ * serving the resource contends for it whichever project it was started in.
20
22
  */
21
- export declare function notificationDispatchLeaseKey(resourceId: string): string;
23
+ export declare function notificationDispatchLeaseKey(resourceId: string, scope?: string): string;
22
24
  /**
23
25
  * A PubSub that manages one Unix socket per topic for cross-process signal
24
26
  * coordination between mastracode processes on this machine.
@@ -1 +1 @@
1
- {"version":3,"file":"signals-pubsub.d.ts","sourceRoot":"","sources":["../../src/utils/signals-pubsub.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAyBrH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAsCF;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvE;AA0DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,cAAM,aAAc,SAAQ,MAAM;;gBAcpB,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB;IAsBlE,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;IAED;;;;OAIG;IACH,gBAAgB,IAAI,aAAa;IAI3B,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAIV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE;;;;;OAKG;IACY,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAc5B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAoMvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAErG"}
1
+ {"version":3,"file":"signals-pubsub.d.ts","sourceRoot":"","sources":["../../src/utils/signals-pubsub.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAyBrH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAsCF;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvF;AA0DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,cAAM,aAAc,SAAQ,MAAM;;gBAcpB,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB;IAsBlE,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;IAED;;;;OAIG;IACH,gBAAgB,IAAI,aAAa;IAI3B,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAIV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE;;;;;OAKG;IACY,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAc5B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAoMvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAErG"}
@@ -57,19 +57,21 @@ function decodeThreadTopic(topic) {
57
57
  }
58
58
  /**
59
59
  * The lease that lets exactly one process dispatch a resource's due
60
- * notifications. Held in that resource's directory, so every process serving
61
- * the resource contends for it whichever project it was started in.
60
+ * notifications: those of the resource's threads that carry `scope` (a
61
+ * `threadScopeKey`, one project's threads), or with no `scope`, those of its
62
+ * threads that carry none. Held in that resource's directory, so every process
63
+ * serving the resource contends for it whichever project it was started in.
62
64
  */
63
- function notificationDispatchLeaseKey(resourceId) {
64
- return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}`;
65
+ function notificationDispatchLeaseKey(resourceId, scope) {
66
+ return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}${scope === void 0 ? "" : `${THREAD_KEY_SEPARATOR}${scope}`}`;
65
67
  }
66
68
  /**
67
69
  * The resource a lease key belongs to: a thread's run lease and claim lease,
68
- * and a resource's notification dispatch lease.
70
+ * and a resource's notification dispatch leases.
69
71
  */
70
72
  function resourceOfLeaseKey(key) {
71
73
  if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {
72
- const resourceId = key.slice(22);
74
+ const [resourceId = ""] = key.slice(22).split(THREAD_KEY_SEPARATOR, 1);
73
75
  return isSafePathSegment(resourceId) ? resourceId : void 0;
74
76
  }
75
77
  return resourceOfThreadKey(key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(13) : key);
@@ -1 +1 @@
1
- {"version":3,"file":"signals-pubsub.js","names":["#resourceId","#rootDir","#leaseSockets","#sockets","#pending","#refs","#liveSubscriptions","#clearGenerations","#closing","#leaseProvider","#sharedPeerDiscovery","#canShareDiscovery","#leasesFor","#closed","#publishTo","#dirFor","#subscribeTo","#unsubscribeFrom","#release","#isRefCounted","#getOrCreate","#retain","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\n\nimport { PubSub, UnixSocketPubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\n\nconst DEFAULT_SOCKET_ROOT = '/tmp/mc';\nconst SHARED_SCOPE_DIR = '_shared';\nconst THREAD_STREAM_PREFIX = 'agent.thread-stream.';\nconst THREAD_KEY_SEPARATOR = '\\0';\nconst THREAD_CLAIM_LEASE_PREFIX = 'thread-claim:';\nconst NOTIFICATION_DISPATCH_LEASE_PREFIX = 'notification-dispatch:';\nconst LEASE_SOCKET_NAME = '.leases.sock';\nconst OWNER_DISCOVERY_TOPIC = 'agent.thread-owner-discovery';\nconst PEER_DISCOVERY_TOPIC = 'agent.thread-peer-discovery';\nconst RESERVED_SOCKET_NAME = /^agent_thread-(?:peer|owner)-discovery(?:_|$)/i;\nconst MAX_PATH_SEGMENT_LENGTH = 128;\nconst UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';\nconst DISCOVERY_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-(?:peer|owner)-discovery\\\\.${UUID_PATTERN}$`, 'i');\nconst IDLE_ACCEPTANCE_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-stream\\\\..+\\\\.idle-acceptance\\\\.${UUID_PATTERN}$`, 'i');\n\n/**\n * One-shot reply topics are minted per request (`<topic>.<uuid>`), used for a\n * single round trip, and never published to again once the request settles.\n */\nfunction isEphemeralTopic(topic: string): boolean {\n return DISCOVERY_REPLY_TOPIC.test(topic) || IDLE_ACCEPTANCE_REPLY_TOPIC.test(topic);\n}\n\nexport type SignalsPubSubOptions = {\n /**\n * Also list this process's threads to, and find threads in, other projects on\n * this machine: peer discovery runs in `<root>/_shared/` instead of this\n * resource's directory.\n */\n sharedAgentDiscovery?: boolean;\n /**\n * Directory that holds every resource's socket directory. Defaults to the\n * absolute path in `MASTRACODE_SIGNALS_SOCKET_ROOT`, else `/tmp/mc`.\n */\n rootDir?: string;\n};\n\nfunction socketRootFromEnv(): string {\n const root = process.env.MASTRACODE_SIGNALS_SOCKET_ROOT;\n return root && isAbsolute(root) ? root : DEFAULT_SOCKET_ROOT;\n}\n\n/**\n * Whether an id can name a single directory: no path separators or control\n * characters, not `.`/`..`, and bounded in length.\n */\nfunction isSafePathSegment(value: string): boolean {\n if (!value || value.length > MAX_PATH_SEGMENT_LENGTH || value === '.' || value === '..') return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/** The resource a thread key (`<resourceId>\\0<threadId>`) belongs to. */\nfunction resourceOfThreadKey(key: string): string | undefined {\n const separatorIdx = key.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx <= 0) return undefined;\n const resourceId = key.slice(0, separatorIdx);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n}\n\n/** Decodes `agent.thread-stream.<encoded resourceId\\0threadId>[...]`. */\nfunction decodeThreadTopic(topic: string): string | undefined {\n if (!topic.startsWith(THREAD_STREAM_PREFIX)) return undefined;\n try {\n return decodeURIComponent(topic.slice(THREAD_STREAM_PREFIX.length));\n } catch {\n return undefined;\n }\n}\n\n/**\n * The lease that lets exactly one process dispatch a resource's due\n * notifications. Held in that resource's directory, so every process serving\n * the resource contends for it whichever project it was started in.\n */\nexport function notificationDispatchLeaseKey(resourceId: string): string {\n return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}`;\n}\n\n/**\n * The resource a lease key belongs to: a thread's run lease and claim lease,\n * and a resource's notification dispatch lease.\n */\nfunction resourceOfLeaseKey(key: string): string | undefined {\n if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {\n const resourceId = key.slice(NOTIFICATION_DISPATCH_LEASE_PREFIX.length);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n }\n return resourceOfThreadKey(\n key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(THREAD_CLAIM_LEASE_PREFIX.length) : key,\n );\n}\n\nfunction isOwnerDiscoveryTopic(topic: string): boolean {\n return topic === OWNER_DISCOVERY_TOPIC || topic.startsWith(`${OWNER_DISCOVERY_TOPIC}.`);\n}\n\nfunction isPeerDiscoveryTopic(topic: string): boolean {\n return topic === PEER_DISCOVERY_TOPIC || topic.startsWith(`${PEER_DISCOVERY_TOPIC}.`);\n}\n\n/**\n * Whether a threadId can be the base name of a thread's socket file: no\n * separators or control characters, not `.`/`..`, and not a name another\n * socket in the same directory already uses (the lease socket, and the\n * sanitized peer/owner discovery topics and their reply topics), compared\n * case-insensitively because default macOS and Windows filesystems are.\n */\nfunction isSafeFileName(value: string): boolean {\n if (!value || value === '.' || value === '..' || `${value}.sock`.toLowerCase() === LEASE_SOCKET_NAME) return false;\n if (RESERVED_SOCKET_NAME.test(value)) return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/**\n * Derive a filesystem-safe key for the topic. Thread-stream topics embed\n * a threadId; all other topics use a sanitized version of the topic name.\n */\nfunction topicKey(topic: string): string {\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) {\n const separatorIdx = decoded.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx !== -1) return decoded.slice(separatorIdx + 1);\n }\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n}\n\nfunction socketKey(dir: string, topic: string): string {\n return `${dir}/${topicKey(topic)}`;\n}\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination between mastracode processes on this machine.\n *\n * Socket paths use `<root>/<dir>/<sanitized-topic>.sock` (root `/tmp/mc`)\n * for inspectability and automatic OS cleanup. Each topic gets its own\n * socket, so broker election and message routing are per-topic.\n *\n * Every process shares one database, so any process can act on any project's\n * thread — the notification dispatcher, for one, delivers whatever is due.\n * Whatever coordinates a thread therefore lives with the thread, not with the\n * process touching it:\n *\n * - A thread's stream topic and its leases (the run lease and the claim\n * lease) live in the directory of the resource that owns the thread, so a\n * process from another project sees that thread's active run and claim.\n * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds\n * a thread claimed can reach the owner and hand it the signal. It only\n * answers for a thread the asker names, so it discloses nothing new.\n * - Peer discovery (which lists threads) runs in `<root>/_shared/` when\n * `sharedAgentDiscovery` is set, so processes in every project can list each\n * other's threads. Without it, it stays in this resource's directory.\n * - Everything else stays in this process's own resource directory.\n *\n * Stale sockets from crashed processes are handled by\n * {@link UnixSocketPubSub}'s built-in election logic: it detects\n * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.\n * No blanket cleanup is needed here — that would break concurrent\n * mc instances sharing a directory.\n *\n * One-shot reply topics (discovery and idle-acceptance replies) are\n * reference-counted: every in-flight publish/subscribe and every live\n * subscription holds a reference, and the topic's socket is closed as soon as\n * the count drops to zero. Without this each request would keep a socket, a\n * file descriptor and (for the broker) a socket file for the process lifetime.\n * With `sharedAgentDiscovery`, another resource's thread topics are counted the\n * same way, so a process that signals many threads elsewhere does not keep a\n * socket open for each.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #rootDir: string;\n readonly #leaseSockets = new Map<string, UnixSocketPubSub>();\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n readonly #refs = new Map<string, number>();\n readonly #liveSubscriptions = new Map<string, Set<EventCallback>>();\n readonly #clearGenerations = new Map<string, number>();\n readonly #closing = new Map<string, Promise<void>>();\n readonly #leaseProvider: LeaseProvider;\n readonly #sharedPeerDiscovery: boolean;\n #closed = false;\n\n constructor(resourceId: string, options: SignalsPubSubOptions = {}) {\n super();\n this.#resourceId = resourceId;\n this.#rootDir = options.rootDir ?? socketRootFromEnv();\n this.#sharedPeerDiscovery = Boolean(options.sharedAgentDiscovery) && this.#canShareDiscovery();\n // Created eagerly, as before, so this resource's lease socket exists from the start.\n this.#leasesFor(resourceId);\n const route = (key: string) => {\n // Lease sockets are created per resource on demand; once closed, never\n // reopen one (a lease renewal timer may outlive close()).\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);\n };\n this.#leaseProvider = {\n acquireLease: async (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),\n getLeaseOwner: async key => route(key).getLeaseOwner(key),\n releaseLease: async (key, owner) => route(key).releaseLease(key, owner),\n renewLease: async (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),\n transferLease: async (key, fromOwner, toOwner, ttlMs) => route(key).transferLease(key, fromOwner, toOwner, ttlMs),\n };\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /**\n * Filesystem leases. A thread's run and claim leases are held in the\n * directory of the thread's resource, so every process contends for the\n * same lease; any other key stays in this resource's directory.\n */\n getLeaseProvider(): LeaseProvider {\n return this.#leaseProvider;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n await this.#publishTo(topic, this.#dirFor(topic), event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n await this.#subscribeTo(topic, this.#dirFor(topic), cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n await this.#unsubscribeFrom(topic, this.#dirFor(topic), cb);\n }\n\n /**\n * Drops a one-shot reply topic: its remaining subscriptions release their\n * references and the socket closes once no operation still uses it. Other\n * topics (workflow events, thread streams) have subscribers in other\n * processes that need the broker, so clearing them is a no-op.\n */\n override async clearTopic(topic: string): Promise<void> {\n if (!isEphemeralTopic(topic)) return;\n const key = socketKey(this.#dirFor(topic), topic);\n if (!this.#refs.has(key)) return;\n this.#clearGenerations.set(key, (this.#clearGenerations.get(key) ?? 0) + 1);\n const subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) return;\n this.#liveSubscriptions.delete(key);\n for (let i = 0; i < subscriptions.size; i++) this.#release(key);\n }\n\n async flush(): Promise<void> {\n await Promise.all([...this.#sockets.values()].map(s => s.flush()));\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n await Promise.allSettled([\n ...[...this.#leaseSockets.values()].map(s => s.close()),\n ...[...this.#sockets.values()].map(s => s.close()),\n ...this.#closing.values(),\n ]);\n this.#sockets.clear();\n this.#leaseSockets.clear();\n this.#refs.clear();\n this.#liveSubscriptions.clear();\n this.#clearGenerations.clear();\n }\n\n /** Get the underlying socket for a topic (for testing/inspection). */\n getSocket(topic: string): UnixSocketPubSub | undefined {\n return this.#sockets.get(socketKey(this.#dirFor(topic), topic));\n }\n\n #canShareDiscovery(): boolean {\n if (isSafePathSegment(this.#resourceId) && this.#resourceId !== SHARED_SCOPE_DIR) return true;\n console.warn(\n `Cross-project agent discovery is disabled: resource id ${JSON.stringify(this.#resourceId)} cannot be used as a directory name, so other projects could not reach this instance.`,\n );\n return false;\n }\n\n /**\n * One-shot reply topics close once unused. With shared discovery, so do thread\n * topics routed to another resource; without it, they behave as before.\n */\n #isRefCounted(topic: string): boolean {\n if (isEphemeralTopic(topic)) return true;\n if (!this.#sharedPeerDiscovery) return false;\n const decoded = decodeThreadTopic(topic);\n if (decoded === undefined) return false;\n const resourceId = resourceOfThreadKey(decoded);\n return resourceId !== undefined && resourceId !== this.#resourceId;\n }\n\n async #publishTo(\n topic: string,\n dir: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n return;\n }\n const key = socketKey(dir, topic);\n // Retain synchronously so a concurrent release cannot close the socket\n // this publish is about to use.\n this.#retain(key);\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n } finally {\n this.#release(key);\n }\n }\n\n async #subscribeTo(topic: string, dir: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n return;\n }\n const key = socketKey(dir, topic);\n this.#retain(key);\n const generation = this.#clearGenerations.get(key) ?? 0;\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n if ((this.#clearGenerations.get(key) ?? 0) !== generation) {\n // The topic was cleared while this subscribe was in flight.\n await socket.unsubscribe(topic, cb);\n return;\n }\n let subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) {\n subscriptions = new Set();\n this.#liveSubscriptions.set(key, subscriptions);\n }\n if (!subscriptions.has(cb)) {\n subscriptions.add(cb);\n this.#retain(key);\n }\n } finally {\n this.#release(key);\n }\n }\n\n async #unsubscribeFrom(topic: string, dir: string, cb: EventCallback): Promise<void> {\n const key = socketKey(dir, topic);\n const socket = this.#sockets.get(key);\n const subscriptions = this.#isRefCounted(topic) ? this.#liveSubscriptions.get(key) : undefined;\n if (!subscriptions?.delete(cb)) {\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\n return;\n }\n if (subscriptions.size === 0) this.#liveSubscriptions.delete(key);\n try {\n await socket?.unsubscribe(topic, cb);\n } finally {\n this.#release(key);\n }\n }\n\n #leasesFor(resourceId: string): UnixSocketPubSub {\n let socket = this.#leaseSockets.get(resourceId);\n if (!socket) {\n socket = new UnixSocketPubSub(join(this.#rootDir, resourceId, LEASE_SOCKET_NAME));\n this.#leaseSockets.set(resourceId, socket);\n }\n return socket;\n }\n\n /** The directory, under the root, that holds a topic's socket. */\n #dirFor(topic: string): string {\n if (isOwnerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n if (this.#sharedPeerDiscovery && isPeerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) return resourceOfThreadKey(decoded) ?? this.#resourceId;\n return this.#resourceId;\n }\n\n #retain(key: string): void {\n this.#refs.set(key, (this.#refs.get(key) ?? 0) + 1);\n }\n\n #release(key: string): void {\n const refs = (this.#refs.get(key) ?? 0) - 1;\n if (refs > 0) {\n this.#refs.set(key, refs);\n return;\n }\n this.#refs.delete(key);\n this.#clearGenerations.delete(key);\n this.#liveSubscriptions.delete(key);\n const socket = this.#sockets.get(key);\n if (!socket) return;\n this.#sockets.delete(key);\n const closing: Promise<void> = socket\n .close()\n .catch(() => {})\n .finally(() => {\n if (this.#closing.get(key) === closing) this.#closing.delete(key);\n });\n this.#closing.set(key, closing);\n }\n\n async #getOrCreate(topic: string, dir: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = socketKey(dir, topic);\n // A socket for this key may still be closing after its last reference\n // was released; never hand out the closing instance.\n const closing = this.#closing.get(key);\n if (closing) {\n await closing;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n }\n const existing = this.#sockets.get(key);\n if (existing) return existing;\n // Deduplicate concurrent callers so only one socket is created per topic.\n let inflight = this.#pending.get(key);\n if (!inflight) {\n inflight = this.#initSocket(topic, dir, key);\n this.#pending.set(key, inflight);\n }\n const socket = await inflight;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return socket;\n }\n\n async #initSocket(topic: string, dir: string, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic, dir);\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const socket = new UnixSocketPubSub(socketPath);\n this.#sockets.set(key, socket);\n return socket;\n } finally {\n this.#pending.delete(key);\n }\n }\n\n async #socketPath(topic: string, scope: string): Promise<string> {\n let key = topicKey(topic);\n // A threadId names a file in its resource's directory, so never let it\n // reach outside that directory, regardless of who owns the thread or\n // whether cross-project discovery is enabled.\n if (decodeThreadTopic(topic) !== undefined && !isSafeFileName(key)) {\n throw new Error('Cannot route an agent thread topic whose threadId is not a safe file name');\n }\n const dir = join(this.#rootDir, scope);\n await mkdir(dir, { recursive: true });\n const candidate = join(dir, `${key}.sock`);\n // macOS sun_path limit is 104 bytes; Linux is 108. Use 104 as the\n // conservative bound. When the path is too long, replace the key with\n // a short hash so the socket can still be created.\n if (Buffer.byteLength(candidate) > 104) {\n key = Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)))\n .toString('hex')\n .slice(0, 16);\n return join(dir, `${key}.sock`);\n }\n return candidate;\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination between mastracode processes.\n *\n * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and\n * leases live under its own resource's directory and thread-owner discovery\n * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). With\n * `sharedAgentDiscovery`, peer discovery runs there too. Stale sockets from\n * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s\n * broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string, options?: SignalsPubSubOptions): SignalsPubSub {\n return new SignalsPubSub(resourceId, options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"signals-pubsub.js","names":["#resourceId","#rootDir","#leaseSockets","#sockets","#pending","#refs","#liveSubscriptions","#clearGenerations","#closing","#leaseProvider","#sharedPeerDiscovery","#canShareDiscovery","#leasesFor","#closed","#publishTo","#dirFor","#subscribeTo","#unsubscribeFrom","#release","#isRefCounted","#getOrCreate","#retain","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\n\nimport { PubSub, UnixSocketPubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\n\nconst DEFAULT_SOCKET_ROOT = '/tmp/mc';\nconst SHARED_SCOPE_DIR = '_shared';\nconst THREAD_STREAM_PREFIX = 'agent.thread-stream.';\nconst THREAD_KEY_SEPARATOR = '\\0';\nconst THREAD_CLAIM_LEASE_PREFIX = 'thread-claim:';\nconst NOTIFICATION_DISPATCH_LEASE_PREFIX = 'notification-dispatch:';\nconst LEASE_SOCKET_NAME = '.leases.sock';\nconst OWNER_DISCOVERY_TOPIC = 'agent.thread-owner-discovery';\nconst PEER_DISCOVERY_TOPIC = 'agent.thread-peer-discovery';\nconst RESERVED_SOCKET_NAME = /^agent_thread-(?:peer|owner)-discovery(?:_|$)/i;\nconst MAX_PATH_SEGMENT_LENGTH = 128;\nconst UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';\nconst DISCOVERY_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-(?:peer|owner)-discovery\\\\.${UUID_PATTERN}$`, 'i');\nconst IDLE_ACCEPTANCE_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-stream\\\\..+\\\\.idle-acceptance\\\\.${UUID_PATTERN}$`, 'i');\n\n/**\n * One-shot reply topics are minted per request (`<topic>.<uuid>`), used for a\n * single round trip, and never published to again once the request settles.\n */\nfunction isEphemeralTopic(topic: string): boolean {\n return DISCOVERY_REPLY_TOPIC.test(topic) || IDLE_ACCEPTANCE_REPLY_TOPIC.test(topic);\n}\n\nexport type SignalsPubSubOptions = {\n /**\n * Also list this process's threads to, and find threads in, other projects on\n * this machine: peer discovery runs in `<root>/_shared/` instead of this\n * resource's directory.\n */\n sharedAgentDiscovery?: boolean;\n /**\n * Directory that holds every resource's socket directory. Defaults to the\n * absolute path in `MASTRACODE_SIGNALS_SOCKET_ROOT`, else `/tmp/mc`.\n */\n rootDir?: string;\n};\n\nfunction socketRootFromEnv(): string {\n const root = process.env.MASTRACODE_SIGNALS_SOCKET_ROOT;\n return root && isAbsolute(root) ? root : DEFAULT_SOCKET_ROOT;\n}\n\n/**\n * Whether an id can name a single directory: no path separators or control\n * characters, not `.`/`..`, and bounded in length.\n */\nfunction isSafePathSegment(value: string): boolean {\n if (!value || value.length > MAX_PATH_SEGMENT_LENGTH || value === '.' || value === '..') return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/** The resource a thread key (`<resourceId>\\0<threadId>`) belongs to. */\nfunction resourceOfThreadKey(key: string): string | undefined {\n const separatorIdx = key.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx <= 0) return undefined;\n const resourceId = key.slice(0, separatorIdx);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n}\n\n/** Decodes `agent.thread-stream.<encoded resourceId\\0threadId>[...]`. */\nfunction decodeThreadTopic(topic: string): string | undefined {\n if (!topic.startsWith(THREAD_STREAM_PREFIX)) return undefined;\n try {\n return decodeURIComponent(topic.slice(THREAD_STREAM_PREFIX.length));\n } catch {\n return undefined;\n }\n}\n\n/**\n * The lease that lets exactly one process dispatch a resource's due\n * notifications: those of the resource's threads that carry `scope` (a\n * `threadScopeKey`, one project's threads), or with no `scope`, those of its\n * threads that carry none. Held in that resource's directory, so every process\n * serving the resource contends for it whichever project it was started in.\n */\nexport function notificationDispatchLeaseKey(resourceId: string, scope?: string): string {\n return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}${scope === undefined ? '' : `${THREAD_KEY_SEPARATOR}${scope}`}`;\n}\n\n/**\n * The resource a lease key belongs to: a thread's run lease and claim lease,\n * and a resource's notification dispatch leases.\n */\nfunction resourceOfLeaseKey(key: string): string | undefined {\n if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {\n const [resourceId = ''] = key.slice(NOTIFICATION_DISPATCH_LEASE_PREFIX.length).split(THREAD_KEY_SEPARATOR, 1);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n }\n return resourceOfThreadKey(\n key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(THREAD_CLAIM_LEASE_PREFIX.length) : key,\n );\n}\n\nfunction isOwnerDiscoveryTopic(topic: string): boolean {\n return topic === OWNER_DISCOVERY_TOPIC || topic.startsWith(`${OWNER_DISCOVERY_TOPIC}.`);\n}\n\nfunction isPeerDiscoveryTopic(topic: string): boolean {\n return topic === PEER_DISCOVERY_TOPIC || topic.startsWith(`${PEER_DISCOVERY_TOPIC}.`);\n}\n\n/**\n * Whether a threadId can be the base name of a thread's socket file: no\n * separators or control characters, not `.`/`..`, and not a name another\n * socket in the same directory already uses (the lease socket, and the\n * sanitized peer/owner discovery topics and their reply topics), compared\n * case-insensitively because default macOS and Windows filesystems are.\n */\nfunction isSafeFileName(value: string): boolean {\n if (!value || value === '.' || value === '..' || `${value}.sock`.toLowerCase() === LEASE_SOCKET_NAME) return false;\n if (RESERVED_SOCKET_NAME.test(value)) return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/**\n * Derive a filesystem-safe key for the topic. Thread-stream topics embed\n * a threadId; all other topics use a sanitized version of the topic name.\n */\nfunction topicKey(topic: string): string {\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) {\n const separatorIdx = decoded.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx !== -1) return decoded.slice(separatorIdx + 1);\n }\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n}\n\nfunction socketKey(dir: string, topic: string): string {\n return `${dir}/${topicKey(topic)}`;\n}\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination between mastracode processes on this machine.\n *\n * Socket paths use `<root>/<dir>/<sanitized-topic>.sock` (root `/tmp/mc`)\n * for inspectability and automatic OS cleanup. Each topic gets its own\n * socket, so broker election and message routing are per-topic.\n *\n * Every process shares one database, so any process can act on any project's\n * thread — the notification dispatcher, for one, delivers whatever is due.\n * Whatever coordinates a thread therefore lives with the thread, not with the\n * process touching it:\n *\n * - A thread's stream topic and its leases (the run lease and the claim\n * lease) live in the directory of the resource that owns the thread, so a\n * process from another project sees that thread's active run and claim.\n * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds\n * a thread claimed can reach the owner and hand it the signal. It only\n * answers for a thread the asker names, so it discloses nothing new.\n * - Peer discovery (which lists threads) runs in `<root>/_shared/` when\n * `sharedAgentDiscovery` is set, so processes in every project can list each\n * other's threads. Without it, it stays in this resource's directory.\n * - Everything else stays in this process's own resource directory.\n *\n * Stale sockets from crashed processes are handled by\n * {@link UnixSocketPubSub}'s built-in election logic: it detects\n * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.\n * No blanket cleanup is needed here — that would break concurrent\n * mc instances sharing a directory.\n *\n * One-shot reply topics (discovery and idle-acceptance replies) are\n * reference-counted: every in-flight publish/subscribe and every live\n * subscription holds a reference, and the topic's socket is closed as soon as\n * the count drops to zero. Without this each request would keep a socket, a\n * file descriptor and (for the broker) a socket file for the process lifetime.\n * With `sharedAgentDiscovery`, another resource's thread topics are counted the\n * same way, so a process that signals many threads elsewhere does not keep a\n * socket open for each.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #rootDir: string;\n readonly #leaseSockets = new Map<string, UnixSocketPubSub>();\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n readonly #refs = new Map<string, number>();\n readonly #liveSubscriptions = new Map<string, Set<EventCallback>>();\n readonly #clearGenerations = new Map<string, number>();\n readonly #closing = new Map<string, Promise<void>>();\n readonly #leaseProvider: LeaseProvider;\n readonly #sharedPeerDiscovery: boolean;\n #closed = false;\n\n constructor(resourceId: string, options: SignalsPubSubOptions = {}) {\n super();\n this.#resourceId = resourceId;\n this.#rootDir = options.rootDir ?? socketRootFromEnv();\n this.#sharedPeerDiscovery = Boolean(options.sharedAgentDiscovery) && this.#canShareDiscovery();\n // Created eagerly, as before, so this resource's lease socket exists from the start.\n this.#leasesFor(resourceId);\n const route = (key: string) => {\n // Lease sockets are created per resource on demand; once closed, never\n // reopen one (a lease renewal timer may outlive close()).\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);\n };\n this.#leaseProvider = {\n acquireLease: async (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),\n getLeaseOwner: async key => route(key).getLeaseOwner(key),\n releaseLease: async (key, owner) => route(key).releaseLease(key, owner),\n renewLease: async (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),\n transferLease: async (key, fromOwner, toOwner, ttlMs) => route(key).transferLease(key, fromOwner, toOwner, ttlMs),\n };\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /**\n * Filesystem leases. A thread's run and claim leases are held in the\n * directory of the thread's resource, so every process contends for the\n * same lease; any other key stays in this resource's directory.\n */\n getLeaseProvider(): LeaseProvider {\n return this.#leaseProvider;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n await this.#publishTo(topic, this.#dirFor(topic), event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n await this.#subscribeTo(topic, this.#dirFor(topic), cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n await this.#unsubscribeFrom(topic, this.#dirFor(topic), cb);\n }\n\n /**\n * Drops a one-shot reply topic: its remaining subscriptions release their\n * references and the socket closes once no operation still uses it. Other\n * topics (workflow events, thread streams) have subscribers in other\n * processes that need the broker, so clearing them is a no-op.\n */\n override async clearTopic(topic: string): Promise<void> {\n if (!isEphemeralTopic(topic)) return;\n const key = socketKey(this.#dirFor(topic), topic);\n if (!this.#refs.has(key)) return;\n this.#clearGenerations.set(key, (this.#clearGenerations.get(key) ?? 0) + 1);\n const subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) return;\n this.#liveSubscriptions.delete(key);\n for (let i = 0; i < subscriptions.size; i++) this.#release(key);\n }\n\n async flush(): Promise<void> {\n await Promise.all([...this.#sockets.values()].map(s => s.flush()));\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n await Promise.allSettled([\n ...[...this.#leaseSockets.values()].map(s => s.close()),\n ...[...this.#sockets.values()].map(s => s.close()),\n ...this.#closing.values(),\n ]);\n this.#sockets.clear();\n this.#leaseSockets.clear();\n this.#refs.clear();\n this.#liveSubscriptions.clear();\n this.#clearGenerations.clear();\n }\n\n /** Get the underlying socket for a topic (for testing/inspection). */\n getSocket(topic: string): UnixSocketPubSub | undefined {\n return this.#sockets.get(socketKey(this.#dirFor(topic), topic));\n }\n\n #canShareDiscovery(): boolean {\n if (isSafePathSegment(this.#resourceId) && this.#resourceId !== SHARED_SCOPE_DIR) return true;\n console.warn(\n `Cross-project agent discovery is disabled: resource id ${JSON.stringify(this.#resourceId)} cannot be used as a directory name, so other projects could not reach this instance.`,\n );\n return false;\n }\n\n /**\n * One-shot reply topics close once unused. With shared discovery, so do thread\n * topics routed to another resource; without it, they behave as before.\n */\n #isRefCounted(topic: string): boolean {\n if (isEphemeralTopic(topic)) return true;\n if (!this.#sharedPeerDiscovery) return false;\n const decoded = decodeThreadTopic(topic);\n if (decoded === undefined) return false;\n const resourceId = resourceOfThreadKey(decoded);\n return resourceId !== undefined && resourceId !== this.#resourceId;\n }\n\n async #publishTo(\n topic: string,\n dir: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n return;\n }\n const key = socketKey(dir, topic);\n // Retain synchronously so a concurrent release cannot close the socket\n // this publish is about to use.\n this.#retain(key);\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n } finally {\n this.#release(key);\n }\n }\n\n async #subscribeTo(topic: string, dir: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n return;\n }\n const key = socketKey(dir, topic);\n this.#retain(key);\n const generation = this.#clearGenerations.get(key) ?? 0;\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n if ((this.#clearGenerations.get(key) ?? 0) !== generation) {\n // The topic was cleared while this subscribe was in flight.\n await socket.unsubscribe(topic, cb);\n return;\n }\n let subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) {\n subscriptions = new Set();\n this.#liveSubscriptions.set(key, subscriptions);\n }\n if (!subscriptions.has(cb)) {\n subscriptions.add(cb);\n this.#retain(key);\n }\n } finally {\n this.#release(key);\n }\n }\n\n async #unsubscribeFrom(topic: string, dir: string, cb: EventCallback): Promise<void> {\n const key = socketKey(dir, topic);\n const socket = this.#sockets.get(key);\n const subscriptions = this.#isRefCounted(topic) ? this.#liveSubscriptions.get(key) : undefined;\n if (!subscriptions?.delete(cb)) {\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\n return;\n }\n if (subscriptions.size === 0) this.#liveSubscriptions.delete(key);\n try {\n await socket?.unsubscribe(topic, cb);\n } finally {\n this.#release(key);\n }\n }\n\n #leasesFor(resourceId: string): UnixSocketPubSub {\n let socket = this.#leaseSockets.get(resourceId);\n if (!socket) {\n socket = new UnixSocketPubSub(join(this.#rootDir, resourceId, LEASE_SOCKET_NAME));\n this.#leaseSockets.set(resourceId, socket);\n }\n return socket;\n }\n\n /** The directory, under the root, that holds a topic's socket. */\n #dirFor(topic: string): string {\n if (isOwnerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n if (this.#sharedPeerDiscovery && isPeerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) return resourceOfThreadKey(decoded) ?? this.#resourceId;\n return this.#resourceId;\n }\n\n #retain(key: string): void {\n this.#refs.set(key, (this.#refs.get(key) ?? 0) + 1);\n }\n\n #release(key: string): void {\n const refs = (this.#refs.get(key) ?? 0) - 1;\n if (refs > 0) {\n this.#refs.set(key, refs);\n return;\n }\n this.#refs.delete(key);\n this.#clearGenerations.delete(key);\n this.#liveSubscriptions.delete(key);\n const socket = this.#sockets.get(key);\n if (!socket) return;\n this.#sockets.delete(key);\n const closing: Promise<void> = socket\n .close()\n .catch(() => {})\n .finally(() => {\n if (this.#closing.get(key) === closing) this.#closing.delete(key);\n });\n this.#closing.set(key, closing);\n }\n\n async #getOrCreate(topic: string, dir: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = socketKey(dir, topic);\n // A socket for this key may still be closing after its last reference\n // was released; never hand out the closing instance.\n const closing = this.#closing.get(key);\n if (closing) {\n await closing;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n }\n const existing = this.#sockets.get(key);\n if (existing) return existing;\n // Deduplicate concurrent callers so only one socket is created per topic.\n let inflight = this.#pending.get(key);\n if (!inflight) {\n inflight = this.#initSocket(topic, dir, key);\n this.#pending.set(key, inflight);\n }\n const socket = await inflight;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return socket;\n }\n\n async #initSocket(topic: string, dir: string, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic, dir);\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const socket = new UnixSocketPubSub(socketPath);\n this.#sockets.set(key, socket);\n return socket;\n } finally {\n this.#pending.delete(key);\n }\n }\n\n async #socketPath(topic: string, scope: string): Promise<string> {\n let key = topicKey(topic);\n // A threadId names a file in its resource's directory, so never let it\n // reach outside that directory, regardless of who owns the thread or\n // whether cross-project discovery is enabled.\n if (decodeThreadTopic(topic) !== undefined && !isSafeFileName(key)) {\n throw new Error('Cannot route an agent thread topic whose threadId is not a safe file name');\n }\n const dir = join(this.#rootDir, scope);\n await mkdir(dir, { recursive: true });\n const candidate = join(dir, `${key}.sock`);\n // macOS sun_path limit is 104 bytes; Linux is 108. Use 104 as the\n // conservative bound. When the path is too long, replace the key with\n // a short hash so the socket can still be created.\n if (Buffer.byteLength(candidate) > 104) {\n key = Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)))\n .toString('hex')\n .slice(0, 16);\n return join(dir, `${key}.sock`);\n }\n return candidate;\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination between mastracode processes.\n *\n * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and\n * leases live under its own resource's directory and thread-owner discovery\n * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). With\n * `sharedAgentDiscovery`, peer discovery runs there too. Stale sockets from\n * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s\n * broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string, options?: SignalsPubSubOptions): SignalsPubSub {\n return new SignalsPubSub(resourceId, options);\n}\n"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { AgentController, Session } from '@mastra/core/agent-controller';
2
+ import type { LeaseProvider } from '@mastra/core/events';
3
+ /** A session here serves the thread's resource, but the thread belongs to another project. */
4
+ export declare const FOREIGN_THREAD: unique symbol;
5
+ /** The session a wake runs as, and the dispatch lease that covers the thread. */
6
+ export type WakeSession<TState> = {
7
+ session: Session<TState>;
8
+ /**
9
+ * The thread's scope (`threadScopeKey`) when it carries the session's whole
10
+ * scope: only the process holding that scope's dispatch lease delivers it.
11
+ * Unset for a thread with no scope, which the resource's lease covers.
12
+ */
13
+ scope?: string;
14
+ };
15
+ /** A stable key for a session's thread scope, or `undefined` when it has none. */
16
+ export declare function threadScopeKey(scope: Record<string, string>): string | undefined;
17
+ /**
18
+ * The session a wake for `threadId` should run as, or `FOREIGN_THREAD` when
19
+ * this process serves the resource but not the thread.
20
+ *
21
+ * A resource is not a project: processes in different directories can share
22
+ * one resource id (to share recall, say), and each then has a live session for
23
+ * it. Picking by resource alone runs the thread in whichever process asked,
24
+ * with that process's workspace, tools and cwd. The thread's scope tags (the
25
+ * ones `thread.create()` stamps, such as `projectPath`) say which session it
26
+ * belongs to. A thread with no scope tags, or with tags no session here
27
+ * contradicts, keeps going to the resource's session.
28
+ *
29
+ * A session that has the thread open always owns it. When the thread belongs to
30
+ * another project, that session delivers it under a lease for this one thread
31
+ * (`openThreadScope`), and the thread's own project yields it while another
32
+ * process holds that lease (`leases`), so only one process runs it and the
33
+ * project's other threads stay with the project.
34
+ */
35
+ export declare function findWakeSession<TState>(controller: Pick<AgentController<TState>, 'getSessionByResource'>, liveSessions: Iterable<Session<TState>>, { resourceId, threadId }: {
36
+ resourceId: string;
37
+ threadId: string;
38
+ }, leases?: Pick<LeaseProvider, 'getLeaseOwner'>): Promise<WakeSession<TState> | typeof FOREIGN_THREAD | undefined>;
39
+ /**
40
+ * The dispatch lease scopes a session needs: its own, plus a lease for its open
41
+ * thread alone when that thread belongs to another project (`findWakeSession`).
42
+ */
43
+ export declare function sessionLeaseScopes<TState>(session: Session<TState>): Promise<Array<string | undefined>>;
44
+ //# sourceMappingURL=wake-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-session.d.ts","sourceRoot":"","sources":["../../src/utils/wake-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,8FAA8F;AAC9F,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD,iFAAiF;AACjF,MAAM,MAAM,WAAW,CAAC,MAAM,IAAI;IAChC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAGhF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAC1C,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC,EACjE,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EACvC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAClE,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,eAAe,CAAC,GAC5C,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,cAAc,GAAG,SAAS,CAAC,CA4BlE;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAW7G"}
@@ -0,0 +1,79 @@
1
+ import { notificationDispatchLeaseKey } from "./signals-pubsub.js";
2
+ //#region src/utils/wake-session.ts
3
+ /** A session here serves the thread's resource, but the thread belongs to another project. */
4
+ const FOREIGN_THREAD = Symbol("foreign-thread");
5
+ /** A stable key for a session's thread scope, or `undefined` when it has none. */
6
+ function threadScopeKey(scope) {
7
+ const entries = Object.entries(scope).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
8
+ return entries.length > 0 ? JSON.stringify(entries) : void 0;
9
+ }
10
+ /**
11
+ * The session a wake for `threadId` should run as, or `FOREIGN_THREAD` when
12
+ * this process serves the resource but not the thread.
13
+ *
14
+ * A resource is not a project: processes in different directories can share
15
+ * one resource id (to share recall, say), and each then has a live session for
16
+ * it. Picking by resource alone runs the thread in whichever process asked,
17
+ * with that process's workspace, tools and cwd. The thread's scope tags (the
18
+ * ones `thread.create()` stamps, such as `projectPath`) say which session it
19
+ * belongs to. A thread with no scope tags, or with tags no session here
20
+ * contradicts, keeps going to the resource's session.
21
+ *
22
+ * A session that has the thread open always owns it. When the thread belongs to
23
+ * another project, that session delivers it under a lease for this one thread
24
+ * (`openThreadScope`), and the thread's own project yields it while another
25
+ * process holds that lease (`leases`), so only one process runs it and the
26
+ * project's other threads stay with the project.
27
+ */
28
+ async function findWakeSession(controller, liveSessions, { resourceId, threadId }, leases) {
29
+ const first = await controller.getSessionByResource(resourceId);
30
+ if (!first) return void 0;
31
+ const thread = await first.thread.getById({ threadId });
32
+ if (!thread || thread.resourceId !== resourceId) return { session: first };
33
+ const metadata = thread.metadata ?? {};
34
+ const candidates = [first, ...[...liveSessions].filter((session) => session !== first && session.identity.getResourceId() === resourceId)];
35
+ const matchesScope = (session) => Object.entries(session.getThreadScope()).every(([key, value]) => metadata[key] === void 0 || metadata[key] === value);
36
+ const open = candidates.find((session) => session.thread.getId() === threadId);
37
+ if (open) return {
38
+ session: open,
39
+ scope: matchesScope(open) ? threadLeaseScope(open, metadata) : openThreadScope(threadId)
40
+ };
41
+ const owner = candidates.find(matchesScope);
42
+ if (!owner) return FOREIGN_THREAD;
43
+ const scope = threadLeaseScope(owner, metadata);
44
+ if (scope !== void 0 && await leases?.getLeaseOwner(notificationDispatchLeaseKey(resourceId, openThreadScope(threadId)))) return FOREIGN_THREAD;
45
+ return {
46
+ session: owner,
47
+ scope
48
+ };
49
+ }
50
+ /**
51
+ * The dispatch lease scopes a session needs: its own, plus a lease for its open
52
+ * thread alone when that thread belongs to another project (`findWakeSession`).
53
+ */
54
+ async function sessionLeaseScopes(session) {
55
+ const own = threadScopeKey(session.getThreadScope());
56
+ const threadId = session.thread.getId();
57
+ if (!threadId) return [own];
58
+ const thread = await session.thread.getById({ threadId });
59
+ if (!thread || thread.resourceId !== session.identity.getResourceId()) return [own];
60
+ const metadata = thread.metadata ?? {};
61
+ return Object.entries(session.getThreadScope()).some(([key, value]) => metadata[key] !== void 0 && metadata[key] !== value) ? [own, openThreadScope(threadId)] : [own];
62
+ }
63
+ /** The lease scope for one thread a session has open from another project. */
64
+ function openThreadScope(threadId) {
65
+ return threadScopeKey({ openThread: threadId });
66
+ }
67
+ /**
68
+ * The lease scope covering a thread for `session`: the thread's own values for
69
+ * the session's scope keys, or unset when the thread lacks any of them.
70
+ */
71
+ function threadLeaseScope(session, metadata = {}) {
72
+ const keys = Object.keys(session.getThreadScope());
73
+ if (!keys.every((key) => metadata[key] !== void 0)) return void 0;
74
+ return threadScopeKey(Object.fromEntries(keys.map((key) => [key, String(metadata[key])])));
75
+ }
76
+ //#endregion
77
+ export { FOREIGN_THREAD, findWakeSession, sessionLeaseScopes, threadScopeKey };
78
+
79
+ //# sourceMappingURL=wake-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-session.js","names":[],"sources":["../../src/utils/wake-session.ts"],"sourcesContent":["import type { AgentController, Session } from '@mastra/core/agent-controller';\nimport type { LeaseProvider } from '@mastra/core/events';\n\nimport { notificationDispatchLeaseKey } from './signals-pubsub.js';\n\n/** A session here serves the thread's resource, but the thread belongs to another project. */\nexport const FOREIGN_THREAD = Symbol('foreign-thread');\n\n/** The session a wake runs as, and the dispatch lease that covers the thread. */\nexport type WakeSession<TState> = {\n session: Session<TState>;\n /**\n * The thread's scope (`threadScopeKey`) when it carries the session's whole\n * scope: only the process holding that scope's dispatch lease delivers it.\n * Unset for a thread with no scope, which the resource's lease covers.\n */\n scope?: string;\n};\n\n/** A stable key for a session's thread scope, or `undefined` when it has none. */\nexport function threadScopeKey(scope: Record<string, string>): string | undefined {\n const entries = Object.entries(scope).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return entries.length > 0 ? JSON.stringify(entries) : undefined;\n}\n\n/**\n * The session a wake for `threadId` should run as, or `FOREIGN_THREAD` when\n * this process serves the resource but not the thread.\n *\n * A resource is not a project: processes in different directories can share\n * one resource id (to share recall, say), and each then has a live session for\n * it. Picking by resource alone runs the thread in whichever process asked,\n * with that process's workspace, tools and cwd. The thread's scope tags (the\n * ones `thread.create()` stamps, such as `projectPath`) say which session it\n * belongs to. A thread with no scope tags, or with tags no session here\n * contradicts, keeps going to the resource's session.\n *\n * A session that has the thread open always owns it. When the thread belongs to\n * another project, that session delivers it under a lease for this one thread\n * (`openThreadScope`), and the thread's own project yields it while another\n * process holds that lease (`leases`), so only one process runs it and the\n * project's other threads stay with the project.\n */\nexport async function findWakeSession<TState>(\n controller: Pick<AgentController<TState>, 'getSessionByResource'>,\n liveSessions: Iterable<Session<TState>>,\n { resourceId, threadId }: { resourceId: string; threadId: string },\n leases?: Pick<LeaseProvider, 'getLeaseOwner'>,\n): Promise<WakeSession<TState> | typeof FOREIGN_THREAD | undefined> {\n const first = await controller.getSessionByResource(resourceId);\n if (!first) return undefined;\n const thread = await first.thread.getById({ threadId });\n if (!thread || thread.resourceId !== resourceId) return { session: first };\n const metadata = (thread.metadata as Record<string, unknown> | undefined) ?? {};\n const candidates = [\n first,\n ...[...liveSessions].filter(session => session !== first && session.identity.getResourceId() === resourceId),\n ];\n const matchesScope = (session: Session<TState>) =>\n Object.entries(session.getThreadScope()).every(\n ([key, value]) => metadata[key] === undefined || metadata[key] === value,\n );\n const open = candidates.find(session => session.thread.getId() === threadId);\n if (open) {\n return { session: open, scope: matchesScope(open) ? threadLeaseScope(open, metadata) : openThreadScope(threadId) };\n }\n const owner = candidates.find(matchesScope);\n if (!owner) return FOREIGN_THREAD;\n const scope = threadLeaseScope(owner, metadata);\n if (\n scope !== undefined &&\n (await leases?.getLeaseOwner(notificationDispatchLeaseKey(resourceId, openThreadScope(threadId))))\n ) {\n return FOREIGN_THREAD;\n }\n return { session: owner, scope };\n}\n\n/**\n * The dispatch lease scopes a session needs: its own, plus a lease for its open\n * thread alone when that thread belongs to another project (`findWakeSession`).\n */\nexport async function sessionLeaseScopes<TState>(session: Session<TState>): Promise<Array<string | undefined>> {\n const own = threadScopeKey(session.getThreadScope());\n const threadId = session.thread.getId();\n if (!threadId) return [own];\n const thread = await session.thread.getById({ threadId });\n if (!thread || thread.resourceId !== session.identity.getResourceId()) return [own];\n const metadata = (thread.metadata as Record<string, unknown> | undefined) ?? {};\n const foreign = Object.entries(session.getThreadScope()).some(\n ([key, value]) => metadata[key] !== undefined && metadata[key] !== value,\n );\n return foreign ? [own, openThreadScope(threadId)] : [own];\n}\n\n/** The lease scope for one thread a session has open from another project. */\nfunction openThreadScope(threadId: string): string {\n return threadScopeKey({ openThread: threadId })!;\n}\n\n/**\n * The lease scope covering a thread for `session`: the thread's own values for\n * the session's scope keys, or unset when the thread lacks any of them.\n */\nfunction threadLeaseScope<TState>(\n session: Session<TState>,\n metadata: Record<string, unknown> = {},\n): string | undefined {\n const keys = Object.keys(session.getThreadScope());\n if (!keys.every(key => metadata[key] !== undefined)) return undefined;\n return threadScopeKey(Object.fromEntries(keys.map(key => [key, String(metadata[key])])));\n}\n"],"mappings":";;;AAMA,MAAa,iBAAiB,OAAO,gBAAgB;;AAcrD,SAAgB,eAAe,OAAmD;CAChF,MAAM,UAAU,OAAO,QAAQ,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE;CACrF,OAAO,QAAQ,SAAS,IAAI,KAAK,UAAU,OAAO,IAAI,KAAA;AACxD;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,gBACpB,YACA,cACA,EAAE,YAAY,YACd,QACkE;CAClE,MAAM,QAAQ,MAAM,WAAW,qBAAqB,UAAU;CAC9D,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,SAAS,MAAM,MAAM,OAAO,QAAQ,EAAE,SAAS,CAAC;CACtD,IAAI,CAAC,UAAU,OAAO,eAAe,YAAY,OAAO,EAAE,SAAS,MAAM;CACzE,MAAM,WAAY,OAAO,YAAoD,CAAC;CAC9E,MAAM,aAAa,CACjB,OACA,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,QAAO,YAAW,YAAY,SAAS,QAAQ,SAAS,cAAc,MAAM,UAAU,CAC7G;CACA,MAAM,gBAAgB,YACpB,OAAO,QAAQ,QAAQ,eAAe,CAAC,CAAC,CAAC,OACtC,CAAC,KAAK,WAAW,SAAS,SAAS,KAAA,KAAa,SAAS,SAAS,KACrE;CACF,MAAM,OAAO,WAAW,MAAK,YAAW,QAAQ,OAAO,MAAM,MAAM,QAAQ;CAC3E,IAAI,MACF,OAAO;EAAE,SAAS;EAAM,OAAO,aAAa,IAAI,IAAI,iBAAiB,MAAM,QAAQ,IAAI,gBAAgB,QAAQ;CAAE;CAEnH,MAAM,QAAQ,WAAW,KAAK,YAAY;CAC1C,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,QAAQ,iBAAiB,OAAO,QAAQ;CAC9C,IACE,UAAU,KAAA,KACT,MAAM,QAAQ,cAAc,6BAA6B,YAAY,gBAAgB,QAAQ,CAAC,CAAC,GAEhG,OAAO;CAET,OAAO;EAAE,SAAS;EAAO;CAAM;AACjC;;;;;AAMA,eAAsB,mBAA2B,SAA8D;CAC7G,MAAM,MAAM,eAAe,QAAQ,eAAe,CAAC;CACnD,MAAM,WAAW,QAAQ,OAAO,MAAM;CACtC,IAAI,CAAC,UAAU,OAAO,CAAC,GAAG;CAC1B,MAAM,SAAS,MAAM,QAAQ,OAAO,QAAQ,EAAE,SAAS,CAAC;CACxD,IAAI,CAAC,UAAU,OAAO,eAAe,QAAQ,SAAS,cAAc,GAAG,OAAO,CAAC,GAAG;CAClF,MAAM,WAAY,OAAO,YAAoD,CAAC;CAI9E,OAHgB,OAAO,QAAQ,QAAQ,eAAe,CAAC,CAAC,CAAC,MACtD,CAAC,KAAK,WAAW,SAAS,SAAS,KAAA,KAAa,SAAS,SAAS,KAExD,IAAI,CAAC,KAAK,gBAAgB,QAAQ,CAAC,IAAI,CAAC,GAAG;AAC1D;;AAGA,SAAS,gBAAgB,UAA0B;CACjD,OAAO,eAAe,EAAE,YAAY,SAAS,CAAC;AAChD;;;;;AAMA,SAAS,iBACP,SACA,WAAoC,CAAC,GACjB;CACpB,MAAM,OAAO,OAAO,KAAK,QAAQ,eAAe,CAAC;CACjD,IAAI,CAAC,KAAK,OAAM,QAAO,SAAS,SAAS,KAAA,CAAS,GAAG,OAAO,KAAA;CAC5D,OAAO,eAAe,OAAO,YAAY,KAAK,KAAI,QAAO,CAAC,KAAK,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC;AACzF"}