@songsid/agend 2.1.2-beta.47 → 2.1.2-beta.48
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/backend/kiro.js +35 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/daemon.js +18 -3
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-manager.d.ts +7 -0
- package/dist/fleet-manager.js +52 -32
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/topic-commands.js +5 -6
- package/dist/topic-commands.js.map +1 -1
- package/dist/web-api.js +2 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +6 -0
- package/dist/web-auth.js +54 -0
- package/dist/web-auth.js.map +1 -0
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -51,6 +51,7 @@ import { readLastInboundAt } from "./daemon.js";
|
|
|
51
51
|
import { clearPausedMarker } from "./pause-marker.js";
|
|
52
52
|
import { releaseProcessFleetLock } from "./fleet-lock.js";
|
|
53
53
|
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
54
|
+
import { loadOrCreateWebToken, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
54
55
|
import { getTmuxSession } from "./config.js";
|
|
55
56
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
56
57
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -285,6 +286,7 @@ export class FleetManager {
|
|
|
285
286
|
sseClients = new Set();
|
|
286
287
|
webToken = null;
|
|
287
288
|
viewToken = null;
|
|
289
|
+
healthServerListening = false;
|
|
288
290
|
constructor(dataDir) {
|
|
289
291
|
this.dataDir = dataDir;
|
|
290
292
|
FleetManager.signalTarget = this;
|
|
@@ -1060,6 +1062,17 @@ export class FleetManager {
|
|
|
1060
1062
|
* TODO: per-instance startup timeout (existing issue, not introduced here)
|
|
1061
1063
|
*/
|
|
1062
1064
|
async startInstancesWithConcurrency(entries, topicMode) {
|
|
1065
|
+
// Persisted pauses are intentionally preserved across fleet restarts. Filter
|
|
1066
|
+
// them before grouping/staggering: startInstance() retains its own guard as
|
|
1067
|
+
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
1068
|
+
// full stagger slot for every distinct working directory.
|
|
1069
|
+
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
1070
|
+
const pausedCount = entries.length - runnableEntries.length;
|
|
1071
|
+
if (pausedCount > 0) {
|
|
1072
|
+
this.logger.info({ pausedCount }, "Paused instances excluded from startup queue");
|
|
1073
|
+
}
|
|
1074
|
+
if (runnableEntries.length === 0)
|
|
1075
|
+
return;
|
|
1063
1076
|
const raw = this.fleetConfig?.defaults?.startup;
|
|
1064
1077
|
const explicitConcurrency = raw?.concurrency;
|
|
1065
1078
|
const staggerMs = Math.max(0, Math.min(30_000, raw?.stagger_delay_ms ?? 500));
|
|
@@ -1074,10 +1087,10 @@ export class FleetManager {
|
|
|
1074
1087
|
else {
|
|
1075
1088
|
const freeMemMB = Math.round(freemem() / (1024 * 1024));
|
|
1076
1089
|
concurrency = Math.max(2, Math.min(10, Math.floor(freeMemMB / ESTIMATED_MB_PER_INSTANCE)));
|
|
1077
|
-
this.logger.info({ concurrency, freeMemMB: freeMemMB, totalInstances:
|
|
1090
|
+
this.logger.info({ concurrency, freeMemMB: freeMemMB, totalInstances: runnableEntries.length }, "Adaptive startup concurrency");
|
|
1078
1091
|
}
|
|
1079
1092
|
const byWorkDir = new Map();
|
|
1080
|
-
for (const [name, config] of
|
|
1093
|
+
for (const [name, config] of runnableEntries) {
|
|
1081
1094
|
const dir = config.working_directory;
|
|
1082
1095
|
if (!byWorkDir.has(dir))
|
|
1083
1096
|
byWorkDir.set(dir, []);
|
|
@@ -1225,6 +1238,21 @@ export class FleetManager {
|
|
|
1225
1238
|
process.env[key] = value;
|
|
1226
1239
|
}
|
|
1227
1240
|
}
|
|
1241
|
+
/** Initialize auth before any adapter can answer /dashboard. */
|
|
1242
|
+
initializeWebAuthTokens() {
|
|
1243
|
+
this.webToken = loadOrCreateWebToken(this.dataDir);
|
|
1244
|
+
this.viewToken = randomBytes(24).toString("hex");
|
|
1245
|
+
const viewTokenPath = join(this.dataDir, "view.token");
|
|
1246
|
+
writeFileSync(viewTokenPath, this.viewToken, { encoding: "utf8", mode: 0o600 });
|
|
1247
|
+
try {
|
|
1248
|
+
chmodSync(viewTokenPath, 0o600);
|
|
1249
|
+
}
|
|
1250
|
+
catch { /* best effort */ }
|
|
1251
|
+
this.healthServerListening = false;
|
|
1252
|
+
}
|
|
1253
|
+
getDashboardAccess() {
|
|
1254
|
+
return { ready: this.healthServerListening, token: this.webToken };
|
|
1255
|
+
}
|
|
1228
1256
|
/** Start all instances from fleet config */
|
|
1229
1257
|
async startAll(configPath) {
|
|
1230
1258
|
FleetManager.signalTarget = this;
|
|
@@ -1238,6 +1266,7 @@ export class FleetManager {
|
|
|
1238
1266
|
rotateLogIfNeeded(join(this.dataDir, "fleet.log"));
|
|
1239
1267
|
const fleet = this.loadConfig(configPath);
|
|
1240
1268
|
setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
|
|
1269
|
+
this.initializeWebAuthTokens();
|
|
1241
1270
|
const topicMode = fleet.channel?.mode === "topic" || !!fleet.channels?.some(ch => ch.mode === "topic");
|
|
1242
1271
|
// Set tmux socket isolation for custom AGEND_HOME
|
|
1243
1272
|
const { getTmuxSocketName: getSocket } = await import("./paths.js");
|
|
@@ -6331,6 +6360,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
6331
6360
|
this.controlClient?.stop();
|
|
6332
6361
|
this.controlClient = null;
|
|
6333
6362
|
if (this.healthServer) {
|
|
6363
|
+
this.healthServerListening = false;
|
|
6334
6364
|
this.healthServer.close();
|
|
6335
6365
|
this.healthServer = null;
|
|
6336
6366
|
}
|
|
@@ -6799,26 +6829,11 @@ When users create specialized instances, suggest these configurations:
|
|
|
6799
6829
|
// ── Health HTTP endpoint ─────────────────────────────────────────────
|
|
6800
6830
|
startHealthServer(port) {
|
|
6801
6831
|
this.startedAt = Date.now();
|
|
6802
|
-
|
|
6803
|
-
this.
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
try {
|
|
6808
|
-
chmodSync(tokenPath, 0o600);
|
|
6809
|
-
}
|
|
6810
|
-
catch {
|
|
6811
|
-
// best-effort
|
|
6812
|
-
}
|
|
6813
|
-
// Separate read-only token for the /view page: grants terminal-view + profile
|
|
6814
|
-
// read, but never write (POSTs still require the full web token).
|
|
6815
|
-
this.viewToken = randomBytes(24).toString("hex");
|
|
6816
|
-
const viewTokenPath = join(this.dataDir, "view.token");
|
|
6817
|
-
writeFileSync(viewTokenPath, this.viewToken, { mode: 0o600 });
|
|
6818
|
-
try {
|
|
6819
|
-
chmodSync(viewTokenPath, 0o600);
|
|
6820
|
-
}
|
|
6821
|
-
catch { /* best-effort */ }
|
|
6832
|
+
this.healthServerListening = false;
|
|
6833
|
+
this.healthPortRetried = false;
|
|
6834
|
+
// Defensive for direct/unit callers; normal startup initializes these before adapters.
|
|
6835
|
+
if (!this.webToken || !this.viewToken)
|
|
6836
|
+
this.initializeWebAuthTokens();
|
|
6822
6837
|
this.healthServer = createServer((req, res) => {
|
|
6823
6838
|
res.setHeader("Content-Type", "application/json");
|
|
6824
6839
|
// Public health probe — no auth required.
|
|
@@ -6845,7 +6860,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
6845
6860
|
?? (typeof headerToken === "string" ? headerToken : null);
|
|
6846
6861
|
if (!this.webToken || providedToken !== this.webToken) {
|
|
6847
6862
|
res.writeHead(401);
|
|
6848
|
-
res.end(JSON.stringify({ error:
|
|
6863
|
+
res.end(JSON.stringify({ error: WEB_TOKEN_INVALID_MESSAGE }));
|
|
6849
6864
|
return;
|
|
6850
6865
|
}
|
|
6851
6866
|
}
|
|
@@ -7060,10 +7075,20 @@ When users create specialized instances, suggest these configurations:
|
|
|
7060
7075
|
res.writeHead(404);
|
|
7061
7076
|
res.end(JSON.stringify({ error: "not found" }));
|
|
7062
7077
|
});
|
|
7078
|
+
const markListening = (afterTakeover = false) => {
|
|
7079
|
+
this.healthServerListening = true;
|
|
7080
|
+
this.logger.info({ port }, afterTakeover
|
|
7081
|
+
? "Health endpoint listening (after takeover)"
|
|
7082
|
+
: "Health endpoint listening");
|
|
7083
|
+
this.logger.info({ url: `http://localhost:${port}/ui?token=${this.webToken}` }, "Web UI available");
|
|
7084
|
+
this.logger.info({ url: `http://localhost:${port}/view?token=${this.viewToken}` }, "Web View available");
|
|
7085
|
+
};
|
|
7063
7086
|
this.healthServer.on("error", (err) => {
|
|
7087
|
+
this.healthServerListening = false;
|
|
7064
7088
|
if (err.code === "EADDRINUSE") {
|
|
7065
7089
|
if (this.healthPortRetried) {
|
|
7066
|
-
this.logger.
|
|
7090
|
+
this.logger.error({ err, port }, "Health port still in use after takeover — dashboard disabled");
|
|
7091
|
+
this.notifyFleetError(`⚠️ Dashboard unavailable — health port ${port} is already in use. Stop the conflicting process or configure a different health_port.`);
|
|
7067
7092
|
return;
|
|
7068
7093
|
}
|
|
7069
7094
|
this.healthPortRetried = true;
|
|
@@ -7084,19 +7109,14 @@ When users create specialized instances, suggest these configurations:
|
|
|
7084
7109
|
setTimeout(() => {
|
|
7085
7110
|
if (!this.healthServer)
|
|
7086
7111
|
return;
|
|
7087
|
-
this.healthServer.listen(port, "127.0.0.1", () =>
|
|
7088
|
-
this.logger.info({ port }, "Health endpoint listening (after takeover)");
|
|
7089
|
-
});
|
|
7112
|
+
this.healthServer.listen(port, "127.0.0.1", () => markListening(true));
|
|
7090
7113
|
}, 1500);
|
|
7091
7114
|
return;
|
|
7092
7115
|
}
|
|
7093
7116
|
this.logger.error({ err, port }, "Health server error");
|
|
7117
|
+
this.notifyFleetError(`⚠️ Dashboard unavailable — health server failed: ${err.message}`);
|
|
7094
7118
|
});
|
|
7095
|
-
this.healthServer.listen(port, "127.0.0.1", () =>
|
|
7096
|
-
this.logger.info({ port }, "Health endpoint listening");
|
|
7097
|
-
});
|
|
7098
|
-
this.logger.info({ url: `http://localhost:${port}/ui?token=${this.webToken}` }, "Web UI available");
|
|
7099
|
-
this.logger.info({ url: `http://localhost:${port}/view?token=${this.viewToken}` }, "Web View available");
|
|
7119
|
+
this.healthServer.listen(port, "127.0.0.1", () => markListening());
|
|
7100
7120
|
}
|
|
7101
7121
|
getUiStatus() {
|
|
7102
7122
|
const fleetNames = Object.keys(this.fleetConfig?.instances ?? {});
|