@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20

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 (389) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli-instructions.md +6 -2
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +3 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.js +16 -4
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +3 -1
  11. package/dist/auth-api.js +17 -1
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/antigravity.d.ts +33 -0
  14. package/dist/backend/antigravity.js +126 -18
  15. package/dist/backend/antigravity.js.map +1 -1
  16. package/dist/backend/claude-carry.d.ts +22 -0
  17. package/dist/backend/claude-carry.js +170 -0
  18. package/dist/backend/claude-carry.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +65 -2
  20. package/dist/backend/claude-code.js +226 -33
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-metadata.d.ts +33 -0
  23. package/dist/backend/codex-metadata.js +122 -18
  24. package/dist/backend/codex-metadata.js.map +1 -1
  25. package/dist/backend/codex.d.ts +3 -0
  26. package/dist/backend/codex.js +17 -1
  27. package/dist/backend/codex.js.map +1 -1
  28. package/dist/backend/credential-profile.d.ts +40 -3
  29. package/dist/backend/credential-profile.js +113 -5
  30. package/dist/backend/credential-profile.js.map +1 -1
  31. package/dist/backend/effort-metadata.d.ts +37 -1
  32. package/dist/backend/effort-metadata.js +86 -15
  33. package/dist/backend/effort-metadata.js.map +1 -1
  34. package/dist/backend/grok.d.ts +4 -0
  35. package/dist/backend/grok.js +14 -0
  36. package/dist/backend/grok.js.map +1 -1
  37. package/dist/backend/kiro.d.ts +27 -1
  38. package/dist/backend/kiro.js +63 -10
  39. package/dist/backend/kiro.js.map +1 -1
  40. package/dist/backend/launch-effort.d.ts +61 -0
  41. package/dist/backend/launch-effort.js +170 -0
  42. package/dist/backend/launch-effort.js.map +1 -0
  43. package/dist/backend/muse.d.ts +84 -0
  44. package/dist/backend/muse.js +449 -53
  45. package/dist/backend/muse.js.map +1 -1
  46. package/dist/backend/types.d.ts +48 -2
  47. package/dist/backend/types.js +12 -1
  48. package/dist/backend/types.js.map +1 -1
  49. package/dist/backend-command.d.ts +91 -0
  50. package/dist/backend-command.js +122 -0
  51. package/dist/backend-command.js.map +1 -0
  52. package/dist/background-work-ended.d.ts +18 -0
  53. package/dist/background-work-ended.js +30 -0
  54. package/dist/background-work-ended.js.map +1 -0
  55. package/dist/cache-service.d.ts +2 -0
  56. package/dist/cache-service.js +2 -1
  57. package/dist/cache-service.js.map +1 -1
  58. package/dist/channel/adapters/discord.d.ts +26 -1
  59. package/dist/channel/adapters/discord.js +103 -3
  60. package/dist/channel/adapters/discord.js.map +1 -1
  61. package/dist/channel/adapters/telegram.d.ts +35 -3
  62. package/dist/channel/adapters/telegram.js +122 -23
  63. package/dist/channel/adapters/telegram.js.map +1 -1
  64. package/dist/channel/mcp-tools.js +5 -3
  65. package/dist/channel/mcp-tools.js.map +1 -1
  66. package/dist/channel/message-edit-errors.d.ts +5 -0
  67. package/dist/channel/message-edit-errors.js +24 -0
  68. package/dist/channel/message-edit-errors.js.map +1 -0
  69. package/dist/channel/telegram-markdown.d.ts +6 -0
  70. package/dist/channel/telegram-markdown.js +104 -0
  71. package/dist/channel/telegram-markdown.js.map +1 -0
  72. package/dist/channel/tool-router.d.ts +2 -0
  73. package/dist/channel/tool-router.js +6 -5
  74. package/dist/channel/tool-router.js.map +1 -1
  75. package/dist/channel/types.d.ts +46 -4
  76. package/dist/cli.js +184 -34
  77. package/dist/cli.js.map +1 -1
  78. package/dist/command-table.d.ts +2 -2
  79. package/dist/command-table.js +5 -2
  80. package/dist/command-table.js.map +1 -1
  81. package/dist/compact-watch.d.ts +86 -0
  82. package/dist/compact-watch.js +288 -0
  83. package/dist/compact-watch.js.map +1 -0
  84. package/dist/config-validator.js +55 -5
  85. package/dist/config-validator.js.map +1 -1
  86. package/dist/config.d.ts +1 -0
  87. package/dist/config.js +2 -1
  88. package/dist/config.js.map +1 -1
  89. package/dist/cross-instance-envelope.js +2 -0
  90. package/dist/cross-instance-envelope.js.map +1 -1
  91. package/dist/daemon.d.ts +129 -1
  92. package/dist/daemon.js +754 -94
  93. package/dist/daemon.js.map +1 -1
  94. package/dist/delivery-outbox.d.ts +64 -8
  95. package/dist/delivery-outbox.js +315 -24
  96. package/dist/delivery-outbox.js.map +1 -1
  97. package/dist/delivery-reconciliation.d.ts +18 -1
  98. package/dist/delivery-reconciliation.js +67 -2
  99. package/dist/delivery-reconciliation.js.map +1 -1
  100. package/dist/doctor.js +7 -0
  101. package/dist/doctor.js.map +1 -1
  102. package/dist/fleet-context.d.ts +12 -1
  103. package/dist/fleet-manager.d.ts +392 -19
  104. package/dist/fleet-manager.js +2030 -275
  105. package/dist/fleet-manager.js.map +1 -1
  106. package/dist/forged-envelope-scan.d.ts +4 -3
  107. package/dist/forged-envelope-scan.js +4 -3
  108. package/dist/forged-envelope-scan.js.map +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  111. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  112. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  113. package/dist/host-memory.d.ts +10 -0
  114. package/dist/host-memory.js +33 -1
  115. package/dist/host-memory.js.map +1 -1
  116. package/dist/human-delivery-receipts.d.ts +83 -0
  117. package/dist/human-delivery-receipts.js +194 -0
  118. package/dist/human-delivery-receipts.js.map +1 -0
  119. package/dist/install-lock.d.ts +18 -1
  120. package/dist/install-lock.js +30 -3
  121. package/dist/install-lock.js.map +1 -1
  122. package/dist/instance-avatar.d.ts +5 -4
  123. package/dist/instance-avatar.js +12 -8
  124. package/dist/instance-avatar.js.map +1 -1
  125. package/dist/instance-config-impact.js +1 -0
  126. package/dist/instance-config-impact.js.map +1 -1
  127. package/dist/instance-lifecycle.d.ts +25 -6
  128. package/dist/instance-lifecycle.js +76 -27
  129. package/dist/instance-lifecycle.js.map +1 -1
  130. package/dist/instructions.d.ts +1 -1
  131. package/dist/instructions.js +3 -3
  132. package/dist/instructions.js.map +1 -1
  133. package/dist/kiro-compact-pane.d.ts +38 -0
  134. package/dist/kiro-compact-pane.js +57 -0
  135. package/dist/kiro-compact-pane.js.map +1 -0
  136. package/dist/kiro-log-gate.d.ts +42 -0
  137. package/dist/kiro-log-gate.js +39 -0
  138. package/dist/kiro-log-gate.js.map +1 -0
  139. package/dist/launchd-handoff.d.ts +43 -0
  140. package/dist/launchd-handoff.js +56 -0
  141. package/dist/launchd-handoff.js.map +1 -0
  142. package/dist/locale.js +268 -42
  143. package/dist/locale.js.map +1 -1
  144. package/dist/log-service.d.ts +99 -0
  145. package/dist/log-service.js +243 -0
  146. package/dist/log-service.js.map +1 -0
  147. package/dist/log-steps.d.ts +169 -0
  148. package/dist/log-steps.js +858 -0
  149. package/dist/log-steps.js.map +1 -0
  150. package/dist/login-controller.d.ts +20 -3
  151. package/dist/login-controller.js +71 -19
  152. package/dist/login-controller.js.map +1 -1
  153. package/dist/login-flows.d.ts +5 -2
  154. package/dist/login-flows.js +5 -4
  155. package/dist/login-flows.js.map +1 -1
  156. package/dist/login-shell.d.ts +21 -0
  157. package/dist/login-shell.js +98 -0
  158. package/dist/login-shell.js.map +1 -0
  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/outbound-handlers.d.ts +100 -0
  168. package/dist/outbound-handlers.js +277 -93
  169. package/dist/outbound-handlers.js.map +1 -1
  170. package/dist/outbound-schemas.d.ts +30 -15
  171. package/dist/outbound-schemas.js +23 -5
  172. package/dist/outbound-schemas.js.map +1 -1
  173. package/dist/provider-probe.d.ts +29 -0
  174. package/dist/provider-probe.js +198 -2
  175. package/dist/provider-probe.js.map +1 -1
  176. package/dist/public-link-progress.d.ts +1 -1
  177. package/dist/public-link-progress.js +8 -2
  178. package/dist/public-link-progress.js.map +1 -1
  179. package/dist/public-preview.d.ts +101 -0
  180. package/dist/public-preview.js +236 -0
  181. package/dist/public-preview.js.map +1 -0
  182. package/dist/public-web-gateway.d.ts +2 -0
  183. package/dist/public-web-gateway.js +20 -6
  184. package/dist/public-web-gateway.js.map +1 -1
  185. package/dist/public-web-link.d.ts +24 -1
  186. package/dist/public-web-link.js +63 -9
  187. package/dist/public-web-link.js.map +1 -1
  188. package/dist/quickstart-api.d.ts +1 -1
  189. package/dist/quickstart-api.js +8 -6
  190. package/dist/quickstart-api.js.map +1 -1
  191. package/dist/quickstart.d.ts +12 -0
  192. package/dist/quickstart.js +36 -6
  193. package/dist/quickstart.js.map +1 -1
  194. package/dist/reply-buttons-controller.d.ts +89 -5
  195. package/dist/reply-buttons-controller.js +273 -8
  196. package/dist/reply-buttons-controller.js.map +1 -1
  197. package/dist/reply-buttons.d.ts +48 -3
  198. package/dist/reply-buttons.js +105 -10
  199. package/dist/reply-buttons.js.map +1 -1
  200. package/dist/reply-polls.d.ts +85 -0
  201. package/dist/reply-polls.js +154 -0
  202. package/dist/reply-polls.js.map +1 -0
  203. package/dist/restart-guard.d.ts +6 -3
  204. package/dist/restart-guard.js +6 -3
  205. package/dist/restart-guard.js.map +1 -1
  206. package/dist/restart-progress.d.ts +33 -2
  207. package/dist/restart-progress.js +30 -2
  208. package/dist/restart-progress.js.map +1 -1
  209. package/dist/sd-notify.d.ts +22 -1
  210. package/dist/sd-notify.js +63 -10
  211. package/dist/sd-notify.js.map +1 -1
  212. package/dist/service-activation.d.ts +15 -7
  213. package/dist/service-activation.js +42 -26
  214. package/dist/service-activation.js.map +1 -1
  215. package/dist/service-installer.d.ts +55 -0
  216. package/dist/service-installer.js +90 -3
  217. package/dist/service-installer.js.map +1 -1
  218. package/dist/service-plan.d.ts +11 -0
  219. package/dist/service-plan.js +23 -0
  220. package/dist/service-plan.js.map +1 -1
  221. package/dist/settings-api.js +14 -4
  222. package/dist/settings-api.js.map +1 -1
  223. package/dist/settings-change.js +4 -2
  224. package/dist/settings-change.js.map +1 -1
  225. package/dist/settings-effect.js +2 -0
  226. package/dist/settings-effect.js.map +1 -1
  227. package/dist/spawn-gate.js +5 -1
  228. package/dist/spawn-gate.js.map +1 -1
  229. package/dist/spawn-hold-reply.d.ts +12 -0
  230. package/dist/spawn-hold-reply.js +39 -0
  231. package/dist/spawn-hold-reply.js.map +1 -0
  232. package/dist/systemd-peer-bus.d.ts +95 -0
  233. package/dist/systemd-peer-bus.js +430 -0
  234. package/dist/systemd-peer-bus.js.map +1 -0
  235. package/dist/systemd-runtime.d.ts +27 -1
  236. package/dist/systemd-runtime.js +69 -7
  237. package/dist/systemd-runtime.js.map +1 -1
  238. package/dist/tips.js +1 -1
  239. package/dist/tips.js.map +1 -1
  240. package/dist/tool-permissions.js +3 -3
  241. package/dist/tool-permissions.js.map +1 -1
  242. package/dist/tool-progress.js +3 -2
  243. package/dist/tool-progress.js.map +1 -1
  244. package/dist/topic-commands.d.ts +71 -4
  245. package/dist/topic-commands.js +167 -34
  246. package/dist/topic-commands.js.map +1 -1
  247. package/dist/transcript-delta-reader.d.ts +3 -3
  248. package/dist/transcript-delta-reader.js +8 -8
  249. package/dist/transcript-delta-reader.js.map +1 -1
  250. package/dist/transcript-jsonl.d.ts +85 -3
  251. package/dist/transcript-jsonl.js +156 -13
  252. package/dist/transcript-jsonl.js.map +1 -1
  253. package/dist/transcript-monitor.d.ts +11 -0
  254. package/dist/transcript-monitor.js +61 -13
  255. package/dist/transcript-monitor.js.map +1 -1
  256. package/dist/transcript-sources.d.ts +24 -0
  257. package/dist/transcript-sources.js +211 -56
  258. package/dist/transcript-sources.js.map +1 -1
  259. package/dist/tunnel/cloudflared.d.ts +7 -15
  260. package/dist/tunnel/cloudflared.js +36 -1
  261. package/dist/tunnel/cloudflared.js.map +1 -1
  262. package/dist/tunnel/purpose-lane.d.ts +5 -1
  263. package/dist/tunnel/purpose-lane.js +14 -7
  264. package/dist/tunnel/purpose-lane.js.map +1 -1
  265. package/dist/tunnel/types.d.ts +5 -1
  266. package/dist/tunnel/types.js.map +1 -1
  267. package/dist/turn-resume.d.ts +2 -0
  268. package/dist/turn-resume.js +1 -1
  269. package/dist/turn-resume.js.map +1 -1
  270. package/dist/types.d.ts +22 -0
  271. package/dist/ui/chat-commands.js +1 -0
  272. package/dist/ui/chat-mentions.js +63 -0
  273. package/dist/ui/chat-render.js +80 -10
  274. package/dist/ui/chat-store.js +70 -6
  275. package/dist/ui/chat-strings.js +62 -32
  276. package/dist/ui/chat-thread.js +199 -24
  277. package/dist/ui/chat-time.js +80 -0
  278. package/dist/ui/chat-tour.js +17 -14
  279. package/dist/ui/panel-chat.js +45 -21
  280. package/dist/ui/panel-details.js +2 -0
  281. package/dist/ui/panel-fleet.js +213 -58
  282. package/dist/ui/panel-log.js +428 -0
  283. package/dist/ui/panel-needs.js +14 -10
  284. package/dist/ui/panel-settings.js +82 -17
  285. package/dist/ui/preview-panel.js +55 -15
  286. package/dist/ui/preview-status.js +28 -0
  287. package/dist/ui/preview.js +44 -3
  288. package/dist/ui/settings-dialogs.js +11 -6
  289. package/dist/ui/settings-model.js +3 -1
  290. package/dist/ui/settings-strings.js +32 -12
  291. package/dist/ui/settings-token.js +8 -1
  292. package/dist/ui/settings-wizard.js +5 -3
  293. package/dist/ui/shared/app-i18n.js +41 -15
  294. package/dist/ui/shared/app-prefs.js +250 -0
  295. package/dist/ui/shared/app-route.js +6 -1
  296. package/dist/ui/shared/app-shell.js +165 -8
  297. package/dist/ui/shared/app-store.js +5 -1
  298. package/dist/ui/shared/app-stream.js +26 -5
  299. package/dist/ui/shared/app.css +170 -30
  300. package/dist/ui/shared/app.js +11 -1
  301. package/dist/ui/shared/fonts/README.md +1 -1
  302. package/dist/ui/shared/header-tools.js +83 -13
  303. package/dist/ui/shared/instance-nav.js +178 -51
  304. package/dist/ui/shared/instance-switch.js +8 -5
  305. package/dist/ui/shared/panel-view.js +21 -4
  306. package/dist/ui/shared/signin.js +15 -9
  307. package/dist/ui/shared/theme.js +39 -7
  308. package/dist/ui/shared/tokens.css +1 -1
  309. package/dist/ui/shared/ui-icons.js +2 -0
  310. package/dist/ui/shared/usage-dialog.js +23 -5
  311. package/dist/ui/shared/view-strings.js +40 -4
  312. package/dist/ui/signin.html +8 -3
  313. package/dist/unrecognised-prompt.d.ts +23 -0
  314. package/dist/unrecognised-prompt.js +74 -0
  315. package/dist/unrecognised-prompt.js.map +1 -0
  316. package/dist/update-check.d.ts +31 -0
  317. package/dist/update-check.js +47 -0
  318. package/dist/update-check.js.map +1 -1
  319. package/dist/update-install.d.ts +13 -1
  320. package/dist/update-install.js +16 -4
  321. package/dist/update-install.js.map +1 -1
  322. package/dist/update-launch.d.ts +48 -1
  323. package/dist/update-launch.js +159 -5
  324. package/dist/update-launch.js.map +1 -1
  325. package/dist/update-marker.d.ts +51 -0
  326. package/dist/update-marker.js +61 -0
  327. package/dist/update-marker.js.map +1 -1
  328. package/dist/usage/i18n-keys.d.ts +1 -1
  329. package/dist/usage/i18n-keys.js +1 -1
  330. package/dist/usage/i18n-keys.js.map +1 -1
  331. package/dist/usage/providers.d.ts +62 -13
  332. package/dist/usage/providers.js +256 -68
  333. package/dist/usage/providers.js.map +1 -1
  334. package/dist/usage/statusline-usage.d.ts +6 -2
  335. package/dist/usage/statusline-usage.js +10 -4
  336. package/dist/usage/statusline-usage.js.map +1 -1
  337. package/dist/view-api.js +31 -9
  338. package/dist/view-api.js.map +1 -1
  339. package/dist/web-api.d.ts +82 -4
  340. package/dist/web-api.js +632 -88
  341. package/dist/web-api.js.map +1 -1
  342. package/dist/web-auth.js +5 -2
  343. package/dist/web-auth.js.map +1 -1
  344. package/dist/web-chat-history.d.ts +26 -1
  345. package/dist/web-chat-history.js +47 -8
  346. package/dist/web-chat-history.js.map +1 -1
  347. package/dist/web-commands.d.ts +41 -5
  348. package/dist/web-commands.js +25 -7
  349. package/dist/web-commands.js.map +1 -1
  350. package/dist/web-login.d.ts +14 -0
  351. package/dist/web-login.js +4 -2
  352. package/dist/web-login.js.map +1 -1
  353. package/dist/web-mentions.d.ts +44 -0
  354. package/dist/web-mentions.js +75 -0
  355. package/dist/web-mentions.js.map +1 -0
  356. package/dist/web-prefs.d.ts +81 -0
  357. package/dist/web-prefs.js +297 -0
  358. package/dist/web-prefs.js.map +1 -0
  359. package/dist/web-preview.d.ts +14 -1
  360. package/dist/web-preview.js +35 -13
  361. package/dist/web-preview.js.map +1 -1
  362. package/dist/web-profile.d.ts +32 -0
  363. package/dist/web-profile.js +132 -0
  364. package/dist/web-profile.js.map +1 -0
  365. package/dist/web-reply-files.d.ts +45 -0
  366. package/dist/web-reply-files.js +223 -0
  367. package/dist/web-reply-files.js.map +1 -0
  368. package/dist/web-schedules.d.ts +12 -0
  369. package/dist/web-schedules.js +34 -0
  370. package/dist/web-schedules.js.map +1 -0
  371. package/dist/web-server-drain.d.ts +10 -0
  372. package/dist/web-server-drain.js +50 -0
  373. package/dist/web-server-drain.js.map +1 -0
  374. package/dist/web-session.d.ts +14 -0
  375. package/dist/web-session.js +11 -1
  376. package/dist/web-session.js.map +1 -1
  377. package/dist/web-settings-patch.d.ts +3 -1
  378. package/dist/web-settings-patch.js +4 -1
  379. package/dist/web-settings-patch.js.map +1 -1
  380. package/dist/web-shell-routes.d.ts +4 -0
  381. package/dist/web-shell-routes.js +9 -0
  382. package/dist/web-shell-routes.js.map +1 -1
  383. package/dist/web-upload.d.ts +135 -7
  384. package/dist/web-upload.js +315 -51
  385. package/dist/web-upload.js.map +1 -1
  386. package/launcher/install-admission.cjs +61 -6
  387. package/package.json +3 -3
  388. package/templates/launchd.plist.ejs +3 -1
  389. 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)
@@ -168,7 +168,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
168
168
  respond(null, v.error);
169
169
  return;
170
170
  }
171
- 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;
172
172
  if (rejectOversizedCrossInstanceMessage(ctx, message, respond))
173
173
  return;
174
174
  const senderLabel = meta.senderSessionName ?? meta.instanceName;
@@ -224,7 +224,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
224
224
  const queuedPredecessor = steerCapable
225
225
  && ctx.hasPendingIdleGatedDelivery?.(targetInstanceName) === true;
226
226
  const useSteer = steerCapable && !queuedPredecessor;
227
- const steerFallbackWarning = steer === true && !useSteer
227
+ let steerFallbackWarning = steer === true && !useSteer
228
228
  ? isExternalSession
229
229
  ? t("delivery.steer_external")
230
230
  : queuedPredecessor
@@ -255,6 +255,8 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
255
255
  ipcMeta.working_directory = working_directory;
256
256
  if (branch)
257
257
  ipcMeta.branch = branch;
258
+ if (forwards?.length)
259
+ ipcMeta.forwards = JSON.stringify(forwards);
258
260
  // #77: surface the sender's display name in the recipient's [from:…] header
259
261
  // (keeps from_instance as the machine name so the reply-back target stays valid).
260
262
  const senderDisplay = ctx.fleetConfig?.instances[senderLabel]?.display_name;
@@ -278,6 +280,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
278
280
  meta: ipcMeta,
279
281
  };
280
282
  let durableReceipt;
283
+ let effectiveUseSteer = useSteer;
281
284
  if (ctx.admitDurableDelivery) {
282
285
  try {
283
286
  durableReceipt = ctx.admitDurableDelivery({
@@ -291,7 +294,13 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
291
294
  kind: useSteer ? "steer" : "fleet_inbound",
292
295
  correlationId,
293
296
  payload,
297
+ forwards,
298
+ requestedKind: steer === true ? "steer" : "fleet_inbound",
294
299
  });
300
+ if (durableReceipt.kind)
301
+ effectiveUseSteer = durableReceipt.kind === "steer";
302
+ if (durableReceipt.duplicate && durableReceipt.kind !== undefined && durableReceipt.kind !== payload.type)
303
+ steerFallbackWarning = undefined;
295
304
  }
296
305
  catch (err) {
297
306
  ctx.logger.error({ err, source: meta.instanceName, target: targetInstanceName }, "Durable outbound admission failed closed");
@@ -300,6 +309,10 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
300
309
  }
301
310
  }
302
311
  else {
312
+ if (forwards?.length) {
313
+ respond(null, "Forwarded message verification requires the durable delivery store");
314
+ return;
315
+ }
303
316
  // Standalone handler harnesses retain the legacy path; production FleetManager
304
317
  // always provides durable admission.
305
318
  void deliverCrossInstanceWithRetry(ctx, targetInstanceName, targetName, senderLabel, correlationId, payload, useSteer ? { isCrossInstance: true, waitForIdle: false } : undefined);
@@ -336,33 +349,43 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
336
349
  displayName: ctx.fleetConfig?.instances[name]?.display_name,
337
350
  })));
338
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;
339
362
  if (groupId && ctx.adapter) {
340
- const instances = ctx.fleetConfig?.instances ?? {};
341
- const notificationLabel = `${displayLabel(senderLabel)} → ${displayLabel(targetName)}`;
342
- const notice = { label: notificationLabel, message, requestKind, taskSummary: ipcMeta.task_summary };
343
363
  // ── Target topic notification ──
344
- const targetInstance = instances[targetInstanceName];
345
364
  const targetTopicId = targetInstance?.topic_id;
346
- const targetIsGeneral = targetInstance?.general_topic === true;
347
- const targetText = targetTopicNotice(crossInstanceVisibility(ctx.fleetConfig, targetInstanceName), notice);
348
- if (targetText !== null && targetTopicId && !targetIsGeneral && !ctx.sessionRegistry.has(targetName)) {
365
+ if (targetShown && targetTopicId) {
349
366
  const targetAdapter = ctx.getAdapterForInstance?.(targetInstanceName) ?? ctx.adapter;
350
367
  const targetGroupId = ctx.getGroupIdForInstance?.(targetInstanceName) ?? String(groupId);
351
368
  targetAdapter.sendText(String(targetGroupId), targetText, { threadId: String(targetTopicId) })
352
369
  .catch(e => ctx.logger.warn({ err: e }, "Failed to post target topic notification"));
353
370
  }
354
371
  // ── Sender topic notification ──
355
- const senderInstance = instances[meta.instanceName];
356
372
  const senderTopicId = senderInstance?.topic_id;
357
- const senderIsGeneral = senderInstance?.general_topic === true;
358
- const senderText = senderTopicNotice(crossInstanceVisibility(ctx.fleetConfig, meta.instanceName), notice);
359
- if (senderText !== null && senderTopicId && !senderIsGeneral) {
373
+ if (senderShown && senderTopicId) {
360
374
  const senderAdapter = ctx.getAdapterForInstance?.(meta.instanceName) ?? ctx.adapter;
361
375
  const senderGroupId = ctx.getGroupIdForInstance?.(meta.instanceName) ?? String(groupId);
362
376
  senderAdapter.sendText(senderGroupId, senderText, { threadId: String(senderTopicId) })
363
377
  .catch(e => ctx.logger.warn({ err: e }, "Failed to post sender topic notification"));
364
378
  }
365
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);
366
389
  // #856: the same line, now carrying the envelope's message_id and a short
367
390
  // content digest, so a disputed message can be matched to its outbox row
368
391
  // and paste digests without adding a log line per delivery.
@@ -382,6 +405,7 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
382
405
  : undefined;
383
406
  const warnings = [steerFallbackWarning, targetStateWarning].filter((value) => !!value);
384
407
  respond({ sent: true, queued: true, target: targetName, target_state: state,
408
+ ...(forwards?.length ? { forwards } : {}),
385
409
  ...(durableReceipt ? {
386
410
  durable: true,
387
411
  delivery_id: durableReceipt.deliveryId,
@@ -390,8 +414,8 @@ const sendToInstance = async (ctx, rawArgs, respond, meta) => {
390
414
  duplicate: durableReceipt.duplicate,
391
415
  } : {}),
392
416
  ...(state === "paused" ? { waking: true } : {}), correlation_id: correlationId,
393
- ...(useSteer ? { delivery_mode: "steer" } : {}),
394
- ...(steerFallbackWarning ? { delivery_mode: "idle_queue" } : {}),
417
+ ...(effectiveUseSteer ? { delivery_mode: "steer" } : {}),
418
+ ...(steer === true && !effectiveUseSteer ? { delivery_mode: "idle_queue" } : {}),
395
419
  ...(warnings.length > 0 ? { warning: warnings.join(" ") } : {}),
396
420
  });
397
421
  };
@@ -763,6 +787,25 @@ const kiroEngineStatusHandler = (ctx, rawArgs, respond) => {
763
787
  }
764
788
  respond(status);
765
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
+ };
766
809
  const getEffort = (ctx, rawArgs, respond, meta) => {
767
810
  const v = validateArgs(GetEffortArgs, rawArgs, "get_effort");
768
811
  if (!v.ok) {
@@ -921,31 +964,86 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
921
964
  respond(null, v.error);
922
965
  return;
923
966
  }
924
- 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];
925
975
  if (!inst) {
926
976
  const isClassic = (ctx.classicChannels?.getAll() ?? [])
927
- .some(channel => channel.instanceName === v.data.name);
977
+ .some(channel => channel.instanceName === name);
928
978
  respond(null, isClassic
929
979
  ? "This is a ClassicBot instance (managed in classicBot.yaml). Use set_display_name or set_description to configure it."
930
- : `Instance '${v.data.name}' not found in fleet config`);
980
+ : `Instance '${name}' not found in fleet config`);
931
981
  return;
932
982
  }
933
- const patch = v.data.config;
934
983
  // Read before anything is mutated: the switch is what decides whether the
935
984
  // next launch may resume, and by then `inst` already holds the new value.
936
- 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
+ }
937
1024
  const profileBefore = instanceCredentialProfile(inst, ctx.fleetConfig?.defaults, backendBefore);
938
1025
  // Snapshot enough to undo the whole patch if validation refuses it.
939
1026
  const beforeEdit = validateFleetConfig(ctx.fleetConfig);
940
1027
  const beforeFields = {};
941
- 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"]) {
942
1029
  if (patch[key] !== undefined)
943
1030
  beforeFields[key] = inst[key];
944
1031
  }
945
1032
  if (patch.backend !== undefined)
946
1033
  inst.backend = patch.backend;
947
- if (patch.model !== undefined)
948
- 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
+ }
949
1047
  if (patch.auto_pause_after !== undefined)
950
1048
  inst.auto_pause_after = patch.auto_pause_after;
951
1049
  if (patch.display_name !== undefined)
@@ -959,20 +1057,7 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
959
1057
  let relaunchNeeded = false;
960
1058
  if (patch.backend_options !== undefined) {
961
1059
  const before = JSON.stringify(previousOptions ?? {});
962
- const merged = { ...(previousOptions ?? {}) };
963
- for (const [backendName, options] of Object.entries(patch.backend_options)) {
964
- const namespace = { ...(merged[backendName] ?? {}), ...options };
965
- // A merge cannot express removal, so null (or "") means "drop this key"
966
- // — that is how an agent goes back to the fleet's default login.
967
- for (const [key, value] of Object.entries(options)) {
968
- if (value === null || value === "")
969
- delete namespace[key];
970
- }
971
- if (Object.keys(namespace).length)
972
- merged[backendName] = namespace;
973
- else
974
- delete merged[backendName];
975
- }
1060
+ const merged = mergeBackendOptions(previousOptions, patch.backend_options);
976
1061
  inst.backend_options = merged;
977
1062
  relaunchNeeded = JSON.stringify(merged) !== before;
978
1063
  }
@@ -1000,7 +1085,7 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1000
1085
  respond(null, `Rejected: ${introduced.map(issue => `${issue.path}: ${issue.message}`).join("; ")}`);
1001
1086
  return;
1002
1087
  }
1003
- const backendAfter = inst.backend ?? "claude-code";
1088
+ const backendAfter = effectiveBackend();
1004
1089
  const profileAfter = instanceCredentialProfile(inst, ctx.fleetConfig?.defaults, backendAfter);
1005
1090
  const profileSwitched = profileAfter !== profileBefore;
1006
1091
  // #1217: a running CLI is the backend it was launched as. Saving a new
@@ -1023,13 +1108,17 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1023
1108
  // the escape hatch from a bad switch and must never be the thing that is
1024
1109
  // blocked, and an unlogged shared store means the CLI was never set up at
1025
1110
  // all — a state that predates this feature.
1026
- 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) {
1027
1114
  // The same root the launcher resolves the profile under, so the store this
1028
1115
  // probes is the store the CLI will be pointed at.
1029
1116
  const login = credentialProfileLogin(ctx.dataDir, backendAfter, profileAfter);
1030
1117
  if (login.state === "signed-out") {
1031
1118
  rollback();
1032
- 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}`);
1033
1122
  return;
1034
1123
  }
1035
1124
  }
@@ -1042,18 +1131,18 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1042
1131
  return;
1043
1132
  }
1044
1133
  if (!relaunchNeeded) {
1045
- respond({ success: true, name: v.data.name, applied: patch });
1134
+ respond({ success: true, name, applied: patch });
1046
1135
  return;
1047
1136
  }
1048
1137
  // Only a running instance is restarted. Switching the subscription of a
1049
1138
  // paused or stopped agent must not start it — restartSingleInstance is
1050
1139
  // stop-then-start and would — so the new credentials are read whenever it
1051
1140
  // next comes up on its own.
1052
- const status = targetState(ctx, v.data.name);
1141
+ const status = targetState(ctx, name);
1053
1142
  if (status !== "running") {
1054
1143
  respond({
1055
1144
  success: true,
1056
- name: v.data.name,
1145
+ name,
1057
1146
  applied: patch,
1058
1147
  restarted: false,
1059
1148
  note: `Instance is ${status}; the new ${backendSwitched ? "backend" : "backend_options"} apply when it next starts.`,
@@ -1073,19 +1162,26 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1073
1162
  // was — taking a handover there would be describing a loss that did not
1074
1163
  // happen, and starting fresh would be causing one.
1075
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;
1076
1171
  const handover = switchLosesConversation
1077
- ? (ctx.lifecycle.daemons.get(v.data.name)?.collectHandoverContext() ?? "")
1172
+ ? (ctx.lifecycle.daemons.get(name)?.collectHandoverContext() ?? "")
1078
1173
  : "";
1079
1174
  // Saving without restarting would report a subscription switch that has not
1080
1175
  // happened — the running CLI keeps the credentials it was launched with.
1081
- // A switch also starts fresh: the new store has no conversation for this
1082
- // directory, so resuming can only spend the resume startup budget waiting for
1083
- // something that is not there.
1084
- ctx.restartSingleInstance(v.data.name, switchLosesConversation || backendSwitched ? { freshStart: true } : undefined).then(async () => {
1085
- 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);
1086
1182
  respond({
1087
1183
  success: true,
1088
- name: v.data.name,
1184
+ name,
1089
1185
  applied: patch,
1090
1186
  restarted: true,
1091
1187
  ...(backendSwitched ? { backend_switched: true, conversation_carried_over: false } : {}),
@@ -1096,21 +1192,43 @@ const updateInstanceConfig = (ctx, rawArgs, respond) => {
1096
1192
  // across accounts is the thing nobody has been able to test yet,
1097
1193
  // so this says where the history lives rather than what will
1098
1194
  // happen when the agent reaches for it.
1099
- conversation_carried_over: !switchLosesConversation,
1100
- ...(switchLosesConversation
1101
- ? { handover_chars: handoverChars }
1102
- : { 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.` }),
1103
1201
  }
1104
1202
  : {}),
1105
1203
  });
1106
1204
  }, (err) => respond({
1107
1205
  success: true,
1108
- name: v.data.name,
1206
+ name,
1109
1207
  applied: patch,
1110
1208
  restarted: false,
1111
1209
  warning: `Config saved, but the restart failed: ${err.message}. The new backend_options take effect on the next start.`,
1112
1210
  }));
1113
- };
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
+ }
1114
1232
  /** Long enough for the relaunched CLI to be listening; matches replace_instance. */
1115
1233
  export const PROFILE_HANDOVER_SETTLE_MS = 3_000;
1116
1234
  /**
@@ -1205,6 +1323,8 @@ function wrapAsSend(schema, toolName, buildArgs, warnMissing) {
1205
1323
  request_kind: kind,
1206
1324
  requires_reply: reply,
1207
1325
  task_summary: summary,
1326
+ ...(v.data.forwards !== undefined
1327
+ ? { forwards: v.data.forwards } : {}),
1208
1328
  };
1209
1329
  // Re-dispatch through the handler map
1210
1330
  return sendToInstance(ctx, newArgs, respond, meta);
@@ -1404,30 +1524,50 @@ const broadcast = async (ctx, rawArgs, respond, meta) => {
1404
1524
  });
1405
1525
  };
1406
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
+ }
1407
1559
  const createTeam = (ctx, rawArgs, respond) => {
1408
1560
  const v = validateArgs(CreateTeamArgs, rawArgs, "create_team");
1409
1561
  if (!v.ok) {
1410
1562
  respond(null, v.error);
1411
1563
  return;
1412
1564
  }
1413
- const { name, members, description } = v.data;
1414
- if (!ctx.fleetConfig) {
1415
- respond(null, "Fleet config not available");
1416
- return;
1417
- }
1418
- const invalid = members.filter(m => !ctx.fleetConfig.instances[m]);
1419
- if (invalid.length) {
1420
- respond(null, `Invalid instance names: ${invalid.join(", ")}`);
1421
- return;
1422
- }
1423
- ctx.fleetConfig.teams ??= {};
1424
- if (ctx.fleetConfig.teams[name]) {
1425
- 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);
1426
1568
  return;
1427
1569
  }
1428
- ctx.fleetConfig.teams[name] = { members, ...(description ? { description } : {}) };
1429
- ctx.saveFleetConfig();
1430
- respond({ created: name, members });
1570
+ respond(r.result);
1431
1571
  };
1432
1572
  const deleteTeam = (ctx, rawArgs, respond) => {
1433
1573
  const v = validateArgs(DeleteTeamArgs, rawArgs, "delete_team");
@@ -1461,31 +1601,74 @@ const listTeams = (ctx, rawArgs, respond) => {
1461
1601
  }));
1462
1602
  respond(result);
1463
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
+ }
1464
1660
  const updateTeam = (ctx, rawArgs, respond) => {
1465
1661
  const v = validateArgs(UpdateTeamArgs, rawArgs, "update_team");
1466
1662
  if (!v.ok) {
1467
1663
  respond(null, v.error);
1468
1664
  return;
1469
1665
  }
1470
- const { name, add, remove } = v.data;
1471
- if (!ctx.fleetConfig?.teams?.[name]) {
1472
- 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);
1473
1669
  return;
1474
1670
  }
1475
- const team = ctx.fleetConfig.teams[name];
1476
- if (add?.length) {
1477
- const invalid = add.filter(m => !ctx.fleetConfig.instances[m]);
1478
- if (invalid.length) {
1479
- respond(null, `Invalid instance names: ${invalid.join(", ")}`);
1480
- return;
1481
- }
1482
- team.members = [...new Set([...team.members, ...add])];
1483
- }
1484
- if (remove?.length) {
1485
- team.members = team.members.filter(m => !remove.includes(m));
1486
- }
1487
- ctx.saveFleetConfig();
1488
- respond({ name, members: team.members });
1671
+ respond(r.result);
1489
1672
  };
1490
1673
  // ── Fleet Templates ────────────────────────────────────────────────────
1491
1674
  const deployTemplate = async (ctx, rawArgs, respond) => {
@@ -1786,6 +1969,7 @@ export const outboundHandlers = new Map([
1786
1969
  ["stop_instance", stopInstance],
1787
1970
  ["get_fleet_status", getFleetStatus],
1788
1971
  ["get_effort", getEffort],
1972
+ ["close_poll", closePoll],
1789
1973
  ["kiro_engine_status", kiroEngineStatusHandler],
1790
1974
  ["get_usage", getUsage],
1791
1975
  ["list_models", listModels],