@delt/claude-alarm 1.4.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) {
@@ -1068,6 +1114,7 @@ var init_codex_supervisor = __esm({
1068
1114
  "src/hub/codex-supervisor.ts"() {
1069
1115
  "use strict";
1070
1116
  init_logger();
1117
+ init_shutdown();
1071
1118
  HEALTHY_RUN_MS = 6e4;
1072
1119
  CodexSupervisor = class {
1073
1120
  constructor(script, opts = {}) {
@@ -1075,7 +1122,7 @@ var init_codex_supervisor = __esm({
1075
1122
  this.spawnFn = opts.spawnFn ?? spawn2;
1076
1123
  this.minDelayMs = opts.minDelayMs ?? 2e3;
1077
1124
  this.maxDelayMs = opts.maxDelayMs ?? 6e4;
1078
- this.stopGraceMs = opts.stopGraceMs ?? 3e3;
1125
+ this.stopGraceMs = opts.stopGraceMs ?? SUPERVISOR_STOP_GRACE_MS;
1079
1126
  this.env = opts.env;
1080
1127
  this.delay = this.minDelayMs;
1081
1128
  }
@@ -1098,11 +1145,22 @@ var init_codex_supervisor = __esm({
1098
1145
  this.timer = void 0;
1099
1146
  const child = this.child;
1100
1147
  this.child = void 0;
1101
- if (!child) return;
1102
- child.stdin?.end();
1103
- const force = setTimeout(() => child.kill(), this.stopGraceMs);
1104
- force.unref();
1105
- 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
+ });
1106
1164
  }
1107
1165
  launch() {
1108
1166
  const startedAt = Date.now();
@@ -1351,9 +1409,9 @@ var init_server = __esm({
1351
1409
  });
1352
1410
  }
1353
1411
  stop() {
1354
- return new Promise((resolve) => {
1355
- this.codexSupervisor?.stop();
1356
- this.codexSupervisor = void 0;
1412
+ const adapterStopped = this.codexSupervisor?.stop();
1413
+ this.codexSupervisor = void 0;
1414
+ const serverStopped = new Promise((resolve) => {
1357
1415
  if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
1358
1416
  if (this.telegramBot) this.telegramBot.stopPolling();
1359
1417
  for (const ws of this.channelSockets.values()) ws.terminate();
@@ -1373,6 +1431,8 @@ var init_server = __esm({
1373
1431
  resolve();
1374
1432
  }, 3e3);
1375
1433
  });
1434
+ return Promise.all([serverStopped, adapterStopped]).then(() => {
1435
+ });
1376
1436
  }
1377
1437
  // --- HTTP Handler ---
1378
1438
  handleHttp(req, res) {
@@ -2691,27 +2751,48 @@ var defaultSpawn = (command, args) => {
2691
2751
  shellSpawns.add(child);
2692
2752
  return child;
2693
2753
  };
2694
- var queryProcesses = () => new Promise((resolve, reject) => {
2695
- const script = "@(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; parentPid = [int]$_.ParentProcessId; creationTime = $_.CreationDate.ToUniversalTime().ToString('o') } }) | ConvertTo-Json -Compress";
2696
- execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
2697
- if (err) {
2698
- reject(err);
2699
- return;
2700
- }
2701
- try {
2702
- const rows = JSON.parse(stdout);
2703
- if (!Array.isArray(rows) || rows.some((row) => !Number.isInteger(row.pid) || !Number.isInteger(row.parentPid) || typeof row.creationTime !== "string")) {
2704
- throw new Error("invalid process snapshot");
2705
- }
2706
- resolve(rows);
2707
- } catch (error) {
2708
- reject(error);
2709
- }
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);
2710
2759
  });
2711
2760
  });
2712
- var runFile = (file, args, options, callback) => {
2713
- execFile(file, args, options, (err) => callback(err));
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;
2714
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();
2715
2796
  function descendantsOf(rootPid, rows) {
2716
2797
  const root = rows.find((row) => row.pid === rootPid);
2717
2798
  if (!root) return void 0;
@@ -2733,16 +2814,32 @@ function descendantsOf(rootPid, rows) {
2733
2814
  return found;
2734
2815
  }
2735
2816
  var exited = (child) => child.exitCode !== null || child.signalCode !== null;
2736
- function treeKiller(platform = process.platform, run = runFile, query = queryProcesses, throughShell = spawnedThroughShell) {
2737
- const recorded = /* @__PURE__ */ new WeakMap();
2738
- const stop = (pid) => new Promise((resolve) => {
2739
- run("taskkill", ["/PID", String(pid), "/F"], { windowsHide: true }, (err) => {
2740
- if (err) logger.debug(`taskkill ${pid} failed: ${err.message}`);
2741
- resolve();
2742
- });
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();
2743
2831
  });
2744
- const stopDeepestFirst = async (descendants) => {
2745
- for (const descendant of [...descendants].sort((a, b) => b.depth - a.depth)) await stop(descendant.pid);
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
+ }
2746
2843
  };
2747
2844
  const snapshot = async () => {
2748
2845
  try {
@@ -2758,20 +2855,12 @@ function treeKiller(platform = process.platform, run = runFile, query = queryPro
2758
2855
  if (!exited(child)) child.kill();
2759
2856
  return;
2760
2857
  }
2761
- if (!exited(child)) {
2762
- const rows = await snapshot();
2763
- if (!exited(child)) {
2764
- const descendants2 = rows && descendantsOf(child.pid, rows);
2765
- if (descendants2) await stopDeepestFirst(descendants2);
2766
- child.kill();
2767
- return;
2768
- }
2769
- }
2770
- const descendants = await recorded.get(child) ?? [];
2771
- if (!descendants.length) return;
2772
- const current = await snapshot();
2773
- if (!current) return;
2774
- await stopDeepestFirst(descendants.filter((d) => current.some((row) => row.pid === d.pid && row.creationTime === d.creationTime)));
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);
2775
2864
  };
2776
2865
  killer.track = async (child) => {
2777
2866
  if (!isShellTree(child) || exited(child)) return;
@@ -2779,8 +2868,7 @@ function treeKiller(platform = process.platform, run = runFile, query = queryPro
2779
2868
  const latest = (async () => {
2780
2869
  const rows = await snapshot();
2781
2870
  const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
2782
- const before = await earlier ?? [];
2783
- return [...before, ...found.filter((d) => !before.some((b) => b.pid === d.pid && b.creationTime === d.creationTime))];
2871
+ return merged(await earlier ?? [], found);
2784
2872
  })();
2785
2873
  recorded.set(child, latest);
2786
2874
  await latest;
@@ -2849,6 +2937,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2849
2937
  }
2850
2938
 
2851
2939
  // src/codex/run.ts
2940
+ init_shutdown();
2852
2941
  import path4 from "path";
2853
2942
 
2854
2943
  // src/codex/rpc.ts
@@ -3134,7 +3223,14 @@ async function runCodexTask(opts) {
3134
3223
  };
3135
3224
  } finally {
3136
3225
  for (const t of timers) clearTimeout(t);
3137
- await 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
+ }
3138
3234
  }
3139
3235
  }
3140
3236
  var VALUE_FLAGS = {
@@ -3207,6 +3303,7 @@ async function runFromCli(argv, io) {
3207
3303
  }
3208
3304
 
3209
3305
  // src/cli.ts
3306
+ init_shutdown();
3210
3307
  init_constants();
3211
3308
  init_logger();
3212
3309
  init_crash_guard();
@@ -3958,8 +4055,10 @@ async function main2() {
3958
4055
  else if (sub === "start") await codexStart();
3959
4056
  else if (sub === "stop") process.exitCode = await stopAdapter(controlDeps(), loadConfig().codex?.enabled === true);
3960
4057
  else if (sub === "status") process.exitCode = await adapterStatus(controlDeps(), loadConfig().codex?.enabled === true);
3961
- else if (sub === "run") process.exitCode = await codexRun(args.slice(2));
3962
- else {
4058
+ else if (sub === "run") {
4059
+ const code = await codexRun(args.slice(2));
4060
+ exitAfterFlush(code, process.stdout, process.stderr);
4061
+ } else {
3963
4062
  console.error(`Unknown codex command: ${sub}`);
3964
4063
  printUsage();
3965
4064
  process.exit(1);