@delt/claude-alarm 1.3.0 → 1.4.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.
package/dist/cli.js CHANGED
@@ -146,6 +146,52 @@ var init_logger = __esm({
146
146
  }
147
147
  });
148
148
 
149
+ // src/codex/shutdown.ts
150
+ async function closeWithinLimit(close) {
151
+ let timer;
152
+ try {
153
+ await Promise.race([
154
+ Promise.resolve().then(close),
155
+ new Promise((_resolve, reject) => {
156
+ timer = setTimeout(() => reject(new Error("Codex proxy cleanup timed out")), RUN_CLOSE_MS);
157
+ })
158
+ ]);
159
+ } finally {
160
+ clearTimeout(timer);
161
+ }
162
+ }
163
+ function exitAfterFlush(code, stdout, stderr, exit = process.exit) {
164
+ let exited2 = false;
165
+ let remaining = 2;
166
+ const finish = () => {
167
+ if (exited2) return;
168
+ exited2 = true;
169
+ clearTimeout(timer);
170
+ exit(code);
171
+ };
172
+ const timer = setTimeout(finish, CLI_EXIT_FLUSH_MS);
173
+ const flushed = () => {
174
+ if (--remaining === 0) finish();
175
+ };
176
+ for (const stream of [stdout, stderr]) {
177
+ try {
178
+ stream.write("", flushed);
179
+ } catch {
180
+ flushed();
181
+ }
182
+ }
183
+ }
184
+ var RUN_CLOSE_MS, SUPERVISOR_STOP_GRACE_MS, CLI_EXIT_FLUSH_MS;
185
+ var init_shutdown = __esm({
186
+ "src/codex/shutdown.ts"() {
187
+ "use strict";
188
+ init_logger();
189
+ RUN_CLOSE_MS = 15e3;
190
+ SUPERVISOR_STOP_GRACE_MS = 3e4;
191
+ CLI_EXIT_FLUSH_MS = 1e3;
192
+ }
193
+ });
194
+
149
195
  // src/shared/crash-guard.ts
150
196
  import fs3 from "fs";
151
197
  function appendToLog(text) {
@@ -301,7 +347,7 @@ var init_session_manager = __esm({
301
347
 
302
348
  // src/hub/notifier.ts
303
349
  import notifier from "node-notifier";
304
- import { execFile } from "child_process";
350
+ import { execFile as execFile2 } from "child_process";
305
351
  var Notifier;
306
352
  var init_notifier = __esm({
307
353
  "src/hub/notifier.ts"() {
@@ -362,7 +408,7 @@ var init_notifier = __esm({
362
408
  }
363
409
  checkWindowsNotifications() {
364
410
  return new Promise((resolve) => {
365
- execFile(
411
+ execFile2(
366
412
  "powershell",
367
413
  ["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
368
414
  { windowsHide: true },
@@ -383,7 +429,7 @@ var init_notifier = __esm({
383
429
  logger.warn("Windows notifications are disabled. Opening notification settings...");
384
430
  logger.warn("Please enable notifications for this app, then try again.");
385
431
  if (process.platform === "win32") {
386
- execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
432
+ execFile2("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
387
433
  }
388
434
  setTimeout(() => {
389
435
  this.notificationSettingsOpened = false;
@@ -445,7 +491,7 @@ import { randomUUID as randomUUID2 } from "crypto";
445
491
  function visibleLength(html) {
446
492
  return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
447
493
  }
448
- var TELEGRAM_API, MAX_CHOICE_MESSAGES, MAX_SELECTIONS, MAX_VISIBLE_CHARS, TRUNCATED, MAX_PHOTO_BYTES, TelegramBot;
494
+ var TELEGRAM_API, MAX_CHOICE_MESSAGES, MAX_SELECTIONS, MAX_VISIBLE_CHARS, TRUNCATED, MAX_PHOTO_BYTES, NO_LONGER_CONNECTED, TelegramBot;
449
495
  var init_telegram = __esm({
450
496
  "src/hub/telegram.ts"() {
451
497
  "use strict";
@@ -459,6 +505,7 @@ var init_telegram = __esm({
459
505
  MAX_VISIBLE_CHARS = 4e3;
460
506
  TRUNCATED = "\u2026(truncated)";
461
507
  MAX_PHOTO_BYTES = 10 * 1024 * 1024;
508
+ NO_LONGER_CONNECTED = "the session is no longer connected";
462
509
  TelegramBot = class _TelegramBot {
463
510
  config;
464
511
  offset = 0;
@@ -629,13 +676,7 @@ ${TRUNCATED}`);
629
676
  this.selections.delete(latest);
630
677
  const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
631
678
  if (session) {
632
- let delivered = true;
633
- if (pending.photoFileId) {
634
- delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
635
- } else if (pending.text) {
636
- this.deliverToSession(session.id, pending.text);
637
- }
638
- if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
679
+ if (await this.deliverPending(session.id, pending)) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
639
680
  } else {
640
681
  this.sendMessage("Invalid session number.");
641
682
  }
@@ -677,9 +718,13 @@ ${TRUNCATED}`);
677
718
  this.sendMessage("Multiple sessions active. Select one:", void 0, { inline_keyboard: rows });
678
719
  }
679
720
  deliverToSession(sessionId2, content) {
680
- if (this.onMessageToSession) {
681
- this.onMessageToSession(sessionId2, content);
682
- }
721
+ const delivered = this.onMessageToSession?.(sessionId2, content) === true;
722
+ if (!delivered) void this.sendMessage(`Not delivered: ${NO_LONGER_CONNECTED}`);
723
+ return delivered;
724
+ }
725
+ async deliverPending(sessionId2, pending) {
726
+ if (pending.photoFileId) return this.deliverPhotoToSessionByFileId(sessionId2, pending.photoFileId, pending.caption);
727
+ return pending.text ? this.deliverToSession(sessionId2, pending.text) : false;
683
728
  }
684
729
  async deliverPhotoToSession(sessionId2, photos, caption) {
685
730
  const largest = photos[photos.length - 1];
@@ -711,8 +756,19 @@ ${TRUNCATED}`);
711
756
  const filePath = path6.join(UPLOADS_DIR, filename);
712
757
  fs5.writeFileSync(filePath, buffer);
713
758
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
714
- if (this.onImageToSession) {
715
- this.onImageToSession(sessionId2, filePath, mimeType, caption);
759
+ let accepted = false;
760
+ try {
761
+ accepted = this.onImageToSession?.(sessionId2, filePath, mimeType, caption) === true;
762
+ } catch (err) {
763
+ logger.warn(`Telegram photo hand-off failed: ${err.message}`);
764
+ }
765
+ if (!accepted) {
766
+ try {
767
+ fs5.unlinkSync(filePath);
768
+ } catch {
769
+ }
770
+ this.photoNotDelivered(NO_LONGER_CONNECTED);
771
+ return false;
716
772
  }
717
773
  setTimeout(() => {
718
774
  try {
@@ -720,7 +776,7 @@ ${TRUNCATED}`);
720
776
  } catch {
721
777
  }
722
778
  }, 5 * 60 * 1e3).unref();
723
- return this.onImageToSession !== void 0;
779
+ return true;
724
780
  } catch (err) {
725
781
  logger.warn(`Telegram photo download failed: ${err.message}`);
726
782
  this.photoNotDelivered("the download failed");
@@ -915,15 +971,10 @@ ${label}`);
915
971
  return;
916
972
  }
917
973
  this.selections.delete(selectionId);
918
- if (pending.photoFileId) {
919
- const delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
920
- if (!delivered) {
921
- await this.answerCallbackQuery(query.id, "Photo not delivered");
922
- if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
923
- return;
924
- }
925
- } else if (pending.text) {
926
- this.deliverToSession(session.id, pending.text);
974
+ if (!await this.deliverPending(session.id, pending)) {
975
+ await this.answerCallbackQuery(query.id, pending.photoFileId ? "Photo not delivered" : "Not delivered");
976
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
977
+ return;
927
978
  }
928
979
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
929
980
  if (query.message) {
@@ -1063,6 +1114,7 @@ var init_codex_supervisor = __esm({
1063
1114
  "src/hub/codex-supervisor.ts"() {
1064
1115
  "use strict";
1065
1116
  init_logger();
1117
+ init_shutdown();
1066
1118
  HEALTHY_RUN_MS = 6e4;
1067
1119
  CodexSupervisor = class {
1068
1120
  constructor(script, opts = {}) {
@@ -1070,7 +1122,7 @@ var init_codex_supervisor = __esm({
1070
1122
  this.spawnFn = opts.spawnFn ?? spawn2;
1071
1123
  this.minDelayMs = opts.minDelayMs ?? 2e3;
1072
1124
  this.maxDelayMs = opts.maxDelayMs ?? 6e4;
1073
- this.stopGraceMs = opts.stopGraceMs ?? 3e3;
1125
+ this.stopGraceMs = opts.stopGraceMs ?? SUPERVISOR_STOP_GRACE_MS;
1074
1126
  this.env = opts.env;
1075
1127
  this.delay = this.minDelayMs;
1076
1128
  }
@@ -1093,11 +1145,22 @@ var init_codex_supervisor = __esm({
1093
1145
  this.timer = void 0;
1094
1146
  const child = this.child;
1095
1147
  this.child = void 0;
1096
- if (!child) return;
1097
- child.stdin?.end();
1098
- const force = setTimeout(() => child.kill(), this.stopGraceMs);
1099
- force.unref();
1100
- child.once("exit", () => clearTimeout(force));
1148
+ if (!child) return Promise.resolve();
1149
+ return new Promise((resolve) => {
1150
+ const done = () => {
1151
+ clearTimeout(force);
1152
+ child.removeListener("exit", done);
1153
+ resolve();
1154
+ };
1155
+ const force = setTimeout(() => {
1156
+ logger.warn(`Codex adapter did not exit within ${this.stopGraceMs}ms of stop; killing it`);
1157
+ child.kill();
1158
+ done();
1159
+ }, this.stopGraceMs);
1160
+ force.unref();
1161
+ child.once("exit", done);
1162
+ child.stdin?.end();
1163
+ });
1101
1164
  }
1102
1165
  launch() {
1103
1166
  const startedAt = Date.now();
@@ -1346,9 +1409,9 @@ var init_server = __esm({
1346
1409
  });
1347
1410
  }
1348
1411
  stop() {
1349
- return new Promise((resolve) => {
1350
- this.codexSupervisor?.stop();
1351
- this.codexSupervisor = void 0;
1412
+ const adapterStopped = this.codexSupervisor?.stop();
1413
+ this.codexSupervisor = void 0;
1414
+ const serverStopped = new Promise((resolve) => {
1352
1415
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
1353
1416
  if (this.telegramBot) this.telegramBot.stopPolling();
1354
1417
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1368,6 +1431,8 @@ var init_server = __esm({
1368
1431
  resolve();
1369
1432
  }, 3e3);
1370
1433
  });
1434
+ return Promise.all([serverStopped, adapterStopped]).then(() => {
1435
+ });
1371
1436
  }
1372
1437
  // --- HTTP Handler ---
1373
1438
  handleHttp(req, res) {
@@ -1624,7 +1689,16 @@ var init_server = __esm({
1624
1689
  this.sessions.updateActivity(msg.sessionId);
1625
1690
  const notifySession = this.sessions.get(msg.sessionId);
1626
1691
  const notifyLabel = this.getSessionLabel(notifySession);
1627
- this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
1692
+ const notifyTitle = `[${notifyLabel}] ${msg.title}`;
1693
+ if (msg.to === "api") {
1694
+ logger.info(`${notifyTitle}: ${msg.message}`);
1695
+ break;
1696
+ }
1697
+ if (msg.to === "telegram") {
1698
+ void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
1699
+ break;
1700
+ }
1701
+ if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
1628
1702
  this.broadcastToDashboards({
1629
1703
  type: "notification",
1630
1704
  sessionId: msg.sessionId,
@@ -1826,6 +1900,11 @@ var init_server = __esm({
1826
1900
  const channelWs = this.channelSockets.get(msg.sessionId);
1827
1901
  if (channelWs?.readyState === WebSocket2.OPEN) {
1828
1902
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1903
+ } else {
1904
+ const reason = "the session is not connected";
1905
+ logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
1906
+ const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
1907
+ if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify(rejected));
1829
1908
  }
1830
1909
  } else if (msg.type === "image_upload") {
1831
1910
  this.handleImageUpload(ws, msg);
@@ -1951,19 +2030,25 @@ var init_server = __esm({
1951
2030
  this.telegramBot.getSessions = () => this.sessions.getAll();
1952
2031
  this.telegramBot.onMessageToSession = (sessionId2, content) => {
1953
2032
  const channelWs = this.channelSockets.get(sessionId2);
1954
- if (channelWs?.readyState === WebSocket2.OPEN) {
1955
- const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
1956
- channelWs.send(JSON.stringify(msg));
1957
- logger.info(`Telegram message forwarded to session: ${sessionId2}`);
2033
+ if (channelWs?.readyState !== WebSocket2.OPEN) {
2034
+ logger.warn(`Telegram message not delivered to ${sessionId2}: the session is not connected`);
2035
+ return false;
1958
2036
  }
2037
+ const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
2038
+ channelWs.send(JSON.stringify(msg));
2039
+ logger.info(`Telegram message forwarded to session: ${sessionId2}`);
2040
+ return true;
1959
2041
  };
1960
2042
  this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
1961
2043
  const channelWs = this.channelSockets.get(sessionId2);
1962
- if (channelWs?.readyState === WebSocket2.OPEN) {
1963
- const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
1964
- channelWs.send(JSON.stringify(msg));
1965
- logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
2044
+ if (channelWs?.readyState !== WebSocket2.OPEN) {
2045
+ logger.warn(`Telegram photo not delivered to ${sessionId2}: the session is not connected`);
2046
+ return false;
1966
2047
  }
2048
+ const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
2049
+ channelWs.send(JSON.stringify(msg));
2050
+ logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
2051
+ return true;
1967
2052
  };
1968
2053
  this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
1969
2054
  if (this.forwardPermissionResponse({ sessionId: sessionId2, requestId, behavior })) {
@@ -2627,7 +2712,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
2627
2712
 
2628
2713
  // src/codex/transport.ts
2629
2714
  init_logger();
2630
- import { spawn } from "child_process";
2715
+ import { execFile, spawn } from "child_process";
2631
2716
  import fs2 from "fs";
2632
2717
  import path3 from "path";
2633
2718
  import { Duplex } from "stream";
@@ -2657,14 +2742,144 @@ function findCodex(command, platform = process.platform, env = process.env) {
2657
2742
  ];
2658
2743
  return candidates.find((file) => !!file && fs2.existsSync(file));
2659
2744
  }
2745
+ var shellSpawns = /* @__PURE__ */ new WeakSet();
2746
+ var spawnedThroughShell = (child) => shellSpawns.has(child);
2660
2747
  var defaultSpawn = (command, args) => {
2661
2748
  const { file, shell } = resolveCommand(command);
2662
- if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
2663
- return spawn(file, args, { stdio: "pipe", windowsHide: true });
2749
+ if (!shell) return spawn(file, args, { stdio: "pipe", windowsHide: true });
2750
+ const child = spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
2751
+ shellSpawns.add(child);
2752
+ return child;
2753
+ };
2754
+ var POWERSHELL_TIMEOUT_MS = 5e3;
2755
+ var powershell = (script, env) => new Promise((resolve, reject) => {
2756
+ execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: POWERSHELL_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, env }, (err, stdout) => {
2757
+ if (err) reject(err);
2758
+ else resolve(stdout);
2759
+ });
2760
+ });
2761
+ var QUERY_SCRIPT = "@(Get-CimInstance Win32_Process | Where-Object CreationDate | Select-Object @{ n = 'pid'; e = { [int]$_.ProcessId } }, @{ n = 'parentPid'; e = { [int]$_.ParentProcessId } }, @{ n = 'creationTime'; e = { $_.CreationDate.ToUniversalTime().ToString('o') } }) | ConvertTo-Json -Compress";
2762
+ var processQuery = (prelude = "") => async () => {
2763
+ const rows = JSON.parse(await powershell(prelude + QUERY_SCRIPT));
2764
+ if (!Array.isArray(rows) || rows.some((row) => !Number.isInteger(row.pid) || !Number.isInteger(row.parentPid) || typeof row.creationTime !== "string")) {
2765
+ throw new Error("invalid process snapshot");
2766
+ }
2767
+ return rows;
2664
2768
  };
2769
+ var queryProcesses = processQuery();
2770
+ var END_SCRIPT = [
2771
+ "foreach ($t in ($env:CLAUDE_ALARM_END_TARGETS | ConvertFrom-Json)) {",
2772
+ " $p = Get-Process -Id $t.pid -ErrorAction SilentlyContinue",
2773
+ // Reading Handle opens the process once, and StartTime, Stop-Process and Wait-Process then use that handle; a getter that throws reads as $null.
2774
+ ' if ($null -eq $p -or $null -eq $p.Handle) { "$($t.pid) is gone or cannot be opened"; continue }',
2775
+ " $start = $p.StartTime",
2776
+ ' if ($null -eq $start) { "$($t.pid) has no readable creation time"; continue }',
2777
+ " $start = $start.ToUniversalTime()",
2778
+ // Win32_Process creation dates stop at microseconds.
2779
+ ` if ($start.AddTicks(-($start.Ticks % 10)).ToString('o') -ne $t.creationTime) { "$($t.pid) now belongs to another process"; continue }`,
2780
+ ' try { Stop-Process -InputObject $p -Force -ErrorAction Stop } catch { "$($t.pid) not ended: $($_.FullyQualifiedErrorId)" }',
2781
+ // A refused termination usually means the process is already exiting, so it is waited for all the same.
2782
+ ' try { Wait-Process -InputObject $p -Timeout 1 -ErrorAction Stop } catch { "$($t.pid) has not exited yet" }',
2783
+ "}"
2784
+ ].join("\n");
2785
+ var processEnder = (prelude = "") => async (targets) => {
2786
+ await Promise.all(targets.map(async (target) => {
2787
+ try {
2788
+ const notes = await powershell(prelude + END_SCRIPT, { ...process.env, CLAUDE_ALARM_END_TARGETS: JSON.stringify([target]) });
2789
+ for (const note of notes.split(/\r?\n/)) if (note) logger.debug(`codex process ${note}`);
2790
+ } catch (err) {
2791
+ logger.debug(`ending codex process ${target.pid} failed: ${err.killed ? "timed out" : String(err)}`);
2792
+ }
2793
+ }));
2794
+ };
2795
+ var endProcesses = processEnder();
2796
+ function descendantsOf(rootPid, rows) {
2797
+ const root = rows.find((row) => row.pid === rootPid);
2798
+ if (!root) return void 0;
2799
+ const found = [];
2800
+ const seen = /* @__PURE__ */ new Set([root.pid]);
2801
+ let level = [root];
2802
+ for (let depth = 1; level.length; depth++) {
2803
+ const next = [];
2804
+ for (const parent of level) {
2805
+ for (const row of rows) {
2806
+ if (row.parentPid !== parent.pid || seen.has(row.pid) || row.creationTime < parent.creationTime) continue;
2807
+ seen.add(row.pid);
2808
+ next.push(row);
2809
+ found.push({ ...row, depth });
2810
+ }
2811
+ }
2812
+ level = next;
2813
+ }
2814
+ return found;
2815
+ }
2816
+ var exited = (child) => child.exitCode !== null || child.signalCode !== null;
2817
+ var SHELL_EXIT_WAIT_MS = 1e3;
2818
+ var PROXY_TREE_CLOSE_BUDGET_MS = 2 * POWERSHELL_TIMEOUT_MS + SHELL_EXIT_WAIT_MS;
2819
+ var exitOf = (child) => new Promise((resolve) => {
2820
+ if (exited(child)) {
2821
+ resolve();
2822
+ return;
2823
+ }
2824
+ const timer = setTimeout(() => {
2825
+ logger.debug(`codex proxy shell ${child.pid} has not exited yet`);
2826
+ resolve();
2827
+ }, SHELL_EXIT_WAIT_MS);
2828
+ child.once("exit", () => {
2829
+ clearTimeout(timer);
2830
+ resolve();
2831
+ });
2832
+ });
2833
+ var merged = (before, found) => [...before, ...found.filter((d) => !before.some((b) => b.pid === d.pid && b.creationTime === d.creationTime))];
2834
+ function treeKiller(platform = process.platform, end = endProcesses, query = queryProcesses, throughShell = spawnedThroughShell) {
2835
+ const recorded = /* @__PURE__ */ new WeakMap();
2836
+ const endByIdentity = async (descendants) => {
2837
+ if (!descendants.length) return;
2838
+ try {
2839
+ await end([...descendants].sort((a, b) => b.depth - a.depth).map(({ depth, ...identity }) => identity));
2840
+ } catch (err) {
2841
+ logger.debug(`ending codex processes failed: ${String(err)}`);
2842
+ }
2843
+ };
2844
+ const snapshot = async () => {
2845
+ try {
2846
+ return await query();
2847
+ } catch (err) {
2848
+ logger.debug(`codex process query failed: ${String(err)}`);
2849
+ return void 0;
2850
+ }
2851
+ };
2852
+ const isShellTree = (child) => platform === "win32" && child.pid !== void 0 && throughShell(child);
2853
+ const killer = async (child) => {
2854
+ if (!isShellTree(child)) {
2855
+ if (!exited(child)) child.kill();
2856
+ return;
2857
+ }
2858
+ const [rows, before] = await Promise.all([exited(child) ? void 0 : snapshot(), recorded.get(child)]);
2859
+ const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
2860
+ await endByIdentity(merged(before ?? [], found));
2861
+ if (exited(child)) return;
2862
+ child.kill();
2863
+ await exitOf(child);
2864
+ };
2865
+ killer.track = async (child) => {
2866
+ if (!isShellTree(child) || exited(child)) return;
2867
+ const earlier = recorded.get(child);
2868
+ const latest = (async () => {
2869
+ const rows = await snapshot();
2870
+ const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
2871
+ return merged(await earlier ?? [], found);
2872
+ })();
2873
+ recorded.set(child, latest);
2874
+ await latest;
2875
+ };
2876
+ return killer;
2877
+ }
2878
+ var killTree = treeKiller();
2665
2879
  var HANDSHAKE_TIMEOUT_MS = 1e4;
2666
- function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
2880
+ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS, killTreeFn = killTree) {
2667
2881
  const child = spawnFn(command, ["app-server", "proxy"]);
2882
+ void killTreeFn.track?.(child);
2668
2883
  child.stdin?.on("error", () => {
2669
2884
  });
2670
2885
  child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
@@ -2691,10 +2906,12 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2691
2906
  const ws = new WebSocket("ws://localhost/", {
2692
2907
  createConnection: (() => stream)
2693
2908
  });
2694
- const close = () => {
2909
+ let closing;
2910
+ const close = () => closing ??= (async () => {
2695
2911
  ws.terminate();
2696
- child.kill();
2697
- };
2912
+ child.stdin?.end();
2913
+ await killTreeFn(child);
2914
+ })();
2698
2915
  return new Promise((resolve, reject) => {
2699
2916
  let settled = false;
2700
2917
  const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
@@ -2702,8 +2919,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2702
2919
  if (settled) return;
2703
2920
  settled = true;
2704
2921
  clearTimeout(timer);
2705
- close();
2706
- reject(err);
2922
+ void close().then(() => reject(err), () => reject(err));
2707
2923
  };
2708
2924
  child.on("error", fail);
2709
2925
  child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
@@ -2714,12 +2930,14 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2714
2930
  clearTimeout(timer);
2715
2931
  child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
2716
2932
  ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
2933
+ void killTreeFn.track?.(child);
2717
2934
  resolve({ ws, close });
2718
2935
  });
2719
2936
  });
2720
2937
  }
2721
2938
 
2722
2939
  // src/codex/run.ts
2940
+ init_shutdown();
2723
2941
  import path4 from "path";
2724
2942
 
2725
2943
  // src/codex/rpc.ts
@@ -3005,7 +3223,14 @@ async function runCodexTask(opts) {
3005
3223
  };
3006
3224
  } finally {
3007
3225
  for (const t of timers) clearTimeout(t);
3008
- conn.close();
3226
+ try {
3227
+ await closeWithinLimit(() => conn.close());
3228
+ } catch (err) {
3229
+ try {
3230
+ progress(`warning: Codex proxy cleanup failed: ${err.message}`);
3231
+ } catch {
3232
+ }
3233
+ }
3009
3234
  }
3010
3235
  }
3011
3236
  var VALUE_FLAGS = {
@@ -3078,6 +3303,7 @@ async function runFromCli(argv, io) {
3078
3303
  }
3079
3304
 
3080
3305
  // src/cli.ts
3306
+ init_shutdown();
3081
3307
  init_constants();
3082
3308
  init_logger();
3083
3309
  init_crash_guard();
@@ -3496,19 +3722,19 @@ async function hubStart(daemon) {
3496
3722
  process.exit(1);
3497
3723
  }
3498
3724
  fs9.writeFileSync(PID_FILE, String(pid), "utf-8");
3499
- let exited = false;
3725
+ let exited2 = false;
3500
3726
  child.once("exit", () => {
3501
- exited = true;
3727
+ exited2 = true;
3502
3728
  });
3503
3729
  child.unref();
3504
3730
  const startup = await waitForHub({
3505
3731
  url: `http://${displayHost}:${port}/api/status`,
3506
3732
  token: config2.hub.token,
3507
3733
  pid,
3508
- isAlive: () => !exited
3734
+ isAlive: () => !exited2
3509
3735
  });
3510
3736
  if (startup !== "ready") {
3511
- if (!exited) child.kill();
3737
+ if (!exited2) child.kill();
3512
3738
  if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
3513
3739
  console.error(startup === "exited" ? `Hub exited during startup. See ${LOG_FILE}` : `Hub did not answer at http://${displayHost}:${port} within ${HUB_START_TIMEOUT_MS / 1e3}s and was stopped. See ${LOG_FILE}`);
3514
3740
  process.exit(1);
@@ -3829,8 +4055,10 @@ async function main2() {
3829
4055
  else if (sub === "start") await codexStart();
3830
4056
  else if (sub === "stop") process.exitCode = await stopAdapter(controlDeps(), loadConfig().codex?.enabled === true);
3831
4057
  else if (sub === "status") process.exitCode = await adapterStatus(controlDeps(), loadConfig().codex?.enabled === true);
3832
- else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
3833
- else {
4058
+ else if (sub === "run") {
4059
+ const code = await codexRun(args.slice(2));
4060
+ exitAfterFlush(code, process.stdout, process.stderr);
4061
+ } else {
3834
4062
  console.error(`Unknown codex command: ${sub}`);
3835
4063
  printUsage();
3836
4064
  process.exit(1);