@songsid/agend 2.1.4-beta.52 → 2.1.4-beta.54
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 +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 +11 -1
- package/dist/fleet-manager.js +166 -91
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +10 -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 */
|
|
@@ -2926,7 +2961,8 @@ export class FleetManager {
|
|
|
2926
2961
|
await data.respond(text);
|
|
2927
2962
|
}
|
|
2928
2963
|
else if (data.command === "sysinfo") {
|
|
2929
|
-
|
|
2964
|
+
// Slash commands are Discord-only; use plain lines (no markdown table)
|
|
2965
|
+
await data.respond(this.topicCommands.getSysInfoText({ platform: "discord" }));
|
|
2930
2966
|
}
|
|
2931
2967
|
else if (data.command === "dashboard") {
|
|
2932
2968
|
// Reply is ephemeral (adapter defers non-chat commands ephemerally), so
|
|
@@ -3272,7 +3308,8 @@ export class FleetManager {
|
|
|
3272
3308
|
await data.respond(text);
|
|
3273
3309
|
}
|
|
3274
3310
|
else if (data.command === "sysinfo") {
|
|
3275
|
-
|
|
3311
|
+
// Slash commands are Discord-only; use plain lines (no markdown table)
|
|
3312
|
+
await data.respond(this.topicCommands.getSysInfoText({ platform: "discord" }));
|
|
3276
3313
|
}
|
|
3277
3314
|
else if (data.command === "dashboard") {
|
|
3278
3315
|
// Reply is ephemeral (adapter defers non-chat commands ephemerally), so
|
|
@@ -5404,14 +5441,6 @@ export class FleetManager {
|
|
|
5404
5441
|
return;
|
|
5405
5442
|
}
|
|
5406
5443
|
const suppressed = seen?.suppressed ?? 0;
|
|
5407
|
-
this.fleetErrorNotices.set(key, { at: now, suppressed: 0 });
|
|
5408
|
-
// Bound the map: it is keyed by message text, and a message with a varying
|
|
5409
|
-
// suffix (a path, an id) would otherwise grow it without limit.
|
|
5410
|
-
if (this.fleetErrorNotices.size > 100) {
|
|
5411
|
-
const oldest = this.fleetErrorNotices.keys().next().value;
|
|
5412
|
-
if (oldest !== undefined)
|
|
5413
|
-
this.fleetErrorNotices.delete(oldest);
|
|
5414
|
-
}
|
|
5415
5444
|
const body = suppressed > 0
|
|
5416
5445
|
? `${text}\n${t("fleet.error_suppressed", suppressed, Math.round(FleetManager.FLEET_ERROR_THROTTLE_MS / 60_000))}`
|
|
5417
5446
|
: text;
|
|
@@ -5421,26 +5450,60 @@ export class FleetManager {
|
|
|
5421
5450
|
// topic_id to post into it.
|
|
5422
5451
|
const general = Object.entries(this.fleetConfig?.instances ?? {})
|
|
5423
5452
|
.find(([, config]) => config.general_topic === true)?.[0];
|
|
5453
|
+
// "dispatched", not "delivered": see notifyInstanceTopic. A platform that
|
|
5454
|
+
// rejects the message afterwards still consumes the throttle window, which
|
|
5455
|
+
// is the pre-existing semantic and not something this change alters.
|
|
5456
|
+
let dispatched = false;
|
|
5424
5457
|
if (general) {
|
|
5425
|
-
this.notifyInstanceTopic(general, body);
|
|
5426
|
-
return;
|
|
5458
|
+
dispatched = this.notifyInstanceTopic(general, body);
|
|
5427
5459
|
}
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5460
|
+
else {
|
|
5461
|
+
// No General instance — fall back to the primary channel's group.
|
|
5462
|
+
const groupId = this.getChannelConfig()?.group_id;
|
|
5463
|
+
if (this.adapter && groupId) {
|
|
5464
|
+
this.adapter.sendText(String(groupId), body)
|
|
5465
|
+
.catch(err => this.logger.warn({ err }, "Failed to send fleet error notification"));
|
|
5466
|
+
dispatched = true;
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
if (!dispatched) {
|
|
5470
|
+
// Do NOT record the throttle key. It used to be claimed before delivery
|
|
5471
|
+
// was attempted, so a message nobody could receive — every fleet error
|
|
5472
|
+
// raised before adapters exist, which is when startup faults happen —
|
|
5473
|
+
// also suppressed the next ten minutes of identical ones. Leaving the key
|
|
5474
|
+
// unspent lets the next caller, once an adapter is up, actually get
|
|
5475
|
+
// through. The counter is not reset either: nothing was shown.
|
|
5476
|
+
this.logger.warn({ text: body }, "Fleet error could not be delivered (no adapter or no target yet) — not consuming the throttle window");
|
|
5434
5477
|
return;
|
|
5435
5478
|
}
|
|
5436
|
-
this.
|
|
5479
|
+
this.fleetErrorNotices.set(key, { at: now, suppressed: 0 });
|
|
5480
|
+
// Bound the map: it is keyed by message text, and a message with a varying
|
|
5481
|
+
// suffix (a path, an id) would otherwise grow it without limit.
|
|
5482
|
+
if (this.fleetErrorNotices.size > 100) {
|
|
5483
|
+
const oldest = this.fleetErrorNotices.keys().next().value;
|
|
5484
|
+
if (oldest !== undefined)
|
|
5485
|
+
this.fleetErrorNotices.delete(oldest);
|
|
5486
|
+
}
|
|
5437
5487
|
}
|
|
5438
5488
|
static FLEET_ERROR_THROTTLE_MS = 10 * 60_000;
|
|
5439
5489
|
fleetErrorNotices = new Map();
|
|
5490
|
+
/**
|
|
5491
|
+
* Post into an instance's topic. Returns whether a send was DISPATCHED — a
|
|
5492
|
+
* target was resolved and sendText was called — so a caller that must not
|
|
5493
|
+
* lose the message (notifyFleetError, which spends a throttle key) can tell
|
|
5494
|
+
* that from silence. Existing callers ignore the result and are unaffected.
|
|
5495
|
+
*
|
|
5496
|
+
* Not a delivery guarantee: sendText is fire-and-forget, and a platform-side
|
|
5497
|
+
* rejection surfaces only as a warn in its .catch. True delivery confirmation
|
|
5498
|
+
* would have to make this async and change every caller.
|
|
5499
|
+
*/
|
|
5440
5500
|
notifyInstanceTopic(instanceName, text, extraOpts) {
|
|
5441
5501
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
5442
|
-
if (!adapter)
|
|
5443
|
-
|
|
5502
|
+
if (!adapter) {
|
|
5503
|
+
// Early startup: adapters are created well after the fleet object exists.
|
|
5504
|
+
this.logger.warn({ instanceName }, "No adapter yet — instance topic notification not sent");
|
|
5505
|
+
return false;
|
|
5506
|
+
}
|
|
5444
5507
|
const channelCfg = this.getChannelConfig(this.getInstanceAdapterId(instanceName));
|
|
5445
5508
|
const groupId = channelCfg?.group_id;
|
|
5446
5509
|
// Fleet topic instance
|
|
@@ -5448,20 +5511,23 @@ export class FleetManager {
|
|
|
5448
5511
|
if (threadId != null && groupId) {
|
|
5449
5512
|
adapter.sendText(String(groupId), text, { threadId: String(threadId), ...extraOpts })
|
|
5450
5513
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
|
|
5451
|
-
return;
|
|
5514
|
+
return true;
|
|
5452
5515
|
}
|
|
5453
5516
|
// Classic instance: find its channelId from the classic manager
|
|
5454
5517
|
const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
5455
5518
|
if (classicChatId) {
|
|
5456
5519
|
adapter.sendText(classicChatId, text, extraOpts)
|
|
5457
5520
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
|
|
5458
|
-
return;
|
|
5521
|
+
return true;
|
|
5459
5522
|
}
|
|
5460
5523
|
// Fallback: send to group without threadId
|
|
5461
5524
|
if (groupId) {
|
|
5462
5525
|
adapter.sendText(String(groupId), text, extraOpts)
|
|
5463
5526
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
|
|
5527
|
+
return true;
|
|
5464
5528
|
}
|
|
5529
|
+
this.logger.warn({ instanceName }, "No group id — instance topic notification not sent");
|
|
5530
|
+
return false;
|
|
5465
5531
|
}
|
|
5466
5532
|
// ── Nonce-armed button prompts (hang / assist / exit / clear) ──
|
|
5467
5533
|
//
|
|
@@ -9802,72 +9868,81 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
9802
9868
|
}
|
|
9803
9869
|
// Fleet API (enriched for agent board)
|
|
9804
9870
|
if (req.method === "GET" && req.url === "/api/fleet") {
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
const
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9871
|
+
try {
|
|
9872
|
+
const sysInfo = this.getSysInfo();
|
|
9873
|
+
const fleetInstances = sysInfo.instances.map(inst => ({ ...inst, classic: false }));
|
|
9874
|
+
const fleetNames = new Set(fleetInstances.map(inst => inst.name));
|
|
9875
|
+
const classicInstances = (this.classicChannels?.getAll() ?? [])
|
|
9876
|
+
.filter(channel => !fleetNames.has(channel.instanceName))
|
|
9877
|
+
.map(channel => ({
|
|
9878
|
+
name: channel.instanceName,
|
|
9879
|
+
status: this.getInstanceStatus(channel.instanceName),
|
|
9880
|
+
state: this.getInstanceExecutionState(channel.instanceName),
|
|
9881
|
+
ipc: this.instanceIpcClients.has(channel.instanceName),
|
|
9882
|
+
costCents: this.costGuard?.getDailyCostCents(channel.instanceName) ?? 0,
|
|
9883
|
+
rateLimits: this.statuslineWatcher.getRateLimits(channel.instanceName) ?? null,
|
|
9884
|
+
classic: true,
|
|
9885
|
+
classicName: channel.name,
|
|
9886
|
+
channelId: channel.channelId,
|
|
9887
|
+
adapterId: channel.adapterId ?? null,
|
|
9888
|
+
}));
|
|
9889
|
+
const enriched = [...fleetInstances, ...classicInstances].map(inst => {
|
|
9890
|
+
const config = this.fleetConfig?.instances[inst.name];
|
|
9891
|
+
const persistedInboundAt = readLastInboundAt(this.getInstanceDir(inst.name));
|
|
9892
|
+
const lastActivity = inst.classic
|
|
9893
|
+
? Math.max(persistedInboundAt ?? 0, readClassicLastActivityAt(this.dataDir, inst.name) ?? 0) || null
|
|
9894
|
+
: (persistedInboundAt ?? this.lastActivityMs(inst.name)) || null;
|
|
9895
|
+
const backend = this.backendNameForInstance(inst.name);
|
|
9896
|
+
const resolvedModel = this.resolveInstanceModel(inst.name);
|
|
9897
|
+
const effortStrategy = this.effortStrategyFor(inst.name);
|
|
9898
|
+
const resolvedEffort = this.resolveInstanceEffort(inst.name);
|
|
9899
|
+
// Find claimed tasks for this instance
|
|
9900
|
+
let currentTask = null;
|
|
9901
|
+
try {
|
|
9902
|
+
const tasks = this.scheduler?.db.listTasks({ assignee: inst.name, status: "claimed" });
|
|
9903
|
+
if (tasks?.length)
|
|
9904
|
+
currentTask = tasks[0].title;
|
|
9905
|
+
}
|
|
9906
|
+
catch (err) {
|
|
9907
|
+
this.logger.debug({ err, name: inst.name }, "Scheduler listTasks failed (/api/fleet)");
|
|
9908
|
+
}
|
|
9909
|
+
return {
|
|
9910
|
+
...inst,
|
|
9911
|
+
description: config?.description ?? ("classicName" in inst ? inst.classicName : null),
|
|
9912
|
+
backend,
|
|
9913
|
+
// Settings renders these runtime-effective values rather than the
|
|
9914
|
+
// sparse user-authored YAML. `auto` means the supported CLI is
|
|
9915
|
+
// using its own effort default; null is reserved for unsupported.
|
|
9916
|
+
model: resolvedModel.model,
|
|
9917
|
+
model_display: resolvedModel.display,
|
|
9918
|
+
model_source: resolvedModel.source,
|
|
9919
|
+
effort: effortStrategy === "unsupported" ? null : (resolvedEffort.effort ?? "auto"),
|
|
9920
|
+
effort_supported: effortStrategy !== "unsupported",
|
|
9921
|
+
tool_set: config?.tool_set ?? "full",
|
|
9922
|
+
general_topic: config?.general_topic ?? false,
|
|
9923
|
+
// User activity is persisted by the daemon, so both the board and
|
|
9924
|
+
// auto-pause retain an accurate age across fleet restarts.
|
|
9925
|
+
lastActivity,
|
|
9926
|
+
currentTask,
|
|
9927
|
+
idle: this.getInstanceIdle(inst.name),
|
|
9928
|
+
state: this.getInstanceExecutionState(inst.name),
|
|
9929
|
+
};
|
|
9930
|
+
});
|
|
9931
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9932
|
+
res.writeHead(200);
|
|
9933
|
+
res.end(JSON.stringify({
|
|
9934
|
+
...sysInfo,
|
|
9935
|
+
version: this.currentVersion,
|
|
9936
|
+
instances: enriched,
|
|
9937
|
+
}));
|
|
9938
|
+
}
|
|
9939
|
+
catch (err) {
|
|
9940
|
+
this.logger.error({ err }, "/api/fleet failed");
|
|
9941
|
+
if (!res.headersSent) {
|
|
9942
|
+
res.writeHead(500);
|
|
9943
|
+
res.end("Internal Server Error");
|
|
9841
9944
|
}
|
|
9842
|
-
|
|
9843
|
-
...inst,
|
|
9844
|
-
description: config?.description ?? ("classicName" in inst ? inst.classicName : null),
|
|
9845
|
-
backend,
|
|
9846
|
-
// Settings renders these runtime-effective values rather than the
|
|
9847
|
-
// sparse user-authored YAML. `auto` means the supported CLI is
|
|
9848
|
-
// using its own effort default; null is reserved for unsupported.
|
|
9849
|
-
model: resolvedModel.model,
|
|
9850
|
-
model_display: resolvedModel.display,
|
|
9851
|
-
model_source: resolvedModel.source,
|
|
9852
|
-
effort: effortStrategy === "unsupported" ? null : (resolvedEffort.effort ?? "auto"),
|
|
9853
|
-
effort_supported: effortStrategy !== "unsupported",
|
|
9854
|
-
tool_set: config?.tool_set ?? "full",
|
|
9855
|
-
general_topic: config?.general_topic ?? false,
|
|
9856
|
-
// User activity is persisted by the daemon, so both the board and
|
|
9857
|
-
// auto-pause retain an accurate age across fleet restarts.
|
|
9858
|
-
lastActivity,
|
|
9859
|
-
currentTask,
|
|
9860
|
-
idle: this.getInstanceIdle(inst.name),
|
|
9861
|
-
state: this.getInstanceExecutionState(inst.name),
|
|
9862
|
-
};
|
|
9863
|
-
});
|
|
9864
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9865
|
-
res.writeHead(200);
|
|
9866
|
-
res.end(JSON.stringify({
|
|
9867
|
-
...sysInfo,
|
|
9868
|
-
version: this.currentVersion,
|
|
9869
|
-
instances: enriched,
|
|
9870
|
-
}));
|
|
9945
|
+
}
|
|
9871
9946
|
return;
|
|
9872
9947
|
}
|
|
9873
9948
|
// Activity API
|