@songsid/agend 2.1.1-beta.11 → 2.1.1-beta.13

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.
Files changed (88) hide show
  1. package/dist/backend/antigravity.js +5 -2
  2. package/dist/backend/antigravity.js.map +1 -1
  3. package/dist/backend/claude-code.js +3 -0
  4. package/dist/backend/claude-code.js.map +1 -1
  5. package/dist/backend/codex.d.ts +16 -0
  6. package/dist/backend/codex.js +248 -56
  7. package/dist/backend/codex.js.map +1 -1
  8. package/dist/backend/grok.js +20 -0
  9. package/dist/backend/grok.js.map +1 -1
  10. package/dist/backend/kiro.js +18 -2
  11. package/dist/backend/kiro.js.map +1 -1
  12. package/dist/backend/types.d.ts +7 -0
  13. package/dist/backend/types.js +1 -1
  14. package/dist/backend/types.js.map +1 -1
  15. package/dist/channel/adapters/discord.js +4 -5
  16. package/dist/channel/adapters/discord.js.map +1 -1
  17. package/dist/channel/adapters/telegram.js +50 -15
  18. package/dist/channel/adapters/telegram.js.map +1 -1
  19. package/dist/channel/ipc-bridge.d.ts +8 -0
  20. package/dist/channel/ipc-bridge.js +41 -14
  21. package/dist/channel/ipc-bridge.js.map +1 -1
  22. package/dist/channel/mcp-server.js +25 -10
  23. package/dist/channel/mcp-server.js.map +1 -1
  24. package/dist/channel/mcp-tools.js +25 -0
  25. package/dist/channel/mcp-tools.js.map +1 -1
  26. package/dist/channel/message-queue.d.ts +1 -0
  27. package/dist/channel/message-queue.js +49 -17
  28. package/dist/channel/message-queue.js.map +1 -1
  29. package/dist/channel/reconnect-backoff.d.ts +17 -0
  30. package/dist/channel/reconnect-backoff.js +21 -0
  31. package/dist/channel/reconnect-backoff.js.map +1 -0
  32. package/dist/cli.js +71 -26
  33. package/dist/cli.js.map +1 -1
  34. package/dist/completion.d.ts +27 -0
  35. package/dist/completion.js +121 -0
  36. package/dist/completion.js.map +1 -0
  37. package/dist/config-validator.js +9 -0
  38. package/dist/config-validator.js.map +1 -1
  39. package/dist/config.js +1 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/daemon.d.ts +74 -8
  42. package/dist/daemon.js +254 -18
  43. package/dist/daemon.js.map +1 -1
  44. package/dist/fleet-manager.d.ts +7 -1
  45. package/dist/fleet-manager.js +89 -16
  46. package/dist/fleet-manager.js.map +1 -1
  47. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +22 -0
  48. package/dist/general-knowledge/skills/model-discovery/SKILL.md +14 -23
  49. package/dist/general-knowledge/skills/session-management/SKILL.md +15 -34
  50. package/dist/instance-lifecycle.d.ts +13 -1
  51. package/dist/instance-lifecycle.js +70 -3
  52. package/dist/instance-lifecycle.js.map +1 -1
  53. package/dist/instructions.d.ts +5 -0
  54. package/dist/instructions.js +9 -11
  55. package/dist/instructions.js.map +1 -1
  56. package/dist/logger.js +14 -0
  57. package/dist/logger.js.map +1 -1
  58. package/dist/mcp-liveness.d.ts +21 -0
  59. package/dist/mcp-liveness.js +27 -0
  60. package/dist/mcp-liveness.js.map +1 -0
  61. package/dist/outbound-handlers.d.ts +9 -0
  62. package/dist/outbound-handlers.js +46 -26
  63. package/dist/outbound-handlers.js.map +1 -1
  64. package/dist/outbound-schemas.js +1 -1
  65. package/dist/outbound-schemas.js.map +1 -1
  66. package/dist/tmux-control.js +4 -2
  67. package/dist/tmux-control.js.map +1 -1
  68. package/dist/tmux-manager.d.ts +14 -0
  69. package/dist/tmux-manager.js +20 -6
  70. package/dist/tmux-manager.js.map +1 -1
  71. package/dist/topic-commands.d.ts +22 -1
  72. package/dist/topic-commands.js +95 -57
  73. package/dist/topic-commands.js.map +1 -1
  74. package/dist/tui-glyphs.d.ts +25 -0
  75. package/dist/tui-glyphs.js +26 -0
  76. package/dist/tui-glyphs.js.map +1 -0
  77. package/dist/types.d.ts +6 -0
  78. package/dist/ui/view.html +114 -2
  79. package/dist/usage/providers.d.ts +26 -0
  80. package/dist/usage/providers.js +577 -0
  81. package/dist/usage/providers.js.map +1 -0
  82. package/dist/usage/usage-api.d.ts +35 -0
  83. package/dist/usage/usage-api.js +52 -0
  84. package/dist/usage/usage-api.js.map +1 -0
  85. package/dist/view-api.d.ts +3 -0
  86. package/dist/view-api.js +10 -2
  87. package/dist/view-api.js.map +1 -1
  88. package/package.json +1 -1
@@ -26,7 +26,7 @@ import { processAttachments } from "./channel/attachment-handler.js";
26
26
  import { routeToolCall } from "./channel/tool-router.js";
27
27
  import { Scheduler } from "./scheduler/index.js";
28
28
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
29
- import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, SAVE_FILENAME_RE, SAVE_UNSUPPORTED_MSG } from "./topic-commands.js";
29
+ import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, SAVE_FILENAME_RE, SAVE_UNSUPPORTED_MSG, resolveInstanceContext } from "./topic-commands.js";
30
30
  import { DailySummary } from "./daily-summary.js";
31
31
  import { WebhookEmitter } from "./webhook-emitter.js";
32
32
  import { TmuxControlClient } from "./tmux-control.js";
@@ -38,6 +38,7 @@ import { StatuslineWatcher } from "./statusline-watcher.js";
38
38
  import { outboundHandlers } from "./outbound-handlers.js";
39
39
  import { handleWebRequest, broadcastSseEvent } from "./web-api.js";
40
40
  import { handleViewRequest, isViewPath } from "./view-api.js";
41
+ import { handleUsageRequest, isUsagePath } from "./usage/usage-api.js";
41
42
  import { handleSettingsRequest } from "./settings-api.js";
42
43
  import { setLocale, detectLocale, t } from "./locale.js";
43
44
  import { handleAgentRequest } from "./agent-endpoint.js";
@@ -163,6 +164,10 @@ export class FleetManager {
163
164
  failoverActive = new Map(); // instance → current failover model
164
165
  // IPC reconnect: tracks instances being intentionally stopped (skip reconnect)
165
166
  ipcStoppingInstances = new Set();
167
+ /** Coalesce concurrent connection attempts for the same daemon socket. */
168
+ ipcConnectInFlight = new Map();
169
+ /** At most one reconnect/backoff loop may exist per instance. */
170
+ ipcReconnectInFlight = new Map();
166
171
  // Adapter restart: prevents re-entrant restart attempts
167
172
  adapterRestarting = new Set();
168
173
  // Adapter isolation: track state per adapter for retry + visibility
@@ -651,7 +656,7 @@ export class FleetManager {
651
656
  await new Promise(resolve => setTimeout(resolve, 250));
652
657
  await this.startClassicInstance(instanceName, this.classicChannels.getBackendByInstance(instanceName, this.fleetConfig?.defaults?.backend), this.classicChannels.getPreTaskCommand(channel.channelId, channel.adapterId), this.classicChannels.getModel(channel.channelId, channel.adapterId, this.fleetConfig?.defaults?.model), this.classicChannels.getAutoPauseAfter(channel.channelId, channel.adapterId, this.fleetConfig?.defaults?.auto_pause_after));
653
658
  }
654
- async startInstance(name, config, topicMode) {
659
+ async startInstance(name, config, topicMode, kind = "fleet-topic") {
655
660
  if (this.lifecycle.isPaused(name)) {
656
661
  this.logger.info({ name }, "Persisted paused instance — skipping startup");
657
662
  return;
@@ -667,7 +672,11 @@ export class FleetManager {
667
672
  this.ensureGeneralInstructions(config.working_directory, config.backend);
668
673
  }
669
674
  this.instanceProcessStatus.delete(name);
670
- await this.lifecycle.start(name, config, topicMode);
675
+ await this.lifecycle.start(name, config, topicMode, {
676
+ kind,
677
+ backend: config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code",
678
+ model: this.resolveInstanceModel(name).display,
679
+ });
671
680
  // Auto-connect IPC — daemon.start() ensures socket is ready before resolving
672
681
  await this.connectIpcToInstance(name);
673
682
  }
@@ -978,6 +987,11 @@ export class FleetManager {
978
987
  // Rotate fleet.log daily too (besides the startup size check above), so a
979
988
  // long-running fleet doesn't accumulate an unbounded log.
980
989
  rotateLogIfNeeded(join(this.dataDir, "fleet.log"));
990
+ // Instance output.log is pipe-pane (TUI ANSI). Daemon health ticks also
991
+ // rotate it; this is the daily safety net for idle/stopped instances.
992
+ for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
993
+ rotateLogIfNeeded(join(this.dataDir, "instances", name, "output.log"));
994
+ }
981
995
  }, () => {
982
996
  const instances = Object.keys(this.fleetConfig?.instances ?? {});
983
997
  const costMap = new Map();
@@ -1931,19 +1945,38 @@ export class FleetManager {
1931
1945
  this.logger.info({ adapterId, type: channelConfig.type }, "Additional adapter started");
1932
1946
  }
1933
1947
  /** Connect IPC to a single instance with all handlers */
1934
- async connectIpcToInstance(name) {
1948
+ connectIpcToInstance(name) {
1949
+ const inFlight = this.ipcConnectInFlight.get(name);
1950
+ if (inFlight)
1951
+ return inFlight;
1952
+ const connection = this.connectIpcToInstanceInternal(name)
1953
+ .finally(() => {
1954
+ if (this.ipcConnectInFlight.get(name) === connection) {
1955
+ this.ipcConnectInFlight.delete(name);
1956
+ }
1957
+ });
1958
+ this.ipcConnectInFlight.set(name, connection);
1959
+ return connection;
1960
+ }
1961
+ async connectIpcToInstanceInternal(name) {
1935
1962
  // Close existing client to prevent socket leak on reconnect
1936
1963
  const existing = this.instanceIpcClients.get(name);
1937
1964
  if (existing) {
1938
- this.ipcStoppingInstances.add(name);
1965
+ // Remove application listeners before destroying the socket. Even if a
1966
+ // future regression creates two clients, the replaced one cannot keep
1967
+ // handling fleet_outbound messages as an orphan.
1968
+ existing.removeAllListeners();
1939
1969
  try {
1940
- existing.close();
1970
+ await existing.close();
1941
1971
  }
1942
1972
  catch (err) {
1943
1973
  this.logger.debug({ err, name }, "IPC client close failed (likely already closed)");
1944
1974
  }
1945
- this.instanceIpcClients.delete(name);
1946
- this.ipcStoppingInstances.delete(name);
1975
+ finally {
1976
+ if (this.instanceIpcClients.get(name) === existing) {
1977
+ this.instanceIpcClients.delete(name);
1978
+ }
1979
+ }
1947
1980
  }
1948
1981
  const sockPath = join(this.getInstanceDir(name), "channel.sock");
1949
1982
  if (!existsSync(sockPath))
@@ -2021,6 +2054,10 @@ export class FleetManager {
2021
2054
  }
2022
2055
  // Auto-reconnect on disconnect (unless intentionally stopping)
2023
2056
  ipc.on("disconnect", () => {
2057
+ // A delayed event from a replaced/stale client must never delete the
2058
+ // current connection or start another reconnect loop.
2059
+ if (this.instanceIpcClients.get(name) !== ipc)
2060
+ return;
2024
2061
  this.instanceIpcClients.delete(name);
2025
2062
  if (this.ipcStoppingInstances.has(name))
2026
2063
  return;
@@ -2032,7 +2069,20 @@ export class FleetManager {
2032
2069
  }
2033
2070
  }
2034
2071
  /** Attempt IPC reconnection with exponential backoff */
2035
- async ipcReconnect(name) {
2072
+ ipcReconnect(name) {
2073
+ const inFlight = this.ipcReconnectInFlight.get(name);
2074
+ if (inFlight)
2075
+ return inFlight;
2076
+ const reconnect = this.runIpcReconnect(name)
2077
+ .finally(() => {
2078
+ if (this.ipcReconnectInFlight.get(name) === reconnect) {
2079
+ this.ipcReconnectInFlight.delete(name);
2080
+ }
2081
+ });
2082
+ this.ipcReconnectInFlight.set(name, reconnect);
2083
+ return reconnect;
2084
+ }
2085
+ async runIpcReconnect(name) {
2036
2086
  for (let attempt = 1;; attempt++) {
2037
2087
  if (this.ipcStoppingInstances.has(name) || !this.daemons.has(name))
2038
2088
  return;
@@ -4771,7 +4821,7 @@ When users create specialized instances, suggest these configurations:
4771
4821
  ...(preTaskCommand ? { pre_task_command: preTaskCommand } : {}),
4772
4822
  };
4773
4823
  const topicMode = this.fleetConfig?.channel?.mode === "topic";
4774
- await this.startInstance(instanceName, config, topicMode);
4824
+ await this.startInstance(instanceName, config, topicMode, "classic");
4775
4825
  }
4776
4826
  /** Handle /start slash command — register classic channel */
4777
4827
  async handleClassicStart(channelId, channelName, userId, guildId, adapterId, backend) {
@@ -5372,6 +5422,10 @@ When users create specialized instances, suggest these configurations:
5372
5422
  // /view routes accept the read-only view.token (or web.token) and do
5373
5423
  // their own per-method auth in view-api.ts — skip the web-token gate.
5374
5424
  }
5425
+ else if (isUsagePath(new URL(req.url ?? "/", `http://localhost:${port}`).pathname)) {
5426
+ // /api/ai-usage is read-only GET data for the /view Usage panel — open
5427
+ // like the other /view data routes (usage-api.ts rejects non-GET).
5428
+ }
5375
5429
  else {
5376
5430
  // All other endpoints require a valid token (query ?token= or X-Agend-Token header).
5377
5431
  // /ui/* will also re-check in web-api.ts, which is harmless.
@@ -5400,20 +5454,22 @@ When users create specialized instances, suggest these configurations:
5400
5454
  if (req.method === "GET" && req.url === "/status") {
5401
5455
  const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => {
5402
5456
  const statusFile = join(this.getInstanceDir(name), "statusline.json");
5403
- let context_pct = 0;
5404
5457
  let cost = 0;
5405
5458
  try {
5406
5459
  const data = JSON.parse(readFileSync(statusFile, "utf-8"));
5407
- context_pct = data.context_window?.used_percentage ?? 0;
5408
5460
  cost = data.cost?.total_cost_usd ?? 0;
5409
5461
  }
5410
5462
  catch (err) {
5411
5463
  this.logger.debug({ err, name }, "statusline.json read failed (/status)");
5412
5464
  }
5465
+ const backend = this.fleetConfig?.instances[name]?.backend
5466
+ ?? this.fleetConfig?.defaults?.backend
5467
+ ?? "claude-code";
5468
+ const { context } = resolveInstanceContext(this.dataDir, name, backend);
5413
5469
  return {
5414
5470
  name,
5415
5471
  status: this.getInstanceStatus(name),
5416
- context_pct,
5472
+ context_pct: context ?? 0,
5417
5473
  cost,
5418
5474
  };
5419
5475
  });
@@ -5584,6 +5640,8 @@ When users create specialized instances, suggest these configurations:
5584
5640
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
5585
5641
  if (handleViewRequest(req, res, url, this))
5586
5642
  return;
5643
+ if (handleUsageRequest(req, res, url, this))
5644
+ return;
5587
5645
  if (handleSettingsRequest(req, res, url, this))
5588
5646
  return;
5589
5647
  if (handleWebRequest(req, res, url, this))
@@ -5630,20 +5688,35 @@ When users create specialized instances, suggest these configurations:
5630
5688
  this.logger.info({ url: `http://localhost:${port}/view?token=${this.viewToken}` }, "Web View available");
5631
5689
  }
5632
5690
  getUiStatus() {
5633
- const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => {
5691
+ const fleetNames = Object.keys(this.fleetConfig?.instances ?? {});
5692
+ // Classic rooms live only in classicBot.yaml — /api/profiles merges them into
5693
+ // the View roster, but previously getUiStatus skipped them so context_pct was
5694
+ // always 0 (live map miss → l?.context_pct ?? 0).
5695
+ const classicOnly = (this.classicChannels?.getAll() ?? [])
5696
+ .map(ch => ch.instanceName)
5697
+ .filter(name => !fleetNames.includes(name));
5698
+ const names = [...fleetNames, ...classicOnly];
5699
+ const instances = names.map(name => {
5634
5700
  const statusFile = join(this.getInstanceDir(name), "statusline.json");
5635
- let context_pct = 0;
5636
5701
  let cost = 0;
5637
5702
  let model = "";
5638
5703
  try {
5639
5704
  const data = JSON.parse(readFileSync(statusFile, "utf-8"));
5640
- context_pct = data.context_window?.used_percentage ?? 0;
5641
5705
  cost = data.cost?.total_cost_usd ?? 0;
5642
5706
  model = data.model?.display_name ?? "";
5643
5707
  }
5644
5708
  catch (err) {
5645
5709
  this.logger.debug({ err, name }, "statusline.json read failed (getUiStatus)");
5646
5710
  }
5711
+ // Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
5712
+ const classic = classicOnly.includes(name);
5713
+ const backend = classic
5714
+ ? this.classicChannels.getBackendByInstance(name, this.fleetConfig?.defaults?.backend)
5715
+ : (this.fleetConfig?.instances[name]?.backend
5716
+ ?? this.fleetConfig?.defaults?.backend
5717
+ ?? "claude-code");
5718
+ const { context } = resolveInstanceContext(this.dataDir, name, backend);
5719
+ const context_pct = context ?? 0;
5647
5720
  return { name, status: this.getInstanceStatus(name), context_pct, cost, model };
5648
5721
  });
5649
5722
  return {