@mastra/code-sdk 1.11.0-alpha.1 → 1.11.0-alpha.3
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.
- package/dist/acp/runtime.d.ts.map +1 -1
- package/dist/acp/runtime.js +17 -9
- package/dist/acp/runtime.js.map +1 -1
- package/dist/headless/cli.d.ts.map +1 -1
- package/dist/headless/cli.js +5 -3
- package/dist/headless/cli.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/signals-pubsub.d.ts +32 -4
- package/dist/utils/signals-pubsub.d.ts.map +1 -1
- package/dist/utils/signals-pubsub.js +197 -27
- package/dist/utils/signals-pubsub.js.map +1 -1
- package/package.json +11 -11
|
@@ -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
|
-
/**
|
|
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
|
-
* -
|
|
33
|
-
*
|
|
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}).
|
|
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;
|
|
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
|
-
* -
|
|
95
|
-
*
|
|
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 ??
|
|
160
|
+
this.#rootDir = options.rootDir ?? socketRootFromEnv();
|
|
161
|
+
this.#sharedPeerDiscovery = Boolean(options.sharedAgentDiscovery) && this.#canShareDiscovery();
|
|
115
162
|
this.#leasesFor(resourceId);
|
|
116
|
-
const route = (key) =>
|
|
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 (
|
|
187
|
+
await this.#publishTo(topic, this.#dirFor(topic), event, options);
|
|
138
188
|
}
|
|
139
189
|
async subscribe(topic, cb, options) {
|
|
140
|
-
await (
|
|
190
|
+
await this.#subscribeTo(topic, this.#dirFor(topic), cb, options);
|
|
141
191
|
}
|
|
142
192
|
async unsubscribe(topic, cb) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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([
|
|
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.#
|
|
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
|
-
#
|
|
176
|
-
|
|
321
|
+
#retain(key) {
|
|
322
|
+
this.#refs.set(key, (this.#refs.get(key) ?? 0) + 1);
|
|
177
323
|
}
|
|
178
|
-
|
|
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 =
|
|
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
|
-
|
|
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}).
|
|
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":";;;;AAMA,MAAM,sBAAsB;AAC5B,MAAM,mBAAmB;AACzB,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,4BAA4B;AAClC,MAAM,qCAAqC;AAC3C,MAAM,wBAAwB;AAC9B,MAAM,uBAAuB;AAC7B,MAAM,0BAA0B;AAChC,MAAM,eAAe;AACrB,MAAM,wBAAwB,IAAI,OAAO,8CAA8C,aAAa,IAAI,GAAG;AAC3G,MAAM,8BAA8B,IAAI,OAAO,mDAAmD,aAAa,IAAI,GAAG;;;;;AAMtH,SAAS,iBAAiB,OAAwB;CAChD,OAAO,sBAAsB,KAAK,KAAK,KAAK,4BAA4B,KAAK,KAAK;AACpF;AAgBA,SAAS,oBAA4B;CACnC,MAAM,OAAO,QAAQ,IAAI;CACzB,OAAO,QAAQ,WAAW,IAAI,IAAI,OAAO;AAC3C;;;;;AAMA,SAAS,kBAAkB,OAAwB;CACjD,IAAI,CAAC,SAAS,MAAM,SAAS,2BAA2B,UAAU,OAAO,UAAU,MAAM,OAAO;CAChG,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,WAAW,CAAC;EAC9B,IAAI,SAAS,OAAO,SAAS,QAAQ,OAAO,MAAQ,SAAS,KAAM,OAAO;CAC5E;CACA,OAAO;AACT;;AAGA,SAAS,oBAAoB,KAAiC;CAC5D,MAAM,eAAe,IAAI,QAAQ,oBAAoB;CACrD,IAAI,gBAAgB,GAAG,OAAO,KAAA;CAC9B,MAAM,aAAa,IAAI,MAAM,GAAG,YAAY;CAC5C,OAAO,kBAAkB,UAAU,IAAI,aAAa,KAAA;AACtD;;AAGA,SAAS,kBAAkB,OAAmC;CAC5D,IAAI,CAAC,MAAM,WAAW,oBAAoB,GAAG,OAAO,KAAA;CACpD,IAAI;EACF,OAAO,mBAAmB,MAAM,MAAM,EAA2B,CAAC;CACpE,QAAQ;EACN;CACF;AACF;;;;;;AAOA,SAAgB,6BAA6B,YAA4B;CACvE,OAAO,GAAG,qCAAqC;AACjD;;;;;AAMA,SAAS,mBAAmB,KAAiC;CAC3D,IAAI,IAAI,WAAW,kCAAkC,GAAG;EACtD,MAAM,aAAa,IAAI,MAAM,EAAyC;EACtE,OAAO,kBAAkB,UAAU,IAAI,aAAa,KAAA;CACtD;CACA,OAAO,oBACL,IAAI,WAAW,yBAAyB,IAAI,IAAI,MAAM,EAAgC,IAAI,GAC5F;AACF;AAEA,SAAS,sBAAsB,OAAwB;CACrD,OAAO,UAAU,yBAAyB,MAAM,WAAW,GAAG,sBAAsB,EAAE;AACxF;AAEA,SAAS,qBAAqB,OAAwB;CACpD,OAAO,UAAU,wBAAwB,MAAM,WAAW,GAAG,qBAAqB,EAAE;AACtF;;AAGA,SAAS,eAAe,OAAwB;CAC9C,IAAI,CAAC,SAAS,UAAU,OAAO,UAAU,MAAM,OAAO;CACtD,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,WAAW,CAAC;EAC9B,IAAI,SAAS,OAAO,SAAS,QAAQ,OAAO,MAAQ,SAAS,KAAM,OAAO;CAC5E;CACA,OAAO;AACT;;;;;AAMA,SAAS,SAAS,OAAuB;CACvC,MAAM,UAAU,kBAAkB,KAAK;CACvC,IAAI,YAAY,KAAA,GAAW;EACzB,MAAM,eAAe,QAAQ,QAAQ,oBAAoB;EACzD,IAAI,iBAAiB,IAAI,OAAO,QAAQ,MAAM,eAAe,CAAC;CAChE;CACA,OAAO,MAAM,QAAQ,mBAAmB,GAAG;AAC7C;AAEA,SAAS,UAAU,KAAa,OAAuB;CACrD,OAAO,GAAG,IAAI,GAAG,SAAS,KAAK;AACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,gBAAN,cAA4B,OAAO;CACjC;CACA;CACA,gCAAyB,IAAI,IAA8B;CAC3D,2BAAoB,IAAI,IAA8B;CACtD,2BAAoB,IAAI,IAAuC;CAC/D,wBAAiB,IAAI,IAAoB;CACzC,qCAA8B,IAAI,IAAgC;CAClE,oCAA6B,IAAI,IAAoB;CACrD,2BAAoB,IAAI,IAA2B;CACnD;CACA;CACA,UAAU;CAEV,YAAY,YAAoB,UAAgC,CAAC,GAAG;EAClE,MAAM;EACN,KAAKA,cAAc;EACnB,KAAKC,WAAW,QAAQ,WAAW,kBAAkB;EACrD,KAAKS,uBAAuB,QAAQ,QAAQ,oBAAoB,KAAK,KAAKC,mBAAmB;EAE7F,KAAKC,WAAW,UAAU;EAC1B,MAAM,SAAS,QAAgB;GAG7B,IAAI,KAAKC,SAAS,MAAM,IAAI,MAAM,yBAAyB;GAC3D,OAAO,KAAKD,WAAW,mBAAmB,GAAG,KAAK,KAAKZ,WAAW;EACpE;EACA,KAAKS,iBAAiB;GACpB,cAAc,OAAO,KAAK,OAAO,UAAU,MAAM,GAAG,CAAC,CAAC,aAAa,KAAK,OAAO,KAAK;GACpF,eAAe,OAAM,QAAO,MAAM,GAAG,CAAC,CAAC,cAAc,GAAG;GACxD,cAAc,OAAO,KAAK,UAAU,MAAM,GAAG,CAAC,CAAC,aAAa,KAAK,KAAK;GACtE,YAAY,OAAO,KAAK,OAAO,UAAU,MAAM,GAAG,CAAC,CAAC,WAAW,KAAK,OAAO,KAAK;GAChF,eAAe,OAAO,KAAK,WAAW,SAAS,UAAU,MAAM,GAAG,CAAC,CAAC,cAAc,KAAK,WAAW,SAAS,KAAK;EAClH;CACF;CAEA,IAAa,iBAAoD;EAC/D,OAAO,CAAC,MAAM;CAChB;;;;;;CAOA,mBAAkC;EAChC,OAAO,KAAKA;CACd;CAEA,MAAM,QACJ,OACA,OACA,SACe;EACf,MAAM,KAAKK,WAAW,OAAO,KAAKC,QAAQ,KAAK,GAAG,OAAO,OAAO;CAClE;CAEA,MAAM,UAAU,OAAe,IAAmB,SAA2C;EAC3F,MAAM,KAAKC,aAAa,OAAO,KAAKD,QAAQ,KAAK,GAAG,IAAI,OAAO;CACjE;CAEA,MAAM,YAAY,OAAe,IAAkC;EACjE,MAAM,KAAKE,iBAAiB,OAAO,KAAKF,QAAQ,KAAK,GAAG,EAAE;CAC5D;;;;;;;CAQA,MAAe,WAAW,OAA8B;EACtD,IAAI,CAAC,iBAAiB,KAAK,GAAG;EAC9B,MAAM,MAAM,UAAU,KAAKA,QAAQ,KAAK,GAAG,KAAK;EAChD,IAAI,CAAC,KAAKV,MAAM,IAAI,GAAG,GAAG;EAC1B,KAAKE,kBAAkB,IAAI,MAAM,KAAKA,kBAAkB,IAAI,GAAG,KAAK,KAAK,CAAC;EAC1E,MAAM,gBAAgB,KAAKD,mBAAmB,IAAI,GAAG;EACrD,IAAI,CAAC,eAAe;EACpB,KAAKA,mBAAmB,OAAO,GAAG;EAClC,KAAK,IAAI,IAAI,GAAG,IAAI,cAAc,MAAM,KAAK,KAAKY,SAAS,GAAG;CAChE;CAEA,MAAM,QAAuB;EAC3B,MAAM,QAAQ,IAAI,CAAC,GAAG,KAAKf,SAAS,OAAO,CAAC,CAAC,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC;CACnE;CAEA,MAAM,QAAuB;EAC3B,KAAKU,UAAU;EACf,MAAM,QAAQ,WAAW;GACvB,GAAG,CAAC,GAAG,KAAKX,cAAc,OAAO,CAAC,CAAC,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;GACtD,GAAG,CAAC,GAAG,KAAKC,SAAS,OAAO,CAAC,CAAC,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC;GACjD,GAAG,KAAKK,SAAS,OAAO;EAC1B,CAAC;EACD,KAAKL,SAAS,MAAM;EACpB,KAAKD,cAAc,MAAM;EACzB,KAAKG,MAAM,MAAM;EACjB,KAAKC,mBAAmB,MAAM;EAC9B,KAAKC,kBAAkB,MAAM;CAC/B;;CAGA,UAAU,OAA6C;EACrD,OAAO,KAAKJ,SAAS,IAAI,UAAU,KAAKY,QAAQ,KAAK,GAAG,KAAK,CAAC;CAChE;CAEA,qBAA8B;EAC5B,IAAI,kBAAkB,KAAKf,WAAW,KAAK,KAAKA,gBAAgB,kBAAkB,OAAO;EACzF,QAAQ,KACN,0DAA0D,KAAK,UAAU,KAAKA,WAAW,EAAE,sFAC7F;EACA,OAAO;CACT;;;;;CAMA,cAAc,OAAwB;EACpC,IAAI,iBAAiB,KAAK,GAAG,OAAO;EACpC,IAAI,CAAC,KAAKU,sBAAsB,OAAO;EACvC,MAAM,UAAU,kBAAkB,KAAK;EACvC,IAAI,YAAY,KAAA,GAAW,OAAO;EAClC,MAAM,aAAa,oBAAoB,OAAO;EAC9C,OAAO,eAAe,KAAA,KAAa,eAAe,KAAKV;CACzD;CAEA,MAAMc,WACJ,OACA,KACA,OACA,SACe;EACf,IAAI,CAAC,KAAKK,cAAc,KAAK,GAAG;GAE9B,OAAM,MADe,KAAKC,aAAa,OAAO,GAAG,EAAA,CACpC,QAAQ,OAAO,OAAO,OAAO;GAC1C;EACF;EACA,MAAM,MAAM,UAAU,KAAK,KAAK;EAGhC,KAAKC,QAAQ,GAAG;EAChB,IAAI;GAEF,OAAM,MADe,KAAKD,aAAa,OAAO,GAAG,EAAA,CACpC,QAAQ,OAAO,OAAO,OAAO;EAC5C,UAAU;GACR,KAAKF,SAAS,GAAG;EACnB;CACF;CAEA,MAAMF,aAAa,OAAe,KAAa,IAAmB,SAA2C;EAC3G,IAAI,CAAC,KAAKG,cAAc,KAAK,GAAG;GAE9B,OAAM,MADe,KAAKC,aAAa,OAAO,GAAG,EAAA,CACpC,UAAU,OAAO,IAAI,OAAO;GACzC;EACF;EACA,MAAM,MAAM,UAAU,KAAK,KAAK;EAChC,KAAKC,QAAQ,GAAG;EAChB,MAAM,aAAa,KAAKd,kBAAkB,IAAI,GAAG,KAAK;EACtD,IAAI;GACF,MAAM,SAAS,MAAM,KAAKa,aAAa,OAAO,GAAG;GACjD,MAAM,OAAO,UAAU,OAAO,IAAI,OAAO;GACzC,KAAK,KAAKb,kBAAkB,IAAI,GAAG,KAAK,OAAO,YAAY;IAEzD,MAAM,OAAO,YAAY,OAAO,EAAE;IAClC;GACF;GACA,IAAI,gBAAgB,KAAKD,mBAAmB,IAAI,GAAG;GACnD,IAAI,CAAC,eAAe;IAClB,gCAAgB,IAAI,IAAI;IACxB,KAAKA,mBAAmB,IAAI,KAAK,aAAa;GAChD;GACA,IAAI,CAAC,cAAc,IAAI,EAAE,GAAG;IAC1B,cAAc,IAAI,EAAE;IACpB,KAAKe,QAAQ,GAAG;GAClB;EACF,UAAU;GACR,KAAKH,SAAS,GAAG;EACnB;CACF;CAEA,MAAMD,iBAAiB,OAAe,KAAa,IAAkC;EACnF,MAAM,MAAM,UAAU,KAAK,KAAK;EAChC,MAAM,SAAS,KAAKd,SAAS,IAAI,GAAG;EACpC,MAAM,gBAAgB,KAAKgB,cAAc,KAAK,IAAI,KAAKb,mBAAmB,IAAI,GAAG,IAAI,KAAA;EACrF,IAAI,CAAC,eAAe,OAAO,EAAE,GAAG;GAC9B,IAAI,CAAC,QAAQ;GACb,MAAM,OAAO,YAAY,OAAO,EAAE;GAClC;EACF;EACA,IAAI,cAAc,SAAS,GAAG,KAAKA,mBAAmB,OAAO,GAAG;EAChE,IAAI;GACF,MAAM,QAAQ,YAAY,OAAO,EAAE;EACrC,UAAU;GACR,KAAKY,SAAS,GAAG;EACnB;CACF;CAEA,WAAW,YAAsC;EAC/C,IAAI,SAAS,KAAKhB,cAAc,IAAI,UAAU;EAC9C,IAAI,CAAC,QAAQ;GACX,SAAS,IAAI,iBAAiB,KAAK,KAAKD,UAAU,YAAY,cAAc,CAAC;GAC7E,KAAKC,cAAc,IAAI,YAAY,MAAM;EAC3C;EACA,OAAO;CACT;;CAGA,QAAQ,OAAuB;EAC7B,IAAI,sBAAsB,KAAK,GAAG,OAAO;EACzC,IAAI,KAAKQ,wBAAwB,qBAAqB,KAAK,GAAG,OAAO;EACrE,MAAM,UAAU,kBAAkB,KAAK;EACvC,IAAI,YAAY,KAAA,GAAW,OAAO,oBAAoB,OAAO,KAAK,KAAKV;EACvE,OAAO,KAAKA;CACd;CAEA,QAAQ,KAAmB;EACzB,KAAKK,MAAM,IAAI,MAAM,KAAKA,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC;CACpD;CAEA,SAAS,KAAmB;EAC1B,MAAM,QAAQ,KAAKA,MAAM,IAAI,GAAG,KAAK,KAAK;EAC1C,IAAI,OAAO,GAAG;GACZ,KAAKA,MAAM,IAAI,KAAK,IAAI;GACxB;EACF;EACA,KAAKA,MAAM,OAAO,GAAG;EACrB,KAAKE,kBAAkB,OAAO,GAAG;EACjC,KAAKD,mBAAmB,OAAO,GAAG;EAClC,MAAM,SAAS,KAAKH,SAAS,IAAI,GAAG;EACpC,IAAI,CAAC,QAAQ;EACb,KAAKA,SAAS,OAAO,GAAG;EACxB,MAAM,UAAyB,OAC5B,MAAM,CAAC,CACP,YAAY,CAAC,CAAC,CAAC,CACf,cAAc;GACb,IAAI,KAAKK,SAAS,IAAI,GAAG,MAAM,SAAS,KAAKA,SAAS,OAAO,GAAG;EAClE,CAAC;EACH,KAAKA,SAAS,IAAI,KAAK,OAAO;CAChC;CAEA,MAAMY,aAAa,OAAe,KAAwC;EACxE,IAAI,KAAKP,SAAS,MAAM,IAAI,MAAM,yBAAyB;EAC3D,MAAM,MAAM,UAAU,KAAK,KAAK;EAGhC,MAAM,UAAU,KAAKL,SAAS,IAAI,GAAG;EACrC,IAAI,SAAS;GACX,MAAM;GACN,IAAI,KAAKK,SAAS,MAAM,IAAI,MAAM,yBAAyB;EAC7D;EACA,MAAM,WAAW,KAAKV,SAAS,IAAI,GAAG;EACtC,IAAI,UAAU,OAAO;EAErB,IAAI,WAAW,KAAKC,SAAS,IAAI,GAAG;EACpC,IAAI,CAAC,UAAU;GACb,WAAW,KAAKkB,YAAY,OAAO,KAAK,GAAG;GAC3C,KAAKlB,SAAS,IAAI,KAAK,QAAQ;EACjC;EACA,MAAM,SAAS,MAAM;EACrB,IAAI,KAAKS,SAAS,MAAM,IAAI,MAAM,yBAAyB;EAC3D,OAAO;CACT;CAEA,MAAMS,YAAY,OAAe,KAAa,KAAwC;EACpF,IAAI;GACF,MAAM,aAAa,MAAM,KAAKC,YAAY,OAAO,GAAG;GACpD,IAAI,KAAKV,SAAS,MAAM,IAAI,MAAM,yBAAyB;GAC3D,MAAM,SAAS,IAAI,iBAAiB,UAAU;GAC9C,KAAKV,SAAS,IAAI,KAAK,MAAM;GAC7B,OAAO;EACT,UAAU;GACR,KAAKC,SAAS,OAAO,GAAG;EAC1B;CACF;CAEA,MAAMmB,YAAY,OAAe,OAAgC;EAC/D,IAAI,MAAM,SAAS,KAAK;EAIxB,IACE,KAAKb,wBACL,UAAU,KAAKV,eACf,kBAAkB,KAAK,MAAM,KAAA,KAC7B,CAAC,eAAe,GAAG,GAEnB,MAAM,IAAI,MAAM,2EAA2E;EAE7F,MAAM,MAAM,KAAK,KAAKC,UAAU,KAAK;EACrC,MAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;EACpC,MAAM,YAAY,KAAK,KAAK,GAAG,IAAI,MAAM;EAIzC,IAAI,OAAO,WAAW,SAAS,IAAI,KAAK;GACtC,MAAM,OAAO,KAAK,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAC/F,SAAS,KAAK,CAAC,CACf,MAAM,GAAG,EAAE;GACd,OAAO,KAAK,KAAK,GAAG,IAAI,MAAM;EAChC;EACA,OAAO;CACT;AACF;;;;;;;;;;;;AAaA,SAAgB,oBAAoB,YAAoB,SAA+C;CACrG,OAAO,IAAI,cAAc,YAAY,OAAO;AAC9C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/code-sdk",
|
|
3
|
-
"version": "1.11.0-alpha.
|
|
3
|
+
"version": "1.11.0-alpha.3",
|
|
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,19 +62,19 @@
|
|
|
62
62
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
63
63
|
"yaml": "^2.7.1",
|
|
64
64
|
"zod": "^4.6.4",
|
|
65
|
-
"@mastra/duckdb": "1.12.1",
|
|
66
65
|
"@mastra/agent-browser": "0.5.3",
|
|
67
|
-
"@mastra/
|
|
66
|
+
"@mastra/core": "1.75.0-alpha.3",
|
|
67
|
+
"@mastra/duckdb": "1.13.0-alpha.1",
|
|
68
|
+
"@mastra/libsql": "1.25.1-alpha.0",
|
|
69
|
+
"@mastra/mcp": "2.2.0-alpha.0",
|
|
68
70
|
"@mastra/fastembed": "1.3.2",
|
|
69
|
-
"@mastra/
|
|
70
|
-
"@mastra/mcp": "2.1.2",
|
|
71
|
-
"@mastra/core": "1.75.0-alpha.1",
|
|
72
|
-
"@mastra/memory": "1.36.0-alpha.1",
|
|
71
|
+
"@mastra/memory": "1.36.0-alpha.2",
|
|
73
72
|
"@mastra/observability": "1.18.3",
|
|
73
|
+
"@mastra/pg": "1.30.0-alpha.2",
|
|
74
|
+
"@mastra/github-signals": "0.5.1",
|
|
75
|
+
"@mastra/schema-compat": "1.3.13",
|
|
74
76
|
"@mastra/parallel": "0.1.2",
|
|
75
|
-
"@mastra/pg": "1.30.0-alpha.0",
|
|
76
77
|
"@mastra/tavily": "1.1.2",
|
|
77
|
-
"@mastra/schema-compat": "1.3.13",
|
|
78
78
|
"@mastra/stagehand": "0.3.5"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
"typescript-eslint": "^8.57.0",
|
|
88
88
|
"vitest": "4.1.11",
|
|
89
89
|
"@internal/lint": "0.0.140",
|
|
90
|
-
"@internal/
|
|
91
|
-
"@internal/
|
|
90
|
+
"@internal/workspace-test-utils": "0.0.84",
|
|
91
|
+
"@internal/types-builder": "0.0.115"
|
|
92
92
|
},
|
|
93
93
|
"engines": {
|
|
94
94
|
"node": ">=22.19.0"
|