@mastra/code-sdk 1.10.2-alpha.0 → 1.11.0-alpha.2

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.
@@ -1,7 +1,16 @@
1
1
  import { PubSub, UnixSocketPubSub } from '@mastra/core/events';
2
2
  import type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';
3
3
  export type SignalsPubSubOptions = {
4
- /** Directory that holds every resource's socket directory. Defaults to `/tmp/mc`. */
4
+ /**
5
+ * Also list this process's threads to, and find threads in, other projects on
6
+ * this machine: peer discovery runs in `<root>/_shared/` instead of this
7
+ * resource's directory.
8
+ */
9
+ sharedAgentDiscovery?: boolean;
10
+ /**
11
+ * Directory that holds every resource's socket directory. Defaults to the
12
+ * absolute path in `MASTRACODE_SIGNALS_SOCKET_ROOT`, else `/tmp/mc`.
13
+ */
5
14
  rootDir?: string;
6
15
  };
7
16
  /**
@@ -29,14 +38,25 @@ export declare function notificationDispatchLeaseKey(resourceId: string): string
29
38
  * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds
30
39
  * a thread claimed can reach the owner and hand it the signal. It only
31
40
  * answers for a thread the asker names, so it discloses nothing new.
32
- * - Everything else, including peer discovery (which lists threads), stays in
33
- * this process's own resource directory.
41
+ * - Peer discovery (which lists threads) runs in `<root>/_shared/` when
42
+ * `sharedAgentDiscovery` is set, so processes in every project can list each
43
+ * other's threads. Without it, it stays in this resource's directory.
44
+ * - Everything else stays in this process's own resource directory.
34
45
  *
35
46
  * Stale sockets from crashed processes are handled by
36
47
  * {@link UnixSocketPubSub}'s built-in election logic: it detects
37
48
  * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.
38
49
  * No blanket cleanup is needed here — that would break concurrent
39
50
  * mc instances sharing a directory.
51
+ *
52
+ * One-shot reply topics (discovery and idle-acceptance replies) are
53
+ * reference-counted: every in-flight publish/subscribe and every live
54
+ * subscription holds a reference, and the topic's socket is closed as soon as
55
+ * the count drops to zero. Without this each request would keep a socket, a
56
+ * file descriptor and (for the broker) a socket file for the process lifetime.
57
+ * With `sharedAgentDiscovery`, another resource's thread topics are counted the
58
+ * same way, so a process that signals many threads elsewhere does not keep a
59
+ * socket open for each.
40
60
  */
41
61
  declare class SignalsPubSub extends PubSub {
42
62
  #private;
@@ -53,6 +73,13 @@ declare class SignalsPubSub extends PubSub {
53
73
  }): Promise<void>;
54
74
  subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void>;
55
75
  unsubscribe(topic: string, cb: EventCallback): Promise<void>;
76
+ /**
77
+ * Drops a one-shot reply topic: its remaining subscriptions release their
78
+ * references and the socket closes once no operation still uses it. Other
79
+ * topics (workflow events, thread streams) have subscribers in other
80
+ * processes that need the broker, so clearing them is a no-op.
81
+ */
82
+ clearTopic(topic: string): Promise<void>;
56
83
  flush(): Promise<void>;
57
84
  close(): Promise<void>;
58
85
  /** Get the underlying socket for a topic (for testing/inspection). */
@@ -64,7 +91,8 @@ declare class SignalsPubSub extends PubSub {
64
91
  *
65
92
  * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and
66
93
  * leases live under its own resource's directory and thread-owner discovery
67
- * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). Stale sockets from
94
+ * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). With
95
+ * `sharedAgentDiscovery`, peer discovery runs there too. Stale sockets from
68
96
  * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s
69
97
  * broker election logic.
70
98
  */
@@ -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;AAWrH,MAAM,MAAM,oBAAoB,GAAG;IACjC,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAiCF;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvE;AAiCD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,cAAM,aAAc,SAAQ,MAAM;;gBASpB,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB;IAgBlE,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;IAKV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAU5B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAqEvD;AAED;;;;;;;;;GASG;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;AAuBrH,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;AAmDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;CAyMvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAErG"}
@@ -1,4 +1,4 @@
1
- import { join } from "path";
1
+ import { isAbsolute, join } from "path";
2
2
  import { mkdir } from "fs/promises";
3
3
  import { PubSub, UnixSocketPubSub } from "@mastra/core/events";
4
4
  //#region src/utils/signals-pubsub.ts
@@ -9,7 +9,22 @@ const THREAD_KEY_SEPARATOR = "\0";
9
9
  const THREAD_CLAIM_LEASE_PREFIX = "thread-claim:";
10
10
  const NOTIFICATION_DISPATCH_LEASE_PREFIX = "notification-dispatch:";
11
11
  const OWNER_DISCOVERY_TOPIC = "agent.thread-owner-discovery";
12
+ const PEER_DISCOVERY_TOPIC = "agent.thread-peer-discovery";
12
13
  const MAX_PATH_SEGMENT_LENGTH = 128;
14
+ const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
15
+ const DISCOVERY_REPLY_TOPIC = new RegExp(`^agent\\.thread-(?:peer|owner)-discovery\\.${UUID_PATTERN}$`, "i");
16
+ const IDLE_ACCEPTANCE_REPLY_TOPIC = new RegExp(`^agent\\.thread-stream\\..+\\.idle-acceptance\\.${UUID_PATTERN}$`, "i");
17
+ /**
18
+ * One-shot reply topics are minted per request (`<topic>.<uuid>`), used for a
19
+ * single round trip, and never published to again once the request settles.
20
+ */
21
+ function isEphemeralTopic(topic) {
22
+ return DISCOVERY_REPLY_TOPIC.test(topic) || IDLE_ACCEPTANCE_REPLY_TOPIC.test(topic);
23
+ }
24
+ function socketRootFromEnv() {
25
+ const root = process.env.MASTRACODE_SIGNALS_SOCKET_ROOT;
26
+ return root && isAbsolute(root) ? root : DEFAULT_SOCKET_ROOT;
27
+ }
13
28
  /**
14
29
  * Whether an id can name a single directory: no path separators or control
15
30
  * characters, not `.`/`..`, and bounded in length.
@@ -60,6 +75,18 @@ function resourceOfLeaseKey(key) {
60
75
  function isOwnerDiscoveryTopic(topic) {
61
76
  return topic === OWNER_DISCOVERY_TOPIC || topic.startsWith(`${OWNER_DISCOVERY_TOPIC}.`);
62
77
  }
78
+ function isPeerDiscoveryTopic(topic) {
79
+ return topic === PEER_DISCOVERY_TOPIC || topic.startsWith(`${PEER_DISCOVERY_TOPIC}.`);
80
+ }
81
+ /** Whether a name can be a file in a directory: no separators or control characters, not `.`/`..`. */
82
+ function isSafeFileName(value) {
83
+ if (!value || value === "." || value === "..") return false;
84
+ for (const char of value) {
85
+ const code = char.charCodeAt(0);
86
+ if (char === "/" || char === "\\" || code < 32 || code === 127) return false;
87
+ }
88
+ return true;
89
+ }
63
90
  /**
64
91
  * Derive a filesystem-safe key for the topic. Thread-stream topics embed
65
92
  * a threadId; all other topics use a sanitized version of the topic name.
@@ -72,6 +99,9 @@ function topicKey(topic) {
72
99
  }
73
100
  return topic.replace(/[^a-zA-Z0-9_-]/g, "_");
74
101
  }
102
+ function socketKey(dir, topic) {
103
+ return `${dir}/${topicKey(topic)}`;
104
+ }
75
105
  /**
76
106
  * A PubSub that manages one Unix socket per topic for cross-process signal
77
107
  * coordination between mastracode processes on this machine.
@@ -91,14 +121,25 @@ function topicKey(topic) {
91
121
  * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds
92
122
  * a thread claimed can reach the owner and hand it the signal. It only
93
123
  * answers for a thread the asker names, so it discloses nothing new.
94
- * - Everything else, including peer discovery (which lists threads), stays in
95
- * this process's own resource directory.
124
+ * - Peer discovery (which lists threads) runs in `<root>/_shared/` when
125
+ * `sharedAgentDiscovery` is set, so processes in every project can list each
126
+ * other's threads. Without it, it stays in this resource's directory.
127
+ * - Everything else stays in this process's own resource directory.
96
128
  *
97
129
  * Stale sockets from crashed processes are handled by
98
130
  * {@link UnixSocketPubSub}'s built-in election logic: it detects
99
131
  * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.
100
132
  * No blanket cleanup is needed here — that would break concurrent
101
133
  * mc instances sharing a directory.
134
+ *
135
+ * One-shot reply topics (discovery and idle-acceptance replies) are
136
+ * reference-counted: every in-flight publish/subscribe and every live
137
+ * subscription holds a reference, and the topic's socket is closed as soon as
138
+ * the count drops to zero. Without this each request would keep a socket, a
139
+ * file descriptor and (for the broker) a socket file for the process lifetime.
140
+ * With `sharedAgentDiscovery`, another resource's thread topics are counted the
141
+ * same way, so a process that signals many threads elsewhere does not keep a
142
+ * socket open for each.
102
143
  */
103
144
  var SignalsPubSub = class extends PubSub {
104
145
  #resourceId;
@@ -106,20 +147,29 @@ var SignalsPubSub = class extends PubSub {
106
147
  #leaseSockets = /* @__PURE__ */ new Map();
107
148
  #sockets = /* @__PURE__ */ new Map();
108
149
  #pending = /* @__PURE__ */ new Map();
150
+ #refs = /* @__PURE__ */ new Map();
151
+ #liveSubscriptions = /* @__PURE__ */ new Map();
152
+ #clearGenerations = /* @__PURE__ */ new Map();
153
+ #closing = /* @__PURE__ */ new Map();
109
154
  #leaseProvider;
155
+ #sharedPeerDiscovery;
110
156
  #closed = false;
111
157
  constructor(resourceId, options = {}) {
112
158
  super();
113
159
  this.#resourceId = resourceId;
114
- this.#rootDir = options.rootDir ?? DEFAULT_SOCKET_ROOT;
160
+ this.#rootDir = options.rootDir ?? socketRootFromEnv();
161
+ this.#sharedPeerDiscovery = Boolean(options.sharedAgentDiscovery) && this.#canShareDiscovery();
115
162
  this.#leasesFor(resourceId);
116
- const route = (key) => this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);
163
+ const route = (key) => {
164
+ if (this.#closed) throw new Error("SignalsPubSub is closed");
165
+ return this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);
166
+ };
117
167
  this.#leaseProvider = {
118
- acquireLease: (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),
119
- getLeaseOwner: (key) => route(key).getLeaseOwner(key),
120
- releaseLease: (key, owner) => route(key).releaseLease(key, owner),
121
- renewLease: (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),
122
- transferLease: (key, fromOwner, toOwner, ttlMs) => route(key).transferLease(key, fromOwner, toOwner, ttlMs)
168
+ acquireLease: async (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),
169
+ getLeaseOwner: async (key) => route(key).getLeaseOwner(key),
170
+ releaseLease: async (key, owner) => route(key).releaseLease(key, owner),
171
+ renewLease: async (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),
172
+ transferLease: async (key, fromOwner, toOwner, ttlMs) => route(key).transferLease(key, fromOwner, toOwner, ttlMs)
123
173
  };
124
174
  }
125
175
  get supportedModes() {
@@ -134,28 +184,123 @@ var SignalsPubSub = class extends PubSub {
134
184
  return this.#leaseProvider;
135
185
  }
136
186
  async publish(topic, event, options) {
137
- await (await this.#getOrCreate(topic)).publish(topic, event, options);
187
+ await this.#publishTo(topic, this.#dirFor(topic), event, options);
138
188
  }
139
189
  async subscribe(topic, cb, options) {
140
- await (await this.#getOrCreate(topic)).subscribe(topic, cb, options);
190
+ await this.#subscribeTo(topic, this.#dirFor(topic), cb, options);
141
191
  }
142
192
  async unsubscribe(topic, cb) {
143
- const socket = this.#sockets.get(this.#socketKey(topic));
144
- if (!socket) return;
145
- await socket.unsubscribe(topic, cb);
193
+ await this.#unsubscribeFrom(topic, this.#dirFor(topic), cb);
194
+ }
195
+ /**
196
+ * Drops a one-shot reply topic: its remaining subscriptions release their
197
+ * references and the socket closes once no operation still uses it. Other
198
+ * topics (workflow events, thread streams) have subscribers in other
199
+ * processes that need the broker, so clearing them is a no-op.
200
+ */
201
+ async clearTopic(topic) {
202
+ if (!isEphemeralTopic(topic)) return;
203
+ const key = socketKey(this.#dirFor(topic), topic);
204
+ if (!this.#refs.has(key)) return;
205
+ this.#clearGenerations.set(key, (this.#clearGenerations.get(key) ?? 0) + 1);
206
+ const subscriptions = this.#liveSubscriptions.get(key);
207
+ if (!subscriptions) return;
208
+ this.#liveSubscriptions.delete(key);
209
+ for (let i = 0; i < subscriptions.size; i++) this.#release(key);
146
210
  }
147
211
  async flush() {
148
212
  await Promise.all([...this.#sockets.values()].map((s) => s.flush()));
149
213
  }
150
214
  async close() {
151
215
  this.#closed = true;
152
- await Promise.allSettled([...[...this.#leaseSockets.values()].map((s) => s.close()), ...[...this.#sockets.values()].map((s) => s.close())]);
216
+ await Promise.allSettled([
217
+ ...[...this.#leaseSockets.values()].map((s) => s.close()),
218
+ ...[...this.#sockets.values()].map((s) => s.close()),
219
+ ...this.#closing.values()
220
+ ]);
153
221
  this.#sockets.clear();
154
222
  this.#leaseSockets.clear();
223
+ this.#refs.clear();
224
+ this.#liveSubscriptions.clear();
225
+ this.#clearGenerations.clear();
155
226
  }
156
227
  /** Get the underlying socket for a topic (for testing/inspection). */
157
228
  getSocket(topic) {
158
- return this.#sockets.get(this.#socketKey(topic));
229
+ return this.#sockets.get(socketKey(this.#dirFor(topic), topic));
230
+ }
231
+ #canShareDiscovery() {
232
+ if (isSafePathSegment(this.#resourceId) && this.#resourceId !== SHARED_SCOPE_DIR) return true;
233
+ console.warn(`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.`);
234
+ return false;
235
+ }
236
+ /**
237
+ * One-shot reply topics close once unused. With shared discovery, so do thread
238
+ * topics routed to another resource; without it, they behave as before.
239
+ */
240
+ #isRefCounted(topic) {
241
+ if (isEphemeralTopic(topic)) return true;
242
+ if (!this.#sharedPeerDiscovery) return false;
243
+ const decoded = decodeThreadTopic(topic);
244
+ if (decoded === void 0) return false;
245
+ const resourceId = resourceOfThreadKey(decoded);
246
+ return resourceId !== void 0 && resourceId !== this.#resourceId;
247
+ }
248
+ async #publishTo(topic, dir, event, options) {
249
+ if (!this.#isRefCounted(topic)) {
250
+ await (await this.#getOrCreate(topic, dir)).publish(topic, event, options);
251
+ return;
252
+ }
253
+ const key = socketKey(dir, topic);
254
+ this.#retain(key);
255
+ try {
256
+ await (await this.#getOrCreate(topic, dir)).publish(topic, event, options);
257
+ } finally {
258
+ this.#release(key);
259
+ }
260
+ }
261
+ async #subscribeTo(topic, dir, cb, options) {
262
+ if (!this.#isRefCounted(topic)) {
263
+ await (await this.#getOrCreate(topic, dir)).subscribe(topic, cb, options);
264
+ return;
265
+ }
266
+ const key = socketKey(dir, topic);
267
+ this.#retain(key);
268
+ const generation = this.#clearGenerations.get(key) ?? 0;
269
+ try {
270
+ const socket = await this.#getOrCreate(topic, dir);
271
+ await socket.subscribe(topic, cb, options);
272
+ if ((this.#clearGenerations.get(key) ?? 0) !== generation) {
273
+ await socket.unsubscribe(topic, cb);
274
+ return;
275
+ }
276
+ let subscriptions = this.#liveSubscriptions.get(key);
277
+ if (!subscriptions) {
278
+ subscriptions = /* @__PURE__ */ new Set();
279
+ this.#liveSubscriptions.set(key, subscriptions);
280
+ }
281
+ if (!subscriptions.has(cb)) {
282
+ subscriptions.add(cb);
283
+ this.#retain(key);
284
+ }
285
+ } finally {
286
+ this.#release(key);
287
+ }
288
+ }
289
+ async #unsubscribeFrom(topic, dir, cb) {
290
+ const key = socketKey(dir, topic);
291
+ const socket = this.#sockets.get(key);
292
+ const subscriptions = this.#isRefCounted(topic) ? this.#liveSubscriptions.get(key) : void 0;
293
+ if (!subscriptions?.delete(cb)) {
294
+ if (!socket) return;
295
+ await socket.unsubscribe(topic, cb);
296
+ return;
297
+ }
298
+ if (subscriptions.size === 0) this.#liveSubscriptions.delete(key);
299
+ try {
300
+ await socket?.unsubscribe(topic, cb);
301
+ } finally {
302
+ this.#release(key);
303
+ }
159
304
  }
160
305
  #leasesFor(resourceId) {
161
306
  let socket = this.#leaseSockets.get(resourceId);
@@ -168,30 +313,53 @@ var SignalsPubSub = class extends PubSub {
168
313
  /** The directory, under the root, that holds a topic's socket. */
169
314
  #dirFor(topic) {
170
315
  if (isOwnerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;
316
+ if (this.#sharedPeerDiscovery && isPeerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;
171
317
  const decoded = decodeThreadTopic(topic);
172
318
  if (decoded !== void 0) return resourceOfThreadKey(decoded) ?? this.#resourceId;
173
319
  return this.#resourceId;
174
320
  }
175
- #socketKey(topic) {
176
- return `${this.#dirFor(topic)}/${topicKey(topic)}`;
321
+ #retain(key) {
322
+ this.#refs.set(key, (this.#refs.get(key) ?? 0) + 1);
177
323
  }
178
- async #getOrCreate(topic) {
324
+ #release(key) {
325
+ const refs = (this.#refs.get(key) ?? 0) - 1;
326
+ if (refs > 0) {
327
+ this.#refs.set(key, refs);
328
+ return;
329
+ }
330
+ this.#refs.delete(key);
331
+ this.#clearGenerations.delete(key);
332
+ this.#liveSubscriptions.delete(key);
333
+ const socket = this.#sockets.get(key);
334
+ if (!socket) return;
335
+ this.#sockets.delete(key);
336
+ const closing = socket.close().catch(() => {}).finally(() => {
337
+ if (this.#closing.get(key) === closing) this.#closing.delete(key);
338
+ });
339
+ this.#closing.set(key, closing);
340
+ }
341
+ async #getOrCreate(topic, dir) {
179
342
  if (this.#closed) throw new Error("SignalsPubSub is closed");
180
- const key = this.#socketKey(topic);
343
+ const key = socketKey(dir, topic);
344
+ const closing = this.#closing.get(key);
345
+ if (closing) {
346
+ await closing;
347
+ if (this.#closed) throw new Error("SignalsPubSub is closed");
348
+ }
181
349
  const existing = this.#sockets.get(key);
182
350
  if (existing) return existing;
183
351
  let inflight = this.#pending.get(key);
184
352
  if (!inflight) {
185
- inflight = this.#initSocket(topic, key);
353
+ inflight = this.#initSocket(topic, dir, key);
186
354
  this.#pending.set(key, inflight);
187
355
  }
188
356
  const socket = await inflight;
189
357
  if (this.#closed) throw new Error("SignalsPubSub is closed");
190
358
  return socket;
191
359
  }
192
- async #initSocket(topic, key) {
360
+ async #initSocket(topic, dir, key) {
193
361
  try {
194
- const socketPath = await this.#socketPath(topic);
362
+ const socketPath = await this.#socketPath(topic, dir);
195
363
  if (this.#closed) throw new Error("SignalsPubSub is closed");
196
364
  const socket = new UnixSocketPubSub(socketPath);
197
365
  this.#sockets.set(key, socket);
@@ -200,9 +368,10 @@ var SignalsPubSub = class extends PubSub {
200
368
  this.#pending.delete(key);
201
369
  }
202
370
  }
203
- async #socketPath(topic) {
371
+ async #socketPath(topic, scope) {
204
372
  let key = topicKey(topic);
205
- const dir = join(this.#rootDir, this.#dirFor(topic));
373
+ if (this.#sharedPeerDiscovery && scope !== this.#resourceId && decodeThreadTopic(topic) !== void 0 && !isSafeFileName(key)) throw new Error("Cannot route an agent thread topic whose threadId is not a safe file name");
374
+ const dir = join(this.#rootDir, scope);
206
375
  await mkdir(dir, { recursive: true });
207
376
  const candidate = join(dir, `${key}.sock`);
208
377
  if (Buffer.byteLength(candidate) > 104) {
@@ -218,7 +387,8 @@ var SignalsPubSub = class extends PubSub {
218
387
  *
219
388
  * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and
220
389
  * leases live under its own resource's directory and thread-owner discovery
221
- * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). Stale sockets from
390
+ * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). With
391
+ * `sharedAgentDiscovery`, peer discovery runs there too. Stale sockets from
222
392
  * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s
223
393
  * broker election logic.
224
394
  */
@@ -1 +1 @@
1
- {"version":3,"file":"signals-pubsub.js","names":["#resourceId","#rootDir","#leaseSockets","#sockets","#pending","#leaseProvider","#leasesFor","#getOrCreate","#socketKey","#closed","#dirFor","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { 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 OWNER_DISCOVERY_TOPIC = 'agent.thread-owner-discovery';\nconst MAX_PATH_SEGMENT_LENGTH = 128;\n\nexport type SignalsPubSubOptions = {\n /** Directory that holds every resource's socket directory. Defaults to `/tmp/mc`. */\n rootDir?: string;\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\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\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 * - Everything else, including peer discovery (which lists threads), stays in\n * 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 */\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 #leaseProvider: LeaseProvider;\n #closed = false;\n\n constructor(resourceId: string, options: SignalsPubSubOptions = {}) {\n super();\n this.#resourceId = resourceId;\n this.#rootDir = options.rootDir ?? DEFAULT_SOCKET_ROOT;\n // Created eagerly, as before, so this resource's lease socket exists from the start.\n this.#leasesFor(resourceId);\n const route = (key: string) => this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);\n this.#leaseProvider = {\n acquireLease: (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),\n getLeaseOwner: key => route(key).getLeaseOwner(key),\n releaseLease: (key, owner) => route(key).releaseLease(key, owner),\n renewLease: (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),\n transferLease: (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 const socket = await this.#getOrCreate(topic);\n await socket.publish(topic, event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n const socket = await this.#getOrCreate(topic);\n await socket.subscribe(topic, cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n const socket = this.#sockets.get(this.#socketKey(topic));\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\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 ]);\n this.#sockets.clear();\n this.#leaseSockets.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(this.#socketKey(topic));\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, '.leases.sock'));\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 const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) return resourceOfThreadKey(decoded) ?? this.#resourceId;\n return this.#resourceId;\n }\n\n #socketKey(topic: string): string {\n return `${this.#dirFor(topic)}/${topicKey(topic)}`;\n }\n\n async #getOrCreate(topic: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = this.#socketKey(topic);\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, 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, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic);\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): Promise<string> {\n let key = topicKey(topic);\n const dir = join(this.#rootDir, this.#dirFor(topic));\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}). 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 OWNER_DISCOVERY_TOPIC = 'agent.thread-owner-discovery';\nconst PEER_DISCOVERY_TOPIC = 'agent.thread-peer-discovery';\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/** Whether a name can be a file in a directory: no separators or control characters, not `.`/`..`. */\nfunction isSafeFileName(value: string): boolean {\n if (!value || 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/**\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, '.leases.sock'));\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 // With shared discovery, another project's threadIds reach this process\n // through discovery; a threadId names a file in that resource's directory,\n // so never let it reach outside it.\n if (\n this.#sharedPeerDiscovery &&\n scope !== this.#resourceId &&\n decodeThreadTopic(topic) !== undefined &&\n !isSafeFileName(key)\n ) {\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/code-sdk",
3
- "version": "1.10.2-alpha.0",
3
+ "version": "1.11.0-alpha.2",
4
4
  "description": "Mastra Code SDK: the agent core behind Mastra Code (everything except the TUI) — build your own UIs and surfaces on top of it",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -62,20 +62,20 @@
62
62
  "vscode-languageserver-protocol": "^3.17.5",
63
63
  "yaml": "^2.7.1",
64
64
  "zod": "^4.6.4",
65
+ "@mastra/core": "1.75.0-alpha.2",
65
66
  "@mastra/agent-browser": "0.5.3",
66
- "@mastra/duckdb": "1.12.1",
67
+ "@mastra/duckdb": "1.13.0-alpha.0",
67
68
  "@mastra/fastembed": "1.3.2",
68
69
  "@mastra/github-signals": "0.5.1",
69
- "@mastra/libsql": "1.25.0",
70
- "@mastra/core": "1.75.0-alpha.0",
71
- "@mastra/mcp": "2.1.2",
72
- "@mastra/observability": "1.18.3",
73
- "@mastra/memory": "1.36.0-alpha.0",
70
+ "@mastra/mcp": "2.2.0-alpha.0",
71
+ "@mastra/libsql": "1.25.1-alpha.0",
72
+ "@mastra/memory": "1.36.0-alpha.1",
73
+ "@mastra/pg": "1.30.0-alpha.1",
74
74
  "@mastra/parallel": "0.1.2",
75
- "@mastra/pg": "1.29.0",
75
+ "@mastra/schema-compat": "1.3.13",
76
+ "@mastra/observability": "1.18.3",
76
77
  "@mastra/stagehand": "0.3.5",
77
- "@mastra/tavily": "1.1.2",
78
- "@mastra/schema-compat": "1.3.13"
78
+ "@mastra/tavily": "1.1.2"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@libsql/client": "^0.18.0",