@songsid/agend 2.2.0-alpha.9 → 2.2.0-beta.10

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 (377) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +21 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +2 -2
  12. package/dist/auth-api.js +4 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +3 -0
  15. package/dist/backend/antigravity.js +9 -0
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +1 -1
  33. package/dist/backend/effort-metadata.js +3 -2
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +33 -0
  48. package/dist/backend/launch-effort.js +123 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +33 -4
  54. package/dist/backend/types.js +2 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +26 -1
  66. package/dist/channel/adapters/discord.js +98 -3
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +26 -2
  69. package/dist/channel/adapters/telegram.js +123 -19
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +24 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.js +9 -3
  83. package/dist/channel/tool-router.js.map +1 -1
  84. package/dist/channel/types.d.ts +41 -3
  85. package/dist/cli.js +133 -28
  86. package/dist/cli.js.map +1 -1
  87. package/dist/command-table.d.ts +2 -2
  88. package/dist/command-table.js +6 -3
  89. package/dist/command-table.js.map +1 -1
  90. package/dist/config-validator.js +59 -0
  91. package/dist/config-validator.js.map +1 -1
  92. package/dist/cross-instance-envelope.d.ts +15 -2
  93. package/dist/cross-instance-envelope.js +29 -5
  94. package/dist/cross-instance-envelope.js.map +1 -1
  95. package/dist/daemon.d.ts +156 -1
  96. package/dist/daemon.js +764 -89
  97. package/dist/daemon.js.map +1 -1
  98. package/dist/delivery-outbox.d.ts +62 -7
  99. package/dist/delivery-outbox.js +309 -22
  100. package/dist/delivery-outbox.js.map +1 -1
  101. package/dist/delivery-reconciliation.d.ts +18 -1
  102. package/dist/delivery-reconciliation.js +67 -2
  103. package/dist/delivery-reconciliation.js.map +1 -1
  104. package/dist/fleet-context.d.ts +10 -1
  105. package/dist/fleet-manager.d.ts +352 -10
  106. package/dist/fleet-manager.js +1925 -175
  107. package/dist/fleet-manager.js.map +1 -1
  108. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  111. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  112. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  113. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  114. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  115. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  116. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  117. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  118. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  119. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  120. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  121. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  122. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  123. package/dist/general-knowledge/steering/core-rules.md +8 -7
  124. package/dist/host-memory.d.ts +10 -0
  125. package/dist/host-memory.js +33 -1
  126. package/dist/host-memory.js.map +1 -1
  127. package/dist/human-delivery-receipts.d.ts +83 -0
  128. package/dist/human-delivery-receipts.js +194 -0
  129. package/dist/human-delivery-receipts.js.map +1 -0
  130. package/dist/instance-avatar.d.ts +36 -0
  131. package/dist/instance-avatar.js +57 -0
  132. package/dist/instance-avatar.js.map +1 -0
  133. package/dist/instance-config-impact.js +6 -0
  134. package/dist/instance-config-impact.js.map +1 -1
  135. package/dist/instance-lifecycle.d.ts +29 -6
  136. package/dist/instance-lifecycle.js +79 -28
  137. package/dist/instance-lifecycle.js.map +1 -1
  138. package/dist/instructions.d.ts +3 -1
  139. package/dist/instructions.js +11 -8
  140. package/dist/instructions.js.map +1 -1
  141. package/dist/kiro-log-gate.d.ts +42 -0
  142. package/dist/kiro-log-gate.js +39 -0
  143. package/dist/kiro-log-gate.js.map +1 -0
  144. package/dist/locale.js +204 -44
  145. package/dist/locale.js.map +1 -1
  146. package/dist/log-service.d.ts +99 -0
  147. package/dist/log-service.js +243 -0
  148. package/dist/log-service.js.map +1 -0
  149. package/dist/log-steps.d.ts +169 -0
  150. package/dist/log-steps.js +858 -0
  151. package/dist/log-steps.js.map +1 -0
  152. package/dist/login-controller.d.ts +20 -3
  153. package/dist/login-controller.js +71 -19
  154. package/dist/login-controller.js.map +1 -1
  155. package/dist/login-flows.d.ts +5 -2
  156. package/dist/login-flows.js +5 -4
  157. package/dist/login-flows.js.map +1 -1
  158. package/dist/memory-pressure.d.ts +13 -1
  159. package/dist/memory-pressure.js +50 -10
  160. package/dist/memory-pressure.js.map +1 -1
  161. package/dist/needs-you-hub.js +2 -0
  162. package/dist/needs-you-hub.js.map +1 -1
  163. package/dist/needs-you-live.d.ts +6 -0
  164. package/dist/needs-you-live.js +23 -3
  165. package/dist/needs-you-live.js.map +1 -1
  166. package/dist/next-steps.js +1 -1
  167. package/dist/next-steps.js.map +1 -1
  168. package/dist/outbound-handlers.d.ts +100 -0
  169. package/dist/outbound-handlers.js +263 -96
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +26 -15
  172. package/dist/outbound-schemas.js +24 -7
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-link-progress.d.ts +1 -1
  175. package/dist/public-link-progress.js +8 -2
  176. package/dist/public-link-progress.js.map +1 -1
  177. package/dist/public-preview.d.ts +101 -0
  178. package/dist/public-preview.js +236 -0
  179. package/dist/public-preview.js.map +1 -0
  180. package/dist/public-web-gateway.js +11 -4
  181. package/dist/public-web-gateway.js.map +1 -1
  182. package/dist/public-web-link.d.ts +24 -1
  183. package/dist/public-web-link.js +63 -9
  184. package/dist/public-web-link.js.map +1 -1
  185. package/dist/reply-buttons-controller.d.ts +3 -0
  186. package/dist/reply-buttons-controller.js +3 -2
  187. package/dist/reply-buttons-controller.js.map +1 -1
  188. package/dist/restart-guard.d.ts +6 -3
  189. package/dist/restart-guard.js +6 -3
  190. package/dist/restart-guard.js.map +1 -1
  191. package/dist/restart-progress.d.ts +33 -0
  192. package/dist/restart-progress.js +30 -0
  193. package/dist/restart-progress.js.map +1 -1
  194. package/dist/service-activation.d.ts +15 -7
  195. package/dist/service-activation.js +46 -29
  196. package/dist/service-activation.js.map +1 -1
  197. package/dist/service-installer.d.ts +31 -0
  198. package/dist/service-installer.js +41 -2
  199. package/dist/service-installer.js.map +1 -1
  200. package/dist/settings-api.js +85 -25
  201. package/dist/settings-api.js.map +1 -1
  202. package/dist/settings-change.js +72 -1
  203. package/dist/settings-change.js.map +1 -1
  204. package/dist/settings-effect.js +28 -4
  205. package/dist/settings-effect.js.map +1 -1
  206. package/dist/settings-http-confirmation.js +7 -0
  207. package/dist/settings-http-confirmation.js.map +1 -1
  208. package/dist/setup-form.d.ts +1 -1
  209. package/dist/setup-form.js +1 -1
  210. package/dist/spawn-gate.js +5 -1
  211. package/dist/spawn-gate.js.map +1 -1
  212. package/dist/spawn-hold-reply.d.ts +12 -0
  213. package/dist/spawn-hold-reply.js +39 -0
  214. package/dist/spawn-hold-reply.js.map +1 -0
  215. package/dist/status-emojis.d.ts +10 -3
  216. package/dist/status-emojis.js +11 -2
  217. package/dist/status-emojis.js.map +1 -1
  218. package/dist/systemd-peer-bus.d.ts +95 -0
  219. package/dist/systemd-peer-bus.js +430 -0
  220. package/dist/systemd-peer-bus.js.map +1 -0
  221. package/dist/systemd-runtime.d.ts +27 -1
  222. package/dist/systemd-runtime.js +69 -7
  223. package/dist/systemd-runtime.js.map +1 -1
  224. package/dist/tips.js +4 -4
  225. package/dist/tips.js.map +1 -1
  226. package/dist/tool-progress.js +3 -2
  227. package/dist/tool-progress.js.map +1 -1
  228. package/dist/topic-commands.d.ts +42 -2
  229. package/dist/topic-commands.js +92 -7
  230. package/dist/topic-commands.js.map +1 -1
  231. package/dist/transcript-delta-reader.d.ts +3 -3
  232. package/dist/transcript-delta-reader.js +8 -8
  233. package/dist/transcript-delta-reader.js.map +1 -1
  234. package/dist/transcript-jsonl.d.ts +80 -3
  235. package/dist/transcript-jsonl.js +147 -10
  236. package/dist/transcript-jsonl.js.map +1 -1
  237. package/dist/transcript-monitor.d.ts +19 -0
  238. package/dist/transcript-monitor.js +87 -15
  239. package/dist/transcript-monitor.js.map +1 -1
  240. package/dist/transcript-sources.d.ts +78 -0
  241. package/dist/transcript-sources.js +375 -64
  242. package/dist/transcript-sources.js.map +1 -1
  243. package/dist/transcript-turns.d.ts +15 -2
  244. package/dist/transcript-turns.js +15 -2
  245. package/dist/transcript-turns.js.map +1 -1
  246. package/dist/tunnel/cloudflared.d.ts +7 -15
  247. package/dist/tunnel/cloudflared.js +36 -1
  248. package/dist/tunnel/cloudflared.js.map +1 -1
  249. package/dist/tunnel/purpose-lane.d.ts +5 -1
  250. package/dist/tunnel/purpose-lane.js +14 -7
  251. package/dist/tunnel/purpose-lane.js.map +1 -1
  252. package/dist/tunnel/types.d.ts +5 -1
  253. package/dist/tunnel/types.js.map +1 -1
  254. package/dist/turn-resume.d.ts +2 -0
  255. package/dist/turn-resume.js +1 -1
  256. package/dist/turn-resume.js.map +1 -1
  257. package/dist/types.d.ts +25 -0
  258. package/dist/ui/chat-commands.js +1 -0
  259. package/dist/ui/chat-emojis.js +33 -0
  260. package/dist/ui/chat-mentions.js +63 -0
  261. package/dist/ui/chat-render.js +42 -2
  262. package/dist/ui/chat-store.js +2 -2
  263. package/dist/ui/chat-strings.js +38 -20
  264. package/dist/ui/chat-thread.js +69 -12
  265. package/dist/ui/chat-time.js +80 -0
  266. package/dist/ui/panel-chat.js +39 -10
  267. package/dist/ui/panel-details.js +2 -0
  268. package/dist/ui/panel-fleet.js +199 -57
  269. package/dist/ui/panel-log.js +428 -0
  270. package/dist/ui/panel-needs.js +6 -2
  271. package/dist/ui/panel-settings.js +137 -32
  272. package/dist/ui/preview-panel.js +55 -15
  273. package/dist/ui/preview-status.js +28 -0
  274. package/dist/ui/preview.js +44 -3
  275. package/dist/ui/settings-dialogs.js +7 -2
  276. package/dist/ui/settings-model.js +2 -1
  277. package/dist/ui/settings-strings.js +46 -4
  278. package/dist/ui/settings-token.js +1 -1
  279. package/dist/ui/shared/app-i18n.js +43 -17
  280. package/dist/ui/shared/app-prefs.js +250 -0
  281. package/dist/ui/shared/app-route.js +7 -2
  282. package/dist/ui/shared/app-shell.js +165 -8
  283. package/dist/ui/shared/app-store.js +5 -1
  284. package/dist/ui/shared/app-stream.js +23 -5
  285. package/dist/ui/shared/app.css +159 -23
  286. package/dist/ui/shared/app.js +11 -1
  287. package/dist/ui/shared/fonts/README.md +1 -1
  288. package/dist/ui/shared/header-tools.js +55 -5
  289. package/dist/ui/shared/instance-nav.js +241 -45
  290. package/dist/ui/shared/instance-switch.js +4 -4
  291. package/dist/ui/shared/panel-view.js +6 -0
  292. package/dist/ui/shared/signin.js +7 -7
  293. package/dist/ui/shared/theme.js +39 -7
  294. package/dist/ui/shared/tokens.css +10 -1
  295. package/dist/ui/shared/ui-icons.js +1 -0
  296. package/dist/ui/shared/usage-dialog.js +23 -5
  297. package/dist/ui/shared/view-strings.js +32 -4
  298. package/dist/ui/signin.html +1 -1
  299. package/dist/unrecognised-prompt.d.ts +23 -0
  300. package/dist/unrecognised-prompt.js +74 -0
  301. package/dist/unrecognised-prompt.js.map +1 -0
  302. package/dist/update-install.d.ts +28 -2
  303. package/dist/update-install.js +22 -4
  304. package/dist/update-install.js.map +1 -1
  305. package/dist/update-marker.d.ts +51 -0
  306. package/dist/update-marker.js +61 -0
  307. package/dist/update-marker.js.map +1 -1
  308. package/dist/update-preflight.d.ts +100 -0
  309. package/dist/update-preflight.js +264 -0
  310. package/dist/update-preflight.js.map +1 -0
  311. package/dist/updater-caller.d.ts +31 -0
  312. package/dist/updater-caller.js +109 -0
  313. package/dist/updater-caller.js.map +1 -0
  314. package/dist/usage/i18n-keys.d.ts +1 -1
  315. package/dist/usage/i18n-keys.js +1 -1
  316. package/dist/usage/i18n-keys.js.map +1 -1
  317. package/dist/usage/providers.d.ts +34 -5
  318. package/dist/usage/providers.js +160 -27
  319. package/dist/usage/providers.js.map +1 -1
  320. package/dist/usage/statusline-usage.d.ts +6 -2
  321. package/dist/usage/statusline-usage.js +10 -4
  322. package/dist/usage/statusline-usage.js.map +1 -1
  323. package/dist/view-api.d.ts +1 -1
  324. package/dist/view-api.js +38 -8
  325. package/dist/view-api.js.map +1 -1
  326. package/dist/wake-coordinator.d.ts +11 -0
  327. package/dist/wake-coordinator.js +38 -4
  328. package/dist/wake-coordinator.js.map +1 -1
  329. package/dist/web-api.d.ts +49 -2
  330. package/dist/web-api.js +421 -31
  331. package/dist/web-api.js.map +1 -1
  332. package/dist/web-auth.d.ts +2 -2
  333. package/dist/web-auth.js +9 -4
  334. package/dist/web-auth.js.map +1 -1
  335. package/dist/web-commands.d.ts +27 -4
  336. package/dist/web-commands.js +19 -4
  337. package/dist/web-commands.js.map +1 -1
  338. package/dist/web-host-guard.d.ts +3 -11
  339. package/dist/web-host-guard.js +3 -12
  340. package/dist/web-host-guard.js.map +1 -1
  341. package/dist/web-images.d.ts +47 -0
  342. package/dist/web-images.js +169 -0
  343. package/dist/web-images.js.map +1 -0
  344. package/dist/web-mentions.d.ts +44 -0
  345. package/dist/web-mentions.js +75 -0
  346. package/dist/web-mentions.js.map +1 -0
  347. package/dist/web-prefs.d.ts +63 -0
  348. package/dist/web-prefs.js +265 -0
  349. package/dist/web-prefs.js.map +1 -0
  350. package/dist/web-preview.d.ts +14 -1
  351. package/dist/web-preview.js +35 -13
  352. package/dist/web-preview.js.map +1 -1
  353. package/dist/web-reply-files.d.ts +45 -0
  354. package/dist/web-reply-files.js +223 -0
  355. package/dist/web-reply-files.js.map +1 -0
  356. package/dist/web-schedules.d.ts +12 -0
  357. package/dist/web-schedules.js +34 -0
  358. package/dist/web-schedules.js.map +1 -0
  359. package/dist/web-server-drain.d.ts +10 -0
  360. package/dist/web-server-drain.js +50 -0
  361. package/dist/web-server-drain.js.map +1 -0
  362. package/dist/web-session.d.ts +36 -1
  363. package/dist/web-session.js +53 -6
  364. package/dist/web-session.js.map +1 -1
  365. package/dist/web-settings-patch.d.ts +23 -0
  366. package/dist/web-settings-patch.js +77 -0
  367. package/dist/web-settings-patch.js.map +1 -0
  368. package/dist/web-shell-routes.d.ts +5 -1
  369. package/dist/web-shell-routes.js +10 -1
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/dist/web-upload.d.ts +17 -4
  372. package/dist/web-upload.js +64 -13
  373. package/dist/web-upload.js.map +1 -1
  374. package/dist/workflow-templates/default.md +1 -1
  375. package/package.json +1 -1
  376. package/templates/launchd.plist.ejs +3 -1
  377. package/templates/systemd.service.ejs +2 -1
@@ -1,17 +1,17 @@
1
- import { validateFleetConfig } from "./config-validator.js";
1
+ import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
2
2
  import { resolve as pathResolve, isAbsolute } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { deliveryContentDigest, deliveryStatusSelector } from "./delivery-outbox.js";
6
6
  import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
7
- import { DEFAULT_MAX_CROSS_INSTANCE_MESSAGE_BYTES, DEFAULT_LIST_INSTANCES_OUTPUT_BUDGET, MAX_INSTANCE_LOG_LINES } from "./config.js";
7
+ import { DEFAULT_MAX_CROSS_INSTANCE_MESSAGE_BYTES, DEFAULT_LIST_INSTANCES_OUTPUT_BUDGET, MAX_INSTANCE_LOG_LINES, getEffectiveInstanceDefaults } from "./config.js";
8
8
  import { t } from "./locale.js";
9
9
  import { presentationState, interactionSummary } from "./interaction-observation.js";
10
10
  import { truncatePreview } from "./channel/markdown-chunk.js";
11
11
  import { crossInstanceVisibility, senderTopicNotice, targetTopicNotice } from "./cross-instance-notice.js";
12
12
  import { instanceSupportsSteer } from "./steer-capability.js";
13
13
  import { assignDisplayLabels, displayInstanceName, readStatuslineModel } from "./topic-commands.js";
14
- import { credentialProfileLogin, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
14
+ import { CHAT_PROFILE_LOGIN_BACKENDS, credentialProfileLogin, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
15
15
  import { kiroEngineCandidates, kiroEngineStatus } from "./kiro-engine-status.js";
16
16
  import { formatCrossInstanceInboundMessage, MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES, } from "./cross-instance-envelope.js";
17
17
  import { BroadcastArgs, CreateInstanceArgs, CreateTeamArgs, DelegateTaskArgs, DeleteInstanceArgs, DeleteTeamArgs, DeployTemplateArgs, DescribeInstanceArgs, ListDeploymentsArgs, ListInstancesArgs, ListTeamsArgs, ReplaceInstanceArgs, ReportResultArgs, DeliveryStatusArgs, RequestInformationArgs, RestartInstanceArgs, SendToInstanceArgs, PauseInstanceArgs, WakeInstanceArgs, StopInstanceArgs, GetInstanceLogsArgs, GetEffortArgs, KiroEngineStatusArgs, GetUsageArgs, ListModelsArgs, GetFleetConfigArgs, UpdateInstanceConfigArgs, UpdateFleetDefaultsArgs, StartInstanceArgs, TeardownDeploymentArgs, UpdateTeamArgs, validateArgs, } from "./outbound-schemas.js";
@@ -32,8 +32,11 @@ function rejectOversizedCrossInstanceMessage(ctx, message, respond) {
32
32
  respond(null, t("cross_instance.message_too_large", formatByteSize(actual), formatByteSize(limit)));
33
33
  return true;
34
34
  }
35
- function rejectOversizedCrossInstanceEnvelope(message, meta, respond) {
36
- const actual = Buffer.byteLength(formatCrossInstanceInboundMessage(message, meta), "utf8");
35
+ function rejectOversizedCrossInstanceEnvelope(message, meta, respond,
36
+ /** #1650: the footer differs by the receiver's mode — its own, or the longer of the two when targets are not known yet. */
37
+ mode = "mcp") {
38
+ const size = (m) => Buffer.byteLength(formatCrossInstanceInboundMessage(message, meta, m), "utf8");
39
+ const actual = mode === "either" ? Math.max(size("mcp"), size("cli")) : size(mode);
37
40
  if (actual <= MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES)
38
41
  return false;
39
42
  respond(null, t("cross_instance.envelope_too_large", formatByteSize(actual), formatByteSize(MAX_ASSEMBLED_CROSS_INSTANCE_MESSAGE_BYTES)));
@@ -165,7 +168,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
165
168
  respond(null, v.error);
166
169
  return;
167
170
  }
168
- const { instance_name: targetName, message, steer, request_kind: reqKind, requires_reply, task_summary, working_directory, branch, correlation_id: parsedCorrelationId } = v.data;
171
+ const { instance_name: targetName, message, forwards, steer, request_kind: reqKind, requires_reply, task_summary, working_directory, branch, correlation_id: parsedCorrelationId } = v.data;
169
172
  if (rejectOversizedCrossInstanceMessage(ctx, message, respond))
170
173
  return;
171
174
  const senderLabel = meta.senderSessionName ?? meta.instanceName;
@@ -221,7 +224,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
221
224
  const queuedPredecessor = steerCapable
222
225
  && ctx.hasPendingIdleGatedDelivery?.(targetInstanceName) === true;
223
226
  const useSteer = steerCapable && !queuedPredecessor;
224
- const steerFallbackWarning = steer === true && !useSteer
227
+ let steerFallbackWarning = steer === true && !useSteer
225
228
  ? isExternalSession
226
229
  ? t("delivery.steer_external")
227
230
  : queuedPredecessor
@@ -252,12 +255,14 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
252
255
  ipcMeta.working_directory = working_directory;
253
256
  if (branch)
254
257
  ipcMeta.branch = branch;
258
+ if (forwards?.length)
259
+ ipcMeta.forwards = JSON.stringify(forwards);
255
260
  // #77: surface the sender's display name in the recipient's [from:…] header
256
261
  // (keeps from_instance as the machine name so the reply-back target stays valid).
257
262
  const senderDisplay = ctx.fleetConfig?.instances[senderLabel]?.display_name;
258
263
  if (senderDisplay && senderDisplay !== senderLabel)
259
264
  ipcMeta.from_display = senderDisplay;
260
- if (rejectOversizedCrossInstanceEnvelope(message, ipcMeta, respond))
265
+ if (rejectOversizedCrossInstanceEnvelope(message, ipcMeta, respond, ctx.fleetConfig?.instances[targetName]?.agent_mode === "cli" ? "cli" : "mcp"))
261
266
  return;
262
267
  // Fire-and-queue: hand the message to the fleet and respond immediately.
263
268
  // deliverToInstance owns waking a paused target, the idle gate and retries;
@@ -275,6 +280,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
275
280
  meta: ipcMeta,
276
281
  };
277
282
  let durableReceipt;
283
+ let effectiveUseSteer = useSteer;
278
284
  if (ctx.admitDurableDelivery) {
279
285
  try {
280
286
  durableReceipt = ctx.admitDurableDelivery({
@@ -288,7 +294,13 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
288
294
  kind: useSteer ? "steer" : "fleet_inbound",
289
295
  correlationId,
290
296
  payload,
297
+ forwards,
298
+ requestedKind: steer === true ? "steer" : "fleet_inbound",
291
299
  });
300
+ if (durableReceipt.kind)
301
+ effectiveUseSteer = durableReceipt.kind === "steer";
302
+ if (durableReceipt.duplicate && durableReceipt.kind !== undefined && durableReceipt.kind !== payload.type)
303
+ steerFallbackWarning = undefined;
292
304
  }
293
305
  catch (err) {
294
306
  ctx.logger.error({ err, source: meta.instanceName, target: targetInstanceName }, "Durable outbound admission failed closed");
@@ -297,6 +309,10 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
297
309
  }
298
310
  }
299
311
  else {
312
+ if (forwards?.length) {
313
+ respond(null, "Forwarded message verification requires the durable delivery store");
314
+ return;
315
+ }
300
316
  // Standalone handler harnesses retain the legacy path; production FleetManager
301
317
  // always provides durable admission.
302
318
  void deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetName, senderLabel, correlationId, payload, useSteer ? { isCrossInstance: true, waitForIdle: false } : undefined);
@@ -333,33 +349,43 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
333
349
  displayName: ctx.fleetConfig?.instances[name]?.display_name,
334
350
  })));
335
351
  const displayLabel = (name) => fleetLabels.get(name) ?? displayInstanceName(name, ctx.fleetConfig?.instances[name]?.display_name);
352
+ const instances = ctx.fleetConfig?.instances ?? {};
353
+ const notice = { label: `${displayLabel(senderLabel)} → ${displayLabel(targetName)}`, message, requestKind, taskSummary: ipcMeta.task_summary };
354
+ const targetInstance = instances[targetInstanceName];
355
+ const targetIsGeneral = targetInstance?.general_topic === true;
356
+ const targetText = targetTopicNotice(crossInstanceVisibility(ctx.fleetConfig, targetInstanceName), notice);
357
+ const targetShown = targetText !== null && !targetIsGeneral && !ctx.sessionRegistry.has(targetName);
358
+ const senderInstance = instances[meta.instanceName];
359
+ const senderIsGeneral = senderInstance?.general_topic === true;
360
+ const senderText = senderTopicNotice(crossInstanceVisibility(ctx.fleetConfig, meta.instanceName), notice);
361
+ const senderShown = senderText !== null && !senderIsGeneral;
336
362
  if (groupId && ctx.adapter) {
337
- const instances = ctx.fleetConfig?.instances ?? {};
338
- const notificationLabel = `${displayLabel(senderLabel)} → ${displayLabel(targetName)}`;
339
- const notice = { label: notificationLabel, message, requestKind, taskSummary: ipcMeta.task_summary };
340
363
  // ── Target topic notification ──
341
- const targetInstance = instances[targetInstanceName];
342
364
  const targetTopicId = targetInstance?.topic_id;
343
- const targetIsGeneral = targetInstance?.general_topic === true;
344
- const targetText = targetTopicNotice(crossInstanceVisibility(ctx.fleetConfig, targetInstanceName), notice);
345
- if (targetText !== null && targetTopicId && !targetIsGeneral && !ctx.sessionRegistry.has(targetName)) {
365
+ if (targetShown && targetTopicId) {
346
366
  const targetAdapter = ctx.getAdapterForInstance?.(targetInstanceName) ?? ctx.adapter;
347
367
  const targetGroupId = ctx.getGroupIdForInstance?.(targetInstanceName) ?? String(groupId);
348
368
  targetAdapter.sendText(String(targetGroupId), targetText, { threadId: String(targetTopicId) })
349
369
  .catch(e => ctx.logger.warn({ err: e }, "Failed to post target topic notification"));
350
370
  }
351
371
  // ── Sender topic notification ──
352
- const senderInstance = instances[meta.instanceName];
353
372
  const senderTopicId = senderInstance?.topic_id;
354
- const senderIsGeneral = senderInstance?.general_topic === true;
355
- const senderText = senderTopicNotice(crossInstanceVisibility(ctx.fleetConfig, meta.instanceName), notice);
356
- if (senderText !== null && senderTopicId && !senderIsGeneral) {
373
+ if (senderShown && senderTopicId) {
357
374
  const senderAdapter = ctx.getAdapterForInstance?.(meta.instanceName) ?? ctx.adapter;
358
375
  const senderGroupId = ctx.getGroupIdForInstance?.(meta.instanceName) ?? String(groupId);
359
376
  senderAdapter.sendText(senderGroupId, senderText, { threadId: String(senderTopicId) })
360
377
  .catch(e => ctx.logger.warn({ err: e }, "Failed to post sender topic notification"));
361
378
  }
362
379
  }
380
+ // #1672: the same notices in each instance's web chat — a muted status line, never an agent's or a person's message.
381
+ // Not gated on a topic or a chat platform (a web-only fleet, a topic-less instance); recorded in its history (#1565).
382
+ const webNotice = (instance, text) => ctx.emitSseEvent?.("message", {
383
+ instance, sender: displayLabel(senderLabel), role: "status", text, ts: new Date().toISOString(),
384
+ });
385
+ if (targetShown)
386
+ webNotice(targetInstanceName, targetText);
387
+ if (senderShown)
388
+ webNotice(meta.instanceName, senderText);
363
389
  // #856: the same line, now carrying the envelope's message_id and a short
364
390
  // content digest, so a disputed message can be matched to its outbox row
365
391
  // and paste digests without adding a log line per delivery.
@@ -379,6 +405,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
379
405
  : undefined;
380
406
  const warnings = [steerFallbackWarning, targetStateWarning].filter((value) => !!value);
381
407
  respond({ sent: true, queued: true, target: targetName, target_state: state,
408
+ ...(forwards?.length ? { forwards } : {}),
382
409
  ...(durableReceipt ? {
383
410
  durable: true,
384
411
  delivery_id: durableReceipt.deliveryId,
@@ -387,8 +414,8 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
387
414
  duplicate: durableReceipt.duplicate,
388
415
  } : {}),
389
416
  ...(state === "paused" ? { waking: true } : {}), correlation_id: correlationId,
390
- ...(useSteer ? { delivery_mode: "steer" } : {}),
391
- ...(steerFallbackWarning ? { delivery_mode: "idle_queue" } : {}),
417
+ ...(effectiveUseSteer ? { delivery_mode: "steer" } : {}),
418
+ ...(steer === true && !effectiveUseSteer ? { delivery_mode: "idle_queue" } : {}),
392
419
  ...(warnings.length > 0 ? { warning: warnings.join(" ") } : {}),
393
420
  });
394
421
  };
@@ -918,31 +945,86 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
918
945
  respond(null, v.error);
919
946
  return;
920
947
  }
921
- const inst = ctx.fleetConfig?.instances[v.data.name];
948
+ applyInstanceConfigPatch(ctx, v.data.name, v.data.config, respond);
949
+ };
950
+ /**
951
+ * update_instance_config's write: one validated config write, rolled back whole on a refusal, then one relaunch when
952
+ * the backend (fresh, #1217), a credential profile or backend_options changed. Answers through `respond` like the tool.
953
+ */
954
+ export function applyInstanceConfigPatch(ctx, name, patch, respond) {
955
+ const inst = ctx.fleetConfig?.instances[name];
922
956
  if (!inst) {
923
957
  const isClassic = (ctx.classicChannels?.getAll() ?? [])
924
- .some(channel => channel.instanceName === v.data.name);
958
+ .some(channel => channel.instanceName === name);
925
959
  respond(null, isClassic
926
960
  ? "This is a ClassicBot instance (managed in classicBot.yaml). Use set_display_name or set_description to configure it."
927
- : `Instance '${v.data.name}' not found in fleet config`);
961
+ : `Instance '${name}' not found in fleet config`);
928
962
  return;
929
963
  }
930
- const patch = v.data.config;
931
964
  // Read before anything is mutated: the switch is what decides whether the
932
965
  // next launch may resume, and by then `inst` already holds the new value.
933
- const backendBefore = inst.backend ?? "claude-code";
966
+ // The backend an instance runs: its own, else the fleet default (#1815 review: a Settings-created entry may omit it).
967
+ const effectiveBackend = () => inst.backend ?? ctx.fleetConfig?.defaults?.backend ?? "claude-code";
968
+ const backendBefore = effectiveBackend();
969
+ // #1808: a model and an effort name the backend they were chosen for. On a switch the new backend gets the ones
970
+ // given — if it takes them, refused here before anything is written — or none of the instance's own (the inherited
971
+ // value, else the CLI's default). Never the old backend's: codex launched with `--model opus` fails every turn.
972
+ if (patch.backend !== undefined && patch.backend !== backendBefore) {
973
+ const backendNew = patch.backend;
974
+ // Only a backend AgEnD knows gets these checks; any other name is refused by validation below, as before (and a
975
+ // "__proto__" or "toString" never reaches a lookup keyed by backend name).
976
+ const known = KNOWN_BACKENDS.includes(backendNew);
977
+ // The provider the new backend will run on (#1815 review): this patch's backend_options over the instance's, else
978
+ // the fleet defaults' — a custom provider has its own catalog, which the account's list says nothing about.
979
+ const options = mergeBackendOptions(inst.backend_options, patch.backend_options ?? {})[backendNew]
980
+ ?? ctx.fleetConfig?.defaults?.backend_options?.[backendNew];
981
+ const rawProvider = options?.provider;
982
+ const provider = typeof rawProvider === "string" && rawProvider.trim() ? rawProvider.trim() : null;
983
+ if (known && typeof patch.model === "string") {
984
+ const verdict = ctx.backendModelVerdict?.(name, backendNew, patch.model, { provider });
985
+ if (verdict && !verdict.ok) {
986
+ respond(null, `Rejected: ${backendNew} does not take the model "${patch.model}". ${verdict.choices.length
987
+ ? `Choose one of: ${verdict.choices.join(", ")}` : `No ${backendNew} model list is known yet`} — or leave model out for its default.`);
988
+ return;
989
+ }
990
+ }
991
+ if (known && typeof patch.effort === "string") {
992
+ // The levels of the model that will run there (codex's differ per model): the one given, else the inherited one.
993
+ const inheritedModel = getEffectiveInstanceDefaults(ctx.fleetConfig?.defaults).model;
994
+ const runs = typeof patch.model === "string" ? patch.model : typeof inheritedModel === "string" ? inheritedModel : null;
995
+ const levels = ctx.backendEffortLevels?.(name, backendNew, runs);
996
+ if (levels && !levels.includes(patch.effort)) {
997
+ respond(null, levels.length
998
+ ? `Rejected: ${backendNew} takes no effort "${patch.effort}" at launch on this host. Choose one of: ${levels.join(", ")} — or leave effort out for its default.`
999
+ : `Rejected: ${backendNew} takes no effort at launch on this host — leave effort out.`);
1000
+ return;
1001
+ }
1002
+ }
1003
+ patch = { ...patch, model: patch.model === undefined ? null : patch.model, effort: patch.effort === undefined ? null : patch.effort };
1004
+ }
934
1005
  const profileBefore = instanceCredentialProfile(inst, ctx.fleetConfig?.defaults, backendBefore);
935
1006
  // Snapshot enough to undo the whole patch if validation refuses it.
936
1007
  const beforeEdit = validateFleetConfig(ctx.fleetConfig);
937
1008
  const beforeFields = {};
938
- for (const key of ["backend", "model", "auto_pause_after", "display_name", "description"]) {
1009
+ for (const key of ["backend", "model", "effort", "auto_pause_after", "display_name", "description"]) {
939
1010
  if (patch[key] !== undefined)
940
1011
  beforeFields[key] = inst[key];
941
1012
  }
942
1013
  if (patch.backend !== undefined)
943
1014
  inst.backend = patch.backend;
944
- if (patch.model !== undefined)
945
- inst.model = patch.model;
1015
+ // null: the value this instance would have with no override of its own — what loading fleet.yaml gives it.
1016
+ const inherited = getEffectiveInstanceDefaults(ctx.fleetConfig?.defaults);
1017
+ for (const key of ["model", "effort"]) {
1018
+ const value = patch[key];
1019
+ if (value === undefined)
1020
+ continue;
1021
+ if (value !== null)
1022
+ inst[key] = value;
1023
+ else if (inherited[key] !== undefined)
1024
+ inst[key] = inherited[key];
1025
+ else
1026
+ delete inst[key];
1027
+ }
946
1028
  if (patch.auto_pause_after !== undefined)
947
1029
  inst.auto_pause_after = patch.auto_pause_after;
948
1030
  if (patch.display_name !== undefined)
@@ -956,20 +1038,7 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
956
1038
  let relaunchNeeded = false;
957
1039
  if (patch.backend_options !== undefined) {
958
1040
  const before = JSON.stringify(previousOptions ?? {});
959
- const merged = { ...(previousOptions ?? {}) };
960
- for (const [backendName, options] of Object.entries(patch.backend_options)) {
961
- const namespace = { ...(merged[backendName] ?? {}), ...options };
962
- // A merge cannot express removal, so null (or "") means "drop this key"
963
- // — that is how an agent goes back to the fleet's default login.
964
- for (const [key, value] of Object.entries(options)) {
965
- if (value === null || value === "")
966
- delete namespace[key];
967
- }
968
- if (Object.keys(namespace).length)
969
- merged[backendName] = namespace;
970
- else
971
- delete merged[backendName];
972
- }
1041
+ const merged = mergeBackendOptions(previousOptions, patch.backend_options);
973
1042
  inst.backend_options = merged;
974
1043
  relaunchNeeded = JSON.stringify(merged) !== before;
975
1044
  }
@@ -997,7 +1066,7 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
997
1066
  respond(null, `Rejected: ${introduced.map(issue => `${issue.path}: ${issue.message}`).join("; ")}`);
998
1067
  return;
999
1068
  }
1000
- const backendAfter = inst.backend ?? "claude-code";
1069
+ const backendAfter = effectiveBackend();
1001
1070
  const profileAfter = instanceCredentialProfile(inst, ctx.fleetConfig?.defaults, backendAfter);
1002
1071
  const profileSwitched = profileAfter !== profileBefore;
1003
1072
  // #1217: a running CLI is the backend it was launched as. Saving a new
@@ -1020,13 +1089,17 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1020
1089
  // the escape hatch from a bad switch and must never be the thing that is
1021
1090
  // blocked, and an unlogged shared store means the CLI was never set up at
1022
1091
  // all — a state that predates this feature.
1023
- if (profileSwitched && profileAfter) {
1092
+ // #1806 review: a profile is a login of ONE backend — the same name under another backend is another store, so a
1093
+ // backend switch onto a named profile is checked too (claude-code "personal" says nothing about kiro-cli "personal").
1094
+ if ((profileSwitched || backendSwitched) && profileAfter) {
1024
1095
  // The same root the launcher resolves the profile under, so the store this
1025
1096
  // probes is the store the CLI will be pointed at.
1026
1097
  const login = credentialProfileLogin(ctx.dataDir, backendAfter, profileAfter);
1027
1098
  if (login.state === "signed-out") {
1028
1099
  rollback();
1029
- respond(null, `Rejected: credential profile "${profileAfter}" has no ${backendAfter} login yet, and switching to it would leave the agent waiting at a sign-in prompt. Log it in first:\n ${login.loginCommand}`);
1100
+ // #1699 part 2: a claude-code profile can also be signed in from chat, in the browser terminal.
1101
+ const fromChat = CHAT_PROFILE_LOGIN_BACKENDS.has(backendAfter) ? `\n or from chat: /login ${backendAfter} ${profileAfter}` : "";
1102
+ respond(null, `Rejected: credential profile "${profileAfter}" has no ${backendAfter} login yet, and switching to it would leave the agent waiting at a sign-in prompt. Log it in first:\n ${login.loginCommand}${fromChat}`);
1030
1103
  return;
1031
1104
  }
1032
1105
  }
@@ -1039,18 +1112,18 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1039
1112
  return;
1040
1113
  }
1041
1114
  if (!relaunchNeeded) {
1042
- respond({ success: true, name: v.data.name, applied: patch });
1115
+ respond({ success: true, name, applied: patch });
1043
1116
  return;
1044
1117
  }
1045
1118
  // Only a running instance is restarted. Switching the subscription of a
1046
1119
  // paused or stopped agent must not start it — restartSingleInstance is
1047
1120
  // stop-then-start and would — so the new credentials are read whenever it
1048
1121
  // next comes up on its own.
1049
- const status = targetState(ctx, v.data.name);
1122
+ const status = targetState(ctx, name);
1050
1123
  if (status !== "running") {
1051
1124
  respond({
1052
1125
  success: true,
1053
- name: v.data.name,
1126
+ name,
1054
1127
  applied: patch,
1055
1128
  restarted: false,
1056
1129
  note: `Instance is ${status}; the new ${backendSwitched ? "backend" : "backend_options"} apply when it next starts.`,
@@ -1070,19 +1143,26 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1070
1143
  // was — taking a handover there would be describing a loss that did not
1071
1144
  // happen, and starting fresh would be causing one.
1072
1145
  const switchLosesConversation = profileSwitched && credentialSwitchStartsFresh(backendAfter);
1146
+ // #1753: Claude Code's conversation can go along — copied into the new profile and resumed there, between the old
1147
+ // CLI's stop and the new one's start (the restart does it, and says how it went). The handover is still taken now,
1148
+ // while the daemon that holds it is alive: it is what the new session gets if the copy cannot be made.
1149
+ const carry = profileSwitched && !backendSwitched && credentialSwitchCarriesConversation(backendAfter)
1150
+ ? { backend: backendAfter, from: profileBefore, to: profileAfter, result: null }
1151
+ : null;
1073
1152
  const handover = switchLosesConversation
1074
- ? (ctx.lifecycle.daemons.get(v.data.name)?.collectHandoverContext() ?? "")
1153
+ ? (ctx.lifecycle.daemons.get(name)?.collectHandoverContext() ?? "")
1075
1154
  : "";
1076
1155
  // Saving without restarting would report a subscription switch that has not
1077
1156
  // happened — the running CLI keeps the credentials it was launched with.
1078
- // A switch also starts fresh: the new store has no conversation for this
1079
- // directory, so resuming can only spend the resume startup budget waiting for
1080
- // something that is not there.
1081
- ctx.restartSingleInstance(v.data.name, switchLosesConversation || backendSwitched ? { freshStart: true } : undefined).then(async () => {
1082
- const handoverChars = await deliverProfileHandover(ctx, v.data.name, profileBefore, profileAfter, handover);
1157
+ // A switch that cannot take the conversation along starts fresh: the new store
1158
+ // has no conversation for this directory, so resuming can only spend the
1159
+ // resume startup budget waiting for something that is not there.
1160
+ ctx.restartSingleInstance(name, carry ? { carry } : switchLosesConversation || backendSwitched ? { freshStart: true } : undefined).then(async () => {
1161
+ const carried = carry?.result?.carried === true;
1162
+ const handoverChars = carried ? 0 : await deliverProfileHandover(ctx, name, profileBefore, profileAfter, handover);
1083
1163
  respond({
1084
1164
  success: true,
1085
- name: v.data.name,
1165
+ name,
1086
1166
  applied: patch,
1087
1167
  restarted: true,
1088
1168
  ...(backendSwitched ? { backend_switched: true, conversation_carried_over: false } : {}),
@@ -1093,21 +1173,43 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1093
1173
  // across accounts is the thing nobody has been able to test yet,
1094
1174
  // so this says where the history lives rather than what will
1095
1175
  // happen when the agent reaches for it.
1096
- conversation_carried_over: !switchLosesConversation,
1097
- ...(switchLosesConversation
1098
- ? { handover_chars: handoverChars }
1099
- : { conversation_note: `${backendAfter} keeps its history outside the login, so it is shared across profiles; switching back to a profile returns to the same history.` }),
1176
+ conversation_carried_over: carried || !switchLosesConversation,
1177
+ ...(carried
1178
+ ? { conversation_note: "The conversation was copied into the new profile and resumed there (the old profile keeps its own copy). The next reply re-reads it from a cold cache: one slower, full-price turn." }
1179
+ : switchLosesConversation
1180
+ ? { handover_chars: handoverChars, ...(carry?.result && !carry.result.carried ? { carry_failed: carry.result.reason } : {}) }
1181
+ : { conversation_note: `${backendAfter} keeps its history outside the login, so it is shared across profiles; switching back to a profile returns to the same history.` }),
1100
1182
  }
1101
1183
  : {}),
1102
1184
  });
1103
1185
  }, (err) => respond({
1104
1186
  success: true,
1105
- name: v.data.name,
1187
+ name,
1106
1188
  applied: patch,
1107
1189
  restarted: false,
1108
1190
  warning: `Config saved, but the restart failed: ${err.message}. The new backend_options take effect on the next start.`,
1109
1191
  }));
1110
- };
1192
+ }
1193
+ /**
1194
+ * backend_options merged per backend namespace, so setting a kiro option cannot silently drop a codex one. A merge
1195
+ * cannot express removal, so null (or "") means "drop this key" — that is how an agent goes back to the fleet's
1196
+ * default login.
1197
+ */
1198
+ function mergeBackendOptions(previous, patch) {
1199
+ const merged = { ...(previous ?? {}) };
1200
+ for (const [backendName, options] of Object.entries(patch)) {
1201
+ const namespace = { ...(merged[backendName] ?? {}), ...options };
1202
+ for (const [key, value] of Object.entries(options)) {
1203
+ if (value === null || value === "")
1204
+ delete namespace[key];
1205
+ }
1206
+ if (Object.keys(namespace).length)
1207
+ merged[backendName] = namespace;
1208
+ else
1209
+ delete merged[backendName];
1210
+ }
1211
+ return merged;
1212
+ }
1111
1213
  /** Long enough for the relaunched CLI to be listening; matches replace_instance. */
1112
1214
  export const PROFILE_HANDOVER_SETTLE_MS = 3_000;
1113
1215
  /**
@@ -1202,6 +1304,8 @@ function wrapAsSend(schema, toolName, buildArgs, warnMissing) {
1202
1304
  request_kind: kind,
1203
1305
  requires_reply: reply,
1204
1306
  task_summary: summary,
1307
+ ...(v.data.forwards !== undefined
1308
+ ? { forwards: v.data.forwards } : {}),
1205
1309
  };
1206
1310
  // Re-dispatch through the handler map
1207
1311
  return sendToInstance(ctx, newArgs, respond, meta);
@@ -1313,7 +1417,7 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
1313
1417
  // Validate once before dispatching any target. The placeholders are the
1314
1418
  // longest values generated below, so every real per-target envelope fits if
1315
1419
  // this one does. Keeping this outside the loop prevents partial broadcasts.
1316
- if (rejectOversizedCrossInstanceEnvelope(message, makeBroadcastMeta("bcast-9999999999999-zzzzzz", "bcast-9999999999999"), respond))
1420
+ if (rejectOversizedCrossInstanceEnvelope(message, makeBroadcastMeta("bcast-9999999999999-zzzzzz", "bcast-9999999999999"), respond, "either"))
1317
1421
  return;
1318
1422
  // Resolve target list: team, explicit targets, tag filter, or all running
1319
1423
  let targetNames;
@@ -1401,30 +1505,50 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
1401
1505
  });
1402
1506
  };
1403
1507
  // ── Teams ────────────────────────────────────────────────────────────────
1508
+ /**
1509
+ * #1692: the one creation of a team — create_team (agents) and POST /ui/teams (the web) both come here. Members must be
1510
+ * instances fleet.yaml defines (own keys; repeats dropped); an existing team is never overwritten (409); a name every
1511
+ * object already has (`__proto__`, `constructor`, `toString`…) is not a team name. The team and its save are one: a
1512
+ * save that throws removes it again (500).
1513
+ */
1514
+ export function commitTeamCreate(fleetConfig, args, save) {
1515
+ if (!fleetConfig)
1516
+ return { ok: false, status: 500, error: "Fleet config not available" };
1517
+ if (args.name in Object.prototype)
1518
+ return { ok: false, status: 400, error: `Invalid team name: ${args.name}` };
1519
+ const invalid = args.members.filter(m => !Object.hasOwn(fleetConfig.instances, m));
1520
+ if (invalid.length)
1521
+ return { ok: false, status: 400, error: `Invalid instance names: ${invalid.join(", ")}` };
1522
+ if (fleetConfig.teams && Object.hasOwn(fleetConfig.teams, args.name))
1523
+ return { ok: false, status: 409, error: `Team already exists: ${args.name}` };
1524
+ const createdMap = !fleetConfig.teams;
1525
+ const teams = (fleetConfig.teams ??= {});
1526
+ const members = [...new Set(args.members)];
1527
+ const description = args.description?.trim();
1528
+ teams[args.name] = { members, ...(description ? { description } : {}) };
1529
+ try {
1530
+ save();
1531
+ }
1532
+ catch (err) {
1533
+ delete teams[args.name];
1534
+ if (createdMap)
1535
+ delete fleetConfig.teams;
1536
+ return { ok: false, status: 500, error: `Saving fleet.yaml failed; no team was created: ${err?.message ?? String(err)}` };
1537
+ }
1538
+ return { ok: true, result: { created: args.name, members } };
1539
+ }
1404
1540
  const createTeam = (ctx, rawArgs, respond) => {
1405
1541
  const v = validateArgs(CreateTeamArgs, rawArgs, "create_team");
1406
1542
  if (!v.ok) {
1407
1543
  respond(null, v.error);
1408
1544
  return;
1409
1545
  }
1410
- const { name, members, description } = v.data;
1411
- if (!ctx.fleetConfig) {
1412
- respond(null, "Fleet config not available");
1413
- return;
1414
- }
1415
- const invalid = members.filter(m => !ctx.fleetConfig.instances[m]);
1416
- if (invalid.length) {
1417
- respond(null, `Invalid instance names: ${invalid.join(", ")}`);
1546
+ const r = commitTeamCreate(ctx.fleetConfig, v.data, () => ctx.saveFleetConfig());
1547
+ if (!r.ok) {
1548
+ respond(null, r.error);
1418
1549
  return;
1419
1550
  }
1420
- ctx.fleetConfig.teams ??= {};
1421
- if (ctx.fleetConfig.teams[name]) {
1422
- respond(null, `Team already exists: ${name}`);
1423
- return;
1424
- }
1425
- ctx.fleetConfig.teams[name] = { members, ...(description ? { description } : {}) };
1426
- ctx.saveFleetConfig();
1427
- respond({ created: name, members });
1551
+ respond(r.result);
1428
1552
  };
1429
1553
  const deleteTeam = (ctx, rawArgs, respond) => {
1430
1554
  const v = validateArgs(DeleteTeamArgs, rawArgs, "delete_team");
@@ -1458,31 +1582,74 @@ const listTeams = (ctx, rawArgs, respond) => {
1458
1582
  }));
1459
1583
  respond(result);
1460
1584
  };
1585
+ /**
1586
+ * #1689: the one update of a team — update_team (agents) and PATCH /ui/teams/<name> (the web) both come here. Own keys
1587
+ * only: a team or an instance is one fleet.yaml defines (`__proto__` and `constructor` are not); nothing changes unless
1588
+ * everything is valid. `description`: "" clears it. Renaming is not an update (the org chart and agents name teams).
1589
+ * The caller saves fleet.yaml after an ok.
1590
+ */
1591
+ export function applyTeamUpdate(fleetConfig, args) {
1592
+ if (!fleetConfig)
1593
+ return { ok: false, status: 500, error: "Fleet config not available" };
1594
+ const teams = fleetConfig.teams;
1595
+ if (!teams || !Object.hasOwn(teams, args.name))
1596
+ return { ok: false, status: 404, error: `Team not found: ${args.name}` };
1597
+ const team = teams[args.name];
1598
+ const invalid = (args.add ?? []).filter(m => !Object.hasOwn(fleetConfig.instances, m));
1599
+ if (invalid.length)
1600
+ return { ok: false, status: 400, error: `Invalid instance names: ${invalid.join(", ")}` };
1601
+ let members = [...team.members];
1602
+ if (args.add?.length)
1603
+ members = [...new Set([...members, ...args.add])];
1604
+ if (args.remove?.length)
1605
+ members = members.filter(m => !args.remove.includes(m));
1606
+ team.members = members;
1607
+ if (args.description !== undefined) {
1608
+ if (args.description.trim())
1609
+ team.description = args.description.trim();
1610
+ else
1611
+ delete team.description;
1612
+ }
1613
+ return { ok: true, result: { name: args.name, members: team.members, description: team.description ?? null } };
1614
+ }
1615
+ /**
1616
+ * #1689 review: the update and its save as one — a save that fails (an I/O error) puts the team back as it was, so the
1617
+ * live fleet never differs from fleet.yaml; that is a 500 (the input was fine), not a 400.
1618
+ */
1619
+ export function commitTeamUpdate(fleetConfig, args, save) {
1620
+ const teams = fleetConfig?.teams;
1621
+ const team = teams && Object.hasOwn(teams, args.name) ? teams[args.name] : null;
1622
+ const before = team ? { members: [...team.members], hadDescription: Object.hasOwn(team, "description"), description: team.description } : null;
1623
+ const r = applyTeamUpdate(fleetConfig, args);
1624
+ if (!r.ok)
1625
+ return r;
1626
+ try {
1627
+ save();
1628
+ }
1629
+ catch (err) {
1630
+ if (team && before) {
1631
+ team.members = before.members;
1632
+ if (before.hadDescription)
1633
+ team.description = before.description;
1634
+ else
1635
+ delete team.description;
1636
+ }
1637
+ return { ok: false, status: 500, error: `Saving fleet.yaml failed; the team is unchanged: ${err?.message ?? String(err)}` };
1638
+ }
1639
+ return r;
1640
+ }
1461
1641
  const updateTeam = (ctx, rawArgs, respond) => {
1462
1642
  const v = validateArgs(UpdateTeamArgs, rawArgs, "update_team");
1463
1643
  if (!v.ok) {
1464
1644
  respond(null, v.error);
1465
1645
  return;
1466
1646
  }
1467
- const { name, add, remove } = v.data;
1468
- if (!ctx.fleetConfig?.teams?.[name]) {
1469
- respond(null, `Team not found: ${name}`);
1647
+ const r = commitTeamUpdate(ctx.fleetConfig, v.data, () => ctx.saveFleetConfig());
1648
+ if (!r.ok) {
1649
+ respond(null, r.error);
1470
1650
  return;
1471
1651
  }
1472
- const team = ctx.fleetConfig.teams[name];
1473
- if (add?.length) {
1474
- const invalid = add.filter(m => !ctx.fleetConfig.instances[m]);
1475
- if (invalid.length) {
1476
- respond(null, `Invalid instance names: ${invalid.join(", ")}`);
1477
- return;
1478
- }
1479
- team.members = [...new Set([...team.members, ...add])];
1480
- }
1481
- if (remove?.length) {
1482
- team.members = team.members.filter(m => !remove.includes(m));
1483
- }
1484
- ctx.saveFleetConfig();
1485
- respond({ name, members: team.members });
1652
+ respond(r.result);
1486
1653
  };
1487
1654
  // ── Fleet Templates ────────────────────────────────────────────────────
1488
1655
  const deployTemplate = async (ctx, rawArgs, respond) => {