@makerbi/remodex 2.0.0 → 2.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.
@@ -0,0 +1,750 @@
1
+ // FILE: desktop-ipc-owner-transport.js
2
+ // Purpose: Framed IPC client and fallback router for the bridge's Desktop stream-owner role.
3
+ // Layer: CLI helper
4
+ // Exports: createDesktopOwnerIpcClient, createDesktopIpcRouterServer
5
+ // Depends on: crypto, fs, path, ./desktop-ipc-shared
6
+
7
+ const { randomUUID } = require("crypto");
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ const {
12
+ CLIENT_STATUS_CHANGED,
13
+ DESKTOP_IPC_METHOD_VERSIONS: METHOD_VERSION_BY_NAME,
14
+ FRAME_HEADER_BYTES,
15
+ MAX_FRAME_BYTES,
16
+ normalizeToken,
17
+ readString,
18
+ requestIdKey,
19
+ safeParseJSON,
20
+ writeFrame,
21
+ } = require("./desktop-ipc-shared");
22
+
23
+ const DEFAULT_DISCOVERY_TIMEOUT_MS = 1_000;
24
+
25
+ function createDesktopOwnerIpcClient({
26
+ socketPath,
27
+ netModule,
28
+ now,
29
+ requestTimeoutMs,
30
+ reconnectMs,
31
+ logPrefix,
32
+ startRouterWhenMissing = true,
33
+ onConnected,
34
+ onBroadcast,
35
+ canHandleRequest,
36
+ handleRequest,
37
+ }) {
38
+ let socket = null;
39
+ let isConnecting = false;
40
+ let isInitialized = false;
41
+ let clientId = "";
42
+ let readBuffer = Buffer.alloc(0);
43
+ let reconnectTimer = null;
44
+ let shouldReconnect = false;
45
+ const localRouter = startRouterWhenMissing
46
+ ? createDesktopIpcRouterServer({
47
+ socketPath,
48
+ netModule,
49
+ now,
50
+ requestTimeoutMs,
51
+ discoveryTimeoutMs: Math.min(requestTimeoutMs, DEFAULT_DISCOVERY_TIMEOUT_MS),
52
+ logPrefix,
53
+ })
54
+ : null;
55
+ const pendingResponses = new Map();
56
+
57
+ function ensureConnected() {
58
+ shouldReconnect = true;
59
+ if (socket || isConnecting) {
60
+ return;
61
+ }
62
+ clearReconnectTimer();
63
+ isConnecting = true;
64
+ const nextSocket = netModule.createConnection(socketPath);
65
+ socket = nextSocket;
66
+
67
+ nextSocket.on("connect", () => {
68
+ isConnecting = false;
69
+ sendRequest("initialize", { clientType: "remodex-bridge" }, { initializing: true })
70
+ .then((result) => {
71
+ clientId = readString(result?.clientId) || clientId;
72
+ isInitialized = true;
73
+ onConnected?.(clientId);
74
+ })
75
+ .catch((error) => {
76
+ console.warn(`${logPrefix} desktop IPC live owner initialize failed: ${error.message}`);
77
+ closeSocket();
78
+ });
79
+ });
80
+ nextSocket.on("data", handleData);
81
+ nextSocket.on("close", () => handleClose(nextSocket));
82
+ nextSocket.on("error", (error) => {
83
+ if (error?.code === "ENOENT" || error?.code === "ECONNREFUSED") {
84
+ startLocalRouterAfterMissingSocket(error.code);
85
+ return;
86
+ }
87
+ if (error?.code !== "ENOENT" && error?.code !== "ECONNREFUSED") {
88
+ console.warn(`${logPrefix} desktop IPC live owner connection failed: ${error.message}`);
89
+ }
90
+ });
91
+ }
92
+
93
+ function startLocalRouterAfterMissingSocket(reasonCode) {
94
+ if (!localRouter || localRouter.isStarted) {
95
+ return;
96
+ }
97
+ localRouter.start({ removeStaleSocket: reasonCode === "ECONNREFUSED" })
98
+ .then(() => {
99
+ if (!shouldReconnect) {
100
+ return;
101
+ }
102
+ closeSocket();
103
+ isConnecting = false;
104
+ clearReconnectTimer();
105
+ ensureConnected();
106
+ })
107
+ .catch((error) => {
108
+ if (error?.code !== "EADDRINUSE") {
109
+ console.warn(`${logPrefix} desktop IPC router fallback failed: ${error.message}`);
110
+ }
111
+ });
112
+ }
113
+
114
+ function sendBroadcast(method, params) {
115
+ ensureConnected();
116
+ if (!socket || socket.destroyed || !isInitialized) {
117
+ return false;
118
+ }
119
+ const envelope = {
120
+ type: "broadcast",
121
+ method,
122
+ sourceClientId: clientId,
123
+ params: params || {},
124
+ version: METHOD_VERSION_BY_NAME.get(method) || 1,
125
+ };
126
+ return writeEnvelope(envelope);
127
+ }
128
+
129
+ function sendRequest(method, params, { initializing = false } = {}) {
130
+ ensureConnected();
131
+ if (!socket || socket.destroyed) {
132
+ return Promise.reject(new Error("Desktop IPC is not connected."));
133
+ }
134
+ const requestId = `remodex-owner-${now().toString(36)}-${randomUUID()}`;
135
+ const envelope = {
136
+ type: "request",
137
+ requestId,
138
+ sourceClientId: initializing ? "initializing-client" : clientId || "remodex-bridge",
139
+ version: METHOD_VERSION_BY_NAME.get(method) || 1,
140
+ method,
141
+ params: params || {},
142
+ };
143
+ return new Promise((resolve, reject) => {
144
+ const timeout = setTimeout(() => {
145
+ pendingResponses.delete(requestId);
146
+ reject(new Error(`Desktop IPC request timed out: ${method}`));
147
+ }, requestTimeoutMs);
148
+ timeout.unref?.();
149
+ pendingResponses.set(requestId, {
150
+ method,
151
+ resolve,
152
+ reject,
153
+ timeout,
154
+ });
155
+ if (!writeEnvelope(envelope)) {
156
+ clearTimeout(timeout);
157
+ pendingResponses.delete(requestId);
158
+ reject(new Error("Desktop IPC write failed."));
159
+ }
160
+ });
161
+ }
162
+
163
+ function handleData(chunk) {
164
+ readBuffer = Buffer.concat([readBuffer, chunk]);
165
+ while (readBuffer.length >= FRAME_HEADER_BYTES) {
166
+ const frameLength = readBuffer.readUInt32LE(0);
167
+ if (frameLength > MAX_FRAME_BYTES) {
168
+ closeSocket();
169
+ return;
170
+ }
171
+ if (readBuffer.length < FRAME_HEADER_BYTES + frameLength) {
172
+ return;
173
+ }
174
+
175
+ const payload = readBuffer.slice(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + frameLength).toString("utf8");
176
+ readBuffer = readBuffer.slice(FRAME_HEADER_BYTES + frameLength);
177
+ const envelope = safeParseJSON(payload);
178
+ if (envelope) {
179
+ dispatchEnvelope(envelope);
180
+ }
181
+ }
182
+ }
183
+
184
+ function dispatchEnvelope(envelope) {
185
+ if (envelope.type === "response") {
186
+ handleResponse(envelope);
187
+ return;
188
+ }
189
+ if (envelope.type === "broadcast") {
190
+ onBroadcast?.(envelope);
191
+ return;
192
+ }
193
+ if (envelope.type === "client-discovery-request") {
194
+ const canHandle = Boolean(canHandleRequest?.(envelope));
195
+ writeEnvelope({
196
+ type: "client-discovery-response",
197
+ requestId: envelope.requestId,
198
+ response: { canHandle },
199
+ });
200
+ return;
201
+ }
202
+ if (envelope.type === "request") {
203
+ handleIncomingRequest(envelope);
204
+ }
205
+ }
206
+
207
+ function handleResponse(envelope) {
208
+ const requestId = requestIdKey(envelope.requestId);
209
+ const waiter = requestId ? pendingResponses.get(requestId) : null;
210
+ if (!waiter) {
211
+ return;
212
+ }
213
+ pendingResponses.delete(requestId);
214
+ clearTimeout(waiter.timeout);
215
+ if (envelope.resultType === "error") {
216
+ waiter.reject(new Error(envelope.error || `Desktop IPC request failed: ${waiter.method}`));
217
+ return;
218
+ }
219
+ waiter.resolve(envelope.result ?? null);
220
+ }
221
+
222
+ function handleIncomingRequest(envelope) {
223
+ Promise.resolve()
224
+ .then(() => handleRequest(envelope))
225
+ .then((result) => {
226
+ writeEnvelope({
227
+ type: "response",
228
+ requestId: envelope.requestId,
229
+ resultType: "success",
230
+ method: envelope.method,
231
+ handledByClientId: clientId,
232
+ result: result ?? null,
233
+ });
234
+ })
235
+ .catch((error) => {
236
+ writeEnvelope({
237
+ type: "response",
238
+ requestId: envelope.requestId,
239
+ resultType: "error",
240
+ method: envelope.method,
241
+ handledByClientId: clientId,
242
+ error: error?.message || "Remodex IPC owner request failed.",
243
+ });
244
+ });
245
+ }
246
+
247
+ function handleClose(closedSocket) {
248
+ if (socket && socket !== closedSocket) {
249
+ return;
250
+ }
251
+ socket = null;
252
+ isConnecting = false;
253
+ isInitialized = false;
254
+ clientId = "";
255
+ readBuffer = Buffer.alloc(0);
256
+ for (const waiter of pendingResponses.values()) {
257
+ clearTimeout(waiter.timeout);
258
+ waiter.reject(new Error("Desktop IPC connection closed."));
259
+ }
260
+ pendingResponses.clear();
261
+ scheduleReconnect();
262
+ }
263
+
264
+ function scheduleReconnect() {
265
+ if (!shouldReconnect || reconnectTimer) {
266
+ return;
267
+ }
268
+ reconnectTimer = setTimeout(() => {
269
+ reconnectTimer = null;
270
+ ensureConnected();
271
+ }, reconnectMs);
272
+ reconnectTimer.unref?.();
273
+ }
274
+
275
+ function clearReconnectTimer() {
276
+ if (!reconnectTimer) {
277
+ return;
278
+ }
279
+ clearTimeout(reconnectTimer);
280
+ reconnectTimer = null;
281
+ }
282
+
283
+ function closeSocket() {
284
+ if (!socket) {
285
+ return;
286
+ }
287
+ const closingSocket = socket;
288
+ socket = null;
289
+ closingSocket.destroy();
290
+ }
291
+
292
+ function close() {
293
+ shouldReconnect = false;
294
+ clearReconnectTimer();
295
+ closeSocket();
296
+ localRouter?.close();
297
+ for (const waiter of pendingResponses.values()) {
298
+ clearTimeout(waiter.timeout);
299
+ waiter.reject(new Error("Desktop IPC live owner stopped."));
300
+ }
301
+ pendingResponses.clear();
302
+ }
303
+
304
+ function writeEnvelope(envelope) {
305
+ if (!socket || socket.destroyed) {
306
+ return false;
307
+ }
308
+ try {
309
+ writeFrame(socket, JSON.stringify(envelope));
310
+ return true;
311
+ } catch {
312
+ closeSocket();
313
+ return false;
314
+ }
315
+ }
316
+
317
+ return {
318
+ ensureConnected,
319
+ sendBroadcast,
320
+ close,
321
+ get clientId() {
322
+ return clientId;
323
+ },
324
+ };
325
+ }
326
+
327
+ function createDesktopIpcRouterServer({
328
+ socketPath,
329
+ netModule,
330
+ now,
331
+ requestTimeoutMs,
332
+ discoveryTimeoutMs,
333
+ logPrefix,
334
+ }) {
335
+ let server = null;
336
+ let started = false;
337
+ let starting = null;
338
+ let closed = false;
339
+ let nextClientSeq = 1;
340
+ const clientsById = new Map();
341
+ const pendingDiscoveryResponses = new Map();
342
+ const pendingRoutedResponses = new Map();
343
+
344
+ function start({ removeStaleSocket = false } = {}) {
345
+ if (started) {
346
+ return Promise.resolve();
347
+ }
348
+ if (starting) {
349
+ return starting;
350
+ }
351
+ closed = false;
352
+ starting = new Promise((resolve, reject) => {
353
+ try {
354
+ prepareSocketPathForListen(socketPath, { removeStaleSocket });
355
+ } catch (error) {
356
+ starting = null;
357
+ reject(error);
358
+ return;
359
+ }
360
+
361
+ const nextServer = netModule.createServer((socket) => attachClient(socket));
362
+ server = nextServer;
363
+ nextServer.on("error", (error) => {
364
+ starting = null;
365
+ server = null;
366
+ reject(error);
367
+ });
368
+ nextServer.listen(socketPath, () => {
369
+ started = true;
370
+ starting = null;
371
+ nextServer.removeAllListeners("error");
372
+ nextServer.on("error", (error) => {
373
+ console.warn(`${logPrefix} desktop IPC router fallback error: ${error.message}`);
374
+ });
375
+ resolve();
376
+ });
377
+ nextServer.unref?.();
378
+ });
379
+ return starting;
380
+ }
381
+
382
+ function attachClient(socket) {
383
+ const client = {
384
+ id: "",
385
+ type: "",
386
+ socket,
387
+ buffer: Buffer.alloc(0),
388
+ initialized: false,
389
+ };
390
+ socket.on("data", (chunk) => handleClientData(client, chunk));
391
+ socket.on("close", () => removeClient(client));
392
+ socket.on("error", () => removeClient(client));
393
+ }
394
+
395
+ function handleClientData(client, chunk) {
396
+ client.buffer = Buffer.concat([client.buffer, chunk]);
397
+ while (client.buffer.length >= FRAME_HEADER_BYTES) {
398
+ const frameLength = client.buffer.readUInt32LE(0);
399
+ if (frameLength > MAX_FRAME_BYTES) {
400
+ client.socket.destroy();
401
+ return;
402
+ }
403
+ if (client.buffer.length < FRAME_HEADER_BYTES + frameLength) {
404
+ return;
405
+ }
406
+
407
+ const payload = client.buffer.slice(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + frameLength).toString("utf8");
408
+ client.buffer = client.buffer.slice(FRAME_HEADER_BYTES + frameLength);
409
+ const envelope = safeParseJSON(payload);
410
+ if (envelope) {
411
+ dispatchClientEnvelope(client, envelope);
412
+ }
413
+ }
414
+ }
415
+
416
+ function dispatchClientEnvelope(client, envelope) {
417
+ if (envelope.type === "request" && envelope.method === "initialize") {
418
+ initializeClient(client, envelope);
419
+ return;
420
+ }
421
+ if (envelope.type === "broadcast") {
422
+ relayBroadcast(client, envelope);
423
+ return;
424
+ }
425
+ if (envelope.type === "request") {
426
+ routeClientRequest(client, envelope);
427
+ return;
428
+ }
429
+ if (envelope.type === "response") {
430
+ routeClientResponse(client, envelope);
431
+ return;
432
+ }
433
+ if (envelope.type === "client-discovery-request") {
434
+ answerClientDiscoveryRequest(client, envelope);
435
+ return;
436
+ }
437
+ if (envelope.type === "client-discovery-response") {
438
+ resolveDiscoveryResponse(envelope);
439
+ }
440
+ }
441
+
442
+ function initializeClient(client, envelope) {
443
+ if (!client.id) {
444
+ client.id = `remodex-router-${now().toString(36)}-${nextClientSeq}`;
445
+ nextClientSeq += 1;
446
+ clientsById.set(client.id, client);
447
+ }
448
+ client.initialized = true;
449
+ client.type = readString(envelope.params?.clientType) || readString(envelope.params?.client_type);
450
+ writeEnvelopeToClient(client, {
451
+ type: "response",
452
+ requestId: envelope.requestId,
453
+ resultType: "success",
454
+ method: "initialize",
455
+ handledByClientId: "remodex-ipc-router",
456
+ result: { clientId: client.id },
457
+ });
458
+ relayBroadcast(client, {
459
+ type: "broadcast",
460
+ method: CLIENT_STATUS_CHANGED,
461
+ sourceClientId: client.id,
462
+ version: METHOD_VERSION_BY_NAME.get(CLIENT_STATUS_CHANGED) || 1,
463
+ params: {
464
+ clientId: client.id,
465
+ clientType: client.type,
466
+ status: "connected",
467
+ },
468
+ });
469
+ }
470
+
471
+ function relayBroadcast(sender, envelope) {
472
+ const normalizedEnvelope = {
473
+ ...envelope,
474
+ sourceClientId: readString(envelope.sourceClientId) || sender.id,
475
+ version: envelope.version || METHOD_VERSION_BY_NAME.get(envelope.method) || 1,
476
+ };
477
+ for (const client of clientsById.values()) {
478
+ if (!client.initialized || client === sender) {
479
+ continue;
480
+ }
481
+ writeEnvelopeToClient(client, normalizedEnvelope);
482
+ }
483
+ }
484
+
485
+ async function routeClientRequest(sender, envelope) {
486
+ const target = await discoverTargetForRequest(sender, envelope);
487
+ if (!target) {
488
+ writeEnvelopeToClient(sender, {
489
+ type: "response",
490
+ requestId: envelope.requestId,
491
+ resultType: "error",
492
+ method: envelope.method,
493
+ handledByClientId: "",
494
+ error: `No Codex IPC client can handle ${envelope.method}.`,
495
+ });
496
+ return;
497
+ }
498
+ const requestId = requestIdKey(envelope.requestId);
499
+ if (!requestId) {
500
+ writeEnvelopeToClient(sender, {
501
+ type: "response",
502
+ requestId: envelope.requestId,
503
+ resultType: "error",
504
+ method: envelope.method,
505
+ handledByClientId: target.id,
506
+ error: "Missing requestId.",
507
+ });
508
+ return;
509
+ }
510
+
511
+ // JSON-RPC request ids are only unique per connection, so forward a rewritten
512
+ // router-scoped id to keep concurrent same-id requests from colliding.
513
+ const routedRequestId = `remodex-routed-${now().toString(36)}-${randomUUID()}`;
514
+ const routeKey = routedResponseKey(target.id, routedRequestId);
515
+ const timeout = setTimeout(() => {
516
+ pendingRoutedResponses.delete(routeKey);
517
+ writeEnvelopeToClient(sender, {
518
+ type: "response",
519
+ requestId: envelope.requestId,
520
+ resultType: "error",
521
+ method: envelope.method,
522
+ handledByClientId: target.id,
523
+ error: `Codex IPC routed request timed out: ${envelope.method}`,
524
+ });
525
+ }, requestTimeoutMs);
526
+ timeout.unref?.();
527
+ pendingRoutedResponses.set(routeKey, {
528
+ sender,
529
+ senderRequestId: envelope.requestId,
530
+ timeout,
531
+ });
532
+ if (!writeEnvelopeToClient(target, {
533
+ ...envelope,
534
+ requestId: routedRequestId,
535
+ sourceClientId: sender.id,
536
+ })) {
537
+ clearTimeout(timeout);
538
+ pendingRoutedResponses.delete(routeKey);
539
+ writeEnvelopeToClient(sender, {
540
+ type: "response",
541
+ requestId: envelope.requestId,
542
+ resultType: "error",
543
+ method: envelope.method,
544
+ handledByClientId: target.id,
545
+ error: "Codex IPC routed request write failed.",
546
+ });
547
+ }
548
+ }
549
+
550
+ function routeClientResponse(client, envelope) {
551
+ const routeKey = routedResponseKey(client.id, requestIdKey(envelope.requestId));
552
+ const route = pendingRoutedResponses.get(routeKey);
553
+ if (!route) {
554
+ return;
555
+ }
556
+ pendingRoutedResponses.delete(routeKey);
557
+ clearTimeout(route.timeout);
558
+ writeEnvelopeToClient(route.sender, {
559
+ ...envelope,
560
+ requestId: route.senderRequestId,
561
+ });
562
+ }
563
+
564
+ async function answerClientDiscoveryRequest(sender, envelope) {
565
+ const target = await discoverTargetForRequest(sender, envelope.request || envelope);
566
+ writeEnvelopeToClient(sender, {
567
+ type: "client-discovery-response",
568
+ requestId: envelope.requestId,
569
+ response: {
570
+ canHandle: Boolean(target),
571
+ },
572
+ });
573
+ }
574
+
575
+ async function discoverTargetForRequest(sender, request) {
576
+ const candidates = Array.from(clientsById.values()).filter((client) => (
577
+ client.initialized && client !== sender && !client.socket.destroyed
578
+ ));
579
+ const results = await Promise.all(candidates.map(async (candidate, index) => {
580
+ const canHandle = await askClientCanHandle(candidate, request);
581
+ return canHandle ? { client: candidate, index } : null;
582
+ }));
583
+ return results
584
+ .filter(Boolean)
585
+ .sort(compareDiscoveryTargets)[0]?.client || null;
586
+ }
587
+
588
+ function compareDiscoveryTargets(left, right) {
589
+ const priorityDelta = discoveryTargetPriority(left.client) - discoveryTargetPriority(right.client);
590
+ return priorityDelta || left.index - right.index;
591
+ }
592
+
593
+ function discoveryTargetPriority(client) {
594
+ // If both sides claim a follower request, the bridge's tagged live owner wins
595
+ // over stale Desktop state to keep phone-owned streams on the local runtime.
596
+ return normalizeToken(client?.type) === "remodexbridge" ? 0 : 1;
597
+ }
598
+
599
+ function askClientCanHandle(client, request) {
600
+ const requestId = `remodex-router-discovery-${now().toString(36)}-${randomUUID()}`;
601
+ return new Promise((resolve) => {
602
+ const timeout = setTimeout(() => {
603
+ pendingDiscoveryResponses.delete(requestId);
604
+ resolve(false);
605
+ }, discoveryTimeoutMs);
606
+ timeout.unref?.();
607
+ pendingDiscoveryResponses.set(requestId, {
608
+ resolve,
609
+ timeout,
610
+ });
611
+ if (!writeEnvelopeToClient(client, {
612
+ type: "client-discovery-request",
613
+ requestId,
614
+ request,
615
+ })) {
616
+ clearTimeout(timeout);
617
+ pendingDiscoveryResponses.delete(requestId);
618
+ resolve(false);
619
+ }
620
+ });
621
+ }
622
+
623
+ function resolveDiscoveryResponse(envelope) {
624
+ const requestId = requestIdKey(envelope.requestId);
625
+ const pending = requestId ? pendingDiscoveryResponses.get(requestId) : null;
626
+ if (!pending) {
627
+ return;
628
+ }
629
+ pendingDiscoveryResponses.delete(requestId);
630
+ clearTimeout(pending.timeout);
631
+ pending.resolve(Boolean(envelope.response?.canHandle));
632
+ }
633
+
634
+ function removeClient(client) {
635
+ if (client.id) {
636
+ clientsById.delete(client.id);
637
+ relayBroadcast(client, {
638
+ type: "broadcast",
639
+ method: CLIENT_STATUS_CHANGED,
640
+ sourceClientId: client.id,
641
+ version: METHOD_VERSION_BY_NAME.get(CLIENT_STATUS_CHANGED) || 1,
642
+ params: {
643
+ clientId: client.id,
644
+ clientType: client.type,
645
+ status: "disconnected",
646
+ },
647
+ });
648
+ }
649
+ for (const [routeKey, route] of Array.from(pendingRoutedResponses.entries())) {
650
+ if (!routeKey.startsWith(`${client.id}:`)) {
651
+ continue;
652
+ }
653
+ pendingRoutedResponses.delete(routeKey);
654
+ clearTimeout(route.timeout);
655
+ writeEnvelopeToClient(route.sender, {
656
+ type: "response",
657
+ requestId: route.senderRequestId,
658
+ resultType: "error",
659
+ method: "",
660
+ handledByClientId: client.id,
661
+ error: "Codex IPC target disconnected.",
662
+ });
663
+ }
664
+ }
665
+
666
+ function close() {
667
+ const shouldRemoveSocketPath = started || server;
668
+ closed = true;
669
+ started = false;
670
+ starting = null;
671
+ for (const pending of pendingDiscoveryResponses.values()) {
672
+ clearTimeout(pending.timeout);
673
+ pending.resolve(false);
674
+ }
675
+ pendingDiscoveryResponses.clear();
676
+ for (const route of pendingRoutedResponses.values()) {
677
+ clearTimeout(route.timeout);
678
+ }
679
+ pendingRoutedResponses.clear();
680
+ for (const client of clientsById.values()) {
681
+ client.socket.destroy();
682
+ }
683
+ clientsById.clear();
684
+ if (server) {
685
+ server.close();
686
+ server = null;
687
+ }
688
+ if (shouldRemoveSocketPath) {
689
+ removeSocketPathAfterClose(socketPath);
690
+ }
691
+ }
692
+
693
+ function writeEnvelopeToClient(client, envelope) {
694
+ if (!client?.socket || client.socket.destroyed) {
695
+ return false;
696
+ }
697
+ try {
698
+ writeFrame(client.socket, JSON.stringify(envelope));
699
+ return true;
700
+ } catch {
701
+ client.socket.destroy();
702
+ return false;
703
+ }
704
+ }
705
+
706
+ return {
707
+ start,
708
+ close,
709
+ get isStarted() {
710
+ return started && !closed;
711
+ },
712
+ };
713
+ }
714
+
715
+ function routedResponseKey(clientId, requestId) {
716
+ return `${clientId}:${requestId}`;
717
+ }
718
+
719
+ function prepareSocketPathForListen(socketPath, { removeStaleSocket = false } = {}) {
720
+ if (process.platform === "win32") {
721
+ return;
722
+ }
723
+ fs.mkdirSync(path.dirname(socketPath), { recursive: true });
724
+ if (removeStaleSocket && fs.existsSync(socketPath)) {
725
+ const socketStat = fs.lstatSync(socketPath);
726
+ if (!socketStat.isSocket()) {
727
+ throw new Error(`Refusing to replace non-socket Codex IPC path: ${socketPath}`);
728
+ }
729
+ fs.unlinkSync(socketPath);
730
+ }
731
+ }
732
+
733
+ function removeSocketPathAfterClose(socketPath) {
734
+ if (process.platform === "win32") {
735
+ return;
736
+ }
737
+ try {
738
+ if (fs.existsSync(socketPath)) {
739
+ fs.unlinkSync(socketPath);
740
+ }
741
+ } catch {
742
+ // Best-effort cleanup only; the next fallback start can remove stale sockets.
743
+ }
744
+ }
745
+
746
+ module.exports = {
747
+ DEFAULT_DISCOVERY_TIMEOUT_MS,
748
+ createDesktopIpcRouterServer,
749
+ createDesktopOwnerIpcClient,
750
+ };