agent-embassy 4.1.0 → 4.3.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.
- package/CHANGELOG.md +17 -0
- package/README.md +135 -35
- package/SECURITY.md +25 -11
- package/dist/src/gateway/broker-control.js +9 -2
- package/dist/src/gateway/broker-control.js.map +1 -1
- package/dist/src/gateway/broker.d.ts +14 -0
- package/dist/src/gateway/broker.js +6 -2
- package/dist/src/gateway/broker.js.map +1 -1
- package/dist/src/gateway/codex-discovery.d.ts +42 -0
- package/dist/src/gateway/codex-discovery.js +604 -0
- package/dist/src/gateway/codex-discovery.js.map +1 -0
- package/dist/src/gateway/codex-stateless-transport.d.ts +1 -1
- package/dist/src/gateway/codex-stateless-transport.js +126 -18
- package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
- package/dist/src/gateway/core-cli.js +14 -6
- package/dist/src/gateway/core-cli.js.map +1 -1
- package/dist/src/gateway/core-version.d.ts +1 -1
- package/dist/src/gateway/core-version.js +1 -1
- package/dist/src/gateway/endpoint-directory.d.ts +12 -0
- package/dist/src/gateway/endpoint-directory.js +94 -4
- package/dist/src/gateway/endpoint-directory.js.map +1 -1
- package/dist/src/gateway/ledger-codec.js +11 -6
- package/dist/src/gateway/ledger-codec.js.map +1 -1
- package/dist/src/gateway/ledger.d.ts +2 -1
- package/dist/src/gateway/ledger.js +5 -2
- package/dist/src/gateway/ledger.js.map +1 -1
- package/dist/src/gateway/local-control.d.ts +1 -1
- package/dist/src/gateway/local-control.js +1 -1
- package/dist/src/gateway/owned-state.d.ts +1 -0
- package/dist/src/gateway/owned-state.js +1 -1
- package/dist/src/gateway/owned-state.js.map +1 -1
- package/dist/src/gateway/runtime.d.ts +4 -2
- package/dist/src/gateway/runtime.js +18 -5
- package/dist/src/gateway/runtime.js.map +1 -1
- package/dist/src/gateway/tui-ssh.d.ts +36 -0
- package/dist/src/gateway/tui-ssh.js +199 -0
- package/dist/src/gateway/tui-ssh.js.map +1 -0
- package/dist/src/gateway/tui.d.ts +8 -1
- package/dist/src/gateway/tui.js +103 -53
- package/dist/src/gateway/tui.js.map +1 -1
- package/docs/CONFIGURATION.md +116 -24
- package/docs/DELIVERY.md +18 -6
- package/docs/GATEWAY-ARCHITECTURE.md +50 -25
- package/package.json +2 -2
- package/skills/embassy-peer/SKILL.md +27 -9
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type LocalCodexTransportFactory, type LocalCodexTransportFactoryOptions } from "./codex-local-transport.js";
|
|
2
|
+
type Timer = ReturnType<typeof setTimeout>;
|
|
3
|
+
export type CodexThread = Readonly<{
|
|
4
|
+
id: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
status: "dormant" | "idle" | "busy" | "waiting" | "systemError" | "unknown";
|
|
7
|
+
loaded: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
export type CodexDiscoveryObservation = Readonly<{
|
|
10
|
+
observedAt?: string;
|
|
11
|
+
safeErrorCode?: string;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
complete: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
export type CodexDiscoverySnapshot = Readonly<{
|
|
16
|
+
threads: readonly CodexThread[];
|
|
17
|
+
removedIds: readonly string[];
|
|
18
|
+
observation: CodexDiscoveryObservation;
|
|
19
|
+
}>;
|
|
20
|
+
export type CodexDiscoveryObserver = Readonly<{
|
|
21
|
+
start: () => void;
|
|
22
|
+
refresh: () => Promise<void>;
|
|
23
|
+
snapshot: () => CodexDiscoverySnapshot;
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}>;
|
|
26
|
+
export type CodexDiscoveryOptions = Readonly<{
|
|
27
|
+
hostId: string;
|
|
28
|
+
local?: Omit<LocalCodexTransportFactoryOptions, "hostId">;
|
|
29
|
+
maxEndpoints?: number;
|
|
30
|
+
refreshIntervalMs?: number;
|
|
31
|
+
requestTimeoutMs?: number;
|
|
32
|
+
reconnectDelaysMs?: readonly number[];
|
|
33
|
+
onSnapshot: (snapshot: CodexDiscoverySnapshot) => void | Promise<void>;
|
|
34
|
+
}>;
|
|
35
|
+
export type CodexDiscoveryDependencies = Readonly<{
|
|
36
|
+
createFactory?: (options: LocalCodexTransportFactoryOptions) => Promise<LocalCodexTransportFactory>;
|
|
37
|
+
now?: () => Date;
|
|
38
|
+
setTimer?: (callback: () => void, milliseconds: number) => Timer;
|
|
39
|
+
clearTimer?: (timer: Timer) => void;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function createCodexDiscoveryObserver(options: CodexDiscoveryOptions, dependencies?: CodexDiscoveryDependencies): CodexDiscoveryObserver;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { CORE_VERSION } from "./core-version.js";
|
|
2
|
+
import { createLocalCodexTransportFactory, LocalCodexTransportError, } from "./codex-local-transport.js";
|
|
3
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
+
const SOURCE_KINDS = [
|
|
5
|
+
"cli", "vscode", "exec", "appServer",
|
|
6
|
+
];
|
|
7
|
+
const OPT_OUTS = [
|
|
8
|
+
"item/started", "item/completed", "item/agentMessage/delta",
|
|
9
|
+
"item/reasoning/textDelta", "item/reasoning/summaryTextDelta",
|
|
10
|
+
"item/commandExecution/outputDelta", "thread/tokenUsage/updated",
|
|
11
|
+
"turn/diff/updated", "turn/plan/updated",
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_RECONNECT_DELAYS = [250, 1_000, 5_000];
|
|
14
|
+
class DiscoveryError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code) {
|
|
17
|
+
super(code);
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
function rootThread(value) {
|
|
23
|
+
if (!record(value) || value.source === undefined)
|
|
24
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
25
|
+
return value.parentThreadId == null && ["cli", "vscode", "exec", "mcp"].includes(String(value.source));
|
|
26
|
+
}
|
|
27
|
+
function status(value) {
|
|
28
|
+
if (!record(value) || typeof value.type !== "string")
|
|
29
|
+
return "unknown";
|
|
30
|
+
if (value.type === "notLoaded")
|
|
31
|
+
return "dormant";
|
|
32
|
+
if (value.type === "idle" || value.type === "systemError") {
|
|
33
|
+
return value.type;
|
|
34
|
+
}
|
|
35
|
+
if (value.type !== "active")
|
|
36
|
+
return "unknown";
|
|
37
|
+
if (value.activeFlags === undefined)
|
|
38
|
+
return "busy";
|
|
39
|
+
if (!Array.isArray(value.activeFlags) || value.activeFlags.length > 32 ||
|
|
40
|
+
!value.activeFlags.every((flag) => typeof flag === "string" && flag.length <= 128)) {
|
|
41
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
42
|
+
}
|
|
43
|
+
if (value.activeFlags.includes("waitingOnApproval") || value.activeFlags.includes("waitingOnUserInput"))
|
|
44
|
+
return "waiting";
|
|
45
|
+
return "busy";
|
|
46
|
+
}
|
|
47
|
+
function optionalText(value) {
|
|
48
|
+
if (value === undefined || value === null)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (typeof value !== "string" || value.length > 256 || value.includes("\0")) {
|
|
51
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function parseThread(value) {
|
|
56
|
+
if (!record(value) || typeof value.id !== "string" || !UUID.test(value.id)) {
|
|
57
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
58
|
+
}
|
|
59
|
+
const observedStatus = status(value.status);
|
|
60
|
+
const thread = { id: value.id.toLowerCase(), status: observedStatus, loaded: observedStatus !== "dormant" && observedStatus !== "unknown" };
|
|
61
|
+
const name = optionalText(value.name);
|
|
62
|
+
if (name !== undefined)
|
|
63
|
+
thread.name = name;
|
|
64
|
+
return Object.freeze(thread);
|
|
65
|
+
}
|
|
66
|
+
class RpcSession {
|
|
67
|
+
transport;
|
|
68
|
+
requestTimeoutMs;
|
|
69
|
+
setTimer;
|
|
70
|
+
clearTimer;
|
|
71
|
+
notification;
|
|
72
|
+
lost;
|
|
73
|
+
#nextId = 1;
|
|
74
|
+
#pending;
|
|
75
|
+
#lost = false;
|
|
76
|
+
#remove;
|
|
77
|
+
constructor(transport, requestTimeoutMs, setTimer, clearTimer, notification, lost) {
|
|
78
|
+
this.transport = transport;
|
|
79
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
80
|
+
this.setTimer = setTimer;
|
|
81
|
+
this.clearTimer = clearTimer;
|
|
82
|
+
this.notification = notification;
|
|
83
|
+
this.lost = lost;
|
|
84
|
+
this.#remove = [
|
|
85
|
+
transport.onMessage((payload) => this.#message(payload)),
|
|
86
|
+
transport.onClose(() => this.#fail("TRANSPORT_CLOSED")),
|
|
87
|
+
transport.onError(() => this.#fail("TRANSPORT_CLOSED")),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
async initialize() {
|
|
91
|
+
const result = await this.request("initialize", {
|
|
92
|
+
capabilities: { experimentalApi: true, optOutNotificationMethods: OPT_OUTS },
|
|
93
|
+
clientInfo: { name: "embassy", title: "Embassy", version: CORE_VERSION },
|
|
94
|
+
});
|
|
95
|
+
if (!record(result))
|
|
96
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
97
|
+
void this.transport.send(JSON.stringify({ method: "initialized", params: {} }))
|
|
98
|
+
.catch(() => this.#fail("TRANSPORT_CLOSED"));
|
|
99
|
+
}
|
|
100
|
+
async request(method, params) {
|
|
101
|
+
if (this.#lost)
|
|
102
|
+
throw new DiscoveryError("TRANSPORT_CLOSED");
|
|
103
|
+
if (this.#pending !== undefined)
|
|
104
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
105
|
+
const id = this.#nextId++;
|
|
106
|
+
const response = new Promise((resolve, reject) => {
|
|
107
|
+
const timer = this.setTimer(() => {
|
|
108
|
+
if (this.#pending?.id !== id)
|
|
109
|
+
return;
|
|
110
|
+
this.#pending = undefined;
|
|
111
|
+
reject(new DiscoveryError("REQUEST_TIMEOUT"));
|
|
112
|
+
}, this.requestTimeoutMs);
|
|
113
|
+
this.#pending = { id, reject, resolve, timer };
|
|
114
|
+
});
|
|
115
|
+
void Promise.resolve().then(() => this.transport.send(JSON.stringify({ id, method, params })))
|
|
116
|
+
.catch(() => this.#reject("TRANSPORT_CLOSED"));
|
|
117
|
+
return await response;
|
|
118
|
+
}
|
|
119
|
+
dispose() {
|
|
120
|
+
this.#lost = true;
|
|
121
|
+
for (const remove of this.#remove.splice(0))
|
|
122
|
+
remove();
|
|
123
|
+
this.#reject("TRANSPORT_CLOSED");
|
|
124
|
+
}
|
|
125
|
+
#message(payload) {
|
|
126
|
+
if (Buffer.byteLength(payload) > 1024 * 1024) {
|
|
127
|
+
this.#fail("PROTOCOL_ERROR");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(payload);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
this.#fail("PROTOCOL_ERROR");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!record(parsed)) {
|
|
139
|
+
this.#fail("PROTOCOL_ERROR");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (typeof parsed.method === "string") {
|
|
143
|
+
if (parsed.id === undefined)
|
|
144
|
+
this.notification(parsed.method, parsed.params);
|
|
145
|
+
return; // Server requests and unwanted deltas are drained, never answered or retained.
|
|
146
|
+
}
|
|
147
|
+
if (typeof parsed.id !== "number" || parsed.id !== this.#pending?.id) {
|
|
148
|
+
this.#fail("PROTOCOL_ERROR");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const pending = this.#pending;
|
|
152
|
+
this.#pending = undefined;
|
|
153
|
+
this.clearTimer(pending.timer);
|
|
154
|
+
if (Object.hasOwn(parsed, "result") === Object.hasOwn(parsed, "error")) {
|
|
155
|
+
pending.reject(new DiscoveryError("PROTOCOL_ERROR"));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (Object.hasOwn(parsed, "error")) {
|
|
159
|
+
pending.reject(new DiscoveryError("RPC_REJECTED"));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
pending.resolve(parsed.result);
|
|
163
|
+
}
|
|
164
|
+
#reject(code) {
|
|
165
|
+
const pending = this.#pending;
|
|
166
|
+
if (pending === undefined)
|
|
167
|
+
return;
|
|
168
|
+
this.#pending = undefined;
|
|
169
|
+
this.clearTimer(pending.timer);
|
|
170
|
+
pending.reject(new DiscoveryError(code));
|
|
171
|
+
}
|
|
172
|
+
#fail(code) {
|
|
173
|
+
if (this.#lost)
|
|
174
|
+
return;
|
|
175
|
+
this.#lost = true;
|
|
176
|
+
this.#reject(code);
|
|
177
|
+
this.lost();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
class Observer {
|
|
181
|
+
options;
|
|
182
|
+
#max;
|
|
183
|
+
#refreshMs;
|
|
184
|
+
#requestMs;
|
|
185
|
+
#delays;
|
|
186
|
+
#now;
|
|
187
|
+
#setTimer;
|
|
188
|
+
#clearTimer;
|
|
189
|
+
#createFactory;
|
|
190
|
+
#threads = new Map();
|
|
191
|
+
#removals = new Set();
|
|
192
|
+
#events = [];
|
|
193
|
+
#sequence = 0;
|
|
194
|
+
#snapshot = Object.freeze({
|
|
195
|
+
threads: [], removedIds: [], observation: { truncated: false, complete: false },
|
|
196
|
+
});
|
|
197
|
+
#factory;
|
|
198
|
+
#session;
|
|
199
|
+
#timer;
|
|
200
|
+
#running;
|
|
201
|
+
#publishPending;
|
|
202
|
+
#publishRunning;
|
|
203
|
+
#unsubscribe = new Set();
|
|
204
|
+
#unsubscribeRunning;
|
|
205
|
+
#cleanup;
|
|
206
|
+
#cleanupFailed = false;
|
|
207
|
+
#epoch = 0;
|
|
208
|
+
#started = false;
|
|
209
|
+
#closed = false;
|
|
210
|
+
#retry = 0;
|
|
211
|
+
constructor(options, dependencies) {
|
|
212
|
+
this.options = options;
|
|
213
|
+
this.#max = Math.min(integer(options.maxEndpoints, 20), 20);
|
|
214
|
+
this.#refreshMs = integer(options.refreshIntervalMs, 5_000);
|
|
215
|
+
this.#requestMs = integer(options.requestTimeoutMs, 15_000);
|
|
216
|
+
this.#delays = options.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS;
|
|
217
|
+
if (this.#delays.length === 0 || this.#delays.some((delay) => !Number.isSafeInteger(delay) || delay < 0))
|
|
218
|
+
throw new DiscoveryError("INVALID_CONFIGURATION");
|
|
219
|
+
this.#now = dependencies.now ?? (() => new Date());
|
|
220
|
+
this.#setTimer = dependencies.setTimer ?? setTimeout;
|
|
221
|
+
this.#clearTimer = dependencies.clearTimer ?? clearTimeout;
|
|
222
|
+
this.#createFactory = dependencies.createFactory ?? ((factoryOptions) => createLocalCodexTransportFactory(factoryOptions));
|
|
223
|
+
}
|
|
224
|
+
start() {
|
|
225
|
+
if (this.#closed || this.#started)
|
|
226
|
+
return;
|
|
227
|
+
this.#started = true;
|
|
228
|
+
void this.refresh().catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
refresh() {
|
|
231
|
+
if (this.#closed)
|
|
232
|
+
return Promise.reject(new DiscoveryError("TRANSPORT_CLOSED"));
|
|
233
|
+
this.#cancelTimer();
|
|
234
|
+
if (this.#running !== undefined)
|
|
235
|
+
return this.#running;
|
|
236
|
+
this.#running = this.#run().finally(() => { this.#running = undefined; });
|
|
237
|
+
return this.#running;
|
|
238
|
+
}
|
|
239
|
+
snapshot() { return this.#snapshot; }
|
|
240
|
+
async close() {
|
|
241
|
+
if (this.#closed)
|
|
242
|
+
return;
|
|
243
|
+
this.#closed = true;
|
|
244
|
+
this.#epoch += 1;
|
|
245
|
+
this.#cancelTimer();
|
|
246
|
+
const running = this.#running;
|
|
247
|
+
await this.#disconnect();
|
|
248
|
+
await running?.catch(() => undefined);
|
|
249
|
+
await this.#publishRunning?.catch(() => undefined);
|
|
250
|
+
}
|
|
251
|
+
async #run() {
|
|
252
|
+
try {
|
|
253
|
+
if (this.#session === undefined)
|
|
254
|
+
await this.#connect();
|
|
255
|
+
await this.#scan(this.#session);
|
|
256
|
+
this.#retry = 0;
|
|
257
|
+
this.#schedule(this.#refreshMs);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
await this.#disconnect();
|
|
261
|
+
await this.#observeError(error);
|
|
262
|
+
this.#schedule(this.#delays[Math.min(this.#retry++, this.#delays.length - 1)]);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async #connect() {
|
|
267
|
+
await this.#cleanup;
|
|
268
|
+
if (this.#cleanupFailed)
|
|
269
|
+
throw new DiscoveryError("CLEANUP_FAILED");
|
|
270
|
+
const epoch = this.#epoch;
|
|
271
|
+
const factory = await this.#createFactory({ ...this.options.local, hostId: this.options.hostId });
|
|
272
|
+
if (this.#closed || epoch !== this.#epoch) {
|
|
273
|
+
await factory.close();
|
|
274
|
+
throw new DiscoveryError("TRANSPORT_CLOSED");
|
|
275
|
+
}
|
|
276
|
+
let session;
|
|
277
|
+
this.#factory = factory;
|
|
278
|
+
try {
|
|
279
|
+
const transport = await factory.connectTransport();
|
|
280
|
+
if (this.#closed || epoch !== this.#epoch) {
|
|
281
|
+
await transport.close();
|
|
282
|
+
throw new DiscoveryError("TRANSPORT_CLOSED");
|
|
283
|
+
}
|
|
284
|
+
session = new RpcSession(transport, this.#requestMs, this.#setTimer, this.#clearTimer, (method, params) => this.#event(method, params), () => this.#connectionLost(session));
|
|
285
|
+
this.#session = session;
|
|
286
|
+
this.#serial = session.initialize();
|
|
287
|
+
await this.#serial;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
await this.#disconnect();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async #scan(session) {
|
|
295
|
+
const startSequence = this.#sequence;
|
|
296
|
+
const scanned = new Map();
|
|
297
|
+
let cursor, truncated = false;
|
|
298
|
+
const cursors = new Set();
|
|
299
|
+
do {
|
|
300
|
+
const page = await this.#serialRequest(session, "thread/list", {
|
|
301
|
+
archived: false, limit: 20, sortKey: "recency_at", sourceKinds: SOURCE_KINDS, useStateDbOnly: true,
|
|
302
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
303
|
+
});
|
|
304
|
+
if (!record(page) || !Array.isArray(page.data))
|
|
305
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
306
|
+
for (const value of page.data) {
|
|
307
|
+
if (!rootThread(value))
|
|
308
|
+
continue;
|
|
309
|
+
const thread = parseThread(value);
|
|
310
|
+
if (!scanned.has(thread.id))
|
|
311
|
+
scanned.set(thread.id, thread);
|
|
312
|
+
if (scanned.size >= this.#max) {
|
|
313
|
+
truncated = this.#max < 20;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (scanned.size >= this.#max || page.nextCursor == null)
|
|
318
|
+
break;
|
|
319
|
+
if (typeof page.nextCursor !== "string" || !page.nextCursor || page.nextCursor.length > 256 || cursors.has(page.nextCursor))
|
|
320
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
321
|
+
cursor = page.nextCursor;
|
|
322
|
+
cursors.add(cursor);
|
|
323
|
+
if (cursors.size >= 21) {
|
|
324
|
+
truncated = true;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
} while (true);
|
|
328
|
+
// Pinned thread/list enriches each row with live ThreadWatchManager status;
|
|
329
|
+
// no whole-daemon loaded-ID enumeration is needed for this root-only window.
|
|
330
|
+
const firstEvent = this.#events[0]?.sequence ?? this.#sequence + 1;
|
|
331
|
+
const eventOverflow = firstEvent > startSequence + 1;
|
|
332
|
+
if (!eventOverflow) {
|
|
333
|
+
const merged = new Map();
|
|
334
|
+
for (const thread of scanned.values()) {
|
|
335
|
+
if (merged.size === this.#max)
|
|
336
|
+
break;
|
|
337
|
+
merged.set(thread.id, thread);
|
|
338
|
+
}
|
|
339
|
+
if (truncated)
|
|
340
|
+
for (const thread of this.#threads.values()) {
|
|
341
|
+
if (!merged.has(thread.id) && merged.size < this.#max)
|
|
342
|
+
merged.set(thread.id, { ...thread, status: "unknown", loaded: false });
|
|
343
|
+
}
|
|
344
|
+
for (const event of this.#events) {
|
|
345
|
+
if (event.sequence > startSequence)
|
|
346
|
+
truncated = this.#apply(event, merged) || truncated;
|
|
347
|
+
}
|
|
348
|
+
this.#threads = merged;
|
|
349
|
+
}
|
|
350
|
+
await this.#setSnapshot({
|
|
351
|
+
observedAt: this.#now().toISOString(),
|
|
352
|
+
truncated: truncated || eventOverflow,
|
|
353
|
+
complete: !truncated && !eventOverflow,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
#event(method, params) {
|
|
357
|
+
// Initialization subscribes us to every new thread, including internal
|
|
358
|
+
// sources outside thread/list's root filters. Release those subscriptions too.
|
|
359
|
+
if (method === "thread/started" && record(params) && record(params.thread) &&
|
|
360
|
+
typeof params.thread.id === "string" && UUID.test(params.thread.id)) {
|
|
361
|
+
const id = params.thread.id.toLowerCase();
|
|
362
|
+
if (!this.#unsubscribe.has(id) && this.#unsubscribe.size >= this.#max) {
|
|
363
|
+
this.#connectionLost(this.#session, new DiscoveryError("OBSERVER_BACKPRESSURE"));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.#unsubscribe.add(id);
|
|
367
|
+
this.#drainUnsubscribe();
|
|
368
|
+
}
|
|
369
|
+
let event;
|
|
370
|
+
try {
|
|
371
|
+
event = this.#parseEvent(method, params);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
this.#session?.dispose();
|
|
375
|
+
this.#connectionLost(this.#session, error);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (event === undefined)
|
|
379
|
+
return;
|
|
380
|
+
this.#events.push(event);
|
|
381
|
+
if (this.#events.length > 512)
|
|
382
|
+
this.#events.shift();
|
|
383
|
+
let overflow;
|
|
384
|
+
try {
|
|
385
|
+
overflow = this.#apply(event, this.#threads);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
this.#connectionLost(this.#session, error);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
void this.#setSnapshot({
|
|
392
|
+
...this.#snapshot.observation,
|
|
393
|
+
complete: this.#snapshot.observation.complete && !overflow,
|
|
394
|
+
truncated: this.#snapshot.observation.truncated || overflow,
|
|
395
|
+
}).catch(() => undefined);
|
|
396
|
+
}
|
|
397
|
+
#parseEvent(method, params) {
|
|
398
|
+
if (method === "thread/started") {
|
|
399
|
+
if (!record(params))
|
|
400
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
401
|
+
const thread = parseThread(params.thread);
|
|
402
|
+
if (!rootThread(params.thread))
|
|
403
|
+
return undefined;
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
kind: "upsert", sequence: ++this.#sequence,
|
|
406
|
+
thread: Object.freeze({ ...thread, loaded: true }),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (!["thread/status/changed", "thread/name/updated", "thread/archived",
|
|
410
|
+
"thread/deleted", "thread/closed"].includes(method))
|
|
411
|
+
return undefined;
|
|
412
|
+
const sequence = ++this.#sequence;
|
|
413
|
+
if (!record(params) || typeof params.threadId !== "string" || !UUID.test(params.threadId)) {
|
|
414
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
415
|
+
}
|
|
416
|
+
const id = params.threadId.toLowerCase();
|
|
417
|
+
if (method === "thread/archived" || method === "thread/deleted") {
|
|
418
|
+
return Object.freeze({ kind: "remove", sequence, id });
|
|
419
|
+
}
|
|
420
|
+
if (method === "thread/closed")
|
|
421
|
+
return Object.freeze({ kind: "closed", sequence, id });
|
|
422
|
+
if (method === "thread/status/changed") {
|
|
423
|
+
return Object.freeze({ kind: "status", sequence, id, status: status(params.status) });
|
|
424
|
+
}
|
|
425
|
+
const name = optionalText(params.threadName);
|
|
426
|
+
return name === undefined
|
|
427
|
+
? Object.freeze({ kind: "name", sequence, id })
|
|
428
|
+
: Object.freeze({ kind: "name", sequence, id, name });
|
|
429
|
+
}
|
|
430
|
+
#apply(event, target) {
|
|
431
|
+
if (event.kind === "upsert") {
|
|
432
|
+
if (target.has(event.thread.id))
|
|
433
|
+
target.set(event.thread.id, event.thread);
|
|
434
|
+
return false; // Only the next recency query can admit a root to the window.
|
|
435
|
+
}
|
|
436
|
+
if (event.kind === "remove") {
|
|
437
|
+
if (!this.#removals.has(event.id) && this.#removals.size >= this.#max * 4)
|
|
438
|
+
throw new DiscoveryError("PROTOCOL_ERROR");
|
|
439
|
+
target.delete(event.id);
|
|
440
|
+
this.#removals.add(event.id);
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const existing = target.get(event.id);
|
|
444
|
+
if (existing === undefined)
|
|
445
|
+
return false;
|
|
446
|
+
if (event.kind === "closed") {
|
|
447
|
+
target.set(event.id, Object.freeze({ ...existing, status: "dormant", loaded: false }));
|
|
448
|
+
}
|
|
449
|
+
else if (event.kind === "status") {
|
|
450
|
+
target.set(event.id, Object.freeze({ ...existing, status: event.status, loaded: true }));
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const next = { ...existing };
|
|
454
|
+
if (event.name === undefined)
|
|
455
|
+
delete next.name;
|
|
456
|
+
else
|
|
457
|
+
next.name = event.name;
|
|
458
|
+
target.set(event.id, Object.freeze(next));
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
#serial = Promise.resolve(undefined);
|
|
463
|
+
#serialRequest(session, method, params) {
|
|
464
|
+
if (session === undefined)
|
|
465
|
+
return Promise.reject(new DiscoveryError("TRANSPORT_CLOSED"));
|
|
466
|
+
const request = this.#serial.then(() => session.request(method, params));
|
|
467
|
+
this.#serial = request.catch(() => undefined);
|
|
468
|
+
return request;
|
|
469
|
+
}
|
|
470
|
+
#drainUnsubscribe() {
|
|
471
|
+
if (this.#unsubscribeRunning !== undefined || this.#closed)
|
|
472
|
+
return;
|
|
473
|
+
const session = this.#session;
|
|
474
|
+
if (session === undefined)
|
|
475
|
+
return;
|
|
476
|
+
this.#unsubscribeRunning = (async () => {
|
|
477
|
+
while (this.#session === session) {
|
|
478
|
+
const id = this.#unsubscribe.values().next().value;
|
|
479
|
+
if (id === undefined)
|
|
480
|
+
break;
|
|
481
|
+
this.#unsubscribe.delete(id);
|
|
482
|
+
await this.#serialRequest(session, "thread/unsubscribe", { threadId: id });
|
|
483
|
+
}
|
|
484
|
+
})().catch((error) => {
|
|
485
|
+
this.#connectionLost(session, error);
|
|
486
|
+
}).finally(() => {
|
|
487
|
+
this.#unsubscribeRunning = undefined;
|
|
488
|
+
if (this.#unsubscribe.size > 0)
|
|
489
|
+
this.#drainUnsubscribe();
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
#connectionLost(session, error = new DiscoveryError("TRANSPORT_CLOSED")) {
|
|
493
|
+
if (session === undefined || session !== this.#session || this.#closed)
|
|
494
|
+
return;
|
|
495
|
+
this.#cancelTimer();
|
|
496
|
+
void this.#observeError(error).catch(() => undefined);
|
|
497
|
+
void this.#disconnect().then(() => this.#schedule(this.#delays[Math.min(this.#retry++, this.#delays.length - 1)]), () => { void this.#observeError(new DiscoveryError("CLEANUP_FAILED")).catch(() => undefined); });
|
|
498
|
+
}
|
|
499
|
+
async #disconnect() {
|
|
500
|
+
const session = this.#session;
|
|
501
|
+
const factory = this.#factory;
|
|
502
|
+
this.#session = undefined;
|
|
503
|
+
this.#factory = undefined;
|
|
504
|
+
session?.dispose();
|
|
505
|
+
this.#unsubscribe.clear();
|
|
506
|
+
if (factory !== undefined) {
|
|
507
|
+
this.#cleanup = (async () => {
|
|
508
|
+
let failure;
|
|
509
|
+
try {
|
|
510
|
+
await session?.transport.close();
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
failure = error;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
await factory.close();
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
failure ??= error;
|
|
520
|
+
}
|
|
521
|
+
if (failure !== undefined) {
|
|
522
|
+
this.#cleanupFailed = true;
|
|
523
|
+
throw failure;
|
|
524
|
+
}
|
|
525
|
+
})();
|
|
526
|
+
}
|
|
527
|
+
await this.#cleanup;
|
|
528
|
+
}
|
|
529
|
+
#observeError(error) {
|
|
530
|
+
const safeErrorCode = error instanceof LocalCodexTransportError || error instanceof DiscoveryError
|
|
531
|
+
? error.code : "PROTOCOL_ERROR";
|
|
532
|
+
return this.#setSnapshot({ ...this.#snapshot.observation, safeErrorCode, complete: false });
|
|
533
|
+
}
|
|
534
|
+
#setSnapshot(observation) {
|
|
535
|
+
const normalized = { ...observation };
|
|
536
|
+
if (normalized.safeErrorCode === undefined)
|
|
537
|
+
delete normalized.safeErrorCode;
|
|
538
|
+
this.#snapshot = Object.freeze({
|
|
539
|
+
threads: Object.freeze([...this.#threads.values()]),
|
|
540
|
+
removedIds: Object.freeze([...this.#removals]),
|
|
541
|
+
observation: Object.freeze(normalized),
|
|
542
|
+
});
|
|
543
|
+
this.#publishPending = this.#snapshot;
|
|
544
|
+
if (this.#publishRunning === undefined) {
|
|
545
|
+
this.#publishRunning = Promise.resolve().then(() => this.#publish()).finally(() => {
|
|
546
|
+
this.#publishRunning = undefined;
|
|
547
|
+
if (this.#publishPending !== undefined)
|
|
548
|
+
void this.#setSnapshot(this.#snapshot.observation).catch(() => undefined);
|
|
549
|
+
});
|
|
550
|
+
return this.#publishRunning;
|
|
551
|
+
}
|
|
552
|
+
return this.#publishRunning;
|
|
553
|
+
}
|
|
554
|
+
async #publish() {
|
|
555
|
+
let failure;
|
|
556
|
+
while (this.#publishPending !== undefined) {
|
|
557
|
+
const delivered = this.#publishPending;
|
|
558
|
+
this.#publishPending = undefined;
|
|
559
|
+
try {
|
|
560
|
+
await this.options.onSnapshot(delivered);
|
|
561
|
+
for (const id of delivered.removedIds) {
|
|
562
|
+
if (!this.#threads.has(id))
|
|
563
|
+
this.#removals.delete(id);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
failure ??= error;
|
|
568
|
+
}
|
|
569
|
+
if (this.#snapshot === delivered &&
|
|
570
|
+
this.#removals.size !== delivered.removedIds.length) {
|
|
571
|
+
this.#snapshot = Object.freeze({
|
|
572
|
+
...delivered, removedIds: Object.freeze([...this.#removals]),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (failure !== undefined)
|
|
577
|
+
throw failure;
|
|
578
|
+
}
|
|
579
|
+
#schedule(milliseconds) {
|
|
580
|
+
if (!this.#started || this.#closed || this.#timer !== undefined)
|
|
581
|
+
return;
|
|
582
|
+
this.#timer = this.#setTimer(() => {
|
|
583
|
+
this.#timer = undefined;
|
|
584
|
+
void this.refresh().catch(() => undefined);
|
|
585
|
+
}, milliseconds);
|
|
586
|
+
}
|
|
587
|
+
#cancelTimer() {
|
|
588
|
+
if (this.#timer === undefined)
|
|
589
|
+
return;
|
|
590
|
+
this.#clearTimer(this.#timer);
|
|
591
|
+
this.#timer = undefined;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function integer(value, fallback) {
|
|
595
|
+
const candidate = value ?? fallback;
|
|
596
|
+
if (!Number.isSafeInteger(candidate) || candidate <= 0) {
|
|
597
|
+
throw new DiscoveryError("INVALID_CONFIGURATION");
|
|
598
|
+
}
|
|
599
|
+
return candidate;
|
|
600
|
+
}
|
|
601
|
+
export function createCodexDiscoveryObserver(options, dependencies = {}) {
|
|
602
|
+
return new Observer(options, dependencies);
|
|
603
|
+
}
|
|
604
|
+
//# sourceMappingURL=codex-discovery.js.map
|