@songsid/agend 2.1.4-beta.52 → 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.
@@ -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
- const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
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
- await data.respond(this.topicCommands.getSysInfoText());
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
- await data.respond(this.topicCommands.getSysInfoText());
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
@@ -9802,72 +9839,81 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
9802
9839
  }
9803
9840
  // Fleet API (enriched for agent board)
9804
9841
  if (req.method === "GET" && req.url === "/api/fleet") {
9805
- const sysInfo = this.getSysInfo();
9806
- const fleetInstances = sysInfo.instances.map(inst => ({ ...inst, classic: false }));
9807
- const fleetNames = new Set(fleetInstances.map(inst => inst.name));
9808
- const classicInstances = (this.classicChannels?.getAll() ?? [])
9809
- .filter(channel => !fleetNames.has(channel.instanceName))
9810
- .map(channel => ({
9811
- name: channel.instanceName,
9812
- status: this.getInstanceStatus(channel.instanceName),
9813
- state: this.getInstanceExecutionState(channel.instanceName),
9814
- ipc: this.instanceIpcClients.has(channel.instanceName),
9815
- costCents: this.costGuard?.getDailyCostCents(channel.instanceName) ?? 0,
9816
- rateLimits: this.statuslineWatcher.getRateLimits(channel.instanceName) ?? null,
9817
- classic: true,
9818
- classicName: channel.name,
9819
- channelId: channel.channelId,
9820
- adapterId: channel.adapterId ?? null,
9821
- }));
9822
- const enriched = [...fleetInstances, ...classicInstances].map(inst => {
9823
- const config = this.fleetConfig?.instances[inst.name];
9824
- const persistedInboundAt = readLastInboundAt(this.getInstanceDir(inst.name));
9825
- const lastActivity = inst.classic
9826
- ? Math.max(persistedInboundAt ?? 0, readClassicLastActivityAt(this.dataDir, inst.name) ?? 0) || null
9827
- : (persistedInboundAt ?? this.lastActivityMs(inst.name)) || null;
9828
- const backend = this.backendNameForInstance(inst.name);
9829
- const resolvedModel = this.resolveInstanceModel(inst.name);
9830
- const effortStrategy = this.effortStrategyFor(inst.name);
9831
- const resolvedEffort = this.resolveInstanceEffort(inst.name);
9832
- // Find claimed tasks for this instance
9833
- let currentTask = null;
9834
- try {
9835
- const tasks = this.scheduler?.db.listTasks({ assignee: inst.name, status: "claimed" });
9836
- if (tasks?.length)
9837
- currentTask = tasks[0].title;
9838
- }
9839
- catch (err) {
9840
- this.logger.debug({ err, name: inst.name }, "Scheduler listTasks failed (/api/fleet)");
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");
9841
9915
  }
9842
- return {
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
- }));
9916
+ }
9871
9917
  return;
9872
9918
  }
9873
9919
  // Activity API