@songsid/agend 2.1.1-beta.13 → 2.1.1-beta.15
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/access-path.js +15 -6
- package/dist/access-path.js.map +1 -1
- package/dist/classic-channel-manager.js +1 -5
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +53 -24
- package/dist/cli.js.map +1 -1
- package/dist/cost-guard.d.ts +3 -1
- package/dist/cost-guard.js +3 -1
- package/dist/cost-guard.js.map +1 -1
- package/dist/daemon.d.ts +21 -0
- package/dist/daemon.js +318 -228
- package/dist/daemon.js.map +1 -1
- package/dist/event-log.js +4 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-manager.d.ts +56 -0
- package/dist/fleet-manager.js +206 -34
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.js +5 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +9 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/scheduler/db.js +3 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/tmux-control.d.ts +10 -4
- package/dist/tmux-control.js +19 -6
- package/dist/tmux-control.js.map +1 -1
- package/dist/topic-commands.js +16 -7
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/view.html +10 -1
- package/dist/usage/providers.d.ts +24 -0
- package/dist/usage/providers.js +43 -9
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.js +1 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.js +5 -2
- package/dist/web-api.js.map +1 -1
- package/package.json +4 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -177,6 +177,9 @@ export class FleetManager {
|
|
|
177
177
|
healthServer = null;
|
|
178
178
|
healthPortRetried = false;
|
|
179
179
|
updateCheckTimer = null;
|
|
180
|
+
eventLogPruneTimer = null;
|
|
181
|
+
/** Days of event/activity history to keep. */
|
|
182
|
+
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
180
183
|
watchdogTimer = null;
|
|
181
184
|
startedAt = 0;
|
|
182
185
|
// Mirror topic: buffer cross-instance messages, flush every 3s
|
|
@@ -898,7 +901,7 @@ export class FleetManager {
|
|
|
898
901
|
}
|
|
899
902
|
const pidPath = join(this.dataDir, "fleet.pid");
|
|
900
903
|
writeFileSync(pidPath, String(process.pid), "utf-8");
|
|
901
|
-
this.eventLog =
|
|
904
|
+
this.eventLog = this.openEventLog();
|
|
902
905
|
// Initialize classic channel manager. The primary adapter (channels[0])
|
|
903
906
|
// migrates legacy single-bot entries and names without a suffix. Classic
|
|
904
907
|
// routing does NOT go through the routing engine (single-key, can't hold two
|
|
@@ -1134,6 +1137,12 @@ export class FleetManager {
|
|
|
1134
1137
|
// Signal systemd: generals ready
|
|
1135
1138
|
sdNotify("READY=1");
|
|
1136
1139
|
this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
|
|
1140
|
+
// EventLog.prune() existed but was never called, so `events` and `activity`
|
|
1141
|
+
// grew without bound for the life of the install. Prune once at startup and
|
|
1142
|
+
// daily after that; the timer is unref'd so it never holds the loop open.
|
|
1143
|
+
this.pruneEventLog();
|
|
1144
|
+
this.eventLogPruneTimer = setInterval(() => this.pruneEventLog(), 24 * 60 * 60_000);
|
|
1145
|
+
this.eventLogPruneTimer.unref?.();
|
|
1137
1146
|
// Phase 2: Start remaining instances with staggered concurrency
|
|
1138
1147
|
if (others.length > 0) {
|
|
1139
1148
|
await this.startInstancesWithConcurrency(others, topicMode);
|
|
@@ -1585,17 +1594,7 @@ export class FleetManager {
|
|
|
1585
1594
|
await data.respond(t("not_authorized"));
|
|
1586
1595
|
return;
|
|
1587
1596
|
}
|
|
1588
|
-
|
|
1589
|
-
const { execSync } = await import("node:child_process");
|
|
1590
|
-
const backend = this.fleetConfig?.defaults?.backend || "claude-code";
|
|
1591
|
-
const result = execSync(`agend backend doctor ${backend}`, { timeout: 30_000, encoding: "utf-8" });
|
|
1592
|
-
const clean = result.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1593
|
-
await data.respond(clean || "No output");
|
|
1594
|
-
}
|
|
1595
|
-
catch (err) {
|
|
1596
|
-
const output = (err.stdout ?? err.message ?? "Doctor failed").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1597
|
-
await data.respond(output);
|
|
1598
|
-
}
|
|
1597
|
+
await data.respond(await this.runBackendDoctor());
|
|
1599
1598
|
}
|
|
1600
1599
|
else if (data.command === "status") {
|
|
1601
1600
|
const text = await this.topicCommands.getStatusText();
|
|
@@ -1867,17 +1866,7 @@ export class FleetManager {
|
|
|
1867
1866
|
await data.respond(t("not_authorized"));
|
|
1868
1867
|
return;
|
|
1869
1868
|
}
|
|
1870
|
-
|
|
1871
|
-
const { execSync } = await import("node:child_process");
|
|
1872
|
-
const backend = this.fleetConfig?.defaults?.backend || "claude-code";
|
|
1873
|
-
const result = execSync(`agend backend doctor ${backend}`, { timeout: 30_000, encoding: "utf-8" });
|
|
1874
|
-
const clean = result.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1875
|
-
await data.respond(clean || "No output");
|
|
1876
|
-
}
|
|
1877
|
-
catch (err) {
|
|
1878
|
-
const output = (err.stdout ?? err.message ?? "Doctor failed").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1879
|
-
await data.respond(output);
|
|
1880
|
-
}
|
|
1869
|
+
await data.respond(await this.runBackendDoctor());
|
|
1881
1870
|
}
|
|
1882
1871
|
else if (data.command === "status") {
|
|
1883
1872
|
const text = await this.topicCommands.getStatusText();
|
|
@@ -2105,9 +2094,22 @@ export class FleetManager {
|
|
|
2105
2094
|
if (existsSync(windowIdPath)) {
|
|
2106
2095
|
const windowId = readFileSync(windowIdPath, "utf-8").trim();
|
|
2107
2096
|
if (windowId) {
|
|
2097
|
+
// Async with an explicit timeout: this was execSync with NO timeout at
|
|
2098
|
+
// all, so a wedged tmux server blocked the whole fleet event loop
|
|
2099
|
+
// indefinitely — while we were here to diagnose a lost connection.
|
|
2100
|
+
// A timeout is also the correct signal: an unresponsive tmux server
|
|
2101
|
+
// means we cannot verify the pane, which is treated as dead (the same
|
|
2102
|
+
// conclusion the old code reached only by throwing).
|
|
2108
2103
|
try {
|
|
2109
|
-
const {
|
|
2110
|
-
|
|
2104
|
+
const { execFile } = await import("node:child_process");
|
|
2105
|
+
const { promisify } = await import("node:util");
|
|
2106
|
+
const { getTmuxSocketName } = await import("./paths.js");
|
|
2107
|
+
// Honour socket isolation: without -L this queried the user's default
|
|
2108
|
+
// tmux server instead of the fleet's, so under a custom AGEND_HOME the
|
|
2109
|
+
// check was meaningless (it reported every pane dead).
|
|
2110
|
+
const socket = getTmuxSocketName();
|
|
2111
|
+
const args = socket ? ["-L", socket, "list-panes", "-t", windowId] : ["list-panes", "-t", windowId];
|
|
2112
|
+
await promisify(execFile)("tmux", args, { timeout: 5_000 });
|
|
2111
2113
|
}
|
|
2112
2114
|
catch {
|
|
2113
2115
|
// Pane dead — respawn
|
|
@@ -3454,6 +3456,136 @@ export class FleetManager {
|
|
|
3454
3456
|
this.collabInstances.add(instanceName);
|
|
3455
3457
|
return true;
|
|
3456
3458
|
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Open the event log, tolerating a corrupt file.
|
|
3461
|
+
*
|
|
3462
|
+
* `events.db` holds history only — event rows and the activity feed. Nothing the
|
|
3463
|
+
* fleet needs to run depends on it, and every consumer already uses
|
|
3464
|
+
* `this.eventLog?.`. An unguarded `new EventLog(...)` here meant a corrupt or
|
|
3465
|
+
* unreadable history file (a truncated WAL after a hard kill, a full disk)
|
|
3466
|
+
* threw during startAll and the WHOLE FLEET FAILED TO BOOT — trading every
|
|
3467
|
+
* running agent for a file whose only job is reporting.
|
|
3468
|
+
*
|
|
3469
|
+
* So: try, move a bad file aside and retry once with a fresh one, and if even
|
|
3470
|
+
* that fails carry on without an event log.
|
|
3471
|
+
*/
|
|
3472
|
+
/**
|
|
3473
|
+
* Run `agend backend doctor` for the fleet's default backend and return its
|
|
3474
|
+
* cleaned output.
|
|
3475
|
+
*
|
|
3476
|
+
* Async on purpose: this was `execSync` with a 30s timeout, reachable by any
|
|
3477
|
+
* allowlisted user through `/doctor`. While it ran, the entire fleet event loop
|
|
3478
|
+
* was frozen — no IPC, no adapter, no message delivery, no health responses,
|
|
3479
|
+
* and critically no WATCHDOG ping, so a slow doctor could push past
|
|
3480
|
+
* WatchdogSec and have systemd SIGABRT the fleet.
|
|
3481
|
+
*/
|
|
3482
|
+
async runBackendDoctor() {
|
|
3483
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
3484
|
+
const backend = this.fleetConfig?.defaults?.backend || "claude-code";
|
|
3485
|
+
try {
|
|
3486
|
+
const { execFile } = await import("node:child_process");
|
|
3487
|
+
const { promisify } = await import("node:util");
|
|
3488
|
+
// execFile with an argv array — no shell, so the backend name cannot be
|
|
3489
|
+
// interpreted as a command even if config is malformed.
|
|
3490
|
+
const { stdout } = await promisify(execFile)("agend", ["backend", "doctor", backend], {
|
|
3491
|
+
timeout: 30_000,
|
|
3492
|
+
encoding: "utf-8",
|
|
3493
|
+
});
|
|
3494
|
+
return stripAnsi(stdout) || "No output";
|
|
3495
|
+
}
|
|
3496
|
+
catch (err) {
|
|
3497
|
+
const e = err;
|
|
3498
|
+
return stripAnsi(e.stdout ?? e.message ?? "Doctor failed");
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
/** Drop event/activity rows older than the retention window. Best-effort. */
|
|
3502
|
+
pruneEventLog() {
|
|
3503
|
+
try {
|
|
3504
|
+
this.eventLog?.prune(FleetManager.EVENT_LOG_RETENTION_DAYS);
|
|
3505
|
+
}
|
|
3506
|
+
catch (err) {
|
|
3507
|
+
this.logger.warn({ err }, "Event log prune failed");
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
openEventLog() {
|
|
3511
|
+
const dbPath = join(this.dataDir, "events.db");
|
|
3512
|
+
try {
|
|
3513
|
+
return new EventLog(dbPath);
|
|
3514
|
+
}
|
|
3515
|
+
catch (err) {
|
|
3516
|
+
this.logger.error({ err, dbPath }, "events.db unusable — moving it aside and starting a fresh one");
|
|
3517
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
3518
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
3519
|
+
try {
|
|
3520
|
+
renameSync(`${dbPath}${suffix}`, `${dbPath}${suffix}.corrupt-${stamp}`);
|
|
3521
|
+
}
|
|
3522
|
+
catch { /* may not exist */ }
|
|
3523
|
+
}
|
|
3524
|
+
try {
|
|
3525
|
+
return new EventLog(dbPath);
|
|
3526
|
+
}
|
|
3527
|
+
catch (retryErr) {
|
|
3528
|
+
// History is worth losing; a fleet that won't start is not.
|
|
3529
|
+
this.logger.error({ err: retryErr, dbPath }, "Could not open a fresh events.db — continuing without event logging");
|
|
3530
|
+
return null;
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
/**
|
|
3535
|
+
* Report a fleet-level fault (not attributable to one instance) to the General
|
|
3536
|
+
* topic, so the operator learns about it without reading daemon.log.
|
|
3537
|
+
*
|
|
3538
|
+
* Throttled per distinct message: an unhandled rejection typically comes from a
|
|
3539
|
+
* loop (a poller, a repeating timer), and one channel message per occurrence
|
|
3540
|
+
* would bury the topic — which is worse than silence. First occurrence goes out
|
|
3541
|
+
* immediately, repeats are suppressed for THROTTLE_MS and then re-sent with a
|
|
3542
|
+
* count.
|
|
3543
|
+
*
|
|
3544
|
+
* The log line is written by the caller regardless: if every adapter is down,
|
|
3545
|
+
* the only notification path is the one that is broken.
|
|
3546
|
+
*/
|
|
3547
|
+
notifyFleetError(text) {
|
|
3548
|
+
const now = Date.now();
|
|
3549
|
+
const key = text.slice(0, 200);
|
|
3550
|
+
const seen = this.fleetErrorNotices.get(key);
|
|
3551
|
+
if (seen && now - seen.at < FleetManager.FLEET_ERROR_THROTTLE_MS) {
|
|
3552
|
+
seen.suppressed++;
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
const suppressed = seen?.suppressed ?? 0;
|
|
3556
|
+
this.fleetErrorNotices.set(key, { at: now, suppressed: 0 });
|
|
3557
|
+
// Bound the map: it is keyed by message text, and a message with a varying
|
|
3558
|
+
// suffix (a path, an id) would otherwise grow it without limit.
|
|
3559
|
+
if (this.fleetErrorNotices.size > 100) {
|
|
3560
|
+
const oldest = this.fleetErrorNotices.keys().next().value;
|
|
3561
|
+
if (oldest !== undefined)
|
|
3562
|
+
this.fleetErrorNotices.delete(oldest);
|
|
3563
|
+
}
|
|
3564
|
+
const body = suppressed > 0
|
|
3565
|
+
? `${text}\n(plus ${suppressed} more in the last ${Math.round(FleetManager.FLEET_ERROR_THROTTLE_MS / 60_000)}m)`
|
|
3566
|
+
: text;
|
|
3567
|
+
// Resolved from config, NOT findGeneralInstance(): that requires a live daemon,
|
|
3568
|
+
// and a fleet-level fault is exactly when the General may be down. The topic
|
|
3569
|
+
// itself still exists, and notifyInstanceTopic only needs adapter + group +
|
|
3570
|
+
// topic_id to post into it.
|
|
3571
|
+
const general = Object.entries(this.fleetConfig?.instances ?? {})
|
|
3572
|
+
.find(([, config]) => config.general_topic === true)?.[0];
|
|
3573
|
+
if (general) {
|
|
3574
|
+
this.notifyInstanceTopic(general, body);
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
// No General instance — fall back to the primary channel's group.
|
|
3578
|
+
const channelCfg = this.getChannelConfig();
|
|
3579
|
+
const groupId = channelCfg?.group_id;
|
|
3580
|
+
if (this.adapter && groupId) {
|
|
3581
|
+
this.adapter.sendText(String(groupId), body)
|
|
3582
|
+
.catch(err => this.logger.warn({ err }, "Failed to send fleet error notification"));
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
this.logger.warn({ text: body }, "Fleet error had no notification target (no General instance, no adapter)");
|
|
3586
|
+
}
|
|
3587
|
+
static FLEET_ERROR_THROTTLE_MS = 10 * 60_000;
|
|
3588
|
+
fleetErrorNotices = new Map();
|
|
3457
3589
|
notifyInstanceTopic(instanceName, text, extraOpts) {
|
|
3458
3590
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
3459
3591
|
if (!adapter)
|
|
@@ -4581,9 +4713,19 @@ When users create specialized instances, suggest these configurations:
|
|
|
4581
4713
|
// DC path: respond immediately with progress text
|
|
4582
4714
|
await pending.respond(progressText).catch(() => { });
|
|
4583
4715
|
}
|
|
4584
|
-
// Apply model in background — don't await here (keeps callback handler fast)
|
|
4716
|
+
// Apply model in background — don't await here (keeps callback handler fast).
|
|
4717
|
+
// Guarded: applyModel() restarts the instance, and an unguarded rejection here
|
|
4718
|
+
// meant a user picking from the /model menu could take the whole fleet down.
|
|
4719
|
+
// On failure the user gets told, rather than the click silently doing nothing.
|
|
4585
4720
|
void (async () => {
|
|
4586
|
-
|
|
4721
|
+
let result;
|
|
4722
|
+
try {
|
|
4723
|
+
result = await this.applyModel(pending.instanceName, model);
|
|
4724
|
+
}
|
|
4725
|
+
catch (err) {
|
|
4726
|
+
this.logger.error({ err, instance: pending.instanceName, model }, "Model switch failed");
|
|
4727
|
+
result = `Model switch to \`${model}\` failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
4728
|
+
}
|
|
4587
4729
|
if (pending.adapter && pending.adapterChatId) {
|
|
4588
4730
|
if (progressMsgId) {
|
|
4589
4731
|
pending.adapter.editMessage(pending.adapterChatId, progressMsgId, result, pending.adapterThreadId).catch(() => {
|
|
@@ -4863,7 +5005,20 @@ When users create specialized instances, suggest these configurations:
|
|
|
4863
5005
|
this.logger.info({ channelId, adapterId, instanceName: ch.instanceName }, "Classic channel stopped");
|
|
4864
5006
|
return t("classic.stopped");
|
|
4865
5007
|
}
|
|
4866
|
-
|
|
5008
|
+
/**
|
|
5009
|
+
* Idempotent while in flight: SIGINT and SIGTERM share one handler and the
|
|
5010
|
+
* uncaughtException path calls this too, so overlapping runs were possible —
|
|
5011
|
+
* each snapshotting the daemon map and calling stop() on the same daemons
|
|
5012
|
+
* concurrently. Deliberately NOT `async`, so callers receive the same promise
|
|
5013
|
+
* object rather than a fresh wrapper around it. The latch clears when the run
|
|
5014
|
+
* settles, so a later genuine stop (after a restart) still does the work.
|
|
5015
|
+
*/
|
|
5016
|
+
stopAll() {
|
|
5017
|
+
this.stopAllInFlight ??= this.doStopAll().finally(() => { this.stopAllInFlight = null; });
|
|
5018
|
+
return this.stopAllInFlight;
|
|
5019
|
+
}
|
|
5020
|
+
stopAllInFlight = null;
|
|
5021
|
+
async doStopAll() {
|
|
4867
5022
|
this.startupComplete = false;
|
|
4868
5023
|
this.reloadPending = false;
|
|
4869
5024
|
this.ipcStoppingInstances.add("__fleet_stopping__");
|
|
@@ -4887,6 +5042,10 @@ When users create specialized instances, suggest these configurations:
|
|
|
4887
5042
|
clearInterval(this.updateCheckTimer);
|
|
4888
5043
|
this.updateCheckTimer = null;
|
|
4889
5044
|
}
|
|
5045
|
+
if (this.eventLogPruneTimer) {
|
|
5046
|
+
clearInterval(this.eventLogPruneTimer);
|
|
5047
|
+
this.eventLogPruneTimer = null;
|
|
5048
|
+
}
|
|
4890
5049
|
if (this.topicCleanupTimer) {
|
|
4891
5050
|
clearInterval(this.topicCleanupTimer);
|
|
4892
5051
|
this.topicCleanupTimer = null;
|
|
@@ -5304,10 +5463,20 @@ When users create specialized instances, suggest these configurations:
|
|
|
5304
5463
|
// ── Update check ────────────────────────────────────────────────────
|
|
5305
5464
|
async checkForUpdates() {
|
|
5306
5465
|
try {
|
|
5307
|
-
|
|
5466
|
+
// Both npm lookups are async: as execSync they froze the fleet event loop for
|
|
5467
|
+
// up to 15s each, and on a beta build BOTH ran — 30s with no WATCHDOG ping,
|
|
5468
|
+
// past WatchdogSec's half-interval and enough for systemd to SIGABRT the fleet
|
|
5469
|
+
// for a background version check.
|
|
5470
|
+
const { execFile } = await import("node:child_process");
|
|
5471
|
+
const { promisify } = await import("node:util");
|
|
5472
|
+
const execFileP = promisify(execFile);
|
|
5473
|
+
const npmVersion = async (spec) => {
|
|
5474
|
+
const { stdout } = await execFileP("npm", ["view", spec, "version"], { timeout: 15_000 });
|
|
5475
|
+
return stdout.toString().trim();
|
|
5476
|
+
};
|
|
5308
5477
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
5309
5478
|
const currentVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
|
|
5310
|
-
const latest =
|
|
5479
|
+
const latest = await npmVersion("@songsid/agend");
|
|
5311
5480
|
let target = latest;
|
|
5312
5481
|
if (currentVersion.includes("-beta")) {
|
|
5313
5482
|
// Beta users track the @beta channel (never fall back to @latest, which is
|
|
@@ -5315,7 +5484,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
5315
5484
|
// of beta/latest is the newest.
|
|
5316
5485
|
let beta = "";
|
|
5317
5486
|
try {
|
|
5318
|
-
beta =
|
|
5487
|
+
beta = await npmVersion("@songsid/agend@beta");
|
|
5319
5488
|
}
|
|
5320
5489
|
catch { /* no beta tag */ }
|
|
5321
5490
|
target = beta || latest;
|
|
@@ -5584,7 +5753,10 @@ When users create specialized instances, suggest these configurations:
|
|
|
5584
5753
|
res.writeHead(500);
|
|
5585
5754
|
res.end(JSON.stringify({ error: `Start failed: ${err.message}` }));
|
|
5586
5755
|
}
|
|
5587
|
-
|
|
5756
|
+
// The inner catch can itself throw (writeHead after a successful
|
|
5757
|
+
// writeHead is ERR_HTTP_HEADERS_SENT), and that rejection escapes the
|
|
5758
|
+
// IIFE. Same for the two handlers below.
|
|
5759
|
+
})().catch(err => this.logger.error({ err, name }, "HTTP start handler failed"));
|
|
5588
5760
|
return;
|
|
5589
5761
|
}
|
|
5590
5762
|
// Instance restart (immediate, no idle wait)
|
|
@@ -5605,7 +5777,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
5605
5777
|
res.writeHead(status);
|
|
5606
5778
|
res.end(JSON.stringify({ error: `Restart failed: ${err.message}` }));
|
|
5607
5779
|
}
|
|
5608
|
-
})();
|
|
5780
|
+
})().catch(err => this.logger.error({ err, name }, "HTTP restart handler failed"));
|
|
5609
5781
|
return;
|
|
5610
5782
|
}
|
|
5611
5783
|
if (req.method === "POST" && req.url?.startsWith("/stop/")) {
|
|
@@ -5628,7 +5800,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
5628
5800
|
res.writeHead(500);
|
|
5629
5801
|
res.end(JSON.stringify({ error: `Stop failed: ${err.message}` }));
|
|
5630
5802
|
}
|
|
5631
|
-
})();
|
|
5803
|
+
})().catch(err => this.logger.error({ err, name }, "HTTP stop handler failed"));
|
|
5632
5804
|
return;
|
|
5633
5805
|
}
|
|
5634
5806
|
// ── Agent CLI endpoint ─────
|