@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.11

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 (401) 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 +24 -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 +5 -3
  12. package/dist/auth-api.js +19 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +8 -0
  15. package/dist/backend/antigravity.js +32 -9
  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 +37 -1
  33. package/dist/backend/effort-metadata.js +86 -15
  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 +61 -0
  48. package/dist/backend/launch-effort.js +170 -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 +47 -4
  54. package/dist/backend/types.js +12 -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 +36 -1
  66. package/dist/channel/adapters/discord.js +126 -4
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +42 -2
  69. package/dist/channel/adapters/telegram.js +142 -23
  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 +26 -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.d.ts +2 -0
  83. package/dist/channel/tool-router.js +10 -4
  84. package/dist/channel/tool-router.js.map +1 -1
  85. package/dist/channel/types.d.ts +55 -3
  86. package/dist/cli.js +167 -39
  87. package/dist/cli.js.map +1 -1
  88. package/dist/command-table.d.ts +2 -2
  89. package/dist/command-table.js +6 -3
  90. package/dist/command-table.js.map +1 -1
  91. package/dist/config-validator.js +59 -0
  92. package/dist/config-validator.js.map +1 -1
  93. package/dist/cross-instance-envelope.d.ts +15 -2
  94. package/dist/cross-instance-envelope.js +29 -5
  95. package/dist/cross-instance-envelope.js.map +1 -1
  96. package/dist/daemon.d.ts +156 -1
  97. package/dist/daemon.js +773 -89
  98. package/dist/daemon.js.map +1 -1
  99. package/dist/delivery-outbox.d.ts +70 -8
  100. package/dist/delivery-outbox.js +315 -24
  101. package/dist/delivery-outbox.js.map +1 -1
  102. package/dist/delivery-reconciliation.d.ts +18 -1
  103. package/dist/delivery-reconciliation.js +67 -2
  104. package/dist/delivery-reconciliation.js.map +1 -1
  105. package/dist/fleet-context.d.ts +10 -1
  106. package/dist/fleet-manager.d.ts +404 -13
  107. package/dist/fleet-manager.js +2039 -210
  108. package/dist/fleet-manager.js.map +1 -1
  109. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  110. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  111. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  112. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  113. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  114. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  115. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  116. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  117. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  118. package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
  119. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  120. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  121. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  122. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  123. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  124. package/dist/general-knowledge/steering/core-rules.md +8 -7
  125. package/dist/host-memory.d.ts +10 -0
  126. package/dist/host-memory.js +33 -1
  127. package/dist/host-memory.js.map +1 -1
  128. package/dist/human-delivery-receipts.d.ts +83 -0
  129. package/dist/human-delivery-receipts.js +194 -0
  130. package/dist/human-delivery-receipts.js.map +1 -0
  131. package/dist/instance-avatar.d.ts +36 -0
  132. package/dist/instance-avatar.js +57 -0
  133. package/dist/instance-avatar.js.map +1 -0
  134. package/dist/instance-config-impact.js +6 -0
  135. package/dist/instance-config-impact.js.map +1 -1
  136. package/dist/instance-lifecycle.d.ts +29 -6
  137. package/dist/instance-lifecycle.js +79 -28
  138. package/dist/instance-lifecycle.js.map +1 -1
  139. package/dist/instructions.d.ts +3 -1
  140. package/dist/instructions.js +11 -8
  141. package/dist/instructions.js.map +1 -1
  142. package/dist/kiro-log-gate.d.ts +42 -0
  143. package/dist/kiro-log-gate.js +39 -0
  144. package/dist/kiro-log-gate.js.map +1 -0
  145. package/dist/locale.js +226 -44
  146. package/dist/locale.js.map +1 -1
  147. package/dist/log-service.d.ts +99 -0
  148. package/dist/log-service.js +243 -0
  149. package/dist/log-service.js.map +1 -0
  150. package/dist/log-steps.d.ts +169 -0
  151. package/dist/log-steps.js +858 -0
  152. package/dist/log-steps.js.map +1 -0
  153. package/dist/login-controller.d.ts +20 -3
  154. package/dist/login-controller.js +71 -19
  155. package/dist/login-controller.js.map +1 -1
  156. package/dist/login-flows.d.ts +5 -2
  157. package/dist/login-flows.js +5 -4
  158. package/dist/login-flows.js.map +1 -1
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/next-steps.js +1 -1
  168. package/dist/next-steps.js.map +1 -1
  169. package/dist/outbound-handlers.d.ts +100 -0
  170. package/dist/outbound-handlers.js +284 -97
  171. package/dist/outbound-handlers.js.map +1 -1
  172. package/dist/outbound-schemas.d.ts +38 -15
  173. package/dist/outbound-schemas.js +36 -8
  174. package/dist/outbound-schemas.js.map +1 -1
  175. package/dist/public-link-progress.d.ts +1 -1
  176. package/dist/public-link-progress.js +8 -2
  177. package/dist/public-link-progress.js.map +1 -1
  178. package/dist/public-preview.d.ts +101 -0
  179. package/dist/public-preview.js +236 -0
  180. package/dist/public-preview.js.map +1 -0
  181. package/dist/public-web-gateway.js +14 -4
  182. package/dist/public-web-gateway.js.map +1 -1
  183. package/dist/public-web-link.d.ts +24 -1
  184. package/dist/public-web-link.js +63 -9
  185. package/dist/public-web-link.js.map +1 -1
  186. package/dist/reply-buttons-controller.d.ts +89 -5
  187. package/dist/reply-buttons-controller.js +273 -8
  188. package/dist/reply-buttons-controller.js.map +1 -1
  189. package/dist/reply-buttons.d.ts +48 -3
  190. package/dist/reply-buttons.js +105 -10
  191. package/dist/reply-buttons.js.map +1 -1
  192. package/dist/reply-polls.d.ts +85 -0
  193. package/dist/reply-polls.js +154 -0
  194. package/dist/reply-polls.js.map +1 -0
  195. package/dist/restart-guard.d.ts +6 -3
  196. package/dist/restart-guard.js +6 -3
  197. package/dist/restart-guard.js.map +1 -1
  198. package/dist/restart-progress.d.ts +33 -0
  199. package/dist/restart-progress.js +30 -0
  200. package/dist/restart-progress.js.map +1 -1
  201. package/dist/service-activation.d.ts +15 -7
  202. package/dist/service-activation.js +46 -29
  203. package/dist/service-activation.js.map +1 -1
  204. package/dist/service-installer.d.ts +31 -0
  205. package/dist/service-installer.js +41 -2
  206. package/dist/service-installer.js.map +1 -1
  207. package/dist/settings-api.js +85 -25
  208. package/dist/settings-api.js.map +1 -1
  209. package/dist/settings-change.js +72 -1
  210. package/dist/settings-change.js.map +1 -1
  211. package/dist/settings-effect.js +28 -4
  212. package/dist/settings-effect.js.map +1 -1
  213. package/dist/settings-http-confirmation.js +7 -0
  214. package/dist/settings-http-confirmation.js.map +1 -1
  215. package/dist/setup-form.d.ts +1 -1
  216. package/dist/setup-form.js +1 -1
  217. package/dist/spawn-gate.js +5 -1
  218. package/dist/spawn-gate.js.map +1 -1
  219. package/dist/spawn-hold-reply.d.ts +12 -0
  220. package/dist/spawn-hold-reply.js +39 -0
  221. package/dist/spawn-hold-reply.js.map +1 -0
  222. package/dist/status-emojis.d.ts +10 -3
  223. package/dist/status-emojis.js +11 -2
  224. package/dist/status-emojis.js.map +1 -1
  225. package/dist/systemd-peer-bus.d.ts +95 -0
  226. package/dist/systemd-peer-bus.js +430 -0
  227. package/dist/systemd-peer-bus.js.map +1 -0
  228. package/dist/systemd-runtime.d.ts +27 -1
  229. package/dist/systemd-runtime.js +69 -7
  230. package/dist/systemd-runtime.js.map +1 -1
  231. package/dist/tips.js +4 -4
  232. package/dist/tips.js.map +1 -1
  233. package/dist/tool-permissions.js +3 -3
  234. package/dist/tool-permissions.js.map +1 -1
  235. package/dist/tool-progress.js +3 -2
  236. package/dist/tool-progress.js.map +1 -1
  237. package/dist/topic-commands.d.ts +42 -2
  238. package/dist/topic-commands.js +101 -11
  239. package/dist/topic-commands.js.map +1 -1
  240. package/dist/transcript-delta-reader.d.ts +3 -3
  241. package/dist/transcript-delta-reader.js +8 -8
  242. package/dist/transcript-delta-reader.js.map +1 -1
  243. package/dist/transcript-jsonl.d.ts +80 -3
  244. package/dist/transcript-jsonl.js +147 -10
  245. package/dist/transcript-jsonl.js.map +1 -1
  246. package/dist/transcript-monitor.d.ts +19 -0
  247. package/dist/transcript-monitor.js +87 -15
  248. package/dist/transcript-monitor.js.map +1 -1
  249. package/dist/transcript-sources.d.ts +78 -0
  250. package/dist/transcript-sources.js +375 -64
  251. package/dist/transcript-sources.js.map +1 -1
  252. package/dist/transcript-turns.d.ts +15 -2
  253. package/dist/transcript-turns.js +15 -2
  254. package/dist/transcript-turns.js.map +1 -1
  255. package/dist/tunnel/cloudflared.d.ts +7 -15
  256. package/dist/tunnel/cloudflared.js +36 -1
  257. package/dist/tunnel/cloudflared.js.map +1 -1
  258. package/dist/tunnel/purpose-lane.d.ts +5 -1
  259. package/dist/tunnel/purpose-lane.js +14 -7
  260. package/dist/tunnel/purpose-lane.js.map +1 -1
  261. package/dist/tunnel/types.d.ts +5 -1
  262. package/dist/tunnel/types.js.map +1 -1
  263. package/dist/turn-resume.d.ts +2 -0
  264. package/dist/turn-resume.js +1 -1
  265. package/dist/turn-resume.js.map +1 -1
  266. package/dist/types.d.ts +25 -0
  267. package/dist/ui/chat-commands.js +1 -0
  268. package/dist/ui/chat-emojis.js +33 -0
  269. package/dist/ui/chat-mentions.js +63 -0
  270. package/dist/ui/chat-render.js +61 -4
  271. package/dist/ui/chat-store.js +37 -3
  272. package/dist/ui/chat-strings.js +40 -20
  273. package/dist/ui/chat-thread.js +111 -12
  274. package/dist/ui/chat-time.js +80 -0
  275. package/dist/ui/panel-chat.js +41 -10
  276. package/dist/ui/panel-details.js +2 -0
  277. package/dist/ui/panel-fleet.js +199 -57
  278. package/dist/ui/panel-log.js +428 -0
  279. package/dist/ui/panel-needs.js +6 -2
  280. package/dist/ui/panel-settings.js +184 -32
  281. package/dist/ui/preview-panel.js +55 -15
  282. package/dist/ui/preview-status.js +28 -0
  283. package/dist/ui/preview.js +44 -3
  284. package/dist/ui/settings-dialogs.js +7 -2
  285. package/dist/ui/settings-model.js +2 -1
  286. package/dist/ui/settings-strings.js +56 -4
  287. package/dist/ui/settings-token.js +1 -1
  288. package/dist/ui/shared/app-i18n.js +43 -17
  289. package/dist/ui/shared/app-prefs.js +250 -0
  290. package/dist/ui/shared/app-route.js +7 -2
  291. package/dist/ui/shared/app-shell.js +165 -8
  292. package/dist/ui/shared/app-store.js +5 -1
  293. package/dist/ui/shared/app-stream.js +26 -5
  294. package/dist/ui/shared/app.css +163 -22
  295. package/dist/ui/shared/app.js +11 -1
  296. package/dist/ui/shared/fonts/README.md +1 -1
  297. package/dist/ui/shared/header-tools.js +55 -5
  298. package/dist/ui/shared/instance-nav.js +241 -45
  299. package/dist/ui/shared/instance-switch.js +4 -4
  300. package/dist/ui/shared/panel-view.js +6 -0
  301. package/dist/ui/shared/signin.js +7 -7
  302. package/dist/ui/shared/theme.js +39 -7
  303. package/dist/ui/shared/tokens.css +1 -1
  304. package/dist/ui/shared/ui-icons.js +1 -0
  305. package/dist/ui/shared/usage-dialog.js +23 -5
  306. package/dist/ui/shared/view-strings.js +32 -4
  307. package/dist/ui/signin.html +1 -1
  308. package/dist/unrecognised-prompt.d.ts +23 -0
  309. package/dist/unrecognised-prompt.js +74 -0
  310. package/dist/unrecognised-prompt.js.map +1 -0
  311. package/dist/update-check.d.ts +31 -0
  312. package/dist/update-check.js +47 -0
  313. package/dist/update-check.js.map +1 -1
  314. package/dist/update-install.d.ts +28 -2
  315. package/dist/update-install.js +22 -4
  316. package/dist/update-install.js.map +1 -1
  317. package/dist/update-launch.d.ts +46 -1
  318. package/dist/update-launch.js +146 -3
  319. package/dist/update-launch.js.map +1 -1
  320. package/dist/update-marker.d.ts +51 -0
  321. package/dist/update-marker.js +61 -0
  322. package/dist/update-marker.js.map +1 -1
  323. package/dist/update-preflight.d.ts +100 -0
  324. package/dist/update-preflight.js +264 -0
  325. package/dist/update-preflight.js.map +1 -0
  326. package/dist/updater-caller.d.ts +31 -0
  327. package/dist/updater-caller.js +109 -0
  328. package/dist/updater-caller.js.map +1 -0
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +1 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/providers.d.ts +34 -5
  333. package/dist/usage/providers.js +160 -27
  334. package/dist/usage/providers.js.map +1 -1
  335. package/dist/usage/statusline-usage.d.ts +6 -2
  336. package/dist/usage/statusline-usage.js +10 -4
  337. package/dist/usage/statusline-usage.js.map +1 -1
  338. package/dist/view-api.d.ts +1 -1
  339. package/dist/view-api.js +38 -8
  340. package/dist/view-api.js.map +1 -1
  341. package/dist/wake-coordinator.d.ts +11 -0
  342. package/dist/wake-coordinator.js +38 -4
  343. package/dist/wake-coordinator.js.map +1 -1
  344. package/dist/web-api.d.ts +89 -4
  345. package/dist/web-api.js +551 -45
  346. package/dist/web-api.js.map +1 -1
  347. package/dist/web-auth.d.ts +2 -2
  348. package/dist/web-auth.js +9 -4
  349. package/dist/web-auth.js.map +1 -1
  350. package/dist/web-chat-history.d.ts +12 -1
  351. package/dist/web-chat-history.js +29 -7
  352. package/dist/web-chat-history.js.map +1 -1
  353. package/dist/web-commands.d.ts +27 -4
  354. package/dist/web-commands.js +19 -4
  355. package/dist/web-commands.js.map +1 -1
  356. package/dist/web-host-guard.d.ts +3 -11
  357. package/dist/web-host-guard.js +3 -12
  358. package/dist/web-host-guard.js.map +1 -1
  359. package/dist/web-images.d.ts +47 -0
  360. package/dist/web-images.js +169 -0
  361. package/dist/web-images.js.map +1 -0
  362. package/dist/web-login.d.ts +14 -0
  363. package/dist/web-login.js +4 -2
  364. package/dist/web-login.js.map +1 -1
  365. package/dist/web-mentions.d.ts +44 -0
  366. package/dist/web-mentions.js +75 -0
  367. package/dist/web-mentions.js.map +1 -0
  368. package/dist/web-prefs.d.ts +81 -0
  369. package/dist/web-prefs.js +297 -0
  370. package/dist/web-prefs.js.map +1 -0
  371. package/dist/web-preview.d.ts +14 -1
  372. package/dist/web-preview.js +35 -13
  373. package/dist/web-preview.js.map +1 -1
  374. package/dist/web-profile.d.ts +32 -0
  375. package/dist/web-profile.js +132 -0
  376. package/dist/web-profile.js.map +1 -0
  377. package/dist/web-reply-files.d.ts +45 -0
  378. package/dist/web-reply-files.js +223 -0
  379. package/dist/web-reply-files.js.map +1 -0
  380. package/dist/web-schedules.d.ts +12 -0
  381. package/dist/web-schedules.js +34 -0
  382. package/dist/web-schedules.js.map +1 -0
  383. package/dist/web-server-drain.d.ts +10 -0
  384. package/dist/web-server-drain.js +50 -0
  385. package/dist/web-server-drain.js.map +1 -0
  386. package/dist/web-session.d.ts +50 -1
  387. package/dist/web-session.js +63 -6
  388. package/dist/web-session.js.map +1 -1
  389. package/dist/web-settings-patch.d.ts +23 -0
  390. package/dist/web-settings-patch.js +77 -0
  391. package/dist/web-settings-patch.js.map +1 -0
  392. package/dist/web-shell-routes.d.ts +5 -1
  393. package/dist/web-shell-routes.js +10 -1
  394. package/dist/web-shell-routes.js.map +1 -1
  395. package/dist/web-upload.d.ts +17 -4
  396. package/dist/web-upload.js +64 -13
  397. package/dist/web-upload.js.map +1 -1
  398. package/dist/workflow-templates/default.md +1 -1
  399. package/package.json +1 -1
  400. package/templates/launchd.plist.ejs +3 -1
  401. package/templates/systemd.service.ejs +2 -1
@@ -1,20 +1,20 @@
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
- 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";
17
+ import { BroadcastArgs, CreateInstanceArgs, CreateTeamArgs, DelegateTaskArgs, DeleteInstanceArgs, DeleteTeamArgs, DeployTemplateArgs, DescribeInstanceArgs, ListDeploymentsArgs, ListInstancesArgs, ListTeamsArgs, ReplaceInstanceArgs, ReportResultArgs, DeliveryStatusArgs, RequestInformationArgs, RestartInstanceArgs, SendToInstanceArgs, PauseInstanceArgs, WakeInstanceArgs, StopInstanceArgs, GetInstanceLogsArgs, GetEffortArgs, ClosePollArgs, KiroEngineStatusArgs, GetUsageArgs, ListModelsArgs, GetFleetConfigArgs, UpdateInstanceConfigArgs, UpdateFleetDefaultsArgs, StartInstanceArgs, TeardownDeploymentArgs, UpdateTeamArgs, validateArgs, } from "./outbound-schemas.js";
18
18
  const HOME_DIR = homedir();
19
19
  function formatByteSize(bytes) {
20
20
  if (bytes < 1024)
@@ -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
  };
@@ -760,6 +787,25 @@ const kiroEngineStatusHandler = (ctx, rawArgs, respond) => {
760
787
  }
761
788
  respond(status);
762
789
  };
790
+ /** #1823: close one of the caller's own polls before its deadline; its results then go out as at the deadline. */
791
+ const closePoll = async (ctx, rawArgs, respond, meta) => {
792
+ const v = validateArgs(ClosePollArgs, rawArgs, "close_poll");
793
+ if (!v.ok) {
794
+ respond(null, v.error);
795
+ return;
796
+ }
797
+ const fm = ctx;
798
+ const instance = meta?.instanceName; // whose polls: the instance's (as the reply recorded)
799
+ if (!instance || !fm.closePollEarly) {
800
+ respond(null, "close_poll is not available here");
801
+ return;
802
+ }
803
+ const r = await fm.closePollEarly(instance, v.data.poll_id);
804
+ if (r.ok)
805
+ respond({ closed: true, poll_id: v.data.poll_id, voted: r.voted });
806
+ else
807
+ respond(null, `close_poll: ${r.error}`);
808
+ };
763
809
  const getEffort = (ctx, rawArgs, respond, meta) => {
764
810
  const v = validateArgs(GetEffortArgs, rawArgs, "get_effort");
765
811
  if (!v.ok) {
@@ -918,31 +964,86 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
918
964
  respond(null, v.error);
919
965
  return;
920
966
  }
921
- const inst = ctx.fleetConfig?.instances[v.data.name];
967
+ applyInstanceConfigPatch(ctx, v.data.name, v.data.config, respond);
968
+ };
969
+ /**
970
+ * update_instance_config's write: one validated config write, rolled back whole on a refusal, then one relaunch when
971
+ * the backend (fresh, #1217), a credential profile or backend_options changed. Answers through `respond` like the tool.
972
+ */
973
+ export function applyInstanceConfigPatch(ctx, name, patch, respond) {
974
+ const inst = ctx.fleetConfig?.instances[name];
922
975
  if (!inst) {
923
976
  const isClassic = (ctx.classicChannels?.getAll() ?? [])
924
- .some(channel => channel.instanceName === v.data.name);
977
+ .some(channel => channel.instanceName === name);
925
978
  respond(null, isClassic
926
979
  ? "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`);
980
+ : `Instance '${name}' not found in fleet config`);
928
981
  return;
929
982
  }
930
- const patch = v.data.config;
931
983
  // Read before anything is mutated: the switch is what decides whether the
932
984
  // next launch may resume, and by then `inst` already holds the new value.
933
- const backendBefore = inst.backend ?? "claude-code";
985
+ // The backend an instance runs: its own, else the fleet default (#1815 review: a Settings-created entry may omit it).
986
+ const effectiveBackend = () => inst.backend ?? ctx.fleetConfig?.defaults?.backend ?? "claude-code";
987
+ const backendBefore = effectiveBackend();
988
+ // #1808: a model and an effort name the backend they were chosen for. On a switch the new backend gets the ones
989
+ // given — if it takes them, refused here before anything is written — or none of the instance's own (the inherited
990
+ // value, else the CLI's default). Never the old backend's: codex launched with `--model opus` fails every turn.
991
+ if (patch.backend !== undefined && patch.backend !== backendBefore) {
992
+ const backendNew = patch.backend;
993
+ // Only a backend AgEnD knows gets these checks; any other name is refused by validation below, as before (and a
994
+ // "__proto__" or "toString" never reaches a lookup keyed by backend name).
995
+ const known = KNOWN_BACKENDS.includes(backendNew);
996
+ // The provider the new backend will run on (#1815 review): this patch's backend_options over the instance's, else
997
+ // the fleet defaults' — a custom provider has its own catalog, which the account's list says nothing about.
998
+ const options = mergeBackendOptions(inst.backend_options, patch.backend_options ?? {})[backendNew]
999
+ ?? ctx.fleetConfig?.defaults?.backend_options?.[backendNew];
1000
+ const rawProvider = options?.provider;
1001
+ const provider = typeof rawProvider === "string" && rawProvider.trim() ? rawProvider.trim() : null;
1002
+ if (known && typeof patch.model === "string") {
1003
+ const verdict = ctx.backendModelVerdict?.(name, backendNew, patch.model, { provider });
1004
+ if (verdict && !verdict.ok) {
1005
+ respond(null, `Rejected: ${backendNew} does not take the model "${patch.model}". ${verdict.choices.length
1006
+ ? `Choose one of: ${verdict.choices.join(", ")}` : `No ${backendNew} model list is known yet`} — or leave model out for its default.`);
1007
+ return;
1008
+ }
1009
+ }
1010
+ if (known && typeof patch.effort === "string") {
1011
+ // The levels of the model that will run there (codex's differ per model): the one given, else the inherited one.
1012
+ const inheritedModel = getEffectiveInstanceDefaults(ctx.fleetConfig?.defaults).model;
1013
+ const runs = typeof patch.model === "string" ? patch.model : typeof inheritedModel === "string" ? inheritedModel : null;
1014
+ const levels = ctx.backendEffortLevels?.(name, backendNew, runs);
1015
+ if (levels && !levels.includes(patch.effort)) {
1016
+ respond(null, levels.length
1017
+ ? `Rejected: ${backendNew} takes no effort "${patch.effort}" at launch on this host. Choose one of: ${levels.join(", ")} — or leave effort out for its default.`
1018
+ : `Rejected: ${backendNew} takes no effort at launch on this host — leave effort out.`);
1019
+ return;
1020
+ }
1021
+ }
1022
+ patch = { ...patch, model: patch.model === undefined ? null : patch.model, effort: patch.effort === undefined ? null : patch.effort };
1023
+ }
934
1024
  const profileBefore = instanceCredentialProfile(inst, ctx.fleetConfig?.defaults, backendBefore);
935
1025
  // Snapshot enough to undo the whole patch if validation refuses it.
936
1026
  const beforeEdit = validateFleetConfig(ctx.fleetConfig);
937
1027
  const beforeFields = {};
938
- for (const key of ["backend", "model", "auto_pause_after", "display_name", "description"]) {
1028
+ for (const key of ["backend", "model", "effort", "auto_pause_after", "display_name", "description"]) {
939
1029
  if (patch[key] !== undefined)
940
1030
  beforeFields[key] = inst[key];
941
1031
  }
942
1032
  if (patch.backend !== undefined)
943
1033
  inst.backend = patch.backend;
944
- if (patch.model !== undefined)
945
- inst.model = patch.model;
1034
+ // null: the value this instance would have with no override of its own — what loading fleet.yaml gives it.
1035
+ const inherited = getEffectiveInstanceDefaults(ctx.fleetConfig?.defaults);
1036
+ for (const key of ["model", "effort"]) {
1037
+ const value = patch[key];
1038
+ if (value === undefined)
1039
+ continue;
1040
+ if (value !== null)
1041
+ inst[key] = value;
1042
+ else if (inherited[key] !== undefined)
1043
+ inst[key] = inherited[key];
1044
+ else
1045
+ delete inst[key];
1046
+ }
946
1047
  if (patch.auto_pause_after !== undefined)
947
1048
  inst.auto_pause_after = patch.auto_pause_after;
948
1049
  if (patch.display_name !== undefined)
@@ -956,20 +1057,7 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
956
1057
  let relaunchNeeded = false;
957
1058
  if (patch.backend_options !== undefined) {
958
1059
  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
- }
1060
+ const merged = mergeBackendOptions(previousOptions, patch.backend_options);
973
1061
  inst.backend_options = merged;
974
1062
  relaunchNeeded = JSON.stringify(merged) !== before;
975
1063
  }
@@ -997,7 +1085,7 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
997
1085
  respond(null, `Rejected: ${introduced.map(issue => `${issue.path}: ${issue.message}`).join("; ")}`);
998
1086
  return;
999
1087
  }
1000
- const backendAfter = inst.backend ?? "claude-code";
1088
+ const backendAfter = effectiveBackend();
1001
1089
  const profileAfter = instanceCredentialProfile(inst, ctx.fleetConfig?.defaults, backendAfter);
1002
1090
  const profileSwitched = profileAfter !== profileBefore;
1003
1091
  // #1217: a running CLI is the backend it was launched as. Saving a new
@@ -1020,13 +1108,17 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1020
1108
  // the escape hatch from a bad switch and must never be the thing that is
1021
1109
  // blocked, and an unlogged shared store means the CLI was never set up at
1022
1110
  // all — a state that predates this feature.
1023
- if (profileSwitched && profileAfter) {
1111
+ // #1806 review: a profile is a login of ONE backend — the same name under another backend is another store, so a
1112
+ // backend switch onto a named profile is checked too (claude-code "personal" says nothing about kiro-cli "personal").
1113
+ if ((profileSwitched || backendSwitched) && profileAfter) {
1024
1114
  // The same root the launcher resolves the profile under, so the store this
1025
1115
  // probes is the store the CLI will be pointed at.
1026
1116
  const login = credentialProfileLogin(ctx.dataDir, backendAfter, profileAfter);
1027
1117
  if (login.state === "signed-out") {
1028
1118
  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}`);
1119
+ // #1699 part 2: a claude-code profile can also be signed in from chat, in the browser terminal.
1120
+ const fromChat = CHAT_PROFILE_LOGIN_BACKENDS.has(backendAfter) ? `\n or from chat: /login ${backendAfter} ${profileAfter}` : "";
1121
+ 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
1122
  return;
1031
1123
  }
1032
1124
  }
@@ -1039,18 +1131,18 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1039
1131
  return;
1040
1132
  }
1041
1133
  if (!relaunchNeeded) {
1042
- respond({ success: true, name: v.data.name, applied: patch });
1134
+ respond({ success: true, name, applied: patch });
1043
1135
  return;
1044
1136
  }
1045
1137
  // Only a running instance is restarted. Switching the subscription of a
1046
1138
  // paused or stopped agent must not start it — restartSingleInstance is
1047
1139
  // stop-then-start and would — so the new credentials are read whenever it
1048
1140
  // next comes up on its own.
1049
- const status = targetState(ctx, v.data.name);
1141
+ const status = targetState(ctx, name);
1050
1142
  if (status !== "running") {
1051
1143
  respond({
1052
1144
  success: true,
1053
- name: v.data.name,
1145
+ name,
1054
1146
  applied: patch,
1055
1147
  restarted: false,
1056
1148
  note: `Instance is ${status}; the new ${backendSwitched ? "backend" : "backend_options"} apply when it next starts.`,
@@ -1070,19 +1162,26 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1070
1162
  // was — taking a handover there would be describing a loss that did not
1071
1163
  // happen, and starting fresh would be causing one.
1072
1164
  const switchLosesConversation = profileSwitched && credentialSwitchStartsFresh(backendAfter);
1165
+ // #1753: Claude Code's conversation can go along — copied into the new profile and resumed there, between the old
1166
+ // CLI's stop and the new one's start (the restart does it, and says how it went). The handover is still taken now,
1167
+ // while the daemon that holds it is alive: it is what the new session gets if the copy cannot be made.
1168
+ const carry = profileSwitched && !backendSwitched && credentialSwitchCarriesConversation(backendAfter)
1169
+ ? { backend: backendAfter, from: profileBefore, to: profileAfter, result: null }
1170
+ : null;
1073
1171
  const handover = switchLosesConversation
1074
- ? (ctx.lifecycle.daemons.get(v.data.name)?.collectHandoverContext() ?? "")
1172
+ ? (ctx.lifecycle.daemons.get(name)?.collectHandoverContext() ?? "")
1075
1173
  : "";
1076
1174
  // Saving without restarting would report a subscription switch that has not
1077
1175
  // 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);
1176
+ // A switch that cannot take the conversation along starts fresh: the new store
1177
+ // has no conversation for this directory, so resuming can only spend the
1178
+ // resume startup budget waiting for something that is not there.
1179
+ ctx.restartSingleInstance(name, carry ? { carry } : switchLosesConversation || backendSwitched ? { freshStart: true } : undefined).then(async () => {
1180
+ const carried = carry?.result?.carried === true;
1181
+ const handoverChars = carried ? 0 : await deliverProfileHandover(ctx, name, profileBefore, profileAfter, handover);
1083
1182
  respond({
1084
1183
  success: true,
1085
- name: v.data.name,
1184
+ name,
1086
1185
  applied: patch,
1087
1186
  restarted: true,
1088
1187
  ...(backendSwitched ? { backend_switched: true, conversation_carried_over: false } : {}),
@@ -1093,21 +1192,43 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1093
1192
  // across accounts is the thing nobody has been able to test yet,
1094
1193
  // so this says where the history lives rather than what will
1095
1194
  // 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.` }),
1195
+ conversation_carried_over: carried || !switchLosesConversation,
1196
+ ...(carried
1197
+ ? { 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." }
1198
+ : switchLosesConversation
1199
+ ? { handover_chars: handoverChars, ...(carry?.result && !carry.result.carried ? { carry_failed: carry.result.reason } : {}) }
1200
+ : { 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
1201
  }
1101
1202
  : {}),
1102
1203
  });
1103
1204
  }, (err) => respond({
1104
1205
  success: true,
1105
- name: v.data.name,
1206
+ name,
1106
1207
  applied: patch,
1107
1208
  restarted: false,
1108
1209
  warning: `Config saved, but the restart failed: ${err.message}. The new backend_options take effect on the next start.`,
1109
1210
  }));
1110
- };
1211
+ }
1212
+ /**
1213
+ * backend_options merged per backend namespace, so setting a kiro option cannot silently drop a codex one. A merge
1214
+ * cannot express removal, so null (or "") means "drop this key" — that is how an agent goes back to the fleet's
1215
+ * default login.
1216
+ */
1217
+ function mergeBackendOptions(previous, patch) {
1218
+ const merged = { ...(previous ?? {}) };
1219
+ for (const [backendName, options] of Object.entries(patch)) {
1220
+ const namespace = { ...(merged[backendName] ?? {}), ...options };
1221
+ for (const [key, value] of Object.entries(options)) {
1222
+ if (value === null || value === "")
1223
+ delete namespace[key];
1224
+ }
1225
+ if (Object.keys(namespace).length)
1226
+ merged[backendName] = namespace;
1227
+ else
1228
+ delete merged[backendName];
1229
+ }
1230
+ return merged;
1231
+ }
1111
1232
  /** Long enough for the relaunched CLI to be listening; matches replace_instance. */
1112
1233
  export const PROFILE_HANDOVER_SETTLE_MS = 3_000;
1113
1234
  /**
@@ -1202,6 +1323,8 @@ function wrapAsSend(schema, toolName, buildArgs, warnMissing) {
1202
1323
  request_kind: kind,
1203
1324
  requires_reply: reply,
1204
1325
  task_summary: summary,
1326
+ ...(v.data.forwards !== undefined
1327
+ ? { forwards: v.data.forwards } : {}),
1205
1328
  };
1206
1329
  // Re-dispatch through the handler map
1207
1330
  return sendToInstance(ctx, newArgs, respond, meta);
@@ -1313,7 +1436,7 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
1313
1436
  // Validate once before dispatching any target. The placeholders are the
1314
1437
  // longest values generated below, so every real per-target envelope fits if
1315
1438
  // this one does. Keeping this outside the loop prevents partial broadcasts.
1316
- if (rejectOversizedCrossInstanceEnvelope(message, makeBroadcastMeta("bcast-9999999999999-zzzzzz", "bcast-9999999999999"), respond))
1439
+ if (rejectOversizedCrossInstanceEnvelope(message, makeBroadcastMeta("bcast-9999999999999-zzzzzz", "bcast-9999999999999"), respond, "either"))
1317
1440
  return;
1318
1441
  // Resolve target list: team, explicit targets, tag filter, or all running
1319
1442
  let targetNames;
@@ -1401,30 +1524,50 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
1401
1524
  });
1402
1525
  };
1403
1526
  // ── Teams ────────────────────────────────────────────────────────────────
1527
+ /**
1528
+ * #1692: the one creation of a team — create_team (agents) and POST /ui/teams (the web) both come here. Members must be
1529
+ * instances fleet.yaml defines (own keys; repeats dropped); an existing team is never overwritten (409); a name every
1530
+ * object already has (`__proto__`, `constructor`, `toString`…) is not a team name. The team and its save are one: a
1531
+ * save that throws removes it again (500).
1532
+ */
1533
+ export function commitTeamCreate(fleetConfig, args, save) {
1534
+ if (!fleetConfig)
1535
+ return { ok: false, status: 500, error: "Fleet config not available" };
1536
+ if (args.name in Object.prototype)
1537
+ return { ok: false, status: 400, error: `Invalid team name: ${args.name}` };
1538
+ const invalid = args.members.filter(m => !Object.hasOwn(fleetConfig.instances, m));
1539
+ if (invalid.length)
1540
+ return { ok: false, status: 400, error: `Invalid instance names: ${invalid.join(", ")}` };
1541
+ if (fleetConfig.teams && Object.hasOwn(fleetConfig.teams, args.name))
1542
+ return { ok: false, status: 409, error: `Team already exists: ${args.name}` };
1543
+ const createdMap = !fleetConfig.teams;
1544
+ const teams = (fleetConfig.teams ??= {});
1545
+ const members = [...new Set(args.members)];
1546
+ const description = args.description?.trim();
1547
+ teams[args.name] = { members, ...(description ? { description } : {}) };
1548
+ try {
1549
+ save();
1550
+ }
1551
+ catch (err) {
1552
+ delete teams[args.name];
1553
+ if (createdMap)
1554
+ delete fleetConfig.teams;
1555
+ return { ok: false, status: 500, error: `Saving fleet.yaml failed; no team was created: ${err?.message ?? String(err)}` };
1556
+ }
1557
+ return { ok: true, result: { created: args.name, members } };
1558
+ }
1404
1559
  const createTeam = (ctx, rawArgs, respond) => {
1405
1560
  const v = validateArgs(CreateTeamArgs, rawArgs, "create_team");
1406
1561
  if (!v.ok) {
1407
1562
  respond(null, v.error);
1408
1563
  return;
1409
1564
  }
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(", ")}`);
1418
- return;
1419
- }
1420
- ctx.fleetConfig.teams ??= {};
1421
- if (ctx.fleetConfig.teams[name]) {
1422
- respond(null, `Team already exists: ${name}`);
1565
+ const r = commitTeamCreate(ctx.fleetConfig, v.data, () => ctx.saveFleetConfig());
1566
+ if (!r.ok) {
1567
+ respond(null, r.error);
1423
1568
  return;
1424
1569
  }
1425
- ctx.fleetConfig.teams[name] = { members, ...(description ? { description } : {}) };
1426
- ctx.saveFleetConfig();
1427
- respond({ created: name, members });
1570
+ respond(r.result);
1428
1571
  };
1429
1572
  const deleteTeam = (ctx, rawArgs, respond) => {
1430
1573
  const v = validateArgs(DeleteTeamArgs, rawArgs, "delete_team");
@@ -1458,31 +1601,74 @@ const listTeams = (ctx, rawArgs, respond) => {
1458
1601
  }));
1459
1602
  respond(result);
1460
1603
  };
1604
+ /**
1605
+ * #1689: the one update of a team — update_team (agents) and PATCH /ui/teams/<name> (the web) both come here. Own keys
1606
+ * only: a team or an instance is one fleet.yaml defines (`__proto__` and `constructor` are not); nothing changes unless
1607
+ * everything is valid. `description`: "" clears it. Renaming is not an update (the org chart and agents name teams).
1608
+ * The caller saves fleet.yaml after an ok.
1609
+ */
1610
+ export function applyTeamUpdate(fleetConfig, args) {
1611
+ if (!fleetConfig)
1612
+ return { ok: false, status: 500, error: "Fleet config not available" };
1613
+ const teams = fleetConfig.teams;
1614
+ if (!teams || !Object.hasOwn(teams, args.name))
1615
+ return { ok: false, status: 404, error: `Team not found: ${args.name}` };
1616
+ const team = teams[args.name];
1617
+ const invalid = (args.add ?? []).filter(m => !Object.hasOwn(fleetConfig.instances, m));
1618
+ if (invalid.length)
1619
+ return { ok: false, status: 400, error: `Invalid instance names: ${invalid.join(", ")}` };
1620
+ let members = [...team.members];
1621
+ if (args.add?.length)
1622
+ members = [...new Set([...members, ...args.add])];
1623
+ if (args.remove?.length)
1624
+ members = members.filter(m => !args.remove.includes(m));
1625
+ team.members = members;
1626
+ if (args.description !== undefined) {
1627
+ if (args.description.trim())
1628
+ team.description = args.description.trim();
1629
+ else
1630
+ delete team.description;
1631
+ }
1632
+ return { ok: true, result: { name: args.name, members: team.members, description: team.description ?? null } };
1633
+ }
1634
+ /**
1635
+ * #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
1636
+ * live fleet never differs from fleet.yaml; that is a 500 (the input was fine), not a 400.
1637
+ */
1638
+ export function commitTeamUpdate(fleetConfig, args, save) {
1639
+ const teams = fleetConfig?.teams;
1640
+ const team = teams && Object.hasOwn(teams, args.name) ? teams[args.name] : null;
1641
+ const before = team ? { members: [...team.members], hadDescription: Object.hasOwn(team, "description"), description: team.description } : null;
1642
+ const r = applyTeamUpdate(fleetConfig, args);
1643
+ if (!r.ok)
1644
+ return r;
1645
+ try {
1646
+ save();
1647
+ }
1648
+ catch (err) {
1649
+ if (team && before) {
1650
+ team.members = before.members;
1651
+ if (before.hadDescription)
1652
+ team.description = before.description;
1653
+ else
1654
+ delete team.description;
1655
+ }
1656
+ return { ok: false, status: 500, error: `Saving fleet.yaml failed; the team is unchanged: ${err?.message ?? String(err)}` };
1657
+ }
1658
+ return r;
1659
+ }
1461
1660
  const updateTeam = (ctx, rawArgs, respond) => {
1462
1661
  const v = validateArgs(UpdateTeamArgs, rawArgs, "update_team");
1463
1662
  if (!v.ok) {
1464
1663
  respond(null, v.error);
1465
1664
  return;
1466
1665
  }
1467
- const { name, add, remove } = v.data;
1468
- if (!ctx.fleetConfig?.teams?.[name]) {
1469
- respond(null, `Team not found: ${name}`);
1666
+ const r = commitTeamUpdate(ctx.fleetConfig, v.data, () => ctx.saveFleetConfig());
1667
+ if (!r.ok) {
1668
+ respond(null, r.error);
1470
1669
  return;
1471
1670
  }
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 });
1671
+ respond(r.result);
1486
1672
  };
1487
1673
  // ── Fleet Templates ────────────────────────────────────────────────────
1488
1674
  const deployTemplate = async (ctx, rawArgs, respond) => {
@@ -1783,6 +1969,7 @@ export const outboundHandlers = new Map([
1783
1969
  ["stop_instance", stopInstance],
1784
1970
  ["get_fleet_status", getFleetStatus],
1785
1971
  ["get_effort", getEffort],
1972
+ ["close_poll", closePoll],
1786
1973
  ["kiro_engine_status", kiroEngineStatusHandler],
1787
1974
  ["get_usage", getUsage],
1788
1975
  ["list_models", listModels],