@mastra/code-sdk 1.10.1 → 1.10.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,37 +2,134 @@ import { 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
5
+ const DEFAULT_SOCKET_ROOT = "/tmp/mc";
6
+ const SHARED_SCOPE_DIR = "_shared";
5
7
  const THREAD_STREAM_PREFIX = "agent.thread-stream.";
8
+ const THREAD_KEY_SEPARATOR = "\0";
9
+ const THREAD_CLAIM_LEASE_PREFIX = "thread-claim:";
10
+ const NOTIFICATION_DISPATCH_LEASE_PREFIX = "notification-dispatch:";
11
+ const OWNER_DISCOVERY_TOPIC = "agent.thread-owner-discovery";
12
+ const MAX_PATH_SEGMENT_LENGTH = 128;
13
+ /**
14
+ * Whether an id can name a single directory: no path separators or control
15
+ * characters, not `.`/`..`, and bounded in length.
16
+ */
17
+ function isSafePathSegment(value) {
18
+ if (!value || value.length > MAX_PATH_SEGMENT_LENGTH || value === "." || value === "..") return false;
19
+ for (const char of value) {
20
+ const code = char.charCodeAt(0);
21
+ if (char === "/" || char === "\\" || code < 32 || code === 127) return false;
22
+ }
23
+ return true;
24
+ }
25
+ /** The resource a thread key (`<resourceId>\0<threadId>`) belongs to. */
26
+ function resourceOfThreadKey(key) {
27
+ const separatorIdx = key.indexOf(THREAD_KEY_SEPARATOR);
28
+ if (separatorIdx <= 0) return void 0;
29
+ const resourceId = key.slice(0, separatorIdx);
30
+ return isSafePathSegment(resourceId) ? resourceId : void 0;
31
+ }
32
+ /** Decodes `agent.thread-stream.<encoded resourceId\0threadId>[...]`. */
33
+ function decodeThreadTopic(topic) {
34
+ if (!topic.startsWith(THREAD_STREAM_PREFIX)) return void 0;
35
+ try {
36
+ return decodeURIComponent(topic.slice(20));
37
+ } catch {
38
+ return;
39
+ }
40
+ }
41
+ /**
42
+ * The lease that lets exactly one process dispatch a resource's due
43
+ * notifications. Held in that resource's directory, so every process serving
44
+ * the resource contends for it whichever project it was started in.
45
+ */
46
+ function notificationDispatchLeaseKey(resourceId) {
47
+ return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}`;
48
+ }
49
+ /**
50
+ * The resource a lease key belongs to: a thread's run lease and claim lease,
51
+ * and a resource's notification dispatch lease.
52
+ */
53
+ function resourceOfLeaseKey(key) {
54
+ if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {
55
+ const resourceId = key.slice(22);
56
+ return isSafePathSegment(resourceId) ? resourceId : void 0;
57
+ }
58
+ return resourceOfThreadKey(key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(13) : key);
59
+ }
60
+ function isOwnerDiscoveryTopic(topic) {
61
+ return topic === OWNER_DISCOVERY_TOPIC || topic.startsWith(`${OWNER_DISCOVERY_TOPIC}.`);
62
+ }
63
+ /**
64
+ * Derive a filesystem-safe key for the topic. Thread-stream topics embed
65
+ * a threadId; all other topics use a sanitized version of the topic name.
66
+ */
67
+ function topicKey(topic) {
68
+ const decoded = decodeThreadTopic(topic);
69
+ if (decoded !== void 0) {
70
+ const separatorIdx = decoded.indexOf(THREAD_KEY_SEPARATOR);
71
+ if (separatorIdx !== -1) return decoded.slice(separatorIdx + 1);
72
+ }
73
+ return topic.replace(/[^a-zA-Z0-9_-]/g, "_");
74
+ }
6
75
  /**
7
76
  * A PubSub that manages one Unix socket per topic for cross-process signal
8
- * coordination within a mastracode resource.
77
+ * coordination between mastracode processes on this machine.
78
+ *
79
+ * Socket paths use `<root>/<dir>/<sanitized-topic>.sock` (root `/tmp/mc`)
80
+ * for inspectability and automatic OS cleanup. Each topic gets its own
81
+ * socket, so broker election and message routing are per-topic.
9
82
  *
10
- * Socket paths use `/tmp/mc/<resourceId>/<sanitized-topic>.sock` for
11
- * inspectability and automatic OS cleanup. Each topic gets its own isolated
12
- * socket so broker election and message routing are per-topic. A dedicated
13
- * resource-wide socket exposes filesystem leases shared by every topic.
83
+ * Every process shares one database, so any process can act on any project's
84
+ * thread — the notification dispatcher, for one, delivers whatever is due.
85
+ * Whatever coordinates a thread therefore lives with the thread, not with the
86
+ * process touching it:
87
+ *
88
+ * - A thread's stream topic and its leases (the run lease and the claim
89
+ * lease) live in the directory of the resource that owns the thread, so a
90
+ * process from another project sees that thread's active run and claim.
91
+ * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds
92
+ * a thread claimed can reach the owner and hand it the signal. It only
93
+ * 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.
14
96
  *
15
97
  * Stale sockets from crashed processes are handled by
16
98
  * {@link UnixSocketPubSub}'s built-in election logic: it detects
17
99
  * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.
18
100
  * No blanket cleanup is needed here — that would break concurrent
19
- * mc instances sharing the same resourceId.
101
+ * mc instances sharing a directory.
20
102
  */
21
103
  var SignalsPubSub = class extends PubSub {
22
104
  #resourceId;
23
- #leaseProvider;
105
+ #rootDir;
106
+ #leaseSockets = /* @__PURE__ */ new Map();
24
107
  #sockets = /* @__PURE__ */ new Map();
25
108
  #pending = /* @__PURE__ */ new Map();
109
+ #leaseProvider;
26
110
  #closed = false;
27
- constructor(resourceId) {
111
+ constructor(resourceId, options = {}) {
28
112
  super();
29
113
  this.#resourceId = resourceId;
30
- this.#leaseProvider = new UnixSocketPubSub(join("/tmp/mc", resourceId, ".leases.sock"));
114
+ this.#rootDir = options.rootDir ?? DEFAULT_SOCKET_ROOT;
115
+ this.#leasesFor(resourceId);
116
+ const route = (key) => this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);
117
+ 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)
123
+ };
31
124
  }
32
125
  get supportedModes() {
33
126
  return ["push"];
34
127
  }
35
- /** Exposes the resource-wide filesystem lease provider shared by every topic socket. */
128
+ /**
129
+ * Filesystem leases. A thread's run and claim leases are held in the
130
+ * directory of the thread's resource, so every process contends for the
131
+ * same lease; any other key stays in this resource's directory.
132
+ */
36
133
  getLeaseProvider() {
37
134
  return this.#leaseProvider;
38
135
  }
@@ -43,7 +140,7 @@ var SignalsPubSub = class extends PubSub {
43
140
  await (await this.#getOrCreate(topic)).subscribe(topic, cb, options);
44
141
  }
45
142
  async unsubscribe(topic, cb) {
46
- const socket = this.#sockets.get(this.#topicKey(topic));
143
+ const socket = this.#sockets.get(this.#socketKey(topic));
47
144
  if (!socket) return;
48
145
  await socket.unsubscribe(topic, cb);
49
146
  }
@@ -52,16 +149,35 @@ var SignalsPubSub = class extends PubSub {
52
149
  }
53
150
  async close() {
54
151
  this.#closed = true;
55
- await Promise.allSettled([this.#leaseProvider.close(), ...[...this.#sockets.values()].map((s) => s.close())]);
152
+ await Promise.allSettled([...[...this.#leaseSockets.values()].map((s) => s.close()), ...[...this.#sockets.values()].map((s) => s.close())]);
56
153
  this.#sockets.clear();
154
+ this.#leaseSockets.clear();
57
155
  }
58
156
  /** Get the underlying socket for a topic (for testing/inspection). */
59
157
  getSocket(topic) {
60
- return this.#sockets.get(this.#topicKey(topic));
158
+ return this.#sockets.get(this.#socketKey(topic));
159
+ }
160
+ #leasesFor(resourceId) {
161
+ let socket = this.#leaseSockets.get(resourceId);
162
+ if (!socket) {
163
+ socket = new UnixSocketPubSub(join(this.#rootDir, resourceId, ".leases.sock"));
164
+ this.#leaseSockets.set(resourceId, socket);
165
+ }
166
+ return socket;
167
+ }
168
+ /** The directory, under the root, that holds a topic's socket. */
169
+ #dirFor(topic) {
170
+ if (isOwnerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;
171
+ const decoded = decodeThreadTopic(topic);
172
+ if (decoded !== void 0) return resourceOfThreadKey(decoded) ?? this.#resourceId;
173
+ return this.#resourceId;
174
+ }
175
+ #socketKey(topic) {
176
+ return `${this.#dirFor(topic)}/${topicKey(topic)}`;
61
177
  }
62
178
  async #getOrCreate(topic) {
63
179
  if (this.#closed) throw new Error("SignalsPubSub is closed");
64
- const key = this.#topicKey(topic);
180
+ const key = this.#socketKey(topic);
65
181
  const existing = this.#sockets.get(key);
66
182
  if (existing) return existing;
67
183
  let inflight = this.#pending.get(key);
@@ -85,8 +201,8 @@ var SignalsPubSub = class extends PubSub {
85
201
  }
86
202
  }
87
203
  async #socketPath(topic) {
88
- let key = this.#topicKey(topic);
89
- const dir = join("/tmp/mc", this.#resourceId);
204
+ let key = topicKey(topic);
205
+ const dir = join(this.#rootDir, this.#dirFor(topic));
90
206
  await mkdir(dir, { recursive: true });
91
207
  const candidate = join(dir, `${key}.sock`);
92
208
  if (Buffer.byteLength(candidate) > 104) {
@@ -95,34 +211,21 @@ var SignalsPubSub = class extends PubSub {
95
211
  }
96
212
  return candidate;
97
213
  }
98
- /**
99
- * Derive a filesystem-safe key for the topic. Thread-stream topics embed
100
- * a threadId; all other topics use a sanitized version of the topic name.
101
- */
102
- #topicKey(topic) {
103
- if (topic.startsWith(THREAD_STREAM_PREFIX)) {
104
- const encoded = topic.slice(20);
105
- try {
106
- const decoded = decodeURIComponent(encoded);
107
- const separatorIdx = decoded.indexOf("\0");
108
- if (separatorIdx !== -1) return decoded.slice(separatorIdx + 1);
109
- } catch {}
110
- }
111
- return topic.replace(/[^a-zA-Z0-9_-]/g, "_");
112
- }
113
214
  };
114
215
  /**
115
216
  * Creates a per-topic PubSub backed by Unix sockets for cross-process signal
116
- * and workflow event coordination within a mastracode resource.
217
+ * and workflow event coordination between mastracode processes.
117
218
  *
118
- * Each topic gets its own Unix socket under `/tmp/mc/<resourceId>/`.
119
- * Stale sockets from crashed processes are handled by the underlying
120
- * {@link UnixSocketPubSub}'s broker election logic.
219
+ * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and
220
+ * leases live under its own resource's directory and thread-owner discovery
221
+ * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). Stale sockets from
222
+ * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s
223
+ * broker election logic.
121
224
  */
122
- function createSignalsPubSub(resourceId) {
123
- return new SignalsPubSub(resourceId);
225
+ function createSignalsPubSub(resourceId, options) {
226
+ return new SignalsPubSub(resourceId, options);
124
227
  }
125
228
  //#endregion
126
- export { createSignalsPubSub };
229
+ export { createSignalsPubSub, notificationDispatchLeaseKey };
127
230
 
128
231
  //# sourceMappingURL=signals-pubsub.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"signals-pubsub.js","names":["#resourceId","#leaseProvider","#sockets","#pending","#getOrCreate","#topicKey","#closed","#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 THREAD_STREAM_PREFIX = 'agent.thread-stream.';\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination within a mastracode resource.\n *\n * Socket paths use `/tmp/mc/<resourceId>/<sanitized-topic>.sock` for\n * inspectability and automatic OS cleanup. Each topic gets its own isolated\n * socket so broker election and message routing are per-topic. A dedicated\n * resource-wide socket exposes filesystem leases shared by every topic.\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 the same resourceId.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #leaseProvider: UnixSocketPubSub;\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n #closed = false;\n\n constructor(resourceId: string) {\n super();\n this.#resourceId = resourceId;\n this.#leaseProvider = new UnixSocketPubSub(join('/tmp/mc', resourceId, '.leases.sock'));\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /** Exposes the resource-wide filesystem lease provider shared by every topic socket. */\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.#topicKey(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([this.#leaseProvider.close(), ...[...this.#sockets.values()].map(s => s.close())]);\n this.#sockets.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.#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.#topicKey(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 = this.#topicKey(topic);\n const dir = join('/tmp/mc', this.#resourceId);\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 * 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 */\n #topicKey(topic: string): string {\n if (topic.startsWith(THREAD_STREAM_PREFIX)) {\n const encoded = topic.slice(THREAD_STREAM_PREFIX.length);\n try {\n const decoded = decodeURIComponent(encoded);\n const separatorIdx = decoded.indexOf('\\0');\n if (separatorIdx !== -1) {\n return decoded.slice(separatorIdx + 1);\n }\n } catch {\n // Malformed URI — fall through to sanitized fallback.\n }\n }\n // Fallback: use the topic directly (sanitized for filesystem)\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination within a mastracode resource.\n *\n * Each topic gets its own Unix socket under `/tmp/mc/<resourceId>/`.\n * Stale sockets from crashed processes are handled by the underlying\n * {@link UnixSocketPubSub}'s broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string): SignalsPubSub {\n return new SignalsPubSub(resourceId);\n}\n"],"mappings":"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"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/code-sdk",
3
- "version": "1.10.1",
3
+ "version": "1.10.2-alpha.0",
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": {
@@ -63,19 +63,19 @@
63
63
  "yaml": "^2.7.1",
64
64
  "zod": "^4.6.4",
65
65
  "@mastra/agent-browser": "0.5.3",
66
- "@mastra/core": "1.74.0",
67
66
  "@mastra/duckdb": "1.12.1",
68
67
  "@mastra/fastembed": "1.3.2",
69
68
  "@mastra/github-signals": "0.5.1",
70
69
  "@mastra/libsql": "1.25.0",
70
+ "@mastra/core": "1.75.0-alpha.0",
71
71
  "@mastra/mcp": "2.1.2",
72
- "@mastra/memory": "1.35.0",
73
72
  "@mastra/observability": "1.18.3",
73
+ "@mastra/memory": "1.36.0-alpha.0",
74
74
  "@mastra/parallel": "0.1.2",
75
75
  "@mastra/pg": "1.29.0",
76
- "@mastra/schema-compat": "1.3.13",
77
76
  "@mastra/stagehand": "0.3.5",
78
- "@mastra/tavily": "1.1.2"
77
+ "@mastra/tavily": "1.1.2",
78
+ "@mastra/schema-compat": "1.3.13"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@libsql/client": "^0.18.0",