@songsid/agend 2.1.4-beta.9 → 2.1.4

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 (166) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +139 -5
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +523 -90
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +34 -5
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +5 -3
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +13 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +491 -14
  66. package/dist/daemon.js +2121 -276
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +402 -11
  74. package/dist/fleet-manager.js +2594 -422
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
  81. package/dist/general-knowledge/skills/session-management/SKILL.md +171 -8
  82. package/dist/instance-lifecycle.d.ts +114 -1
  83. package/dist/instance-lifecycle.js +404 -14
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.js +42 -4
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +1024 -320
  88. package/dist/locale.js.map +1 -1
  89. package/dist/login-flows.d.ts +91 -0
  90. package/dist/login-flows.js +170 -0
  91. package/dist/login-flows.js.map +1 -0
  92. package/dist/login-manager.d.ts +63 -0
  93. package/dist/login-manager.js +134 -0
  94. package/dist/login-manager.js.map +1 -0
  95. package/dist/network-family.d.ts +18 -0
  96. package/dist/network-family.js +20 -0
  97. package/dist/network-family.js.map +1 -0
  98. package/dist/outbound-handlers.d.ts +16 -1
  99. package/dist/outbound-handlers.js +174 -40
  100. package/dist/outbound-handlers.js.map +1 -1
  101. package/dist/outbound-schemas.d.ts +10 -3
  102. package/dist/outbound-schemas.js +10 -0
  103. package/dist/outbound-schemas.js.map +1 -1
  104. package/dist/pane-input-residue.d.ts +52 -0
  105. package/dist/pane-input-residue.js +107 -0
  106. package/dist/pane-input-residue.js.map +1 -0
  107. package/dist/process-memory.d.ts +6 -0
  108. package/dist/process-memory.js +45 -0
  109. package/dist/process-memory.js.map +1 -1
  110. package/dist/reply-dedup.d.ts +7 -8
  111. package/dist/reply-dedup.js +0 -0
  112. package/dist/reply-dedup.js.map +1 -1
  113. package/dist/restart-progress.d.ts +2 -0
  114. package/dist/restart-progress.js +3 -0
  115. package/dist/restart-progress.js.map +1 -1
  116. package/dist/scheduler/db.d.ts +12 -0
  117. package/dist/scheduler/db.js +59 -0
  118. package/dist/scheduler/db.js.map +1 -1
  119. package/dist/service-installer.d.ts +11 -0
  120. package/dist/service-installer.js +84 -18
  121. package/dist/service-installer.js.map +1 -1
  122. package/dist/settings-api.js +1 -1
  123. package/dist/settings-api.js.map +1 -1
  124. package/dist/setup-wizard.js +2 -2
  125. package/dist/setup-wizard.js.map +1 -1
  126. package/dist/spawn-gate.d.ts +30 -0
  127. package/dist/spawn-gate.js +79 -0
  128. package/dist/spawn-gate.js.map +1 -0
  129. package/dist/steer-capability.d.ts +1 -0
  130. package/dist/steer-capability.js +6 -0
  131. package/dist/steer-capability.js.map +1 -0
  132. package/dist/storm-window.d.ts +83 -0
  133. package/dist/storm-window.js +251 -0
  134. package/dist/storm-window.js.map +1 -0
  135. package/dist/tips.d.ts +44 -0
  136. package/dist/tips.js +355 -0
  137. package/dist/tips.js.map +1 -0
  138. package/dist/tmux-manager.d.ts +16 -0
  139. package/dist/tmux-manager.js +110 -25
  140. package/dist/tmux-manager.js.map +1 -1
  141. package/dist/topic-commands.d.ts +31 -7
  142. package/dist/topic-commands.js +342 -70
  143. package/dist/topic-commands.js.map +1 -1
  144. package/dist/transcript-sources.d.ts +35 -10
  145. package/dist/transcript-sources.js +190 -38
  146. package/dist/transcript-sources.js.map +1 -1
  147. package/dist/types.d.ts +6 -1
  148. package/dist/ui/dashboard.html +55 -32
  149. package/dist/ui/settings.html +200 -60
  150. package/dist/ui/view.html +147 -30
  151. package/dist/usage/format-rich.d.ts +1 -1
  152. package/dist/usage/format-rich.js +28 -24
  153. package/dist/usage/format-rich.js.map +1 -1
  154. package/dist/usage/i18n-keys.d.ts +7 -0
  155. package/dist/usage/i18n-keys.js +34 -0
  156. package/dist/usage/i18n-keys.js.map +1 -0
  157. package/dist/usage/i18n.d.ts +5 -0
  158. package/dist/usage/i18n.js +27 -0
  159. package/dist/usage/i18n.js.map +1 -0
  160. package/dist/usage/providers.d.ts +21 -0
  161. package/dist/usage/providers.js +153 -75
  162. package/dist/usage/providers.js.map +1 -1
  163. package/dist/usage/usage-api.d.ts +11 -3
  164. package/dist/usage/usage-api.js +61 -24
  165. package/dist/usage/usage-api.js.map +1 -1
  166. package/package.json +2 -2
@@ -22,6 +22,8 @@ export interface OutboundContext {
22
22
  name: string;
23
23
  backend?: string;
24
24
  model?: string;
25
+ displayName?: string;
26
+ description?: string;
25
27
  channelId: string;
26
28
  adapterId?: string;
27
29
  }[];
@@ -34,7 +36,9 @@ export interface OutboundContext {
34
36
  restartSingleInstance(name: string): Promise<void>;
35
37
  connectIpcToInstance(name: string): Promise<void>;
36
38
  /** FleetManager facade that wakes paused instances before delivery. */
37
- deliverToInstance?(instanceName: string, payload: Record<string, unknown>): Promise<void>;
39
+ deliverToInstance?(instanceName: string, payload: Record<string, unknown>, options?: OutboundDeliveryOptions): Promise<void>;
40
+ /** True while an earlier idle-gated delivery still owns this target's FIFO tail. */
41
+ hasPendingIdleGatedDelivery?(instanceName: string): boolean;
38
42
  saveFleetConfig(): void;
39
43
  queueMirrorMessage?(text: string): void;
40
44
  getAdapterForInstance?(name: string): ChannelAdapter | null;
@@ -50,6 +54,8 @@ export interface OutboundContext {
50
54
  clearCancelButton?(instanceName: string): void;
51
55
  clearCancelButtonByCorrelation?(correlationId: string): void;
52
56
  getInstanceExecutionState?(name: string): "idle" | "working" | "stuck" | "paused" | null;
57
+ /** Subscription provider IDs used by running or paused fleet/Classic instances. */
58
+ getActiveUsageProviderIds?(): ReadonlySet<string>;
53
59
  /** Effective process state used to annotate cross-instance delivery results. */
54
60
  getInstanceStatus?(name: string): "running" | "paused" | "stopped" | "crashed";
55
61
  resolveInstanceModel?(name: string): {
@@ -58,6 +64,15 @@ export interface OutboundContext {
58
64
  display: string;
59
65
  reason?: string;
60
66
  };
67
+ listModelCatalog?(opts: {
68
+ backend?: string;
69
+ instanceName?: string;
70
+ }): Promise<import("./fleet-manager.js").ModelCatalog>;
71
+ }
72
+ interface OutboundDeliveryOptions {
73
+ isCrossInstance?: boolean;
74
+ waitForIdle?: boolean;
75
+ idleTimeoutMs?: number;
61
76
  }
62
77
  /** Metadata extracted from the raw outbound message. */
63
78
  export interface OutboundMeta {
@@ -1,8 +1,36 @@
1
1
  import { resolve as pathResolve, isAbsolute } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
4
- import { BroadcastArgs, CreateInstanceArgs, CreateTeamArgs, DelegateTaskArgs, DeleteInstanceArgs, DeleteTeamArgs, DeployTemplateArgs, DescribeInstanceArgs, ListDeploymentsArgs, ListInstancesArgs, ListTeamsArgs, ReplaceInstanceArgs, ReportResultArgs, RequestInformationArgs, RestartInstanceArgs, SendToInstanceArgs, PauseInstanceArgs, WakeInstanceArgs, StopInstanceArgs, GetInstanceLogsArgs, GetEffortArgs, GetUsageArgs, GetFleetConfigArgs, UpdateInstanceConfigArgs, UpdateFleetDefaultsArgs, StartInstanceArgs, TeardownDeploymentArgs, UpdateTeamArgs, validateArgs, } from "./outbound-schemas.js";
4
+ import { DEFAULT_MAX_CROSS_INSTANCE_MESSAGE_BYTES } from "./config.js";
5
+ import { t } from "./locale.js";
6
+ import { truncatePreview } from "./channel/markdown-chunk.js";
7
+ import { backendSupportsSteer } from "./steer-capability.js";
8
+ import { formatCrossInstanceInboundMessage, MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES, } from "./cross-instance-envelope.js";
9
+ import { BroadcastArgs, CreateInstanceArgs, CreateTeamArgs, DelegateTaskArgs, DeleteInstanceArgs, DeleteTeamArgs, DeployTemplateArgs, DescribeInstanceArgs, ListDeploymentsArgs, ListInstancesArgs, ListTeamsArgs, ReplaceInstanceArgs, ReportResultArgs, RequestInformationArgs, RestartInstanceArgs, SendToInstanceArgs, PauseInstanceArgs, WakeInstanceArgs, StopInstanceArgs, GetInstanceLogsArgs, GetEffortArgs, GetUsageArgs, ListModelsArgs, GetFleetConfigArgs, UpdateInstanceConfigArgs, UpdateFleetDefaultsArgs, StartInstanceArgs, TeardownDeploymentArgs, UpdateTeamArgs, validateArgs, } from "./outbound-schemas.js";
5
10
  const HOME_DIR = homedir();
11
+ function formatByteSize(bytes) {
12
+ if (bytes < 1024)
13
+ return `${bytes} B`;
14
+ const kib = bytes / 1024;
15
+ return `${Number.isInteger(kib) ? kib : kib.toFixed(1)} KiB`;
16
+ }
17
+ /** Hard rejects must happen while the originating MCP request can still fail. */
18
+ function rejectOversizedCrossInstanceMessage(ctx, message, respond) {
19
+ const limit = ctx.fleetConfig?.defaults?.max_cross_instance_message_bytes
20
+ ?? DEFAULT_MAX_CROSS_INSTANCE_MESSAGE_BYTES;
21
+ const actual = Buffer.byteLength(message, "utf8");
22
+ if (actual <= limit)
23
+ return false;
24
+ respond(null, t("cross_instance.message_too_large", formatByteSize(actual), formatByteSize(limit)));
25
+ return true;
26
+ }
27
+ function rejectOversizedCrossInstanceEnvelope(message, meta, respond) {
28
+ const actual = Buffer.byteLength(formatCrossInstanceInboundMessage(message, meta), "utf8");
29
+ if (actual <= MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES)
30
+ return false;
31
+ respond(null, t("cross_instance.envelope_too_large", formatByteSize(actual), formatByteSize(MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES)));
32
+ return true;
33
+ }
6
34
  /**
7
35
  * Sanitize an error for inclusion in outbound responses sent to agents.
8
36
  * Logs the full error server-side, then returns a redacted message that
@@ -24,9 +52,12 @@ function sanitizeError(err, ctx, operation) {
24
52
  * The fallback keeps standalone handler harnesses/backward-compatible embedders
25
53
  * working; FleetManager always supplies the facade in production.
26
54
  */
27
- async function deliverToInstance(ctx, instanceName, payload) {
55
+ async function deliverToInstance(ctx, instanceName, payload, options) {
28
56
  if (ctx.deliverToInstance) {
29
- await ctx.deliverToInstance(instanceName, payload);
57
+ if (options)
58
+ await ctx.deliverToInstance(instanceName, payload, options);
59
+ else
60
+ await ctx.deliverToInstance(instanceName, payload);
30
61
  return;
31
62
  }
32
63
  const ipc = ctx.instanceIpcClients.get(instanceName);
@@ -73,10 +104,10 @@ export function setCrossInstanceRetryForTests(cfg) {
73
104
  * sender's and the target's topic so the agent that sent it and any watching human
74
105
  * both find out.
75
106
  */
76
- async function deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetLabel, senderLabel, correlationId, payload) {
107
+ async function deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetLabel, senderLabel, correlationId, payload, deliveryOptions) {
77
108
  const attempt = async () => {
78
109
  try {
79
- await deliverToInstance(ctx, targetInstanceName, payload);
110
+ await deliverToInstance(ctx, targetInstanceName, payload, deliveryOptions);
80
111
  return null;
81
112
  }
82
113
  catch (err) {
@@ -106,6 +137,21 @@ async function deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetLabe
106
137
  if (targetInstanceName !== senderLabel)
107
138
  ctx.notifyInstanceTopic?.(targetInstanceName, notice);
108
139
  }
140
+ function effectiveBackendForTarget(ctx, instanceName) {
141
+ const configured = ctx.fleetConfig?.instances?.[instanceName]?.backend;
142
+ if (configured)
143
+ return configured;
144
+ const classic = ctx.classicChannels?.getAll().find(channel => channel.instanceName === instanceName);
145
+ if (classic) {
146
+ if (classic.backend)
147
+ return classic.backend;
148
+ return ctx.classicChannels?.getBackendByInstance?.(instanceName, ctx.fleetConfig?.defaults?.backend) ?? (ctx.fleetConfig ? ctx.fleetConfig.defaults?.backend ?? "claude-code" : undefined);
149
+ }
150
+ if (ctx.fleetConfig?.instances?.[instanceName]) {
151
+ return ctx.fleetConfig.defaults?.backend ?? "claude-code";
152
+ }
153
+ return undefined;
154
+ }
109
155
  // ── Handler implementations ─────────────────────────────────────────────
110
156
  const sendToInstance = async (ctx, rawArgs, respond, meta) => {
111
157
  const v = validateArgs(SendToInstanceArgs, rawArgs, "send_to_instance");
@@ -113,7 +159,9 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
113
159
  respond(null, v.error);
114
160
  return;
115
161
  }
116
- const { instance_name: targetName, message, request_kind: reqKind, requires_reply, task_summary, working_directory, branch, correlation_id: parsedCorrelationId } = v.data;
162
+ const { instance_name: targetName, message, steer, request_kind: reqKind, requires_reply, task_summary, working_directory, branch, correlation_id: parsedCorrelationId } = v.data;
163
+ if (rejectOversizedCrossInstanceMessage(ctx, message, respond))
164
+ return;
117
165
  const senderLabel = meta.senderSessionName ?? meta.instanceName;
118
166
  const isExternalSender = meta.senderSessionName != null && meta.senderSessionName !== meta.instanceName;
119
167
  let targetIpc = ctx.instanceIpcClients.get(targetName);
@@ -145,6 +193,33 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
145
193
  respond(null, `Instance '${targetName}' is stopped (target_state=stopped). Use start_instance('${targetName}') to start it first.`);
146
194
  return;
147
195
  }
196
+ // Pane steering can target only the hosting instance's own CLI. External MCP
197
+ // sessions share that daemon but are separate conversations, so steering the
198
+ // host pane would deliver to the wrong recipient; safely keep their ordinary
199
+ // targetSession-aware idle queue instead.
200
+ const isExternalSession = targetSession !== targetInstanceName;
201
+ const targetBackend = steer === true
202
+ ? effectiveBackendForTarget(ctx, targetInstanceName)
203
+ : undefined;
204
+ const steerCapable = steer === true
205
+ && !isExternalSession
206
+ && targetBackend !== undefined
207
+ && backendSupportsSteer(targetBackend);
208
+ // A supplement must not jump ahead of the work it is meant to amend. The
209
+ // facade publishes its idle-gated tail synchronously, so a send immediately
210
+ // followed by steer observes the predecessor and joins the same FIFO queue.
211
+ const queuedPredecessor = steerCapable
212
+ && ctx.hasPendingIdleGatedDelivery?.(targetInstanceName) === true;
213
+ const useSteer = steerCapable && !queuedPredecessor;
214
+ const steerFallbackWarning = steer === true && !useSteer
215
+ ? isExternalSession
216
+ ? "Steer was not applied because external sessions cannot be targeted safely at pane level. The message was safely queued for idle delivery instead."
217
+ : queuedPredecessor
218
+ ? "Steer was not applied because a previous message to this target is still queued and steering would overtake it. The supplement was safely queued behind it for idle delivery instead."
219
+ : targetBackend === undefined
220
+ ? "Steer was not applied because the target backend could not be confirmed. The message was safely queued for idle delivery instead."
221
+ : `Steer was not applied because the '${targetBackend}' backend cannot accept mid-turn input. The message was safely queued for idle delivery instead.`
222
+ : undefined;
148
223
  const correlationId = parsedCorrelationId || `cid-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
149
224
  const ipcMeta = {
150
225
  chat_id: "",
@@ -171,6 +246,8 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
171
246
  const senderDisplay = ctx.fleetConfig?.instances[senderLabel]?.display_name;
172
247
  if (senderDisplay && senderDisplay !== senderLabel)
173
248
  ipcMeta.from_display = senderDisplay;
249
+ if (rejectOversizedCrossInstanceEnvelope(message, ipcMeta, respond))
250
+ return;
174
251
  // Fire-and-queue: hand the message to the fleet and respond immediately.
175
252
  // deliverToInstance owns waking a paused target, the idle gate and retries;
176
253
  // awaiting it here blocked this call for as long as the target stayed busy
@@ -180,7 +257,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
180
257
  // failing past this point is a delivery problem to log, not a caller error.
181
258
  // Retries and, on final failure, tells both agents' topics — see
182
259
  // deliverCrossInstanceWithRetry. It never rejects, so nothing escapes here.
183
- void deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetName, senderLabel, correlationId, { type: "fleet_inbound", targetSession, content: message, meta: ipcMeta });
260
+ void deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetName, senderLabel, correlationId, { type: useSteer ? "steer" : "fleet_inbound", targetSession, content: message, meta: ipcMeta }, useSteer ? { isCrossInstance: true, waitForIdle: false } : undefined);
184
261
  // Show a cancel button on the target's topic so a watching user can interrupt
185
262
  // work started by another instance — but only for messages that actually put
186
263
  // the recipient to work (task/query). Informational report/update messages
@@ -219,7 +296,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
219
296
  const showFull = requestKind === "task" || requestKind === "query";
220
297
  const text = showFull
221
298
  ? `${notificationLabel}:\n${message}`
222
- : `${notificationLabel}: ${ipcMeta.task_summary ?? `${message.slice(0, 100)}${message.length > 100 ? "…" : ""}`}`;
299
+ : `${notificationLabel}: ${ipcMeta.task_summary ?? truncatePreview(message, 100)}`;
223
300
  targetAdapter.sendText(String(targetGroupId), text, { threadId: String(targetTopicId) })
224
301
  .catch(e => ctx.logger.warn({ err: e }, "Failed to post target topic notification"));
225
302
  }
@@ -238,22 +315,23 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
238
315
  ctx.logger.info(`✉ ${senderLabel} → ${targetName}: ${(message ?? "").slice(0, 100)}`);
239
316
  const taskSummary = ipcMeta.task_summary || (message ?? "").slice(0, 200);
240
317
  ctx.eventLog?.logActivity("message", senderLabel, taskSummary, targetName, ipcMeta.request_kind);
241
- ctx.queueMirrorMessage?.(`${senderLabel} → ${targetName}: ${(message ?? "").slice(0, 500)}${(message ?? "").length > 500 ? " […]" : ""}`);
318
+ ctx.queueMirrorMessage?.(`${senderLabel} → ${targetName}: ${truncatePreview(message ?? "", 500)}`);
319
+ const targetDaemon = ctx.lifecycle.daemons.get(targetInstanceName);
320
+ const targetStateWarning = targetDaemon?.isErrorState
321
+ ? targetDaemon.isCrashLoop
322
+ ? `${targetName} is in a crash loop — restart or replace required. Message delivered but may not be processed.`
323
+ : targetDaemon.lastErrorType === "rate_limit" || targetDaemon.lastErrorType === "timeout"
324
+ ? `${targetName} is rate-limited by provider — paused. Message delivered but may be delayed.`
325
+ : targetDaemon.lastErrorType === "auth_error"
326
+ ? `${targetName} has an authentication error — paused. Check credentials. Message delivered but may not be processed.`
327
+ : `${targetName} is in an error state (paused due to repeated errors). Message delivered but may not be processed.`
328
+ : undefined;
329
+ const warnings = [steerFallbackWarning, targetStateWarning].filter((value) => !!value);
242
330
  respond({ sent: true, queued: true, target: targetName, target_state: state,
243
331
  ...(state === "paused" ? { waking: true } : {}), correlation_id: correlationId,
244
- ...(ctx.lifecycle.daemons.get(targetInstanceName)?.isErrorState && {
245
- warning: (() => {
246
- const daemon = ctx.lifecycle.daemons.get(targetInstanceName);
247
- if (daemon.isCrashLoop)
248
- return `${targetName} is in a crash loop — restart or replace required. Message delivered but may not be processed.`;
249
- const errType = daemon.lastErrorType;
250
- if (errType === "rate_limit" || errType === "timeout")
251
- return `${targetName} is rate-limited by provider — paused. Message delivered but may be delayed.`;
252
- if (errType === "auth_error")
253
- return `${targetName} has an authentication error — paused. Check credentials. Message delivered but may not be processed.`;
254
- return `${targetName} is in an error state (paused due to repeated errors). Message delivered but may not be processed.`;
255
- })(),
256
- }),
332
+ ...(useSteer ? { delivery_mode: "steer" } : {}),
333
+ ...(steerFallbackWarning ? { delivery_mode: "idle_queue" } : {}),
334
+ ...(warnings.length > 0 ? { warning: warnings.join(" ") } : {}),
257
335
  });
258
336
  };
259
337
  const listInstances = (ctx, rawArgs, respond, meta) => {
@@ -297,8 +375,8 @@ const listInstances = (ctx, rawArgs, respond, meta) => {
297
375
  instance_state: ctx.getInstanceExecutionState?.(ch.instanceName) ?? null,
298
376
  working_directory: "",
299
377
  topic_id: ch.channelId,
300
- display_name: `classic: ${ch.name}`,
301
- description: `ClassicBot channel (${ch.name})`,
378
+ display_name: ch.displayName ?? `classic: ${ch.name}`,
379
+ description: ch.description ?? `ClassicBot channel (${ch.name})`,
302
380
  backend: ctx.classicChannels.getBackendByInstance?.(ch.instanceName, ctx.fleetConfig?.defaults?.backend) ?? ch.backend ?? ctx.fleetConfig?.defaults?.backend ?? "claude-code",
303
381
  model: ctx.resolveInstanceModel?.(ch.instanceName).model
304
382
  ?? ctx.classicChannels.getModel?.(ch.channelId, ch.adapterId, ctx.fleetConfig?.defaults?.model)
@@ -558,12 +636,46 @@ const getUsage = async (ctx, rawArgs, respond) => {
558
636
  try {
559
637
  // Same structure as GET /api/ai-usage, same 5-minute cache behind it.
560
638
  const { getUsageSnapshot } = await import("./usage/usage-api.js");
561
- respond(await getUsageSnapshot(v.data.force === true));
639
+ respond(await getUsageSnapshot(v.data.force === true, ctx.getActiveUsageProviderIds?.()));
562
640
  }
563
641
  catch (err) {
564
642
  respond(null, `Usage fetch failed: ${err.message}`);
565
643
  }
566
644
  };
645
+ /**
646
+ * Enumerate a backend's models. Read-only and best-effort by design: the
647
+ * catalog is an aid for picking a name, and AgEnD passes model strings through
648
+ * to the CLI regardless, so an empty list is a reportable answer rather than an
649
+ * error. The one real error is naming an instance that does not exist — that is
650
+ * a mistake in the call, and silently answering with the fleet default backend
651
+ * would hide it.
652
+ */
653
+ const listModels = async (ctx, rawArgs, respond) => {
654
+ const v = validateArgs(ListModelsArgs, rawArgs, "list_models");
655
+ if (!v.ok) {
656
+ respond(null, v.error);
657
+ return;
658
+ }
659
+ const { backend, instance_name } = v.data;
660
+ if (!ctx.listModelCatalog) {
661
+ respond(null, "list_models is unavailable on this fleet");
662
+ return;
663
+ }
664
+ if (instance_name) {
665
+ const known = Boolean(ctx.fleetConfig?.instances?.[instance_name])
666
+ || (ctx.classicChannels?.getAll() ?? []).some(c => c.instanceName === instance_name);
667
+ if (!known) {
668
+ respond(null, `Instance not found: ${instance_name}`);
669
+ return;
670
+ }
671
+ }
672
+ try {
673
+ respond(await ctx.listModelCatalog({ backend, instanceName: instance_name }));
674
+ }
675
+ catch (err) {
676
+ respond(null, `Model listing failed: ${err.message}`);
677
+ }
678
+ };
567
679
  const getInstanceLogs = async (ctx, rawArgs, respond) => {
568
680
  const v = validateArgs(GetInstanceLogsArgs, rawArgs, "get_instance_logs");
569
681
  if (!v.ok) {
@@ -614,7 +726,11 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
614
726
  }
615
727
  const inst = ctx.fleetConfig?.instances[v.data.name];
616
728
  if (!inst) {
617
- respond(null, `Instance '${v.data.name}' not found in fleet config`);
729
+ const isClassic = (ctx.classicChannels?.getAll() ?? [])
730
+ .some(channel => channel.instanceName === v.data.name);
731
+ respond(null, isClassic
732
+ ? "This is a ClassicBot instance (managed in classicBot.yaml). Use set_display_name or set_description to configure it."
733
+ : `Instance '${v.data.name}' not found in fleet config`);
618
734
  return;
619
735
  }
620
736
  const patch = v.data.config;
@@ -654,6 +770,9 @@ const updateFleetDefaults = (ctx, rawArgs, respond) => {
654
770
  defaults.model = patch.model;
655
771
  if (patch.auto_pause_after !== undefined)
656
772
  defaults.auto_pause_after = patch.auto_pause_after;
773
+ if (patch.max_cross_instance_message_bytes !== undefined) {
774
+ defaults.max_cross_instance_message_bytes = patch.max_cross_instance_message_bytes;
775
+ }
657
776
  ctx.fleetConfig.defaults = defaults;
658
777
  try {
659
778
  ctx.saveFleetConfig();
@@ -682,6 +801,9 @@ function wrapAsSend(schema, toolName, buildArgs, warnMissing) {
682
801
  ...extra,
683
802
  instance_name: targetName,
684
803
  message: body,
804
+ ...(v.data.steer !== undefined
805
+ ? { steer: v.data.steer }
806
+ : {}),
685
807
  request_kind: kind,
686
808
  requires_reply: reply,
687
809
  task_summary: summary,
@@ -765,8 +887,31 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
765
887
  return;
766
888
  }
767
889
  const { message, targets, team: teamName, tags: filterTags, task_summary, request_kind, requires_reply } = v.data;
890
+ if (rejectOversizedCrossInstanceMessage(ctx, message, respond))
891
+ return;
768
892
  const senderLabel = meta.senderSessionName ?? meta.instanceName;
769
893
  const senderDisplay = ctx.fleetConfig?.instances[senderLabel]?.display_name;
894
+ const makeBroadcastMeta = (correlationId, messageId = `bcast-${Date.now()}`) => {
895
+ const ipcMeta = {
896
+ chat_id: "", message_id: messageId, user: `instance:${senderLabel}`,
897
+ user_id: `instance:${senderLabel}`, ts: new Date().toISOString(), thread_id: "",
898
+ from_instance: senderLabel, correlation_id: correlationId,
899
+ };
900
+ if (request_kind)
901
+ ipcMeta.request_kind = request_kind;
902
+ if (requires_reply != null)
903
+ ipcMeta.requires_reply = String(requires_reply);
904
+ if (task_summary)
905
+ ipcMeta.task_summary = task_summary;
906
+ if (senderDisplay && senderDisplay !== senderLabel)
907
+ ipcMeta.from_display = senderDisplay;
908
+ return ipcMeta;
909
+ };
910
+ // Validate once before dispatching any target. The placeholders are the
911
+ // longest values generated below, so every real per-target envelope fits if
912
+ // this one does. Keeping this outside the loop prevents partial broadcasts.
913
+ if (rejectOversizedCrossInstanceEnvelope(message, makeBroadcastMeta("bcast-9999999999999-zzzzzz", "bcast-9999999999999"), respond))
914
+ return;
770
915
  // Resolve target list: team, explicit targets, tag filter, or all running
771
916
  let targetNames;
772
917
  if (teamName) {
@@ -803,19 +948,7 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
803
948
  continue;
804
949
  }
805
950
  const correlationId = `bcast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
806
- const ipcMeta = {
807
- chat_id: "", message_id: `bcast-${Date.now()}`, user: `instance:${senderLabel}`,
808
- user_id: `instance:${senderLabel}`, ts: new Date().toISOString(), thread_id: "",
809
- from_instance: senderLabel, correlation_id: correlationId,
810
- };
811
- if (request_kind)
812
- ipcMeta.request_kind = request_kind;
813
- if (requires_reply != null)
814
- ipcMeta.requires_reply = String(requires_reply);
815
- if (task_summary)
816
- ipcMeta.task_summary = task_summary;
817
- if (senderDisplay && senderDisplay !== senderLabel)
818
- ipcMeta.from_display = senderDisplay;
951
+ const ipcMeta = makeBroadcastMeta(correlationId);
819
952
  // Same at-least-once treatment as send_to_instance: retry, then surface a
820
953
  // final failure on both topics instead of dying in a warn-level log line.
821
954
  void deliverCrossInstanceWithRetry(ctx, hostInstance, targetName, senderLabel, correlationId, { type: "fleet_inbound", targetSession: targetName, content: message, meta: ipcMeta });
@@ -826,7 +959,7 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
826
959
  for (const target of sentTo) {
827
960
  ctx.eventLog?.logActivity("message", senderLabel, summary, target);
828
961
  }
829
- ctx.queueMirrorMessage?.(`📢 ${senderLabel} → [${sentTo.join(", ")}]: ${message.slice(0, 500)}${message.length > 500 ? " […]" : ""}`);
962
+ ctx.queueMirrorMessage?.(`📢 ${senderLabel} → [${sentTo.join(", ")}]: ${truncatePreview(message, 500)}`);
830
963
  respond({ sent_to: sentTo, failed, count: sentTo.length, queued: true });
831
964
  };
832
965
  // ── Teams ────────────────────────────────────────────────────────────────
@@ -1173,6 +1306,7 @@ export const outboundHandlers = new Map([
1173
1306
  ["get_fleet_status", getFleetStatus],
1174
1307
  ["get_effort", getEffort],
1175
1308
  ["get_usage", getUsage],
1309
+ ["list_models", listModels],
1176
1310
  ["get_instance_logs", getInstanceLogs],
1177
1311
  ["get_fleet_config", getFleetConfig],
1178
1312
  ["update_instance_config", updateInstanceConfig],