@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 +288 -60
- package/dist/cli.js.map +1 -1
- package/dist/codex/main.js +220 -46
- package/dist/codex/main.js.map +1 -1
- package/dist/dashboard/index.html +77 -46
- package/dist/hub/server.js +83 -40
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +83 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/dashboard/index.html +77 -46
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
681
|
-
|
|
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
|
-
|
|
715
|
-
|
|
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
|
|
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
|
|
919
|
-
|
|
920
|
-
if (
|
|
921
|
-
|
|
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 ??
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
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
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
|
1963
|
-
|
|
1964
|
-
|
|
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(
|
|
2663
|
-
|
|
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
|
-
|
|
2909
|
+
let closing;
|
|
2910
|
+
const close = () => closing ??= (async () => {
|
|
2695
2911
|
ws.terminate();
|
|
2696
|
-
child.
|
|
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
|
-
|
|
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
|
|
3725
|
+
let exited2 = false;
|
|
3500
3726
|
child.once("exit", () => {
|
|
3501
|
-
|
|
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: () => !
|
|
3734
|
+
isAlive: () => !exited2
|
|
3509
3735
|
});
|
|
3510
3736
|
if (startup !== "ready") {
|
|
3511
|
-
if (!
|
|
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")
|
|
3833
|
-
|
|
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);
|