@songsid/agend 2.1.4-beta.51 → 2.1.4-beta.53
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/classic-channel-manager.d.ts +14 -0
- package/dist/classic-channel-manager.js +15 -0
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +2 -40
- package/dist/cli.js.map +1 -1
- package/dist/fleet-context.d.ts +14 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-manager.d.ts +10 -0
- package/dist/fleet-manager.js +154 -70
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +12 -0
- package/dist/locale.js.map +1 -1
- package/dist/process-memory.d.ts +6 -0
- package/dist/process-memory.js +45 -0
- package/dist/process-memory.js.map +1 -1
- package/dist/topic-commands.d.ts +6 -2
- package/dist/topic-commands.js +39 -7
- package/dist/topic-commands.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync } from "node:fs";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { freemem } from "node:os";
|
|
4
|
+
import { freemem, totalmem } from "node:os";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { join, dirname, basename } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -567,7 +567,8 @@ export class FleetManager {
|
|
|
567
567
|
getSysInfo() {
|
|
568
568
|
const mem = process.memoryUsage();
|
|
569
569
|
const toMB = (b) => Math.round(b / 1024 / 1024 * 10) / 10;
|
|
570
|
-
|
|
570
|
+
// Fleet instances (fleet.yaml)
|
|
571
|
+
const fleetInstances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
|
|
571
572
|
name,
|
|
572
573
|
status: this.getInstanceStatus(name),
|
|
573
574
|
state: this.getInstanceExecutionState(name),
|
|
@@ -575,12 +576,46 @@ export class FleetManager {
|
|
|
575
576
|
costCents: this.costGuard?.getDailyCostCents(name) ?? 0,
|
|
576
577
|
rateLimits: this.statuslineWatcher.getRateLimits(name) ?? null,
|
|
577
578
|
}));
|
|
579
|
+
// Classic instances (classicBot.yaml) — dedupe against fleet
|
|
580
|
+
const fleetNames = new Set(fleetInstances.map(i => i.name));
|
|
581
|
+
const classicInstances = (this.classicChannels?.getAll() ?? [])
|
|
582
|
+
.filter(ch => !fleetNames.has(ch.instanceName))
|
|
583
|
+
.map(ch => ({
|
|
584
|
+
name: ch.instanceName,
|
|
585
|
+
status: this.getInstanceStatus(ch.instanceName),
|
|
586
|
+
state: this.getInstanceExecutionState(ch.instanceName),
|
|
587
|
+
ipc: this.instanceIpcClients.has(ch.instanceName),
|
|
588
|
+
costCents: this.costGuard?.getDailyCostCents(ch.instanceName) ?? 0,
|
|
589
|
+
rateLimits: this.statuslineWatcher.getRateLimits(ch.instanceName) ?? null,
|
|
590
|
+
}));
|
|
591
|
+
// Combined roster (matches /api/fleet and agend ls)
|
|
592
|
+
const allInstances = [...fleetInstances, ...classicInstances];
|
|
593
|
+
// Fleet summary counts (fleet + Classic combined)
|
|
594
|
+
const running_count = allInstances.filter(i => i.status === "running").length;
|
|
595
|
+
const paused_count = allInstances.filter(i => i.status === "paused").length;
|
|
596
|
+
// System memory (GB, 1 decimal)
|
|
597
|
+
const totalGB = totalmem() / (1024 ** 3);
|
|
598
|
+
const usedGB = (totalmem() - freemem()) / (1024 ** 3);
|
|
599
|
+
const system_mem_gb = {
|
|
600
|
+
used: Math.round(usedGB * 10) / 10,
|
|
601
|
+
total: Math.round(totalGB * 10) / 10,
|
|
602
|
+
};
|
|
603
|
+
// Fleet memory: O(1) cgroup read (includes entire service tree: fleet + CLIs + MCP servers)
|
|
604
|
+
// This avoids blocking the event loop with per-instance tree scans.
|
|
605
|
+
const fleetMem = readFleetMemory();
|
|
606
|
+
const fleet_mem_mb = fleetMem.cgroupAnonBytes != null
|
|
607
|
+
? Math.round(fleetMem.cgroupAnonBytes / (1024 * 1024) * 10) / 10
|
|
608
|
+
: null;
|
|
578
609
|
return {
|
|
579
610
|
uptime_seconds: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
580
611
|
memory_mb: { rss: toMB(mem.rss), heapUsed: toMB(mem.heapUsed), heapTotal: toMB(mem.heapTotal) },
|
|
581
|
-
instances,
|
|
612
|
+
instances: fleetInstances, // SysInfo.instances is fleet-only; /api/fleet enriches with Classic
|
|
582
613
|
fleet_cost_cents: this.costGuard?.getFleetTotalCents() ?? 0,
|
|
583
614
|
fleet_cost_limit_cents: this.costGuard?.getLimitCents() ?? 0,
|
|
615
|
+
running_count,
|
|
616
|
+
paused_count,
|
|
617
|
+
fleet_mem_mb,
|
|
618
|
+
system_mem_gb,
|
|
584
619
|
};
|
|
585
620
|
}
|
|
586
621
|
/** Load fleet.yaml and build routing table */
|
|
@@ -1334,6 +1369,7 @@ export class FleetManager {
|
|
|
1334
1369
|
throw new Error("Classic channel manager not initialized");
|
|
1335
1370
|
const wasRunning = this.daemons.has(instanceName);
|
|
1336
1371
|
this.classicChannels.reloadFromDisk();
|
|
1372
|
+
this.reportClassicUnrecoverableIds();
|
|
1337
1373
|
this.reregisterClassicChannels();
|
|
1338
1374
|
const channel = this.classicChannels.getAll().find(item => item.instanceName === instanceName);
|
|
1339
1375
|
if (!channel)
|
|
@@ -2000,6 +2036,11 @@ export class FleetManager {
|
|
|
2000
2036
|
this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger);
|
|
2001
2037
|
const classicAdapters = fleet.channels?.length ? fleet.channels : (fleet.channel ? [fleet.channel] : []);
|
|
2002
2038
|
this.classicChannels.configureAdapters(classicAdapters);
|
|
2039
|
+
// The unrecoverable-id report is deliberately NOT sent here: no adapter
|
|
2040
|
+
// exists yet at this point in startAll(), so notifyFleetError would find
|
|
2041
|
+
// nothing to deliver through, drop the message silently, AND burn its
|
|
2042
|
+
// 10-minute throttle key on the way out. It is sent once the shared adapter
|
|
2043
|
+
// is up — see the adapterStartup continuation below.
|
|
2003
2044
|
// Restore the persisted bot binding so replies/cancel go through the right
|
|
2004
2045
|
// bot after a restart (before this, inbound would re-bind lazily).
|
|
2005
2046
|
for (const ch of this.classicChannels.getAll()) {
|
|
@@ -2023,6 +2064,9 @@ export class FleetManager {
|
|
|
2023
2064
|
}
|
|
2024
2065
|
if (!this.classicChannels.checkReload())
|
|
2025
2066
|
return;
|
|
2067
|
+
// A reload can introduce a bad id (hand edit) or clear one; the
|
|
2068
|
+
// throttle keeps a repeated report from flooding the topic.
|
|
2069
|
+
this.reportClassicUnrecoverableIds();
|
|
2026
2070
|
this.reregisterClassicChannels();
|
|
2027
2071
|
for (const ch of this.classicChannels.getAll()) {
|
|
2028
2072
|
const newBackend = this.classicChannels.getBackendByInstance(ch.instanceName, fleetBackend);
|
|
@@ -2257,6 +2301,10 @@ export class FleetManager {
|
|
|
2257
2301
|
catch (err) {
|
|
2258
2302
|
this.logger.error({ err }, "startSharedAdapter failed — fleet continues without some adapters");
|
|
2259
2303
|
}
|
|
2304
|
+
// Now that there is somewhere to deliver: a classicBot.yaml that already
|
|
2305
|
+
// holds an unmatchable id at boot is the COMMON case, and reporting it
|
|
2306
|
+
// before the adapter existed meant the operator heard nothing at all.
|
|
2307
|
+
this.reportClassicUnrecoverableIds();
|
|
2260
2308
|
})();
|
|
2261
2309
|
progressStart = adapterStartup.then(() => {
|
|
2262
2310
|
if (pendingUpdateProgress) {
|
|
@@ -2913,7 +2961,8 @@ export class FleetManager {
|
|
|
2913
2961
|
await data.respond(text);
|
|
2914
2962
|
}
|
|
2915
2963
|
else if (data.command === "sysinfo") {
|
|
2916
|
-
|
|
2964
|
+
// Slash commands are Discord-only; use plain lines (no markdown table)
|
|
2965
|
+
await data.respond(this.topicCommands.getSysInfoText({ platform: "discord" }));
|
|
2917
2966
|
}
|
|
2918
2967
|
else if (data.command === "dashboard") {
|
|
2919
2968
|
// Reply is ephemeral (adapter defers non-chat commands ephemerally), so
|
|
@@ -3259,7 +3308,8 @@ export class FleetManager {
|
|
|
3259
3308
|
await data.respond(text);
|
|
3260
3309
|
}
|
|
3261
3310
|
else if (data.command === "sysinfo") {
|
|
3262
|
-
|
|
3311
|
+
// Slash commands are Discord-only; use plain lines (no markdown table)
|
|
3312
|
+
await data.respond(this.topicCommands.getSysInfoText({ platform: "discord" }));
|
|
3263
3313
|
}
|
|
3264
3314
|
else if (data.command === "dashboard") {
|
|
3265
3315
|
// Reply is ephemeral (adapter defers non-chat commands ephemerally), so
|
|
@@ -5357,6 +5407,31 @@ export class FleetManager {
|
|
|
5357
5407
|
* The log line is written by the caller regardless: if every adapter is down,
|
|
5358
5408
|
* the only notification path is the one that is broken.
|
|
5359
5409
|
*/
|
|
5410
|
+
/**
|
|
5411
|
+
* Surface ids that can never match into the operator's General topic.
|
|
5412
|
+
*
|
|
5413
|
+
* The load-time log is still silence for anyone not reading daemon.log, and a
|
|
5414
|
+
* chat that never gets in with nothing said anywhere is exactly the failure
|
|
5415
|
+
* this line of work exists to remove. notifyFleetError throttles by message
|
|
5416
|
+
* text for 10 minutes and resolves its target from config, so the 30s reload
|
|
5417
|
+
* poll cannot flood the topic and a down General does not swallow it.
|
|
5418
|
+
*/
|
|
5419
|
+
reportClassicUnrecoverableIds() {
|
|
5420
|
+
const bad = this.classicChannels?.getUnrecoverableIds?.() ?? [];
|
|
5421
|
+
if (bad.length === 0)
|
|
5422
|
+
return;
|
|
5423
|
+
// notifyFleetError claims its throttle key BEFORE attempting delivery, so
|
|
5424
|
+
// calling it with no adapter would silence this message for ten minutes
|
|
5425
|
+
// without anyone having seen it. Log and leave the key unspent; a later
|
|
5426
|
+
// call, once an adapter exists, still gets through.
|
|
5427
|
+
if (!this.adapter && this.adapters.size === 0) {
|
|
5428
|
+
this.logger.error({ ids: bad }, "classicBot.yaml holds ids that can never match — deferring the operator notice until an adapter is up");
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
const list = bad.map(e => `${e.field}: ${e.value}`).join(", ");
|
|
5432
|
+
this.logger.error({ ids: bad }, "classicBot.yaml holds ids that can never match");
|
|
5433
|
+
this.notifyFleetError(t("classic.unrecoverable_ids", list));
|
|
5434
|
+
}
|
|
5360
5435
|
notifyFleetError(text) {
|
|
5361
5436
|
const now = Date.now();
|
|
5362
5437
|
const key = text.slice(0, 200);
|
|
@@ -9764,72 +9839,81 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
9764
9839
|
}
|
|
9765
9840
|
// Fleet API (enriched for agent board)
|
|
9766
9841
|
if (req.method === "GET" && req.url === "/api/fleet") {
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
const
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9842
|
+
try {
|
|
9843
|
+
const sysInfo = this.getSysInfo();
|
|
9844
|
+
const fleetInstances = sysInfo.instances.map(inst => ({ ...inst, classic: false }));
|
|
9845
|
+
const fleetNames = new Set(fleetInstances.map(inst => inst.name));
|
|
9846
|
+
const classicInstances = (this.classicChannels?.getAll() ?? [])
|
|
9847
|
+
.filter(channel => !fleetNames.has(channel.instanceName))
|
|
9848
|
+
.map(channel => ({
|
|
9849
|
+
name: channel.instanceName,
|
|
9850
|
+
status: this.getInstanceStatus(channel.instanceName),
|
|
9851
|
+
state: this.getInstanceExecutionState(channel.instanceName),
|
|
9852
|
+
ipc: this.instanceIpcClients.has(channel.instanceName),
|
|
9853
|
+
costCents: this.costGuard?.getDailyCostCents(channel.instanceName) ?? 0,
|
|
9854
|
+
rateLimits: this.statuslineWatcher.getRateLimits(channel.instanceName) ?? null,
|
|
9855
|
+
classic: true,
|
|
9856
|
+
classicName: channel.name,
|
|
9857
|
+
channelId: channel.channelId,
|
|
9858
|
+
adapterId: channel.adapterId ?? null,
|
|
9859
|
+
}));
|
|
9860
|
+
const enriched = [...fleetInstances, ...classicInstances].map(inst => {
|
|
9861
|
+
const config = this.fleetConfig?.instances[inst.name];
|
|
9862
|
+
const persistedInboundAt = readLastInboundAt(this.getInstanceDir(inst.name));
|
|
9863
|
+
const lastActivity = inst.classic
|
|
9864
|
+
? Math.max(persistedInboundAt ?? 0, readClassicLastActivityAt(this.dataDir, inst.name) ?? 0) || null
|
|
9865
|
+
: (persistedInboundAt ?? this.lastActivityMs(inst.name)) || null;
|
|
9866
|
+
const backend = this.backendNameForInstance(inst.name);
|
|
9867
|
+
const resolvedModel = this.resolveInstanceModel(inst.name);
|
|
9868
|
+
const effortStrategy = this.effortStrategyFor(inst.name);
|
|
9869
|
+
const resolvedEffort = this.resolveInstanceEffort(inst.name);
|
|
9870
|
+
// Find claimed tasks for this instance
|
|
9871
|
+
let currentTask = null;
|
|
9872
|
+
try {
|
|
9873
|
+
const tasks = this.scheduler?.db.listTasks({ assignee: inst.name, status: "claimed" });
|
|
9874
|
+
if (tasks?.length)
|
|
9875
|
+
currentTask = tasks[0].title;
|
|
9876
|
+
}
|
|
9877
|
+
catch (err) {
|
|
9878
|
+
this.logger.debug({ err, name: inst.name }, "Scheduler listTasks failed (/api/fleet)");
|
|
9879
|
+
}
|
|
9880
|
+
return {
|
|
9881
|
+
...inst,
|
|
9882
|
+
description: config?.description ?? ("classicName" in inst ? inst.classicName : null),
|
|
9883
|
+
backend,
|
|
9884
|
+
// Settings renders these runtime-effective values rather than the
|
|
9885
|
+
// sparse user-authored YAML. `auto` means the supported CLI is
|
|
9886
|
+
// using its own effort default; null is reserved for unsupported.
|
|
9887
|
+
model: resolvedModel.model,
|
|
9888
|
+
model_display: resolvedModel.display,
|
|
9889
|
+
model_source: resolvedModel.source,
|
|
9890
|
+
effort: effortStrategy === "unsupported" ? null : (resolvedEffort.effort ?? "auto"),
|
|
9891
|
+
effort_supported: effortStrategy !== "unsupported",
|
|
9892
|
+
tool_set: config?.tool_set ?? "full",
|
|
9893
|
+
general_topic: config?.general_topic ?? false,
|
|
9894
|
+
// User activity is persisted by the daemon, so both the board and
|
|
9895
|
+
// auto-pause retain an accurate age across fleet restarts.
|
|
9896
|
+
lastActivity,
|
|
9897
|
+
currentTask,
|
|
9898
|
+
idle: this.getInstanceIdle(inst.name),
|
|
9899
|
+
state: this.getInstanceExecutionState(inst.name),
|
|
9900
|
+
};
|
|
9901
|
+
});
|
|
9902
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9903
|
+
res.writeHead(200);
|
|
9904
|
+
res.end(JSON.stringify({
|
|
9905
|
+
...sysInfo,
|
|
9906
|
+
version: this.currentVersion,
|
|
9907
|
+
instances: enriched,
|
|
9908
|
+
}));
|
|
9909
|
+
}
|
|
9910
|
+
catch (err) {
|
|
9911
|
+
this.logger.error({ err }, "/api/fleet failed");
|
|
9912
|
+
if (!res.headersSent) {
|
|
9913
|
+
res.writeHead(500);
|
|
9914
|
+
res.end("Internal Server Error");
|
|
9803
9915
|
}
|
|
9804
|
-
|
|
9805
|
-
...inst,
|
|
9806
|
-
description: config?.description ?? ("classicName" in inst ? inst.classicName : null),
|
|
9807
|
-
backend,
|
|
9808
|
-
// Settings renders these runtime-effective values rather than the
|
|
9809
|
-
// sparse user-authored YAML. `auto` means the supported CLI is
|
|
9810
|
-
// using its own effort default; null is reserved for unsupported.
|
|
9811
|
-
model: resolvedModel.model,
|
|
9812
|
-
model_display: resolvedModel.display,
|
|
9813
|
-
model_source: resolvedModel.source,
|
|
9814
|
-
effort: effortStrategy === "unsupported" ? null : (resolvedEffort.effort ?? "auto"),
|
|
9815
|
-
effort_supported: effortStrategy !== "unsupported",
|
|
9816
|
-
tool_set: config?.tool_set ?? "full",
|
|
9817
|
-
general_topic: config?.general_topic ?? false,
|
|
9818
|
-
// User activity is persisted by the daemon, so both the board and
|
|
9819
|
-
// auto-pause retain an accurate age across fleet restarts.
|
|
9820
|
-
lastActivity,
|
|
9821
|
-
currentTask,
|
|
9822
|
-
idle: this.getInstanceIdle(inst.name),
|
|
9823
|
-
state: this.getInstanceExecutionState(inst.name),
|
|
9824
|
-
};
|
|
9825
|
-
});
|
|
9826
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9827
|
-
res.writeHead(200);
|
|
9828
|
-
res.end(JSON.stringify({
|
|
9829
|
-
...sysInfo,
|
|
9830
|
-
version: this.currentVersion,
|
|
9831
|
-
instances: enriched,
|
|
9832
|
-
}));
|
|
9916
|
+
}
|
|
9833
9917
|
return;
|
|
9834
9918
|
}
|
|
9835
9919
|
// Activity API
|