@songsid/agend 2.1.1-beta.14 → 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/fleet-manager.d.ts +11 -0
- package/dist/fleet-manager.js +59 -27
- package/dist/fleet-manager.js.map +1 -1
- package/dist/topic-commands.js +16 -7
- package/dist/topic-commands.js.map +1 -1
- package/dist/web-api.js +5 -2
- package/dist/web-api.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -329,6 +329,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
329
329
|
* So: try, move a bad file aside and retry once with a fresh one, and if even
|
|
330
330
|
* that fails carry on without an event log.
|
|
331
331
|
*/
|
|
332
|
+
/**
|
|
333
|
+
* Run `agend backend doctor` for the fleet's default backend and return its
|
|
334
|
+
* cleaned output.
|
|
335
|
+
*
|
|
336
|
+
* Async on purpose: this was `execSync` with a 30s timeout, reachable by any
|
|
337
|
+
* allowlisted user through `/doctor`. While it ran, the entire fleet event loop
|
|
338
|
+
* was frozen — no IPC, no adapter, no message delivery, no health responses,
|
|
339
|
+
* and critically no WATCHDOG ping, so a slow doctor could push past
|
|
340
|
+
* WatchdogSec and have systemd SIGABRT the fleet.
|
|
341
|
+
*/
|
|
342
|
+
private runBackendDoctor;
|
|
332
343
|
/** Drop event/activity rows older than the retention window. Best-effort. */
|
|
333
344
|
private pruneEventLog;
|
|
334
345
|
private openEventLog;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -1594,17 +1594,7 @@ export class FleetManager {
|
|
|
1594
1594
|
await data.respond(t("not_authorized"));
|
|
1595
1595
|
return;
|
|
1596
1596
|
}
|
|
1597
|
-
|
|
1598
|
-
const { execSync } = await import("node:child_process");
|
|
1599
|
-
const backend = this.fleetConfig?.defaults?.backend || "claude-code";
|
|
1600
|
-
const result = execSync(`agend backend doctor ${backend}`, { timeout: 30_000, encoding: "utf-8" });
|
|
1601
|
-
const clean = result.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1602
|
-
await data.respond(clean || "No output");
|
|
1603
|
-
}
|
|
1604
|
-
catch (err) {
|
|
1605
|
-
const output = (err.stdout ?? err.message ?? "Doctor failed").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1606
|
-
await data.respond(output);
|
|
1607
|
-
}
|
|
1597
|
+
await data.respond(await this.runBackendDoctor());
|
|
1608
1598
|
}
|
|
1609
1599
|
else if (data.command === "status") {
|
|
1610
1600
|
const text = await this.topicCommands.getStatusText();
|
|
@@ -1876,17 +1866,7 @@ export class FleetManager {
|
|
|
1876
1866
|
await data.respond(t("not_authorized"));
|
|
1877
1867
|
return;
|
|
1878
1868
|
}
|
|
1879
|
-
|
|
1880
|
-
const { execSync } = await import("node:child_process");
|
|
1881
|
-
const backend = this.fleetConfig?.defaults?.backend || "claude-code";
|
|
1882
|
-
const result = execSync(`agend backend doctor ${backend}`, { timeout: 30_000, encoding: "utf-8" });
|
|
1883
|
-
const clean = result.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1884
|
-
await data.respond(clean || "No output");
|
|
1885
|
-
}
|
|
1886
|
-
catch (err) {
|
|
1887
|
-
const output = (err.stdout ?? err.message ?? "Doctor failed").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
1888
|
-
await data.respond(output);
|
|
1889
|
-
}
|
|
1869
|
+
await data.respond(await this.runBackendDoctor());
|
|
1890
1870
|
}
|
|
1891
1871
|
else if (data.command === "status") {
|
|
1892
1872
|
const text = await this.topicCommands.getStatusText();
|
|
@@ -2114,9 +2094,22 @@ export class FleetManager {
|
|
|
2114
2094
|
if (existsSync(windowIdPath)) {
|
|
2115
2095
|
const windowId = readFileSync(windowIdPath, "utf-8").trim();
|
|
2116
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).
|
|
2117
2103
|
try {
|
|
2118
|
-
const {
|
|
2119
|
-
|
|
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 });
|
|
2120
2113
|
}
|
|
2121
2114
|
catch {
|
|
2122
2115
|
// Pane dead — respawn
|
|
@@ -3476,6 +3469,35 @@ export class FleetManager {
|
|
|
3476
3469
|
* So: try, move a bad file aside and retry once with a fresh one, and if even
|
|
3477
3470
|
* that fails carry on without an event log.
|
|
3478
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
|
+
}
|
|
3479
3501
|
/** Drop event/activity rows older than the retention window. Best-effort. */
|
|
3480
3502
|
pruneEventLog() {
|
|
3481
3503
|
try {
|
|
@@ -5441,10 +5463,20 @@ When users create specialized instances, suggest these configurations:
|
|
|
5441
5463
|
// ── Update check ────────────────────────────────────────────────────
|
|
5442
5464
|
async checkForUpdates() {
|
|
5443
5465
|
try {
|
|
5444
|
-
|
|
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
|
+
};
|
|
5445
5477
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
5446
5478
|
const currentVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
|
|
5447
|
-
const latest =
|
|
5479
|
+
const latest = await npmVersion("@songsid/agend");
|
|
5448
5480
|
let target = latest;
|
|
5449
5481
|
if (currentVersion.includes("-beta")) {
|
|
5450
5482
|
// Beta users track the @beta channel (never fall back to @latest, which is
|
|
@@ -5452,7 +5484,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
5452
5484
|
// of beta/latest is the newest.
|
|
5453
5485
|
let beta = "";
|
|
5454
5486
|
try {
|
|
5455
|
-
beta =
|
|
5487
|
+
beta = await npmVersion("@songsid/agend@beta");
|
|
5456
5488
|
}
|
|
5457
5489
|
catch { /* no beta tag */ }
|
|
5458
5490
|
target = beta || latest;
|