@delt/claude-alarm 1.1.1 → 1.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.
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/codex/main.ts
4
- import fs4 from "fs";
5
-
6
3
  // src/shared/config.ts
7
4
  import fs from "fs";
8
5
  import path2 from "path";
@@ -21,6 +18,7 @@ var UPLOADS_DIR = path.join(CONFIG_DIR, "uploads");
21
18
  var CODEX_PID_FILE = path.join(CONFIG_DIR, "codex.pid");
22
19
  var CODEX_LOG_FILE = path.join(CONFIG_DIR, "codex.log");
23
20
  var WS_PATH_CHANNEL = "/ws/channel";
21
+ var WS_PATH_CODEX = "/ws/codex";
24
22
  var CHANNEL_SERVER_VERSION = "0.1.0";
25
23
 
26
24
  // src/shared/config.ts
@@ -133,18 +131,122 @@ function logStartup(context) {
133
131
  `);
134
132
  }
135
133
 
134
+ // src/codex/adapter.ts
135
+ import fs5 from "fs";
136
+ import os2 from "os";
137
+ import path4 from "path";
138
+
139
+ // src/codex/hub-link.ts
140
+ import WebSocket from "ws";
141
+ var CodexHubLink = class {
142
+ constructor(hub, identity, onCall, reconnectMs = 5e3, pingTimeoutMs = 75e3) {
143
+ this.hub = hub;
144
+ this.identity = identity;
145
+ this.onCall = onCall;
146
+ this.reconnectMs = reconnectMs;
147
+ this.pingTimeoutMs = pingTimeoutMs;
148
+ }
149
+ ws = null;
150
+ retryTimer = null;
151
+ pingTimer = null;
152
+ closed = true;
153
+ ready = false;
154
+ connect() {
155
+ this.closed = false;
156
+ const query = this.hub.token ? `?token=${encodeURIComponent(this.hub.token)}` : "";
157
+ let ws;
158
+ try {
159
+ ws = new WebSocket(`ws://${this.hub.host}:${this.hub.port}${WS_PATH_CODEX}${query}`);
160
+ } catch (err) {
161
+ logger.debug(`Codex hub link failed: ${err.message}`);
162
+ this.scheduleReconnect();
163
+ return;
164
+ }
165
+ this.ws = ws;
166
+ ws.on("open", () => {
167
+ if (this.ws !== ws) return;
168
+ this.armWatchdog(ws);
169
+ this.hello();
170
+ });
171
+ ws.on("ping", () => this.armWatchdog(ws));
172
+ ws.on("message", (data) => {
173
+ if (this.ws === ws) this.onMessage(String(data));
174
+ });
175
+ ws.on("close", () => {
176
+ if (this.ws !== ws) return;
177
+ this.clearWatchdog();
178
+ this.ws = null;
179
+ this.scheduleReconnect();
180
+ });
181
+ ws.on("error", (err) => logger.debug(`Codex hub link error: ${err.message}`));
182
+ }
183
+ setReady(ready) {
184
+ if (this.ready === ready) return;
185
+ this.ready = ready;
186
+ this.hello();
187
+ }
188
+ disconnect() {
189
+ this.closed = true;
190
+ this.clearWatchdog();
191
+ if (this.retryTimer) clearTimeout(this.retryTimer);
192
+ this.retryTimer = null;
193
+ const ws = this.ws;
194
+ this.ws = null;
195
+ ws?.close();
196
+ }
197
+ hello() {
198
+ this.send({ type: "adapter_hello", adapter: { ...this.identity, ready: this.ready } });
199
+ }
200
+ send(msg) {
201
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(msg));
202
+ }
203
+ onMessage(text) {
204
+ let msg;
205
+ try {
206
+ msg = JSON.parse(text);
207
+ } catch {
208
+ return;
209
+ }
210
+ if (!msg || typeof msg !== "object") return;
211
+ if (msg.type !== "adapter_call") return;
212
+ const { requestId, call } = msg;
213
+ this.onCall(call).then(
214
+ (data) => this.send({ type: "adapter_result", requestId, ok: true, data }),
215
+ (err) => this.send({ type: "adapter_result", requestId, ok: false, error: err.message })
216
+ );
217
+ }
218
+ clearWatchdog() {
219
+ if (this.pingTimer) clearTimeout(this.pingTimer);
220
+ this.pingTimer = null;
221
+ }
222
+ armWatchdog(ws) {
223
+ if (this.ws !== ws) return;
224
+ this.clearWatchdog();
225
+ this.pingTimer = setTimeout(() => {
226
+ if (this.ws === ws) ws.terminate();
227
+ }, this.pingTimeoutMs);
228
+ }
229
+ scheduleReconnect() {
230
+ if (this.closed || this.retryTimer) return;
231
+ this.retryTimer = setTimeout(() => {
232
+ this.retryTimer = null;
233
+ this.connect();
234
+ }, this.reconnectMs);
235
+ }
236
+ };
237
+
136
238
  // src/codex/adapter.ts
137
239
  import { randomUUID as randomUUID2 } from "crypto";
138
240
 
139
241
  // src/channel/hub-client.ts
140
- import WebSocket from "ws";
242
+ import WebSocket2 from "ws";
141
243
  var HubClient = class {
142
- constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token2, getPeerName = () => void 0, getRegistration = () => ({})) {
244
+ constructor(sessionId, sessionName, hubHost = DEFAULT_HUB_HOST, hubPort = DEFAULT_HUB_PORT, token, getPeerName = () => void 0, getRegistration = () => ({})) {
143
245
  this.sessionId = sessionId;
144
246
  this.sessionName = sessionName;
145
247
  this.hubHost = hubHost;
146
248
  this.hubPort = hubPort;
147
- this.token = token2;
249
+ this.token = token;
148
250
  this.getPeerName = getPeerName;
149
251
  this.getRegistration = getRegistration;
150
252
  }
@@ -160,7 +262,7 @@ var HubClient = class {
160
262
  const url = `ws://${this.hubHost}:${this.hubPort}${WS_PATH_CHANNEL}${tokenQuery}`;
161
263
  logger.debug(`Connecting to hub at ${url}`);
162
264
  try {
163
- const ws = new WebSocket(url);
265
+ const ws = new WebSocket2(url);
164
266
  this.ws = ws;
165
267
  ws.on("open", () => {
166
268
  if (this.ws !== ws) return;
@@ -200,7 +302,7 @@ var HubClient = class {
200
302
  }
201
303
  }
202
304
  send(msg) {
203
- if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
305
+ if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
204
306
  this.ws.send(JSON.stringify(msg));
205
307
  } else {
206
308
  if (this.queue.length < 100) {
@@ -225,7 +327,7 @@ var HubClient = class {
225
327
  this.connected = false;
226
328
  }
227
329
  reregister() {
228
- if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
330
+ if (this.connected && this.ws?.readyState === WebSocket2.OPEN) {
229
331
  this.ws.send(JSON.stringify(this.registration()));
230
332
  }
231
333
  }
@@ -259,10 +361,11 @@ import { spawn } from "child_process";
259
361
  import fs3 from "fs";
260
362
  import path3 from "path";
261
363
  import { Duplex } from "stream";
262
- import WebSocket2 from "ws";
364
+ import WebSocket3 from "ws";
263
365
  function resolveCommand(command, platform = process.platform, env = process.env) {
264
- if (platform !== "win32" || /[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: false };
265
- const file = findOnPath(command, platform, env);
366
+ if (platform !== "win32") return { file: command, shell: false };
367
+ if (/[\\/]/.test(command) || path3.extname(command)) return { file: command, shell: /\.(cmd|bat)$/i.test(command) };
368
+ const file = findCodex(command, platform, env);
266
369
  return file ? { file, shell: file.endsWith(".cmd") } : { file: command, shell: false };
267
370
  }
268
371
  function findOnPath(command, platform = process.platform, env = process.env) {
@@ -275,12 +378,22 @@ function findOnPath(command, platform = process.platform, env = process.env) {
275
378
  }
276
379
  return void 0;
277
380
  }
381
+ function findCodex(command, platform = process.platform, env = process.env) {
382
+ const onPath = findOnPath(command, platform, env);
383
+ if (onPath || platform !== "win32") return onPath;
384
+ const candidates = [
385
+ env.LOCALAPPDATA && path3.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin", `${command}.exe`),
386
+ env.APPDATA && path3.join(env.APPDATA, "npm", `${command}.cmd`)
387
+ ];
388
+ return candidates.find((file) => !!file && fs3.existsSync(file));
389
+ }
278
390
  var defaultSpawn = (command, args) => {
279
391
  const { file, shell } = resolveCommand(command);
280
392
  if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
281
393
  return spawn(file, args, { stdio: "pipe", windowsHide: true });
282
394
  };
283
- function connectProxy(command, spawnFn = defaultSpawn) {
395
+ var HANDSHAKE_TIMEOUT_MS = 1e4;
396
+ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
284
397
  const child = spawnFn(command, ["app-server", "proxy"]);
285
398
  child.stdin?.on("error", () => {
286
399
  });
@@ -305,7 +418,7 @@ function connectProxy(command, spawnFn = defaultSpawn) {
305
418
  }, ref() {
306
419
  }, unref() {
307
420
  } });
308
- const ws = new WebSocket2("ws://localhost/", {
421
+ const ws = new WebSocket3("ws://localhost/", {
309
422
  createConnection: (() => stream)
310
423
  });
311
424
  const close = () => {
@@ -314,9 +427,11 @@ function connectProxy(command, spawnFn = defaultSpawn) {
314
427
  };
315
428
  return new Promise((resolve, reject) => {
316
429
  let settled = false;
430
+ const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
317
431
  const fail = (err) => {
318
432
  if (settled) return;
319
433
  settled = true;
434
+ clearTimeout(timer);
320
435
  close();
321
436
  reject(err);
322
437
  };
@@ -326,6 +441,7 @@ function connectProxy(command, spawnFn = defaultSpawn) {
326
441
  ws.once("open", () => {
327
442
  if (settled) return;
328
443
  settled = true;
444
+ clearTimeout(timer);
329
445
  child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
330
446
  ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
331
447
  resolve({ ws, close });
@@ -333,6 +449,57 @@ function connectProxy(command, spawnFn = defaultSpawn) {
333
449
  });
334
450
  }
335
451
 
452
+ // src/codex/inputs.ts
453
+ import fs4 from "fs/promises";
454
+
455
+ // src/codex/mapping.ts
456
+ var TITLE_MAX = 30;
457
+ var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
458
+ var SOURCE_PREFIX = new RegExp(`^\\[claude-alarm(?: \xB7 (?:${Object.values(SOURCE_LABEL).join("|")}))?\\] ?`);
459
+ function codexSessionId(threadId) {
460
+ return `codex:${threadId}`;
461
+ }
462
+ function isTrackable(thread) {
463
+ return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== "notLoaded";
464
+ }
465
+ function threadTitle(thread) {
466
+ const name = thread.name?.trim();
467
+ if (name) return name;
468
+ const preview = (thread.preview ?? "").replace(/\s+/g, " ").trim().replace(SOURCE_PREFIX, "").trim();
469
+ if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}\u2026` : preview;
470
+ return thread.cwd.replace(/^.*[/\\]/, "") || thread.id.slice(0, 8);
471
+ }
472
+ function hubStatus(status) {
473
+ if (status.type !== "active") return "idle";
474
+ return status.activeFlags.some((f) => f === "waitingOnApproval" || f === "waitingOnUserInput") ? "waiting_input" : "working";
475
+ }
476
+ function withSourcePrefix(content, source) {
477
+ return source ? `[claude-alarm \xB7 ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;
478
+ }
479
+ function finalAnswer(messages) {
480
+ const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
481
+ if (finals.length) return finals.join("\n\n");
482
+ return messages.at(-1)?.text ?? null;
483
+ }
484
+ function cleanFolder(input) {
485
+ const s = input.trim();
486
+ return s.length >= 2 && s.startsWith('"') && s.endsWith('"') ? s.slice(1, -1).trim() : s;
487
+ }
488
+
489
+ // src/codex/inputs.ts
490
+ var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
491
+ function textInput(content, source) {
492
+ return [{ type: "text", text: withSourcePrefix(content, source) }];
493
+ }
494
+ async function imageInput(imagePath, mimeType, caption, source) {
495
+ if (!IMAGE_TYPES.has(mimeType)) throw new Error(`unsupported image type ${mimeType}`);
496
+ const data = await fs4.readFile(imagePath);
497
+ return [
498
+ { type: "text", text: withSourcePrefix(caption?.trim() || "(image)", source) },
499
+ { type: "image", url: `data:${mimeType};base64,${data.toString("base64")}` }
500
+ ];
501
+ }
502
+
336
503
  // src/codex/rpc.ts
337
504
  import { EventEmitter } from "events";
338
505
  var RpcError = class extends Error {
@@ -358,14 +525,14 @@ var RpcClient = class extends EventEmitter {
358
525
  }
359
526
  nextId = 1;
360
527
  pending = /* @__PURE__ */ new Map();
361
- request(method, params) {
528
+ request(method, params, timeoutMs = this.timeoutMs) {
362
529
  if (this.ws.readyState !== this.ws.OPEN) return Promise.reject(new Error("connection closed"));
363
530
  const id = this.nextId++;
364
531
  return new Promise((resolve, reject) => {
365
- const timer = setTimeout(() => {
532
+ const timer = timeoutMs === null ? void 0 : setTimeout(() => {
366
533
  this.pending.delete(id);
367
534
  reject(new Error(`${method} timed out`));
368
- }, this.timeoutMs);
535
+ }, timeoutMs);
369
536
  this.pending.set(id, { resolve, reject, timer });
370
537
  this.ws.send(JSON.stringify(params === void 0 ? { id, method } : { id, method, params }));
371
538
  });
@@ -476,41 +643,14 @@ ${f.diff}` : f.path).join("\n\n") : "The file list is not available here. Check
476
643
  }
477
644
  }
478
645
 
479
- // src/codex/mapping.ts
480
- var TITLE_MAX = 30;
481
- var SOURCE_LABEL = { dashboard: "Dashboard", telegram: "Telegram", api: "API" };
482
- function codexSessionId(threadId) {
483
- return `codex:${threadId}`;
484
- }
485
- function isTrackable(thread) {
486
- return !thread.parentThreadId && !thread.ephemeral && thread.status.type !== "notLoaded";
487
- }
488
- function threadTitle(thread) {
489
- const name = thread.name?.trim();
490
- if (name) return name;
491
- const preview = (thread.preview ?? "").replace(/\s+/g, " ").trim();
492
- if (preview) return preview.length > TITLE_MAX ? `${preview.slice(0, TITLE_MAX)}\u2026` : preview;
493
- return thread.cwd.replace(/^.*[/\\]/, "") || thread.id.slice(0, 8);
494
- }
495
- function hubStatus(status) {
496
- if (status.type !== "active") return "idle";
497
- return status.activeFlags.some((f) => f === "waitingOnApproval" || f === "waitingOnUserInput") ? "waiting_input" : "working";
498
- }
499
- function withSourcePrefix(content, source) {
500
- return source ? `[claude-alarm \xB7 ${SOURCE_LABEL[source]}] ${content}` : `[claude-alarm] ${content}`;
501
- }
502
- function finalAnswer(messages) {
503
- const finals = messages.filter((m) => m.phase === "final_answer").map((m) => m.text);
504
- if (finals.length) return finals.join("\n\n");
505
- return messages.at(-1)?.text ?? null;
506
- }
507
-
508
646
  // src/codex/adapter.ts
509
647
  var USER_REQUESTS = /* @__PURE__ */ new Set(["item/tool/requestUserInput", "item/permissions/requestApproval", "mcpServer/elicitation/request"]);
510
648
  var CodexAdapter = class {
511
649
  constructor(opts) {
512
650
  this.opts = opts;
513
651
  this.delay = opts.reconnectMinMs ?? 2e3;
652
+ this.hostName = opts.hostName ?? os2.hostname();
653
+ this.link = new CodexHubLink(opts.hub, { id: randomUUID2(), host: this.hostName }, (call) => this.onCall(call), opts.linkReconnectMs);
514
654
  }
515
655
  conn;
516
656
  rpc;
@@ -519,12 +659,20 @@ var CodexAdapter = class {
519
659
  stopped = false;
520
660
  retryTimer;
521
661
  delay;
662
+ notFoundNotice = "unsent";
663
+ firstConnectReported = false;
664
+ pins = /* @__PURE__ */ new Set();
665
+ ready = false;
666
+ hostName;
667
+ link;
522
668
  start() {
523
669
  this.stopped = false;
670
+ this.link.connect();
524
671
  void this.connect();
525
672
  }
526
673
  stop() {
527
674
  this.stopped = true;
675
+ this.link.disconnect();
528
676
  if (this.retryTimer) clearTimeout(this.retryTimer);
529
677
  this.retryTimer = void 0;
530
678
  for (const id of [...this.threads.keys()]) this.drop(id);
@@ -538,7 +686,7 @@ var CodexAdapter = class {
538
686
  conn.close();
539
687
  return;
540
688
  }
541
- const live = new RpcClient(conn.ws);
689
+ const live = new RpcClient(conn.ws, this.opts.rpcTimeoutMs);
542
690
  rpc = live;
543
691
  live.on("notification", (method, params) => this.onNotification(method, params));
544
692
  live.on("request", (id, method, params) => this.onServerRequest(id, method, params));
@@ -552,10 +700,19 @@ var CodexAdapter = class {
552
700
  });
553
701
  live.notify("initialized");
554
702
  logger.info(`Connected to Codex daemon (${init.userAgent ?? "unknown version"})`);
703
+ this.reportFirstConnect({ connected: true, ...init.userAgent ? { userAgent: init.userAgent } : {} });
555
704
  await this.discover();
705
+ if (this.rpc === live) this.setReady(true);
556
706
  this.delay = this.opts.reconnectMinMs ?? 2e3;
557
707
  } catch (err) {
708
+ this.setReady(false);
558
709
  logger.warn(`Codex daemon connection failed: ${err.message}`);
710
+ if (err.code === "ENOENT") this.noticeNotFound();
711
+ this.reportFirstConnect({
712
+ connected: false,
713
+ error: err.message,
714
+ notFound: err.code === "ENOENT"
715
+ });
559
716
  if (rpc && this.rpc === rpc) {
560
717
  this.rpc = void 0;
561
718
  this.conn?.close();
@@ -566,6 +723,7 @@ var CodexAdapter = class {
566
723
  }
567
724
  }
568
725
  onDaemonLost() {
726
+ this.setReady(false);
569
727
  this.rpc = void 0;
570
728
  this.conn?.close();
571
729
  this.conn = void 0;
@@ -581,18 +739,86 @@ var CodexAdapter = class {
581
739
  }, this.delay);
582
740
  this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
583
741
  }
742
+ reportFirstConnect(outcome) {
743
+ if (this.firstConnectReported || this.stopped) return;
744
+ this.firstConnectReported = true;
745
+ try {
746
+ this.opts.onFirstConnect?.(outcome);
747
+ } catch (err) {
748
+ logger.debug(`First-connect observer failed: ${err.message}`);
749
+ }
750
+ }
751
+ noticeNotFound() {
752
+ if (this.notFoundNotice !== "unsent") return;
753
+ this.notFoundNotice = "sending";
754
+ const { host, port, token } = this.opts.hub;
755
+ fetch(`http://${host}:${port}/api/notify`, {
756
+ method: "POST",
757
+ headers: { "Content-Type": "application/json", ...token ? { Authorization: `Bearer ${token}` } : {} },
758
+ body: JSON.stringify({
759
+ title: "Codex not found",
760
+ message: `The Codex adapter cannot find "${this.opts.command}". Open a new terminal and restart the hub, or set "codex.command" in ~/.claude-alarm/config.json.`,
761
+ level: "warning"
762
+ }),
763
+ signal: AbortSignal.timeout(this.opts.noticeTimeoutMs ?? 5e3)
764
+ }).then((res) => {
765
+ this.notFoundNotice = res.ok ? "sent" : "unsent";
766
+ if (!res.ok) logger.debug(`Codex not-found notice was refused (${res.status})`);
767
+ }).catch((err) => {
768
+ this.notFoundNotice = err.name === "TimeoutError" ? "sent" : "unsent";
769
+ logger.debug(`Codex not-found notice failed: ${err.message}`);
770
+ });
771
+ }
772
+ setReady(ready) {
773
+ this.ready = ready;
774
+ this.link.setReady(ready);
775
+ }
776
+ requireDaemon() {
777
+ if (!this.ready || !this.rpc) throw new Error(`Codex is not connected on ${this.hostName}.`);
778
+ return this.rpc;
779
+ }
780
+ async onCall(call) {
781
+ if (call.kind === "folders") return { folders: await this.folders() };
782
+ return { sessionId: await this.create(call.cwd) };
783
+ }
784
+ async folders() {
785
+ const page = await this.requireDaemon().request("thread/list", { limit: 50, sortKey: "updated_at" });
786
+ return [...new Set(page.data.map((t) => t.cwd).filter(Boolean))].slice(0, 10);
787
+ }
788
+ async create(input) {
789
+ const rpc = this.requireDaemon();
790
+ const cwd = cleanFolder(input);
791
+ const isDir = path4.isAbsolute(cwd) && await fs5.promises.stat(cwd).then((s) => s.isDirectory(), () => false);
792
+ if (!isDir) throw new Error(`Folder not found on ${this.hostName}: ${cwd}`);
793
+ let thread;
794
+ try {
795
+ ({ thread } = await rpc.request("thread/start", { cwd, sandbox: "danger-full-access", approvalPolicy: "never" }, null));
796
+ } catch (err) {
797
+ throw new Error(`Codex could not start the conversation: ${err.message}`);
798
+ }
799
+ this.pins.add(thread.id);
800
+ this.upsert(thread);
801
+ const t = this.threads.get(thread.id);
802
+ if (!t) {
803
+ this.pins.delete(thread.id);
804
+ throw new Error("Codex started a conversation claude-alarm cannot follow.");
805
+ }
806
+ t.subscribed = true;
807
+ return codexSessionId(thread.id);
808
+ }
584
809
  async discover() {
810
+ const rpc = this.rpc;
811
+ const pinned = [...this.pins];
585
812
  const ids = [];
586
813
  let cursor;
587
814
  do {
588
- const page = await this.rpc.request(
589
- "thread/loaded/list",
590
- cursor ? { cursor } : {}
591
- );
815
+ const page = await rpc.request("thread/loaded/list", cursor ? { cursor } : {});
592
816
  ids.push(...page.data);
593
817
  cursor = page.nextCursor;
594
818
  } while (cursor);
595
819
  for (const id of ids) await this.refresh(id);
820
+ if (this.rpc !== rpc) return;
821
+ for (const id of pinned) if (!ids.includes(id)) this.pins.delete(id);
596
822
  }
597
823
  async refresh(threadId) {
598
824
  try {
@@ -607,18 +833,18 @@ var CodexAdapter = class {
607
833
  this.drop(thread.id);
608
834
  return;
609
835
  }
610
- const existing2 = this.threads.get(thread.id);
611
- if (existing2) {
612
- existing2.thread = thread;
613
- existing2.hub.reregister();
836
+ const existing = this.threads.get(thread.id);
837
+ if (existing) {
838
+ existing.thread = thread;
839
+ existing.hub.reregister();
614
840
  } else {
615
- const { host: host2, port: port2, token: token2 } = this.opts.hub;
841
+ const { host, port, token } = this.opts.hub;
616
842
  const hub = new HubClient(
617
843
  codexSessionId(thread.id),
618
844
  threadTitle(thread),
619
- host2,
620
- port2,
621
- token2,
845
+ host,
846
+ port,
847
+ token,
622
848
  () => void 0,
623
849
  () => this.registration(thread.id)
624
850
  );
@@ -629,6 +855,7 @@ var CodexAdapter = class {
629
855
  wantSubscribed: false,
630
856
  sync: Promise.resolve(),
631
857
  pendingTurn: false,
858
+ sending: Promise.resolve(),
632
859
  turns: /* @__PURE__ */ new Map(),
633
860
  unrelayed: false,
634
861
  files: /* @__PURE__ */ new Map()
@@ -642,12 +869,12 @@ var CodexAdapter = class {
642
869
  const t = this.threads.get(threadId);
643
870
  if (!t) return {};
644
871
  const title = threadTitle(t.thread);
645
- return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status) };
872
+ return { name: title, title, cwd: t.thread.cwd, agentKind: "codex", status: hubStatus(t.thread.status), closable: this.pins.has(threadId) };
646
873
  }
647
874
  want(threadId, subscribed) {
648
875
  const t = this.threads.get(threadId);
649
876
  if (!t) return Promise.resolve();
650
- t.wantSubscribed = subscribed;
877
+ t.wantSubscribed = subscribed || this.pins.has(threadId);
651
878
  t.sync = t.sync.then(() => this.syncSubscription(t));
652
879
  return t.sync;
653
880
  }
@@ -665,6 +892,7 @@ var CodexAdapter = class {
665
892
  }
666
893
  } catch (err) {
667
894
  logger.debug(`subscription sync for ${threadId} deferred: ${err.message}`);
895
+ if (t.wantSubscribed && !t.subscribed && t.thread.status.type === "active") t.unrelayed = true;
668
896
  }
669
897
  }
670
898
  releaseLater(t) {
@@ -687,6 +915,25 @@ var CodexAdapter = class {
687
915
  t.releaseTimer = void 0;
688
916
  t.hub.disconnect();
689
917
  }
918
+ busy(t) {
919
+ return t.thread.status.type === "active" || t.pendingTurn;
920
+ }
921
+ async close(threadId) {
922
+ const t = this.threads.get(threadId);
923
+ if (!t || !this.pins.delete(threadId)) return;
924
+ t.hub.reregister();
925
+ if (this.busy(t)) return;
926
+ await this.want(threadId, false);
927
+ if (this.threads.get(threadId) !== t || this.busy(t) || t.wantSubscribed) return;
928
+ if (!t.subscribed) {
929
+ this.drop(threadId);
930
+ return;
931
+ }
932
+ this.pins.add(threadId);
933
+ t.hub.reregister();
934
+ void this.want(threadId, true);
935
+ this.notify(threadId, "Not closed", "Codex did not release the conversation. Try closing it again.", "warning");
936
+ }
690
937
  onNotification(method, params) {
691
938
  switch (method) {
692
939
  case "thread/started":
@@ -706,6 +953,7 @@ var CodexAdapter = class {
706
953
  case "thread/closed":
707
954
  case "thread/archived":
708
955
  case "thread/deleted":
956
+ this.pins.delete(params.threadId);
709
957
  this.drop(params.threadId);
710
958
  break;
711
959
  case "item/started":
@@ -730,6 +978,7 @@ var CodexAdapter = class {
730
978
  return;
731
979
  }
732
980
  if (status.type === "notLoaded") {
981
+ this.pins.delete(threadId);
733
982
  this.drop(threadId);
734
983
  return;
735
984
  }
@@ -763,6 +1012,7 @@ var CodexAdapter = class {
763
1012
  const collected = t.turns.get(turn.id) ?? [];
764
1013
  t.turns.delete(turn.id);
765
1014
  t.files.clear();
1015
+ if (!t.thread.name?.trim() && !t.thread.preview?.trim()) void this.refresh(threadId);
766
1016
  if (t.thread.status.type !== "active") void this.want(threadId, false);
767
1017
  if (turn.status === "failed" || turn.error) {
768
1018
  this.notify(threadId, "Codex task failed", turn.error?.message ?? "The task ended with an error.", "error");
@@ -778,33 +1028,75 @@ var CodexAdapter = class {
778
1028
  }
779
1029
  onHubMessage(threadId, msg) {
780
1030
  if (msg.type === "message_to_session") {
781
- void this.sendTurn(threadId, msg.content, msg.source);
1031
+ this.enqueue(threadId, async () => textInput(msg.content, msg.source));
782
1032
  } else if (msg.type === "image_to_session") {
783
- this.notify(threadId, "Not delivered", "Codex sessions do not accept images yet.", "warning");
1033
+ this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source));
784
1034
  } else if (msg.type === "permission_response") {
785
1035
  this.answer(threadId, msg.requestId, msg.choiceId);
1036
+ } else if (msg.type === "codex_close") {
1037
+ void this.close(threadId);
786
1038
  }
787
1039
  }
788
- async sendTurn(threadId, content, source) {
1040
+ // One message at a time per conversation, so a message right behind another sees the turn the first one started.
1041
+ enqueue(threadId, build) {
789
1042
  const t = this.threads.get(threadId);
790
1043
  if (!t) return;
791
- if (t.thread.status.type !== "idle" || t.pendingTurn) {
792
- this.notify(threadId, "Not delivered", "Codex is busy, so the message was not delivered. Send it again when the task finishes.", "warning");
1044
+ t.sending = t.sending.then(() => this.deliver(t, build)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
1045
+ }
1046
+ async deliver(t, build) {
1047
+ const threadId = t.thread.id;
1048
+ if (this.threads.get(threadId) !== t) return;
1049
+ if (hubStatus(t.thread.status) === "waiting_input") {
1050
+ this.notify(threadId, "Not delivered", "Codex is waiting for an approval or input. Answer it first, then send the message again.", "warning");
793
1051
  return;
794
1052
  }
1053
+ let input;
1054
+ try {
1055
+ input = await build();
1056
+ } catch (err) {
1057
+ logger.debug(`Codex input for ${threadId} could not be built: ${err.message}`);
1058
+ this.notify(threadId, "Not delivered", "The image could not be read here, so it was not delivered. Codex may be running on another PC.", "warning");
1059
+ return;
1060
+ }
1061
+ if (this.threads.get(threadId) !== t) return;
795
1062
  t.pendingTurn = true;
1063
+ const wasUnrelayed = t.unrelayed;
796
1064
  try {
797
- if (!this.rpc) throw new Error("not connected to the Codex daemon");
1065
+ const rpc = this.rpc;
1066
+ if (!rpc) throw new Error("not connected to the Codex daemon");
798
1067
  await this.want(threadId, true);
799
- t.unrelayed = !t.subscribed;
800
- await this.rpc.request("turn/start", { threadId, input: [{ type: "text", text: withSourcePrefix(content, source) }] });
1068
+ const unsubscribed = !t.subscribed;
1069
+ const running = await this.runningTurn(rpc, threadId);
1070
+ if (this.threads.get(threadId) !== t) return;
1071
+ if (unsubscribed) t.unrelayed = true;
1072
+ if (running) {
1073
+ await rpc.request("turn/steer", { threadId, expectedTurnId: running, input });
1074
+ this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info");
1075
+ return;
1076
+ }
1077
+ await rpc.request("turn/start", { threadId, input });
801
1078
  } catch (err) {
802
1079
  t.pendingTurn = false;
803
- t.unrelayed = false;
1080
+ t.unrelayed = wasUnrelayed;
804
1081
  this.releaseLater(t);
805
1082
  this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
806
1083
  }
807
1084
  }
1085
+ // The list is unavailable before a new conversation's first turn; starting a turn is safe then, since turn/start steers a running turn.
1086
+ async runningTurn(rpc, threadId) {
1087
+ try {
1088
+ const page = await rpc.request("thread/turns/list", {
1089
+ threadId,
1090
+ limit: 1,
1091
+ sortDirection: "desc"
1092
+ });
1093
+ const turn = page.data?.[0];
1094
+ return turn?.status === "inProgress" ? turn.id : void 0;
1095
+ } catch (err) {
1096
+ logger.debug(`thread/turns/list ${threadId} failed: ${err.message}`);
1097
+ return void 0;
1098
+ }
1099
+ }
808
1100
  onServerRequest(rpcId, method, params) {
809
1101
  const t = params?.threadId ? this.threads.get(params.threadId) : void 0;
810
1102
  let view = null;
@@ -819,7 +1111,7 @@ var CodexAdapter = class {
819
1111
  }
820
1112
  if (!t || !view) {
821
1113
  if (t && (broken || USER_REQUESTS.has(method))) {
822
- this.notify(t.thread.id, "Codex is waiting", "Codex asked for input that claude-alarm cannot relay. Handle it in Codex.", "warning");
1114
+ this.waitingInCodex(t.thread.id);
823
1115
  } else {
824
1116
  logger.debug(`Ignoring Codex server request ${method}`);
825
1117
  }
@@ -829,17 +1121,26 @@ var CodexAdapter = class {
829
1121
  if (a.threadId === t.thread.id && a.rpcId === rpcId) return;
830
1122
  }
831
1123
  const requestId = randomUUID2();
832
- this.approvals.set(requestId, { threadId: t.thread.id, rpcId, turnId: params.turnId, choices: view.choices, answered: false });
833
- t.hub.send({
834
- type: "permission_request",
835
- sessionId: codexSessionId(t.thread.id),
836
- requestId,
837
- toolName: view.toolName,
838
- description: view.description,
839
- inputPreview: view.inputPreview,
840
- timestamp: Date.now(),
841
- choices: view.choices.map((c, i) => ({ id: String(i), label: c.label }))
842
- });
1124
+ try {
1125
+ this.approvals.set(requestId, { threadId: t.thread.id, rpcId, turnId: params.turnId, choices: view.choices, answered: false });
1126
+ t.hub.send({
1127
+ type: "permission_request",
1128
+ sessionId: codexSessionId(t.thread.id),
1129
+ requestId,
1130
+ toolName: view.toolName,
1131
+ description: view.description,
1132
+ inputPreview: view.inputPreview,
1133
+ timestamp: Date.now(),
1134
+ choices: view.choices.map((c, i) => ({ id: String(i), label: c.label }))
1135
+ });
1136
+ } catch (err) {
1137
+ this.approvals.delete(requestId);
1138
+ logger.warn(`Codex ${method} could not be relayed: ${err.message}`);
1139
+ this.waitingInCodex(t.thread.id);
1140
+ }
1141
+ }
1142
+ waitingInCodex(threadId) {
1143
+ this.notify(threadId, "Codex is waiting", "Codex asked for input that claude-alarm cannot relay. Handle it in Codex.", "warning");
843
1144
  }
844
1145
  answer(threadId, requestId, choiceId) {
845
1146
  const a = this.approvals.get(requestId);
@@ -865,49 +1166,302 @@ var CodexAdapter = class {
865
1166
  }
866
1167
  };
867
1168
 
868
- // src/codex/main.ts
869
- function isRunning(pid) {
1169
+ // src/shared/hub-url.ts
1170
+ import net from "net";
1171
+ function hubUrlHost(host) {
1172
+ if (host === "" || host === "0.0.0.0") return "127.0.0.1";
1173
+ if (host === "::") return "[::1]";
1174
+ return net.isIPv6(host) ? `[${host}]` : host;
1175
+ }
1176
+
1177
+ // src/codex/hub-target.ts
1178
+ function resolveAdapterHub(config2, env = process.env) {
1179
+ const envHost = env.CLAUDE_ALARM_HUB_HOST;
1180
+ const envPort = env.CLAUDE_ALARM_HUB_PORT;
1181
+ const envToken = env.CLAUDE_ALARM_HUB_TOKEN;
1182
+ return {
1183
+ host: hubUrlHost(envHost ?? config2.hub.host),
1184
+ port: envPort ? parseInt(envPort, 10) : config2.hub.port,
1185
+ token: envToken ?? config2.hub.token,
1186
+ fromEnv: { host: envHost !== void 0, port: !!envPort, token: envToken !== void 0 }
1187
+ };
1188
+ }
1189
+
1190
+ // src/codex/instance-lock.ts
1191
+ import crypto from "crypto";
1192
+ import fs6 from "fs";
1193
+ import net2 from "net";
1194
+ import path5 from "path";
1195
+ var CONTROL_PROTOCOL = 1;
1196
+ var STALE_GUARD_MS = 1e4;
1197
+ var STOP_PROOF_LABEL = "claude-alarm codex stop";
1198
+ var MAX_LINE_BYTES = 4096;
1199
+ var REQUEST_IDLE_MS = 2e3;
1200
+ var MAX_SOCKET_PATH_BYTES = 103;
1201
+ function stopProof(token) {
1202
+ return crypto.createHmac("sha256", token).update(STOP_PROOF_LABEL).digest("hex");
1203
+ }
1204
+ function controlEndpoint(configDir, platform = process.platform) {
1205
+ if (platform !== "win32") return path5.posix.join(configDir, "codex.sock");
1206
+ let dir;
870
1207
  try {
871
- process.kill(pid, 0);
872
- return true;
1208
+ dir = fs6.realpathSync.native(configDir);
873
1209
  } catch {
874
- return false;
1210
+ dir = path5.win32.resolve(configDir);
1211
+ }
1212
+ const hash = crypto.createHash("sha256").update(dir.toLowerCase()).digest("hex").slice(0, 16);
1213
+ return `\\\\.\\pipe\\claude-alarm-codex-${hash}`;
1214
+ }
1215
+ function ask(endpoint2, request, timeoutMs) {
1216
+ return new Promise((resolve) => {
1217
+ let buffer = "";
1218
+ let settled = false;
1219
+ const socket = net2.connect(endpoint2);
1220
+ const finish = (answer) => {
1221
+ if (settled) return;
1222
+ settled = true;
1223
+ clearTimeout(timer);
1224
+ socket.destroy();
1225
+ resolve(answer);
1226
+ };
1227
+ const timer = setTimeout(() => finish({ state: "unknown", error: `no answer within ${timeoutMs}ms` }), timeoutMs);
1228
+ socket.setEncoding("utf8");
1229
+ socket.on("connect", () => socket.write(`${JSON.stringify(request)}
1230
+ `));
1231
+ socket.on("data", (chunk) => {
1232
+ buffer += chunk;
1233
+ const end = buffer.indexOf("\n");
1234
+ if (end < 0) {
1235
+ if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) finish({ state: "unknown", error: "reply too long" });
1236
+ return;
1237
+ }
1238
+ try {
1239
+ finish({ reply: JSON.parse(buffer.slice(0, end)) });
1240
+ } catch {
1241
+ finish({ state: "unknown", error: "unreadable reply" });
1242
+ }
1243
+ });
1244
+ socket.on("end", () => finish({ state: "unknown", error: "closed without a reply" }));
1245
+ socket.on("error", (err) => {
1246
+ if (err.code === "ENOENT" || err.code === "ECONNREFUSED") finish({ state: "absent" });
1247
+ else finish({ state: "unknown", error: err.code ?? err.message });
1248
+ });
1249
+ });
1250
+ }
1251
+ async function queryOwner(endpoint2, timeoutMs = 1e3) {
1252
+ const answer = await ask(endpoint2, { type: "status" }, timeoutMs);
1253
+ if (!("reply" in answer)) return answer;
1254
+ const reply = answer.reply;
1255
+ if (reply?.type === "status" && reply.protocol === CONTROL_PROTOCOL && Number.isInteger(reply.pid)) {
1256
+ return { state: "running", pid: reply.pid };
875
1257
  }
1258
+ return { state: "unknown", error: "unexpected reply" };
876
1259
  }
1260
+ var line = (message) => `${JSON.stringify(message)}
1261
+ `;
1262
+ function controlServer(owner) {
1263
+ const expectedProof = Buffer.from(stopProof(owner.token));
1264
+ return net2.createServer((socket) => {
1265
+ let buffer = "";
1266
+ socket.setEncoding("utf8");
1267
+ socket.on("error", () => {
1268
+ });
1269
+ const idle = setTimeout(() => socket.destroy(), REQUEST_IDLE_MS);
1270
+ socket.on("close", () => clearTimeout(idle));
1271
+ const onData = (chunk) => {
1272
+ buffer += chunk;
1273
+ const end = buffer.indexOf("\n");
1274
+ if (end < 0) {
1275
+ if (Buffer.byteLength(buffer) > MAX_LINE_BYTES) socket.destroy();
1276
+ return;
1277
+ }
1278
+ socket.off("data", onData);
1279
+ clearTimeout(idle);
1280
+ let request = null;
1281
+ try {
1282
+ request = JSON.parse(buffer.slice(0, end));
1283
+ } catch {
1284
+ }
1285
+ if (request?.type === "status") {
1286
+ socket.end(line({ type: "status", protocol: CONTROL_PROTOCOL, pid: owner.pid }));
1287
+ } else if (request?.type === "stop") {
1288
+ const proof = typeof request.proof === "string" ? Buffer.from(request.proof) : void 0;
1289
+ if (owner.token !== "" && proof && proof.length === expectedProof.length && crypto.timingSafeEqual(proof, expectedProof)) {
1290
+ socket.end(line({ type: "stopping", pid: owner.pid }), () => owner.onStop());
1291
+ } else {
1292
+ socket.end(line({ type: "error", error: "unauthorized" }));
1293
+ }
1294
+ } else {
1295
+ socket.end(line({ type: "error", error: "unknown request" }));
1296
+ }
1297
+ };
1298
+ socket.on("data", onData);
1299
+ });
1300
+ }
1301
+ function listenControl(endpoint2, owner) {
1302
+ const server = controlServer(owner);
1303
+ return new Promise((resolve) => {
1304
+ const onError = (err) => {
1305
+ resolve(err.code === "EADDRINUSE" ? { kind: "busy" } : { kind: "unknown", error: err.code ?? err.message });
1306
+ };
1307
+ server.once("error", onError);
1308
+ server.listen(endpoint2, () => {
1309
+ server.off("error", onError);
1310
+ server.on("error", () => {
1311
+ });
1312
+ resolve({ kind: "owner", close: () => new Promise((done) => server.close(() => done())) });
1313
+ });
1314
+ });
1315
+ }
1316
+ function settle(attempt, busy) {
1317
+ return attempt.kind === "busy" ? { kind: "unknown", error: busy } : attempt;
1318
+ }
1319
+ async function acquireLock(endpoint2, owner, deps = {}) {
1320
+ const platform = deps.platform ?? process.platform;
1321
+ const listen = deps.listen ?? listenControl;
1322
+ const query = deps.queryOwner ?? ((e) => queryOwner(e));
1323
+ if (platform !== "win32" && Buffer.byteLength(endpoint2) > MAX_SOCKET_PATH_BYTES) {
1324
+ return { kind: "unknown", error: `control socket path is too long: ${endpoint2}` };
1325
+ }
1326
+ const first = await listen(endpoint2, owner);
1327
+ if (first.kind !== "busy") return first;
1328
+ const state = await query(endpoint2);
1329
+ if (state.state === "running") return { kind: "held", pid: state.pid };
1330
+ if (state.state === "unknown") return { kind: "unknown", error: state.error };
1331
+ if (platform === "win32") return settle(await listen(endpoint2, owner), "the control pipe is in use but does not answer");
1332
+ return recoverSocket(endpoint2, owner, listen, query, deps.fs ?? fs6, deps.now ?? Date.now);
1333
+ }
1334
+ async function recoverSocket(endpoint2, owner, listen, query, f, now) {
1335
+ const guard = `${endpoint2}.lock`;
1336
+ const blocked = takeGuard(guard, f, now);
1337
+ if (blocked) return { kind: "unknown", error: blocked };
1338
+ try {
1339
+ const state = await query(endpoint2);
1340
+ if (state.state === "running") return { kind: "held", pid: state.pid };
1341
+ if (state.state === "unknown") return { kind: "unknown", error: state.error };
1342
+ try {
1343
+ f.unlinkSync(endpoint2);
1344
+ } catch (err) {
1345
+ if (err.code !== "ENOENT") return { kind: "unknown", error: err.message };
1346
+ }
1347
+ return settle(await listen(endpoint2, owner), "the control socket is in use but does not answer");
1348
+ } finally {
1349
+ try {
1350
+ f.rmdirSync(guard);
1351
+ } catch {
1352
+ }
1353
+ }
1354
+ }
1355
+ function takeGuard(guard, f, now) {
1356
+ for (let attempt = 0; attempt < 2; attempt++) {
1357
+ try {
1358
+ f.mkdirSync(guard);
1359
+ return void 0;
1360
+ } catch (err) {
1361
+ if (err.code !== "EEXIST") return err.message;
1362
+ }
1363
+ let age;
1364
+ try {
1365
+ age = now() - f.statSync(guard).mtimeMs;
1366
+ } catch {
1367
+ continue;
1368
+ }
1369
+ if (age <= STALE_GUARD_MS) return "another Codex adapter is starting";
1370
+ try {
1371
+ f.rmdirSync(guard);
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ return "another Codex adapter is starting";
1376
+ }
1377
+
1378
+ // src/codex/main.ts
1379
+ var WAIT_MIN_MS = 2e3;
1380
+ var WAIT_MAX_MS = 3e4;
877
1381
  installCrashGuard("codex adapter");
878
1382
  process.stdout.on("error", () => {
879
1383
  });
880
1384
  process.stderr.on("error", () => {
881
1385
  });
882
1386
  var config = loadConfig();
883
- var existing = fs4.existsSync(CODEX_PID_FILE) ? parseInt(fs4.readFileSync(CODEX_PID_FILE, "utf-8").trim(), 10) : NaN;
884
- if (!Number.isNaN(existing) && existing !== process.pid && isRunning(existing)) {
885
- logger.info(`Codex adapter already running (PID: ${existing})`);
886
- process.exit(0);
887
- }
888
- fs4.writeFileSync(CODEX_PID_FILE, String(process.pid), "utf-8");
889
- logStartup("Codex adapter");
890
- var host = process.env.CLAUDE_ALARM_HUB_HOST ?? (config.hub.host === "0.0.0.0" ? DEFAULT_HUB_HOST : config.hub.host);
891
- var port = process.env.CLAUDE_ALARM_HUB_PORT ? parseInt(process.env.CLAUDE_ALARM_HUB_PORT, 10) : config.hub.port;
892
- var token = process.env.CLAUDE_ALARM_HUB_TOKEN ?? config.hub.token;
893
- var adapter = new CodexAdapter({ command: config.codex?.command ?? "codex", hub: { host, port, token } });
894
- adapter.start();
1387
+ var supervised = process.argv.includes("--watch-stdin");
1388
+ var endpoint = controlEndpoint(CONFIG_DIR);
1389
+ var adapter;
1390
+ var release;
895
1391
  var exiting = false;
896
1392
  var shutdown = () => {
897
1393
  if (exiting) return;
898
1394
  exiting = true;
899
- adapter.stop();
900
- try {
901
- if (fs4.readFileSync(CODEX_PID_FILE, "utf-8").trim() === String(process.pid)) fs4.unlinkSync(CODEX_PID_FILE);
902
- } catch {
903
- }
904
- process.exit(0);
1395
+ adapter?.stop();
1396
+ const exit = () => process.exit(0);
1397
+ setTimeout(exit, 3e3).unref();
1398
+ if (release) release().then(exit, exit);
1399
+ else exit();
905
1400
  };
906
1401
  process.on("SIGINT", shutdown);
907
1402
  process.on("SIGTERM", shutdown);
908
- if (process.argv.includes("--watch-stdin")) {
1403
+ if (supervised) {
909
1404
  process.stdin.on("end", shutdown);
910
1405
  process.stdin.on("close", shutdown);
911
1406
  process.stdin.resume();
912
1407
  }
1408
+ function send(message, then) {
1409
+ if (!process.send || !process.connected) {
1410
+ then?.();
1411
+ return;
1412
+ }
1413
+ process.send(message, void 0, void 0, (err) => {
1414
+ if (err) logger.debug(`Report to the parent not delivered: ${err.message}`);
1415
+ then?.();
1416
+ });
1417
+ }
1418
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1419
+ async function takeLock() {
1420
+ let delay = WAIT_MIN_MS;
1421
+ let waitReason;
1422
+ for (; ; ) {
1423
+ const lock = await acquireLock(endpoint, {
1424
+ pid: process.pid,
1425
+ token: config.hub.token ?? "",
1426
+ onStop: () => {
1427
+ if (supervised) logger.warn("Stopped by claude-alarm codex stop; the hub will not start the Codex adapter again until the hub restarts");
1428
+ shutdown();
1429
+ }
1430
+ });
1431
+ if (lock.kind === "owner") {
1432
+ release = lock.close;
1433
+ if (waitReason !== void 0) logger.info("Codex adapter took over: the other adapter has stopped");
1434
+ return true;
1435
+ }
1436
+ if (!supervised) {
1437
+ if (lock.kind === "held") {
1438
+ logger.info(`Codex adapter already running (PID: ${lock.pid})`);
1439
+ send({ type: "codex-already-running", pid: lock.pid }, () => process.exit(0));
1440
+ } else {
1441
+ logger.error(`Codex adapter cannot start: ${lock.error}`);
1442
+ send({ type: "codex-lock-failed", error: lock.error }, () => process.exit(1));
1443
+ }
1444
+ return false;
1445
+ }
1446
+ const reason = lock.kind === "held" ? `held:${lock.pid}` : `unknown:${lock.error}`;
1447
+ if (reason !== waitReason) {
1448
+ waitReason = reason;
1449
+ if (lock.kind === "held") logger.info(`Another Codex adapter is running (PID: ${lock.pid}); this one takes over when it stops`);
1450
+ else logger.warn(`Codex adapter control endpoint is unavailable (${lock.error}); retrying`);
1451
+ }
1452
+ await sleep(delay);
1453
+ delay = Math.min(delay * 2, WAIT_MAX_MS);
1454
+ }
1455
+ }
1456
+ void takeLock().then((owned) => {
1457
+ if (!owned) return;
1458
+ logStartup("Codex adapter");
1459
+ const { host, port, token } = resolveAdapterHub(config);
1460
+ adapter = new CodexAdapter({
1461
+ command: config.codex?.command ?? "codex",
1462
+ hub: { host, port, token },
1463
+ onFirstConnect: (outcome) => send({ type: "codex-first-connect", ...outcome })
1464
+ });
1465
+ adapter.start();
1466
+ });
913
1467
  //# sourceMappingURL=main.js.map