@songsid/agend 2.3.5-alpha.4 → 2.3.5-beta.1

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 (670) hide show
  1. package/README.md +0 -2
  2. package/README.zh-TW.md +0 -2
  3. package/THIRD_PARTY_NOTICES.md +12 -28
  4. package/dist/agent-cli-instructions.md +1 -1
  5. package/dist/agent-credentials.d.ts +24 -0
  6. package/dist/agent-credentials.js +66 -0
  7. package/dist/agent-credentials.js.map +1 -0
  8. package/dist/agent-endpoint.d.ts +4 -6
  9. package/dist/agent-endpoint.js +15 -12
  10. package/dist/agent-endpoint.js.map +1 -1
  11. package/dist/auth-api.d.ts +0 -21
  12. package/dist/auth-api.js +8 -93
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/session-authorizations.d.ts +30 -4
  15. package/dist/authority/session-authorizations.js +69 -17
  16. package/dist/authority/session-authorizations.js.map +1 -1
  17. package/dist/authority/web-authority.d.ts +2 -12
  18. package/dist/authority/web-authority.js +4 -25
  19. package/dist/authority/web-authority.js.map +1 -1
  20. package/dist/backend/antigravity.js +3 -1
  21. package/dist/backend/antigravity.js.map +1 -1
  22. package/dist/backend/claude-code.d.ts +12 -15
  23. package/dist/backend/claude-code.js +133 -152
  24. package/dist/backend/claude-code.js.map +1 -1
  25. package/dist/backend/codex-metadata.d.ts +14 -0
  26. package/dist/backend/codex-metadata.js +45 -0
  27. package/dist/backend/codex-metadata.js.map +1 -1
  28. package/dist/backend/codex-package.d.ts +2 -5
  29. package/dist/backend/codex-package.js +5 -1
  30. package/dist/backend/codex-package.js.map +1 -1
  31. package/dist/backend/codex-quota-history.d.ts +7 -0
  32. package/dist/backend/codex-quota-history.js +72 -0
  33. package/dist/backend/codex-quota-history.js.map +1 -0
  34. package/dist/backend/codex-session-lookup.d.ts +6 -0
  35. package/dist/backend/codex-session-lookup.js +1 -1
  36. package/dist/backend/codex-session-lookup.js.map +1 -1
  37. package/dist/backend/codex-speed.d.ts +14 -0
  38. package/dist/backend/codex-speed.js +35 -0
  39. package/dist/backend/codex-speed.js.map +1 -0
  40. package/dist/backend/codex.d.ts +116 -136
  41. package/dist/backend/codex.js +912 -942
  42. package/dist/backend/codex.js.map +1 -1
  43. package/dist/backend/grok-weekly-reset.d.ts +24 -0
  44. package/dist/backend/grok-weekly-reset.js +95 -0
  45. package/dist/backend/grok-weekly-reset.js.map +1 -0
  46. package/dist/backend/grok.d.ts +30 -3
  47. package/dist/backend/grok.js +301 -76
  48. package/dist/backend/grok.js.map +1 -1
  49. package/dist/backend/kiro.d.ts +3 -0
  50. package/dist/backend/kiro.js +50 -2
  51. package/dist/backend/kiro.js.map +1 -1
  52. package/dist/backend/muse.js +5 -3
  53. package/dist/backend/muse.js.map +1 -1
  54. package/dist/backend/types.d.ts +15 -8
  55. package/dist/backend/types.js +3 -0
  56. package/dist/backend/types.js.map +1 -1
  57. package/dist/bundled-tmux-guard.d.ts +21 -0
  58. package/dist/bundled-tmux-guard.js +96 -0
  59. package/dist/bundled-tmux-guard.js.map +1 -0
  60. package/dist/cache-ledger.d.ts +95 -0
  61. package/dist/cache-ledger.js +276 -4
  62. package/dist/cache-ledger.js.map +1 -1
  63. package/dist/cache-service.d.ts +5 -0
  64. package/dist/cache-service.js +16 -4
  65. package/dist/cache-service.js.map +1 -1
  66. package/dist/channel/adapters/discord.js +11 -3
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +51 -0
  69. package/dist/channel/adapters/telegram.js +235 -17
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/ipc-bridge.d.ts +0 -2
  72. package/dist/channel/mcp-tools.js +3 -3
  73. package/dist/channel/mcp-tools.js.map +1 -1
  74. package/dist/channel/types.d.ts +2 -0
  75. package/dist/classic-channel-manager.d.ts +2 -0
  76. package/dist/classic-channel-manager.js +5 -1
  77. package/dist/classic-channel-manager.js.map +1 -1
  78. package/dist/cli.js +14 -10
  79. package/dist/cli.js.map +1 -1
  80. package/dist/config-validator.js +30 -71
  81. package/dist/config-validator.js.map +1 -1
  82. package/dist/config.js +3 -17
  83. package/dist/config.js.map +1 -1
  84. package/dist/cross-instance-notice.d.ts +10 -0
  85. package/dist/cross-instance-notice.js +67 -5
  86. package/dist/cross-instance-notice.js.map +1 -1
  87. package/dist/daemon.d.ts +70 -68
  88. package/dist/daemon.js +774 -846
  89. package/dist/daemon.js.map +1 -1
  90. package/dist/dashboard-permissions.d.ts +8 -0
  91. package/dist/dashboard-permissions.js +16 -0
  92. package/dist/dashboard-permissions.js.map +1 -1
  93. package/dist/delivery-consumed-watch.d.ts +26 -16
  94. package/dist/delivery-consumed-watch.js +31 -26
  95. package/dist/delivery-consumed-watch.js.map +1 -1
  96. package/dist/delivery-outbox.d.ts +0 -5
  97. package/dist/delivery-outbox.js +4 -32
  98. package/dist/delivery-outbox.js.map +1 -1
  99. package/dist/delivery-reconciliation.d.ts +42 -2
  100. package/dist/delivery-reconciliation.js +164 -2
  101. package/dist/delivery-reconciliation.js.map +1 -1
  102. package/dist/export-import.d.ts +7 -0
  103. package/dist/export-import.js +44 -10
  104. package/dist/export-import.js.map +1 -1
  105. package/dist/fleet-context.d.ts +4 -2
  106. package/dist/fleet-level-config.d.ts +1 -1
  107. package/dist/fleet-level-config.js +0 -2
  108. package/dist/fleet-level-config.js.map +1 -1
  109. package/dist/fleet-manager.d.ts +108 -245
  110. package/dist/fleet-manager.js +656 -2028
  111. package/dist/fleet-manager.js.map +1 -1
  112. package/dist/general-instance.d.ts +0 -7
  113. package/dist/general-instance.js +3 -20
  114. package/dist/general-instance.js.map +1 -1
  115. package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
  116. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  117. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  118. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  119. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +9 -3
  120. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  121. package/dist/instance-config-impact.js +6 -7
  122. package/dist/instance-config-impact.js.map +1 -1
  123. package/dist/instance-lifecycle.d.ts +19 -23
  124. package/dist/instance-lifecycle.js +216 -383
  125. package/dist/instance-lifecycle.js.map +1 -1
  126. package/dist/instance-topic-delete.d.ts +77 -0
  127. package/dist/instance-topic-delete.js +123 -0
  128. package/dist/instance-topic-delete.js.map +1 -0
  129. package/dist/instructions.d.ts +1 -3
  130. package/dist/instructions.js +1 -7
  131. package/dist/instructions.js.map +1 -1
  132. package/dist/kiro-engine-status.d.ts +11 -0
  133. package/dist/kiro-engine-status.js +6 -0
  134. package/dist/kiro-engine-status.js.map +1 -1
  135. package/dist/locale.js +63 -86
  136. package/dist/locale.js.map +1 -1
  137. package/dist/log-service.d.ts +28 -0
  138. package/dist/log-service.js +29 -0
  139. package/dist/log-service.js.map +1 -1
  140. package/dist/log-steps.d.ts +4 -3
  141. package/dist/log-steps.js +32 -7
  142. package/dist/log-steps.js.map +1 -1
  143. package/dist/logger.d.ts +16 -2
  144. package/dist/logger.js +346 -66
  145. package/dist/logger.js.map +1 -1
  146. package/dist/login-flows.d.ts +7 -0
  147. package/dist/login-flows.js +5 -2
  148. package/dist/login-flows.js.map +1 -1
  149. package/dist/login-manager.js +25 -4
  150. package/dist/login-manager.js.map +1 -1
  151. package/dist/missed-offline.d.ts +25 -0
  152. package/dist/missed-offline.js +79 -0
  153. package/dist/missed-offline.js.map +1 -0
  154. package/dist/muse-usage-relay.d.ts +2 -0
  155. package/dist/muse-usage-relay.js +5 -3
  156. package/dist/muse-usage-relay.js.map +1 -1
  157. package/dist/needs-you-hub.d.ts +6 -3
  158. package/dist/needs-you-hub.js +31 -9
  159. package/dist/needs-you-hub.js.map +1 -1
  160. package/dist/needs-you.d.ts +20 -18
  161. package/dist/needs-you.js +18 -14
  162. package/dist/needs-you.js.map +1 -1
  163. package/dist/office-share-page.js +1 -1
  164. package/dist/office-share-page.js.map +1 -1
  165. package/dist/outbound-handlers.d.ts +2 -16
  166. package/dist/outbound-handlers.js +10 -72
  167. package/dist/outbound-handlers.js.map +1 -1
  168. package/dist/outbound-schemas.d.ts +0 -38
  169. package/dist/outbound-schemas.js +5 -14
  170. package/dist/outbound-schemas.js.map +1 -1
  171. package/dist/pane-quota-history.d.ts +7 -0
  172. package/dist/pane-quota-history.js +47 -0
  173. package/dist/pane-quota-history.js.map +1 -0
  174. package/dist/platform-general.d.ts +46 -0
  175. package/dist/platform-general.js +91 -0
  176. package/dist/platform-general.js.map +1 -0
  177. package/dist/public-web-gateway.js +0 -9
  178. package/dist/public-web-gateway.js.map +1 -1
  179. package/dist/public-web-link.d.ts +0 -11
  180. package/dist/public-web-link.js +0 -13
  181. package/dist/public-web-link.js.map +1 -1
  182. package/dist/queued-handoffs.d.ts +33 -0
  183. package/dist/queued-handoffs.js +75 -0
  184. package/dist/queued-handoffs.js.map +1 -0
  185. package/dist/quickstart-api.js +5 -9
  186. package/dist/quickstart-api.js.map +1 -1
  187. package/dist/quickstart.d.ts +8 -1
  188. package/dist/quickstart.js +42 -4
  189. package/dist/quickstart.js.map +1 -1
  190. package/dist/quota-notice-config.d.ts +22 -0
  191. package/dist/quota-notice-config.js +41 -0
  192. package/dist/quota-notice-config.js.map +1 -0
  193. package/dist/quota-notice.d.ts +7 -0
  194. package/dist/quota-notice.js +40 -0
  195. package/dist/quota-notice.js.map +1 -1
  196. package/dist/reply-buttons-controller.js +10 -6
  197. package/dist/reply-buttons-controller.js.map +1 -1
  198. package/dist/reply-polls.d.ts +2 -2
  199. package/dist/reply-polls.js +3 -3
  200. package/dist/reply-polls.js.map +1 -1
  201. package/dist/scheduler/db.d.ts +2 -1
  202. package/dist/scheduler/db.js +23 -9
  203. package/dist/scheduler/db.js.map +1 -1
  204. package/dist/scheduler/task-board.d.ts +9 -0
  205. package/dist/scheduler/task-board.js +19 -0
  206. package/dist/scheduler/task-board.js.map +1 -1
  207. package/dist/settings-api.d.ts +13 -5
  208. package/dist/settings-api.js +26 -96
  209. package/dist/settings-api.js.map +1 -1
  210. package/dist/settings-baseline.d.ts +0 -1
  211. package/dist/settings-baseline.js +1 -1
  212. package/dist/settings-baseline.js.map +1 -1
  213. package/dist/settings-change.js +23 -28
  214. package/dist/settings-change.js.map +1 -1
  215. package/dist/settings-confirmation.d.ts +1 -16
  216. package/dist/settings-confirmation.js +9 -47
  217. package/dist/settings-confirmation.js.map +1 -1
  218. package/dist/settings-effect.d.ts +0 -4
  219. package/dist/settings-effect.js +1 -58
  220. package/dist/settings-effect.js.map +1 -1
  221. package/dist/settings-http-confirmation.d.ts +0 -4
  222. package/dist/settings-http-confirmation.js +3 -31
  223. package/dist/settings-http-confirmation.js.map +1 -1
  224. package/dist/settings-instance-patch.js +28 -1
  225. package/dist/settings-instance-patch.js.map +1 -1
  226. package/dist/settings-request-capability.d.ts +0 -2
  227. package/dist/settings-request-capability.js +0 -4
  228. package/dist/settings-request-capability.js.map +1 -1
  229. package/dist/settings-transaction.d.ts +0 -4
  230. package/dist/settings-transaction.js +0 -9
  231. package/dist/settings-transaction.js.map +1 -1
  232. package/dist/setup-form.js +3 -6
  233. package/dist/setup-form.js.map +1 -1
  234. package/dist/setup-i18n.js +0 -3
  235. package/dist/setup-i18n.js.map +1 -1
  236. package/dist/task-board-tool.d.ts +11 -0
  237. package/dist/task-board-tool.js +44 -0
  238. package/dist/task-board-tool.js.map +1 -1
  239. package/dist/task-board-waker.d.ts +6 -0
  240. package/dist/task-board-waker.js +4 -0
  241. package/dist/task-board-waker.js.map +1 -1
  242. package/dist/tmux-control.d.ts +1 -2
  243. package/dist/tmux-control.js.map +1 -1
  244. package/dist/tmux-manager.d.ts +1 -2
  245. package/dist/tmux-manager.js.map +1 -1
  246. package/dist/tool-permissions.d.ts +5 -2
  247. package/dist/tool-permissions.js +7 -7
  248. package/dist/tool-permissions.js.map +1 -1
  249. package/dist/tool-progress.d.ts +2 -0
  250. package/dist/tool-progress.js +19 -3
  251. package/dist/tool-progress.js.map +1 -1
  252. package/dist/topic-commands.js +4 -3
  253. package/dist/topic-commands.js.map +1 -1
  254. package/dist/transcript-delta-reader.d.ts +1 -1
  255. package/dist/transcript-delta-reader.js +1 -1
  256. package/dist/transcript-delta-reader.js.map +1 -1
  257. package/dist/transcript-monitor.d.ts +20 -3
  258. package/dist/transcript-monitor.js +92 -24
  259. package/dist/transcript-monitor.js.map +1 -1
  260. package/dist/transcript-sources.d.ts +0 -5
  261. package/dist/transcript-sources.js +79 -8
  262. package/dist/transcript-sources.js.map +1 -1
  263. package/dist/types.d.ts +3 -23
  264. package/dist/ui/app.html +8 -1
  265. package/dist/ui/backend-signin.js +21 -36
  266. package/dist/ui/chat-md-strings.js +9 -0
  267. package/dist/ui/chat-store.js +2 -13
  268. package/dist/ui/chat-strings.js +2 -50
  269. package/dist/ui/chat-thread.js +53 -3
  270. package/dist/ui/fleet-task-card.js +3 -3
  271. package/dist/ui/fleet-task-common.js +10 -0
  272. package/dist/ui/fleet-tasks.js +7 -5
  273. package/dist/ui/fleet-usage.js +90 -27
  274. package/dist/ui/panel-chat.js +37 -66
  275. package/dist/ui/panel-details.js +1 -2
  276. package/dist/ui/panel-fleet.js +88 -107
  277. package/dist/ui/panel-needs.js +21 -163
  278. package/dist/ui/panel-notices.js +4 -3
  279. package/dist/ui/panel-settings.js +149 -239
  280. package/dist/ui/preview-panel.js +84 -6
  281. package/dist/ui/search-dialog.js +3 -2
  282. package/dist/ui/settings-appearance.js +23 -122
  283. package/dist/ui/settings-catalog.js +10 -37
  284. package/dist/ui/settings-codex-copy.js +19 -0
  285. package/dist/ui/settings-confirm.js +1 -3
  286. package/dist/ui/settings-dialogs.js +20 -27
  287. package/dist/ui/settings-guests.js +14 -27
  288. package/dist/ui/settings-model.js +17 -12
  289. package/dist/ui/settings-quota-notice.js +100 -0
  290. package/dist/ui/settings-registry.js +4 -11
  291. package/dist/ui/settings-strings.js +4 -113
  292. package/dist/ui/settings-web-authority.js +13 -15
  293. package/dist/ui/shared/app-i18n.js +2 -2
  294. package/dist/ui/shared/app-needs.js +1 -1
  295. package/dist/ui/shared/app-route.js +2 -6
  296. package/dist/ui/shared/app-shell.js +18 -23
  297. package/dist/ui/shared/app-store.js +2 -3
  298. package/dist/ui/shared/app-stream.js +1 -3
  299. package/dist/ui/shared/app.css +40 -1312
  300. package/dist/ui/shared/app.js +2 -5
  301. package/dist/ui/shared/header-tools.js +4 -6
  302. package/dist/ui/shared/instance-nav.js +7 -34
  303. package/dist/ui/shared/instance-switch.js +3 -4
  304. package/dist/ui/shared/loading.css +37 -0
  305. package/dist/ui/shared/panel-view.js +77 -4
  306. package/dist/ui/shared/signin.js +6 -39
  307. package/dist/ui/shared/tokens.css +0 -1
  308. package/dist/ui/shared/ui-dialog.js +2 -2
  309. package/dist/ui/shared/ui-menu.js +10 -30
  310. package/dist/ui/shared/ui-states.js +14 -3
  311. package/dist/ui/shared/usage-dialog.js +18 -1
  312. package/dist/ui/shared/vendor/vendor.json +2 -19
  313. package/dist/ui/shared/view-history.css +17 -0
  314. package/dist/ui/shared/view-history.js +438 -0
  315. package/dist/ui/shared/view-strings.js +16 -8
  316. package/dist/ui/signin.html +5 -2
  317. package/dist/unattended-install.d.ts +11 -2
  318. package/dist/unattended-install.js +39 -5
  319. package/dist/unattended-install.js.map +1 -1
  320. package/dist/usage/capacity.d.ts +69 -0
  321. package/dist/usage/capacity.js +119 -0
  322. package/dist/usage/capacity.js.map +1 -0
  323. package/dist/usage/i18n-keys.d.ts +1 -1
  324. package/dist/usage/i18n-keys.js +3 -0
  325. package/dist/usage/i18n-keys.js.map +1 -1
  326. package/dist/usage/profile-timeline.d.ts +16 -0
  327. package/dist/usage/profile-timeline.js +58 -0
  328. package/dist/usage/profile-timeline.js.map +1 -0
  329. package/dist/usage/providers.d.ts +5 -0
  330. package/dist/usage/providers.js +5 -0
  331. package/dist/usage/providers.js.map +1 -1
  332. package/dist/usage/quota-history.d.ts +43 -0
  333. package/dist/usage/quota-history.js +168 -0
  334. package/dist/usage/quota-history.js.map +1 -0
  335. package/dist/usage/quota-notices.d.ts +38 -0
  336. package/dist/usage/quota-notices.js +160 -0
  337. package/dist/usage/quota-notices.js.map +1 -0
  338. package/dist/usage/usage-api.d.ts +32 -0
  339. package/dist/usage/usage-api.js +96 -1
  340. package/dist/usage/usage-api.js.map +1 -1
  341. package/dist/usage-report.d.ts +25 -1
  342. package/dist/usage-report.js +55 -4
  343. package/dist/usage-report.js.map +1 -1
  344. package/dist/usage-rollup.d.ts +6 -1
  345. package/dist/usage-rollup.js +16 -4
  346. package/dist/usage-rollup.js.map +1 -1
  347. package/dist/view-api.d.ts +16 -0
  348. package/dist/view-api.js +100 -16
  349. package/dist/view-api.js.map +1 -1
  350. package/dist/view-history.d.ts +84 -0
  351. package/dist/view-history.js +116 -0
  352. package/dist/view-history.js.map +1 -0
  353. package/dist/web-api.d.ts +6 -38
  354. package/dist/web-api.js +20 -255
  355. package/dist/web-api.js.map +1 -1
  356. package/dist/web-chat-history.d.ts +2 -19
  357. package/dist/web-chat-history.js +3 -72
  358. package/dist/web-chat-history.js.map +1 -1
  359. package/dist/web-only-fleet.d.ts +0 -20
  360. package/dist/web-only-fleet.js +0 -21
  361. package/dist/web-only-fleet.js.map +1 -1
  362. package/dist/web-session-authorization.d.ts +13 -2
  363. package/dist/web-session-authorization.js +10 -3
  364. package/dist/web-session-authorization.js.map +1 -1
  365. package/dist/web-session.d.ts +0 -7
  366. package/dist/web-session.js +0 -2
  367. package/dist/web-session.js.map +1 -1
  368. package/dist/web-shell-routes.d.ts +1 -4
  369. package/dist/web-shell-routes.js +2 -7
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/launcher/private-home.cjs +1 -1
  372. package/package.json +3 -9
  373. package/dist/agent-runtime-api.d.ts +0 -4
  374. package/dist/agent-runtime-api.js +0 -59
  375. package/dist/agent-runtime-api.js.map +0 -1
  376. package/dist/agent-runtime-change.d.ts +0 -74
  377. package/dist/agent-runtime-change.js +0 -102
  378. package/dist/agent-runtime-change.js.map +0 -1
  379. package/dist/backend/grok-model-cache.d.ts +0 -7
  380. package/dist/backend/grok-model-cache.js +0 -65
  381. package/dist/backend/grok-model-cache.js.map +0 -1
  382. package/dist/backend/native-pane.d.ts +0 -9
  383. package/dist/backend/native-pane.js +0 -59
  384. package/dist/backend/native-pane.js.map +0 -1
  385. package/dist/fleet-windows-runtime.d.ts +0 -97
  386. package/dist/fleet-windows-runtime.js +0 -449
  387. package/dist/fleet-windows-runtime.js.map +0 -1
  388. package/dist/general-binding.d.ts +0 -85
  389. package/dist/general-binding.js +0 -298
  390. package/dist/general-binding.js.map +0 -1
  391. package/dist/general-host-owner.d.ts +0 -8
  392. package/dist/general-host-owner.js +0 -26
  393. package/dist/general-host-owner.js.map +0 -1
  394. package/dist/general-role.d.ts +0 -5
  395. package/dist/general-role.js +0 -26
  396. package/dist/general-role.js.map +0 -1
  397. package/dist/local-pane-endpoint.d.ts +0 -46
  398. package/dist/local-pane-endpoint.js +0 -189
  399. package/dist/local-pane-endpoint.js.map +0 -1
  400. package/dist/local-pane-launcher.d.ts +0 -2
  401. package/dist/local-pane-launcher.js +0 -38
  402. package/dist/local-pane-launcher.js.map +0 -1
  403. package/dist/local-pane-stage.ps1 +0 -101
  404. package/dist/model-catalog-defaults.d.ts +0 -14
  405. package/dist/model-catalog-defaults.js +0 -20
  406. package/dist/model-catalog-defaults.js.map +0 -1
  407. package/dist/native-agent-admission.d.ts +0 -7
  408. package/dist/native-agent-admission.js +0 -26
  409. package/dist/native-agent-admission.js.map +0 -1
  410. package/dist/native-daemon-client.d.ts +0 -13
  411. package/dist/native-daemon-client.js +0 -28
  412. package/dist/native-daemon-client.js.map +0 -1
  413. package/dist/native-instance-api.d.ts +0 -14
  414. package/dist/native-instance-api.js +0 -205
  415. package/dist/native-instance-api.js.map +0 -1
  416. package/dist/native-instance-input.d.ts +0 -84
  417. package/dist/native-instance-input.js +0 -54
  418. package/dist/native-instance-input.js.map +0 -1
  419. package/dist/native-instance-registry.d.ts +0 -32
  420. package/dist/native-instance-registry.js +0 -105
  421. package/dist/native-instance-registry.js.map +0 -1
  422. package/dist/native-mcp.d.ts +0 -40
  423. package/dist/native-mcp.js +0 -278
  424. package/dist/native-mcp.js.map +0 -1
  425. package/dist/native-model-catalog.d.ts +0 -5
  426. package/dist/native-model-catalog.js +0 -76
  427. package/dist/native-model-catalog.js.map +0 -1
  428. package/dist/native-pane-crash-budget.d.ts +0 -20
  429. package/dist/native-pane-crash-budget.js +0 -60
  430. package/dist/native-pane-crash-budget.js.map +0 -1
  431. package/dist/native-pane-create.d.ts +0 -26
  432. package/dist/native-pane-create.js +0 -53
  433. package/dist/native-pane-create.js.map +0 -1
  434. package/dist/native-pane-lifecycle.d.ts +0 -65
  435. package/dist/native-pane-lifecycle.js +0 -230
  436. package/dist/native-pane-lifecycle.js.map +0 -1
  437. package/dist/native-pane-manager.d.ts +0 -59
  438. package/dist/native-pane-manager.js +0 -584
  439. package/dist/native-pane-manager.js.map +0 -1
  440. package/dist/native-pane-output-log.d.ts +0 -15
  441. package/dist/native-pane-output-log.js +0 -108
  442. package/dist/native-pane-output-log.js.map +0 -1
  443. package/dist/native-pane-reconciliation.d.ts +0 -4
  444. package/dist/native-pane-reconciliation.js +0 -40
  445. package/dist/native-pane-reconciliation.js.map +0 -1
  446. package/dist/native-pane-recovery.d.ts +0 -12
  447. package/dist/native-pane-recovery.js +0 -10
  448. package/dist/native-pane-recovery.js.map +0 -1
  449. package/dist/native-pane-tools.d.ts +0 -2
  450. package/dist/native-pane-tools.js +0 -14
  451. package/dist/native-pane-tools.js.map +0 -1
  452. package/dist/pane-port.d.ts +0 -118
  453. package/dist/pane-port.js +0 -2
  454. package/dist/pane-port.js.map +0 -1
  455. package/dist/runtime-api.d.ts +0 -8
  456. package/dist/runtime-api.js +0 -171
  457. package/dist/runtime-api.js.map +0 -1
  458. package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +0 -21
  459. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +0 -2
  460. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +0 -19
  461. package/dist/runtime-assets/@xterm/addon-serialize/package.json +0 -29
  462. package/dist/runtime-assets/@xterm/headless/LICENSE +0 -21
  463. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +0 -2
  464. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +0 -46
  465. package/dist/runtime-assets/@xterm/headless/package.json +0 -29
  466. package/dist/runtime-assets/node-pty/CONPTY-LICENSE +0 -21
  467. package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +0 -503
  468. package/dist/runtime-assets/node-pty/LICENSE +0 -69
  469. package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +0 -16
  470. package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +0 -47
  471. package/dist/runtime-assets/node-pty/lib/index.js +0 -52
  472. package/dist/runtime-assets/node-pty/lib/interfaces.js +0 -7
  473. package/dist/runtime-assets/node-pty/lib/shared/conout.js +0 -11
  474. package/dist/runtime-assets/node-pty/lib/terminal.js +0 -190
  475. package/dist/runtime-assets/node-pty/lib/types.js +0 -7
  476. package/dist/runtime-assets/node-pty/lib/unixTerminal.js +0 -346
  477. package/dist/runtime-assets/node-pty/lib/utils.js +0 -39
  478. package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +0 -125
  479. package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +0 -320
  480. package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +0 -199
  481. package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +0 -22
  482. package/dist/runtime-assets/node-pty/package.json +0 -64
  483. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  484. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  485. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  486. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  487. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  488. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  489. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  490. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  491. package/dist/runtime-build-info.d.ts +0 -9
  492. package/dist/runtime-build-info.js +0 -57
  493. package/dist/runtime-build-info.js.map +0 -1
  494. package/dist/runtime-bundle.d.ts +0 -19
  495. package/dist/runtime-bundle.js +0 -156
  496. package/dist/runtime-bundle.js.map +0 -1
  497. package/dist/runtime-cli.d.ts +0 -7
  498. package/dist/runtime-cli.js +0 -152
  499. package/dist/runtime-cli.js.map +0 -1
  500. package/dist/runtime-client-store.d.ts +0 -68
  501. package/dist/runtime-client-store.js +0 -344
  502. package/dist/runtime-client-store.js.map +0 -1
  503. package/dist/runtime-client.d.ts +0 -112
  504. package/dist/runtime-client.js +0 -736
  505. package/dist/runtime-client.js.map +0 -1
  506. package/dist/runtime-connection.d.ts +0 -22
  507. package/dist/runtime-connection.js +0 -93
  508. package/dist/runtime-connection.js.map +0 -1
  509. package/dist/runtime-coordinator.d.ts +0 -91
  510. package/dist/runtime-coordinator.js +0 -911
  511. package/dist/runtime-coordinator.js.map +0 -1
  512. package/dist/runtime-general-protocol.d.ts +0 -22
  513. package/dist/runtime-general-protocol.js +0 -57
  514. package/dist/runtime-general-protocol.js.map +0 -1
  515. package/dist/runtime-general-workspace.d.ts +0 -17
  516. package/dist/runtime-general-workspace.js +0 -213
  517. package/dist/runtime-general-workspace.js.map +0 -1
  518. package/dist/runtime-pane-assets.d.ts +0 -17
  519. package/dist/runtime-pane-assets.js +0 -23
  520. package/dist/runtime-pane-assets.js.map +0 -1
  521. package/dist/runtime-pane-boot.d.ts +0 -9
  522. package/dist/runtime-pane-boot.js +0 -47
  523. package/dist/runtime-pane-boot.js.map +0 -1
  524. package/dist/runtime-pane-codex-index.d.ts +0 -1
  525. package/dist/runtime-pane-codex-index.js +0 -47
  526. package/dist/runtime-pane-codex-index.js.map +0 -1
  527. package/dist/runtime-pane-config.d.ts +0 -16
  528. package/dist/runtime-pane-config.js +0 -54
  529. package/dist/runtime-pane-config.js.map +0 -1
  530. package/dist/runtime-pane-host.d.ts +0 -44
  531. package/dist/runtime-pane-host.js +0 -424
  532. package/dist/runtime-pane-host.js.map +0 -1
  533. package/dist/runtime-pane-job-owner.d.ts +0 -1
  534. package/dist/runtime-pane-job-owner.js +0 -29
  535. package/dist/runtime-pane-job-owner.js.map +0 -1
  536. package/dist/runtime-pane-job.ps1 +0 -116
  537. package/dist/runtime-pane-link.d.ts +0 -40
  538. package/dist/runtime-pane-link.js +0 -246
  539. package/dist/runtime-pane-link.js.map +0 -1
  540. package/dist/runtime-pane-mcp-protocol.d.ts +0 -32
  541. package/dist/runtime-pane-mcp-protocol.js +0 -39
  542. package/dist/runtime-pane-mcp-protocol.js.map +0 -1
  543. package/dist/runtime-pane-mcp-relay.d.ts +0 -17
  544. package/dist/runtime-pane-mcp-relay.js +0 -70
  545. package/dist/runtime-pane-mcp-relay.js.map +0 -1
  546. package/dist/runtime-pane-mcp.d.ts +0 -51
  547. package/dist/runtime-pane-mcp.js +0 -150
  548. package/dist/runtime-pane-mcp.js.map +0 -1
  549. package/dist/runtime-pane-native.d.ts +0 -45
  550. package/dist/runtime-pane-native.js +0 -65
  551. package/dist/runtime-pane-native.js.map +0 -1
  552. package/dist/runtime-pane-output.d.ts +0 -19
  553. package/dist/runtime-pane-output.js +0 -53
  554. package/dist/runtime-pane-output.js.map +0 -1
  555. package/dist/runtime-pane-port.d.ts +0 -139
  556. package/dist/runtime-pane-port.js +0 -558
  557. package/dist/runtime-pane-port.js.map +0 -1
  558. package/dist/runtime-pane-private.d.ts +0 -14
  559. package/dist/runtime-pane-private.js +0 -51
  560. package/dist/runtime-pane-private.js.map +0 -1
  561. package/dist/runtime-pane-private.ps1 +0 -14
  562. package/dist/runtime-pane-process.d.ts +0 -4
  563. package/dist/runtime-pane-process.js +0 -108
  564. package/dist/runtime-pane-process.js.map +0 -1
  565. package/dist/runtime-pane-protocol.d.ts +0 -172
  566. package/dist/runtime-pane-protocol.js +0 -195
  567. package/dist/runtime-pane-protocol.js.map +0 -1
  568. package/dist/runtime-pane-screen.d.ts +0 -19
  569. package/dist/runtime-pane-screen.js +0 -76
  570. package/dist/runtime-pane-screen.js.map +0 -1
  571. package/dist/runtime-pane-service.d.ts +0 -11
  572. package/dist/runtime-pane-service.js +0 -221
  573. package/dist/runtime-pane-service.js.map +0 -1
  574. package/dist/runtime-pane-stdio-protocol.d.ts +0 -54
  575. package/dist/runtime-pane-stdio-protocol.js +0 -94
  576. package/dist/runtime-pane-stdio-protocol.js.map +0 -1
  577. package/dist/runtime-pane-stdio.d.ts +0 -3
  578. package/dist/runtime-pane-stdio.js +0 -123
  579. package/dist/runtime-pane-stdio.js.map +0 -1
  580. package/dist/runtime-pane-store.d.ts +0 -33
  581. package/dist/runtime-pane-store.js +0 -164
  582. package/dist/runtime-pane-store.js.map +0 -1
  583. package/dist/runtime-pane-transcript-protocol.d.ts +0 -43
  584. package/dist/runtime-pane-transcript-protocol.js +0 -58
  585. package/dist/runtime-pane-transcript-protocol.js.map +0 -1
  586. package/dist/runtime-pane-transcript.d.ts +0 -45
  587. package/dist/runtime-pane-transcript.js +0 -605
  588. package/dist/runtime-pane-transcript.js.map +0 -1
  589. package/dist/runtime-pane-worker.d.ts +0 -1
  590. package/dist/runtime-pane-worker.js +0 -154
  591. package/dist/runtime-pane-worker.js.map +0 -1
  592. package/dist/runtime-protocol.d.ts +0 -162
  593. package/dist/runtime-protocol.js +0 -37
  594. package/dist/runtime-protocol.js.map +0 -1
  595. package/dist/runtime-switches.d.ts +0 -79
  596. package/dist/runtime-switches.js +0 -90
  597. package/dist/runtime-switches.js.map +0 -1
  598. package/dist/runtime-transcript-delta-reader.d.ts +0 -23
  599. package/dist/runtime-transcript-delta-reader.js +0 -74
  600. package/dist/runtime-transcript-delta-reader.js.map +0 -1
  601. package/dist/runtime-transcript-source.d.ts +0 -36
  602. package/dist/runtime-transcript-source.js +0 -189
  603. package/dist/runtime-transcript-source.js.map +0 -1
  604. package/dist/runtime-windows-bridge.d.ts +0 -41
  605. package/dist/runtime-windows-bridge.js +0 -157
  606. package/dist/runtime-windows-bridge.js.map +0 -1
  607. package/dist/settings-route-lifetime.d.ts +0 -14
  608. package/dist/settings-route-lifetime.js +0 -48
  609. package/dist/settings-route-lifetime.js.map +0 -1
  610. package/dist/tmux-pane-host.d.ts +0 -26
  611. package/dist/tmux-pane-host.js +0 -25
  612. package/dist/tmux-pane-host.js.map +0 -1
  613. package/dist/transcript-needles.d.ts +0 -35
  614. package/dist/transcript-needles.js +0 -158
  615. package/dist/transcript-needles.js.map +0 -1
  616. package/dist/transcript-records.d.ts +0 -21
  617. package/dist/transcript-records.js +0 -140
  618. package/dist/transcript-records.js.map +0 -1
  619. package/dist/ui/agent-proposals.js +0 -90
  620. package/dist/ui/agent-runtime-settings.js +0 -67
  621. package/dist/ui/agent-runtime.js +0 -124
  622. package/dist/ui/cli-picker.js +0 -42
  623. package/dist/ui/general-location.js +0 -71
  624. package/dist/ui/native-instance-settings.js +0 -44
  625. package/dist/ui/native-pane-recovery.js +0 -45
  626. package/dist/ui/panel-runtimes.js +0 -334
  627. package/dist/ui/panel-windows-agent.js +0 -86
  628. package/dist/ui/panel-windows-cli.js +0 -179
  629. package/dist/ui/provider-marks.js +0 -83
  630. package/dist/ui/runtime-catalog.js +0 -55
  631. package/dist/ui/settings-agent-filters.js +0 -94
  632. package/dist/ui/settings-layout.js +0 -245
  633. package/dist/ui/settings-outside.js +0 -176
  634. package/dist/ui/settings-shortcuts.js +0 -65
  635. package/dist/ui/shared/machine-name.js +0 -13
  636. package/dist/ui/shared/qrcode.module.js +0 -2237
  637. package/dist/ui/shared/settings-width.js +0 -31
  638. package/dist/ui/shared/vendor/qrcode-generator.LICENSE +0 -27
  639. package/dist/ui/windows-agents-nav.js +0 -2
  640. package/dist/ui/windows-cli-catalog.js +0 -108
  641. package/dist/web-remote-access.d.ts +0 -152
  642. package/dist/web-remote-access.js +0 -368
  643. package/dist/web-remote-access.js.map +0 -1
  644. package/dist/windows-agents.d.ts +0 -126
  645. package/dist/windows-agents.js +0 -490
  646. package/dist/windows-agents.js.map +0 -1
  647. package/dist/windows-cli-api.d.ts +0 -23
  648. package/dist/windows-cli-api.js +0 -159
  649. package/dist/windows-cli-api.js.map +0 -1
  650. package/dist/windows-cli-providers.d.ts +0 -115
  651. package/dist/windows-cli-providers.js +0 -429
  652. package/dist/windows-cli-providers.js.map +0 -1
  653. package/dist/windows-cli-worker.d.ts +0 -59
  654. package/dist/windows-cli-worker.js +0 -479
  655. package/dist/windows-cli-worker.js.map +0 -1
  656. package/dist/windows-cli.d.ts +0 -116
  657. package/dist/windows-cli.js +0 -407
  658. package/dist/windows-cli.js.map +0 -1
  659. package/dist/windows-native-mcp.d.ts +0 -19
  660. package/dist/windows-native-mcp.js +0 -143
  661. package/dist/windows-native-mcp.js.map +0 -1
  662. package/dist/windows-pane-mcp.d.ts +0 -26
  663. package/dist/windows-pane-mcp.js +0 -187
  664. package/dist/windows-pane-mcp.js.map +0 -1
  665. package/templates/runtime-assets/conpty-LICENSE +0 -21
  666. package/templates/runtime-assets/conpty-NOTICE.md +0 -503
  667. package/templates/runtime-assets/local-pane-stage.ps1 +0 -101
  668. package/templates/runtime-assets/runtime-pane-job.ps1 +0 -116
  669. package/templates/runtime-assets/runtime-pane-private.ps1 +0 -14
  670. package/templates/runtime-assets/xterm-LICENSE +0 -21
@@ -1,30 +1,22 @@
1
- import { buildNativeFleetInstructions } from "./instructions.js";
2
- import { GENERAL_ROLE_INSTRUCTIONS } from "./general-role.js";
3
- import { NativePaneRecoveryError } from "./native-pane-recovery.js";
4
- import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
5
- import { windowsAgentsFor } from "./windows-agents.js";
6
- import { AgentRuntimeChanges } from "./agent-runtime-change.js";
1
+ import { QuotaNotices } from "./usage/quota-notices.js";
2
+ import { quotaNoticeRule } from "./quota-notice-config.js";
3
+ import { onUsageSnapshot } from "./usage/usage-api.js";
7
4
  import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
8
5
  import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
9
6
  import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
10
7
  import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
11
8
  import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
9
+ import { maskSecrets } from "./log-steps.js";
12
10
  import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
13
11
  import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
14
12
  import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
15
13
  import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
16
14
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
17
15
  import { SettingsBaselines } from "./settings-baseline.js";
18
- import { prepareSettingsEffect } from "./settings-effect.js";
19
- import { settingsDisplay } from "./settings-change.js";
20
- import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
16
+ import { SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
21
17
  import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
22
18
  import { SettingsControlServer } from "./settings-control.js";
23
- import { SettingsRouteLifetime } from "./settings-route-lifetime.js";
24
- import { GeneralManagedFileError } from "./runtime-general-protocol.js";
25
- import { GeneralBindings, generalBindingRevision, parseGeneralSwitchInput, switchGeneralLocation } from "./general-binding.js";
26
- import { confirmGeneralHostExit } from "./general-host-owner.js";
27
- import { settingsFingerprint, SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
19
+ import { SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
28
20
  import { performance } from "node:perf_hooks";
29
21
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
30
22
  import { gatewayRequestContext } from "./web-request-context.js";
@@ -39,7 +31,7 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
39
31
  import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
40
32
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
41
33
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
42
- import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
34
+ import { installExitMarker, unattendedInstallCommand, loginShell } from "./unattended-install.js";
43
35
  import { InstallStream } from "./install-stream.js";
44
36
  import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
45
37
  import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
@@ -54,7 +46,7 @@ import { ProfileControlServer } from "./profile-control.js";
54
46
  import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
55
47
  import { atomicWriteFileSync } from "./atomic-write.js";
56
48
  import { createHash, randomBytes, randomUUID } from "node:crypto";
57
- import { freemem, totalmem, homedir, hostname } from "node:os";
49
+ import { freemem, totalmem, homedir } from "node:os";
58
50
  import { effectiveCpuCount } from "./cpu-quota.js";
59
51
  import { createServer } from "node:http";
60
52
  import { Worker } from "node:worker_threads";
@@ -87,6 +79,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
87
79
  import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
88
80
  import { EventLog } from "./event-log.js";
89
81
  import { binaryProbe } from "./binary-probe.js";
82
+ import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
90
83
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
91
84
  import { classifySqliteOpenError } from "./sqlite-open-errors.js";
92
85
  import { OutboxOpenError } from "./outbox-open-error.js";
@@ -96,18 +89,17 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
96
89
  import { TmuxManager } from "./tmux-manager.js";
97
90
  import { AccessManager } from "./channel/access-manager.js";
98
91
  import { IpcClient } from "./channel/ipc-bridge.js";
99
- import { NativeDaemonClient } from "./native-daemon-client.js";
100
- import { usesChannelAddress, TOOLS } from "./channel/mcp-tools.js";
92
+ import { usesChannelAddress } from "./channel/mcp-tools.js";
101
93
  import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
102
94
  import { formatFleetTime } from "./tz-utils.js";
103
95
  import { createAdapter } from "./channel/factory.js";
104
96
  import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
105
97
  import { TelegramAdapter } from "./channel/adapters/telegram.js";
98
+ import { MissedOfflineStore } from "./missed-offline.js";
106
99
  import { createBackend } from "./backend/factory.js";
107
100
  import { readEffortMetadata } from "./backend/effort-metadata.js";
108
101
  import { codexEffort, flagEffort } from "./backend/launch-effort.js";
109
102
  import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
110
- import { readGrokModelCache } from "./backend/grok-model-cache.js";
111
103
  import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
112
104
  import { createLogger, rotateLogIfNeeded } from "./logger.js";
113
105
  import { processAttachments } from "./channel/attachment-handler.js";
@@ -154,6 +146,20 @@ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked", "review"];
154
146
  * " \t " and [" ", "\t"] both normalize to undefined (not treated as explicit
155
147
  * filters). The same result drives the live-only default and the cap decision.
156
148
  */
149
+ /**
150
+ * #3195: the saved value of each plain-update field the call named (assignee, priority, result), read back from the
151
+ * task — so the answer shows what was persisted, not what was asked. Status is always in the ack already.
152
+ */
153
+ export function savedFields(t, named) {
154
+ const out = {};
155
+ if (named.assignee !== undefined)
156
+ out.assignee = t.assignee;
157
+ if (named.priority !== undefined)
158
+ out.priority = t.priority;
159
+ if (named.result !== undefined)
160
+ out.result = t.result;
161
+ return out;
162
+ }
157
163
  export function normalizeStatusFilter(v) {
158
164
  if (typeof v === "string") {
159
165
  const s = v.trim();
@@ -182,17 +188,8 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
182
188
  return tasks;
183
189
  }
184
190
  import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
185
- import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
186
- import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
187
- import { RuntimeCoordinator } from "./runtime-coordinator.js";
188
- import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
189
- import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
190
- import { NativeMcpSessions } from "./native-mcp.js";
191
- import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
192
- import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED } from "./native-instance-registry.js";
193
- import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
194
- import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
195
191
  import { LogService } from "./log-service.js";
192
+ import { viewHistory } from "./view-history.js";
196
193
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
197
194
  import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
198
195
  import { WebChatDiskStore } from "./web-chat-store.js";
@@ -225,7 +222,7 @@ import { envFileKeys } from "./token-env-name.js";
225
222
  import { discordGeneralChannelId } from "./discord-general.js";
226
223
  import { parseEnvText } from "./env-file.js";
227
224
  import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
228
- import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
225
+ import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
229
226
  import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
230
227
  import { LoginSession } from "./login-manager.js";
231
228
  import { runOnLoginShell } from "./login-shell.js";
@@ -256,6 +253,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
256
253
  import { recentChatContext } from "./log-tail.js";
257
254
  import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
258
255
  import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
256
+ import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
257
+ import { writeSecretFile } from "./secret-file.js";
259
258
  import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
260
259
  import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
261
260
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
@@ -286,14 +285,13 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
286
285
  import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
287
286
  import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
288
287
  import { sharedRolloutIndex } from "./rollout-index.js";
289
- import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
288
+ import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardOffIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
290
289
  import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
291
290
  import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
292
291
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
293
292
  import { isWebPageNavigation } from "./web-shell-routes.js";
294
293
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
295
294
  import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
296
- import { isOutsideLinkOwner, OUTSIDE_LINK_OWNER, OUTSIDE_LINK_RETRY_MS, OUTSIDE_OWN_CLOSES, outsideDuration, outsideTimer, TOTP_STEP_SECONDS, WEB_REMOTE_ACCESS_FILE, WebRemoteAccess } from "./web-remote-access.js";
297
295
  import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
298
296
  import { AuthorityStateStore } from "./authority/authority-state.js";
299
297
  import { SessionAuthorizations } from "./authority/session-authorizations.js";
@@ -315,7 +313,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
315
313
  const SELF_RESTART_DEADLINE_MS = 300_000;
316
314
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
317
315
  import { kiroHome } from "./backend/kiro-kas-store.js";
318
- import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
316
+ import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
319
317
  import { carryClaudeConversation } from "./backend/claude-carry.js";
320
318
  import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
321
319
  import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
@@ -331,6 +329,12 @@ import { BackendOutageTracker } from "./backend-outage.js";
331
329
  import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
332
330
  import { getTmuxSession } from "./config.js";
333
331
  import { OwnStatusMessages, discordMessageKey } from "./chat-log-ids.js";
332
+ import { PROFILE_TIMELINE_FILE, PROFILE_UNRECORDED, recordProfileLaunch } from "./usage/profile-timeline.js";
333
+ import { readQuotaHistory } from "./usage/quota-history.js";
334
+ import { estimateCapacity } from "./usage/capacity.js";
335
+ import { covered, hourSources } from "./usage-rollup.js";
336
+ import { KEEP_DAYS } from "./cache-ledger.js";
337
+ import { ratesFor } from "./cache-prices.js";
334
338
  // Closed migration cohort: these 11 names existed immediately before the first
335
339
  // managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
336
340
  // adding a bundled skill: its new name is not proof of old AgEnD ownership.
@@ -482,6 +486,8 @@ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
482
486
  * while anything real shows signs of life well before the old two minutes.
483
487
  */
484
488
  const PROGRESS_MIN_ELAPSED_MS = 30_000;
489
+ /** M-2391: the threshold tick fires this long after it, never a millisecond before (timers can fire early). */
490
+ const PROGRESS_THRESHOLD_MARGIN_MS = 50;
485
491
  /** How much of a tool summary the progress line will show before eliding. */
486
492
  const PROGRESS_ACTIVITY_MAX_CHARS = 48;
487
493
  /**
@@ -762,974 +768,6 @@ export class FleetManager {
762
768
  /** Live view of lifecycle.daemons — used throughout; not deprecated. */
763
769
  get daemons() { return this.lifecycle.daemons; }
764
770
  fleetConfig = null;
765
- fleetWindowsRuntime = null;
766
- nativeMcpStore = null;
767
- paneManager = null;
768
- localPaneRelay = null;
769
- openingLocalPane = null;
770
- runtimeHub = null;
771
- runtimeExecutor = null;
772
- runtimeStatusScheduled = false;
773
- reconciledRemoteResults = new Set();
774
- /** Machine credentials are separate from dashboard sessions and per-turn agent capabilities. */
775
- runtimeCoordinator() {
776
- if (!remoteRuntimeEnabled())
777
- throw new Error("Remote runtimes are not enabled");
778
- return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
779
- handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
780
- handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
781
- warn: (fields, message) => this.logger.warn(fields, message),
782
- changed: event => {
783
- if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
784
- for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
785
- if (config.machine_id === event.machineId) {
786
- this.nativeMcpStore?.closeInstance(name);
787
- if (config.general_topic) {
788
- this.cancelPendingDeliveries(name);
789
- this.holdGeneralOldDeliveries(name);
790
- }
791
- }
792
- this.needsYou?.poke();
793
- }
794
- if (this.runtimeStatusScheduled || this.shuttingDown)
795
- return;
796
- this.runtimeStatusScheduled = true;
797
- queueMicrotask(() => {
798
- this.runtimeStatusScheduled = false;
799
- if (this.shuttingDown)
800
- return;
801
- try {
802
- this.fleetWindowsRuntime?.importHistory();
803
- for (const snapshot of this.runtimeHub?.tasks() ?? [])
804
- this.reconcileRemoteDelivery(snapshot.machineId, snapshot.task);
805
- this.emitSseEvent("status", this.getUiStatus());
806
- this.scheduleDeliveryOutboxPump();
807
- }
808
- catch (error) {
809
- this.logger.warn({ err: error }, "Runtime status reconciliation failed");
810
- }
811
- });
812
- },
813
- });
814
- }
815
- runtimeWindowsExecutor() {
816
- return this.runtimeExecutor ??= new RuntimeWindowsBridge(this, () => remoteRuntimeEnabled() && !this.shuttingDown ? this.runtimeCoordinator() : null);
817
- }
818
- /** Per-turn capabilities for native MCP, separate from browser and Linux-agent credentials. */
819
- nativeMcpSessions() {
820
- return this.nativeMcpStore ??= new NativeMcpSessions(this);
821
- }
822
- /** The same instance registry and delivery entry point, with a native Windows execution adapter. */
823
- nativeInstanceRuntime() {
824
- return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
825
- owner: this, fleet: () => this.fleetConfig, save: () => this.saveFleetConfig(), stopping: () => this.shuttingDown, history: this.webChatHistory,
826
- openMcp: (instance, requestId, sandbox, stillCurrent) => this.healthServerListening
827
- ? this.nativeMcpSessions().grant({ instance, requestId, sandbox, stillCurrent, endpoint: `http://127.0.0.1:${this.fleetConfig?.health_port ?? 19280}/agent` }) : null,
828
- warn: (message, error) => this.logger.warn({ err: error }, message),
829
- removed: name => this.nativeInstanceRemoved(name),
830
- paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
831
- machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
832
- machineGeneration: id => this.runtimeCoordinator().machine(id)?.bootId ?? null,
833
- paneAvailable: id => this.runtimeCoordinator().machine(id)?.paneProtocol === 1,
834
- validateMachine: (id, cli, sandbox) => {
835
- const machine = this.runtimeCoordinator().machine(id);
836
- const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
837
- if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
838
- throw new Error("Choose an online runtime and an available CLI");
839
- if (sandbox === "read-only" && !provider.readOnlySupport)
840
- throw new Error("This runtime CLI cannot enforce read-only execution");
841
- },
842
- emit: (event, data) => {
843
- this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
844
- if (event === "activity")
845
- this.scheduleDeliveryOutboxPump();
846
- },
847
- }, { bridge: this.runtimeWindowsExecutor() });
848
- }
849
- agentRuntimeChangeService;
850
- agentRuntimeChanges() {
851
- if (this.agentRuntimeChangeService)
852
- return this.agentRuntimeChangeService;
853
- const windowPins = new Map();
854
- return this.agentRuntimeChangeService ??= new AgentRuntimeChanges({
855
- config: name => Object.hasOwn(this.fleetConfig?.instances ?? {}, name) ? this.fleetConfig.instances[name] : undefined,
856
- defaults: () => this.fleetConfig?.defaults,
857
- generation: () => [this.managerBootId, this.settingsGeneration],
858
- epoch: name => this.lifecycle.epochOf(name),
859
- backendState: (name, backend) => !isWindowsInstance(this.fleetConfig?.instances[name]) ? this.backendAvailabilityFor(name).find(row => row.backend === backend)?.state : undefined,
860
- transition: (name, run) => this.lifecycle.runTransition(name, async (transition) => {
861
- const config = this.fleetConfig?.instances[name];
862
- if (!isWindowsInstance(config)) {
863
- const path = join(this.getInstanceDir(name), "window-id");
864
- let pin;
865
- if (existsSync(path)) {
866
- if (statSync(path).size > 128)
867
- throw Error("Invalid saved CLI window identity");
868
- pin = readFileSync(path, "utf8").trim();
869
- if (!/^@\d+$/.test(pin))
870
- throw Error("Invalid saved CLI window identity");
871
- }
872
- else if (this.daemons.has(name))
873
- throw Error("The current CLI window identity is unavailable");
874
- windowPins.set(name, pin);
875
- }
876
- try {
877
- return await run(async () => {
878
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
879
- this.lifecycle.invalidate(name);
880
- this.nativeInstanceRuntime().setPaused(name, true);
881
- }
882
- else
883
- await this.stopInstance(name, transition);
884
- });
885
- }
886
- finally {
887
- windowPins.delete(name);
888
- }
889
- }),
890
- idle: name => {
891
- if (this.shuttingDown || this.lifecycle.hasWorkLease(name) || this.deliveryOutbox?.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending", "uncertain"]))
892
- return false;
893
- const config = this.fleetConfig?.instances[name];
894
- if (!config)
895
- return false;
896
- if (isWindowsInstance(config)) {
897
- try {
898
- const agent = this.nativeInstanceRuntime().get(name);
899
- if (!agent || agent.blocked || agent.activeId && !isNativePaneInstance(config))
900
- return false;
901
- }
902
- catch {
903
- return false;
904
- }
905
- }
906
- if (!this.daemons.has(name) && !isWindowsInstance(config) && existsSync(join(this.getInstanceDir(name), "window-id")))
907
- return false;
908
- return !this.daemons.has(name) || this.getInstanceExecutionState(name) === "idle";
909
- },
910
- prepare: async (name, before, target, current) => {
911
- if (!current())
912
- throw Error("Runtime change authority expired");
913
- if (isWindowsInstance(before))
914
- this.nativeInstanceRuntime().importHistory(name);
915
- if (target.runtime === "windows")
916
- return this.nativeInstanceRuntime().prepareRuntimeChange(name, before, target, current);
917
- const sameAccount = !isWindowsInstance(before) && target.backend === (before.backend || this.fleetConfig?.defaults?.backend || "claude-code");
918
- const availability = (sameAccount ? this.backendAvailabilityFor(name) : this.backendAvailabilityRows()).find(row => row.backend === target.backend);
919
- if (availability && ["not_installed", "not_signed_in", "too_old"].includes(availability.state))
920
- throw Error("Choose an available CLI on the Fleet host");
921
- const supplied = target.directory.trim(), cwd = supplied || join(this.dataDir, "workspaces", name);
922
- if (!isAbsolute(cwd) || /^[A-Za-z]:[\\/]/.test(cwd) || /[\x00-\x1f]/.test(cwd))
923
- throw Error("Enter an absolute Linux working directory or leave it empty");
924
- if (!current())
925
- throw Error("Runtime change authority expired");
926
- if (supplied) {
927
- if (!statSync(cwd).isDirectory())
928
- throw Error("The Linux working directory is not a directory");
929
- }
930
- else
931
- mkdirSync(cwd, { recursive: true, mode: 0o700 });
932
- const config = { ...before, runtime: "wsl", working_directory: cwd, backend: target.backend, model: target.model };
933
- if (before.backend !== target.backend)
934
- config.model_failover = [];
935
- for (const key of ["machine_id", "native_agent_id", "native_adapter", "native_sandbox"])
936
- delete config[key];
937
- if (!sameAccount)
938
- delete config.backend_options;
939
- return { config, discard: () => { } };
940
- },
941
- closed: async (name, before) => {
942
- if (this.daemons.has(name))
943
- return false;
944
- if (isWindowsInstance(before))
945
- return !this.nativeInstanceRuntime().get(name)?.activeId;
946
- // An unreadable tmux state is not evidence that the old process exited.
947
- const session = getTmuxSession();
948
- if (!await TmuxManager.sessionExistsStrict(session))
949
- return true;
950
- const pin = windowPins.get(name);
951
- return !(await TmuxManager.listWindowsStrict(session)).some(window => window.name === name || pin && window.id === pin);
952
- },
953
- fresh: (name, requestId) => {
954
- const dir = this.getInstanceDir(name), moved = [];
955
- const restore = () => { for (const [source, saved] of moved.reverse())
956
- if (!existsSync(source) && existsSync(saved))
957
- renameSync(saved, source); };
958
- try {
959
- for (const file of ["session-id", "session-id.backend"]) {
960
- const source = join(dir, file), saved = join(dir, `${file}.runtime-${requestId}`);
961
- if (existsSync(source)) {
962
- if (existsSync(saved))
963
- throw Error("Session backup already exists");
964
- renameSync(source, saved);
965
- moved.push([source, saved]);
966
- }
967
- }
968
- return restore;
969
- }
970
- catch (error) {
971
- restore();
972
- throw error;
973
- }
974
- },
975
- save: (name, before, next) => {
976
- if (this.fleetConfig?.instances[name] !== before)
977
- throw Error("Agent settings changed before saving Runtime");
978
- // Mark the replacement stopped before committing its config. A marker failure must not leave a saved
979
- // Windows config referring to a profile the preparation rollback would then remove.
980
- writePausedMarker(this.getInstanceDir(name), Date.now(), "operator");
981
- const fleet = this.fleetConfig;
982
- fleet.instances[name] = next;
983
- try {
984
- this.saveFleetConfig();
985
- }
986
- catch (error) {
987
- fleet.instances[name] = before;
988
- throw error;
989
- }
990
- // A move deliberately leaves the replacement stopped; its next explicit Start opens the fresh CLI.
991
- },
992
- changed: name => { this.instanceStateCache.delete(name); this.instanceProcessStatus.delete(name); this.emitSseEvent("status", this.getUiStatus()); },
993
- });
994
- }
995
- /** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
996
- async createNativePaneInstance(profile, channelId, authorized, execution) {
997
- const { createNativePane } = await import("./native-pane-create.js");
998
- return createNativePane({ fleet: () => this.fleetConfig, runtime: this.nativeInstanceRuntime(),
999
- topic: target => {
1000
- const world = this.worlds.get(target.id), adapter = world?.adapter;
1001
- if (!world || !adapter?.createTopic || !adapter.deleteTopic)
1002
- throw Error("The approved connection cannot create and compensate a topic");
1003
- const same = () => {
1004
- const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
1005
- const matches = channels.filter(row => (row.id ?? row.type) === target.id);
1006
- return !this.shuttingDown && this.worlds.get(target.id) === world && world.adapter === adapter && matches.length === 1 && matches[0].type === target.type && matches[0].group_id === target.group_id;
1007
- };
1008
- return { current: same, create: name => adapter.createTopic(name), remove: id => adapter.deleteTopic(id) };
1009
- },
1010
- epoch: name => this.lifecycle.epochOf(name),
1011
- start: async (name, config, current) => {
1012
- if (!current())
1013
- throw Error("Native creation expired before start");
1014
- await this.startInstance(name, config, true);
1015
- if (!current())
1016
- throw Error("Native creation expired during start");
1017
- await this.connectIpcToInstance(name);
1018
- if (!current())
1019
- throw Error("Native creation expired during channel connection");
1020
- },
1021
- close: (name, config, epoch) => this.lifecycle.runTransition(name, async (transition) => {
1022
- if (this.fleetConfig?.instances[name] !== config || this.lifecycle.epochOf(name) !== epoch)
1023
- return false;
1024
- await this.stopInstance(name, transition);
1025
- return this.fleetConfig?.instances[name] === config && this.lifecycle.epochOf(name) === epoch + 1;
1026
- }),
1027
- routes: () => { if (this.fleetConfig)
1028
- this.routing.rebuild(this.fleetConfig); },
1029
- warn: error => this.logger.warn({ err: error }, "Native creation cleanup is held; retained identity and topic require attention"), }, profile, channelId, () => !this.shuttingDown && authorized(), execution);
1030
- }
1031
- /** Admin-only settings path: validate first, then close the exact old launch before changing cold fields. */
1032
- async updateNativePaneConfig(name, patch, authorized, transaction) {
1033
- return this.lifecycle.runTransition(name, async (transition) => {
1034
- const before = this.fleetConfig?.instances[name];
1035
- if (!isNativePaneInstance(before))
1036
- throw Error("Persistent Windows instance not found");
1037
- const runtime = this.nativeInstanceRuntime(), generation = this.explicitStopGeneration.get(name) ?? 0;
1038
- let expectedGeneration = generation;
1039
- const current = () => authorized() && !this.shuttingDown && this.fleetConfig?.instances[name] === before
1040
- && (this.explicitStopGeneration.get(name) ?? 0) === expectedGeneration
1041
- && (before.machine_id ? remoteRuntimeEnabled() : localWindowsCliEnabled());
1042
- if (!current())
1043
- throw Error("Native settings authority expired");
1044
- runtime.previewUpdate(name, patch);
1045
- const cold = ["working_directory", "model", "systemPrompt", "native_sandbox"].some(key => Object.hasOwn(patch, key) && (patch[key] ?? "") !== (before[key] ?? ""));
1046
- const running = this.daemons.get(name);
1047
- if (cold) {
1048
- if (running && !running.nativePaneHasExited() && (!await running.nativePauseReady({ current, accepts: () => true, revalidate: async () => current() }) || !running.nativePauseStillReady()))
1049
- throw Error("Wait for this Windows agent to be idle before changing its launch settings");
1050
- if (!current())
1051
- throw Error("Native settings authority expired before closing its old pane");
1052
- await this.stopInstance(name, transition);
1053
- expectedGeneration = generation + 1;
1054
- if (!current())
1055
- throw Error("Native settings authority expired after closing its old pane");
1056
- if (runtime.get(name)?.activeId)
1057
- throw Error("Native pane exit is unconfirmed; its settings were not changed");
1058
- }
1059
- const result = runtime.update(name, patch, current, transaction), next = this.fleetConfig.instances[name];
1060
- if (cold && running) {
1061
- if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== next || (this.explicitStopGeneration.get(name) ?? 0) !== expectedGeneration)
1062
- throw Error("Native settings saved; restart authority expired");
1063
- await this.startInstance(name, next, true, "fleet-topic", false, transition);
1064
- }
1065
- return result;
1066
- });
1067
- }
1068
- async localNativePaneEndpoint() {
1069
- const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
1070
- throw Error("Local Windows runtime is switched off or draining"); };
1071
- admit();
1072
- if (this.localPaneDrain)
1073
- throw Error("Local Windows runtime cleanup is pending");
1074
- if (this.localPaneRelay?.online())
1075
- return this.localPaneRelay;
1076
- if (this.openingLocalPane)
1077
- return this.openingLocalPane;
1078
- const opening = (async () => {
1079
- const [{ LocalPaneEndpoint }, { launchLocalPaneRelay, localPaneScope }, inventory] = await Promise.all([
1080
- import("./local-pane-endpoint.js"), import("./local-pane-launcher.js"), this.runtimeWindowsExecutor().probe({ force: false }),
1081
- ]);
1082
- admit();
1083
- this.localPaneRelay?.stop();
1084
- const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
1085
- diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
1086
- version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
1087
- launch: () => launchLocalPaneRelay(this.dataDir, admit),
1088
- mcp: async (body, current) => (await this.nativePanes()).handle("local", body, current),
1089
- });
1090
- try {
1091
- admit();
1092
- this.localPaneRelay = relay;
1093
- return relay;
1094
- }
1095
- catch (error) {
1096
- relay.stop();
1097
- throw error;
1098
- }
1099
- })();
1100
- this.openingLocalPane = opening;
1101
- try {
1102
- return await opening;
1103
- }
1104
- finally {
1105
- if (this.openingLocalPane === opening)
1106
- this.openingLocalPane = null;
1107
- }
1108
- }
1109
- /** Persistent ownership is independent of the exec worker's per-turn capabilities. */
1110
- async nativePanes() {
1111
- if (this.paneManager)
1112
- return this.paneManager;
1113
- const { NativePaneManager } = await import("./native-pane-manager.js");
1114
- if (this.paneManager)
1115
- return this.paneManager;
1116
- const store = windowsAgentsFor(this, this.runtimeWindowsExecutor());
1117
- const allowed = (name, tool) => nativePaneToolAvailable(tool) && mayUseTool(resolveToolSet(this.fleetConfig?.instances[name], name), tool)
1118
- && !dashboardRefusalIn(this.fleetConfig, name, tool);
1119
- return this.paneManager = new NativePaneManager({
1120
- home: this.dataDir, store, config: name => this.fleetConfig?.instances[name],
1121
- freshNotice: async (name, provider) => {
1122
- const message = t(provider === "codex" ? "native_pane.codex_fresh_started" : "native_pane.fresh_started");
1123
- this.eventLog?.insert(name, "native_pane_fresh_started", {});
1124
- this.logger.info({ name }, message);
1125
- await this.notifyInstanceTopic(name, message);
1126
- },
1127
- admit: (name, config) => {
1128
- const now = this.fleetConfig?.instances[name];
1129
- if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
1130
- || now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
1131
- || now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
1132
- || now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding)
1133
- throw Error("Native pane configuration or lifetime changed");
1134
- if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
1135
- throw Error("Native runtime is switched off");
1136
- },
1137
- endpoint: async (agent, cleanup = false) => {
1138
- if (!agent.machineId) {
1139
- if (!cleanup)
1140
- return this.localNativePaneEndpoint();
1141
- if (!this.localPaneRelay)
1142
- throw Error("Local cleanup transport is unavailable; pane remains reserved");
1143
- return this.localPaneRelay;
1144
- }
1145
- const hub = cleanup ? this.runtimeHub : this.runtimeCoordinator(), machineId = agent.machineId;
1146
- if (!hub)
1147
- throw Error("Remote cleanup transport is unavailable; pane remains reserved");
1148
- const draining = () => !!this.remotePaneDrain;
1149
- return { machineId, online: () => !this.shuttingDown && (draining() || remoteRuntimeEnabled()) && this.runtimeHub === hub && hub.machine(machineId)?.status === "online",
1150
- call: request => {
1151
- if (this.runtimeHub !== hub || !(draining() || remoteRuntimeEnabled()) || draining() && request.action !== "snapshot" && request.action !== "close")
1152
- return Promise.reject(Error("Remote pane transport only permits owned cleanup"));
1153
- return hub.callPane(machineId, request, maxAgentsPerMachine());
1154
- }, subscribe: listener => hub.subscribe(listener),
1155
- epoch: () => hub.paneTransportEpoch(machineId), version: provider => hub.machine(machineId)?.inventory?.clis.find(row => row.id === provider && row.runnable)?.version ?? null };
1156
- },
1157
- instructions: (name, config) => buildNativeFleetInstructions({ instanceName: name, workingDirectory: config.working_directory,
1158
- displayName: config.display_name, description: config.description, customPrompt: config.systemPrompt,
1159
- workflow: config.workflow, taskBoard: !dashboardRefusalIn(this.fleetConfig, name, "task"),
1160
- runtimeIdentity: { kind: "fleet-topic", backend: config.backend ?? "codex", model: config.model || "default" } }, config.general_topic === true),
1161
- menu: name => TOOLS.filter(tool => allowed(name, tool.name)).map(tool => ({ ...tool })),
1162
- allowed: (name, tool, args) => allowed(name, tool) && !nativePaneToolRefusal(tool, args), published: (name, daemon) => this.daemons.get(name) === daemon,
1163
- invoke: async (daemon, input, current, name) => {
1164
- if (input.tool === "list_models") {
1165
- const { ListModelsArgs } = await import("./outbound-schemas.js");
1166
- const parsed = ListModelsArgs.safeParse(input.args);
1167
- if (!parsed.success || !current())
1168
- return { result: null, error: "Invalid native model request or expired owner" };
1169
- const target = parsed.data.instance_name ?? name;
1170
- if (!isWindowsInstance(this.fleetConfig?.instances[target]))
1171
- return { result: null, error: "Native panes can only read Windows runtime model inventories" };
1172
- return { result: await this.listModelCatalog({ instanceName: target }) };
1173
- }
1174
- const ops = { list_instances: "list", describe_instance: "describe", task: "task", send_to_instance: "send",
1175
- delegate_task: "delegate", request_information: "ask", report_result: "report", delivery_status: "delivery-status" };
1176
- const { nativeAgentAdmissionContext } = await import("./native-agent-admission.js");
1177
- const { dispatchAgentOperation } = await import("./agent-endpoint.js");
1178
- return daemon.handlePaneToolCall(input, { current, allowed: tool => allowed(name, tool),
1179
- dispatch: (tool, args) => ops[tool] ? dispatchAgentOperation(nativeAgentAdmissionContext(this, {
1180
- instance: name, current: () => current() && allowed(name, tool), taskAllowed: () => current() && allowed(name, "task"),
1181
- }), name, ops[tool], args, input.operationId) : undefined });
1182
- },
1183
- });
1184
- }
1185
- /** Prepare only bundled role files; this neither switches General nor starts a provider process. */
1186
- async prepareWindowsGeneralWorkspace(name, machineId, cli, bindingId, authorized) {
1187
- const config = this.fleetConfig?.instances[name], hub = this.runtimeCoordinator(), epoch = hub.paneTransportEpoch(machineId);
1188
- if (!config?.general_topic || !epoch)
1189
- throw Error("General or its paired runtime is unavailable");
1190
- const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
1191
- && this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
1192
- const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertAuditedNativePaneVersion }] = await Promise.all([
1193
- import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
1194
- ]);
1195
- if (!current())
1196
- throw Error("General workspace authority expired");
1197
- assertAuditedNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
1198
- const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
1199
- const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
1200
- if (!current())
1201
- throw Error("General workspace authority expired");
1202
- if (inventory?.capabilities?.generalWorkspace !== true)
1203
- throw new NativePaneRecoveryError({ kind: "host-upgrade" });
1204
- const result = await hub.callPane(machineId, request, maxAgentsPerMachine());
1205
- if (!current())
1206
- throw Error("General workspace authority expired");
1207
- if (generalWorkspaceUpgrade(result, request))
1208
- throw new NativePaneRecoveryError({ kind: "host-upgrade" });
1209
- return validateGeneralWorkspaceReceipt(result, request);
1210
- }
1211
- generalLocationFlights = new Map();
1212
- generalAttentionEpisodes = new Map();
1213
- generalBindings() { return new GeneralBindings(this.dataDir); }
1214
- generalLocations() {
1215
- const store = this.generalBindings();
1216
- return { generals: Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => cfg.general_topic).map(([name, cfg]) => {
1217
- const record = store.read(name), machine = cfg.machine_id ? this.runtimeHub?.machine(cfg.machine_id) : null;
1218
- return { name, displayName: cfg.display_name ?? name, location: isWindowsInstance(cfg) ? "windows" : "host",
1219
- machineId: cfg.machine_id ?? null, cli: cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code", model: cfg.model ?? "",
1220
- revision: generalBindingRevision(record, cfg), canSwitchBack: !!record && record.active !== "host",
1221
- offline: isWindowsInstance(cfg) && (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(cfg.machine_id)),
1222
- retryTarget: record?.pending?.phase === "selected" ? (record.pending.to === "host" ? { location: "host" } : (() => { const row = record.windows.find(item => item.id === record.pending.to); return { location: "windows", machineId: row.machineId, cli: row.cli, model: row.config.model ?? "" }; })()) : null,
1223
- hold: record?.hold ?? (record?.pending ? record.pending.phase === "selected" ? "start" : "exit" : null), switching: this.generalLocationFlights.has(name) };
1224
- }), machines: (this.runtimeHub?.listMachines() ?? []).filter(row => row.status !== "pending" && row.status !== "revoked").map(row => ({
1225
- id: row.id, name: row.name, online: row.status === "online" && !!this.runtimeHub?.paneTransportEpoch(row.id),
1226
- clis: row.inventory?.clis.filter(cli => cli.runnable && (cli.id === "claude-code" || cli.id === "codex")).map(cli => ({ id: cli.id, version: cli.version })) ?? [],
1227
- })) };
1228
- }
1229
- /** Pure preflight for the confirmation diff and stale-tab refusal; never prepares files or starts a CLI. */
1230
- previewGeneralLocation(raw) {
1231
- const input = parseGeneralSwitchInput(raw), cfg = this.fleetConfig?.instances[input.name];
1232
- if (!cfg?.general_topic || this.shuttingDown)
1233
- throw Error("General is unavailable");
1234
- const record = this.generalBindings().read(input.name);
1235
- if (input.revision !== generalBindingRevision(record, cfg))
1236
- throw Error("General location changed; read its settings again");
1237
- const connections = cfg.channel_id ? [cfg.channel_id] : [];
1238
- const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
1239
- if (channels.length && (connections.length !== 1 || channels.filter(row => (row.id ?? row.type) === connections[0]).length !== 1))
1240
- throw Error("General's connection cannot be resolved");
1241
- if (input.target.location === "host") {
1242
- if (!record)
1243
- throw Error("General's saved host location is unavailable");
1244
- return { input, connections, summary: [t("general_location.confirm_host"), t("general_location.history")] };
1245
- }
1246
- const active = record?.windows.find(row => row.id === record.active);
1247
- if (!record?.pending && record?.hold !== "start" && active?.machineId === input.target.machineId && active.cli === input.target.cli)
1248
- throw Error("General already runs at this location");
1249
- const machine = this.runtimeHub?.machine(input.target.machineId);
1250
- if (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(machine.id))
1251
- throw Error(t("general_location.offline"));
1252
- const provider = input.target.cli;
1253
- if (!machine.inventory?.clis.some(cli => cli.id === provider && cli.runnable))
1254
- throw Error("The chosen Windows CLI is unavailable");
1255
- return { input, connections, summary: [t("general_location.confirm_windows", settingsDisplay(machine.name)), t("general_location.history")] };
1256
- }
1257
- /** All entry points, including automatic native recovery, must respect an interrupted binding switch. */
1258
- assertGeneralBindingStart(name, config) {
1259
- if (!config.general_topic)
1260
- return;
1261
- try {
1262
- this.generalBindings().assertStart(name, config);
1263
- }
1264
- catch (err) {
1265
- this.logger.warn({ name, err }, "General location start held by its saved binding");
1266
- this.needsYou?.poke();
1267
- throw new UnsupportedCliError(t("general_location.exit_unconfirmed"));
1268
- }
1269
- }
1270
- /** Only the Settings replay's confirmed admin can enter; the whole switch owns one lifecycle transition. */
1271
- async switchGeneralLocation(raw, authorized, execution) {
1272
- const { input } = this.previewGeneralLocation(raw), name = input.name;
1273
- return this.lifecycle.runTransition(name, async (transition) => {
1274
- execution.assert();
1275
- if (!authorized() || this.generalLocationFlights.has(name))
1276
- throw Error("General location authority expired or a switch is already running");
1277
- const token = Symbol(name), generation = this.settingsGeneration;
1278
- const destinationEpoch = input.target.location === "windows" ? this.runtimeHub?.paneTransportEpoch(input.target.machineId) : null;
1279
- let expected = this.fleetConfig.instances[name], stopGeneration = this.explicitStopGeneration.get(name) ?? 0, epoch = this.lifecycle.epochOf(name);
1280
- const hostSnapshot = !isWindowsInstance(expected) ? structuredClone(this.daemons.get(name)?.getConfigSnapshot() ?? expected) : undefined;
1281
- const current = () => !this.shuttingDown && this.settingsGeneration === generation && authorized()
1282
- && this.generalLocationFlights.get(name) === token && this.fleetConfig?.instances[name] === expected
1283
- && (input.target.location !== "windows" || remoteRuntimeEnabled() && !!destinationEpoch && this.runtimeHub?.paneTransportEpoch(input.target.machineId) === destinationEpoch)
1284
- && (this.explicitStopGeneration.get(name) ?? 0) === stopGeneration && this.lifecycle.epochOf(name) === epoch;
1285
- const check = () => { if (!current())
1286
- throw Error("General location switch was superseded"); };
1287
- this.generalLocationFlights.set(name, token);
1288
- // Old ephemeral waiters carry this epoch; they cannot wake into the selected conversation.
1289
- this.cancelPendingDeliveries(name);
1290
- let lease = null;
1291
- try {
1292
- lease = await waitSettingsLease([settingsFileResource(this.getSettingsConfigPath()), `general:${this.dataDir}:${name}`], () => current() && execution.current(), performance.now() + execution.remainingMs, execution.owner);
1293
- const store = this.generalBindings();
1294
- const result = await switchGeneralLocation({ store, current: () => expected, assert: check,
1295
- prepare: async (target, retained) => {
1296
- if (target.location !== "windows")
1297
- return null;
1298
- const old = retained.windows.find(row => row.machineId === target.machineId && row.cli === target.cli);
1299
- const id = old?.id ?? randomUUID(), agentId = old?.agentId ?? randomUUID();
1300
- const publication = await this.prepareWindowsGeneralWorkspace(name, target.machineId, target.cli, id, () => current() && execution.current());
1301
- check();
1302
- execution.assert();
1303
- const config = { ...structuredClone(expected), runtime: "windows", machine_id: target.machineId,
1304
- native_adapter: "pane", native_agent_id: agentId, native_general_binding: id, native_sandbox: "workspace-write",
1305
- backend: target.cli, model: target.model, working_directory: publication.cwd, systemPrompt: "", agent_mode: "cli" };
1306
- // Host credentials, shell hooks and provider-specific options never become Windows launch configuration.
1307
- for (const key of ["backend_options", "credential_profile", "pre_task_command"])
1308
- delete config[key];
1309
- windowsAgentsFor(this, this.runtimeWindowsExecutor()).ensureGeneralBinding({ id: agentId, instance: name, machineId: target.machineId,
1310
- cli: target.cli, cwd: publication.cwd, name: (expected.display_name ?? name).slice(0, 64), model: target.model, instructions: "", description: expected.description ?? "" }, () => current() && execution.current());
1311
- return { id, agentId, machineId: target.machineId, cli: target.cli, config };
1312
- },
1313
- idle: async () => {
1314
- check();
1315
- const daemon = this.daemons.get(name);
1316
- return !daemon || daemon.getInstanceStateSnapshot().state === "idle" && daemon.getWorkingRecoveryState().idle
1317
- && daemon.getInteractionSnapshot().phase === "clear" && !this.lifecycle.hasWorkLease(name)
1318
- && Date.now() - (this.lastDeliveryAt.get(name) ?? 0) >= 2000;
1319
- },
1320
- hostSnapshot: () => hostSnapshot,
1321
- capture: async () => {
1322
- if (isWindowsInstance(expected))
1323
- return undefined;
1324
- const daemon = this.daemons.get(name);
1325
- if (!daemon)
1326
- throw Error(t("general_location.exit_unconfirmed"));
1327
- const owner = await daemon.generalHostOwner();
1328
- check();
1329
- execution.assert();
1330
- return owner;
1331
- },
1332
- stop: async (owner) => {
1333
- check();
1334
- const stopping = this.stopInstance(name, transition);
1335
- stopGeneration++;
1336
- epoch++;
1337
- await stopping;
1338
- check();
1339
- if (isWindowsInstance(expected)) {
1340
- const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(expected.native_agent_id);
1341
- if (profile.activeId || profile.paneLaunchId)
1342
- throw Error(t("general_location.exit_unconfirmed"));
1343
- execution.mutate(() => this.holdGeneralOldDeliveries(name));
1344
- return undefined;
1345
- }
1346
- if (!owner || !await confirmGeneralHostExit(owner, getTmuxSession()))
1347
- throw Error(t("general_location.exit_unconfirmed"));
1348
- check();
1349
- execution.mutate(() => this.holdGeneralOldDeliveries(name));
1350
- return owner.pin;
1351
- },
1352
- save: next => {
1353
- check();
1354
- const previous = expected;
1355
- try {
1356
- this.fleetConfig.instances[name] = next;
1357
- this.saveFleetConfig([{ path: ["instances", name], value: next }]);
1358
- expected = next;
1359
- this.routing.rebuild(this.fleetConfig);
1360
- }
1361
- catch (err) {
1362
- this.fleetConfig.instances[name] = previous;
1363
- throw err;
1364
- }
1365
- },
1366
- start: async () => {
1367
- check();
1368
- if (!execution.completed)
1369
- throw Error("General's selected binding has no committed approval");
1370
- // The committed receipt permits only this selected startup. Live authority still fences every awaited spawn boundary.
1371
- const startup = new SettingsExecution({ current: () => current() && execution.completed && execution.remainingMs > 0,
1372
- snapshot: () => this.fleetConfig });
1373
- try {
1374
- await this.startInstance(name, expected, true, "fleet-topic", false, transition, startup);
1375
- }
1376
- finally {
1377
- startup.close();
1378
- }
1379
- check();
1380
- const daemon = this.daemons.get(name);
1381
- if (!daemon || daemon.getProcessStatus() !== "running" || isNativePaneInstance(expected) && !daemon.nativePaneCurrent())
1382
- throw Error(t("general_location.start_failed"));
1383
- await this.connectIpcToInstance(name);
1384
- check();
1385
- },
1386
- notify: (kind, err) => {
1387
- const message = t(kind === "exit" ? "general_location.exit_unconfirmed" : kind === "start" ? "general_location.start_failed" : "general_location.complete", isWindowsInstance(expected) ? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host"));
1388
- this.eventLog?.insert(name, "general_location", { phase: kind, message });
1389
- if (err)
1390
- this.logger.warn({ name, phase: kind, err }, message);
1391
- else
1392
- this.logger.info({ name, phase: kind }, message);
1393
- this.needsYou?.poke();
1394
- this.emitSseEvent("status", this.getUiStatus());
1395
- this.notifyInstanceTopic(name, message);
1396
- },
1397
- }, input, execution);
1398
- return { ...result, message: result.started ? t("general_location.complete", isWindowsInstance(expected)
1399
- ? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host")) : t("general_location.start_failed") };
1400
- }
1401
- finally {
1402
- lease?.release();
1403
- if (this.generalLocationFlights.get(name) === token)
1404
- this.generalLocationFlights.delete(name);
1405
- this.needsYou?.poke();
1406
- }
1407
- });
1408
- }
1409
- /** An offline PC or a confirmed binding boundary holds old waiting input; it never becomes another conversation's queue. */
1410
- holdGeneralOldDeliveries(name) {
1411
- const outbox = this.deliveryOutbox;
1412
- if (!outbox)
1413
- return;
1414
- for (const row of outbox.listPending().filter(row => row.targetInstance === name)) {
1415
- if (row.state !== "submission_started")
1416
- outbox.failBeforeBegin(row.deliveryId, t("general_location.input_not_replayed"));
1417
- else if (row.targetDaemonBootId) {
1418
- outbox.markTargetReconciliationPending(name, row.targetDaemonBootId);
1419
- if (!outbox.reconcileAttempt(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "uncertain", t("general_location.input_not_replayed")))
1420
- throw Error("General's old delivery could not be held");
1421
- }
1422
- else
1423
- throw Error("General's old delivery has no confirmed owner");
1424
- }
1425
- this.needsYou?.poke();
1426
- }
1427
- generalLocationDeliveryHold(name) {
1428
- const config = this.fleetConfig?.instances[name];
1429
- if (!config?.general_topic)
1430
- return null;
1431
- if (this.generalLocationFlights?.has(name))
1432
- return t("general_location.exit_unconfirmed");
1433
- let record;
1434
- try {
1435
- record = this.generalBindings().read(name);
1436
- }
1437
- catch {
1438
- return t("general_location.start_failed");
1439
- }
1440
- if (record?.pending || record?.hold)
1441
- return t(record.pending?.phase === "selected" || record.hold === "start" ? "general_location.start_failed" : "general_location.exit_unconfirmed");
1442
- if (isWindowsInstance(config) && config.machine_id && (!remoteRuntimeEnabled() || this.runtimeHub?.machine(config.machine_id)?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(config.machine_id)))
1443
- return t("general_location.offline");
1444
- return null;
1445
- }
1446
- generalLocationAttention(name) {
1447
- const cfg = this.fleetConfig?.instances[name];
1448
- if (!cfg?.general_topic)
1449
- return null;
1450
- const detail = this.generalLocationDeliveryHold(name);
1451
- if (!detail) {
1452
- this.generalAttentionEpisodes.delete(name);
1453
- return null;
1454
- }
1455
- let record = null;
1456
- try {
1457
- record = this.generalBindings().read(name);
1458
- }
1459
- catch { /* a corrupt journal is a visible hold too */ }
1460
- const offline = !record?.pending && !record?.hold && isWindowsInstance(cfg) && detail === t("general_location.offline");
1461
- const key = record?.pending?.id ?? `${record?.revision ?? "unknown"}:${offline ? "offline" : "hold"}`;
1462
- let episode = this.generalAttentionEpisodes.get(name);
1463
- if (!episode || episode.key !== key) {
1464
- episode = { key, since: record?.pending?.at ?? Date.now() };
1465
- this.generalAttentionEpisodes.set(name, episode);
1466
- }
1467
- return { ...episode, detail, offline };
1468
- }
1469
- async nativePaneFor(name, config, assertCurrent) {
1470
- try {
1471
- return await (await this.nativePanes()).prepare(name, config, assertCurrent);
1472
- }
1473
- catch (err) {
1474
- if (config.general_topic && err instanceof GeneralManagedFileError) {
1475
- assertCurrent();
1476
- const store = this.generalBindings(), record = store.read(name);
1477
- if (record && record.active === config.native_general_binding) {
1478
- record.hold = "start";
1479
- store.write(record);
1480
- }
1481
- this.logger.warn({ name, managedFile: err.file }, t("general_location.start_failed"));
1482
- this.needsYou?.poke();
1483
- throw new UnsupportedCliError(t("general_location.start_failed"));
1484
- }
1485
- throw err;
1486
- }
1487
- }
1488
- async recoverNativePane(name, launchId, authorized) {
1489
- const config = this.fleetConfig?.instances[name];
1490
- if (!isNativePaneInstance(config))
1491
- throw Error("Persistent Windows pane is not configured");
1492
- const generation = this.explicitStopGeneration.get(name) ?? 0;
1493
- await this.lifecycle.runTransition(name, async (transition) => {
1494
- const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
1495
- throw Error(t("native_pane.authority_expired")); };
1496
- current();
1497
- if (this.daemons.get(name)?.nativePaneCurrent())
1498
- throw Error(t("native_pane.still_running"));
1499
- this.cancelStartupRetry(name);
1500
- this.lifecycle.cancelNativeRecovery(name);
1501
- await (await this.nativePanes()).recover(name, config, launchId, true, current);
1502
- current();
1503
- // The host supplied a positive exit before ordinary lifecycle cleanup. No signal or input is required.
1504
- await this.lifecycle.stop(name, transition);
1505
- current();
1506
- this.instanceProcessStatus.set(name, "stopped");
1507
- this.eventLog?.insert(name, "native_pane_recovered", { launchId, confirmedStopped: true });
1508
- this.logger.info({ name, launchId }, "Windows saved pane recovered by an administrator; waiting for Start");
1509
- this.emitSseEvent("status", this.getUiStatus());
1510
- });
1511
- }
1512
- holdNativePaneRecovery(name, error) {
1513
- if (!(error instanceof NativePaneRecoveryError))
1514
- return false;
1515
- this.instanceProcessStatus.set(name, "stopped");
1516
- this.logger.warn({ name, recovery: error.recovery }, error.message);
1517
- this.eventLog?.insert(name, "native_pane_recovery_needed", { ...error.recovery });
1518
- this.needsYou?.poke();
1519
- this.emitSseEvent("status", this.getUiStatus());
1520
- return true;
1521
- }
1522
- async startFreshNativePane(name, launchId, sessionId, authorized) {
1523
- const config = this.fleetConfig?.instances[name];
1524
- if (!isNativePaneInstance(config))
1525
- throw Error("Persistent Windows pane is not configured");
1526
- const generation = this.explicitStopGeneration.get(name) ?? 0;
1527
- await this.lifecycle.runTransition(name, async (transition) => {
1528
- const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
1529
- throw Error(t("native_pane.authority_expired")); };
1530
- current();
1531
- const held = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id).paneRecovery;
1532
- if (held?.kind !== "session-unconfirmed" || held.launchId !== launchId || (held.sessionId ?? null) !== sessionId)
1533
- throw Error(t("native_pane.changed"));
1534
- if (this.daemons.get(name)?.nativePaneCurrent())
1535
- throw Error(t("native_pane.still_running"));
1536
- this.cancelStartupRetry(name);
1537
- this.lifecycle.cancelNativeRecovery(name);
1538
- await this.lifecycle.stop(name, transition);
1539
- current();
1540
- try {
1541
- await (await this.nativePanes()).startFresh(name, config, launchId, sessionId, current);
1542
- current();
1543
- }
1544
- catch (error) {
1545
- current();
1546
- this.needsYou?.poke();
1547
- this.emitSseEvent("status", this.getUiStatus());
1548
- throw error;
1549
- }
1550
- this.eventLog?.insert(name, "native_pane_new_conversation", { launchId, sessionId });
1551
- await this.startInstance(name, config, true, "fleet-topic", true, transition);
1552
- current();
1553
- this.needsYou?.poke();
1554
- this.emitSseEvent("status", this.getUiStatus());
1555
- });
1556
- }
1557
- nativePanePinnedSession(name) {
1558
- const config = this.fleetConfig?.instances[name];
1559
- if (!isNativePaneInstance(config) || !config.native_agent_id)
1560
- return null;
1561
- const agent = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
1562
- return agent.fleet_instance === name && agent.machineId === config.machine_id ? agent.threadId ?? null : null;
1563
- }
1564
- onNativePaneRecoverySettled(name) {
1565
- if (this.daemons.get(name)?.nativePaneCurrent())
1566
- this.instanceProcessStatus.delete(name);
1567
- this.onAttentionChanged(name);
1568
- this.scheduleDeliveryOutboxPump();
1569
- }
1570
- setNativePanePaused(name, paused) {
1571
- const id = this.fleetConfig?.instances[name]?.native_agent_id;
1572
- if (!id)
1573
- throw Error("Native profile is missing");
1574
- windowsAgentsFor(this, this.runtimeWindowsExecutor()).setPaused(id, paused);
1575
- }
1576
- async closeRetainedNativePane(name) {
1577
- const config = this.fleetConfig?.instances[name];
1578
- if (!config)
1579
- throw Error("Native instance configuration is missing");
1580
- await (await this.nativePanes()).closeRetained(name, config, () => { if (this.shuttingDown)
1581
- throw Error("Fleet is stopping"); });
1582
- }
1583
- async reconcileNativePaneBeforeStart(name, daemon) {
1584
- const outbox = this.deliveryOutbox;
1585
- if (!outbox)
1586
- return true;
1587
- const epoch = this.lifecycle.epochOf(name);
1588
- return daemon.reconcileNativeDeliveries(outbox, () => !this.shuttingDown && this.lifecycle.epochOf(name) === epoch);
1589
- }
1590
- /** M-2255: the Windows runtimes' switches the fleet's own state reflects (null until the start-up has applied them). */
1591
- runtimeEffects = null;
1592
- runtimeEffectsScheduled = false;
1593
- localPaneDrain = null;
1594
- remotePaneDrain = null;
1595
- /** Revoke admissions synchronously; keep the captured transport only long enough for exact owned closes. */
1596
- drainNativeRuntime(remote) {
1597
- const existing = remote ? this.remotePaneDrain : this.localPaneDrain;
1598
- if (existing)
1599
- return existing;
1600
- const hub = this.runtimeHub, relay = this.localPaneRelay;
1601
- if (!remote)
1602
- relay?.beginCleanup();
1603
- const targets = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, config]) => isWindowsInstance(config) && !!config.machine_id === remote);
1604
- for (const [name, config] of targets) {
1605
- this.nativeMcpStore?.closeInstance(name);
1606
- if (isNativePaneInstance(config)) {
1607
- this.lifecycle.cancelNativeRecovery(name);
1608
- this.lifecycle.invalidate(name);
1609
- this.daemons.get(name)?.fenceDeliveryWritesForStop();
1610
- }
1611
- }
1612
- const run = Promise.resolve().then(async () => {
1613
- const results = await Promise.allSettled(targets.filter(([, config]) => isNativePaneInstance(config)).map(([name]) => this.stopInstance(name)));
1614
- results.forEach((result, index) => { if (result.status === "rejected")
1615
- this.logger.warn({ err: result.reason, remote, index }, "Windows pane cleanup unconfirmed; its reservation is retained"); });
1616
- try {
1617
- if (remote)
1618
- await hub?.shutdownPaneHosts();
1619
- else
1620
- await relay?.shutdownHost();
1621
- }
1622
- catch (error) {
1623
- this.logger.warn({ err: error, remote }, "Windows pane host shutdown unconfirmed; owned state is retained");
1624
- }
1625
- }).finally(() => {
1626
- if (remote) {
1627
- hub?.close();
1628
- if (this.runtimeHub === hub)
1629
- this.runtimeHub = null;
1630
- }
1631
- else {
1632
- relay?.stop();
1633
- if (this.localPaneRelay === relay)
1634
- this.localPaneRelay = null;
1635
- closeWindowsBridge(this);
1636
- }
1637
- });
1638
- if (remote)
1639
- this.remotePaneDrain = run;
1640
- else
1641
- this.localPaneDrain = run;
1642
- void run.finally(() => {
1643
- if (remote) {
1644
- if (this.remotePaneDrain === run)
1645
- this.remotePaneDrain = null;
1646
- }
1647
- else if (this.localPaneDrain === run)
1648
- this.localPaneDrain = null;
1649
- if (!this.shuttingDown && (remote ? remoteRuntimeEnabled() : localWindowsCliEnabled())) {
1650
- this.initializeNativeInstances();
1651
- this.initializeNativeInstances(true);
1652
- }
1653
- try {
1654
- this.emitSseEvent("status", this.getUiStatus());
1655
- }
1656
- catch { }
1657
- }).catch(error => this.logger.warn({ err: error }, "Windows runtime cleanup failed; retained ownership requires reconciliation"));
1658
- return run;
1659
- }
1660
- /**
1661
- * M-2255: Settings → Runtime's switches (fleet.yaml `runtimes`, or their environment override) in force — set each time
1662
- * the configuration in force changes (a load, a save, a reload's rollback), next to the web authority's. Every gate reads
1663
- * them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
1664
- * sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
1665
- */
1666
- observeRuntimeSwitches() {
1667
- setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
1668
- if (!this.runtimeEffects || this.runtimeEffectsScheduled)
1669
- return;
1670
- this.runtimeEffectsScheduled = true;
1671
- queueMicrotask(() => { this.runtimeEffectsScheduled = false; this.applyRuntimeSwitches(); });
1672
- }
1673
- /**
1674
- * Turned off: what it allowed ends now — the paired computers' connections (their agents show as offline) or this PC's
1675
- * Windows process (a turn in progress is cancelled, as when the fleet stops), and those agents' tool sessions.
1676
- * Turned on: the Windows agents are registered and their chat history read, as at start-up.
1677
- */
1678
- applyRuntimeSwitches() {
1679
- const was = this.runtimeEffects;
1680
- if (!was || this.shuttingDown)
1681
- return;
1682
- const now = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
1683
- if (was.windows_cli === now.windows_cli && was.remote_runtime === now.remote_runtime)
1684
- return;
1685
- this.runtimeEffects = now;
1686
- try {
1687
- if (was.remote_runtime && !now.remote_runtime) {
1688
- void this.drainNativeRuntime(true);
1689
- }
1690
- if (was.windows_cli && !now.windows_cli) {
1691
- void this.drainNativeRuntime(false);
1692
- }
1693
- if (!was.windows_cli && !was.remote_runtime && !this.localPaneDrain && !this.remotePaneDrain) {
1694
- this.initializeNativeInstances();
1695
- this.initializeNativeInstances(true);
1696
- }
1697
- this.logger.info({ windows_cli: now.windows_cli, remote_runtime: now.remote_runtime }, "Windows runtime switches applied");
1698
- }
1699
- catch (error) {
1700
- this.logger.warn({ err: error }, "Windows runtime switches could not be applied in full");
1701
- }
1702
- try {
1703
- this.emitSseEvent("status", this.getUiStatus());
1704
- }
1705
- catch { /* the next status frame carries it */ }
1706
- }
1707
- /** An unavailable opt-in runtime must not prevent the Linux fleet from starting or restoring its own history. */
1708
- initializeNativeInstances(history = false) {
1709
- if (!windowsCliEnabled())
1710
- return;
1711
- try {
1712
- const runtime = this.nativeInstanceRuntime();
1713
- if (history)
1714
- runtime.importHistory();
1715
- else
1716
- runtime.register();
1717
- }
1718
- catch (error) {
1719
- this.logger.warn({ err: error }, "Windows runtime state could not be loaded; affected native instances remain unavailable");
1720
- }
1721
- }
1722
- /** Native CLIs are headless: the manager owns the durable delivery generation instead of a tmux daemon. */
1723
- deliveryBootId(name) {
1724
- const config = this.fleetConfig?.instances[name];
1725
- if (isNativeExecInstance(config)) {
1726
- if (!this.nativeInstanceRuntime().available(name))
1727
- return null;
1728
- const machine = config.machine_id ? this.runtimeCoordinator().machine(config.machine_id) : null;
1729
- return machine ? `runtime:${machine.id}:${machine.bootId ?? machine.generation}` : this.managerBootId;
1730
- }
1731
- return this.daemons.get(name)?.bootId ?? null;
1732
- }
1733
771
  rawFleetConfig = {};
1734
772
  rawFleetDocument = null;
1735
773
  /** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
@@ -1880,6 +918,7 @@ export class FleetManager {
1880
918
  startupRetryNotices = new Map();
1881
919
  /** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
1882
920
  unsupportedCliNotices = new Map();
921
+ kiroCompatibilityHolds = new Map();
1883
922
  kiroIncompatNotices = new Map();
1884
923
  /** Backoff between automatic startup retries; the last step repeats while the backend is down. */
1885
924
  static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
@@ -1908,6 +947,7 @@ export class FleetManager {
1908
947
  kiroMigrationRuns = new Map();
1909
948
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
1910
949
  needsYou = null;
950
+ missedOfflineStore = null;
1911
951
  configHistory = null;
1912
952
  configHistoryTimer = null;
1913
953
  loadedFleetSource = null;
@@ -2023,6 +1063,9 @@ export class FleetManager {
2023
1063
  discordPresenceEagerPending = false;
2024
1064
  discordPresenceInFlight = null;
2025
1065
  static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
1066
+ /** #3066 §1d: how often a fleet with no Discord bot reads the quota for its history (the user's choice: 30 min). */
1067
+ static QUOTA_BACKGROUND_READ_MS = 30 * 60_000;
1068
+ quotaReadTimer = null;
2026
1069
  /** Days of event/activity history to keep. */
2027
1070
  static EVENT_LOG_RETENTION_DAYS = 30;
2028
1071
  watchdogTimer = null;
@@ -2529,8 +1572,6 @@ export class FleetManager {
2529
1572
  * authorizing revoked links and cookies until the fleet restarted.
2530
1573
  */
2531
1574
  settingsGeneration = 0;
2532
- /** #2942: the settings baseline the agent-requested Windows changes are checked against (set with the gate). */
2533
- settingsBaselineReader = null;
2534
1575
  settingsRuntimeRevision = 0;
2535
1576
  settingsConfirmation = null;
2536
1577
  settingsControl = null;
@@ -2587,9 +1628,8 @@ export class FleetManager {
2587
1628
  return this.settingsConfirmation;
2588
1629
  const generation = this.settingsGeneration;
2589
1630
  const current = () => !this.shuttingDown && this.settingsGeneration === generation;
2590
- const baselines = this.settingsBaselineReader = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
1631
+ const baselines = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
2591
1632
  config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
2592
- generalLocation: input => this.previewGeneralLocation(input),
2593
1633
  proof: (kind, target, id, binding, key) => {
2594
1634
  const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
2595
1635
  const proof = map.get(id);
@@ -2610,17 +1650,6 @@ export class FleetManager {
2610
1650
  const store = new SettingsConfirmationStore({
2611
1651
  audit: (event, fields) => this.logger.info({ event: `settings_confirmation.${event}`, ...fields }, "Settings confirmation audit"),
2612
1652
  notify: view => view.state === "pending" ? this.promptSettingsChange(store, view) : Promise.resolve(),
2613
- // alpha.2 (M-2300): an agent's request came, was decided or ran out — the open pages read the list again (a ping
2614
- // with nothing in it: only a web-only fleet's verified owner may read it, GET /ui/agent-proposals).
2615
- changed: view => {
2616
- if (view.source !== "agent_request")
2617
- return;
2618
- this.agentProposalsRev++;
2619
- broadcastSseEvent(this.sseClients, "agent_proposals", {}, err => this.logger.debug({ err }, "agent_proposals event failed"));
2620
- // …and a status frame with the new revision: a tab from before the upgrade (its stream knows no `agent_proposals`)
2621
- // applies status frames, and a polling page reads the same frame at its next /ui/poll.
2622
- this.emitSseEvent("status", this.getUiStatus());
2623
- },
2624
1653
  });
2625
1654
  const gate = new SettingsHttpConfirmation(store, {
2626
1655
  principal: req => {
@@ -2634,7 +1663,6 @@ export class FleetManager {
2634
1663
  && sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
2635
1664
  }, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
2636
1665
  selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
2637
- directAgentCreation: true,
2638
1666
  job: async (id) => {
2639
1667
  const settlement = this.settingsJobSettlements.get(id);
2640
1668
  if (!settlement)
@@ -2669,10 +1697,8 @@ export class FleetManager {
2669
1697
  const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
2670
1698
  if (auth.kind !== "ok")
2671
1699
  return null;
2672
- // #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there. #3008: except
2673
- // this computer's admin signed in from outside (the authenticator's code, over the link connections from outside
2674
- // opened): the web-only fleet's owner, judged below by the same authority as here (hostOperator).
2675
- if (gatewayRequestContext(req) && auth.session.remote !== "totp")
1700
+ // #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
1701
+ if (gatewayRequestContext(req))
2676
1702
  return null;
2677
1703
  // …and never a change that turns the named-link option on: that one needs a General's card or the host.
2678
1704
  if (!store.selfConfirmable(id))
@@ -2705,97 +1731,30 @@ export class FleetManager {
2705
1731
  if (own.length !== 1 || own[0].type !== chat.platform || !chat.admin(actor.user_id))
2706
1732
  return null;
2707
1733
  const affected = [...store.affectedConnections(id)]; // read now: a finished change no longer has them
2708
- const destinations = this.settingsNoticeGenerals(affected, chat.platform);
2709
- if (!destinations.length)
1734
+ if (!this.settingsNoticeGenerals(affected, chat.platform).length)
2710
1735
  return null;
2711
1736
  const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
2712
1737
  return {
2713
1738
  label: t("settings_confirm.web_verified_label", name),
2714
1739
  // admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
2715
- current: () => !!verified() && chat.admin(actor.user_id) && destinations.some(route => route.current()),
2716
- followStop: async (name, begin) => { const route = destinations.find(item => item.current()); if (!route)
2717
- throw Error("Settings route expired"); await route.followStop(name, begin); },
1740
+ current: () => !!verified() && chat.admin(actor.user_id) && this.settingsNoticeGenerals(affected, chat.platform).length > 0,
2718
1741
  done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
2719
1742
  };
2720
1743
  }
2721
- /**
2722
- * alpha.2 (M-2300; #2973's agent requests, junsion's §6): on a fleet with no chat connection there is no General to
2723
- * post an agent's Windows request in — it waited for `agend settings confirm` on the host. Its owner (decision
2724
- * 28c2d3a3: the `agend web --code` session on this computer, web-verified) now sees and decides it from the web's
2725
- * Needs you. The authority is asked from the request each time, and again at execution by the store (`current`):
2726
- * still that owner now (this computer, not revoked, signed out or run out), still a fleet with no chat connection,
2727
- * never over the public link. Anyone else — a guest, a LAN or public-link session, a chat-connected fleet's admin —
2728
- * gets nothing (a chat fleet keeps General's card as its way).
2729
- */
2730
- /**
2731
- * #2997 review (Prism P2): how many times an agent's request came or was decided — in a web-only fleet's status frame,
2732
- * so a page that never hears the stream's ping (one polling /ui/poll, one reconnecting, a tab from before the upgrade)
2733
- * reads the list again when it moves. A count, nothing more.
2734
- */
2735
- agentProposalsRev = 0;
2736
- agentProposalOwner(req) {
2737
- // #3008: this computer's admin here, or signed in from outside (over the link connections from outside opened) —
2738
- // asked of the LIVE session each time (#2997 review): a sign-out, an expiry, a revocation or the switch going off
2739
- // while the request is applied fences it.
2740
- return this.liveOwner(req, { hostOnly: false });
2741
- }
2742
- /** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
2743
- webAgentProposals(req) {
2744
- if (!this.agentProposalOwner(req))
2745
- return null;
2746
- return this.settingsGate().store.listBySource("agent_request");
2747
- }
2748
- /**
2749
- * alpha.2: the owner's Approve or Reject of an agent's request. The store claims it before its first await (a second
2750
- * click, from here or the host, finds it no longer pending) and asks the owner, the request (its fleet generation and
2751
- * Windows switch) and the target machine again before and while it runs the same creation code the web uses. The
2752
- * answer is always the request as it now stands: applied (with the new agent's name), rejected, expired, stale or
2753
- * failed (with its reason), or already decided.
2754
- */
2755
- async webDecideAgentProposal(req, id, decision) {
2756
- const owner = this.agentProposalOwner(req);
2757
- if (!owner)
2758
- return { status: 403, body: { error: "owner_required", message: t("settings_confirm.web_owner_required") } };
2759
- const store = this.settingsGate().store, before = store.inspect(id);
2760
- if (!before || before.source !== "agent_request")
2761
- return { status: 404, body: { error: "not_found", message: t("settings_confirm.no_longer_pending") } };
2762
- try {
2763
- const view = await store.decide(id, decision, { ...owner, via: "web" });
2764
- this.logger.info({ id, decision, state: view.state }, "Agent request decided from the web by this computer's admin");
2765
- return { status: 200, body: { view, agent: agentNameOf(view) } };
2766
- }
2767
- catch (err) {
2768
- const code = err instanceof SettingsConfirmationError ? err.code : "decide_failed";
2769
- const status = err instanceof SettingsConfirmationError ? err.status : 500;
2770
- // Already decided (a double click, the host's command), run out or withdrawn: say what it is now.
2771
- return { status, body: { error: code, message: t(code === "admin_required" ? "settings_confirm.web_owner_required" : "settings_confirm.no_longer_pending"), view: store.inspect(id) } };
2772
- }
2773
- }
2774
- /** A confirmation belongs to the server route/lifetime, not the availability of General's model. */
2775
- settingsGeneralRoute(name, platform, excluded) {
2776
- const config = this.fleetConfig?.instances[name];
2777
- if (!config?.general_topic || this.shuttingDown || this.ipcStoppingInstances.has(name))
2778
- return null;
2779
- const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
2780
- if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform)
2781
- return null;
2782
- const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
2783
- if (!chatId || !topic)
2784
- return null;
2785
- const route = () => !this.shuttingDown && this.fleetConfig?.instances[name]?.general_topic === true
2786
- && this.adapterState.get(adapterId)?.status === "connected"
2787
- && this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
2788
- && this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic;
2789
- const lifetime = new SettingsRouteLifetime(name, route, () => this.lifecycle.epochOf(name), () => this.ipcStoppingInstances.has(name));
2790
- return lifetime.current() ? { adapterId, adapter, chatId, topic, current: lifetime.current, followStop: lifetime.followStop.bind(lifetime) } : null;
2791
- }
2792
- /** Connected, same-platform, unaffected server routes can post cards even while the model is offline. */
1744
+ /** #2170: the Generals a chat card could be posted in (promptSettingsChange's rule): connected, on the platform, unaffected. */
2793
1745
  settingsNoticeGenerals(affected, platform) {
2794
- const excluded = new Set(affected);
2795
- return Object.keys(this.fleetConfig?.instances ?? {}).flatMap(name => {
2796
- const route = this.settingsGeneralRoute(name, platform, excluded);
2797
- return route ? [route] : [];
2798
- });
1746
+ const excluded = new Set(affected), found = [];
1747
+ for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
1748
+ if (!config.general_topic || !this.daemons.get(name))
1749
+ continue;
1750
+ const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
1751
+ if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform || this.adapterState.get(adapterId)?.status !== "connected")
1752
+ continue;
1753
+ const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
1754
+ if (chatId && topic)
1755
+ found.push({ adapterId, adapter, chatId, topic });
1756
+ }
1757
+ return found;
2799
1758
  }
2800
1759
  /**
2801
1760
  * #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
@@ -2854,119 +1813,6 @@ export class FleetManager {
2854
1813
  return { platform, current, admin: userId => current() && !!userId
2855
1814
  && identities.every(owner => this.isFleetAdmin(userId, owner.id)) };
2856
1815
  }
2857
- /**
2858
- * #2942: a Windows create or settings change an AGENT asked for. Nothing is created or written here: it becomes a
2859
- * pending settings change whose General card a fleet admin must confirm (promptSettingsChange — the same admin rule
2860
- * as every card). Its apply runs the same creation/patch code the web uses, inside the store's SettingsExecution, with
2861
- * the confirming admin, the Windows switch and the target machine asked again at that moment.
2862
- */
2863
- async proposeAgentWindowsChange(input) {
2864
- const gate = this.settingsGate(), baselines = this.settingsBaselineReader;
2865
- if (!baselines || !this.fleetConfig)
2866
- throw new Error("Fleet configuration is unavailable");
2867
- const generation = this.settingsGeneration, machineId = input.machineId;
2868
- const switchOn = () => machineId ? remoteRuntimeEnabled() : localWindowsCliEnabled();
2869
- const online = () => !machineId || (remoteRuntimeEnabled() && this.runtimeCoordinator().machine(machineId)?.status === "online");
2870
- if (!switchOn())
2871
- throw new Error(machineId ? "Remote runtime is not enabled" : LOCAL_WINDOWS_CLI_OFF);
2872
- const baseline = await baselines.read(), snapshot = settingsFingerprint(baselines.snapshot());
2873
- const { view } = gate.store.propose({ session: `agent:${input.requester}`, key: `agent-windows:${input.key}`,
2874
- requestFingerprint: input.fingerprint, fingerprint: input.fingerprint, section: "channel", requestedBy: input.requester,
2875
- source: "agent_request", summary: input.summary, bytes: input.bytes, authority: input.authority, affectedConnections: [],
2876
- current: () => !this.shuttingDown && this.settingsGeneration === generation && switchOn(),
2877
- snapshot: () => baselines.snapshot(),
2878
- unchanged: async () => (await baselines.read()).fingerprint === baseline.fingerprint && settingsFingerprint(baselines.snapshot()) === snapshot,
2879
- // The store's own fence (this confirming admin, the session, the state) plus the switch and machine now.
2880
- apply: async (actorCurrent, execution) => {
2881
- execution.assert();
2882
- // alpha.2 polish: a paired machine whose CLI inventory has not arrived yet (just paired) is asked for its own
2883
- // report first (it runs on that PC), so the creation validates against what is installed there — never a refusal
2884
- // that only a manual Refresh would undo. Its answer or failure changes nothing else; the fence is asked again.
2885
- if (machineId && online() && !inventoryProbed(this.runtimeCoordinator().machine(machineId)?.inventory)) {
2886
- // `force`: the runner's cached answer on a fresh profile is the placeholder itself (#3005 rig recheck).
2887
- try {
2888
- await this.runtimeCoordinator().call(machineId, "probe", { force: true });
2889
- }
2890
- catch { /* validation says what is missing */ }
2891
- execution.assert();
2892
- }
2893
- return input.apply(() => actorCurrent() && online(), execution);
2894
- } });
2895
- return view;
2896
- }
2897
- /** #2942: create_instance with runtime "windows" from an agent: validated like the web, then held for General. */
2898
- async requestWindowsCreate(requester, rawArgs) {
2899
- const cfg = this.fleetConfig;
2900
- if (!cfg)
2901
- throw new Error("Fleet configuration is unavailable");
2902
- let parsed;
2903
- try {
2904
- parsed = parseNativeInstanceCreate(rawArgs);
2905
- }
2906
- catch {
2907
- throw new Error("Invalid Windows agent request: check the directory, sandbox, backend and model");
2908
- }
2909
- // The web's own projection: the same validation, topic target and diff the admin's dashboard confirmation shows.
2910
- let diff;
2911
- try {
2912
- diff = prepareSettingsEffect("POST", "/ui/instances", rawArgs, { config: cfg, classic: {} }).diff;
2913
- }
2914
- catch {
2915
- throw new Error("Invalid Windows agent request: its topic connection is not one this fleet can use");
2916
- }
2917
- if (!diff)
2918
- throw new Error("Invalid Windows agent request");
2919
- return this.proposeAgentWindowsChange({ requester, key: diff.fingerprint, fingerprint: diff.fingerprint, summary: diff.summary,
2920
- machineId: parsed.input.machineId, bytes: JSON.stringify(rawArgs).length,
2921
- // The General card is the confirmation for an agent's request, whatever the requested connection (a topic or none).
2922
- authority: { connections: diff.authority?.connections ?? [], primaryGeneral: true, unknown: false },
2923
- apply: (allowed, execution) => createNativeWindowsInstance(this, parsed, allowed, execution) });
2924
- }
2925
- /** #2942: update_instance_config on a Windows agent: only the fields the web editor allows, held for General. */
2926
- async requestWindowsConfig(requester, name, patch) {
2927
- const inst = this.fleetConfig?.instances[name];
2928
- if (!isWindowsInstance(inst))
2929
- throw new Error("Windows agent not found");
2930
- let edit;
2931
- try {
2932
- edit = NativeInstanceConfigEdit.parse(patch);
2933
- }
2934
- catch {
2935
- throw new Error("Invalid Windows agent settings: display_name, description and model only");
2936
- }
2937
- const before = { display_name: inst.display_name ?? "", description: inst.description ?? "", model: inst.model ?? "" };
2938
- const changed = Object.keys(edit).filter(key => edit[key] !== before[key]);
2939
- if (!changed.length)
2940
- throw new Error("Nothing to change: these settings already have these values");
2941
- const summary = changed.map(key => `${name} ${key}: ${settingsDisplay(before[key])} → ${settingsDisplay(edit[key])}`);
2942
- const machineId = typeof inst.machine_id === "string" ? inst.machine_id : undefined;
2943
- // #2973 review (Prism P1): a persistent pane is bound to a chat connection; changing it (a model change restarts it)
2944
- // needs that connection's admins as well as the primary General's, exactly as its creation did. An instance whose
2945
- // binding names no single configured connection is refused (fail closed), never confirmed by the General alone.
2946
- const connections = [];
2947
- if (isNativePaneInstance(inst) || inst.channel_id !== undefined) {
2948
- const bound = inst.channel_id, channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
2949
- if (typeof bound !== "string" || channels.filter(ch => (ch.id ?? ch.type) === bound).length !== 1)
2950
- throw new Error("This Windows agent's chat connection cannot be resolved; change its settings in the web dashboard");
2951
- connections.push(bound);
2952
- }
2953
- return this.proposeAgentWindowsChange({ requester, key: `${name}:${settingsFingerprint(edit)}`, fingerprint: settingsFingerprint([name, edit]),
2954
- summary, machineId, bytes: JSON.stringify(edit).length, authority: { connections, primaryGeneral: true, unknown: false },
2955
- // #2973 review (Prism P2): the update may wait for its lifecycle turn; if the agent is rebound meanwhile (B → C), the
2956
- // approval for B must not stop or change it on C. Pin the approved target and require the execution's snapshot to be
2957
- // unchanged before every pre-commit effect; the post-commit restart keeps its own (refreshed) checks.
2958
- apply: (allowed, execution) => {
2959
- const pinned = { channel: inst.channel_id, machine: inst.machine_id, adapter: inst.native_adapter, runtime: inst.runtime };
2960
- const sameTarget = () => {
2961
- const now = this.fleetConfig?.instances[name];
2962
- return !!now && now.channel_id === pinned.channel && now.machine_id === pinned.machine && now.native_adapter === pinned.adapter && now.runtime === pinned.runtime;
2963
- };
2964
- // Before the commit: the exact target and an unchanged snapshot. After runtime.update completes the transaction,
2965
- // current() is false by design; the committed operation's restart receipt then needs the same actor, target and
2966
- // switch checks plus `completed` — a completed execution can never authorize another write (mutate refuses).
2967
- return updateNativeWindowsInstance(this, name, edit, () => allowed() && sameTarget() && (execution.completed || execution.current()), execution);
2968
- } });
2969
- }
2970
1816
  async promptSettingsChange(store, view) {
2971
1817
  const generation = this.settingsGeneration;
2972
1818
  const authority = this.settingsChatAuthority(store, view.id);
@@ -2975,15 +1821,25 @@ export class FleetManager {
2975
1821
  return;
2976
1822
  }
2977
1823
  const excluded = new Set(store.affectedConnections(view.id));
2978
- for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
2979
- const route = this.settingsGeneralRoute(name, authority.platform, excluded);
2980
- if (!route)
1824
+ for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
1825
+ if (!config.general_topic)
1826
+ continue;
1827
+ const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
1828
+ const general = this.daemons.get(name);
1829
+ if (!adapterId || !adapter || !general || excluded.has(adapterId) || adapter.type !== authority.platform)
1830
+ continue;
1831
+ const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
1832
+ if (!chatId || !topic)
2981
1833
  continue;
2982
- const { adapterId, adapter, chatId, topic } = route;
2983
1834
  let retired = false, attached = false;
2984
1835
  const postingDeadline = performance.now() + Math.min(5000, view.remaining_ms);
2985
- const current = () => !retired && (attached || performance.now() < postingDeadline) && this.settingsGeneration === generation
2986
- && route.current() && authority.current();
1836
+ const current = () => !retired && (attached || performance.now() < postingDeadline) && !this.shuttingDown && this.settingsGeneration === generation
1837
+ && this.fleetConfig?.instances[name]?.general_topic === true
1838
+ && this.daemons.get(name) === general && !this.ipcStoppingInstances.has(name)
1839
+ && this.adapterState.get(adapterId)?.status === "connected"
1840
+ && this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
1841
+ && this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic
1842
+ && authority.current();
2987
1843
  if (!current())
2988
1844
  continue;
2989
1845
  try {
@@ -2993,7 +1849,7 @@ export class FleetManager {
2993
1849
  t("settings_confirm.requester", view.requested_by), ...view.summary].join("\n"),
2994
1850
  choices: [{ action: "confirm", label: t("settings_confirm.confirm_button") }, { action: "reject", label: t("settings_confirm.reject_button") }],
2995
1851
  expiredText: t("settings_confirm.no_longer_pending"), timeoutMs: view.remaining_ms,
2996
- extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId), confirmationStop: route.followStop },
1852
+ extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId) },
2997
1853
  });
2998
1854
  void posted.then(nonce => {
2999
1855
  if (!retired)
@@ -3261,7 +2117,6 @@ export class FleetManager {
3261
2117
  try {
3262
2118
  const view = await this.settingsGate().store.decide(entry.pendingChangeId, action, {
3263
2119
  label: t("settings_confirm.admin_label", data.userId ?? ""),
3264
- followStop: entry.confirmationStop,
3265
2120
  current: () => entry.confirmationCurrent?.() === true && this.getAdapterForInstance(entry.instanceName) === entry.adapter
3266
2121
  && !!data.userId && entry.confirmationAdmin?.(data.userId) === true,
3267
2122
  });
@@ -3334,6 +2189,22 @@ export class FleetManager {
3334
2189
  * startup-only key at startup. See checkStartupSignatureConsistency(). */
3335
2190
  fleetSignatureMismatch = null;
3336
2191
  healthServerListening = false;
2192
+ agentCredentialsCache;
2193
+ get agentCredentials() {
2194
+ return this.agentCredentialsCache ??= new AgentCredentials(name => agentCredentialPolicy(this.fleetConfig, name, this.isClassicInstance(name)), name => {
2195
+ try {
2196
+ const token = randomBytes(32).toString("hex");
2197
+ const result = writeSecretFile(join(this.getInstanceDir(name), "agent.token"), token);
2198
+ if (!result.ok)
2199
+ this.logger.error({ name, mode: result.mode, reason: result.reason }, "Agent token file is not owner-only");
2200
+ return token;
2201
+ }
2202
+ catch {
2203
+ this.logger.error({ name }, "Agent credential renewal failed; prior generation remains retired");
2204
+ return null;
2205
+ }
2206
+ });
2207
+ }
3337
2208
  constructor(dataDir) {
3338
2209
  this.dataDir = dataDir;
3339
2210
  FleetManager.signalTarget = this;
@@ -3356,6 +2227,7 @@ export class FleetManager {
3356
2227
  this.topicCommands = new TopicCommands(this);
3357
2228
  this.topicArchiver = new TopicArchiver(this);
3358
2229
  this.statuslineWatcher = new StatuslineWatcher(this);
2230
+ this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
3359
2231
  }
3360
2232
  /** #1266: a reply's buttons — the store (reply-buttons.db) and what a click does. Created on first use. */
3361
2233
  replyButtonsCtl = null;
@@ -3636,15 +2508,12 @@ export class FleetManager {
3636
2508
  }
3637
2509
  /** Synchronous SQLite admission; the caller may acknowledge only after this returns. */
3638
2510
  admitDurableDelivery(input) {
3639
- const bindingHold = this.generalLocationDeliveryHold(input.targetInstance);
3640
- if (bindingHold)
3641
- throw Error(bindingHold);
3642
2511
  const outbox = this.deliveryOutbox;
3643
2512
  if (!outbox)
3644
2513
  throw new Error("Durable delivery store is unavailable");
3645
- const currentSourceBootId = this.getDaemonBootId(input.sourceInstance);
3646
- const sourceDaemonBootId = input.sourceDaemonBootId ?? currentSourceBootId;
3647
- if (!currentSourceBootId || !sourceDaemonBootId || currentSourceBootId !== sourceDaemonBootId) {
2514
+ const source = this.daemons.get(input.sourceInstance);
2515
+ const sourceDaemonBootId = input.sourceDaemonBootId ?? source?.bootId;
2516
+ if (!source || !sourceDaemonBootId || source.bootId !== sourceDaemonBootId) {
3648
2517
  throw new Error("Source daemon generation is no longer current; outcome is unknown");
3649
2518
  }
3650
2519
  const sourceKey = `mcp:${input.sourceInstance}:${input.operationId}:${input.targetInstance}:${input.targetSession ?? ""}:${input.kind}`;
@@ -3678,7 +2547,7 @@ export class FleetManager {
3678
2547
  const human = (item) => {
3679
2548
  if (!item.authority || !state)
3680
2549
  return item;
3681
- const authority = this.receiptAuthorityNow(item.authority, state);
2550
+ const authority = authorityNow(item.authority, state);
3682
2551
  return { ...item, authority, ...(authority.downgraded ? { source_instance: "human:web:unknown" } : {}) };
3683
2552
  };
3684
2553
  // #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
@@ -3707,11 +2576,6 @@ export class FleetManager {
3707
2576
  return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
3708
2577
  }
3709
2578
  getDaemonBootId(instanceName) {
3710
- if (this.generalLocationDeliveryHold(instanceName))
3711
- return undefined;
3712
- if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
3713
- return !this.shuttingDown && windowsCliEnabled() && this.nativeMcpStore?.hasActive(instanceName) ? this.managerBootId : undefined;
3714
- }
3715
2579
  return this.daemons.get(instanceName)?.bootId;
3716
2580
  }
3717
2581
  markDurableResponseDelivered(sourceInstance, operationId) {
@@ -3720,21 +2584,11 @@ export class FleetManager {
3720
2584
  /** Called after a replacement Daemon object is installed in lifecycle.daemons. */
3721
2585
  onDaemonReady(name, daemonBootId) {
3722
2586
  const daemon = this.daemons.get(name);
3723
- if (daemon?.bootId === daemonBootId) {
2587
+ if (daemon?.bootId === daemonBootId)
3724
2588
  this.listenForWorkingState(name, daemon);
3725
- const cfg = this.fleetConfig?.instances[name];
3726
- if (cfg?.general_topic && !this.generalLocationFlights.has(name) && daemon.getProcessStatus() === "running"
3727
- && (!isNativePaneInstance(cfg) || daemon.nativePaneCurrent())) {
3728
- try {
3729
- if (this.generalBindings().completeSelected(name, cfg)) {
3730
- this.needsYou?.poke();
3731
- this.logger.info({ name }, "General location's selected start receipt settled after recovery");
3732
- }
3733
- }
3734
- catch (err) {
3735
- this.logger.warn({ name, err }, "General location start receipt remains held");
3736
- }
3737
- }
2589
+ // #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
2590
+ if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
2591
+ void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
3738
2592
  }
3739
2593
  // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
3740
2594
  // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
@@ -3879,15 +2733,6 @@ export class FleetManager {
3879
2733
  this.deliveryPumpRunning = true;
3880
2734
  this.deliveryPumpScheduled = false;
3881
2735
  try {
3882
- // Native workers have no resident daemon to wake. Pending durable work resumes their logical instance;
3883
- // the Windows bridge still owns the single physical execution slot.
3884
- for (const target of new Set(outbox.listPending().map(row => row.targetInstance))) {
3885
- if (isNativeExecInstance(this.fleetConfig?.instances[target])) {
3886
- const runtime = this.nativeInstanceRuntime(), agent = runtime.get(target);
3887
- if (agent?.paused && !agent.blocked)
3888
- runtime.setPaused(target, false);
3889
- }
3890
- }
3891
2736
  outbox.expireStale(undefined, undefined, target => {
3892
2737
  const failure = this.wakeCoordinator?.wakeFailure(target);
3893
2738
  return failure ? `target could not be woken (${failure})` : undefined;
@@ -3900,7 +2745,7 @@ export class FleetManager {
3900
2745
  // Phase 2c: a lane a queue worker owns is never claimed here.
3901
2746
  target => this.queueWorkers.has(target) || this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target)
3902
2747
  ? null
3903
- : this.deliveryBootId(target), this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
2748
+ : this.daemons.get(target)?.bootId ?? null, this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
3904
2749
  if (!claimed)
3905
2750
  break;
3906
2751
  this.activeDurableTargets.add(claimed.targetInstance);
@@ -3961,7 +2806,7 @@ export class FleetManager {
3961
2806
  wanted: () => this.deliveryWorkerMode(target) === "on",
3962
2807
  available: () => !this.shuttingDown && this.deliveryOutbox?.isOpen === true,
3963
2808
  blocked: () => this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target) === true,
3964
- daemonBootId: () => this.deliveryBootId(target),
2809
+ daemonBootId: () => this.daemons.get(target)?.bootId ?? null,
3965
2810
  claim: bootId => this.deliveryOutbox?.claimNext(this.managerBootId, candidate => candidate === target ? bootId : null, new Set(), row => this.recoveryDeliveryEligible(row)),
3966
2811
  tryAcquireBudget: () => {
3967
2812
  if (this.activeDurableTargets.size >= 8)
@@ -3992,10 +2837,6 @@ export class FleetManager {
3992
2837
  const outbox = this.deliveryOutbox;
3993
2838
  if (!outbox)
3994
2839
  return;
3995
- if (isNativeExecInstance(this.fleetConfig?.instances[claimed.targetInstance])) {
3996
- await this.dispatchNativeDurableDelivery(claimed);
3997
- return;
3998
- }
3999
2840
  const target = claimed.targetInstance;
4000
2841
  const attempt = claimed.attemptNo;
4001
2842
  const payload = { ...claimed.payload };
@@ -4077,85 +2918,6 @@ export class FleetManager {
4077
2918
  }
4078
2919
  await laneReleased;
4079
2920
  }
4080
- /** The ordinary durable lane, with provider completion proof instead of terminal/IPC submission proof. */
4081
- async dispatchNativeDurableDelivery(claimed) {
4082
- const outbox = this.deliveryOutbox;
4083
- if (!outbox)
4084
- return;
4085
- const { deliveryId, targetDaemonBootId, attemptNo, targetInstance: target } = claimed;
4086
- let begun = false;
4087
- try {
4088
- if (["raw_paste", "steer"].includes(claimed.kind)) {
4089
- if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) === "begun")
4090
- outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "failed", "Windows runtime does not support terminal input or steering");
4091
- return;
4092
- }
4093
- const meta = claimed.payload.meta && typeof claimed.payload.meta === "object" ? claimed.payload.meta : {};
4094
- const task = await this.nativeInstanceRuntime().execute(target, { ...claimed.payload, requestId: deliveryId, meta: { ...meta, from_instance: claimed.sourceInstance, correlation_id: claimed.correlationId } }, {
4095
- stillCurrent: () => !this.shuttingDown,
4096
- beforeStart: () => { if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) !== "begun")
4097
- throw new Error("Windows delivery attempt is no longer current"); begun = true; },
4098
- });
4099
- const outcome = task.outcomeConfirmed === false ? "uncertain" : task.status === "completed" ? "delivered" : "failed";
4100
- outbox.complete(deliveryId, targetDaemonBootId, attemptNo, outcome, task.error || task.status);
4101
- // Return a requested peer report when the provider did not explicitly report it through scoped MCP.
4102
- if (meta.requires_reply === "true" && claimed.correlationId && !task.reportedCorrelationIds?.includes(claimed.correlationId) && this.fleetConfig?.instances[claimed.sourceInstance]) {
4103
- const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
4104
- outbox.admit({ sourceKey: `native-result:${deliveryId}`, operationId: `native-result:${deliveryId}`, sourceInstance: target, sourceDaemonBootId: this.managerBootId,
4105
- targetInstance: claimed.sourceInstance, kind: "fleet_inbound", correlationId: claimed.correlationId,
4106
- payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: target, correlation_id: claimed.correlationId, requires_reply: "false", source: "native_runtime" } } });
4107
- this.scheduleDeliveryOutboxPump();
4108
- }
4109
- }
4110
- catch (error) {
4111
- const reason = error instanceof Error ? error.message : String(error);
4112
- if (begun)
4113
- outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "uncertain", reason);
4114
- else
4115
- outbox.retryBeforeBegin(deliveryId, targetDaemonBootId, attemptNo, reason, 5000);
4116
- this.logger.warn({ target, deliveryId, err: reason }, "Windows durable delivery did not complete");
4117
- }
4118
- }
4119
- /** A runtime's persisted completion can outlive this manager. Resolve its original delivery fence, never send
4120
- * the prompt again, and reconstruct a missing peer report using the same durable operation ID. */
4121
- reconcileRemoteDelivery(machineId, task) {
4122
- const outbox = this.deliveryOutbox;
4123
- if (!outbox?.isOpen || !task.finishedAt || task.outcomeConfirmed !== true || !["completed", "failed", "cancelled"].includes(task.status))
4124
- return;
4125
- const key = `${task.id}:${task.finishedAt}:${task.status}`;
4126
- if (this.reconciledRemoteResults.has(key))
4127
- return;
4128
- const row = outbox.get(task.id);
4129
- if (!row || !isNativeExecInstance(this.fleetConfig?.instances[row.targetInstance]) || this.fleetConfig?.instances[row.targetInstance]?.machine_id !== machineId
4130
- || !row.targetDaemonBootId || this.activeDurableTargets.has(row.targetInstance))
4131
- return;
4132
- if (row.state === "submission_started" || row.state === "reconciliation_pending") {
4133
- if (!outbox.complete(row.deliveryId, row.targetDaemonBootId, row.attemptNo, task.status === "completed" ? "delivered" : "failed", task.error || "Runtime completion recovered after reconnect"))
4134
- return;
4135
- }
4136
- else if (row.state === "uncertain" && task.status === "completed") {
4137
- if (outbox.markConsumed(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "turn", "Paired runtime journal confirmed this exact execution completed") === "ignored")
4138
- return;
4139
- }
4140
- else if (row.state !== "delivered" && row.state !== "failed")
4141
- return;
4142
- const meta = row.payload.meta && typeof row.payload.meta === "object" ? row.payload.meta : {};
4143
- if (meta.requires_reply !== "true" || !row.correlationId || !this.fleetConfig?.instances[row.sourceInstance]) {
4144
- this.reconciledRemoteResults.add(key);
4145
- return;
4146
- }
4147
- const reports = outbox.queryStatusForInstance(row.targetInstance, { correlationId: row.correlationId, limit: 100 }).items;
4148
- if (reports.some(report => report.source_instance === row.targetInstance && report.target_instance === row.sourceInstance
4149
- && report.kind === "report" && !["failed", "cancelled"].includes(report.state))) {
4150
- this.reconciledRemoteResults.add(key);
4151
- return;
4152
- }
4153
- const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
4154
- outbox.admit({ sourceKey: `native-result:${row.deliveryId}`, operationId: `native-result:${row.deliveryId}`, sourceInstance: row.targetInstance, sourceDaemonBootId: this.managerBootId,
4155
- targetInstance: row.sourceInstance, kind: "fleet_inbound", correlationId: row.correlationId,
4156
- payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: row.targetInstance, correlation_id: row.correlationId, requires_reply: "false", source: "native_runtime" } } });
4157
- this.reconciledRemoteResults.add(key);
4158
- }
4159
2921
  async waitForDurableLaneRelease(claimed) {
4160
2922
  const outbox = this.deliveryOutbox;
4161
2923
  if (!outbox)
@@ -4612,7 +3374,6 @@ export class FleetManager {
4612
3374
  this.warnAboutRemovedLoginMode(loaded);
4613
3375
  this.enforceGuestSwitch();
4614
3376
  this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
4615
- this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
4616
3377
  // Child tool/guidance readers must follow this fleet's actual config, including --config.
4617
3378
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
4618
3379
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
@@ -4666,6 +3427,7 @@ export class FleetManager {
4666
3427
  * guild channel.
4667
3428
  */
4668
3429
  reregisterClassicChannels() {
3430
+ this.agentCredentialsCache?.observePolicies();
4669
3431
  if (!this.classicChannels)
4670
3432
  return;
4671
3433
  const channels = this.classicChannels.getAll();
@@ -5041,8 +3803,6 @@ export class FleetManager {
5041
3803
  }
5042
3804
  /** Get the adapter bound to an instance, falling back to primary adapter */
5043
3805
  getAdapterForInstance(name) {
5044
- if (isNativeExecInstance(this.fleetConfig?.instances[name]))
5045
- return null;
5046
3806
  const worldId = this.getInstanceAdapterId(name);
5047
3807
  if (worldId)
5048
3808
  return this.worlds.get(worldId)?.adapter ?? this.adapter;
@@ -5105,8 +3865,6 @@ export class FleetManager {
5105
3865
  }
5106
3866
  /** Get the group_id for an instance's bound adapter */
5107
3867
  getGroupIdForInstance(name) {
5108
- if (isNativeExecInstance(this.fleetConfig?.instances[name]))
5109
- return "";
5110
3868
  const adapterId = this.getInstanceAdapterId(name);
5111
3869
  const world = adapterId ? this.worlds.get(adapterId) : undefined;
5112
3870
  return world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
@@ -5143,8 +3901,6 @@ export class FleetManager {
5143
3901
  */
5144
3902
  getInstanceAdapterId(name) {
5145
3903
  const cfg = this.fleetConfig?.instances[name];
5146
- if (isNativeExecInstance(cfg))
5147
- return undefined;
5148
3904
  if (cfg)
5149
3905
  return cfg.channel_id ?? this.getPrimaryAdapterId();
5150
3906
  if (this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
@@ -5172,32 +3928,9 @@ export class FleetManager {
5172
3928
  this.instanceWorldBinding.set(name, adapterId);
5173
3929
  }
5174
3930
  getInstanceStatus(name) {
5175
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
5176
- try {
5177
- return this.nativeInstanceRuntime().status(name);
5178
- }
5179
- catch {
5180
- return "crashed";
5181
- }
5182
- }
5183
3931
  if (this.lifecycle.isPaused(name))
5184
3932
  return "paused";
5185
3933
  const daemon = this.lifecycle.daemons.get(name);
5186
- if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
5187
- const config = this.fleetConfig.instances[name];
5188
- if (config.machine_id ? !remoteRuntimeEnabled() : !localWindowsCliEnabled())
5189
- return "stopped";
5190
- if (daemon) {
5191
- const native = daemon, status = native.getProcessStatus?.() ?? "crashed";
5192
- return status === "running" && native.nativePaneCurrent?.() !== true ? "crashed" : status;
5193
- }
5194
- try {
5195
- return this.nativeInstanceRuntime().get(name)?.activeId ? "crashed" : "stopped";
5196
- }
5197
- catch {
5198
- return "crashed";
5199
- }
5200
- }
5201
3934
  const processStatus = this.instanceProcessStatus.get(name);
5202
3935
  // IPC can be disconnected during a respawn, so the event which announces
5203
3936
  // the new live pane may be missed. The in-process daemon is authoritative
@@ -5231,14 +3964,6 @@ export class FleetManager {
5231
3964
  }
5232
3965
  }
5233
3966
  getInstanceExecutionState(name) {
5234
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
5235
- try {
5236
- return this.nativeInstanceRuntime().execution(name);
5237
- }
5238
- catch {
5239
- return null;
5240
- }
5241
- }
5242
3967
  if (this.lifecycle.isPaused(name))
5243
3968
  return null;
5244
3969
  // Process status wins over a stale pane snapshot. A dead remain-on-exit pane
@@ -5249,8 +3974,6 @@ export class FleetManager {
5249
3974
  }
5250
3975
  /** Pure read of the single daemon-owned observation; no probe/capture or second tracker. */
5251
3976
  getInstanceInteraction(name) {
5252
- if (isNativeExecInstance(this.fleetConfig?.instances[name]))
5253
- return null;
5254
3977
  if (this.lifecycle.isPaused(name) || this.instanceProcessStatus.has(name))
5255
3978
  return null;
5256
3979
  const daemon = this.lifecycle.daemons.get(name);
@@ -5338,8 +4061,6 @@ export class FleetManager {
5338
4061
  bindings.push([name, backend, profile]);
5339
4062
  };
5340
4063
  for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
5341
- if (isWindowsInstance(config))
5342
- continue; // Windows sign-ins and usage do not belong to the Linux account.
5343
4064
  // loadFleetConfig() has already merged the fleet default into each row.
5344
4065
  const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
5345
4066
  add(name, backend, instanceCredentialProfile(config, this.fleetConfig?.defaults, backend));
@@ -5397,6 +4118,13 @@ export class FleetManager {
5397
4118
  changed: () => this.emitSseEvent("status", this.getUiStatus()),
5398
4119
  });
5399
4120
  }
4121
+ /**
4122
+ * Claude history in the View: an instance's earlier conversation for its View, from the transcript the Log reads —
4123
+ * the View's route applied its own access rules first (view-api.ts); the Log's switch plays no part.
4124
+ */
4125
+ viewHistory(name, q) {
4126
+ return viewHistory((n, rq) => this.log.readBack(n, rq), name, q);
4127
+ }
5400
4128
  /** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
5401
4129
  logSteps(name, q) { return this.log.steps(name, q); }
5402
4130
  /**
@@ -5946,8 +4674,6 @@ export class FleetManager {
5946
4674
  * id is no platform message, and the echo is the fleet's own post. The web send calls this on the echo's lane.
5947
4675
  */
5948
4676
  startWebChatProgress(instanceName) {
5949
- if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
5950
- return Promise.resolve();
5951
4677
  if (this.shuttingDown)
5952
4678
  return Promise.resolve();
5953
4679
  return this.sendCancelButton(instanceName);
@@ -6213,12 +4939,6 @@ export class FleetManager {
6213
4939
  }
6214
4940
  /** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
6215
4941
  async deliverToInstance(instanceName, payload, options = {}) {
6216
- if (this.generalLocationDeliveryHold(instanceName))
6217
- return false;
6218
- if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
6219
- return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
6220
- ...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
6221
- }
6222
4942
  const deliveryEpoch = this.getDeliveryEpoch(instanceName);
6223
4943
  const deliveryPayload = { ...payload, delivery_epoch: deliveryEpoch };
6224
4944
  const meta = payload.meta && typeof payload.meta === "object"
@@ -6776,10 +5496,6 @@ export class FleetManager {
6776
5496
  * slot admission and joins any wake already in flight for the target.
6777
5497
  */
6778
5498
  async explicitWake(name, timeoutMs = 30_000, retryStoppedSession = false) {
6779
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
6780
- this.nativeInstanceRuntime().setPaused(name, false);
6781
- return;
6782
- }
6783
5499
  if (this.sessionOomStopped(name)) {
6784
5500
  if (!retryStoppedSession)
6785
5501
  throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
@@ -6804,10 +5520,6 @@ export class FleetManager {
6804
5520
  .sort((a, b) => (readLastInboundAt(this.getInstanceDir(a)) ?? 0) - (readLastInboundAt(this.getInstanceDir(b)) ?? 0))[0];
6805
5521
  }
6806
5522
  async changeInstancePauseState(name, action) {
6807
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
6808
- this.nativeInstanceRuntime().setPaused(name, action === "pause");
6809
- return action === "pause" ? "paused" : "awake";
6810
- }
6811
5523
  if (action === "wake") {
6812
5524
  await this.explicitWake(name, 30_000, true);
6813
5525
  this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
@@ -6834,6 +5546,7 @@ export class FleetManager {
6834
5546
  }
6835
5547
  classicBehaviorUpdate(instanceName) {
6836
5548
  return {
5549
+ quota_notice: this.classicChannels?.getQuotaNoticeByInstance?.(instanceName) ?? null,
6837
5550
  reply_triggers: this.classicChannels?.getReplyTriggersByInstance(instanceName, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS,
6838
5551
  tool_progress: this.classicChannels?.getToolProgressByInstance(instanceName, this.fleetConfig?.defaults?.tool_progress) ?? "off",
6839
5552
  reply_completion_guard: this.classicChannels?.getReplyCompletionGuardByInstance(instanceName, this.fleetConfig?.defaults?.reply_completion_guard) ?? true,
@@ -6985,14 +5698,6 @@ export class FleetManager {
6985
5698
  resumePaused = false,
6986
5699
  /** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
6987
5700
  transition, execution, kiroMigration) {
6988
- if (isNativeExecInstance(config)) {
6989
- execution?.assert();
6990
- if (!windowsCliEnabled())
6991
- throw new Error("Windows runtime is disabled");
6992
- if (resumePaused)
6993
- this.nativeInstanceRuntime().setPaused(name, false);
6994
- return;
6995
- }
6996
5701
  const migration = this.kiroMigrationRuns?.get(name);
6997
5702
  if (migration && migration.token !== kiroMigration)
6998
5703
  throw new Error(t("kiro_migrate.in_progress"));
@@ -7043,29 +5748,27 @@ export class FleetManager {
7043
5748
  if (resumePaused)
7044
5749
  this.lifecycle.markActivitySeedNow(name);
7045
5750
  const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
7046
- // #1490 row 67 (main): off-loop resolve before skill/instruction publication
5751
+ // #1490 row 67: off-loop resolve before skill/instruction publication
7047
5752
  // constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
7048
5753
  // ownership first: a Stop during this wait finishes and bumps the epoch,
7049
5754
  // and the inner lifecycle check would otherwise capture the new epoch and
7050
5755
  // accept the stale start. Lifecycle start looks up again before its own
7051
- // createBackend; the probe cache makes that cheap. (alpha: a native Windows
7052
- // agent's CLI is on that PC — nothing to resolve on this one.)
5756
+ // createBackend; the probe cache makes that cheap.
7053
5757
  const epoch = this.lifecycle.epochOf(name);
7054
5758
  const stopGen = this.explicitStopGeneration.get(name) ?? 0;
7055
- const windows = isWindowsInstance(config);
7056
- const resolved = windows ? null : await resolveBackendBinary(backend);
5759
+ const resolved = await resolveBackendBinary(backend);
7057
5760
  execution?.assert();
7058
5761
  if (this.lifecycle.epochOf(name) !== epoch
7059
5762
  || (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
7060
5763
  throw new SupersededStartError(name);
7061
5764
  }
7062
5765
  const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
7063
- if (binary && !windows)
5766
+ if (binary)
7064
5767
  noteResolvedBinary(binary, resolved ?? binary);
7065
- if (!windows && config.general_topic) {
5768
+ if (config.general_topic) {
7066
5769
  this.ensureGeneralInstructions(config.working_directory, backend, name);
7067
5770
  }
7068
- else if (!isWindowsInstance(config) && kind === "fleet-topic") {
5771
+ else if (kind === "fleet-topic") {
7069
5772
  // Workers get only role-eligible on-demand skills. Classic instances are
7070
5773
  // deliberately excluded: their workspace and conversation lifecycle are
7071
5774
  // managed independently from fleet-topic workers.
@@ -7080,7 +5783,7 @@ export class FleetManager {
7080
5783
  this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
7081
5784
  }
7082
5785
  }
7083
- else if (!isWindowsInstance(config) && kind === "classic") {
5786
+ else if (kind === "classic") {
7084
5787
  // #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
7085
5788
  // coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
7086
5789
  // room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
@@ -7097,7 +5800,7 @@ export class FleetManager {
7097
5800
  }
7098
5801
  }
7099
5802
  const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
7100
- const identity = { kind, backend, model: isWindowsInstance(config) ? config.model || "default" : this.resolveInstanceModel(name).display };
5803
+ const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
7101
5804
  if (receipt) {
7102
5805
  await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
7103
5806
  // Only this transition's actual publication can acquire its replacement.
@@ -7167,7 +5870,7 @@ export class FleetManager {
7167
5870
  // them before grouping/staggering: startInstance() retains its own guard as
7168
5871
  // a final backstop, but putting a no-op entry in this queue still consumes a
7169
5872
  // full stagger slot for every distinct working directory.
7170
- const runnableEntries = entries.filter(([name, config]) => !isNativeExecInstance(config) && !this.lifecycle.isPaused(name));
5873
+ const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
7171
5874
  // ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
7172
5875
  // only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
7173
5876
  const fleetNames = new Set(entries.map(([name]) => name));
@@ -7233,8 +5936,6 @@ export class FleetManager {
7233
5936
  return false;
7234
5937
  if (err instanceof SessionOomStoppedError)
7235
5938
  await this.notifySessionOomStop(name);
7236
- if (this.holdNativePaneRecovery(name, err))
7237
- return false;
7238
5939
  if (this.stopOnUnsupportedCli(name, err, what))
7239
5940
  return false;
7240
5941
  this.logger.error({ err, name }, `Failed to start ${what}`);
@@ -7376,8 +6077,6 @@ export class FleetManager {
7376
6077
  if (err instanceof SupersededStartError)
7377
6078
  return;
7378
6079
  // An earlier attempt could not tell (unknown probe); this one could.
7379
- if (this.holdNativePaneRecovery(name, err))
7380
- return;
7381
6080
  if (this.stopOnUnsupportedCli(name, err, "instance"))
7382
6081
  return;
7383
6082
  this.logger.error({ err, name, attempt: attempt + 1 }, "Automatic startup retry failed");
@@ -7522,6 +6221,17 @@ export class FleetManager {
7522
6221
  this.queueKiroIncompatNotice(name, err.message);
7523
6222
  return true;
7524
6223
  }
6224
+ /** No pane is needed for a compatibility refusal to remain in Needs you. */
6225
+ setKiroCompatibilityHold(name, message) {
6226
+ const holds = this.kiroCompatibilityHolds ??= new Map();
6227
+ if ((message === null && !holds.has(name)) || (message !== null && holds.get(name)?.message === message))
6228
+ return;
6229
+ if (message === null)
6230
+ holds.delete(name);
6231
+ else
6232
+ holds.set(name, { message, since: Date.now() });
6233
+ this.needsYou?.poke();
6234
+ }
7525
6235
  /**
7526
6236
  * LifecycleContext: a kiro instance the installed kiro-cli cannot run as
7527
6237
  * configured (#1109) — at start, or at a respawn after kiro-cli replaced
@@ -7628,7 +6338,7 @@ export class FleetManager {
7628
6338
  return count;
7629
6339
  }
7630
6340
  configuredStartupInstanceNames(fleet, includeClassic) {
7631
- const names = new Set(Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config)).map(([name]) => name));
6341
+ const names = new Set(Object.keys(fleet.instances));
7632
6342
  if (includeClassic) {
7633
6343
  for (const channel of this.classicChannels?.getAll() ?? [])
7634
6344
  names.add(channel.instanceName);
@@ -7679,10 +6389,6 @@ export class FleetManager {
7679
6389
  return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
7680
6390
  }
7681
6391
  async stopInstance(name, transition) {
7682
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
7683
- this.nativeInstanceRuntime().setPaused(name, true);
7684
- return { backgroundWorkEnded: false };
7685
- }
7686
6392
  this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
7687
6393
  this.cancelStartupRetry(name);
7688
6394
  this.failoverActive.delete(name);
@@ -7722,16 +6428,6 @@ export class FleetManager {
7722
6428
  async restartSingleInstance(name, opts) {
7723
6429
  if (opts?.ownerCurrent && !opts.ownerCurrent())
7724
6430
  throw new RestartSupersededError(this.shownInstanceName(name));
7725
- if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
7726
- const runtime = this.nativeInstanceRuntime();
7727
- if (runtime.get(name)?.blocked)
7728
- throw new Error("Verify the disconnected Windows process stopped and recover this agent before restarting");
7729
- runtime.cancel(name);
7730
- runtime.setPaused(name, false);
7731
- return;
7732
- }
7733
- if (opts?.explicit && isNativePaneInstance(this.fleetConfig?.instances[name]))
7734
- this.lifecycle.resetNativeCrashBudget(name);
7735
6431
  const migration = this.kiroMigrationRuns?.get(name);
7736
6432
  if (migration && migration.token !== opts?.kiroMigration)
7737
6433
  throw new Error(t("kiro_migrate.in_progress"));
@@ -7795,7 +6491,7 @@ export class FleetManager {
7795
6491
  return run;
7796
6492
  }
7797
6493
  isInstanceRestarting(name) {
7798
- return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true || this.lifecycle?.nativeRecoveryPending?.(name) === true;
6494
+ return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
7799
6495
  }
7800
6496
  /** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
7801
6497
  restartInFlight(name) {
@@ -8090,9 +6786,6 @@ export class FleetManager {
8090
6786
  log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
8091
6787
  }
8092
6788
  publicOwnerCurrent(owner) {
8093
- // #3008: the outside link is this computer's admin's — while "Allow connections from outside" is on, web-only.
8094
- if (isOutsideLinkOwner(owner))
8095
- return !this.shuttingDown && this.outsideOn();
8096
6789
  if (this.shuttingDown || !this.hasFleetAdmins(owner.adapterId) || !this.isFleetAdmin(owner.userId, owner.adapterId))
8097
6790
  return false;
8098
6791
  const configured = (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).find(ch => (ch.id ?? ch.type) === owner.adapterId);
@@ -8182,11 +6875,7 @@ export class FleetManager {
8182
6875
  }
8183
6876
  getPublicWebLink() {
8184
6877
  this.initializeWebSessions();
8185
- return this.publicWebLink ??= new PublicWebLink(this.publicWebLinkDeps());
8186
- }
8187
- /** The public link's wiring to this fleet (a test builds one with the tunnel itself stubbed: `ensure`, `provider`). */
8188
- publicWebLinkDeps() {
8189
- return {
6878
+ return this.publicWebLink ??= new PublicWebLink({
8190
6879
  dataDir: this.dataDir, web: () => this.fleetConfig?.web,
8191
6880
  permitted: owner => this.publicOwnerCurrent(owner),
8192
6881
  reserve: id => this.getTunnelLane().reserve("dashboard", id),
@@ -8219,9 +6908,7 @@ export class FleetManager {
8219
6908
  if (world && world.adapter === owner.binding)
8220
6909
  void world.adapter.sendText(owner.chatId, t("dashboard.public_cleanup"), { threadId: owner.threadId }).catch(() => { });
8221
6910
  },
8222
- onClose: (owner, reason) => { if (isOutsideLinkOwner(owner))
8223
- this.outsideLinkClosed(reason); },
8224
- };
6911
+ });
8225
6912
  }
8226
6913
  publicLinkAutoOpen = null;
8227
6914
  /**
@@ -9580,9 +8267,6 @@ export class FleetManager {
9580
8267
  const fleet = this.loadConfig(configPath, { startup: true });
9581
8268
  this.slimFleetConfigAtStartup();
9582
8269
  this.logConfigWarningsAtStartup();
9583
- this.initializeNativeInstances();
9584
- // M-2255: from here on a change to the Windows runtimes' switches applies live (observeRuntimeSwitches).
9585
- this.runtimeEffects = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
9586
8270
  setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
9587
8271
  // #1737 review: the marker this boot adopts — the startup clear may only ever clear this one.
9588
8272
  const bootUpdateMarker = updateMarkerIdentity(this.dataDir);
@@ -9605,8 +8289,20 @@ export class FleetManager {
9605
8289
  // #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
9606
8290
  const topicMode = daemonTopicMode(fleet);
9607
8291
  // Set tmux socket isolation for custom AGEND_HOME
9608
- const { getTmuxSocketName: getSocket } = await import("./paths.js");
8292
+ const { getTmuxSocketName: getSocket, isDefaultAgendHome } = await import("./paths.js");
9609
8293
  TmuxManager.setSocketName(getSocket());
8294
+ // M-2457 §7: after a raw downgrade from 2.4, its own tmux server may still run the agents — start none beside it.
8295
+ // main / the 2.3 line ONLY: on the 2.4 line (release/2.3.5-alpha) AgEnD's own server IS the one on `agend` — a
8296
+ // main → alpha sync must keep alpha's side of this block, never both.
8297
+ {
8298
+ const { newerTmuxServer, realGuardDeps, guardMessage } = await import("./bundled-tmux-guard.js");
8299
+ const hit = await newerTmuxServer(realGuardDeps(getAgendHome(), isDefaultAgendHome(), getSocket()));
8300
+ if (hit) {
8301
+ const message = guardMessage(hit);
8302
+ this.logger.error({ socket: hit.socket, confirmed: hit.confirmed }, message);
8303
+ throw new Error(message);
8304
+ }
8305
+ }
9610
8306
  await TmuxManager.ensureSession(getTmuxSession());
9611
8307
  this.tmuxSessionEnsured = true;
9612
8308
  // #2072: what this host can run — probed in the background, published for every picker.
@@ -9614,7 +8310,7 @@ export class FleetManager {
9614
8310
  // Pre-flight (advisory, fire-and-forget): warm the per-backend auth cache
9615
8311
  // before instances spawn, so a CLI that comes up on its sign-in screen gets
9616
8312
  // an instant 🔑 verdict instead of decaying into crash/MCP-died noise.
9617
- this.lifecycle.primeAuthVerification(Object.values(fleet.instances).filter(config => !isWindowsInstance(config)).map(config => {
8313
+ this.lifecycle.primeAuthVerification(Object.values(fleet.instances).map(config => {
9618
8314
  const backend = config.backend ?? fleet.defaults?.backend ?? "claude-code";
9619
8315
  // #1699 follow-up: each instance's own login — a profile is probed with its own environment.
9620
8316
  return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, fleet.defaults, backend) : null };
@@ -9676,7 +8372,6 @@ export class FleetManager {
9676
8372
  // #1565: the web chat as it was before this process — the configured instances' only (fleet.yaml and ClassicBot
9677
8373
  // rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
9678
8374
  this.restoreWebChat(fleet);
9679
- this.initializeNativeInstances(true);
9680
8375
  this.startSearchIndex(fleet);
9681
8376
  // Parse-only source monitoring; it does not apply hand-edited Fleet settings.
9682
8377
  this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
@@ -9859,7 +8554,7 @@ export class FleetManager {
9859
8554
  }
9860
8555
  }
9861
8556
  // Phase 1: Start general instances first and wait for them
9862
- const allEntries = Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config));
8557
+ const allEntries = Object.entries(fleet.instances);
9863
8558
  const generals = allEntries.filter(([_, cfg]) => cfg.general_topic);
9864
8559
  const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
9865
8560
  const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
@@ -9973,6 +8668,9 @@ export class FleetManager {
9973
8668
  if (others.length > 0 || classicAtBoot.length > 0) {
9974
8669
  await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
9975
8670
  }
8671
+ // #3066 §1d (Fable r1): on every fleet — a web-only one (#2910) has no chat connection at all, and is the clearest
8672
+ // case with no Discord bot reading. Each tick decides for itself whether to read (backgroundQuotaRead).
8673
+ this.startQuotaBackgroundReads();
9976
8674
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
9977
8675
  await adapterStartup;
9978
8676
  this.startDiscordUsagePresence();
@@ -10098,10 +8796,6 @@ export class FleetManager {
10098
8796
  // #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
10099
8797
  this.startPublicLinkAutoOpen();
10100
8798
  }
10101
- // #3008: connections from outside were on when the fleet stopped: the link opens again (its address is new for a
10102
- // quick link — Settings shows it on this computer).
10103
- if (this.outsideOn())
10104
- this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
10105
8799
  // #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
10106
8800
  // #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
10107
8801
  void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
@@ -10148,6 +8842,214 @@ export class FleetManager {
10148
8842
  this.logger.warn({ health }, "Fleet started with problems — see /health");
10149
8843
  }
10150
8844
  }
8845
+ /**
8846
+ * #3066 §1d (the user: yes, every 30 min): the quota history (usage/quota-history.ts) records only the readings AgEnD
8847
+ * makes. A Discord bot's presence refresh reads every 15 min; without one, nothing reads while nobody looks. So, with no
8848
+ * Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
8849
+ * still bounds the vendor calls), recorded like any other.
8850
+ */
8851
+ quotaNotices = null;
8852
+ stopQuotaObserver = null;
8853
+ quotaNoticePolicy() {
8854
+ const defaults = this.fleetConfig?.defaults.quota_notice;
8855
+ const wanted = defaults?.target ?? "general";
8856
+ const instances = this.fleetConfig?.instances ?? {};
8857
+ const target = wanted === "general"
8858
+ ? Object.entries(instances).find(([name, c]) => c.general_topic === true || name === "general")?.[0]
8859
+ : Object.hasOwn(instances, wanted) ? wanted : undefined;
8860
+ const webOnly = !!this.webOnlyPromptPlace();
8861
+ const config = target ? instances[target] : undefined;
8862
+ const adapterId = target ? this.getInstanceAdapterId(target) : undefined;
8863
+ const groupId = target ? this.getChannelConfig(adapterId)?.group_id : undefined;
8864
+ const ready = !!target && (webOnly || (config?.topic_id != null && !!groupId && !!(this.getAdapterForInstance(target) ?? this.adapter)));
8865
+ return {
8866
+ target: this.shuttingDown ? "" : ready ? target : !target && wanted === "general" && webOnly ? "/notices" : "",
8867
+ destinationBinding: JSON.stringify([webOnly, adapterId, groupId, config?.topic_id]),
8868
+ locale: this.fleetConfig?.defaults.locale ?? getLocale(),
8869
+ subscriptions: this.shuttingDown ? [] : this.activeBackendBindings().flatMap(([name, backend, profile]) => {
8870
+ const provider = usageProviderIdForBackend(backend);
8871
+ if (!provider)
8872
+ return [];
8873
+ const providerId = profile ? `${provider}:${profile}` : provider;
8874
+ return [{ account: `${backend}:host:${profile ?? ""}`, providerId,
8875
+ rule: quotaNoticeRule(defaults, backend, instances[name]?.quota_notice ?? this.classicChannels?.getQuotaNoticeByInstance?.(name)) }];
8876
+ }),
8877
+ };
8878
+ }
8879
+ startQuotaBackgroundReads() {
8880
+ if (!this.quotaNotices) {
8881
+ this.quotaNotices = new QuotaNotices({
8882
+ file: join(this.dataDir, "quota-notices.json"), policy: () => this.quotaNoticePolicy(),
8883
+ send: (target, text) => target === "/notices" ? this.recordWebNotice("fleet", text) : this.notifyInstanceTopic(target, text),
8884
+ warn: err => this.logger.warn({ err }, "Optional quota reminders held: could not retain dispatch state"),
8885
+ });
8886
+ this.stopQuotaObserver = onUsageSnapshot(payload => { void this.quotaNotices?.observe(payload); });
8887
+ }
8888
+ if (this.quotaReadTimer)
8889
+ clearInterval(this.quotaReadTimer);
8890
+ this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
8891
+ this.quotaReadTimer.unref?.();
8892
+ // #3066 §1: the measured capacity, published for the usage reads (get_usage, the page) — refreshed every 10 min.
8893
+ setCapacityLookup((provider, metric) => this.capacityEstimates.get(`${provider}\u0000${metric}`) ?? null);
8894
+ if (this.capacityTimer)
8895
+ clearInterval(this.capacityTimer);
8896
+ void this.refreshCapacity();
8897
+ this.capacityTimer = setInterval(() => { void this.refreshCapacity(); }, FleetManager.CAPACITY_REFRESH_MS);
8898
+ this.capacityTimer.unref?.();
8899
+ }
8900
+ /** #3066 §1: the latest estimates, by `provider\0metric` (what usage-api's lookup reads, synchronously). */
8901
+ capacityEstimates = new Map();
8902
+ capacityTimer = null;
8903
+ static CAPACITY_REFRESH_MS = 10 * 60_000;
8904
+ /**
8905
+ * #3066 §1: estimate each account's weekly 100% from the quota history (§1d) and the usage ledger's hours — both read
8906
+ * asynchronously, the arithmetic small (usage/capacity.ts). An account is its usage provider id ("claude",
8907
+ * "claude:hsinlin", "codex:yanxianchen"): its hours are the ledger sources of that backend under that credential profile
8908
+ * (Codex's "unrecorded" hours are no account's). A per-model weekly ("Sonnet (weekly)") counts only that model's tokens.
8909
+ *
8910
+ * Prism #3073: no estimate rather than a wrong one —
8911
+ * - a window is measured only where the ledger has its hours: kept (31 days) and scanned (`days.coverage`) for every
8912
+ * instance of the account's backend, and every instance whose hours count for it; the quota history goes back 12 weeks;
8913
+ * - a profile literally named "default", "unknown" or "unrecorded" (configured, on disk, or in the quota history) makes
8914
+ * those hours ambiguous: neither the host
8915
+ * account nor the named one is estimated (each would be credited the other's use, or lose its own);
8916
+ * - only a metric the vendor marks per-model (`scope: "model"`) is filtered by model; another named weekly pool
8917
+ * ("Luna Reserve (weekly)", Antigravity pools) is no model name and is not estimated.
8918
+ */
8919
+ async refreshCapacity() {
8920
+ try {
8921
+ const readings = await readQuotaHistory();
8922
+ const groups = new Map();
8923
+ for (const r of readings) {
8924
+ const k = `${r.provider}\u0000${r.metric}`;
8925
+ (groups.get(k) ?? groups.set(k, []).get(k)).push(r);
8926
+ }
8927
+ if (!groups.size) {
8928
+ this.capacityEstimates = new Map();
8929
+ return 0;
8930
+ }
8931
+ const data = await this.cacheServiceFor().usageData();
8932
+ const now = Date.now(), DAY = 24 * 3_600_000, HOUR = 3_600_000;
8933
+ const keptFrom = now - KEEP_DAYS * DAY + HOUR; // the oldest hour still in every ledger (an hour of margin)
8934
+ // Profiles whose names are also the ledger's own tags: their hours cannot be told from the host's / unattributed.
8935
+ // Any trace of such a profile counts (Prism #3073 r2), not only today's config — a binding removed or switched
8936
+ // leaves its hours in the ledger: configured now, its directory on this host, or its account in the quota history.
8937
+ const RESERVED = new Set(["default", "unknown", PROFILE_UNRECORDED]);
8938
+ const historyProviders = new Set(readings.map((r) => r.provider));
8939
+ const reservedNamed = new Map();
8940
+ const reservedFor = (backend) => {
8941
+ const known = reservedNamed.get(backend);
8942
+ if (known)
8943
+ return known;
8944
+ const found = new Set(listConfiguredProfiles(this.fleetConfig, backend).filter((n) => RESERVED.has(n)));
8945
+ const base = usageProviderIdForBackend(backend);
8946
+ for (const n of RESERVED) {
8947
+ if (base && historyProviders.has(`${base}:${n}`))
8948
+ found.add(n);
8949
+ if (credentialHomeSpec(backend) && existsSync(credentialProfileHome(this.dataDir, backend, n)))
8950
+ found.add(n);
8951
+ }
8952
+ return reservedNamed.set(backend, found).get(backend);
8953
+ };
8954
+ // Every instance of a backend may have used any of its accounts (bindings change): an account is measured only
8955
+ // where all of them were scanned (Prism #3073 r2) — one still waiting for its first scan has no hours yet, not none.
8956
+ const daysOfBase = new Map();
8957
+ for (const inst of data.instances) {
8958
+ const base = usageProviderIdForBackend(inst.inst.backend);
8959
+ if (base)
8960
+ (daysOfBase.get(base) ?? daysOfBase.set(base, []).get(base)).push(inst.days);
8961
+ }
8962
+ // provider id → hour → model key → totals; and the instances whose hours count for it (their scan coverage).
8963
+ const byProvider = new Map();
8964
+ const daysOf = new Map();
8965
+ for (const inst of data.instances) {
8966
+ for (const [hk, hour] of Object.entries(inst.ledger.hours)) {
8967
+ for (const src of hourSources(hour)) {
8968
+ const base = usageProviderIdForBackend(src.backend);
8969
+ if (!base)
8970
+ continue;
8971
+ // Ambiguous (a named profile with the tag's name), or no account's: counted for no one, so an account whose
8972
+ // hours these may be has none — and gets no estimate, never a low one.
8973
+ if (reservedFor(src.backend).has(src.profile) || src.profile === "unknown" || src.profile === PROFILE_UNRECORDED)
8974
+ continue;
8975
+ const provider = src.profile === "default" ? base : `${base}:${src.profile}`;
8976
+ const list = daysOf.get(provider) ?? daysOf.set(provider, []).get(provider);
8977
+ if (!list.includes(inst.days))
8978
+ list.push(inst.days);
8979
+ const hours = byProvider.get(provider) ?? byProvider.set(provider, new Map()).get(provider);
8980
+ const into = hours.get(Number(hk)) ?? hours.set(Number(hk), Object.create(null)).get(Number(hk));
8981
+ for (const [mk, tot] of Object.entries(src.m)) {
8982
+ const cur = into[mk] ??= [0, 0, 0, 0, 0, 0, 0];
8983
+ for (let i = 0; i < 7; i++)
8984
+ cur[i] = (cur[i] ?? 0) + (tot[i] ?? 0);
8985
+ }
8986
+ }
8987
+ }
8988
+ }
8989
+ const next = new Map();
8990
+ for (const [key, list] of groups) {
8991
+ const [provider, metric] = key.split("\u0000");
8992
+ // A per-model weekly ("Sonnet (weekly)", the vendor's scope "model"): that model's tokens only. Another named
8993
+ // pool is not a model: no estimate. "Weekly", "Weekly limit": everything.
8994
+ const named = /^(.+) \((?:weekly|monthly|\d+d)\)$/i.exec(metric)?.[1]?.toLowerCase() ?? null;
8995
+ const perModel = list.some((r) => r.scope === "model");
8996
+ if (named && !perModel)
8997
+ continue;
8998
+ const hours = [];
8999
+ for (const [hour, models] of byProvider.get(provider) ?? []) {
9000
+ let api = 0, output = 0, all = 0;
9001
+ for (const [mk, tot] of Object.entries(models)) {
9002
+ const bar = mk.lastIndexOf("|"), model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
9003
+ if (named && !model.toLowerCase().includes(named))
9004
+ continue;
9005
+ const [, uncached, read, w5, w1h, w, out] = tot;
9006
+ const r = ratesFor(model, long ? Number.MAX_SAFE_INTEGER : 0);
9007
+ api = api === null || !r ? null : api + (uncached * r.input + read * r.read + w5 * r.write5m + w1h * r.write1h + w * r.write + out * r.output) / 1_000_000;
9008
+ output += out;
9009
+ all += uncached + read + w5 + w1h + w + out;
9010
+ }
9011
+ if (all > 0)
9012
+ hours.push({ hour, api, output, all });
9013
+ }
9014
+ const base = provider.split(":")[0];
9015
+ const scanned = [...new Set([...(daysOf.get(provider) ?? []), ...(daysOfBase.get(base) ?? [])])];
9016
+ const available = (from, to) => {
9017
+ if (from < keptFrom || !scanned.length)
9018
+ return false;
9019
+ for (let h = from; h <= to; h += HOUR)
9020
+ if (!scanned.every((d) => covered(d, h)))
9021
+ return false;
9022
+ return true;
9023
+ };
9024
+ const est = estimateCapacity(list, hours, available);
9025
+ if (est)
9026
+ next.set(key, est);
9027
+ }
9028
+ this.capacityEstimates = next;
9029
+ return next.size;
9030
+ }
9031
+ catch (err) {
9032
+ this.logger.debug({ err }, "capacity estimate refresh failed");
9033
+ return -1;
9034
+ }
9035
+ }
9036
+ /** One background quota read, when nothing else reads (no Discord bot) and an account has a running agent. */
9037
+ async backgroundQuotaRead() {
9038
+ if (this.shuttingDown)
9039
+ return false;
9040
+ if ([...this.adapters.values()].some(a => a.type === "discord" && typeof a.setActivity === "function"))
9041
+ return false;
9042
+ const active = this.getActiveUsageProviderIds();
9043
+ if (active.size === 0)
9044
+ return false;
9045
+ try {
9046
+ await getUsageSnapshot(false, active);
9047
+ return true;
9048
+ }
9049
+ catch {
9050
+ return false;
9051
+ }
9052
+ }
10151
9053
  /** Keep Discord profile activity aligned with the same cached usage source as /usage. */
10152
9054
  startDiscordUsagePresence() {
10153
9055
  if (this.discordPresenceTimer)
@@ -10623,6 +9525,11 @@ export class FleetManager {
10623
9525
  adapter.on("callback_query", safeHandler(async (data) => {
10624
9526
  await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
10625
9527
  }, this.logger, "adapter.callback_query"));
9528
+ adapter.on("missed_offline", (ev) => {
9529
+ if (!isCurrentAdapter())
9530
+ return;
9531
+ this.recordMissedOffline(ev);
9532
+ });
10626
9533
  this.bindTopicClosedHandler(adapter, adapterId, "adapter.topic_closed");
10627
9534
  // Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
10628
9535
  adapter.on("slash_command", safeHandler(async (data) => {
@@ -10776,6 +9683,11 @@ export class FleetManager {
10776
9683
  adapter.on("callback_query", safeHandler(async (data) => {
10777
9684
  await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
10778
9685
  }, this.logger, `adapter[${adapterId}].callback_query`));
9686
+ adapter.on("missed_offline", (ev) => {
9687
+ if (!isCurrentAdapter())
9688
+ return;
9689
+ this.recordMissedOffline(ev);
9690
+ });
10779
9691
  this.bindTopicClosedHandler(adapter, adapterId, `adapter[${adapterId}].topic_closed`);
10780
9692
  // Slash commands: classic bot + admin commands
10781
9693
  adapter.on("slash_command", safeHandler(async (data) => {
@@ -10865,14 +9777,10 @@ export class FleetManager {
10865
9777
  }
10866
9778
  }
10867
9779
  }
10868
- const daemon = this.daemons.get(name);
10869
- const native = daemon?.usesNativePane?.();
10870
9780
  const sockPath = join(this.getInstanceDir(name), "channel.sock");
10871
- if (!native && !existsSync(sockPath))
9781
+ if (!existsSync(sockPath))
10872
9782
  return;
10873
- const ipc = native
10874
- ? new NativeDaemonClient(daemon, () => this.daemons.get(name) === daemon && (!this.instanceIpcClients.has(name) || this.instanceIpcClients.get(name) === ipc))
10875
- : new IpcClient(sockPath);
9783
+ const ipc = new IpcClient(sockPath);
10876
9784
  try {
10877
9785
  await ipc.connect();
10878
9786
  this.instanceIpcClients.set(name, ipc);
@@ -10988,7 +9896,7 @@ export class FleetManager {
10988
9896
  // connected, so seed the cache instead of waiting for another transition.
10989
9897
  ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
10990
9898
  this.logger.debug({ name }, "Connected to instance IPC");
10991
- if (!native && !this.statuslineWatcher.has(name)) {
9899
+ if (!this.statuslineWatcher.has(name)) {
10992
9900
  this.statuslineWatcher.watch(name);
10993
9901
  }
10994
9902
  // Auto-reconnect on disconnect (unless intentionally stopping)
@@ -11516,6 +10424,34 @@ export class FleetManager {
11516
10424
  }
11517
10425
  return true;
11518
10426
  }
10427
+ /** File a Telegram update that was too old to inject; Needs you lists it until Acknowledge. */
10428
+ recordMissedOffline(ev) {
10429
+ if (!this.missedOfflineStore)
10430
+ this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
10431
+ const stub = {
10432
+ source: "telegram", adapterId: ev.adapterId, chatId: ev.chatId, threadId: ev.threadId,
10433
+ messageId: ev.messageId, userId: ev.userId, username: ev.username, text: ev.preview,
10434
+ timestamp: ev.timestamp, isBotMessage: false,
10435
+ };
10436
+ const threadId = this.inboundRouteThreadId(stub);
10437
+ const target = threadId ? this.resolveInboundTarget(stub, threadId) : undefined;
10438
+ let instance = target?.name ?? "";
10439
+ if (!instance) {
10440
+ instance = Object.entries(this.fleetConfig?.instances ?? {}).find(([name, cfg]) => cfg.general_topic && this.getInstanceAdapterId(name) === ev.adapterId)?.[0] ?? "";
10441
+ }
10442
+ const place = [ev.chatTitle, ev.threadId ? `#${ev.threadId}` : ""].filter(Boolean).join(" · ") || ev.chatId;
10443
+ this.missedOfflineStore.add({
10444
+ id: `missed:${ev.adapterId}:${ev.updateId}`,
10445
+ adapterId: ev.adapterId,
10446
+ instance,
10447
+ username: ev.username,
10448
+ place,
10449
+ preview: ev.preview,
10450
+ since: ev.timestamp.getTime(),
10451
+ });
10452
+ this.logger.info({ adapterId: ev.adapterId, updateId: ev.updateId, instance, chatId: ev.chatId, threadId: ev.threadId }, "Telegram message missed while AgEnD was offline — filed for Needs you");
10453
+ this.needsYou?.poke();
10454
+ }
11519
10455
  /**
11520
10456
  * #1085: coincident topic numbers must not route across Telegram groups.
11521
10457
  */
@@ -12035,12 +10971,6 @@ export class FleetManager {
12035
10971
  if (msg.adapterId)
12036
10972
  this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
12037
10973
  const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
12038
- const locationHold = this.generalLocationDeliveryHold(generalInstance);
12039
- if (locationHold) {
12040
- await inboundAdapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
12041
- this.needsYou?.poke();
12042
- return;
12043
- }
12044
10974
  // React immediately — before any other API calls — through the status
12045
10975
  // path, which tracks Telegram slot ownership and Discord status adds.
12046
10976
  if (msg.chatId && msg.messageId) {
@@ -12128,13 +11058,6 @@ export class FleetManager {
12128
11058
  if (instanceConfig?.general_topic && await this.topicCommands.handleGeneralCommand(msg, instanceName)) {
12129
11059
  return;
12130
11060
  }
12131
- const locationHold = this.generalLocationDeliveryHold(instanceName);
12132
- if (locationHold) {
12133
- const adapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
12134
- await adapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
12135
- this.needsYou?.poke();
12136
- return;
12137
- }
12138
11061
  // Bind instance to the adapter that delivered this message
12139
11062
  if (msg.adapterId)
12140
11063
  this.bindInstanceAdapter(instanceName, msg.adapterId, true);
@@ -13152,6 +12075,11 @@ export class FleetManager {
13152
12075
  let result;
13153
12076
  switch (msg.type) {
13154
12077
  case "fleet_decision_create": {
12078
+ const supersedes = this.decisionSupersedes("ipc-typed", instanceName, payload.supersedes);
12079
+ if ("error" in supersedes) {
12080
+ ipc.send({ type: "fleet_decision_response", fleetRequestId, error: supersedes.error });
12081
+ return;
12082
+ }
13155
12083
  // Prune expired decisions on create
13156
12084
  db.pruneExpiredDecisions();
13157
12085
  result = db.createDecision({
@@ -13162,7 +12090,7 @@ export class FleetManager {
13162
12090
  tags: payload.tags,
13163
12091
  ttl_days: payload.ttl_days,
13164
12092
  created_by: instanceName,
13165
- supersedes: payload.supersedes,
12093
+ supersedes: supersedes.id,
13166
12094
  });
13167
12095
  break;
13168
12096
  }
@@ -13339,6 +12267,21 @@ export class FleetManager {
13339
12267
  return "ok";
13340
12268
  }
13341
12269
  }
12270
+ /**
12271
+ * `post_decision` with `supersedes` changes a decision someone else may have recorded (it stops being active): the
12272
+ * same permission as `update_decision`, asked through the same live check (tool set + dashboard switches), on both
12273
+ * create paths. An empty or absent value supersedes nothing and needs nothing; anything else must be an id.
12274
+ */
12275
+ decisionSupersedes(sink, instanceName, value) {
12276
+ if (value === undefined || value === null || value === "")
12277
+ return { id: undefined };
12278
+ if (typeof value !== "string")
12279
+ return { error: "supersedes must be a decision id" };
12280
+ const permitted = this.checkToolPermission(sink, instanceName, "update_decision");
12281
+ if (!permitted.allowed)
12282
+ return { error: `Superseding a decision needs update_decision. ${permitted.message}` };
12283
+ return { id: value };
12284
+ }
13342
12285
  async handleDecisionCrudHttp(instance, op, args) {
13343
12286
  if (!this.scheduler)
13344
12287
  return { error: "Scheduler not available" };
@@ -13354,16 +12297,24 @@ export class FleetManager {
13354
12297
  if (!title || !content)
13355
12298
  return { error: "title and content are required" };
13356
12299
  const scope = args.scope === "fleet" ? "fleet" : "project";
13357
- return db.createDecision({
13358
- project_root: projectRoot,
13359
- scope,
13360
- title,
13361
- content,
13362
- tags: asStrArr(args.tags),
13363
- ttl_days: asNum(args.ttl_days),
13364
- supersedes: asStr(args.supersedes),
13365
- created_by: instance,
13366
- });
12300
+ const supersedes = this.decisionSupersedes("agent-endpoint", instance, args.supersedes);
12301
+ if ("error" in supersedes)
12302
+ return { error: supersedes.error };
12303
+ try {
12304
+ return db.createDecision({
12305
+ project_root: projectRoot,
12306
+ scope,
12307
+ title,
12308
+ content,
12309
+ tags: asStrArr(args.tags),
12310
+ ttl_days: asNum(args.ttl_days),
12311
+ supersedes: supersedes.id,
12312
+ created_by: instance,
12313
+ });
12314
+ }
12315
+ catch (err) {
12316
+ return { error: err.message }; // a refused predecessor: nothing was inserted
12317
+ }
13367
12318
  }
13368
12319
  // #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
13369
12320
  // The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
@@ -13406,6 +12357,8 @@ export class FleetManager {
13406
12357
  const asStatusFilter = normalizeStatusFilter;
13407
12358
  // #1336: small write ack — identifying fields only.
13408
12359
  const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
12360
+ // #3195: an update's ack also carries what was SAVED for each field it named, read back from the task.
12361
+ const updatedAck = (t, named) => ({ ...ack(t), ...savedFields(t, named) });
13409
12362
  // The board's notes for the write (a send-back reason, a comment the assignee had not seen): with the ack when any.
13410
12363
  const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
13411
12364
  try {
@@ -13463,12 +12416,12 @@ export class FleetManager {
13463
12416
  const id = asStr(args.id);
13464
12417
  if (!id)
13465
12418
  return { error: "id is required" };
13466
- return noted(ack(db.updateTask(db.getTaskByPrefix(id).id, {
12419
+ return noted(updatedAck(db.updateTask(db.getTaskByPrefix(id).id, {
13467
12420
  status: asStatus(args.status),
13468
12421
  assignee: asStr(args.assignee),
13469
12422
  result: asStr(args.result),
13470
12423
  priority: asPriority(args.priority),
13471
- }, { kind: "instance", name: instance })));
12424
+ }, { kind: "instance", name: instance }), args));
13472
12425
  }
13473
12426
  default: return { error: `Unknown task action: ${action}` };
13474
12427
  }
@@ -13622,7 +12575,8 @@ export class FleetManager {
13622
12575
  result: payload.result,
13623
12576
  priority: payload.priority,
13624
12577
  }, { kind: "instance", name: meta.instance_name || instanceName });
13625
- result = noted(ack(logTask));
12578
+ // #3195: with what was SAVED for each field the update named, read back from the task.
12579
+ result = noted({ ...ack(logTask), ...savedFields(logTask, payload) });
13626
12580
  break;
13627
12581
  default:
13628
12582
  throw new Error(`Unknown task action: ${action}`);
@@ -13652,16 +12606,89 @@ export class FleetManager {
13652
12606
  }
13653
12607
  return adapter.createTopic(topicName);
13654
12608
  }
13655
- async deleteForumTopic(topicId, adapterId) {
13656
- try {
13657
- const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
13658
- if (!adapter?.deleteTopic)
13659
- return;
13660
- await adapter.deleteTopic(topicId);
13661
- }
13662
- catch (err) {
13663
- this.logger.warn({ err, topicId }, "Failed to delete forum topic during rollback");
13664
- }
12609
+ /**
12610
+ * Delete an agent and, unless asked not to, its own chat topic (user decision: by default). The topic is planned
12611
+ * before the removal (its id and bot go with the fleet.yaml entry) and deleted through the agent's OWN bot after it;
12612
+ * the outcome is the platform's answer (instance-topic-delete.ts). A topic that cannot go never fails the removal.
12613
+ */
12614
+ async removeInstanceAndTopic(name, authorization, opts) {
12615
+ const plan = opts.deleteTopic ? this.planInstanceTopicDelete(name) : null;
12616
+ if (opts.execution)
12617
+ await this.removeInstance(name, authorization, opts.execution);
12618
+ else
12619
+ await this.removeInstance(name, authorization);
12620
+ // #3055 review (Prism P1): asked again right before the platform call — the fleet as it is NOW, not as planned
12621
+ // before the removal was awaited (a new owner of the topic, or the bot rebuilt).
12622
+ return deletePlannedTopic(plan, this.logger, (p) => {
12623
+ if (this.instanceTopicAdapter(p.adapterId) !== p.adapter)
12624
+ return "the agent's bot changed while it was being deleted, so its topic was left";
12625
+ return topicMustStay(p.destination, this.topicOwnership(), null); // the original is gone: every owner now counts
12626
+ }, { begin: (d) => this.topicDeletionsInFlight.add(d), end: (d) => this.topicDeletionsInFlight.delete(d) });
12627
+ }
12628
+ /** #3055: destinations whose platform deletion is running now; a binding to one waits until it is over. */
12629
+ topicDeletionsInFlight = new Set();
12630
+ /** True while a topic with this id is being deleted on any connection (a new binding of it is refused meanwhile). */
12631
+ isTopicDeletionInFlight(topicId) {
12632
+ const tail = `:${topicId}`;
12633
+ return [...this.topicDeletionsInFlight].some(d => d.endsWith(tail));
12634
+ }
12635
+ /**
12636
+ * #3055 review r2 (Prism P1-3): would this instance config bind a topic whose deletion is running now? Compared by
12637
+ * physical destination, so the same thread number in another Telegram group is not held. Settings asks it at commit.
12638
+ */
12639
+ topicBindingInFlight(inst) {
12640
+ if (this.topicDeletionsInFlight.size === 0 || inst.topic_id == null || inst.topic_id === "")
12641
+ return false;
12642
+ const adapterId = typeof inst.channel_id === "string" && inst.channel_id ? inst.channel_id : this.getPrimaryAdapterId();
12643
+ return this.topicDeletionsInFlight.has(this.destinationFor(adapterId, inst.topic_id));
12644
+ }
12645
+ /** Any topic deletion running now (a connection's group or General may not be re-pointed meanwhile). */
12646
+ topicDeletionPending() { return this.topicDeletionsInFlight.size > 0; }
12647
+ /** The physical destination of `topicId` on the bot `adapterId`. */
12648
+ destinationFor(adapterId, topicId) {
12649
+ const world = adapterId ? this.worlds.get(adapterId) : undefined;
12650
+ const platform = world?.adapter?.type ?? this.getChannelConfig(adapterId)?.type;
12651
+ const groupId = world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
12652
+ return topicDestination(platform, groupId, adapterId, topicId);
12653
+ }
12654
+ /** The adapter of the bot `adapterId` names — never a substitute for a bot that is not there. */
12655
+ instanceTopicAdapter(adapterId) {
12656
+ return (adapterId ? this.worlds.get(adapterId)?.adapter : this.worlds.size === 0 ? this.adapter ?? undefined : undefined) ?? undefined;
12657
+ }
12658
+ /**
12659
+ * Topic ownership by physical destination (#3055 review, Prism P1): an instance's topic is where its bot's platform
12660
+ * puts it (a Telegram group + thread, a Discord channel), so two bots in one group, or two instances on one
12661
+ * channel, are seen to share it; and every connection's own General is protected, with or without a General agent.
12662
+ */
12663
+ topicOwnership() {
12664
+ const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
12665
+ const destinationOf = (n) => {
12666
+ const cfg = this.fleetConfig?.instances?.[n];
12667
+ if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
12668
+ return null;
12669
+ return this.destinationFor(this.getInstanceAdapterId(n), cfg.topic_id);
12670
+ };
12671
+ const generalDestinations = () => {
12672
+ const out = new Set();
12673
+ for (const ch of channels) {
12674
+ const id = ch.id ?? ch.type;
12675
+ const groupId = this.worlds.get(id)?.groupId ?? (ch.group_id != null ? String(ch.group_id) : undefined);
12676
+ if (ch.type === "telegram")
12677
+ out.add(topicDestination("telegram", groupId, id, 1));
12678
+ if (ch.type === "discord") {
12679
+ const gcid = discordGeneralChannelId(ch);
12680
+ if (gcid)
12681
+ out.add(topicDestination("discord", groupId, id, gcid));
12682
+ }
12683
+ }
12684
+ return out;
12685
+ };
12686
+ return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
12687
+ }
12688
+ /** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
12689
+ planInstanceTopicDelete(name) {
12690
+ const adapterId = this.getInstanceAdapterId(name);
12691
+ return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
13665
12692
  }
13666
12693
  getForumTopicDeleter(adapterId) {
13667
12694
  const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
@@ -14062,9 +13089,9 @@ export class FleetManager {
14062
13089
  Object.assign(current, resolved);
14063
13090
  }
14064
13091
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
13092
+ this.agentCredentialsCache?.observePolicies();
14065
13093
  this.refreshSavedDashboardPermissions();
14066
13094
  this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
14067
- this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
14068
13095
  }
14069
13096
  /** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
14070
13097
  refreshSavedDashboardPermissions() {
@@ -14298,43 +13325,10 @@ export class FleetManager {
14298
13325
  }
14299
13326
  getSettingsConfigPath() { return this.configPath; }
14300
13327
  async removeInstance(name, authorization, execution) {
14301
- if (isWindowsInstance(this.fleetConfig?.instances[name])) {
14302
- execution?.assert();
14303
- assertExplicitInstanceRemoval(authorization);
14304
- if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
14305
- await this.stopInstance(name);
14306
- execution?.assert();
14307
- }
14308
- this.nativeInstanceRuntime().remove(name, () => !this.shuttingDown && (!execution || execution.current()), execution);
14309
- return;
14310
- }
14311
13328
  if (execution)
14312
13329
  return this.lifecycle.runTransition(name, transition => this.removeInstanceOwned(name, authorization, execution, transition));
14313
13330
  return this.removeInstanceOwned(name, authorization);
14314
13331
  }
14315
- /** Shared instance metadata goes with a native identity; its Windows workspace and provider sessions stay intact. */
14316
- nativeInstanceRemoved(name) {
14317
- const clean = (what, run) => { try {
14318
- run();
14319
- }
14320
- catch (error) {
14321
- this.logger.warn({ name, err: error }, `Native instance ${what} cleanup failed`);
14322
- } };
14323
- clean("delivery", () => { this.cancelPendingDeliveries(name); });
14324
- forgetInstanceContext(name);
14325
- clean("schedules", () => { for (const schedule of this.scheduler?.list(name) ?? [])
14326
- this.scheduler.delete(schedule.id); });
14327
- this.lastActivity.delete(name);
14328
- this.lastInboundUser.delete(name);
14329
- this.rateLimitWarnedAt.delete(name);
14330
- this.lastInboundMsg.delete(name);
14331
- clean("history", () => this.webChatHistory.forget(name));
14332
- clean("search", () => this.searchIndex?.removeInstance(name));
14333
- clean("files", () => this.webFiles.forget(name));
14334
- clean("compact", () => this.compactWatchService?.forget(name));
14335
- clean("preferences", () => this.webPrefs.forgetInstance(name));
14336
- clean("appearance", () => { this.themeStore.forget(name); this.webAppearance.forget(name); this.appearanceTools.forget(name); this.mods.forgetInstance(name); this.officeActsInstance?.forget(name); this.officePackToolsInstance?.forget(name); });
14337
- }
14338
13332
  async removeInstanceOwned(name, authorization, execution, transition) {
14339
13333
  execution?.assert();
14340
13334
  assertExplicitInstanceRemoval(authorization);
@@ -14460,8 +13454,6 @@ export class FleetManager {
14460
13454
  execution?.complete();
14461
13455
  }
14462
13456
  startStatuslineWatcher(name) {
14463
- if (isWindowsInstance(this.fleetConfig?.instances[name]))
14464
- return;
14465
13457
  if (this.lifecycle.isPaused(name))
14466
13458
  return;
14467
13459
  this.statuslineWatcher.watch(name);
@@ -15910,10 +14902,6 @@ export class FleetManager {
15910
14902
  * would have to make this async and change every caller.
15911
14903
  */
15912
14904
  notifyInstanceTopic(instanceName, text, extraOpts) {
15913
- if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
15914
- this.emitSseEvent("message", { instance: instanceName, role: "status", sender: "AgEnD", text, ts: new Date().toISOString() });
15915
- return true;
15916
- }
15917
14905
  // #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
15918
14906
  if (this.webOnlyPromptPlace())
15919
14907
  return this.recordWebNotice("instance", text, instanceName);
@@ -16394,8 +15382,6 @@ export class FleetManager {
16394
15382
  * (also for a backend that has no profiles, where the option is ignored).
16395
15383
  */
16396
15384
  credentialProfileOf(name, backend) {
16397
- if (isWindowsInstance(this.fleetConfig?.instances[name]))
16398
- return null;
16399
15385
  if (!credentialHomeSpec(backend))
16400
15386
  return null;
16401
15387
  const cfg = this.fleetConfig?.instances?.[name];
@@ -16418,9 +15404,10 @@ export class FleetManager {
16418
15404
  }
16419
15405
  /** The instances the cache analysis reads: fleet.yaml's, then the ClassicBot rooms (their workspace under the AgEnD home, their own backend). */
16420
15406
  cacheInstances() {
16421
- const fleet = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => !isWindowsInstance(cfg)).map(([name, cfg]) => ({
15407
+ const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
16422
15408
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
16423
15409
  claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
15410
+ ...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
16424
15411
  kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
16425
15412
  }));
16426
15413
  const taken = new Set(fleet.map(i => i.name));
@@ -16432,6 +15419,9 @@ export class FleetManager {
16432
15419
  claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
16433
15420
  profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
16434
15421
  kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
15422
+ // #3066 §1d (Fable r1 #2): a ClassicBot Codex room records its launches too (onDaemonReady) — read them the same way.
15423
+ ...((this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend) === "codex"
15424
+ ? { profileTimelinePath: join(this.getInstanceDir(ch.instanceName), PROFILE_TIMELINE_FILE) } : {}),
16435
15425
  }));
16436
15426
  return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
16437
15427
  }
@@ -16592,25 +15582,12 @@ export class FleetManager {
16592
15582
  out.push({
16593
15583
  name, state: p.state ?? undefined,
16594
15584
  interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
16595
- interactionSummary: p.interaction_summary ?? null,
15585
+ interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
15586
+ kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
16596
15587
  pauseReason, pausedAt,
16597
15588
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
16598
15589
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
16599
15590
  usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
16600
- nativeDeliveryHold: this.daemons.get(name)?.nativeDeliveryHold?.() ?? null,
16601
- generalLocation: this.generalLocationAttention(name),
16602
- paneRecovery: (() => {
16603
- const config = this.fleetConfig?.instances[name];
16604
- if (!isNativePaneInstance(config) || !config.native_agent_id)
16605
- return null;
16606
- try {
16607
- const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
16608
- return profile.paneRecovery ? { key: `${profile.paneRecovery.kind}:${profile.paneRecovery.launchId ?? "-"}`, detail: new NativePaneRecoveryError(profile.paneRecovery).message, since: profile.paneRecoveryAt ?? profile.createdAt } : null;
16609
- }
16610
- catch {
16611
- return null;
16612
- }
16613
- })(),
16614
15591
  });
16615
15592
  }
16616
15593
  return out;
@@ -16666,8 +15643,6 @@ export class FleetManager {
16666
15643
  const target = this.fleetNoticeTarget(world);
16667
15644
  return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
16668
15645
  },
16669
- // The name the web page shows for the instance (its View alias, else its own name), never the raw id alone.
16670
- instanceName: instance => resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[instance], profile: profileIdentities(this.dataDir).get(instance) }).display_name || instance,
16671
15646
  instanceTopic: instance => {
16672
15647
  const topic = this.fleetConfig?.instances[instance]?.topic_id;
16673
15648
  if (topic != null)
@@ -16684,6 +15659,10 @@ export class FleetManager {
16684
15659
  throw new Error("the delivery outbox is not open");
16685
15660
  return this.deliveryOutbox.acknowledge(deliveryId, by);
16686
15661
  },
15662
+ missedOffline: () => (this.missedOfflineStore?.list(Date.now()) ?? []).map(m => ({
15663
+ id: m.id, instance: m.instance, owner: m.adapterId, username: m.username, place: m.place, preview: m.preview, since: m.since,
15664
+ })),
15665
+ acknowledgeMissed: (id, by) => this.missedOfflineStore?.acknowledge(id, by) ?? false,
16687
15666
  tasks: () => this.needsYouTaskItems(),
16688
15667
  backendSignIns: () => this.needsYouBackendSignIns(),
16689
15668
  configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
@@ -17212,7 +16191,7 @@ export class FleetManager {
17212
16191
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
17213
16192
  authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
17214
16193
  policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
17215
- hostOperator: this.isWebOnlyFleet(), outside: this.outsideLink(),
16194
+ hostOperator: this.isWebOnlyFleet(),
17216
16195
  });
17217
16196
  }
17218
16197
  sessionAuthorizations = null;
@@ -17272,269 +16251,6 @@ export class FleetManager {
17272
16251
  const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
17273
16252
  this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
17274
16253
  }
17275
- // ── #3008: connections from outside (a web-only fleet; decision 28c2d3a3 rule 3, the user's simplified form) ──
17276
- webRemoteAccessState = null;
17277
- /** The authenticator, the outside switch and the wrong-code limiter (web-remote-access.json), read on first use. */
17278
- webRemoteAccess() {
17279
- if (!this.dataDir)
17280
- return null;
17281
- this.webRemoteAccessState ??= new WebRemoteAccess(join(this.dataDir, WEB_REMOTE_ACCESS_FILE), this.logger);
17282
- return this.webRemoteAccessState;
17283
- }
17284
- /** "Allow connections from outside" is on (an app set up) and this is a web-only fleet. */
17285
- outsideOn() {
17286
- return this.isWebOnlyFleet() && (this.webRemoteAccess()?.outside ?? false);
17287
- }
17288
- /** The link connections from outside opened, while it is open and still theirs: its exposure. Else null. */
17289
- outsideLink() {
17290
- if (!this.outsideOn())
17291
- return null;
17292
- const open = this.publicWebLink?.openSignin();
17293
- return open && isOutsideLinkOwner(open.owner) ? { exposureId: open.exposureId } : null;
17294
- }
17295
- /**
17296
- * The request's live session, asked again each time `current()` is called (its own cookie authenticated anew, the same
17297
- * handle): this web-only fleet's owner — this computer's admin, on this computer (`hostOnly`), or also signed in from
17298
- * outside — and web-verified now. Null when it is not (a guest, a LAN session, a chat fleet's admin, nobody).
17299
- */
17300
- liveOwner(req, opts) {
17301
- if (!this.isWebOnlyFleet())
17302
- return null;
17303
- if (opts.hostOnly && gatewayRequestContext(req))
17304
- return null;
17305
- const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
17306
- if (auth.kind !== "ok")
17307
- return null;
17308
- const handle = auth.session.handle, peer = webPeerOf(req), gateway = !!gatewayRequestContext(req);
17309
- const owner = () => {
17310
- if (!this.isWebOnlyFleet())
17311
- return false;
17312
- const live = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
17313
- if (live.kind !== "ok" || live.session.handle !== handle)
17314
- return false;
17315
- if (opts.hostOnly && (live.session.host !== true || peer !== "loopback"))
17316
- return false;
17317
- const now = this.webAuthority({ session: live.session, gateway, peer });
17318
- const a = now.authority.kind === "web-verified" && now.authority.session?.handle === handle ? now.authority.actor : null;
17319
- return !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId;
17320
- };
17321
- return owner() ? { label: t("settings_confirm.host_operator_label"), handle, current: owner } : null;
17322
- }
17323
- /** Every session signed in from outside ends (and the authorizations kept for them). */
17324
- endOutsideSessions() {
17325
- const ended = new Set(this.webSessions?.list(undefined, r => r.remote === "totp").map(r => r.handle) ?? []);
17326
- if (!ended.size)
17327
- return;
17328
- this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
17329
- const result = this.webSessions?.revokeAll(r => r.remote === "totp");
17330
- if (result && !result.durable)
17331
- this.logger.warn("Outside sessions ended, but that could not be saved");
17332
- this.logger.info({ count: ended.size }, "Sessions signed in from outside ended");
17333
- }
17334
- /**
17335
- * #1939 §10 / #3008: a stored receipt's authority as it reads now — the web authority state, and for this computer's
17336
- * admin from outside the switch and the last revocation of remote authority (Prism P2 2).
17337
- */
17338
- receiptAuthorityNow(stored, state = this.webAuthorityState()) {
17339
- return authorityNow(stored, { ...state, outside: this.outsideOn(), outsideHandles: this.webRemoteAccess()?.admitted ?? new Set() });
17340
- }
17341
- /** #3008 (the sign-in page): this exposure is the outside link, open now. */
17342
- isOutsideLink(exposureId) {
17343
- return this.outsideLink()?.exposureId === exposureId;
17344
- }
17345
- /** What Settings shows: set up or not, the switch, a wait in force, the link — its address only to this computer. */
17346
- webOutsideStatus(req) {
17347
- const store = this.webRemoteAccess(), status = store?.status();
17348
- const here = !!this.liveOwner(req, { hostOnly: true });
17349
- const open = this.outsideOn() ? this.publicWebLink?.openSignin() : null;
17350
- const mine = !!open && isOutsideLinkOwner(open.owner);
17351
- const linkStatus = this.outsideOn() ? this.getPublicWebStatus() : null;
17352
- return { webOnly: this.isWebOnlyFleet(), here, enrolled: !!status?.enrolled, enrolledAt: status?.enrolledAt ?? null,
17353
- outside: this.outsideOn(), pausedForMs: status?.pausedForMs ?? 0,
17354
- link: linkStatus && linkStatus.state !== "closed" ? { ...linkStatus, ...(here && mine ? { url: open.url } : {}), quick: !publicLinkNamedHostname(this.fleetConfig?.web) } : null };
17355
- }
17356
- /** Set up (or set up again): a new secret, shown once to this computer's admin. Null: not them. */
17357
- webOutsideBeginEnrollment(req) {
17358
- const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17359
- if (!owner || !store)
17360
- return null;
17361
- return store.beginEnrollment(owner.handle, hostname() || "fleet");
17362
- }
17363
- /** The new app's first code: on "ok" it replaces the old one, whose sessions from outside end. */
17364
- webOutsideConfirmEnrollment(req, code) {
17365
- const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17366
- if (!owner || !store || !owner.current())
17367
- return "not_here";
17368
- const had = store.enrolled;
17369
- const r = store.confirmEnrollment(owner.handle, code);
17370
- if (r === "ok") {
17371
- this.logger.info({ again: had }, "Authenticator app set up for connections from outside");
17372
- if (had)
17373
- this.endOutsideSessions();
17374
- }
17375
- return r;
17376
- }
17377
- /** Forget the app: the switch goes off, the link closes, every session from outside ends. */
17378
- webOutsideRemove(req) {
17379
- const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17380
- if (!owner || !store || !owner.current())
17381
- return "not_here";
17382
- // #3008 review (Prism P2 1): shut at once whatever the disk says; say so when it could not be saved (retried).
17383
- const saved = store.remove();
17384
- this.closeOutsideLink("outside removed");
17385
- this.logger.info({ saved }, "Authenticator app removed; connections from outside off");
17386
- return saved ? "ok" : "unsaved";
17387
- }
17388
- /** The switch. On: needs an app set up; the link opens. Off: it closes and every session from outside ends. */
17389
- webOutsideSwitch(req, on) {
17390
- const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17391
- if (!owner || !store || !owner.current())
17392
- return "not_here";
17393
- if (on && !publicLinkSettings(this.fleetConfig?.web).allowed)
17394
- return "not_allowed";
17395
- const r = store.setOutside(on);
17396
- if (r === "not_enrolled")
17397
- return r;
17398
- this.logger.info({ on, saved: r === "ok" }, "Connections from outside switched");
17399
- // #3008 review (Prism P2 1): Off shuts at once even when it could not be saved (the store retries); On opens only
17400
- // once saved — a switch the disk does not hold would not survive a restart.
17401
- if (!on)
17402
- this.closeOutsideLink("outside off");
17403
- else if (r === "ok")
17404
- this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
17405
- return r;
17406
- }
17407
- outsideAttempt = null;
17408
- outsideOpening = false;
17409
- /**
17410
- * Open the outside link: an attempt after each delay in turn until one opens (#3008 B: twice more at most — the quick
17411
- * link's address service limits how often it may be asked). Every attempt asks again: the switch still on, the fleet
17412
- * running. A final failure: one Notice; the switch stays on, so the next start tries again.
17413
- */
17414
- openOutsideLink(delays) {
17415
- this.outsideAttempt?.cancel();
17416
- this.outsideAttempt = null;
17417
- const attempt = (n) => {
17418
- this.outsideAttempt = null;
17419
- if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
17420
- return;
17421
- this.outsideOpening = true;
17422
- // alpha CI 4abe58cd: an On right after an Off finds the old link still closing (its tunnel stopping): wait for that
17423
- // close, then open — never "busy" and a 30 s retry. The switch is asked again after the wait by openUnattended
17424
- // itself (permitted → publicOwnerCurrent: the fleet running, the switch on): an Off meanwhile opens nothing.
17425
- const link = this.getPublicWebLink();
17426
- void link.whenClosed().then(() => link.openUnattended(OUTSIDE_LINK_OWNER))
17427
- .catch(() => "failed").then(outcome => {
17428
- this.outsideOpening = false;
17429
- if (outcome === "opened") {
17430
- this.logger.info({ attempt: n + 1 }, "Outside link opened");
17431
- return;
17432
- }
17433
- if (outcome === "busy" && this.outsideLink())
17434
- return; // already open (ours)
17435
- if (this.shuttingDown || !this.outsideOn())
17436
- return;
17437
- if (n + 1 < delays.length) {
17438
- this.outsideAttempt = outsideTimer(() => attempt(n + 1), delays[n + 1]);
17439
- return;
17440
- }
17441
- this.logger.warn({ attempts: n + 1 }, "Outside link could not be opened; giving up until the next start or switch");
17442
- this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_failed")));
17443
- });
17444
- };
17445
- this.outsideAttempt = outsideTimer(() => attempt(0), delays[0] ?? 0);
17446
- }
17447
- closeOutsideLink(reason) {
17448
- this.outsideAttempt?.cancel();
17449
- this.outsideAttempt = null;
17450
- const open = this.publicWebLink?.openSignin();
17451
- if (this.publicWebLink && (!open || isOutsideLinkOwner(open.owner)))
17452
- void this.publicWebLink.close(reason);
17453
- this.endOutsideSessions();
17454
- }
17455
- /** PublicWebLink.onClose for the outside link: what closed it decides what happens next (#3008 B). */
17456
- outsideLinkClosed(reason) {
17457
- if (this.shuttingDown || OUTSIDE_OWN_CLOSES.has(reason) || !this.outsideOn())
17458
- return;
17459
- // #3008 review (Prism P2 3): expiry first — its lifetime counts from the opening request, so it can run out while the
17460
- // opening is still under way: the switch goes off then too, and that attempt's end (outsideOn false) retries nothing.
17461
- if (reason === "expired") {
17462
- // Its lifetime ran out: the switch goes off (only this computer turns it on again — a session from outside can't).
17463
- this.outsideAttempt?.cancel();
17464
- this.outsideAttempt = null;
17465
- if (this.webRemoteAccess()?.setOutside(false) === "unsaved")
17466
- this.logger.warn("Outside link expired; the switch's Off could not be saved yet (retried)");
17467
- this.endOutsideSessions();
17468
- this.logger.info("Outside link expired; connections from outside off");
17469
- this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_expired")));
17470
- return;
17471
- }
17472
- if (this.outsideOpening)
17473
- return; // that attempt's own failure: its retry
17474
- // Dropped (the tunnel exited, the gateway failed, the configuration changed): reopened up to twice.
17475
- this.logger.info({ reason }, "Outside link closed; reopening");
17476
- this.openOutsideLink(OUTSIDE_LINK_RETRY_MS);
17477
- }
17478
- /**
17479
- * #3008 (auth-api POST /auth/totp): a sign-in through the outside link with the authenticator's code. "ok": the caller
17480
- * makes the session (SessionRecord.remote) and then calls authorizeOutsideSession. Each wrong code counts; a wait that
17481
- * starts is a Notice; a code that matches a step just outside the window logs how far this clock seems to be off.
17482
- */
17483
- outsideSignIn(exposureId, code) {
17484
- const link = this.outsideLink(), store = this.webRemoteAccess();
17485
- if (!link || !store || !exposureId || link.exposureId !== exposureId)
17486
- return { kind: "closed" };
17487
- const v = store.verify(code);
17488
- if (v.kind === "ok")
17489
- return { kind: "ok" };
17490
- if (v.kind === "paused")
17491
- return v;
17492
- if (v.kind === "not_enrolled")
17493
- return { kind: "closed" };
17494
- if (v.kind === "unavailable")
17495
- return v;
17496
- if (v.skewSteps !== null)
17497
- this.logger.warn({ offsetSeconds: v.skewSteps * TOTP_STEP_SECONDS }, "A wrong authenticator code matched a nearby time step: this computer's clock may be off");
17498
- if (v.paused) {
17499
- this.logger.warn({ waitMs: v.retryAfterMs }, "Sign-in from outside paused after wrong codes");
17500
- this.recordWebNotice("fleet", t("outside.notice_paused", outsideDuration(v.retryAfterMs), v.wrong));
17501
- return { kind: "paused", retryAfterMs: v.retryAfterMs };
17502
- }
17503
- return { kind: "wrong" };
17504
- }
17505
- /**
17506
- * #3008: a session just made by outsideSignIn's "ok" is this computer's admin for its authorization's life. False: it
17507
- * could not be admitted (its handle not saved, #3015 review r3) — the caller ends it.
17508
- */
17509
- authorizeOutsideSession(session, peer, label) {
17510
- const store = this.sessionAuthorizationStore(), link = this.outsideLink();
17511
- if (!store || session.remote !== "totp" || session.owner || !link || session.exposureId !== link.exposureId)
17512
- return false;
17513
- if (!this.webRemoteAccess()?.admit(session.handle)) {
17514
- this.logger.warn({ handle: session.handle }, "Session from outside not admitted: it could not be saved");
17515
- return false;
17516
- }
17517
- const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
17518
- const check = evaluateWebAuthority({
17519
- session, gateway: true, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
17520
- isAdmin: () => false,
17521
- authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
17522
- policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
17523
- hostOperator: true, outside: link,
17524
- });
17525
- if (check.refusal) {
17526
- this.logger.info({ handle: session.handle, refusal: check.refusal }, "Session from outside not authorized");
17527
- return false;
17528
- }
17529
- {
17530
- const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
17531
- store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
17532
- const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
17533
- this.logger.info({ handle: session.handle, durable }, "Session from outside authorized (this computer's admin)");
17534
- }
17535
- this.recordWebNotice("fleet", t("outside.notice_signed_in", label));
17536
- return true;
17537
- }
17538
16254
  authorityStateStore = null;
17539
16255
  /**
17540
16256
  * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
@@ -17555,10 +16271,6 @@ export class FleetManager {
17555
16271
  return true;
17556
16272
  if (session.surface === "local")
17557
16273
  return false;
17558
- // #3008 review (Prism P2 4): signed in from outside with the authenticator's code — its opt-in is the outside
17559
- // switch, not these options (the authority judges it so): it keeps its authorization.
17560
- if (session.remote === "totp")
17561
- return false;
17562
16274
  const peer = a.peer ?? "named_link";
17563
16275
  return (!!state.namedLinkClosed && peer === "named_link") || (!!state.quickLinkClosed && peer === "quick_link");
17564
16276
  };
@@ -17665,8 +16377,9 @@ export class FleetManager {
17665
16377
  return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
17666
16378
  },
17667
16379
  signOut: (handle) => {
17668
- this.webSessions?.revokeByHandle(handle);
17669
- this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
16380
+ const session = this.webSessions?.revokeByHandle(handle) ?? { found: false, durable: true };
16381
+ const authorization = this.sessionAuthorizationStore()?.prune(a => a.handle !== handle) ?? { dropped: 0, durable: true };
16382
+ return { durable: session.durable, authorizationDurable: authorization.durable };
17670
16383
  },
17671
16384
  event: (type, payload) => { try {
17672
16385
  this.eventLog?.insert("web", type, payload);
@@ -17679,6 +16392,7 @@ export class FleetManager {
17679
16392
  expired: (c) => t("web_auth.card_expired", c.label),
17680
16393
  refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
17681
16394
  revoked: (c) => t("web_auth.card_revoked", c.label),
16395
+ notSaved: (c) => t("web_auth.card_not_saved", c.label),
17682
16396
  ended: (c) => t("web_auth.card_ended", c.label),
17683
16397
  generalHint: () => t("web_auth.general_hint"),
17684
16398
  confirmLabel: () => t("web_auth.confirm"),
@@ -17708,7 +16422,7 @@ export class FleetManager {
17708
16422
  authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
17709
16423
  policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
17710
16424
  // #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
17711
- hostOperator: this.isWebOnlyFleet(), outside: this.outsideLink(),
16425
+ hostOperator: this.isWebOnlyFleet(),
17712
16426
  });
17713
16427
  return r.refusal;
17714
16428
  }
@@ -18574,8 +17288,6 @@ export class FleetManager {
18574
17288
  publication.retirePending = true;
18575
17289
  }
18576
17290
  async sendCancelButton(instanceName, correlationId, preserveProgress = false) {
18577
- if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
18578
- return;
18579
17291
  // Post first, retire after (see the tail of this method). Retiring up front
18580
17292
  // meant that from the delete until the new message came back — a chat API
18581
17293
  // round trip, and every reply goes through here — the instance had NO live
@@ -18601,7 +17313,14 @@ export class FleetManager {
18601
17313
  return;
18602
17314
  }
18603
17315
  const publication = this.beginCancelButtonPublication(instanceName, correlationId);
18604
- const initialProgressText = FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
17316
+ // M-2391: a re-post below a reply (preserveProgress) continues the same turn — its elapsed time continues from
17317
+ // the live bubble's start, as its tool list does. Restarting it at 0 meant an agent replying more often than the
17318
+ // threshold never showed elapsed time at all. A fresh turn (no preserve) starts at 0 as before.
17319
+ const carriedStart = preserveProgress ? this.liveButtonStart(instanceName) : undefined;
17320
+ const carriedMs = carriedStart !== undefined ? Math.max(0, Date.now() - carriedStart) : 0;
17321
+ const initialProgressText = carriedStart !== undefined && carriedMs >= this.progressMinElapsedMs()
17322
+ ? FleetManager.progressText(carriedMs, null, this.progressMinElapsedMs(), this.progressPrefixFor(instanceName, carriedMs))
17323
+ : FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
18605
17324
  try {
18606
17325
  const sent = await adapter.notifyAlert(chatId, {
18607
17326
  type: "cancel",
@@ -18624,7 +17343,7 @@ export class FleetManager {
18624
17343
  // Elapsed time is measured from when this button was posted — i.e. from
18625
17344
  // when the work was handed over — not from the pane's working transition,
18626
17345
  // which resets if the CLI blips idle mid-turn.
18627
- startedAt: Date.now(),
17346
+ startedAt: carriedStart ?? Date.now(),
18628
17347
  // Matches the text notifyAlert just posted, so the first 60s tick does
18629
17348
  // not re-edit identical text — which put a "(edited)" mark on Discord
18630
17349
  // with nothing visibly changed.
@@ -18747,14 +17466,16 @@ export class FleetManager {
18747
17466
  * Make a tool summary safe to paste into a channel message.
18748
17467
  *
18749
17468
  * The text is agent-controlled (it is built from tool inputs — file paths,
18750
- * shell commands), so it gets flattened to one line, capped, and stripped of
18751
- * the two Discord mass-mention triggers. Neither channel renders it with a
18752
- * parse mode, so no markup escaping is needed beyond that.
17469
+ * shell commands), so it gets flattened to one line, secrets masked, capped,
17470
+ * and stripped of the two Discord mass-mention triggers. Masking comes BEFORE
17471
+ * the cap: a cap can cut a mask's context but must never be what hides (or
17472
+ * fails to hide) a secret. Neither channel renders it with a parse mode, so no
17473
+ * markup escaping is needed beyond that.
18753
17474
  */
18754
17475
  static sanitizeActivity(activity) {
18755
17476
  if (!activity)
18756
17477
  return null;
18757
- const flat = activity.replace(/\s+/g, " ").replace(/@(everyone|here)/g, "@​$1").trim();
17478
+ const flat = maskSecrets(activity.replace(/\s+/g, " ")).replace(/@(everyone|here)/g, "@​$1").trim();
18758
17479
  if (!flat)
18759
17480
  return null;
18760
17481
  return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
@@ -18873,6 +17594,12 @@ export class FleetManager {
18873
17594
  }
18874
17595
  return PROGRESS_MIN_ELAPSED_MS;
18875
17596
  }
17597
+ /** M-2391: when the instance's live (not retiring) bubble started — the current turn's start — or undefined. */
17598
+ liveButtonStart(instanceName) {
17599
+ const starts = [...this.cancelButtons.values()].filter((e) => e.instanceName === instanceName && !e.retiring && typeof e.startedAt === "number")
17600
+ .map((e) => e.startedAt);
17601
+ return starts.length ? Math.min(...starts) : undefined;
17602
+ }
18876
17603
  startProgressTicker(entry) {
18877
17604
  const tick = () => this.refreshBubble(entry);
18878
17605
  entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
@@ -18882,7 +17609,9 @@ export class FleetManager {
18882
17609
  // one additional edit per turn that lives past the threshold.
18883
17610
  const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
18884
17611
  if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
18885
- const firstTick = setTimeout(tick, firstAt);
17612
+ // M-2391: a little past the threshold — a timer can fire a millisecond before Date.now() reaches it, which
17613
+ // rendered the unchanged "處理中…" text and skipped the edit until the next 60 s interval.
17614
+ const firstTick = setTimeout(tick, firstAt + PROGRESS_THRESHOLD_MARGIN_MS);
18886
17615
  firstTick.unref?.();
18887
17616
  }
18888
17617
  }
@@ -19108,14 +17837,6 @@ export class FleetManager {
19108
17837
  }
19109
17838
  /** Interrupt an instance's current generation (cancel button / /cancel). */
19110
17839
  cancelInstance(instanceName) {
19111
- if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
19112
- this.cancelPendingDeliveries(instanceName);
19113
- const cancelled = this.nativeInstanceRuntime().cancel(instanceName);
19114
- this.lastInboundMsg.delete(instanceName);
19115
- for (const message of this.webChatHistory.cancelPending(instanceName))
19116
- this.emitSseEvent("delivery", { instance: instanceName, messageId: message.messageId, delivery: message.delivery });
19117
- return cancelled !== null;
19118
- }
19119
17840
  const daemon = this.daemons.get(instanceName);
19120
17841
  if (!daemon)
19121
17842
  return false;
@@ -19820,9 +18541,12 @@ export class FleetManager {
19820
18541
  this.loginWindow.release(claim);
19821
18542
  return t("login.web_shutting_down");
19822
18543
  }
18544
+ // The wrapper says its own exit code after this marker: tmux may never record it (unattended-install.ts).
18545
+ const exitMarker = installExitMarker();
19823
18546
  const flow = {
19824
18547
  backend,
19825
- command: unattendedInstallCommand(info.unattended ?? info.install, shell),
18548
+ command: unattendedInstallCommand(info.unattended ?? info.install, shell, exitMarker),
18549
+ exitMarker,
19826
18550
  successPattern: /$^/,
19827
18551
  timeoutMs: this.installTimeoutMs,
19828
18552
  };
@@ -20167,9 +18891,12 @@ export class FleetManager {
20167
18891
  this.taskBoardWaker?.stop();
20168
18892
  // #2303: each instance's cards at once, read at every claim (a fleet.yaml edit applies to the next one).
20169
18893
  db.board.setCardLimit((name) => boardCardLimit(this.fleetConfig, name));
18894
+ // An instance with the task board switched off is handed no cards and sent no notices (read at each write and wake).
18895
+ db.board.setBoardOff((name) => taskBoardOffIn(this.fleetConfig, name));
20170
18896
  const waker = this.taskBoardWaker = new TaskBoardWaker({
20171
18897
  deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
20172
18898
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
18899
+ boardOff: (name) => taskBoardOffIn(this.fleetConfig, name),
20173
18900
  // #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
20174
18901
  messagedAbout: (from, to, codes, sinceMs) => {
20175
18902
  const outbox = this.deliveryOutbox;
@@ -20333,7 +19060,36 @@ export class FleetManager {
20333
19060
  "antigravity": ".agents/AGENTS.md",
20334
19061
  "mock": "CLAUDE.md",
20335
19062
  };
20336
- static GENERAL_INSTRUCTIONS = GENERAL_ROLE_INSTRUCTIONS;
19063
+ static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
19064
+
19065
+ You are the fleet coordinator — the central entry point for this AgEnD fleet.
19066
+ You route tasks, manage instances, enforce policies, and synthesize results.
19067
+ Do NOT modify project files directly — delegate file changes to the project's instance.
19068
+ You CAN write code snippets, explain code, and answer technical questions directly.
19069
+
19070
+ ## Task Routing
19071
+
19072
+ - **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
19073
+ - **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
19074
+ - **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
19075
+
19076
+ Instance discovery: start with list_instances(); follow the guidance in its response. Prefer reuse; never duplicate a running instance.
19077
+
19078
+ ## Reply Contract
19079
+
19080
+ Every final response to the user contains: the result (the actual answer or deliverable) and gaps (anything incomplete — omit if none). Summarize instance reports; omit internal coordination noise.
19081
+
19082
+ ## After Restart
19083
+
19084
+ BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
19085
+
19086
+ ## Playbooks (on-demand skills)
19087
+
19088
+ Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
19089
+ - **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
19090
+ - **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
19091
+ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
19092
+ `;
20337
19093
  /** Ensure the general instance has its project instructions file + knowledge */
20338
19094
  ensureGeneralInstructions(workDir, backendName, instanceName) {
20339
19095
  measureSyncWork("fleet.generalInstructions", () => this.ensureGeneralInstructionsSync(workDir, backendName, instanceName));
@@ -21355,8 +20111,6 @@ export class FleetManager {
21355
20111
  : model,
21356
20112
  });
21357
20113
  const fleetInstance = this.fleetConfig?.instances[instanceName];
21358
- if (isWindowsInstance(fleetInstance))
21359
- return fleetInstance.model?.trim() ? done(fleetInstance.model.trim(), "instance") : done("default", "unresolved", "this CLI does not report a default");
21360
20114
  if (fleetInstance) {
21361
20115
  if (fleetInstance.model?.trim())
21362
20116
  return done(fleetInstance.model.trim(), "instance");
@@ -21438,7 +20192,7 @@ export class FleetManager {
21438
20192
  if (this.fleetConfig?.defaults?.backend)
21439
20193
  backends.add(this.fleetConfig.defaults.backend);
21440
20194
  for (const inst of Object.values(this.fleetConfig?.instances ?? {}))
21441
- if (!isWindowsInstance(inst) && inst.backend)
20195
+ if (inst.backend)
21442
20196
  backends.add(inst.backend);
21443
20197
  for (const ch of this.classicChannels?.getAll() ?? [])
21444
20198
  if (ch.backend)
@@ -21453,8 +20207,6 @@ export class FleetManager {
21453
20207
  // here; a ClassicBot room on a profile is probed on its first /model (catalogProfile knows rooms too).
21454
20208
  const profiles = new Set();
21455
20209
  for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
21456
- if (isWindowsInstance(this.fleetConfig?.instances[name]))
21457
- continue;
21458
20210
  if (this.backendNameForInstance(name) !== "claude-code")
21459
20211
  continue;
21460
20212
  const profile = this.catalogProfile("claude-code", name);
@@ -21475,8 +20227,6 @@ export class FleetManager {
21475
20227
  * instead of presenting an old list as a fresh one.
21476
20228
  */
21477
20229
  async getModelOptionsWithSource(instanceName, opts = {}) {
21478
- if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
21479
- return { models: (await this.listModelCatalog({ instanceName })).models, source: "cache" };
21480
20230
  const backendName = this.backendNameForInstance(instanceName);
21481
20231
  const profile = this.catalogProfile(backendName, instanceName);
21482
20232
  const cached = this.readCliEnv(backendName, profile);
@@ -21532,21 +20282,6 @@ export class FleetManager {
21532
20282
  */
21533
20283
  async listModelCatalog(opts = {}) {
21534
20284
  const { instanceName } = opts;
21535
- if (instanceName && isWindowsInstance(this.fleetConfig?.instances[instanceName])) {
21536
- const config = this.fleetConfig.instances[instanceName], backend = config.backend ?? "codex";
21537
- // Detection is admin-owned. A read never launches a provider or falls back to this host's account.
21538
- let inventory = null;
21539
- try {
21540
- inventory = await this.runtimeWindowsExecutor().probe({ force: false, machineId: config.machine_id });
21541
- }
21542
- catch { /* offline remains unavailable */ }
21543
- const row = inventory?.clis.find(value => value.id === backend);
21544
- return { backend, scope: "instance", instance: instanceName, current_model: config.model || null,
21545
- models: (row?.models ?? []).map(value => ({ id: value.id, label: value.name ?? value.id, ...(value.description ? { description: value.description } : {}) })),
21546
- source: row?.models?.length ? "cache" : "fallback",
21547
- ...(inventory?.probedAt ? { probed_at: new Date(inventory.probedAt).toISOString() } : {}),
21548
- note: "Windows runtime's last detected inventory; no coordinator account or live probe is used." };
21549
- }
21550
20285
  if (instanceName) {
21551
20286
  const backend = this.backendNameForInstance(instanceName);
21552
20287
  const resolved = this.resolveInstanceModel(instanceName);
@@ -21582,8 +20317,6 @@ export class FleetManager {
21582
20317
  }
21583
20318
  /** Ask a backend for its catalog using ONE instance's real config. Never throws. */
21584
20319
  async instanceScopedModels(instanceName, backend) {
21585
- if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
21586
- return [];
21587
20320
  try {
21588
20321
  const inst = this.fleetConfig?.instances?.[instanceName];
21589
20322
  const instanceDir = this.getInstanceDir(instanceName);
@@ -22221,8 +20954,6 @@ export class FleetManager {
22221
20954
  }
22222
20955
  /** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
22223
20956
  instanceEffortMetadata(instanceName) {
22224
- if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
22225
- return { strategy: "unsupported", levels: [] };
22226
20957
  const backend = this.backendNameForInstance(instanceName);
22227
20958
  let model = null;
22228
20959
  if (backend === "antigravity") {
@@ -22233,8 +20964,6 @@ export class FleetManager {
22233
20964
  }
22234
20965
  /** Configured effort for an instance: per-instance, else fleet default, else none. */
22235
20966
  resolveInstanceEffort(instanceName) {
22236
- if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
22237
- return { effort: null, source: "unset" };
22238
20967
  const own = this.fleetConfig?.instances[instanceName]?.effort;
22239
20968
  if (own)
22240
20969
  return { effort: own, source: "instance" };
@@ -22272,11 +21001,9 @@ export class FleetManager {
22272
21001
  * instance respawns.
22273
21002
  */
22274
21003
  async applyEffort(instanceName, requested, opts) {
22275
- // #1490 row 27 (main): a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
21004
+ // #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
22276
21005
  if (opts?.current && !opts.current())
22277
21006
  return t("menu.instance_replaced", this.shownInstanceName(instanceName));
22278
- if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
22279
- return t("effort.unsupported", this.backendNameForInstance(instanceName));
22280
21007
  const level = requested.trim().toLowerCase();
22281
21008
  const backendName = this.backendNameForInstance(instanceName);
22282
21009
  let strategy = "unsupported";
@@ -22324,20 +21051,6 @@ export class FleetManager {
22324
21051
  if (/[\x00-\x1f\x7f]/.test(model)) {
22325
21052
  return t("model.invalid_chars");
22326
21053
  }
22327
- const native = this.fleetConfig?.instances[instanceName];
22328
- if (isWindowsInstance(native)) {
22329
- if (!isNativePaneInstance(native))
22330
- return WINDOWS_CONFIG_REFUSED;
22331
- const catalog = await this.listModelCatalog({ instanceName });
22332
- if (!catalog.models.some(row => row.id === model))
22333
- return t("model.unknown_name", model, catalog.models.map(row => row.id).join(", "));
22334
- const id = native.native_agent_id, machine = native.machine_id;
22335
- await this.updateNativePaneConfig(instanceName, { model }, () => {
22336
- const now = this.fleetConfig?.instances[instanceName];
22337
- return isNativePaneInstance(now) && now.native_agent_id === id && now.machine_id === machine;
22338
- });
22339
- return t("model.restart_success", instanceName, model);
22340
- }
22341
21054
  const backendName = this.backendNameForInstance(instanceName);
22342
21055
  let strategy = "restart";
22343
21056
  try {
@@ -22526,8 +21239,6 @@ export class FleetManager {
22526
21239
  * profile's, when it is named and has one), claude's account catalog included. Read only: never a probe.
22527
21240
  */
22528
21241
  modelCatalogFor(backend, instance) {
22529
- if (instance && isWindowsInstance(this.fleetConfig?.instances[instance]))
22530
- return [];
22531
21242
  if (!KNOWN_BACKENDS.includes(backend) || backend === "mock")
22532
21243
  return [];
22533
21244
  const own = instance && Object.prototype.hasOwnProperty.call(this.fleetConfig?.instances ?? {}, instance) ? instance
@@ -22541,40 +21252,6 @@ export class FleetManager {
22541
21252
  }
22542
21253
  return out;
22543
21254
  }
22544
- /** Host-default fallback only: Grok has no modeled profile home. Never construct a backend or start a probe. */
22545
- async cliModelCatalogFor(backend, instance) {
22546
- if (backend !== "grok")
22547
- return [];
22548
- const fleet = this.fleetConfig;
22549
- const configured = instance && Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) ? fleet.instances[instance] : undefined;
22550
- const classic = this.classicChannels;
22551
- const room = instance && !configured ? classic?.getAll().find(channel => channel.instanceName === instance) : undefined;
22552
- if (instance && !configured && !room)
22553
- return [];
22554
- const daemon = room ? this.daemons.get(instance) : undefined;
22555
- const grokHome = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
22556
- const stillHostDefault = () => {
22557
- if (this.fleetConfig !== fleet || (process.env.GROK_HOME?.trim() || join(homedir(), ".grok")) !== grokHome)
22558
- return false;
22559
- if (configured && (isWindowsInstance(configured) || !Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) || fleet.instances[instance] !== configured))
22560
- return false;
22561
- if (room && (this.classicChannels !== classic || classic?.getAll().find(channel => channel.instanceName === instance) !== room || this.daemons.get(instance) !== daemon))
22562
- return false;
22563
- // The ordinary profile helpers deliberately ignore Grok: it has no credential-home mapping yet.
22564
- // Check raw declarations, including invalid ones, rather than mistaking them for the host login.
22565
- const defaults = fleet?.defaults?.backend_options;
22566
- const profiles = configured
22567
- ? [configured.backend_options?.grok?.credential_profile, defaults?.grok?.credential_profile]
22568
- : room
22569
- ? [mergeBackendOptions(defaults, room.backendOptions)?.grok?.credential_profile, daemon?.getConfigSnapshot?.().backend_options?.grok?.credential_profile]
22570
- : [defaults?.grok?.credential_profile];
22571
- return profiles.every(profile => profile === undefined || profile === null || profile === "");
22572
- };
22573
- if (!stillHostDefault())
22574
- return [];
22575
- const models = await readGrokModelCache(grokHome);
22576
- return stillHostDefault() ? models : [];
22577
- }
22578
21255
  /**
22579
21256
  * #2072 (#2078 review): the snapshot as THIS instance would see it. Its sign-in on a backend is its own credential
22580
21257
  * profile's when it has one there — the host's default login says nothing about it. A profile's login is a file check
@@ -23129,6 +21806,7 @@ export class FleetManager {
23129
21806
  ...this.fleetConfig?.defaults,
23130
21807
  working_directory: workDir,
23131
21808
  lightweight: true,
21809
+ quota_notice: classicIdentity?.quotaNotice,
23132
21810
  tool_progress: toolProgress ?? "off",
23133
21811
  reply_completion_guard: replyCompletionGuard ?? true,
23134
21812
  ...(backend ? { backend } : {}),
@@ -23233,16 +21911,6 @@ export class FleetManager {
23233
21911
  * settles, so a later genuine stop (after a restart) still does the work.
23234
21912
  */
23235
21913
  stopAll() {
23236
- this.lifecycle.cancelNativeRecovery?.();
23237
- this.nativeMcpStore?.closeAll();
23238
- this.runtimeExecutor?.close();
23239
- this.runtimeExecutor = null;
23240
- this.fleetWindowsRuntime = null;
23241
- this.runtimeHub?.close();
23242
- this.runtimeHub = null;
23243
- this.localPaneRelay?.stop();
23244
- this.localPaneRelay = null;
23245
- closeWindowsBridge(this);
23246
21914
  this.stopAllInFlight ??= this.doStopAll().finally(() => {
23247
21915
  // #1956: after the stop's own tmux work; anything later goes direct. A later startAll starts it again.
23248
21916
  if (this.tmuxExecHelperTimer) {
@@ -23305,6 +21973,25 @@ export class FleetManager {
23305
21973
  // because we asked it to. Set synchronously — doStopAll runs to its first
23306
21974
  // await in the same tick as the signal handler, so no event can slip in.
23307
21975
  this.shuttingDown = true;
21976
+ this.stopQuotaObserver?.();
21977
+ this.stopQuotaObserver = null;
21978
+ const quotaNoticesStopped = this.quotaNotices?.stop();
21979
+ // Stop receive admission before the first await or any instance shutdown; outbound
21980
+ // adapters remain available below to retire notices and pending prompts.
21981
+ for (const state of this.adapterState.values()) {
21982
+ if (state.retryTimer) {
21983
+ clearTimeout(state.retryTimer);
21984
+ state.retryTimer = undefined;
21985
+ }
21986
+ }
21987
+ const inboundStopped = Promise.all([...this.adapters.values()].map(async (adapter) => {
21988
+ try {
21989
+ await adapter.stopReceiving?.();
21990
+ }
21991
+ catch (err) {
21992
+ this.logger.warn({ adapterId: adapter.id, err }, "Adapter receive stop failed; inbound remains fenced");
21993
+ }
21994
+ }));
23308
21995
  this.workingRecovery?.reset();
23309
21996
  if (this.workingRecoveryTimer)
23310
21997
  clearInterval(this.workingRecoveryTimer);
@@ -23321,7 +22008,6 @@ export class FleetManager {
23321
22008
  const settingsControlStopped = settingsControl?.close();
23322
22009
  this.publicLinkAutoOpen?.stop();
23323
22010
  const publicStopped = this.publicWebLink?.close("fleet shutdown");
23324
- this.webRemoteAccessState?.close(); // #3008: an unsaved switch gets its last write
23325
22011
  const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
23326
22012
  // #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
23327
22013
  // adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
@@ -23365,23 +22051,19 @@ export class FleetManager {
23365
22051
  // A login/install window is a dedicated tmux server with its own TTL
23366
22052
  // timer and HTTP listener living in THIS process: without an explicit
23367
22053
  // shutdown it would outlive us as an owner-less login CLI (sol B3).
22054
+ await inboundStopped;
23368
22055
  await publicStopped;
23369
22056
  await sharesStopped?.catch(() => { });
23370
22057
  await profileStopped;
23371
22058
  await needsStopped;
23372
22059
  await settingsControlStopped;
23373
22060
  await cacheStopped;
22061
+ await quotaNoticesStopped;
22062
+ this.quotaNotices = null;
23374
22063
  await webChatStopped;
23375
22064
  await this.cpuProfileControl?.close();
23376
22065
  this.cpuProfileControl = null;
23377
22066
  await this.shutdownLoginWindows();
23378
- // Cancel adapter retry timers
23379
- for (const state of this.adapterState.values()) {
23380
- if (state.retryTimer) {
23381
- clearTimeout(state.retryTimer);
23382
- state.retryTimer = undefined;
23383
- }
23384
- }
23385
22067
  this.clearStatuslineWatchers();
23386
22068
  this.costGuard?.stop();
23387
22069
  this.dailySummary?.stop();
@@ -23419,6 +22101,15 @@ export class FleetManager {
23419
22101
  clearInterval(this.discordPresenceTimer);
23420
22102
  this.discordPresenceTimer = null;
23421
22103
  }
22104
+ if (this.quotaReadTimer) {
22105
+ clearInterval(this.quotaReadTimer);
22106
+ this.quotaReadTimer = null;
22107
+ }
22108
+ if (this.capacityTimer) {
22109
+ clearInterval(this.capacityTimer);
22110
+ this.capacityTimer = null;
22111
+ }
22112
+ setCapacityLookup(null);
23422
22113
  if (this.discordPresenceEagerTimer) {
23423
22114
  clearTimeout(this.discordPresenceEagerTimer);
23424
22115
  this.discordPresenceEagerTimer = null;
@@ -23704,7 +22395,6 @@ export class FleetManager {
23704
22395
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
23705
22396
  this.loadedFleetSource = previousSource;
23706
22397
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
23707
- this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
23708
22398
  this.logger.error({
23709
22399
  oldCount,
23710
22400
  newCount,
@@ -23724,6 +22414,7 @@ export class FleetManager {
23724
22414
  // mark every row done and tell the user "changes applied".
23725
22415
  return { rejected: why };
23726
22416
  }
22417
+ this.agentCredentialsCache?.observePolicies();
23727
22418
  if (this.loadedFleetSource)
23728
22419
  this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
23729
22420
  this.migrateOfficeStorage();
@@ -24624,6 +23315,9 @@ export class FleetManager {
24624
23315
  }
24625
23316
  async runConnectionBindingApply(job, binding, execution) {
24626
23317
  try {
23318
+ // #3055 review r2: a connection is not re-pointed while one of its topics is being deleted (nothing is changed).
23319
+ if (this.topicDeletionPending())
23320
+ throw new Error("a topic deletion is running; try again in a moment");
24627
23321
  await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
24628
23322
  job.result = "applied";
24629
23323
  }
@@ -25340,7 +24034,7 @@ export class FleetManager {
25340
24034
  const topicMode = daemonTopicMode(fleet);
25341
24035
  const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
25342
24036
  // Phase 1: generals first
25343
- const restartEntries = Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config));
24037
+ const restartEntries = Object.entries(fleet.instances);
25344
24038
  const restartGenerals = restartEntries.filter(([_, cfg]) => cfg.general_topic);
25345
24039
  const restartOthers = restartEntries.filter(([_, cfg]) => !cfg.general_topic);
25346
24040
  for (const [name, cfg] of restartGenerals) {
@@ -25794,9 +24488,6 @@ export class FleetManager {
25794
24488
  return;
25795
24489
  }
25796
24490
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
25797
- if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
25798
- && this.runtimeCoordinator().handlePublicRequest(req, res, new URL(req.url ?? "/", `http://localhost:${port}`)))
25799
- return;
25800
24491
  // Branding is public, after Host admission: automatic favicon requests
25801
24492
  // must not become noisy web-token 401s in the browser console.
25802
24493
  // #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
@@ -25823,8 +24514,7 @@ export class FleetManager {
25823
24514
  if (decision.reason === "no-credential" && req.method === "GET"
25824
24515
  && String(req.headers.accept ?? "").includes("text/html")
25825
24516
  && isWebPageNavigation(requestPath)) {
25826
- const link = gatewayRequestContext(req);
25827
- serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId) });
24517
+ serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
25828
24518
  return;
25829
24519
  }
25830
24520
  res.writeHead(decision.status);
@@ -26181,12 +24871,6 @@ export class FleetManager {
26181
24871
  if (!this.webToken || !this.webSessions)
26182
24872
  this.initializeWebAuthTokens();
26183
24873
  const server = createServer((req, res) => this.dispatchWebHttp(req, res, port, drain.closing));
26184
- server.on("upgrade", (req, socket, head) => {
26185
- if (!this.shuttingDown && remoteRuntimeEnabled() && isHostAllowed(req.headers.host, allowedHostNames(this.fleetConfig))
26186
- && this.runtimeCoordinator().handleUpgrade(req, socket, head))
26187
- return;
26188
- socket.destroy();
26189
- });
26190
24874
  // #1937: Node's default (300 s to receive a request) would cut a large upload on a slow link. The listener allows
26191
24875
  // more; the upload route keeps its own clocks (no byte for a minute, or 30 minutes in all), and every other request
26192
24876
  // keeps the old 300 s (limitRequestReceive in dispatchWebHttp).
@@ -26411,36 +25095,9 @@ export class FleetManager {
26411
25095
  this.logger.debug({ err }, "bots unreadable (getUiStatus)");
26412
25096
  }
26413
25097
  const instances = names.map(name => {
26414
- const nativeConfig = this.fleetConfig?.instances[name];
26415
- if (isNativeExecInstance(nativeConfig)) {
26416
- let runtime = null;
26417
- let agent = null;
26418
- try {
26419
- runtime = this.nativeInstanceRuntime();
26420
- agent = runtime.get(name);
26421
- }
26422
- catch { /* A damaged native profile is a crashed native row, never a Linux spawn. */ }
26423
- const display_name = nativeConfig.display_name || agent?.name || name;
26424
- const backend = nativeConfig.backend || agent?.cli || "codex";
26425
- return { name, display_name, description: nativeConfig.description || undefined, tags: nativeConfig.tags || [],
26426
- teams: instanceTeams(this.fleetConfig?.teams, name), runtime: "windows", native_agent_id: nativeConfig.native_agent_id,
26427
- ...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
26428
- machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}),
26429
- native_blocked: !agent || agent.blocked === true,
26430
- native_busy: runtime?.busy(name) ?? false,
26431
- backend,
26432
- model: nativeConfig.model || "default", model_source: nativeConfig.model ? "instance" : "cli-default",
26433
- status: agent && runtime ? runtime.status(name) : "crashed", state: agent && runtime ? runtime.execution(name) : null, execution_state: agent && runtime ? runtime.execution(name) : null,
26434
- interaction: null, interaction_summary: null, context_pct: null, cost: null,
26435
- last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined,
26436
- avatar: avatarFor({ name, displayName: display_name }),
26437
- capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: nativeProviderMcpSupported(backend), channels: false, terminal: false, attachments: false, usage: false, send_while_busy: false },
26438
- ...(!agent ? { runtime_error: "Windows profile is missing or invalid; restore its saved conversation before using this instance" }
26439
- : agent.blocked ? { runtime_error: "Windows process outcome is unknown; verify it stopped before recovery" } : {}) };
26440
- }
26441
25098
  // #1956: read once for this instance; its cost, context and model come from it.
26442
- const statusline = isNativePaneInstance(nativeConfig) ? undefined : readStatuslineJson(this.dataDir, name);
26443
- const cost = isNativePaneInstance(nativeConfig) ? null : statusline?.cost?.total_cost_usd ?? 0;
25099
+ const statusline = readStatuslineJson(this.dataDir, name);
25100
+ const cost = statusline?.cost?.total_cost_usd ?? 0;
26444
25101
  // Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
26445
25102
  const classic = classicOnly.includes(name);
26446
25103
  const backend = classic
@@ -26448,7 +25105,7 @@ export class FleetManager {
26448
25105
  : (this.fleetConfig?.instances[name]?.backend
26449
25106
  ?? this.fleetConfig?.defaults?.backend
26450
25107
  ?? "claude-code");
26451
- const { context } = isNativePaneInstance(nativeConfig) ? { context: null } : resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
25108
+ const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
26452
25109
  // context_pct: null when unavailable, not 0
26453
25110
  const context_pct = context ?? null;
26454
25111
  // Model: Only Claude Code has live statusline; others use the effective resolver.
@@ -26479,22 +25136,6 @@ export class FleetManager {
26479
25136
  }
26480
25137
  return {
26481
25138
  name,
26482
- general_topic: nativeConfig?.general_topic === true,
26483
- ...(isNativePaneInstance(nativeConfig) ? { runtime: "windows", native_adapter: "pane", native_agent_id: nativeConfig.native_agent_id,
26484
- ...(() => {
26485
- try {
26486
- const profile = this.nativeInstanceRuntime().get(name), bound = this.daemons.get(name);
26487
- return { native_recovery: profile?.paneRecovery ?? (profile?.paneLaunchId && bound?.usesNativePane?.() && !bound.nativePaneCurrent()
26488
- ? { kind: "exit-unconfirmed", launchId: profile.paneLaunchId } : null) };
26489
- }
26490
- catch {
26491
- return { native_recovery: null };
26492
- }
26493
- })(),
26494
- capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
26495
- commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
26496
- ...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
26497
- machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
26498
25139
  display_name: identity.display_name || undefined,
26499
25140
  avatar: image.avatar,
26500
25141
  ...(image.accent ? { accent: image.accent } : {}),
@@ -26517,8 +25158,6 @@ export class FleetManager {
26517
25158
  role: identity.role || undefined,
26518
25159
  tags: identity.tags,
26519
25160
  teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
26520
- // #3008 G2: a web-only fleet's General (the agent its owner ticked) — the sidebar pins it first.
26521
- ...(this.isWebOnlyFleet() && this.fleetConfig?.instances[name]?.general_topic === true ? { general: true } : {}),
26522
25161
  last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined, // #2179: its last reply to a person (ISO)
26523
25162
  // #2855: its last reply that reached the channel — the only reply time a guest's sidebar gets (guestStatus).
26524
25163
  last_reply_channel: this.webChatHistory.lastAgentReply(name, { channel: true }) ?? undefined,
@@ -26548,7 +25187,6 @@ export class FleetManager {
26548
25187
  uptime: Math.floor((Date.now() - this.startedAt) / 1000),
26549
25188
  // #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
26550
25189
  connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
26551
- windows_cli: localWindowsCliEnabled(),
26552
25190
  // #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
26553
25191
  prefs_rev: this.webPrefs.rev(),
26554
25192
  // #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
@@ -26558,8 +25196,6 @@ export class FleetManager {
26558
25196
  host_memory: this.hostMemoryIndicator(),
26559
25197
  // #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
26560
25198
  ...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
26561
- // alpha.2 (M-2300, #2997 review): agents' requests changed — the owner's page reads GET /ui/agent-proposals again.
26562
- ...(this.isWebOnlyFleet() ? { agent_proposals_rev: this.agentProposalsRev } : {}),
26563
25199
  // #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
26564
25200
  ...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
26565
25201
  };
@@ -27002,12 +25638,4 @@ load();
27002
25638
  </script>
27003
25639
  </body>
27004
25640
  </html>`;
27005
- /** alpha.2 (M-2300): the agent an applied Windows request created (its name, for the page's "Open chat"), or null. */
27006
- export function agentNameOf(view) {
27007
- const r = view.state === "applied" ? view.outcome?.result : null;
27008
- if (!r || typeof r !== "object")
27009
- return null;
27010
- const o = r, name = o.name ?? o.instance_name ?? (o.agent && typeof o.agent === "object" ? o.agent.name : undefined);
27011
- return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) ? name : null;
27012
- }
27013
25641
  //# sourceMappingURL=fleet-manager.js.map