mesurer-solid 0.2.1 → 0.2.2-beta.1

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.
@@ -0,0 +1,2649 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { createServer } from "node:http";
6
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { parseArgs } from "node:util";
10
+
11
+ const DEFAULT_PORT = 47365;
12
+
13
+ const DEFAULT_BRIDGE = `http://127.0.0.1:${DEFAULT_PORT}`;
14
+
15
+ const BRIDGE_NAME = "mesurer-codex";
16
+
17
+ const BRIDGE_PROTOCOL_VERSION = 2;
18
+
19
+ const BRIDGE_CAPABILITIES = Object.freeze([
20
+ "thread-discovery-v1",
21
+ "durable-queue-v1",
22
+ "history-recovery-v2",
23
+ "client-message-correlation-v1",
24
+ "idle-safe-shutdown-v1",
25
+ "nonterminal-retention-v1",
26
+ "instance-binding-v1",
27
+ ]);
28
+
29
+ const BRIDGE_SOURCE_HASH = createHash("sha256")
30
+ .update(await readFile(new URL(import.meta.url)))
31
+ .digest("hex");
32
+
33
+ const BRIDGE_INSTANCE_ID = randomUUID();
34
+
35
+ const BRIDGE_IDENTITY = Object.freeze({
36
+ name: BRIDGE_NAME,
37
+ protocol: BRIDGE_PROTOCOL_VERSION,
38
+ capabilities: BRIDGE_CAPABILITIES,
39
+ sourceHash: BRIDGE_SOURCE_HASH,
40
+ instanceId: BRIDGE_INSTANCE_ID,
41
+ pid: process.pid,
42
+ canShutdown: true,
43
+ });
44
+
45
+ const MAX_MESSAGE_BYTES = 64 * 1024;
46
+
47
+ const CODEX_TIMEOUT_MS = 30_000;
48
+
49
+ const APP_SERVER_TIMEOUT_MS = 5_000;
50
+
51
+ const DAEMON_RESUME_TIMEOUT_MS = 10_000;
52
+
53
+ const DAEMON_START_TIMEOUT_MS = 15_000;
54
+
55
+ const DELIVERY_STATE_VERSION = 1;
56
+
57
+ const MAX_DISCOVERED_THREADS = 10;
58
+
59
+ const MAX_DELIVERIES = 100;
60
+
61
+ const TERMINAL_DELIVERY_TTL_MS = 10 * 60_000;
62
+
63
+ const DESKTOP_LIFECYCLE_POLL_MS = 1_000;
64
+
65
+ const DESKTOP_INTERRUPTED_RECONCILE_WINDOW_MS = 2 * 60_000;
66
+
67
+ const DESKTOP_TURN_HISTORY_LIMIT = 10;
68
+
69
+ const DELIVERY_TURN_START_SKEW_MS = 60_000;
70
+
71
+ const LAST_OWNER_SHUTDOWN_DELAY_MS = 250;
72
+
73
+ const IDLE_PENDING_RECHECK_MS = 5_000;
74
+
75
+ const configuredOwnerHealthPollMs = Number(process.env.MESURER_CODEX_OWNER_POLL_MS);
76
+
77
+ const OWNER_HEALTH_POLL_MS = Number.isFinite(configuredOwnerHealthPollMs) && configuredOwnerHealthPollMs > 0
78
+ ? Math.max(50, configuredOwnerHealthPollMs)
79
+ : 5_000;
80
+
81
+ const OWNED_CHILD_SHUTDOWN_GRACE_MS = 250;
82
+
83
+ const usage = `Usage:
84
+ mesurer-codex [--thread <session-id-or-name>] [options]
85
+ mesurer-codex --register-current [--bridge <url>]
86
+ mesurer-codex --register <session-id-or-name> [--bridge <url>]
87
+
88
+ Server options:
89
+ --thread <value> Initial Codex session UUID or exact session name.
90
+ Defaults to CODEX_THREAD_ID when launched by Codex.
91
+ --cwd <path> Project directory used to scope recent-thread discovery.
92
+ Defaults to the current working directory.
93
+ --port <number> Loopback port (default: ${DEFAULT_PORT}; use 0 for any free port)
94
+ --origin <origin> Additional allowed browser Origin. Repeatable.
95
+ --codex <path> Codex executable (default: CODEX_BIN or codex)
96
+ --once Exit after one successful queued message
97
+
98
+ Thread registration:
99
+ --register-current Register CODEX_THREAD_ID with a running bridge and make it active.
100
+ --register <value> Register a specific existing/newly-created Codex thread and make it active.
101
+ --bridge <url> Running bridge URL for registration (default: ${DEFAULT_BRIDGE})
102
+
103
+ Other:
104
+ --help Show this help
105
+
106
+ Loopback browser origins such as http://localhost:* and http://127.0.0.1:* are allowed by default.
107
+ For file:// or Electron pages, pass --origin null explicitly.
108
+ Browser pages may send to locally registered threads and same-project recent threads returned by Codex app-server discovery.
109
+ `;
110
+
111
+ const { values } = parseArgs({
112
+ options: {
113
+ thread: { type: "string" },
114
+ cwd: { type: "string" },
115
+ port: { type: "string", default: String(DEFAULT_PORT) },
116
+ origin: { type: "string", multiple: true },
117
+ codex: { type: "string" },
118
+ once: { type: "boolean", default: false },
119
+ register: { type: "string" },
120
+ "register-current": { type: "boolean", default: false },
121
+ bridge: { type: "string", default: DEFAULT_BRIDGE },
122
+ help: { type: "boolean", default: false },
123
+ },
124
+ allowPositionals: false,
125
+ strict: true,
126
+ });
127
+
128
+ if (values.help) {
129
+ process.stdout.write(usage);
130
+ process.exit(0);
131
+ }
132
+
133
+ const normalizeThread = (value) => value?.trim() || null;
134
+
135
+ const normalizeCwd = (value) => value?.trim() || null;
136
+
137
+ const envThread = normalizeThread(process.env.CODEX_THREAD_ID);
138
+
139
+ const requestedRegistration = normalizeThread(values.register);
140
+
141
+ const registerCurrent = values["register-current"];
142
+
143
+ if (requestedRegistration && registerCurrent) {
144
+ process.stderr.write("Use either --register or --register-current, not both.\n");
145
+ process.exit(2);
146
+ }
147
+
148
+ const registrationThread = registerCurrent ? envThread : requestedRegistration;
149
+
150
+ if (registerCurrent && !registrationThread) {
151
+ process.stderr.write("--register-current requires CODEX_THREAD_ID. Run it from a Codex shell/tool command or use --register <thread>.\n");
152
+ process.exit(2);
153
+ }
154
+
155
+ if (registrationThread) {
156
+ const bridge = values.bridge?.trim() || DEFAULT_BRIDGE;
157
+ const cwd = normalizeCwd(values.cwd) ?? process.cwd();
158
+ let response;
159
+
160
+ try {
161
+ response = await fetch(new URL("threads/register", bridge.endsWith("/") ? bridge : `${bridge}/`), {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({ thread: registrationThread, cwd }),
165
+ });
166
+ } catch (cause) {
167
+ const error = cause instanceof Error ? cause.message : String(cause);
168
+ process.stderr.write(`Could not reach Mesurer Codex bridge at ${bridge}: ${error}\n`);
169
+ process.exit(1);
170
+ }
171
+
172
+ const text = await response.text();
173
+ let payload = {};
174
+
175
+ if (text) {
176
+ try {
177
+ payload = JSON.parse(text);
178
+ } catch {
179
+ payload = { error: text };
180
+ }
181
+ }
182
+
183
+ if (!response.ok || payload.ok === false) {
184
+ process.stderr.write(`${payload.error || `Mesurer Codex bridge returned HTTP ${response.status}.`}\n`);
185
+ process.exit(1);
186
+ }
187
+
188
+ console.log(`Registered Codex thread: ${registrationThread}`);
189
+ console.log(`BRIDGE_THREAD=${registrationThread}`);
190
+ process.exit(0);
191
+ }
192
+
193
+ const parsedPort = Number(values.port);
194
+
195
+ if (!Number.isInteger(parsedPort) || parsedPort < 0 || parsedPort > 65_535) {
196
+ process.stderr.write(`Invalid --port: ${values.port}\n`);
197
+ process.exit(2);
198
+ }
199
+
200
+ const initialThread = normalizeThread(values.thread) ?? envThread;
201
+
202
+ const initialCwd = initialThread ? (normalizeCwd(values.cwd) ?? process.cwd()) : null;
203
+
204
+ const initialAppToolsPipe = normalizeThread(process.env.CODEX_APP_TOOLS_PIPE_PATH);
205
+
206
+ const initialCodexHome = normalizeCwd(process.env.CODEX_HOME);
207
+
208
+ const codexBin = values.codex?.trim() || process.env.CODEX_BIN?.trim() || "codex";
209
+
210
+ const normalizeConfiguredOrigin = (value) => {
211
+ const origin = value?.trim();
212
+
213
+ if (!origin) return null;
214
+
215
+ if (origin === "null") return "null";
216
+
217
+ try {
218
+ const url = new URL(origin);
219
+
220
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
221
+ } catch {
222
+ return null;
223
+ }
224
+ };
225
+
226
+ const additionalOrigins = new Set();
227
+
228
+ for (const value of values.origin ?? []) {
229
+ const origin = normalizeConfiguredOrigin(value);
230
+
231
+ if (!origin) {
232
+ process.stderr.write(`Invalid --origin: ${value}\n`);
233
+ process.exit(2);
234
+ }
235
+
236
+ additionalOrigins.add(origin);
237
+ }
238
+
239
+ const registeredThreads = new Map();
240
+
241
+ const discoveredThreads = new Map();
242
+
243
+ const deliveries = new Map();
244
+
245
+ const terminalTurnEvents = new Map();
246
+
247
+ const desktopDispatchTimers = new Map();
248
+
249
+ const desktopLifecycleChecks = new Map();
250
+
251
+ const desktopLifecycleTimers = new Map();
252
+
253
+ const ownedChildren = new Set();
254
+
255
+ const clearDesktopLifecycleTimer = (deliveryId) => {
256
+ const timer = desktopLifecycleTimers.get(deliveryId);
257
+
258
+ if (timer) clearTimeout(timer);
259
+ desktopLifecycleTimers.delete(deliveryId);
260
+ };
261
+
262
+ let activeThread = null;
263
+
264
+ let activeCwd = null;
265
+
266
+ let idleShutdownTimer = null;
267
+
268
+ let ownerHealthTimer = null;
269
+
270
+ let server = null;
271
+
272
+ let shutdownStarted = false;
273
+
274
+ const spawnOwned = (...args) => {
275
+ const child = spawn(...args);
276
+ ownedChildren.add(child);
277
+
278
+ const release = () => ownedChildren.delete(child);
279
+ child.once("close", release);
280
+ child.once("error", release);
281
+
282
+ return child;
283
+ };
284
+
285
+ const cancelIdleShutdown = () => {
286
+ if (!idleShutdownTimer) return;
287
+ clearTimeout(idleShutdownTimer);
288
+ idleShutdownTimer = null;
289
+ };
290
+
291
+ const deliveryNeedsDesktopLifecycleCheck = (delivery) => {
292
+ if (delivery.transport !== "desktop-app" || delivery.dispatch !== "desktop-opened") {
293
+ return false;
294
+ }
295
+
296
+ if (delivery.status === "queued" || delivery.status === "working") return true;
297
+
298
+ return delivery.status === "interrupted"
299
+ && Date.now() - delivery.updatedAt < DESKTOP_INTERRUPTED_RECONCILE_WINDOW_MS;
300
+ };
301
+
302
+ const nonTerminalDeliveryCount = () =>
303
+ [...deliveries.values()].filter((delivery) =>
304
+ delivery.status === "queued" || delivery.status === "working").length;
305
+
306
+ const reconcilingDeliveryCount = () =>
307
+ [...deliveries.values()].filter((delivery) =>
308
+ deliveryNeedsDesktopLifecycleCheck(delivery)).length;
309
+
310
+ const retainedDeliveryCount = () =>
311
+ [...deliveries.values()].filter((delivery) =>
312
+ delivery.status === "queued"
313
+ || delivery.status === "working"
314
+ || deliveryNeedsDesktopLifecycleCheck(delivery)).length;
315
+
316
+ const bridgeCanShutdown = () =>
317
+ registeredThreads.size === 0 && retainedDeliveryCount() === 0;
318
+
319
+ const bridgeState = () => ({
320
+ registeredThreads: registeredThreads.size,
321
+ deliveries: deliveries.size,
322
+ nonTerminalDeliveries: nonTerminalDeliveryCount(),
323
+ reconcilingDeliveries: reconcilingDeliveryCount(),
324
+ idle: bridgeCanShutdown(),
325
+ });
326
+
327
+ const ownerAnchorExists = async (path) => {
328
+ try {
329
+ await stat(path);
330
+
331
+ return true;
332
+ } catch (cause) {
333
+ return cause?.code !== "ENOENT" && cause?.code !== "ENOTDIR";
334
+ }
335
+ };
336
+
337
+ const refreshOwnerHealthMonitor = () => {
338
+ const needsMonitor = process.platform !== "win32"
339
+ && [...registeredThreads.values()].some((record) => record.appToolsPipe);
340
+
341
+ if (!needsMonitor) {
342
+ if (ownerHealthTimer) clearInterval(ownerHealthTimer);
343
+ ownerHealthTimer = null;
344
+
345
+ return;
346
+ }
347
+
348
+ if (ownerHealthTimer) return;
349
+
350
+ ownerHealthTimer = setInterval(() => {
351
+ void pruneMissingDesktopOwners();
352
+ }, OWNER_HEALTH_POLL_MS);
353
+ ownerHealthTimer.unref?.();
354
+ };
355
+
356
+ const scheduleIdleShutdown = () => {
357
+ if (shutdownStarted || registeredThreads.size > 0 || idleShutdownTimer) return;
358
+
359
+ const delay = bridgeCanShutdown()
360
+ ? LAST_OWNER_SHUTDOWN_DELAY_MS
361
+ : IDLE_PENDING_RECHECK_MS;
362
+
363
+ idleShutdownTimer = setTimeout(() => {
364
+ idleShutdownTimer = null;
365
+ pruneDeliveries();
366
+
367
+ if (bridgeCanShutdown()) {
368
+ void shutdownBridge();
369
+
370
+ return;
371
+ }
372
+
373
+ if (registeredThreads.size === 0) scheduleIdleShutdown();
374
+ }, delay);
375
+ idleShutdownTimer.unref?.();
376
+ };
377
+
378
+ const unregisterThread = (thread) => {
379
+ const record = registeredThreads.get(thread);
380
+
381
+ if (!record) return false;
382
+
383
+ registeredThreads.delete(thread);
384
+
385
+ const dispatchTimer = desktopDispatchTimers.get(thread);
386
+
387
+ if (dispatchTimer) {
388
+ clearTimeout(dispatchTimer);
389
+ desktopDispatchTimers.delete(thread);
390
+ }
391
+
392
+ if (activeThread === thread) {
393
+ const next = [...registeredThreads.values()]
394
+ .sort((left, right) => right.seenAt - left.seenAt)[0] ?? null;
395
+
396
+ activeThread = next?.id ?? null;
397
+ activeCwd = next?.cwd ?? null;
398
+ }
399
+
400
+ refreshOwnerHealthMonitor();
401
+ scheduleIdleShutdown();
402
+
403
+ return true;
404
+ };
405
+
406
+ async function pruneMissingDesktopOwners() {
407
+ for (const record of registeredThreads.values()) {
408
+ if (!record.appToolsPipe) continue;
409
+
410
+ if (!(await ownerAnchorExists(record.appToolsPipe))) {
411
+ unregisterThread(record.id);
412
+ }
413
+ }
414
+ }
415
+
416
+ const registerThread = (thread, cwd, registration = {}) => {
417
+ cancelIdleShutdown();
418
+
419
+ const previous = registeredThreads.get(thread);
420
+ const nextCwd = normalizeCwd(cwd) ?? previous?.cwd ?? null;
421
+ const appToolsPipe = normalizeThread(registration.appToolsPipe) ?? previous?.appToolsPipe ?? null;
422
+ const codexHome = normalizeCwd(registration.codexHome) ?? previous?.codexHome ?? null;
423
+ registeredThreads.set(thread, {
424
+ id: thread,
425
+ cwd: nextCwd,
426
+ appToolsPipe,
427
+ codexHome,
428
+ seenAt: Date.now(),
429
+ });
430
+ activeThread = thread;
431
+
432
+ if (nextCwd) activeCwd = nextCwd;
433
+
434
+ refreshOwnerHealthMonitor();
435
+ };
436
+
437
+ if (initialThread) {
438
+ registerThread(initialThread, initialCwd, {
439
+ appToolsPipe: initialAppToolsPipe,
440
+ codexHome: initialCodexHome,
441
+ });
442
+ }
443
+
444
+ const isLoopbackOrigin = (origin) => {
445
+ try {
446
+ const url = new URL(origin);
447
+
448
+ return url.hostname === "localhost"
449
+ || url.hostname === "127.0.0.1"
450
+ || url.hostname === "::1"
451
+ || url.hostname === "[::1]";
452
+ } catch {
453
+ return false;
454
+ }
455
+ };
456
+
457
+ const corsOrigin = (origin) => {
458
+ if (!origin) return null;
459
+
460
+ if (origin === "null") return "null";
461
+
462
+ try {
463
+ const url = new URL(origin);
464
+
465
+ return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
466
+ } catch {
467
+ return null;
468
+ }
469
+ };
470
+
471
+ const originAllowed = (origin) => {
472
+ if (!origin) return true;
473
+ const normalized = corsOrigin(origin);
474
+
475
+ if (!normalized) return false;
476
+
477
+ return additionalOrigins.has(normalized) || isLoopbackOrigin(normalized);
478
+ };
479
+
480
+ const corsHeaders = (origin, exposeDenied = false) => {
481
+ const safeOrigin = corsOrigin(origin);
482
+
483
+ if (!safeOrigin || (!exposeDenied && !originAllowed(safeOrigin))) return {};
484
+
485
+ return {
486
+ "Access-Control-Allow-Origin": safeOrigin,
487
+ "Access-Control-Allow-Headers": "Content-Type, X-Mesurer-Bridge-Instance",
488
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
489
+ "Vary": "Origin",
490
+ };
491
+ };
492
+
493
+ const writeJson = (response, status, payload, origin, exposeDenied = false) => {
494
+ response.writeHead(status, {
495
+ "Content-Type": "application/json; charset=utf-8",
496
+ "Cache-Control": "no-store",
497
+ ...corsHeaders(origin, exposeDenied),
498
+ });
499
+ response.end(`${JSON.stringify(payload)}\n`);
500
+ };
501
+
502
+ const readJsonBody = async (request) => {
503
+ const chunks = [];
504
+ let bytes = 0;
505
+
506
+ for await (const chunk of request) {
507
+ bytes += chunk.length;
508
+
509
+ if (bytes > MAX_MESSAGE_BYTES) throw new Error("Mesurer Codex message is too large.");
510
+ chunks.push(chunk);
511
+ }
512
+
513
+ const text = Buffer.concat(chunks).toString("utf8");
514
+
515
+ if (!text) return {};
516
+
517
+ return JSON.parse(text);
518
+ };
519
+
520
+ const defaultCodexHome = normalizeCwd(process.env.CODEX_HOME) ?? join(homedir(), ".codex");
521
+
522
+ const deliveryStatePath = join(defaultCodexHome, "mesurer", "codex-deliveries.json");
523
+
524
+ let deliveryStateWrite = Promise.resolve();
525
+
526
+ const persistedDelivery = (delivery) => ({
527
+ id: delivery.id,
528
+ thread: delivery.thread,
529
+ message: delivery.message,
530
+ messageHash: delivery.messageHash,
531
+ transport: delivery.transport,
532
+ status: delivery.status,
533
+ turnId: delivery.turnId,
534
+ queuedSubmissionId: delivery.queuedSubmissionId,
535
+ clientUserMessageId: delivery.clientUserMessageId,
536
+ dispatch: delivery.dispatch,
537
+ dispatchError: delivery.dispatchError,
538
+ createdAt: delivery.createdAt,
539
+ updatedAt: delivery.updatedAt,
540
+ });
541
+
542
+ const persistDeliveryState = () => {
543
+ const persistedDeliveries = [];
544
+
545
+ for (const delivery of deliveries.values()) {
546
+ if (delivery.transport === "desktop-app") {
547
+ persistedDeliveries.push(persistedDelivery(delivery));
548
+ }
549
+ }
550
+
551
+ const state = {
552
+ version: DELIVERY_STATE_VERSION,
553
+ deliveries: persistedDeliveries,
554
+ };
555
+
556
+ deliveryStateWrite = deliveryStateWrite.then(async () => {
557
+ const directory = join(defaultCodexHome, "mesurer");
558
+ await mkdir(directory, { recursive: true });
559
+ const temporaryPath = `${deliveryStatePath}.${process.pid}.tmp`;
560
+ await writeFile(temporaryPath, `${JSON.stringify(state)}\n`, "utf8");
561
+ await rename(temporaryPath, deliveryStatePath);
562
+ });
563
+
564
+ return deliveryStateWrite;
565
+ };
566
+
567
+ const persistDeliveryStateSoon = () => {
568
+ void persistDeliveryState().catch((cause) => {
569
+ const error = cause instanceof Error ? cause.message : String(cause);
570
+ process.stderr.write(`Mesurer Codex bridge could not persist delivery state: ${error}\n`);
571
+ });
572
+ };
573
+
574
+ const loadDeliveryState = async () => {
575
+ let text;
576
+
577
+ try {
578
+ text = await readFile(deliveryStatePath, "utf8");
579
+ } catch (cause) {
580
+ if (cause?.code === "ENOENT") return;
581
+ throw cause;
582
+ }
583
+
584
+ const state = JSON.parse(text);
585
+
586
+ if (state?.version !== DELIVERY_STATE_VERSION || !Array.isArray(state.deliveries)) return;
587
+ const now = Date.now();
588
+
589
+ for (const value of state.deliveries) {
590
+ const id = normalizeThread(value?.id);
591
+ const thread = normalizeThread(value?.thread);
592
+ const message = value?.message == null ? "" : String(value.message);
593
+ const transport = value?.transport === "desktop-app" ? "desktop-app" : "codex-queue";
594
+ const status = value?.status;
595
+ const createdAt = Number(value?.createdAt);
596
+ const updatedAt = Number(value?.updatedAt);
597
+
598
+ if (!id || !thread || !message.trim()) continue;
599
+
600
+ if (!["queued", "working", "completed", "interrupted"].includes(status)) continue;
601
+
602
+ if (!Number.isFinite(createdAt) || !Number.isFinite(updatedAt)) continue;
603
+
604
+ if ((status === "completed" || status === "interrupted")
605
+ && now - updatedAt > TERMINAL_DELIVERY_TTL_MS) {
606
+ continue;
607
+ }
608
+
609
+ deliveries.set(id, {
610
+ id,
611
+ thread,
612
+ message,
613
+ messageHash: normalizeThread(value?.messageHash) ?? createHash("sha256").update(message).digest("hex"),
614
+ transport,
615
+ status,
616
+ turnId: normalizeThread(value?.turnId),
617
+ queuedSubmissionId: normalizeThread(value?.queuedSubmissionId),
618
+ clientUserMessageId: normalizeThread(value?.clientUserMessageId),
619
+ dispatch: normalizeThread(value?.dispatch) ?? "persisted",
620
+ dispatchError: normalizeThread(value?.dispatchError),
621
+ createdAt,
622
+ updatedAt,
623
+ });
624
+ }
625
+ };
626
+
627
+ const queuedSubmissionFromOutput = (output, thread) => {
628
+ const match = output.match(/Queued message (\S+) for thread (\S+)\.?/);
629
+
630
+ if (!match) return null;
631
+ const queuedThread = match[2].replace(/\.$/, "");
632
+
633
+ return queuedThread === thread ? match[1] : null;
634
+ };
635
+
636
+ const codexControlSocketPath = () => {
637
+ const codexHome = process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
638
+
639
+ return join(codexHome, "app-server-control", "app-server-control.sock");
640
+ };
641
+
642
+ const runCodexDaemonStart = () => new Promise((resolve, reject) => {
643
+ const child = spawnOwned(codexBin, ["app-server", "daemon", "start"], {
644
+ env: process.env,
645
+ shell: false,
646
+ stdio: ["ignore", "pipe", "pipe"],
647
+ windowsHide: true,
648
+ });
649
+
650
+ let stdout = "";
651
+ let stderr = "";
652
+ let settled = false;
653
+ let timeout;
654
+
655
+ const finish = (error, output) => {
656
+ if (settled) return;
657
+ settled = true;
658
+
659
+ if (timeout) clearTimeout(timeout);
660
+
661
+ if (error) reject(error);
662
+ else resolve(output);
663
+ };
664
+
665
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
666
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
667
+ child.on("error", (error) => finish(error));
668
+ child.on("close", (code, signal) => {
669
+ if (code === 0) {
670
+ finish(null, stdout.trim());
671
+
672
+ return;
673
+ }
674
+
675
+ const detail = stderr.trim() || stdout.trim() || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
676
+ finish(new Error(`codex app-server daemon start failed: ${detail}`));
677
+ });
678
+
679
+ timeout = setTimeout(() => {
680
+ child.kill("SIGTERM");
681
+ finish(new Error(`codex app-server daemon start timed out after ${DAEMON_START_TIMEOUT_MS}ms.`));
682
+ }, DAEMON_START_TIMEOUT_MS);
683
+ });
684
+
685
+ const missingDaemonSocket = (cause) => {
686
+ if (!(cause instanceof Error)) return false;
687
+
688
+ return cause.message.includes("No such file or directory")
689
+ || cause.message.includes("ENOENT")
690
+ || cause.message.includes("os error 2");
691
+ };
692
+
693
+ const resumeColdCodexThreadViaDaemon = (thread) => new Promise((resolve, reject) => {
694
+ if (process.platform === "win32") {
695
+ resolve({ action: "unsupported", threadStatus: null });
696
+
697
+ return;
698
+ }
699
+
700
+ const child = spawnOwned(codexBin, ["stdio-to-uds", codexControlSocketPath()], {
701
+ env: process.env,
702
+ shell: false,
703
+ stdio: ["pipe", "pipe", "pipe"],
704
+ windowsHide: true,
705
+ });
706
+
707
+ let stdoutBuffer = "";
708
+ let stderr = "";
709
+ let settled = false;
710
+ let timeout;
711
+
712
+ const finish = (error, result) => {
713
+ if (settled) return;
714
+ settled = true;
715
+
716
+ if (timeout) clearTimeout(timeout);
717
+
718
+ if (child.exitCode === null) child.kill("SIGTERM");
719
+
720
+ if (error) reject(error);
721
+ else resolve(result);
722
+ };
723
+
724
+ const send = (message) => {
725
+ child.stdin.write(`${JSON.stringify(message)}\n`);
726
+ };
727
+
728
+ const handleMessage = (message) => {
729
+ if (message?.id === "mesurer-daemon-init") {
730
+ if (message.error) {
731
+ finish(new Error(message.error.message || "Codex daemon initialize failed."));
732
+
733
+ return;
734
+ }
735
+
736
+ send({ method: "initialized" });
737
+ send({
738
+ id: "mesurer-thread-read",
739
+ method: "thread/read",
740
+ params: {
741
+ threadId: thread,
742
+ includeTurns: false,
743
+ },
744
+ });
745
+
746
+ return;
747
+ }
748
+
749
+ if (message?.id === "mesurer-thread-read") {
750
+ if (message.error) {
751
+ finish(new Error(message.error.message || "Codex daemon thread/read failed."));
752
+
753
+ return;
754
+ }
755
+
756
+ const threadStatus = message.result?.thread?.status?.type ?? null;
757
+
758
+ if (threadStatus !== "notLoaded") {
759
+ finish(null, { action: "already-loaded", threadStatus });
760
+
761
+ return;
762
+ }
763
+
764
+ send({
765
+ id: "mesurer-thread-resume",
766
+ method: "thread/resume",
767
+ params: { threadId: thread },
768
+ });
769
+
770
+ return;
771
+ }
772
+
773
+ if (message?.id === "mesurer-thread-resume") {
774
+ if (message.error) {
775
+ finish(new Error(message.error.message || "Codex daemon thread/resume failed."));
776
+
777
+ return;
778
+ }
779
+
780
+ finish(null, { action: "resumed", threadStatus: "notLoaded" });
781
+ }
782
+ };
783
+
784
+ child.stdout.on("data", (chunk) => {
785
+ stdoutBuffer += chunk.toString();
786
+
787
+ while (true) {
788
+ const newline = stdoutBuffer.indexOf("\n");
789
+
790
+ if (newline < 0) break;
791
+ const line = stdoutBuffer.slice(0, newline).trim();
792
+ stdoutBuffer = stdoutBuffer.slice(newline + 1);
793
+
794
+ if (!line) continue;
795
+
796
+ try {
797
+ handleMessage(JSON.parse(line));
798
+ } catch {
799
+ // The relay carries app-server JSONL. Ignore unrelated stdout defensively.
800
+ }
801
+ }
802
+ });
803
+ child.stderr.on("data", (chunk) => {
804
+ stderr = `${stderr}${chunk.toString()}`.slice(-8_192);
805
+ });
806
+ child.on("error", (error) => finish(error));
807
+ child.on("close", (code, signal) => {
808
+ if (settled) return;
809
+ const detail = stderr.trim() || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
810
+ finish(new Error(`Codex daemon relay exited before thread resume check completed: ${detail}`));
811
+ });
812
+
813
+ timeout = setTimeout(() => {
814
+ child.kill("SIGTERM");
815
+ finish(new Error(`Codex daemon resume check timed out after ${DAEMON_RESUME_TIMEOUT_MS}ms.`));
816
+ }, DAEMON_RESUME_TIMEOUT_MS);
817
+
818
+ send({
819
+ id: "mesurer-daemon-init",
820
+ method: "initialize",
821
+ params: {
822
+ clientInfo: {
823
+ name: "mesurer-solid",
824
+ title: "Mesurer Solid",
825
+ version: "1",
826
+ },
827
+ capabilities: {
828
+ experimentalApi: true,
829
+ },
830
+ },
831
+ });
832
+ });
833
+
834
+ const resumeColdCodexThread = async (thread) => {
835
+ try {
836
+ return await resumeColdCodexThreadViaDaemon(thread);
837
+ } catch (cause) {
838
+ if (!missingDaemonSocket(cause)) throw cause;
839
+ await runCodexDaemonStart();
840
+
841
+ return resumeColdCodexThreadViaDaemon(thread);
842
+ }
843
+ };
844
+
845
+ const runCodexQueue = (thread, message) => new Promise((resolve, reject) => {
846
+ const child = spawnOwned(codexBin, ["queue", "--thread", thread, "--message", message], {
847
+ env: process.env,
848
+ shell: false,
849
+ stdio: ["ignore", "pipe", "pipe"],
850
+ windowsHide: true,
851
+ });
852
+
853
+ let stdout = "";
854
+ let stderr = "";
855
+ let settled = false;
856
+ let timeout;
857
+
858
+ const finish = (error, output) => {
859
+ if (settled) return;
860
+ settled = true;
861
+
862
+ if (timeout) clearTimeout(timeout);
863
+
864
+ if (error) reject(error);
865
+ else resolve(output);
866
+ };
867
+
868
+ child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
869
+ child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
870
+ child.on("error", (error) => finish(error));
871
+ child.on("close", (code, signal) => {
872
+ if (code === 0) {
873
+ finish(null, stdout.trim());
874
+
875
+ return;
876
+ }
877
+
878
+ const detail = stderr.trim() || stdout.trim() || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
879
+ finish(new Error(`codex queue failed: ${detail}`));
880
+ });
881
+
882
+ timeout = setTimeout(() => {
883
+ child.kill("SIGTERM");
884
+ finish(new Error(`codex queue timed out after ${CODEX_TIMEOUT_MS}ms.`));
885
+ }, CODEX_TIMEOUT_MS);
886
+ });
887
+
888
+ const runCodexThreadList = (cwd, limit) => new Promise((resolve, reject) => {
889
+ const child = spawnOwned(codexBin, ["app-server", "--listen", "stdio://"], {
890
+ env: process.env,
891
+ shell: false,
892
+ stdio: ["pipe", "pipe", "pipe"],
893
+ windowsHide: true,
894
+ });
895
+
896
+ let stdoutBuffer = "";
897
+ let stderr = "";
898
+ let settled = false;
899
+ let timeout;
900
+
901
+ const finish = (error, result) => {
902
+ if (settled) return;
903
+ settled = true;
904
+
905
+ if (timeout) clearTimeout(timeout);
906
+
907
+ if (child.exitCode === null) child.kill("SIGTERM");
908
+
909
+ if (error) reject(error);
910
+ else resolve(result);
911
+ };
912
+
913
+ const send = (message) => {
914
+ child.stdin.write(`${JSON.stringify(message)}\n`);
915
+ };
916
+
917
+ const handleMessage = (message) => {
918
+ if (message?.id === "mesurer-init") {
919
+ if (message.error) {
920
+ finish(new Error(message.error.message || "Codex app-server initialize failed."));
921
+
922
+ return;
923
+ }
924
+
925
+ send({ method: "initialized" });
926
+ send({
927
+ id: "mesurer-thread-list",
928
+ method: "thread/list",
929
+ params: {
930
+ limit,
931
+ sortKey: "recency_at",
932
+ sortDirection: "desc",
933
+ cwd,
934
+ },
935
+ });
936
+
937
+ return;
938
+ }
939
+
940
+ if (message?.id === "mesurer-thread-list") {
941
+ if (message.error) {
942
+ finish(new Error(message.error.message || "Codex app-server thread/list failed."));
943
+
944
+ return;
945
+ }
946
+
947
+ finish(null, message.result ?? {});
948
+ }
949
+ };
950
+
951
+ child.stdout.on("data", (chunk) => {
952
+ stdoutBuffer += chunk.toString();
953
+
954
+ while (true) {
955
+ const newline = stdoutBuffer.indexOf("\n");
956
+
957
+ if (newline < 0) break;
958
+ const line = stdoutBuffer.slice(0, newline).trim();
959
+ stdoutBuffer = stdoutBuffer.slice(newline + 1);
960
+
961
+ if (!line) continue;
962
+
963
+ try {
964
+ handleMessage(JSON.parse(line));
965
+ } catch {
966
+ // App-server protocol is JSONL; ignore unrelated stdout defensively.
967
+ }
968
+ }
969
+ });
970
+ child.stderr.on("data", (chunk) => {
971
+ stderr = `${stderr}${chunk.toString()}`.slice(-8_192);
972
+ });
973
+ child.on("error", (error) => finish(error));
974
+ child.on("close", (code, signal) => {
975
+ if (settled) return;
976
+ const detail = stderr.trim() || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
977
+ finish(new Error(`Codex app-server exited before thread/list completed: ${detail}`));
978
+ });
979
+
980
+ timeout = setTimeout(() => {
981
+ child.kill("SIGTERM");
982
+ finish(new Error(`Codex app-server thread/list timed out after ${APP_SERVER_TIMEOUT_MS}ms.`));
983
+ }, APP_SERVER_TIMEOUT_MS);
984
+
985
+ send({
986
+ id: "mesurer-init",
987
+ method: "initialize",
988
+ params: {
989
+ clientInfo: {
990
+ name: "mesurer-solid",
991
+ title: "Mesurer Solid",
992
+ version: "1",
993
+ },
994
+ capabilities: {
995
+ experimentalApi: true,
996
+ },
997
+ },
998
+ });
999
+ });
1000
+
1001
+ const runCodexQueueLookup = (thread, queuedSubmissionId = null) => new Promise((resolve, reject) => {
1002
+ const child = spawnOwned(codexBin, ["app-server", "--listen", "stdio://"], {
1003
+ env: process.env,
1004
+ shell: false,
1005
+ stdio: ["pipe", "pipe", "pipe"],
1006
+ windowsHide: true,
1007
+ });
1008
+
1009
+ let stdoutBuffer = "";
1010
+ let stderr = "";
1011
+ let settled = false;
1012
+ let timeout;
1013
+ let cursor = null;
1014
+ let page = 0;
1015
+
1016
+ const finish = (error, result) => {
1017
+ if (settled) return;
1018
+ settled = true;
1019
+
1020
+ if (timeout) clearTimeout(timeout);
1021
+
1022
+ if (child.exitCode === null) child.kill("SIGTERM");
1023
+
1024
+ if (error) reject(error);
1025
+ else resolve(result);
1026
+ };
1027
+
1028
+ const send = (message) => {
1029
+ child.stdin.write(`${JSON.stringify(message)}\n`);
1030
+ };
1031
+
1032
+ const requestPage = () => {
1033
+ page += 1;
1034
+
1035
+ const params = {
1036
+ threadId: thread,
1037
+ limit: queuedSubmissionId ? 100 : 2,
1038
+ };
1039
+
1040
+ if (cursor) params.cursor = cursor;
1041
+ send({
1042
+ id: `mesurer-queue-list-${page}`,
1043
+ method: "thread/queue/list",
1044
+ params,
1045
+ });
1046
+ };
1047
+
1048
+ const handleMessage = (message) => {
1049
+ if (message?.id === "mesurer-queue-init") {
1050
+ if (message.error) {
1051
+ finish(new Error(message.error.message || "Codex app-server initialize failed."));
1052
+
1053
+ return;
1054
+ }
1055
+
1056
+ send({ method: "initialized" });
1057
+ requestPage();
1058
+
1059
+ return;
1060
+ }
1061
+
1062
+ const messageId = message?.id == null ? "" : String(message.id);
1063
+
1064
+ if (!messageId.startsWith("mesurer-queue-list-")) return;
1065
+
1066
+ if (message.error) {
1067
+ finish(new Error(message.error.message || "Codex app-server thread/queue/list failed."));
1068
+
1069
+ return;
1070
+ }
1071
+
1072
+ const data = Array.isArray(message.result?.data) ? message.result.data : [];
1073
+ const nextCursor = normalizeThread(message.result?.nextCursor);
1074
+
1075
+ if (queuedSubmissionId) {
1076
+ const submission = data.find((candidate) => candidate?.id === queuedSubmissionId);
1077
+
1078
+ if (submission) {
1079
+ finish(null, { submission, ambiguous: false });
1080
+
1081
+ return;
1082
+ }
1083
+
1084
+ if (nextCursor) {
1085
+ cursor = nextCursor;
1086
+ requestPage();
1087
+
1088
+ return;
1089
+ }
1090
+
1091
+ finish(null, { submission: null, ambiguous: false });
1092
+
1093
+ return;
1094
+ }
1095
+
1096
+ if (data.length === 1 && !nextCursor) {
1097
+ finish(null, { submission: data[0], ambiguous: false });
1098
+
1099
+ return;
1100
+ }
1101
+
1102
+ finish(null, {
1103
+ submission: null,
1104
+ ambiguous: data.length > 1 || Boolean(nextCursor),
1105
+ });
1106
+ };
1107
+
1108
+ child.stdout.on("data", (chunk) => {
1109
+ stdoutBuffer += chunk.toString();
1110
+
1111
+ while (true) {
1112
+ const newline = stdoutBuffer.indexOf("\n");
1113
+
1114
+ if (newline < 0) break;
1115
+ const line = stdoutBuffer.slice(0, newline).trim();
1116
+ stdoutBuffer = stdoutBuffer.slice(newline + 1);
1117
+
1118
+ if (!line) continue;
1119
+
1120
+ try {
1121
+ handleMessage(JSON.parse(line));
1122
+ } catch {
1123
+ // App-server protocol is JSONL; ignore unrelated stdout defensively.
1124
+ }
1125
+ }
1126
+ });
1127
+ child.stderr.on("data", (chunk) => {
1128
+ stderr = `${stderr}${chunk.toString()}`.slice(-8_192);
1129
+ });
1130
+ child.on("error", (error) => finish(error));
1131
+ child.on("close", (code, signal) => {
1132
+ if (settled) return;
1133
+ const detail = stderr.trim() || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
1134
+ finish(new Error(`Codex app-server exited before thread/queue/list completed: ${detail}`));
1135
+ });
1136
+
1137
+ timeout = setTimeout(() => {
1138
+ child.kill("SIGTERM");
1139
+ finish(new Error(`Codex app-server thread/queue/list timed out after ${APP_SERVER_TIMEOUT_MS}ms.`));
1140
+ }, APP_SERVER_TIMEOUT_MS);
1141
+
1142
+ send({
1143
+ id: "mesurer-queue-init",
1144
+ method: "initialize",
1145
+ params: {
1146
+ clientInfo: {
1147
+ name: "mesurer-solid",
1148
+ title: "Mesurer Solid",
1149
+ version: "1",
1150
+ },
1151
+ capabilities: {
1152
+ experimentalApi: true,
1153
+ },
1154
+ },
1155
+ });
1156
+ });
1157
+
1158
+ const queuedSubmissionMessage = (submission) => {
1159
+ if (!Array.isArray(submission?.input) || submission.input.length !== 1) return null;
1160
+ const input = submission.input[0];
1161
+ const text = String(input?.text ?? "");
1162
+
1163
+ return input?.type === "text" && text.trim() ? text : null;
1164
+ };
1165
+
1166
+ const runCodexTurnHistory = (thread) => new Promise((resolve, reject) => {
1167
+ const child = spawnOwned(codexBin, ["app-server", "--listen", "stdio://"], {
1168
+ env: process.env,
1169
+ shell: false,
1170
+ stdio: ["pipe", "pipe", "pipe"],
1171
+ windowsHide: true,
1172
+ });
1173
+
1174
+ let stdoutBuffer = "";
1175
+ let stderr = "";
1176
+ let settled = false;
1177
+ let timeout;
1178
+
1179
+ const finish = (error, turns) => {
1180
+ if (settled) return;
1181
+ settled = true;
1182
+
1183
+ if (timeout) clearTimeout(timeout);
1184
+
1185
+ if (child.exitCode === null) child.kill("SIGTERM");
1186
+
1187
+ if (error) reject(error);
1188
+ else resolve(turns);
1189
+ };
1190
+
1191
+ const send = (message) => {
1192
+ child.stdin.write(`${JSON.stringify(message)}\n`);
1193
+ };
1194
+
1195
+ const handleMessage = (message) => {
1196
+ if (message?.id === "mesurer-history-init") {
1197
+ if (message.error) {
1198
+ finish(new Error(message.error.message || "Codex app-server initialize failed."));
1199
+
1200
+ return;
1201
+ }
1202
+
1203
+ send({ method: "initialized" });
1204
+ send({
1205
+ id: "mesurer-history-turns",
1206
+ method: "thread/turns/list",
1207
+ params: {
1208
+ threadId: thread,
1209
+ limit: DESKTOP_TURN_HISTORY_LIMIT,
1210
+ sortDirection: "desc",
1211
+ itemsView: "summary",
1212
+ },
1213
+ });
1214
+
1215
+ return;
1216
+ }
1217
+
1218
+ if (message?.id === "mesurer-history-turns") {
1219
+ if (message.error) {
1220
+ send({
1221
+ id: "mesurer-history-read",
1222
+ method: "thread/read",
1223
+ params: {
1224
+ threadId: thread,
1225
+ includeTurns: true,
1226
+ },
1227
+ });
1228
+
1229
+ return;
1230
+ }
1231
+
1232
+ finish(null, Array.isArray(message.result?.data) ? message.result.data : []);
1233
+
1234
+ return;
1235
+ }
1236
+
1237
+ if (message?.id === "mesurer-history-read") {
1238
+ if (message.error) {
1239
+ finish(new Error(message.error.message || "Codex app-server thread history read failed."));
1240
+
1241
+ return;
1242
+ }
1243
+
1244
+ finish(null, Array.isArray(message.result?.thread?.turns) ? message.result.thread.turns : []);
1245
+ }
1246
+ };
1247
+
1248
+ child.stdout.on("data", (chunk) => {
1249
+ stdoutBuffer += chunk.toString();
1250
+
1251
+ while (true) {
1252
+ const newline = stdoutBuffer.indexOf("\n");
1253
+
1254
+ if (newline < 0) break;
1255
+ const line = stdoutBuffer.slice(0, newline).trim();
1256
+ stdoutBuffer = stdoutBuffer.slice(newline + 1);
1257
+
1258
+ if (!line) continue;
1259
+
1260
+ try {
1261
+ handleMessage(JSON.parse(line));
1262
+ } catch {
1263
+ // App-server protocol is JSONL; ignore unrelated stdout defensively.
1264
+ }
1265
+ }
1266
+ });
1267
+ child.stderr.on("data", (chunk) => {
1268
+ stderr = `${stderr}${chunk.toString()}`.slice(-8_192);
1269
+ });
1270
+ child.on("error", (error) => finish(error));
1271
+ child.on("close", (code, signal) => {
1272
+ if (settled) return;
1273
+ const detail = stderr.trim() || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
1274
+ finish(new Error(`Codex app-server exited before lifecycle history completed: ${detail}`));
1275
+ });
1276
+
1277
+ timeout = setTimeout(() => {
1278
+ child.kill("SIGTERM");
1279
+ finish(new Error(`Codex app-server lifecycle history timed out after ${APP_SERVER_TIMEOUT_MS}ms.`));
1280
+ }, APP_SERVER_TIMEOUT_MS);
1281
+
1282
+ send({
1283
+ id: "mesurer-history-init",
1284
+ method: "initialize",
1285
+ params: {
1286
+ clientInfo: {
1287
+ name: "mesurer-solid",
1288
+ title: "Mesurer Solid",
1289
+ version: "1",
1290
+ },
1291
+ capabilities: {
1292
+ experimentalApi: true,
1293
+ },
1294
+ },
1295
+ });
1296
+ });
1297
+
1298
+ const openDesktopThread = (thread) => new Promise((resolveOpen, rejectOpen) => {
1299
+ const url = `codex://threads/${thread}`;
1300
+ const override = process.env.MESURER_CODEX_DESKTOP_OPEN_BIN?.trim();
1301
+ let command;
1302
+ let args;
1303
+
1304
+ if (override) {
1305
+ command = override;
1306
+ args = [url];
1307
+ } else if (process.platform === "darwin") {
1308
+ command = "/usr/bin/open";
1309
+ args = [url];
1310
+ } else if (process.platform === "win32") {
1311
+ command = "powershell.exe";
1312
+ const quotedUrl = url.replaceAll("'", "''");
1313
+ args = [
1314
+ "-NoProfile",
1315
+ "-Command",
1316
+ `Start-Process -FilePath '${quotedUrl}'`,
1317
+ ];
1318
+ } else {
1319
+ rejectOpen(new Error(
1320
+ `Codex Desktop thread wake is unsupported on platform ${process.platform}.`,
1321
+ ));
1322
+
1323
+ return;
1324
+ }
1325
+
1326
+ const child = spawnOwned(command, args, {
1327
+ env: process.env,
1328
+ shell: false,
1329
+ stdio: ["ignore", "ignore", "pipe"],
1330
+ windowsHide: true,
1331
+ });
1332
+
1333
+ let stderr = "";
1334
+ let settled = false;
1335
+
1336
+ const timeout = setTimeout(() => {
1337
+ child.kill("SIGTERM");
1338
+
1339
+ if (settled) return;
1340
+ settled = true;
1341
+ rejectOpen(new Error("Codex Desktop thread open timed out."));
1342
+ }, APP_SERVER_TIMEOUT_MS);
1343
+
1344
+ child.stderr.on("data", (chunk) => {
1345
+ stderr = `${stderr}${chunk.toString()}`.slice(-8_192);
1346
+ });
1347
+ child.on("error", (error) => {
1348
+ if (settled) return;
1349
+ settled = true;
1350
+ clearTimeout(timeout);
1351
+ rejectOpen(error);
1352
+ });
1353
+ child.on("close", (code, signal) => {
1354
+ if (settled) return;
1355
+ settled = true;
1356
+ clearTimeout(timeout);
1357
+
1358
+ if (code === 0) {
1359
+ resolveOpen({ url });
1360
+
1361
+ return;
1362
+ }
1363
+
1364
+ const detail = stderr.trim()
1365
+ || `exit code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`;
1366
+
1367
+ rejectOpen(new Error(`Codex Desktop thread open failed: ${detail}`));
1368
+ });
1369
+ });
1370
+
1371
+ let maybeDispatchDesktopThread = async (_thread) => {};
1372
+
1373
+ const scheduleDesktopDispatch = (thread, delay = 0) => {
1374
+ if (desktopDispatchTimers.has(thread)) return;
1375
+
1376
+ const timer = setTimeout(() => {
1377
+ desktopDispatchTimers.delete(thread);
1378
+ void maybeDispatchDesktopThread(thread);
1379
+ }, delay);
1380
+
1381
+ desktopDispatchTimers.set(thread, timer);
1382
+ };
1383
+
1384
+ maybeDispatchDesktopThread = async (thread) => {
1385
+ const record = registeredThreads.get(thread);
1386
+
1387
+ if (!record?.appToolsPipe) return;
1388
+
1389
+ const delivery = [...deliveries.values()]
1390
+ .filter((candidate) =>
1391
+ candidate.thread === thread
1392
+ && candidate.transport === "desktop-app"
1393
+ && candidate.status === "queued"
1394
+ && candidate.dispatch !== "desktop-opened")
1395
+ .sort((left, right) => left.createdAt - right.createdAt)[0];
1396
+
1397
+ if (!delivery) return;
1398
+
1399
+ try {
1400
+ if (!delivery.queuedSubmissionId) {
1401
+ const output = await runCodexQueue(thread, delivery.message);
1402
+ delivery.queuedSubmissionId = queuedSubmissionFromOutput(output, thread);
1403
+
1404
+ if (!delivery.queuedSubmissionId) {
1405
+ throw new Error("Codex queue succeeded but did not return a queued submission id.");
1406
+ }
1407
+
1408
+ delivery.dispatch = "persisted";
1409
+ delivery.dispatchError = null;
1410
+ delivery.updatedAt = Date.now();
1411
+ await persistDeliveryState();
1412
+ }
1413
+
1414
+ await openDesktopThread(thread);
1415
+ delivery.dispatch = "desktop-opened";
1416
+ delivery.dispatchError = null;
1417
+ delivery.updatedAt = Date.now();
1418
+ persistDeliveryStateSoon();
1419
+ scheduleDesktopLifecycleCheck(delivery, 0);
1420
+ } catch (cause) {
1421
+ delivery.dispatch = "desktop-wait-failed";
1422
+ delivery.dispatchError = cause instanceof Error ? cause.message : String(cause);
1423
+ delivery.updatedAt = Date.now();
1424
+ persistDeliveryStateSoon();
1425
+ }
1426
+ };
1427
+
1428
+ const normalizeTitle = (value) => {
1429
+ if (value == null) return null;
1430
+ const title = String(value).replace(/\s+/g, " ").trim();
1431
+
1432
+ if (!title) return null;
1433
+
1434
+ return title.length > 80 ? `${title.slice(0, 79)}…` : title;
1435
+ };
1436
+
1437
+ const normalizeTimestamp = (value) => Number.isFinite(value) ? Number(value) : null;
1438
+
1439
+ const shortThread = (thread) => thread.length > 16 ? `${thread.slice(0, 8)}…${thread.slice(-4)}` : thread;
1440
+
1441
+ const appServerSummary = (thread, cwd) => {
1442
+ const id = normalizeThread(thread?.id);
1443
+
1444
+ if (!id) return null;
1445
+
1446
+ const title = normalizeTitle(thread?.name)
1447
+ ?? normalizeTitle(thread?.preview)
1448
+ ?? `Codex ${shortThread(id)}`;
1449
+
1450
+ return {
1451
+ id,
1452
+ title,
1453
+ updatedAt: normalizeTimestamp(thread?.recencyAt ?? thread?.updatedAt),
1454
+ connected: registeredThreads.has(id),
1455
+ cwd,
1456
+ };
1457
+ };
1458
+
1459
+ const registeredSummary = (record) => ({
1460
+ id: record.id,
1461
+ title: `Codex ${shortThread(record.id)}`,
1462
+ updatedAt: Math.floor(record.seenAt / 1_000),
1463
+ connected: true,
1464
+ cwd: record.cwd,
1465
+ });
1466
+
1467
+ const listThreadSummaries = async (scopeThread, limit) => {
1468
+ const scopeRecord = scopeThread
1469
+ ? registeredThreads.get(scopeThread) ?? discoveredThreads.get(scopeThread)
1470
+ : activeThread
1471
+ ? registeredThreads.get(activeThread) ?? discoveredThreads.get(activeThread)
1472
+ : null;
1473
+
1474
+ const scopeCwd = scopeRecord?.cwd ?? activeCwd;
1475
+ const preferredThread = scopeThread ?? activeThread;
1476
+ const summaries = [];
1477
+ let appHasMore = false;
1478
+
1479
+ if (scopeCwd) {
1480
+ try {
1481
+ const listed = await runCodexThreadList(scopeCwd, MAX_DISCOVERED_THREADS);
1482
+ const data = Array.isArray(listed?.data) ? listed.data : [];
1483
+ appHasMore = Boolean(listed?.nextCursor);
1484
+
1485
+ for (const item of data) {
1486
+ const summary = appServerSummary(item, scopeCwd);
1487
+
1488
+ if (!summary) continue;
1489
+ discoveredThreads.set(summary.id, summary);
1490
+ summaries.push(summary);
1491
+ }
1492
+ } catch {
1493
+ // Discovery is optional. Registered SessionStart destinations still work without app-server listing.
1494
+ }
1495
+ }
1496
+
1497
+ const byId = new Map(summaries.map((summary) => [summary.id, summary]));
1498
+ const ordered = [];
1499
+ const seen = new Set();
1500
+
1501
+ const push = (summary) => {
1502
+ if (!summary || seen.has(summary.id)) return;
1503
+ seen.add(summary.id);
1504
+ ordered.push({
1505
+ id: summary.id,
1506
+ title: summary.title,
1507
+ updatedAt: summary.updatedAt,
1508
+ connected: registeredThreads.has(summary.id),
1509
+ });
1510
+ };
1511
+
1512
+ if (preferredThread) {
1513
+ push(byId.get(preferredThread)
1514
+ ?? (registeredThreads.get(preferredThread) ? registeredSummary(registeredThreads.get(preferredThread)) : null)
1515
+ ?? discoveredThreads.get(preferredThread));
1516
+ }
1517
+
1518
+ for (const summary of summaries) push(summary);
1519
+
1520
+ for (const record of registeredThreads.values()) {
1521
+ if (scopeCwd && record.cwd && record.cwd !== scopeCwd) continue;
1522
+ push(byId.get(record.id) ?? registeredSummary(record));
1523
+ }
1524
+
1525
+ return {
1526
+ thread: preferredThread,
1527
+ threadDetails: ordered.slice(0, limit),
1528
+ hasMore: appHasMore || ordered.length > limit,
1529
+ };
1530
+ };
1531
+
1532
+ const threadPayload = () => ({
1533
+ thread: activeThread,
1534
+ threads: [...registeredThreads.keys()],
1535
+ });
1536
+
1537
+ const hashMessage = (message) => createHash("sha256").update(message).digest("hex");
1538
+
1539
+ const turnKey = (thread, turnId) => `${thread}:${turnId}`;
1540
+
1541
+ const publicDelivery = (delivery) => ({
1542
+ deliveryId: delivery.id,
1543
+ thread: delivery.thread,
1544
+ transport: delivery.transport,
1545
+ status: delivery.status,
1546
+ turnId: delivery.turnId,
1547
+ queuedSubmissionId: delivery.queuedSubmissionId,
1548
+ clientUserMessageId: delivery.clientUserMessageId,
1549
+ dispatch: delivery.dispatch,
1550
+ dispatchError: delivery.dispatchError,
1551
+ createdAt: delivery.createdAt,
1552
+ updatedAt: delivery.updatedAt,
1553
+ });
1554
+
1555
+ const deliveryIsTerminal = (delivery) =>
1556
+ delivery.status === "completed" || delivery.status === "interrupted";
1557
+
1558
+ const pruneDeliveries = () => {
1559
+ const now = Date.now();
1560
+
1561
+ for (const [id, delivery] of deliveries) {
1562
+ if (!deliveryIsTerminal(delivery)) continue;
1563
+
1564
+ if (now - delivery.updatedAt > TERMINAL_DELIVERY_TTL_MS) {
1565
+ deliveries.delete(id);
1566
+ clearDesktopLifecycleTimer(id);
1567
+ desktopLifecycleChecks.delete(id);
1568
+ }
1569
+ }
1570
+
1571
+ for (const [key, event] of terminalTurnEvents) {
1572
+ if (now - event.at > TERMINAL_DELIVERY_TTL_MS) terminalTurnEvents.delete(key);
1573
+ }
1574
+
1575
+ if (deliveries.size <= MAX_DELIVERIES) return;
1576
+
1577
+ const terminal = [...deliveries.values()]
1578
+ .filter(deliveryIsTerminal)
1579
+ .sort((left, right) => left.updatedAt - right.updatedAt);
1580
+
1581
+ for (const delivery of terminal) {
1582
+ if (deliveries.size <= MAX_DELIVERIES) break;
1583
+ deliveries.delete(delivery.id);
1584
+ clearDesktopLifecycleTimer(delivery.id);
1585
+ desktopLifecycleChecks.delete(delivery.id);
1586
+ }
1587
+ };
1588
+
1589
+ const createDelivery = (thread, message, transport = "codex-queue") => {
1590
+ pruneDeliveries();
1591
+ const now = Date.now();
1592
+
1593
+ const delivery = {
1594
+ id: randomUUID(),
1595
+ thread,
1596
+ message,
1597
+ messageHash: hashMessage(message),
1598
+ transport,
1599
+ status: "queued",
1600
+ turnId: null,
1601
+ queuedSubmissionId: null,
1602
+ clientUserMessageId: null,
1603
+ dispatch: "persisting",
1604
+ dispatchError: null,
1605
+ createdAt: now,
1606
+ updatedAt: now,
1607
+ };
1608
+
1609
+ deliveries.set(delivery.id, delivery);
1610
+
1611
+ if (delivery.transport === "desktop-app") persistDeliveryStateSoon();
1612
+
1613
+ return delivery;
1614
+ };
1615
+
1616
+ const xmlEscape = (value) => value
1617
+ .replaceAll("&", "&amp;")
1618
+ .replaceAll("<", "&lt;")
1619
+ .replaceAll(">", "&gt;")
1620
+ .replaceAll('"', "&quot;")
1621
+ .replaceAll("'", "&apos;");
1622
+
1623
+ const promptMatchesDelivery = (delivery, prompt) => {
1624
+ if (delivery.messageHash === hashMessage(prompt)) return true;
1625
+
1626
+ if (delivery.transport !== "desktop-app" || !delivery.message) return false;
1627
+
1628
+ return prompt.includes(delivery.message)
1629
+ || prompt.includes(`<input>${xmlEscape(delivery.message)}</input>`);
1630
+ };
1631
+
1632
+ const turnUserMessageEntries = (turn) => {
1633
+ if (!Array.isArray(turn?.items)) return [];
1634
+
1635
+ return turn.items.flatMap((item) => {
1636
+ if (item?.type !== "userMessage" || !Array.isArray(item.content)) return [];
1637
+
1638
+ const message = item.content
1639
+ .filter((input) => input?.type === "text")
1640
+ .map((input) => String(input.text ?? ""))
1641
+ .join("\n")
1642
+ .trim();
1643
+
1644
+ if (!message) return [];
1645
+
1646
+ return [{
1647
+ message,
1648
+ clientUserMessageId: normalizeThread(item.clientId),
1649
+ }];
1650
+ });
1651
+ };
1652
+
1653
+ const turnUserMessages = (turn) =>
1654
+ turnUserMessageEntries(turn).map((entry) => entry.message);
1655
+
1656
+ const deliveryStateFromTurn = (turn) => {
1657
+ const turnId = normalizeThread(turn?.id);
1658
+ const ended = turn?.completedAt != null && Number.isFinite(Number(turn.completedAt));
1659
+ let status = null;
1660
+
1661
+ if (turn?.status === "inProgress") status = "working";
1662
+
1663
+ if (turn?.status === "interrupted" && !ended) status = "working";
1664
+
1665
+ if (turn?.status === "completed") status = "completed";
1666
+
1667
+ if ((turn?.status === "interrupted" && ended) || turn?.status === "failed") {
1668
+ status = "interrupted";
1669
+ }
1670
+
1671
+ if (!turnId || !status) return null;
1672
+
1673
+ return {
1674
+ turnId,
1675
+ status,
1676
+ dispatchError: turn?.status === "failed"
1677
+ ? normalizeThread(turn?.error?.message) ?? "Codex turn failed."
1678
+ : null,
1679
+ createdAt: Number.isFinite(Number(turn?.startedAt))
1680
+ ? Number(turn.startedAt) * 1_000
1681
+ : null,
1682
+ updatedAt: Number.isFinite(Number(turn?.completedAt))
1683
+ ? Number(turn.completedAt) * 1_000
1684
+ : null,
1685
+ };
1686
+ };
1687
+
1688
+ const historicalDeliveryMatch = (turns, identity) => {
1689
+ const expectedClientId = normalizeThread(identity?.clientUserMessageId);
1690
+ const expectedMessage = identity?.message?.trim?.() ?? "";
1691
+
1692
+ if (!expectedClientId && !expectedMessage) return null;
1693
+ const expectedHash = expectedMessage ? hashMessage(expectedMessage) : null;
1694
+ const matches = [];
1695
+
1696
+ for (const turn of turns) {
1697
+ for (const entry of turnUserMessageEntries(turn)) {
1698
+ if (expectedClientId && entry.clientUserMessageId !== expectedClientId) continue;
1699
+
1700
+ if (expectedHash && hashMessage(entry.message) !== expectedHash) continue;
1701
+
1702
+ matches.push({ turn, entry });
1703
+ }
1704
+ }
1705
+
1706
+ if (matches.length !== 1) return null;
1707
+
1708
+ const state = deliveryStateFromTurn(matches[0].turn);
1709
+
1710
+ return state ? { ...matches[0], state } : null;
1711
+ };
1712
+
1713
+ const deliveryTurn = (delivery, turns) => {
1714
+ if (delivery.turnId) {
1715
+ return turns.find((turn) => normalizeThread(turn?.id) === delivery.turnId) ?? null;
1716
+ }
1717
+
1718
+ const earliestStartedAt = delivery.createdAt - DELIVERY_TURN_START_SKEW_MS;
1719
+
1720
+ const matches = turns.filter((turn) => {
1721
+ const turnId = normalizeThread(turn?.id);
1722
+ const startedAt = Number(turn?.startedAt);
1723
+
1724
+ if (!turnId || !Number.isFinite(startedAt) || startedAt * 1_000 < earliestStartedAt) {
1725
+ return false;
1726
+ }
1727
+
1728
+ return turnUserMessages(turn).some((prompt) => promptMatchesDelivery(delivery, prompt));
1729
+ });
1730
+
1731
+ return matches.length === 1 ? matches[0] : null;
1732
+ };
1733
+
1734
+ const reconcileDesktopDelivery = async (delivery) => {
1735
+ if (delivery.transport !== "desktop-app" || delivery.dispatch !== "desktop-opened") {
1736
+ return;
1737
+ }
1738
+
1739
+ const turns = await runCodexTurnHistory(delivery.thread);
1740
+
1741
+ if (deliveries.get(delivery.id) !== delivery) return;
1742
+ const turn = deliveryTurn(delivery, turns);
1743
+
1744
+ if (!turn) return;
1745
+
1746
+ const state = deliveryStateFromTurn(turn);
1747
+
1748
+ if (!state) return;
1749
+
1750
+ const changed = delivery.turnId !== state.turnId
1751
+ || delivery.status !== state.status
1752
+ || (state.dispatchError && delivery.dispatchError !== state.dispatchError);
1753
+
1754
+ if (!changed) return;
1755
+
1756
+ delivery.turnId = state.turnId;
1757
+ delivery.status = state.status;
1758
+
1759
+ if (state.dispatchError) delivery.dispatchError = state.dispatchError;
1760
+ delivery.updatedAt = Date.now();
1761
+ await persistDeliveryState();
1762
+ scheduleIdleShutdown();
1763
+ };
1764
+
1765
+ const captureQueuedIdentity = async (delivery) => {
1766
+ if (!delivery.queuedSubmissionId) return;
1767
+
1768
+ try {
1769
+ const lookup = await runCodexQueueLookup(delivery.thread, delivery.queuedSubmissionId);
1770
+
1771
+ if (lookup.submission) {
1772
+ delivery.clientUserMessageId = normalizeThread(lookup.submission.clientUserMessageId);
1773
+
1774
+ return;
1775
+ }
1776
+ } catch {
1777
+ // Queue delivery already succeeded. Identity enrichment is best-effort here.
1778
+ }
1779
+
1780
+ try {
1781
+ const turns = await runCodexTurnHistory(delivery.thread);
1782
+ const turn = deliveryTurn(delivery, turns);
1783
+
1784
+ if (!turn) return;
1785
+
1786
+ const entry = turnUserMessageEntries(turn).find((candidate) =>
1787
+ hashMessage(candidate.message) === delivery.messageHash);
1788
+
1789
+ if (entry?.clientUserMessageId) {
1790
+ delivery.clientUserMessageId = entry.clientUserMessageId;
1791
+ }
1792
+
1793
+ const state = deliveryStateFromTurn(turn);
1794
+
1795
+ if (state) {
1796
+ delivery.turnId = state.turnId;
1797
+ delivery.status = state.status;
1798
+
1799
+ if (state.dispatchError) delivery.dispatchError = state.dispatchError;
1800
+ }
1801
+ } catch {
1802
+ // A later delivery poll can still reconcile through history.
1803
+ }
1804
+ };
1805
+
1806
+ const scheduleDesktopLifecycleCheck = (delivery, delay = DESKTOP_LIFECYCLE_POLL_MS) => {
1807
+ if (shutdownStarted
1808
+ || deliveries.get(delivery.id) !== delivery
1809
+ || !deliveryNeedsDesktopLifecycleCheck(delivery)) {
1810
+ clearDesktopLifecycleTimer(delivery.id);
1811
+
1812
+ return;
1813
+ }
1814
+
1815
+ if (desktopLifecycleChecks.has(delivery.id)
1816
+ || desktopLifecycleTimers.has(delivery.id)) {
1817
+ return;
1818
+ }
1819
+
1820
+ const timer = setTimeout(() => {
1821
+ desktopLifecycleTimers.delete(delivery.id);
1822
+
1823
+ if (shutdownStarted
1824
+ || deliveries.get(delivery.id) !== delivery
1825
+ || !deliveryNeedsDesktopLifecycleCheck(delivery)) {
1826
+ return;
1827
+ }
1828
+
1829
+ const check = reconcileDesktopDelivery(delivery)
1830
+ .catch(() => undefined)
1831
+ .finally(() => {
1832
+ if (desktopLifecycleChecks.get(delivery.id) === check) {
1833
+ desktopLifecycleChecks.delete(delivery.id);
1834
+ }
1835
+
1836
+ if (!shutdownStarted
1837
+ && deliveries.get(delivery.id) === delivery
1838
+ && deliveryNeedsDesktopLifecycleCheck(delivery)) {
1839
+ scheduleDesktopLifecycleCheck(delivery);
1840
+ } else {
1841
+ clearDesktopLifecycleTimer(delivery.id);
1842
+ }
1843
+ });
1844
+
1845
+ desktopLifecycleChecks.set(delivery.id, check);
1846
+ }, Math.max(0, delay));
1847
+
1848
+ timer.unref?.();
1849
+ desktopLifecycleTimers.set(delivery.id, timer);
1850
+ };
1851
+
1852
+ const markPromptStarted = (thread, turnId, prompt) => {
1853
+ const delivery = [...deliveries.values()]
1854
+ .filter((candidate) =>
1855
+ candidate.thread === thread
1856
+ && promptMatchesDelivery(candidate, prompt)
1857
+ && candidate.status === "queued")
1858
+ .sort((a, b) => a.createdAt - b.createdAt)[0];
1859
+
1860
+ if (!delivery) return null;
1861
+ delivery.status = "working";
1862
+ delivery.turnId = turnId;
1863
+ delivery.updatedAt = Date.now();
1864
+
1865
+ if (delivery.transport === "desktop-app") persistDeliveryStateSoon();
1866
+ const terminal = terminalTurnEvents.get(turnKey(thread, turnId));
1867
+
1868
+ if (terminal) {
1869
+ delivery.status = terminal.status;
1870
+ delivery.updatedAt = Math.max(delivery.updatedAt, terminal.at);
1871
+ terminalTurnEvents.delete(turnKey(thread, turnId));
1872
+
1873
+ if (delivery.transport === "desktop-app") persistDeliveryStateSoon();
1874
+ }
1875
+
1876
+ return delivery;
1877
+ };
1878
+
1879
+ const markTurnTerminal = (thread, turnId, status) => {
1880
+ const delivery = [...deliveries.values()].find((candidate) =>
1881
+ candidate.thread === thread
1882
+ && candidate.turnId === turnId
1883
+ && candidate.status === "working");
1884
+
1885
+ if (delivery) {
1886
+ delivery.status = status;
1887
+ delivery.updatedAt = Date.now();
1888
+
1889
+ if (delivery.transport === "desktop-app") {
1890
+ persistDeliveryStateSoon();
1891
+ clearDesktopLifecycleTimer(delivery.id);
1892
+ }
1893
+
1894
+ scheduleIdleShutdown();
1895
+
1896
+ return delivery;
1897
+ }
1898
+
1899
+ terminalTurnEvents.set(turnKey(thread, turnId), { status, at: Date.now() });
1900
+
1901
+ return null;
1902
+ };
1903
+
1904
+ const desktopOwnsLifecycle = (thread) =>
1905
+ Boolean(registeredThreads.get(thread)?.appToolsPipe)
1906
+ || [...deliveries.values()].some((delivery) =>
1907
+ delivery.thread === thread && delivery.transport === "desktop-app");
1908
+
1909
+ await loadDeliveryState();
1910
+
1911
+ for (const delivery of deliveries.values()) scheduleDesktopLifecycleCheck(delivery, 0);
1912
+
1913
+ let successfulSends = 0;
1914
+
1915
+ server = createServer(async (request, response) => {
1916
+ const originHeaderPresent = Object.hasOwn(request.headers, "origin");
1917
+ const origin = [request.headers.origin].flat().find((value) => value !== undefined);
1918
+
1919
+ if (!originAllowed(origin)) {
1920
+ if (request.method === "GET" && request.url === "/health") {
1921
+ writeJson(response, 200, {
1922
+ ok: true,
1923
+ bridge: {
1924
+ name: BRIDGE_IDENTITY.name,
1925
+ protocol: BRIDGE_IDENTITY.protocol,
1926
+ capabilities: BRIDGE_IDENTITY.capabilities,
1927
+ },
1928
+ access: { allowed: false },
1929
+ }, origin, true);
1930
+
1931
+ return;
1932
+ }
1933
+
1934
+ writeJson(
1935
+ response,
1936
+ 403,
1937
+ { ok: false, error: `Origin is not allowed: ${origin}` },
1938
+ origin,
1939
+ true,
1940
+ );
1941
+
1942
+ return;
1943
+ }
1944
+
1945
+ if (request.method === "OPTIONS") {
1946
+ response.writeHead(204, corsHeaders(origin));
1947
+ response.end();
1948
+
1949
+ return;
1950
+ }
1951
+
1952
+ if (request.method === "POST" && request.url === "/shutdown") {
1953
+ if (originHeaderPresent) {
1954
+ writeJson(response, 403, { ok: false, error: "Bridge shutdown is local-process-only." }, origin);
1955
+
1956
+ return;
1957
+ }
1958
+
1959
+ const state = bridgeState();
1960
+
1961
+ if (!state.idle) {
1962
+ writeJson(response, 409, {
1963
+ ok: false,
1964
+ error: "Mesurer Codex Bridge is still in use and cannot shut down yet.",
1965
+ bridgeState: state,
1966
+ }, origin);
1967
+
1968
+ return;
1969
+ }
1970
+
1971
+ response.setHeader("Connection", "close");
1972
+ response.once("finish", () => {
1973
+ void shutdownBridge();
1974
+ });
1975
+ writeJson(response, 200, { ok: true, bridgeState: state }, origin);
1976
+
1977
+ return;
1978
+ }
1979
+
1980
+ if (originHeaderPresent
1981
+ && !(request.method === "GET" && request.url === "/health")
1982
+ && request.headers["x-mesurer-bridge-instance"] !== BRIDGE_INSTANCE_ID) {
1983
+ writeJson(response, 409, {
1984
+ ok: false,
1985
+ error: "Mesurer Codex Bridge instance changed. Reconnect before sending another request.",
1986
+ }, origin);
1987
+
1988
+ return;
1989
+ }
1990
+
1991
+ if (request.method === "GET" && request.url === "/health") {
1992
+ writeJson(response, 200, {
1993
+ ok: true,
1994
+ bridge: BRIDGE_IDENTITY,
1995
+ bridgeState: bridgeState(),
1996
+ access: { allowed: true },
1997
+ ...threadPayload(),
1998
+ }, origin);
1999
+
2000
+ return;
2001
+ }
2002
+
2003
+ if (request.method === "GET" && request.url?.startsWith("/deliveries/")) {
2004
+ pruneDeliveries();
2005
+ const url = new URL(request.url, DEFAULT_BRIDGE);
2006
+ const prefix = "/deliveries/";
2007
+ const deliveryId = decodeURIComponent(url.pathname.slice(prefix.length));
2008
+ const delivery = deliveries.get(deliveryId);
2009
+
2010
+ if (!delivery) {
2011
+ writeJson(response, 404, { ok: false, error: `Codex delivery is not available: ${deliveryId}` }, origin);
2012
+
2013
+ return;
2014
+ }
2015
+
2016
+ scheduleDesktopLifecycleCheck(delivery);
2017
+ writeJson(response, 200, { ok: true, ...publicDelivery(delivery) }, origin);
2018
+
2019
+ return;
2020
+ }
2021
+
2022
+ if (request.method === "POST" && request.url === "/deliveries/restore") {
2023
+ try {
2024
+ const body = await readJsonBody(request);
2025
+ const deliveryId = normalizeThread(body?.deliveryId);
2026
+ const thread = normalizeThread(body?.thread);
2027
+ const queuedSubmissionId = normalizeThread(body?.queuedSubmissionId);
2028
+ const clientUserMessageId = normalizeThread(body?.clientUserMessageId);
2029
+ const recoveryMessage = body?.message?.trim?.() ?? "";
2030
+
2031
+ if (!deliveryId || !thread) {
2032
+ writeJson(response, 400, { ok: false, error: "deliveryId and thread are required." }, origin);
2033
+
2034
+ return;
2035
+ }
2036
+
2037
+ if (!registeredThreads.has(thread) && !discoveredThreads.has(thread)) {
2038
+ writeJson(response, 409, {
2039
+ ok: false,
2040
+ error: `Codex thread is not available to this bridge: ${thread}`,
2041
+ }, origin);
2042
+
2043
+ return;
2044
+ }
2045
+
2046
+ const existing = deliveries.get(deliveryId);
2047
+
2048
+ if (existing) {
2049
+ if (clientUserMessageId && existing.clientUserMessageId
2050
+ && clientUserMessageId !== existing.clientUserMessageId) {
2051
+ writeJson(response, 409, {
2052
+ ok: false,
2053
+ error: "Codex delivery client-message identity does not match the persisted delivery.",
2054
+ }, origin);
2055
+
2056
+ return;
2057
+ }
2058
+
2059
+ if (clientUserMessageId && !existing.clientUserMessageId) {
2060
+ existing.clientUserMessageId = clientUserMessageId;
2061
+ await persistDeliveryState();
2062
+ }
2063
+
2064
+ if (existing.transport === "desktop-app") {
2065
+ scheduleDesktopDispatch(existing.thread);
2066
+ scheduleDesktopLifecycleCheck(existing, 0);
2067
+ }
2068
+
2069
+ writeJson(response, 200, { ok: true, restored: false, ...publicDelivery(existing) }, origin);
2070
+
2071
+ return;
2072
+ }
2073
+
2074
+ const lookup = await runCodexQueueLookup(thread, queuedSubmissionId);
2075
+
2076
+ if (lookup.submission) {
2077
+ const message = queuedSubmissionMessage(lookup.submission);
2078
+ const queuedClientUserMessageId = normalizeThread(lookup.submission.clientUserMessageId);
2079
+
2080
+ if (!message) {
2081
+ writeJson(response, 409, {
2082
+ ok: false,
2083
+ error: "The queued Codex submission is not a single text message and cannot be restored safely.",
2084
+ }, origin);
2085
+
2086
+ return;
2087
+ }
2088
+
2089
+ if (clientUserMessageId && queuedClientUserMessageId
2090
+ && clientUserMessageId !== queuedClientUserMessageId) {
2091
+ writeJson(response, 409, {
2092
+ ok: false,
2093
+ error: "Codex queued submission client-message identity does not match Mesurer state.",
2094
+ }, origin);
2095
+
2096
+ return;
2097
+ }
2098
+
2099
+ if (recoveryMessage && hashMessage(recoveryMessage) !== hashMessage(message)) {
2100
+ writeJson(response, 409, {
2101
+ ok: false,
2102
+ error: "Codex queued submission text does not match the saved Mesurer feedback.",
2103
+ }, origin);
2104
+
2105
+ return;
2106
+ }
2107
+
2108
+ pruneDeliveries();
2109
+ const now = Date.now();
2110
+ const record = registeredThreads.get(thread);
2111
+ const transport = record?.appToolsPipe ? "desktop-app" : "codex-queue";
2112
+
2113
+ const delivery = {
2114
+ id: deliveryId,
2115
+ thread,
2116
+ message,
2117
+ messageHash: hashMessage(message),
2118
+ transport,
2119
+ status: "queued",
2120
+ turnId: null,
2121
+ queuedSubmissionId: normalizeThread(lookup.submission.id) ?? queuedSubmissionId,
2122
+ clientUserMessageId: queuedClientUserMessageId ?? clientUserMessageId,
2123
+ dispatch: "persisted",
2124
+ dispatchError: null,
2125
+ createdAt: now,
2126
+ updatedAt: now,
2127
+ };
2128
+
2129
+ deliveries.set(delivery.id, delivery);
2130
+ await persistDeliveryState();
2131
+
2132
+ if (transport === "desktop-app") {
2133
+ scheduleDesktopDispatch(thread);
2134
+ } else {
2135
+ try {
2136
+ const wake = await resumeColdCodexThread(thread);
2137
+ delivery.dispatch = wake.action;
2138
+ delivery.updatedAt = Date.now();
2139
+ } catch (cause) {
2140
+ delivery.dispatch = "wake-failed";
2141
+ delivery.dispatchError = cause instanceof Error ? cause.message : String(cause);
2142
+ delivery.updatedAt = Date.now();
2143
+ }
2144
+
2145
+ persistDeliveryStateSoon();
2146
+ }
2147
+
2148
+ writeJson(response, 200, {
2149
+ ok: true,
2150
+ restored: true,
2151
+ ...publicDelivery(delivery),
2152
+ }, origin);
2153
+
2154
+ return;
2155
+ }
2156
+
2157
+ if (lookup.ambiguous) {
2158
+ writeJson(response, 409, {
2159
+ ok: false,
2160
+ error: "Multiple queued Codex submissions exist for this thread; an exact queuedSubmissionId is required to restore the delivery safely.",
2161
+ }, origin);
2162
+
2163
+ return;
2164
+ }
2165
+
2166
+ const turns = await runCodexTurnHistory(thread);
2167
+
2168
+ const historical = historicalDeliveryMatch(turns, {
2169
+ clientUserMessageId,
2170
+ message: recoveryMessage,
2171
+ });
2172
+
2173
+ if (!historical) {
2174
+ const error = queuedSubmissionId
2175
+ ? `Codex queued submission is not available and no unique matching turn exists in history: ${queuedSubmissionId}`
2176
+ : "No queued Codex submission or unique matching turn is available to restore for this thread.";
2177
+
2178
+ writeJson(response, 409, { ok: false, error }, origin);
2179
+
2180
+ return;
2181
+ }
2182
+
2183
+ const record = registeredThreads.get(thread);
2184
+ const transport = record?.appToolsPipe ? "desktop-app" : "codex-queue";
2185
+ const state = historical.state;
2186
+ const now = Date.now();
2187
+
2188
+ const delivery = {
2189
+ id: deliveryId,
2190
+ thread,
2191
+ message: historical.entry.message,
2192
+ messageHash: hashMessage(historical.entry.message),
2193
+ transport,
2194
+ status: state.status,
2195
+ turnId: state.turnId,
2196
+ queuedSubmissionId,
2197
+ clientUserMessageId: historical.entry.clientUserMessageId ?? clientUserMessageId,
2198
+ dispatch: transport === "desktop-app" ? "desktop-opened" : "persisted",
2199
+ dispatchError: state.dispatchError,
2200
+ createdAt: state.createdAt ?? now,
2201
+ updatedAt: state.updatedAt ?? now,
2202
+ };
2203
+
2204
+ pruneDeliveries();
2205
+ deliveries.set(delivery.id, delivery);
2206
+ await persistDeliveryState();
2207
+
2208
+ writeJson(response, 200, {
2209
+ ok: true,
2210
+ restored: true,
2211
+ ...publicDelivery(delivery),
2212
+ }, origin);
2213
+ } catch (cause) {
2214
+ const error = cause instanceof Error ? cause.message : String(cause);
2215
+ writeJson(response, 502, { ok: false, error }, origin);
2216
+ }
2217
+
2218
+ return;
2219
+ }
2220
+
2221
+ if (request.method === "GET" && request.url?.startsWith("/threads")) {
2222
+ const url = new URL(request.url, DEFAULT_BRIDGE);
2223
+
2224
+ if (url.pathname !== "/threads") {
2225
+ writeJson(response, 404, { ok: false, error: "Not found." }, origin);
2226
+
2227
+ return;
2228
+ }
2229
+
2230
+ const requestedLimit = Number(url.searchParams.get("limit") ?? "5");
2231
+
2232
+ const limit = Number.isInteger(requestedLimit)
2233
+ ? Math.min(MAX_DISCOVERED_THREADS, Math.max(1, requestedLimit))
2234
+ : 5;
2235
+
2236
+ const scopeThread = normalizeThread(url.searchParams.get("thread")) ?? activeThread;
2237
+
2238
+ if (scopeThread && !registeredThreads.has(scopeThread) && !discoveredThreads.has(scopeThread)) {
2239
+ writeJson(response, 409, { ok: false, error: `Codex thread is not known to this bridge: ${scopeThread}` }, origin);
2240
+
2241
+ return;
2242
+ }
2243
+
2244
+ const payload = await listThreadSummaries(scopeThread, limit);
2245
+ writeJson(response, 200, { ok: true, ...payload }, origin);
2246
+
2247
+ return;
2248
+ }
2249
+
2250
+ if (request.method === "POST" && request.url === "/threads/unregister") {
2251
+ if (originHeaderPresent) {
2252
+ writeJson(response, 403, {
2253
+ ok: false,
2254
+ error: "Thread unregistration is available only to a local process, not a browser Origin.",
2255
+ }, origin);
2256
+
2257
+ return;
2258
+ }
2259
+
2260
+ try {
2261
+ const body = await readJsonBody(request);
2262
+ const thread = normalizeThread(body?.thread);
2263
+
2264
+ if (!thread) {
2265
+ writeJson(response, 400, { ok: false, error: "thread must be a non-empty string." }, origin);
2266
+
2267
+ return;
2268
+ }
2269
+
2270
+ const removed = unregisterThread(thread);
2271
+ writeJson(response, 200, {
2272
+ ok: true,
2273
+ removed,
2274
+ ...threadPayload(),
2275
+ }, origin);
2276
+ } catch (cause) {
2277
+ const error = cause instanceof Error ? cause.message : String(cause);
2278
+ writeJson(response, 400, { ok: false, error }, origin);
2279
+ }
2280
+
2281
+ return;
2282
+ }
2283
+
2284
+ if (request.method === "POST" && request.url === "/threads/register") {
2285
+ if (originHeaderPresent) {
2286
+ writeJson(response, 403, {
2287
+ ok: false,
2288
+ error: "Thread registration is available only to a local process, not a browser Origin.",
2289
+ }, origin);
2290
+
2291
+ return;
2292
+ }
2293
+
2294
+ try {
2295
+ const body = await readJsonBody(request);
2296
+ const thread = normalizeThread(body?.thread);
2297
+ const cwd = normalizeCwd(body?.cwd);
2298
+ const appToolsPipe = normalizeThread(body?.appToolsPipe);
2299
+ const codexHome = normalizeCwd(body?.codexHome);
2300
+
2301
+ if (!thread) {
2302
+ writeJson(response, 400, { ok: false, error: "thread must be a non-empty string." }, origin);
2303
+
2304
+ return;
2305
+ }
2306
+
2307
+ registerThread(thread, cwd, { appToolsPipe, codexHome });
2308
+
2309
+ if (appToolsPipe) scheduleDesktopDispatch(thread);
2310
+ writeJson(response, 200, { ok: true, ...threadPayload() }, origin);
2311
+ } catch (cause) {
2312
+ const error = cause instanceof Error ? cause.message : String(cause);
2313
+ writeJson(response, 400, { ok: false, error }, origin);
2314
+ }
2315
+
2316
+ return;
2317
+ }
2318
+
2319
+ if (request.method === "POST" && request.url === "/lifecycle") {
2320
+ if (originHeaderPresent) {
2321
+ writeJson(response, 403, {
2322
+ ok: false,
2323
+ error: "Codex lifecycle updates are available only to a local process, not a browser Origin.",
2324
+ }, origin);
2325
+
2326
+ return;
2327
+ }
2328
+
2329
+ try {
2330
+ const body = await readJsonBody(request);
2331
+ const event = body?.event;
2332
+ const thread = normalizeThread(body?.sessionId);
2333
+ const turnId = normalizeThread(body?.turnId);
2334
+
2335
+ if (!thread || !turnId) {
2336
+ writeJson(response, 400, { ok: false, error: "sessionId and turnId are required." }, origin);
2337
+
2338
+ return;
2339
+ }
2340
+
2341
+ if (desktopOwnsLifecycle(thread)) {
2342
+ writeJson(response, 200, {
2343
+ ok: true,
2344
+ matched: false,
2345
+ ignored: "desktop-history-authoritative",
2346
+ }, origin);
2347
+
2348
+ return;
2349
+ }
2350
+
2351
+ let delivery = null;
2352
+
2353
+ if (event === "UserPromptSubmit") {
2354
+ const prompt = body?.prompt?.trim?.() ?? "";
2355
+
2356
+ if (!prompt) {
2357
+ writeJson(response, 400, { ok: false, error: "UserPromptSubmit requires prompt." }, origin);
2358
+
2359
+ return;
2360
+ }
2361
+
2362
+ delivery = markPromptStarted(thread, turnId, prompt);
2363
+ } else if (event === "Stop") {
2364
+ delivery = markTurnTerminal(thread, turnId, "completed");
2365
+ scheduleDesktopDispatch(thread);
2366
+ } else if (event === "Interrupt") {
2367
+ delivery = markTurnTerminal(thread, turnId, "interrupted");
2368
+ scheduleDesktopDispatch(thread);
2369
+ } else {
2370
+ writeJson(response, 400, { ok: false, error: `Unsupported Codex lifecycle event: ${event ?? "<missing>"}` }, origin);
2371
+
2372
+ return;
2373
+ }
2374
+
2375
+ pruneDeliveries();
2376
+
2377
+ if (delivery) {
2378
+ writeJson(response, 200, {
2379
+ ok: true,
2380
+ matched: true,
2381
+ ...publicDelivery(delivery),
2382
+ }, origin);
2383
+ } else {
2384
+ writeJson(response, 200, { ok: true, matched: false }, origin);
2385
+ }
2386
+ } catch (cause) {
2387
+ const error = cause instanceof Error ? cause.message : String(cause);
2388
+ writeJson(response, 400, { ok: false, error }, origin);
2389
+ }
2390
+
2391
+ return;
2392
+ }
2393
+
2394
+ if (request.method === "POST" && request.url === "/target") {
2395
+ try {
2396
+ const body = await readJsonBody(request);
2397
+ const thread = normalizeThread(body?.thread);
2398
+
2399
+ if (!thread) {
2400
+ writeJson(response, 400, { ok: false, error: "thread must be a non-empty string." }, origin);
2401
+
2402
+ return;
2403
+ }
2404
+
2405
+ const record = registeredThreads.get(thread) ?? discoveredThreads.get(thread);
2406
+
2407
+ if (!record) {
2408
+ writeJson(response, 409, {
2409
+ ok: false,
2410
+ error: `Codex thread is not available to this bridge: ${thread}`,
2411
+ }, origin);
2412
+
2413
+ return;
2414
+ }
2415
+
2416
+ activeThread = thread;
2417
+
2418
+ if (record.cwd) activeCwd = record.cwd;
2419
+ writeJson(response, 200, { ok: true, ...threadPayload() }, origin);
2420
+ } catch (cause) {
2421
+ const error = cause instanceof Error ? cause.message : String(cause);
2422
+ writeJson(response, 400, { ok: false, error }, origin);
2423
+ }
2424
+
2425
+ return;
2426
+ }
2427
+
2428
+ if (request.method !== "POST" || request.url !== "/send") {
2429
+ writeJson(response, 404, { ok: false, error: "Not found." }, origin);
2430
+
2431
+ return;
2432
+ }
2433
+
2434
+ try {
2435
+ const body = await readJsonBody(request);
2436
+ const message = body?.message?.trim?.() ?? "";
2437
+ const requestedThread = normalizeThread(body?.thread);
2438
+ const thread = requestedThread ?? activeThread;
2439
+
2440
+ if (!message) {
2441
+ writeJson(response, 400, { ok: false, error: "message must be a non-empty string." }, origin);
2442
+
2443
+ return;
2444
+ }
2445
+
2446
+ if (!thread) {
2447
+ writeJson(response, 409, {
2448
+ ok: false,
2449
+ error: "No Codex thread is registered. Start the bridge from Codex, pass --thread, or run mesurer-codex --register-current.",
2450
+ }, origin);
2451
+
2452
+ return;
2453
+ }
2454
+
2455
+ if (!registeredThreads.has(thread) && !discoveredThreads.has(thread)) {
2456
+ writeJson(response, 409, {
2457
+ ok: false,
2458
+ error: `Codex thread is not available to this bridge: ${thread}`,
2459
+ }, origin);
2460
+
2461
+ return;
2462
+ }
2463
+
2464
+ const record = registeredThreads.get(thread);
2465
+
2466
+ if (record?.appToolsPipe) {
2467
+ const delivery = createDelivery(thread, message, "desktop-app");
2468
+ let output;
2469
+
2470
+ try {
2471
+ output = await runCodexQueue(thread, message);
2472
+ } catch (cause) {
2473
+ deliveries.delete(delivery.id);
2474
+ persistDeliveryStateSoon();
2475
+ throw cause;
2476
+ }
2477
+
2478
+ delivery.queuedSubmissionId = queuedSubmissionFromOutput(output, thread);
2479
+
2480
+ if (!delivery.queuedSubmissionId) {
2481
+ deliveries.delete(delivery.id);
2482
+ persistDeliveryStateSoon();
2483
+ throw new Error("Codex queue succeeded but did not return a queued submission id.");
2484
+ }
2485
+
2486
+ delivery.dispatch = "persisted";
2487
+ delivery.updatedAt = Date.now();
2488
+ await captureQueuedIdentity(delivery);
2489
+ await persistDeliveryState();
2490
+ scheduleDesktopDispatch(thread);
2491
+ successfulSends += 1;
2492
+ writeJson(response, 200, {
2493
+ ok: true,
2494
+ thread,
2495
+ output,
2496
+ delivery: "queued",
2497
+ ...publicDelivery(delivery),
2498
+ }, origin);
2499
+
2500
+ if (values.once && successfulSends >= 1) setImmediate(() => void shutdownBridge());
2501
+
2502
+ return;
2503
+ }
2504
+
2505
+ const delivery = createDelivery(thread, message, "codex-queue");
2506
+ let output;
2507
+
2508
+ try {
2509
+ output = await runCodexQueue(thread, message);
2510
+ } catch (cause) {
2511
+ deliveries.delete(delivery.id);
2512
+ persistDeliveryStateSoon();
2513
+ throw cause;
2514
+ }
2515
+
2516
+ delivery.queuedSubmissionId = queuedSubmissionFromOutput(output, thread);
2517
+ delivery.dispatch = "persisted";
2518
+ delivery.updatedAt = Date.now();
2519
+ await captureQueuedIdentity(delivery);
2520
+
2521
+ if (delivery.queuedSubmissionId && delivery.status === "queued") {
2522
+ try {
2523
+ const wake = await resumeColdCodexThread(thread);
2524
+ delivery.dispatch = wake.action;
2525
+ delivery.updatedAt = Date.now();
2526
+ } catch (cause) {
2527
+ delivery.dispatch = "wake-failed";
2528
+ delivery.dispatchError = cause instanceof Error ? cause.message : String(cause);
2529
+ delivery.updatedAt = Date.now();
2530
+ }
2531
+ } else {
2532
+ delivery.dispatch = "untracked";
2533
+ delivery.dispatchError = "Codex queue succeeded but did not return a queued submission id.";
2534
+ delivery.updatedAt = Date.now();
2535
+ }
2536
+
2537
+ persistDeliveryStateSoon();
2538
+
2539
+ successfulSends += 1;
2540
+ writeJson(response, 200, {
2541
+ ok: true,
2542
+ thread,
2543
+ output,
2544
+ delivery: "queued",
2545
+ ...publicDelivery(delivery),
2546
+ }, origin);
2547
+
2548
+ if (values.once && successfulSends >= 1) setImmediate(() => void shutdownBridge());
2549
+ } catch (cause) {
2550
+ const error = cause instanceof Error ? cause.message : String(cause);
2551
+ writeJson(response, 502, { ok: false, error }, origin);
2552
+ }
2553
+ });
2554
+
2555
+ server.on("error", (error) => {
2556
+ process.stderr.write(`Mesurer Codex bridge failed: ${error.message}\n`);
2557
+ process.exitCode = 1;
2558
+ });
2559
+
2560
+ server.listen(parsedPort, "127.0.0.1", () => {
2561
+ for (const [thread, record] of registeredThreads) {
2562
+ if (record.appToolsPipe) scheduleDesktopDispatch(thread);
2563
+ }
2564
+
2565
+ const address = server.address();
2566
+ const port = address?.port ?? parsedPort;
2567
+ const url = `http://127.0.0.1:${port}`;
2568
+ console.log(`Mesurer Codex bridge listening on ${url}`);
2569
+
2570
+ if (activeThread) console.log(`Active Codex thread: ${activeThread}`);
2571
+ else console.log("No Codex thread is registered yet.");
2572
+ console.log(`BRIDGE_URL=${url}`);
2573
+
2574
+ if (activeThread) console.log(`BRIDGE_THREAD=${activeThread}`);
2575
+ });
2576
+
2577
+ async function shutdownBridge() {
2578
+ if (shutdownStarted) return;
2579
+
2580
+ shutdownStarted = true;
2581
+ cancelIdleShutdown();
2582
+
2583
+ if (ownerHealthTimer) {
2584
+ clearInterval(ownerHealthTimer);
2585
+ ownerHealthTimer = null;
2586
+ }
2587
+
2588
+ for (const timer of desktopDispatchTimers.values()) clearTimeout(timer);
2589
+ desktopDispatchTimers.clear();
2590
+
2591
+ for (const timer of desktopLifecycleTimers.values()) clearTimeout(timer);
2592
+ desktopLifecycleTimers.clear();
2593
+ desktopLifecycleChecks.clear();
2594
+
2595
+ const children = [...ownedChildren];
2596
+
2597
+ for (const child of children) {
2598
+ if (child.exitCode === null && child.signalCode === null) {
2599
+ try {
2600
+ child.kill("SIGTERM");
2601
+ } catch {}
2602
+ }
2603
+ }
2604
+
2605
+ const closeServer = new Promise((resolve) => {
2606
+ if (!server?.listening) {
2607
+ resolve();
2608
+
2609
+ return;
2610
+ }
2611
+
2612
+ server.close(() => resolve());
2613
+ server.closeAllConnections?.();
2614
+ });
2615
+
2616
+ const childExit = Promise.allSettled(children.map((child) => new Promise((resolve) => {
2617
+ if (child.exitCode !== null || child.signalCode !== null) {
2618
+ resolve();
2619
+
2620
+ return;
2621
+ }
2622
+
2623
+ child.once("close", resolve);
2624
+ child.once("error", resolve);
2625
+ })));
2626
+
2627
+ await Promise.race([
2628
+ Promise.all([closeServer, childExit]),
2629
+ new Promise((resolve) => setTimeout(resolve, OWNED_CHILD_SHUTDOWN_GRACE_MS)),
2630
+ ]);
2631
+
2632
+ for (const child of ownedChildren) {
2633
+ if (child.exitCode === null && child.signalCode === null) {
2634
+ try {
2635
+ child.kill("SIGKILL");
2636
+ } catch {}
2637
+ }
2638
+ }
2639
+
2640
+ process.exit(0);
2641
+ }
2642
+
2643
+ process.on("SIGINT", () => {
2644
+ void shutdownBridge();
2645
+ });
2646
+
2647
+ process.on("SIGTERM", () => {
2648
+ void shutdownBridge();
2649
+ });