@delt/claude-alarm 1.3.0 → 1.4.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.
@@ -357,7 +357,7 @@ var HubClient = class {
357
357
  };
358
358
 
359
359
  // src/codex/transport.ts
360
- import { spawn } from "child_process";
360
+ import { execFile, spawn } from "child_process";
361
361
  import fs3 from "fs";
362
362
  import path3 from "path";
363
363
  import { Duplex } from "stream";
@@ -387,14 +387,116 @@ function findCodex(command, platform = process.platform, env = process.env) {
387
387
  ];
388
388
  return candidates.find((file) => !!file && fs3.existsSync(file));
389
389
  }
390
+ var shellSpawns = /* @__PURE__ */ new WeakSet();
391
+ var spawnedThroughShell = (child) => shellSpawns.has(child);
390
392
  var defaultSpawn = (command, args) => {
391
393
  const { file, shell } = resolveCommand(command);
392
- if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
393
- return spawn(file, args, { stdio: "pipe", windowsHide: true });
394
+ if (!shell) return spawn(file, args, { stdio: "pipe", windowsHide: true });
395
+ const child = spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
396
+ shellSpawns.add(child);
397
+ return child;
394
398
  };
399
+ var queryProcesses = () => new Promise((resolve, reject) => {
400
+ const script = "@(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; parentPid = [int]$_.ParentProcessId; creationTime = $_.CreationDate.ToUniversalTime().ToString('o') } }) | ConvertTo-Json -Compress";
401
+ execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
402
+ if (err) {
403
+ reject(err);
404
+ return;
405
+ }
406
+ try {
407
+ const rows = JSON.parse(stdout);
408
+ if (!Array.isArray(rows) || rows.some((row) => !Number.isInteger(row.pid) || !Number.isInteger(row.parentPid) || typeof row.creationTime !== "string")) {
409
+ throw new Error("invalid process snapshot");
410
+ }
411
+ resolve(rows);
412
+ } catch (error) {
413
+ reject(error);
414
+ }
415
+ });
416
+ });
417
+ var runFile = (file, args, options, callback) => {
418
+ execFile(file, args, options, (err) => callback(err));
419
+ };
420
+ function descendantsOf(rootPid, rows) {
421
+ const root = rows.find((row) => row.pid === rootPid);
422
+ if (!root) return void 0;
423
+ const found = [];
424
+ const seen = /* @__PURE__ */ new Set([root.pid]);
425
+ let level = [root];
426
+ for (let depth = 1; level.length; depth++) {
427
+ const next = [];
428
+ for (const parent of level) {
429
+ for (const row of rows) {
430
+ if (row.parentPid !== parent.pid || seen.has(row.pid) || row.creationTime < parent.creationTime) continue;
431
+ seen.add(row.pid);
432
+ next.push(row);
433
+ found.push({ ...row, depth });
434
+ }
435
+ }
436
+ level = next;
437
+ }
438
+ return found;
439
+ }
440
+ var exited = (child) => child.exitCode !== null || child.signalCode !== null;
441
+ function treeKiller(platform = process.platform, run = runFile, query = queryProcesses, throughShell = spawnedThroughShell) {
442
+ const recorded = /* @__PURE__ */ new WeakMap();
443
+ const stop = (pid) => new Promise((resolve) => {
444
+ run("taskkill", ["/PID", String(pid), "/F"], { windowsHide: true }, (err) => {
445
+ if (err) logger.debug(`taskkill ${pid} failed: ${err.message}`);
446
+ resolve();
447
+ });
448
+ });
449
+ const stopDeepestFirst = async (descendants) => {
450
+ for (const descendant of [...descendants].sort((a, b) => b.depth - a.depth)) await stop(descendant.pid);
451
+ };
452
+ const snapshot = async () => {
453
+ try {
454
+ return await query();
455
+ } catch (err) {
456
+ logger.debug(`codex process query failed: ${String(err)}`);
457
+ return void 0;
458
+ }
459
+ };
460
+ const isShellTree = (child) => platform === "win32" && child.pid !== void 0 && throughShell(child);
461
+ const killer = async (child) => {
462
+ if (!isShellTree(child)) {
463
+ if (!exited(child)) child.kill();
464
+ return;
465
+ }
466
+ if (!exited(child)) {
467
+ const rows = await snapshot();
468
+ if (!exited(child)) {
469
+ const descendants2 = rows && descendantsOf(child.pid, rows);
470
+ if (descendants2) await stopDeepestFirst(descendants2);
471
+ child.kill();
472
+ return;
473
+ }
474
+ }
475
+ const descendants = await recorded.get(child) ?? [];
476
+ if (!descendants.length) return;
477
+ const current = await snapshot();
478
+ if (!current) return;
479
+ await stopDeepestFirst(descendants.filter((d) => current.some((row) => row.pid === d.pid && row.creationTime === d.creationTime)));
480
+ };
481
+ killer.track = async (child) => {
482
+ if (!isShellTree(child) || exited(child)) return;
483
+ const earlier = recorded.get(child);
484
+ const latest = (async () => {
485
+ const rows = await snapshot();
486
+ const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
487
+ const before = await earlier ?? [];
488
+ return [...before, ...found.filter((d) => !before.some((b) => b.pid === d.pid && b.creationTime === d.creationTime))];
489
+ })();
490
+ recorded.set(child, latest);
491
+ await latest;
492
+ };
493
+ return killer;
494
+ }
495
+ var killTree = treeKiller();
395
496
  var HANDSHAKE_TIMEOUT_MS = 1e4;
396
- function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
497
+ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS, killTreeFn = killTree) {
397
498
  const child = spawnFn(command, ["app-server", "proxy"]);
499
+ void killTreeFn.track?.(child);
398
500
  child.stdin?.on("error", () => {
399
501
  });
400
502
  child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
@@ -421,10 +523,12 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
421
523
  const ws = new WebSocket3("ws://localhost/", {
422
524
  createConnection: (() => stream)
423
525
  });
424
- const close = () => {
526
+ let closing;
527
+ const close = () => closing ??= (async () => {
425
528
  ws.terminate();
426
- child.kill();
427
- };
529
+ child.stdin?.end();
530
+ await killTreeFn(child);
531
+ })();
428
532
  return new Promise((resolve, reject) => {
429
533
  let settled = false;
430
534
  const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
@@ -432,8 +536,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
432
536
  if (settled) return;
433
537
  settled = true;
434
538
  clearTimeout(timer);
435
- close();
436
- reject(err);
539
+ void close().then(() => reject(err), () => reject(err));
437
540
  };
438
541
  child.on("error", fail);
439
542
  child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
@@ -444,6 +547,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
444
547
  clearTimeout(timer);
445
548
  child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
446
549
  ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
550
+ void killTreeFn.track?.(child);
447
551
  resolve({ ws, close });
448
552
  });
449
553
  });
@@ -653,6 +757,8 @@ var CodexAdapter = class {
653
757
  this.link = new CodexHubLink(opts.hub, { id: randomUUID2(), host: this.hostName }, (call) => this.onCall(call), opts.linkReconnectMs);
654
758
  }
655
759
  conn;
760
+ connecting;
761
+ closing = /* @__PURE__ */ new Set();
656
762
  rpc;
657
763
  threads = /* @__PURE__ */ new Map();
658
764
  approvals = /* @__PURE__ */ new Map();
@@ -668,22 +774,29 @@ var CodexAdapter = class {
668
774
  start() {
669
775
  this.stopped = false;
670
776
  this.link.connect();
671
- void this.connect();
777
+ this.connecting = this.connect();
672
778
  }
673
- stop() {
779
+ async stop() {
674
780
  this.stopped = true;
675
781
  this.link.disconnect();
676
782
  if (this.retryTimer) clearTimeout(this.retryTimer);
677
783
  this.retryTimer = void 0;
678
784
  for (const id of [...this.threads.keys()]) this.drop(id);
679
- this.conn?.close();
785
+ this.closeConn(this.conn);
786
+ await this.connecting;
787
+ await Promise.all(this.closing);
788
+ }
789
+ closeConn(conn) {
790
+ if (!conn) return;
791
+ const closing = conn.close().finally(() => this.closing.delete(closing));
792
+ this.closing.add(closing);
680
793
  }
681
794
  async connect() {
682
795
  let rpc;
683
796
  try {
684
797
  const conn = await connectProxy(this.opts.command, this.opts.spawnFn);
685
798
  if (this.stopped) {
686
- conn.close();
799
+ this.closeConn(conn);
687
800
  return;
688
801
  }
689
802
  const live = new RpcClient(conn.ws, this.opts.rpcTimeoutMs);
@@ -715,7 +828,7 @@ var CodexAdapter = class {
715
828
  });
716
829
  if (rpc && this.rpc === rpc) {
717
830
  this.rpc = void 0;
718
- this.conn?.close();
831
+ this.closeConn(this.conn);
719
832
  this.conn = void 0;
720
833
  }
721
834
  for (const id of [...this.threads.keys()]) this.drop(id);
@@ -725,7 +838,7 @@ var CodexAdapter = class {
725
838
  onDaemonLost() {
726
839
  this.setReady(false);
727
840
  this.rpc = void 0;
728
- this.conn?.close();
841
+ this.closeConn(this.conn);
729
842
  this.conn = void 0;
730
843
  for (const id of [...this.threads.keys()]) this.drop(id);
731
844
  if (!this.stopped) logger.warn("Codex daemon connection lost");
@@ -735,7 +848,7 @@ var CodexAdapter = class {
735
848
  if (this.stopped || this.retryTimer) return;
736
849
  this.retryTimer = setTimeout(() => {
737
850
  this.retryTimer = void 0;
738
- void this.connect();
851
+ this.connecting = this.connect();
739
852
  }, this.delay);
740
853
  this.delay = Math.min(this.delay * 2, this.opts.reconnectMaxMs ?? 6e4);
741
854
  }
@@ -1028,9 +1141,9 @@ var CodexAdapter = class {
1028
1141
  }
1029
1142
  onHubMessage(threadId, msg) {
1030
1143
  if (msg.type === "message_to_session") {
1031
- this.enqueue(threadId, async () => textInput(msg.content, msg.source));
1144
+ this.enqueue(threadId, async () => textInput(msg.content, msg.source), msg.source);
1032
1145
  } else if (msg.type === "image_to_session") {
1033
- this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source));
1146
+ this.enqueue(threadId, () => imageInput(msg.imagePath, msg.mimeType, msg.content, msg.source), msg.source);
1034
1147
  } else if (msg.type === "permission_response") {
1035
1148
  this.answer(threadId, msg.requestId, msg.choiceId);
1036
1149
  } else if (msg.type === "codex_close") {
@@ -1038,16 +1151,16 @@ var CodexAdapter = class {
1038
1151
  }
1039
1152
  }
1040
1153
  // One message at a time per conversation, so a message right behind another sees the turn the first one started.
1041
- enqueue(threadId, build) {
1154
+ enqueue(threadId, build, source) {
1042
1155
  const t = this.threads.get(threadId);
1043
1156
  if (!t) return;
1044
- t.sending = t.sending.then(() => this.deliver(t, build)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
1157
+ t.sending = t.sending.then(() => this.deliver(t, build, source)).catch((err) => logger.warn(`Codex delivery for ${threadId} failed: ${err.message}`));
1045
1158
  }
1046
- async deliver(t, build) {
1159
+ async deliver(t, build, source) {
1047
1160
  const threadId = t.thread.id;
1048
1161
  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");
1162
+ if (this.waitingForAnswer(t)) {
1163
+ this.refuseWhileWaiting(threadId);
1051
1164
  return;
1052
1165
  }
1053
1166
  let input;
@@ -1068,20 +1181,37 @@ var CodexAdapter = class {
1068
1181
  const unsubscribed = !t.subscribed;
1069
1182
  const running = await this.runningTurn(rpc, threadId);
1070
1183
  if (this.threads.get(threadId) !== t) return;
1184
+ if (this.waitingForAnswer(t)) {
1185
+ this.abandonDelivery(t, wasUnrelayed);
1186
+ this.refuseWhileWaiting(threadId);
1187
+ return;
1188
+ }
1071
1189
  if (unsubscribed) t.unrelayed = true;
1072
1190
  if (running) {
1073
1191
  await rpc.request("turn/steer", { threadId, expectedTurnId: running, input });
1074
- this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info");
1192
+ this.notify(threadId, "Queued", "Queued: Codex will read it after its current step.", "info", source);
1075
1193
  return;
1076
1194
  }
1077
1195
  await rpc.request("turn/start", { threadId, input });
1078
1196
  } catch (err) {
1079
- t.pendingTurn = false;
1080
- t.unrelayed = wasUnrelayed;
1081
- this.releaseLater(t);
1197
+ this.abandonDelivery(t, wasUnrelayed);
1082
1198
  this.notify(threadId, "Not delivered", `Codex rejected the message: ${err.message}`, "warning");
1083
1199
  }
1084
1200
  }
1201
+ abandonDelivery(t, wasUnrelayed) {
1202
+ t.pendingTurn = false;
1203
+ t.unrelayed = wasUnrelayed;
1204
+ this.releaseLater(t);
1205
+ }
1206
+ // The approval request itself can arrive before the status broadcast that marks the thread as waiting.
1207
+ waitingForAnswer(t) {
1208
+ if (hubStatus(t.thread.status) === "waiting_input") return true;
1209
+ for (const a of this.approvals.values()) if (a.threadId === t.thread.id && !a.answered) return true;
1210
+ return false;
1211
+ }
1212
+ refuseWhileWaiting(threadId) {
1213
+ this.notify(threadId, "Not delivered", "Codex is waiting for an approval or input. Answer it first, then send the message again.", "warning");
1214
+ }
1085
1215
  // 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
1216
  async runningTurn(rpc, threadId) {
1087
1217
  try {
@@ -1161,8 +1291,8 @@ var CodexAdapter = class {
1161
1291
  this.approvals.delete(requestId);
1162
1292
  this.threads.get(a.threadId)?.hub.send({ type: "permission_resolved", sessionId: codexSessionId(a.threadId), requestId, state });
1163
1293
  }
1164
- notify(threadId, title, message, level) {
1165
- this.threads.get(threadId)?.hub.send({ type: "notify", sessionId: codexSessionId(threadId), title, message, level });
1294
+ notify(threadId, title, message, level, to) {
1295
+ this.threads.get(threadId)?.hub.send({ type: "notify", sessionId: codexSessionId(threadId), title, message, level, ...to ? { to } : {} });
1166
1296
  }
1167
1297
  };
1168
1298
 
@@ -1392,11 +1522,10 @@ var exiting = false;
1392
1522
  var shutdown = () => {
1393
1523
  if (exiting) return;
1394
1524
  exiting = true;
1395
- adapter?.stop();
1525
+ const stopped = adapter?.stop();
1396
1526
  const exit = () => process.exit(0);
1397
1527
  setTimeout(exit, 3e3).unref();
1398
- if (release) release().then(exit, exit);
1399
- else exit();
1528
+ Promise.all([stopped, release?.()]).then(exit, exit);
1400
1529
  };
1401
1530
  process.on("SIGINT", shutdown);
1402
1531
  process.on("SIGTERM", shutdown);