@songsid/agend 2.3.5-alpha.3 → 2.3.5-alpha.5

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 (865) hide show
  1. package/THIRD_PARTY_NOTICES.md +52 -0
  2. package/dist/accounts-view.d.ts +97 -0
  3. package/dist/accounts-view.js +116 -0
  4. package/dist/accounts-view.js.map +1 -0
  5. package/dist/agent-cli-instructions.md +1 -1
  6. package/dist/agent-credentials.d.ts +24 -0
  7. package/dist/agent-credentials.js +66 -0
  8. package/dist/agent-credentials.js.map +1 -0
  9. package/dist/agent-endpoint.d.ts +3 -1
  10. package/dist/agent-endpoint.js +13 -0
  11. package/dist/agent-endpoint.js.map +1 -1
  12. package/dist/agent-file-read.js +1 -1
  13. package/dist/agent-file-read.js.map +1 -1
  14. package/dist/agent-runtime-api.d.ts +4 -0
  15. package/dist/agent-runtime-api.js +59 -0
  16. package/dist/agent-runtime-api.js.map +1 -0
  17. package/dist/agent-runtime-change.d.ts +76 -0
  18. package/dist/agent-runtime-change.js +114 -0
  19. package/dist/agent-runtime-change.js.map +1 -0
  20. package/dist/agent-workspace.d.ts +93 -0
  21. package/dist/agent-workspace.js +353 -0
  22. package/dist/agent-workspace.js.map +1 -0
  23. package/dist/auth-api.d.ts +27 -0
  24. package/dist/auth-api.js +168 -3
  25. package/dist/auth-api.js.map +1 -1
  26. package/dist/authority/session-authorizations.d.ts +28 -4
  27. package/dist/authority/session-authorizations.js +63 -17
  28. package/dist/authority/session-authorizations.js.map +1 -1
  29. package/dist/backend/antigravity.js +3 -1
  30. package/dist/backend/antigravity.js.map +1 -1
  31. package/dist/backend/codex-footer-hints.d.ts +7 -0
  32. package/dist/backend/codex-footer-hints.js +41 -0
  33. package/dist/backend/codex-footer-hints.js.map +1 -0
  34. package/dist/backend/codex-metadata.d.ts +14 -0
  35. package/dist/backend/codex-metadata.js +45 -0
  36. package/dist/backend/codex-metadata.js.map +1 -1
  37. package/dist/backend/codex-quota-history.d.ts +7 -0
  38. package/dist/backend/codex-quota-history.js +57 -0
  39. package/dist/backend/codex-quota-history.js.map +1 -0
  40. package/dist/backend/codex-session-lookup.d.ts +6 -0
  41. package/dist/backend/codex-session-lookup.js +1 -1
  42. package/dist/backend/codex-session-lookup.js.map +1 -1
  43. package/dist/backend/codex-speed.d.ts +14 -0
  44. package/dist/backend/codex-speed.js +35 -0
  45. package/dist/backend/codex-speed.js.map +1 -0
  46. package/dist/backend/codex.d.ts +7 -0
  47. package/dist/backend/codex.js +85 -24
  48. package/dist/backend/codex.js.map +1 -1
  49. package/dist/backend/grok-weekly-reset.d.ts +24 -0
  50. package/dist/backend/grok-weekly-reset.js +95 -0
  51. package/dist/backend/grok-weekly-reset.js.map +1 -0
  52. package/dist/backend/grok.d.ts +30 -1
  53. package/dist/backend/grok.js +301 -54
  54. package/dist/backend/grok.js.map +1 -1
  55. package/dist/backend/kiro.d.ts +3 -0
  56. package/dist/backend/kiro.js +50 -2
  57. package/dist/backend/kiro.js.map +1 -1
  58. package/dist/backend/muse.js +5 -3
  59. package/dist/backend/muse.js.map +1 -1
  60. package/dist/backend/native-pane.d.ts +3 -1
  61. package/dist/backend/native-pane.js +2 -2
  62. package/dist/backend/native-pane.js.map +1 -1
  63. package/dist/backend/types.d.ts +13 -1
  64. package/dist/backend/types.js +3 -0
  65. package/dist/backend/types.js.map +1 -1
  66. package/dist/build-provenance-cli.d.ts +3 -0
  67. package/dist/build-provenance-cli.js +11 -0
  68. package/dist/build-provenance-cli.js.map +1 -0
  69. package/dist/build-provenance.d.ts +20 -0
  70. package/dist/build-provenance.js +67 -0
  71. package/dist/build-provenance.js.map +1 -0
  72. package/dist/build-provenance.json +1 -0
  73. package/dist/cache-ledger.d.ts +95 -0
  74. package/dist/cache-ledger.js +276 -4
  75. package/dist/cache-ledger.js.map +1 -1
  76. package/dist/cache-service.d.ts +5 -0
  77. package/dist/cache-service.js +16 -4
  78. package/dist/cache-service.js.map +1 -1
  79. package/dist/channel/adapters/discord.js +11 -3
  80. package/dist/channel/adapters/discord.js.map +1 -1
  81. package/dist/channel/adapters/telegram.d.ts +51 -0
  82. package/dist/channel/adapters/telegram.js +235 -17
  83. package/dist/channel/adapters/telegram.js.map +1 -1
  84. package/dist/channel/mcp-tools.js +3 -3
  85. package/dist/channel/mcp-tools.js.map +1 -1
  86. package/dist/channel/types.d.ts +2 -0
  87. package/dist/claude-auth-status.d.ts +48 -0
  88. package/dist/claude-auth-status.js +149 -0
  89. package/dist/claude-auth-status.js.map +1 -0
  90. package/dist/cli.js +138 -37
  91. package/dist/cli.js.map +1 -1
  92. package/dist/config-validator.js +21 -1
  93. package/dist/config-validator.js.map +1 -1
  94. package/dist/cross-instance-notice.d.ts +10 -0
  95. package/dist/cross-instance-notice.js +67 -5
  96. package/dist/cross-instance-notice.js.map +1 -1
  97. package/dist/daemon.d.ts +70 -1
  98. package/dist/daemon.js +547 -25
  99. package/dist/daemon.js.map +1 -1
  100. package/dist/dashboard-permissions.d.ts +8 -0
  101. package/dist/dashboard-permissions.js +16 -0
  102. package/dist/dashboard-permissions.js.map +1 -1
  103. package/dist/delivery-consumed-watch.d.ts +23 -1
  104. package/dist/delivery-consumed-watch.js +30 -3
  105. package/dist/delivery-consumed-watch.js.map +1 -1
  106. package/dist/delivery-reconciliation.d.ts +1 -1
  107. package/dist/delivery-reconciliation.js +1 -1
  108. package/dist/delivery-reconciliation.js.map +1 -1
  109. package/dist/doctor.js +20 -9
  110. package/dist/doctor.js.map +1 -1
  111. package/dist/export-import.d.ts +7 -0
  112. package/dist/export-import.js +44 -10
  113. package/dist/export-import.js.map +1 -1
  114. package/dist/fleet-context.d.ts +2 -0
  115. package/dist/fleet-manager.d.ts +362 -7
  116. package/dist/fleet-manager.js +1522 -91
  117. package/dist/fleet-manager.js.map +1 -1
  118. package/dist/fleet-windows-runtime.d.ts +8 -2
  119. package/dist/fleet-windows-runtime.js +93 -6
  120. package/dist/fleet-windows-runtime.js.map +1 -1
  121. package/dist/general-knowledge/skills/concepts/SKILL.md +425 -0
  122. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  123. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  124. package/dist/general-knowledge/skills/fleet-health/SKILL.md +2 -2
  125. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  126. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  127. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
  128. package/dist/general-knowledge/skills/tui-effort/SKILL.md +10 -10
  129. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +31 -15
  130. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  131. package/dist/general-role.d.ts +1 -1
  132. package/dist/general-role.js +1 -0
  133. package/dist/general-role.js.map +1 -1
  134. package/dist/hub/admission.d.ts +28 -0
  135. package/dist/hub/admission.js +62 -0
  136. package/dist/hub/admission.js.map +1 -0
  137. package/dist/hub/attachments/limits.d.ts +20 -0
  138. package/dist/hub/attachments/limits.js +48 -0
  139. package/dist/hub/attachments/limits.js.map +1 -0
  140. package/dist/hub/attachments/store.d.ts +116 -0
  141. package/dist/hub/attachments/store.js +246 -0
  142. package/dist/hub/attachments/store.js.map +1 -0
  143. package/dist/hub/audit/index.d.ts +62 -0
  144. package/dist/hub/audit/index.js +344 -0
  145. package/dist/hub/audit/index.js.map +1 -0
  146. package/dist/hub/browser-certificate.d.ts +38 -0
  147. package/dist/hub/browser-certificate.js +247 -0
  148. package/dist/hub/browser-certificate.js.map +1 -0
  149. package/dist/hub/buttons-model.d.ts +9 -0
  150. package/dist/hub/buttons-model.js +42 -0
  151. package/dist/hub/buttons-model.js.map +1 -0
  152. package/dist/hub/buttons.d.ts +54 -0
  153. package/dist/hub/buttons.js +346 -0
  154. package/dist/hub/buttons.js.map +1 -0
  155. package/dist/hub/catalog.d.ts +74 -0
  156. package/dist/hub/catalog.js +195 -0
  157. package/dist/hub/catalog.js.map +1 -0
  158. package/dist/hub/client.d.ts +69 -0
  159. package/dist/hub/client.js +300 -0
  160. package/dist/hub/client.js.map +1 -0
  161. package/dist/hub/compose/member.d.ts +139 -0
  162. package/dist/hub/compose/member.js +450 -0
  163. package/dist/hub/compose/member.js.map +1 -0
  164. package/dist/hub/compose/server.d.ts +77 -0
  165. package/dist/hub/compose/server.js +349 -0
  166. package/dist/hub/compose/server.js.map +1 -0
  167. package/dist/hub/compose/views.d.ts +12 -0
  168. package/dist/hub/compose/views.js +4 -0
  169. package/dist/hub/compose/views.js.map +1 -0
  170. package/dist/hub/db.d.ts +56 -0
  171. package/dist/hub/db.js +188 -0
  172. package/dist/hub/db.js.map +1 -0
  173. package/dist/hub/directory/config.d.ts +39 -0
  174. package/dist/hub/directory/config.js +108 -0
  175. package/dist/hub/directory/config.js.map +1 -0
  176. package/dist/hub/directory/directory.d.ts +18 -0
  177. package/dist/hub/directory/directory.js +320 -0
  178. package/dist/hub/directory/directory.js.map +1 -0
  179. package/dist/hub/directory/index.d.ts +7 -0
  180. package/dist/hub/directory/index.js +4 -0
  181. package/dist/hub/directory/index.js.map +1 -0
  182. package/dist/hub/directory/profiles.d.ts +41 -0
  183. package/dist/hub/directory/profiles.js +90 -0
  184. package/dist/hub/directory/profiles.js.map +1 -0
  185. package/dist/hub/directory/types.d.ts +71 -0
  186. package/dist/hub/directory/types.js +14 -0
  187. package/dist/hub/directory/types.js.map +1 -0
  188. package/dist/hub/execution/cli-provider.d.ts +23 -0
  189. package/dist/hub/execution/cli-provider.js +341 -0
  190. package/dist/hub/execution/cli-provider.js.map +1 -0
  191. package/dist/hub/execution/index.d.ts +13 -0
  192. package/dist/hub/execution/index.js +8 -0
  193. package/dist/hub/execution/index.js.map +1 -0
  194. package/dist/hub/execution/model-broker.d.ts +24 -0
  195. package/dist/hub/execution/model-broker.js +208 -0
  196. package/dist/hub/execution/model-broker.js.map +1 -0
  197. package/dist/hub/execution/provider-setup.d.ts +11 -0
  198. package/dist/hub/execution/provider-setup.js +21 -0
  199. package/dist/hub/execution/provider-setup.js.map +1 -0
  200. package/dist/hub/execution/sandbox-runner.d.ts +1 -0
  201. package/dist/hub/execution/sandbox-runner.js +221 -0
  202. package/dist/hub/execution/sandbox-runner.js.map +1 -0
  203. package/dist/hub/execution/service.d.ts +136 -0
  204. package/dist/hub/execution/service.js +459 -0
  205. package/dist/hub/execution/service.js.map +1 -0
  206. package/dist/hub/execution/store.d.ts +61 -0
  207. package/dist/hub/execution/store.js +182 -0
  208. package/dist/hub/execution/store.js.map +1 -0
  209. package/dist/hub/execution/views.d.ts +20 -0
  210. package/dist/hub/execution/views.js +48 -0
  211. package/dist/hub/execution/views.js.map +1 -0
  212. package/dist/hub/http/compose.d.ts +20 -0
  213. package/dist/hub/http/compose.js +518 -0
  214. package/dist/hub/http/compose.js.map +1 -0
  215. package/dist/hub/http/core.d.ts +49 -0
  216. package/dist/hub/http/core.js +124 -0
  217. package/dist/hub/http/core.js.map +1 -0
  218. package/dist/hub/http/deps.d.ts +61 -0
  219. package/dist/hub/http/deps.js +2 -0
  220. package/dist/hub/http/deps.js.map +1 -0
  221. package/dist/hub/http/schema.d.ts +14 -0
  222. package/dist/hub/http/schema.js +80 -0
  223. package/dist/hub/http/schema.js.map +1 -0
  224. package/dist/hub/identity.d.ts +32 -0
  225. package/dist/hub/identity.js +80 -0
  226. package/dist/hub/identity.js.map +1 -0
  227. package/dist/hub/init/browser-certificate.d.ts +9 -0
  228. package/dist/hub/init/browser-certificate.js +78 -0
  229. package/dist/hub/init/browser-certificate.js.map +1 -0
  230. package/dist/hub/init/browser-tls.d.ts +18 -0
  231. package/dist/hub/init/browser-tls.js +62 -0
  232. package/dist/hub/init/browser-tls.js.map +1 -0
  233. package/dist/hub/init/cli.d.ts +3 -0
  234. package/dist/hub/init/cli.js +83 -0
  235. package/dist/hub/init/cli.js.map +1 -0
  236. package/dist/hub/init/directory-certificate.d.ts +24 -0
  237. package/dist/hub/init/directory-certificate.js +222 -0
  238. package/dist/hub/init/directory-certificate.js.map +1 -0
  239. package/dist/hub/init/existing-fleet.d.ts +6 -0
  240. package/dist/hub/init/existing-fleet.js +58 -0
  241. package/dist/hub/init/existing-fleet.js.map +1 -0
  242. package/dist/hub/init/handoff.d.ts +8 -0
  243. package/dist/hub/init/handoff.js +36 -0
  244. package/dist/hub/init/handoff.js.map +1 -0
  245. package/dist/hub/init/identity.d.ts +5 -0
  246. package/dist/hub/init/identity.js +24 -0
  247. package/dist/hub/init/identity.js.map +1 -0
  248. package/dist/hub/init/initializer.d.ts +24 -0
  249. package/dist/hub/init/initializer.js +385 -0
  250. package/dist/hub/init/initializer.js.map +1 -0
  251. package/dist/hub/init/inventory.d.ts +17 -0
  252. package/dist/hub/init/inventory.js +33 -0
  253. package/dist/hub/init/inventory.js.map +1 -0
  254. package/dist/hub/init/layout.d.ts +32 -0
  255. package/dist/hub/init/layout.js +53 -0
  256. package/dist/hub/init/layout.js.map +1 -0
  257. package/dist/hub/init/network.d.ts +18 -0
  258. package/dist/hub/init/network.js +104 -0
  259. package/dist/hub/init/network.js.map +1 -0
  260. package/dist/hub/init/plan-digest.d.ts +5 -0
  261. package/dist/hub/init/plan-digest.js +47 -0
  262. package/dist/hub/init/plan-digest.js.map +1 -0
  263. package/dist/hub/init/prompt.d.ts +26 -0
  264. package/dist/hub/init/prompt.js +110 -0
  265. package/dist/hub/init/prompt.js.map +1 -0
  266. package/dist/hub/init/secret-file.d.ts +1 -0
  267. package/dist/hub/init/secret-file.js +59 -0
  268. package/dist/hub/init/secret-file.js.map +1 -0
  269. package/dist/hub/init/text.d.ts +127 -0
  270. package/dist/hub/init/text.js +247 -0
  271. package/dist/hub/init/text.js.map +1 -0
  272. package/dist/hub/init/types.d.ts +114 -0
  273. package/dist/hub/init/types.js +9 -0
  274. package/dist/hub/init/types.js.map +1 -0
  275. package/dist/hub/init/wizard.d.ts +34 -0
  276. package/dist/hub/init/wizard.js +554 -0
  277. package/dist/hub/init/wizard.js.map +1 -0
  278. package/dist/hub/initialization.d.ts +23 -0
  279. package/dist/hub/initialization.js +28 -0
  280. package/dist/hub/initialization.js.map +1 -0
  281. package/dist/hub/member/agent-mode.d.ts +6 -0
  282. package/dist/hub/member/agent-mode.js +23 -0
  283. package/dist/hub/member/agent-mode.js.map +1 -0
  284. package/dist/hub/member/attachments.d.ts +29 -0
  285. package/dist/hub/member/attachments.js +107 -0
  286. package/dist/hub/member/attachments.js.map +1 -0
  287. package/dist/hub/member/exposure-api.d.ts +87 -0
  288. package/dist/hub/member/exposure-api.js +64 -0
  289. package/dist/hub/member/exposure-api.js.map +1 -0
  290. package/dist/hub/member/exposures.d.ts +123 -0
  291. package/dist/hub/member/exposures.js +282 -0
  292. package/dist/hub/member/exposures.js.map +1 -0
  293. package/dist/hub/member/pairing-api.d.ts +85 -0
  294. package/dist/hub/member/pairing-api.js +385 -0
  295. package/dist/hub/member/pairing-api.js.map +1 -0
  296. package/dist/hub/member/quota.d.ts +20 -0
  297. package/dist/hub/member/quota.js +98 -0
  298. package/dist/hub/member/quota.js.map +1 -0
  299. package/dist/hub/member-journal.d.ts +104 -0
  300. package/dist/hub/member-journal.js +336 -0
  301. package/dist/hub/member-journal.js.map +1 -0
  302. package/dist/hub/member-reconciliation.d.ts +14 -0
  303. package/dist/hub/member-reconciliation.js +97 -0
  304. package/dist/hub/member-reconciliation.js.map +1 -0
  305. package/dist/hub/mount.d.ts +39 -0
  306. package/dist/hub/mount.js +123 -0
  307. package/dist/hub/mount.js.map +1 -0
  308. package/dist/hub/pairing-client.d.ts +17 -0
  309. package/dist/hub/pairing-client.js +108 -0
  310. package/dist/hub/pairing-client.js.map +1 -0
  311. package/dist/hub/pairing.d.ts +65 -0
  312. package/dist/hub/pairing.js +211 -0
  313. package/dist/hub/pairing.js.map +1 -0
  314. package/dist/hub/policy.d.ts +63 -0
  315. package/dist/hub/policy.js +147 -0
  316. package/dist/hub/policy.js.map +1 -0
  317. package/dist/hub/preview.d.ts +32 -0
  318. package/dist/hub/preview.js +64 -0
  319. package/dist/hub/preview.js.map +1 -0
  320. package/dist/hub/private-files.d.ts +5 -0
  321. package/dist/hub/private-files.js +44 -0
  322. package/dist/hub/private-files.js.map +1 -0
  323. package/dist/hub/profile.d.ts +59 -0
  324. package/dist/hub/profile.js +73 -0
  325. package/dist/hub/profile.js.map +1 -0
  326. package/dist/hub/protocol.d.ts +779 -0
  327. package/dist/hub/protocol.js +93 -0
  328. package/dist/hub/protocol.js.map +1 -0
  329. package/dist/hub/quota.d.ts +24 -0
  330. package/dist/hub/quota.js +112 -0
  331. package/dist/hub/quota.js.map +1 -0
  332. package/dist/hub/representative.d.ts +141 -0
  333. package/dist/hub/representative.js +528 -0
  334. package/dist/hub/representative.js.map +1 -0
  335. package/dist/hub/retention.d.ts +17 -0
  336. package/dist/hub/retention.js +45 -0
  337. package/dist/hub/retention.js.map +1 -0
  338. package/dist/hub/rooms.d.ts +120 -0
  339. package/dist/hub/rooms.js +535 -0
  340. package/dist/hub/rooms.js.map +1 -0
  341. package/dist/hub/runtime.d.ts +106 -0
  342. package/dist/hub/runtime.js +298 -0
  343. package/dist/hub/runtime.js.map +1 -0
  344. package/dist/hub/server.d.ts +56 -0
  345. package/dist/hub/server.js +202 -0
  346. package/dist/hub/server.js.map +1 -0
  347. package/dist/hub/service-start.d.ts +17 -0
  348. package/dist/hub/service-start.js +109 -0
  349. package/dist/hub/service-start.js.map +1 -0
  350. package/dist/hub/sessions.d.ts +53 -0
  351. package/dist/hub/sessions.js +196 -0
  352. package/dist/hub/sessions.js.map +1 -0
  353. package/dist/hub/socket.d.ts +24 -0
  354. package/dist/hub/socket.js +79 -0
  355. package/dist/hub/socket.js.map +1 -0
  356. package/dist/hub/store.d.ts +77 -0
  357. package/dist/hub/store.js +88 -0
  358. package/dist/hub/store.js.map +1 -0
  359. package/dist/hub/tls.d.ts +31 -0
  360. package/dist/hub/tls.js +135 -0
  361. package/dist/hub/tls.js.map +1 -0
  362. package/dist/hub/types.d.ts +401 -0
  363. package/dist/hub/types.js +2 -0
  364. package/dist/hub/types.js.map +1 -0
  365. package/dist/hub/validation.d.ts +33 -0
  366. package/dist/hub/validation.js +136 -0
  367. package/dist/hub/validation.js.map +1 -0
  368. package/dist/hub/version-floor.d.ts +10 -0
  369. package/dist/hub/version-floor.js +45 -0
  370. package/dist/hub/version-floor.js.map +1 -0
  371. package/dist/hub-browser-certificate-policy.d.ts +31 -0
  372. package/dist/hub-browser-certificate-policy.js +25 -0
  373. package/dist/hub-browser-certificate-policy.js.map +1 -0
  374. package/dist/hub-membership-settings.d.ts +6 -0
  375. package/dist/hub-membership-settings.js +21 -0
  376. package/dist/hub-membership-settings.js.map +1 -0
  377. package/dist/hub-mode.d.ts +15 -0
  378. package/dist/hub-mode.js +17 -0
  379. package/dist/hub-mode.js.map +1 -0
  380. package/dist/hub-pairing-summary.d.ts +48 -0
  381. package/dist/hub-pairing-summary.js +36 -0
  382. package/dist/hub-pairing-summary.js.map +1 -0
  383. package/dist/hub-report-target.d.ts +32 -0
  384. package/dist/hub-report-target.js +33 -0
  385. package/dist/hub-report-target.js.map +1 -0
  386. package/dist/hub-retention-policy.d.ts +14 -0
  387. package/dist/hub-retention-policy.js +15 -0
  388. package/dist/hub-retention-policy.js.map +1 -0
  389. package/dist/instance-config-impact.js +3 -0
  390. package/dist/instance-config-impact.js.map +1 -1
  391. package/dist/instance-lifecycle.d.ts +17 -2
  392. package/dist/instance-lifecycle.js +58 -8
  393. package/dist/instance-lifecycle.js.map +1 -1
  394. package/dist/instance-topic-delete.d.ts +77 -0
  395. package/dist/instance-topic-delete.js +123 -0
  396. package/dist/instance-topic-delete.js.map +1 -0
  397. package/dist/instructions.d.ts +1 -1
  398. package/dist/instructions.js +1 -1
  399. package/dist/instructions.js.map +1 -1
  400. package/dist/kiro-engine-status.d.ts +11 -0
  401. package/dist/kiro-engine-status.js +6 -0
  402. package/dist/kiro-engine-status.js.map +1 -1
  403. package/dist/launchd-service-domain.d.ts +5 -0
  404. package/dist/launchd-service-domain.js +21 -0
  405. package/dist/launchd-service-domain.js.map +1 -0
  406. package/dist/locale.js +125 -3
  407. package/dist/locale.js.map +1 -1
  408. package/dist/log-service.d.ts +28 -0
  409. package/dist/log-service.js +29 -0
  410. package/dist/log-service.js.map +1 -1
  411. package/dist/log-steps.d.ts +4 -3
  412. package/dist/log-steps.js +32 -7
  413. package/dist/log-steps.js.map +1 -1
  414. package/dist/logger.d.ts +16 -2
  415. package/dist/logger.js +346 -66
  416. package/dist/logger.js.map +1 -1
  417. package/dist/login-flows.d.ts +15 -7
  418. package/dist/login-flows.js +29 -3
  419. package/dist/login-flows.js.map +1 -1
  420. package/dist/login-manager.js +25 -4
  421. package/dist/login-manager.js.map +1 -1
  422. package/dist/ls-rows.js +3 -4
  423. package/dist/ls-rows.js.map +1 -1
  424. package/dist/missed-offline.d.ts +25 -0
  425. package/dist/missed-offline.js +79 -0
  426. package/dist/missed-offline.js.map +1 -0
  427. package/dist/native-instance-api.d.ts +0 -1
  428. package/dist/native-instance-api.js +15 -3
  429. package/dist/native-instance-api.js.map +1 -1
  430. package/dist/native-instance-input.d.ts +3 -0
  431. package/dist/native-instance-input.js +6 -1
  432. package/dist/native-instance-input.js.map +1 -1
  433. package/dist/native-instance-registry.d.ts +6 -0
  434. package/dist/native-instance-registry.js +24 -0
  435. package/dist/native-instance-registry.js.map +1 -1
  436. package/dist/native-mcp.d.ts +3 -0
  437. package/dist/native-mcp.js +15 -2
  438. package/dist/native-mcp.js.map +1 -1
  439. package/dist/native-pane-lifecycle.d.ts +8 -0
  440. package/dist/native-pane-lifecycle.js +13 -4
  441. package/dist/native-pane-lifecycle.js.map +1 -1
  442. package/dist/native-pane-manager.js +17 -9
  443. package/dist/native-pane-manager.js.map +1 -1
  444. package/dist/needs-you-hub.d.ts +9 -1
  445. package/dist/needs-you-hub.js +46 -8
  446. package/dist/needs-you-hub.js.map +1 -1
  447. package/dist/needs-you.d.ts +47 -2
  448. package/dist/needs-you.js +31 -3
  449. package/dist/needs-you.js.map +1 -1
  450. package/dist/office-share-page.js +3 -3
  451. package/dist/office-share-page.js.map +1 -1
  452. package/dist/office-share.d.ts +11 -0
  453. package/dist/office-share.js +14 -0
  454. package/dist/office-share.js.map +1 -1
  455. package/dist/office-talk.d.ts +26 -0
  456. package/dist/office-talk.js +50 -0
  457. package/dist/office-talk.js.map +1 -0
  458. package/dist/outbound-handlers.d.ts +14 -0
  459. package/dist/outbound-handlers.js +49 -6
  460. package/dist/outbound-handlers.js.map +1 -1
  461. package/dist/outbound-schemas.d.ts +33 -1
  462. package/dist/outbound-schemas.js +29 -16
  463. package/dist/outbound-schemas.js.map +1 -1
  464. package/dist/pane-quota-history.d.ts +9 -0
  465. package/dist/pane-quota-history.js +67 -0
  466. package/dist/pane-quota-history.js.map +1 -0
  467. package/dist/pause-marker.d.ts +1 -1
  468. package/dist/pause-marker.js +1 -1
  469. package/dist/pause-marker.js.map +1 -1
  470. package/dist/platform-general.d.ts +46 -0
  471. package/dist/platform-general.js +91 -0
  472. package/dist/platform-general.js.map +1 -0
  473. package/dist/public-web-gateway.js +7 -0
  474. package/dist/public-web-gateway.js.map +1 -1
  475. package/dist/public-web-link.d.ts +6 -0
  476. package/dist/public-web-link.js +9 -0
  477. package/dist/public-web-link.js.map +1 -1
  478. package/dist/queued-handoffs.d.ts +33 -0
  479. package/dist/queued-handoffs.js +75 -0
  480. package/dist/queued-handoffs.js.map +1 -0
  481. package/dist/quickstart.d.ts +13 -0
  482. package/dist/quickstart.js +81 -5
  483. package/dist/quickstart.js.map +1 -1
  484. package/dist/quota-notice.d.ts +7 -0
  485. package/dist/quota-notice.js +40 -0
  486. package/dist/quota-notice.js.map +1 -1
  487. package/dist/reply-buttons-controller.js +10 -6
  488. package/dist/reply-buttons-controller.js.map +1 -1
  489. package/dist/reply-polls.d.ts +2 -2
  490. package/dist/reply-polls.js +3 -3
  491. package/dist/reply-polls.js.map +1 -1
  492. package/dist/runtime-api.d.ts +2 -0
  493. package/dist/runtime-api.js +106 -13
  494. package/dist/runtime-api.js.map +1 -1
  495. package/dist/runtime-build-info.d.ts +9 -0
  496. package/dist/runtime-build-info.js +59 -0
  497. package/dist/runtime-build-info.js.map +1 -0
  498. package/dist/runtime-bundle.d.ts +6 -1
  499. package/dist/runtime-bundle.js +17 -9
  500. package/dist/runtime-bundle.js.map +1 -1
  501. package/dist/runtime-cli.d.ts +13 -1
  502. package/dist/runtime-cli.js +173 -19
  503. package/dist/runtime-cli.js.map +1 -1
  504. package/dist/runtime-client-store.d.ts +27 -1
  505. package/dist/runtime-client-store.js +86 -6
  506. package/dist/runtime-client-store.js.map +1 -1
  507. package/dist/runtime-client.d.ts +74 -0
  508. package/dist/runtime-client.js +336 -26
  509. package/dist/runtime-client.js.map +1 -1
  510. package/dist/runtime-connection.d.ts +22 -0
  511. package/dist/runtime-connection.js +93 -0
  512. package/dist/runtime-connection.js.map +1 -0
  513. package/dist/runtime-coordinator.d.ts +16 -0
  514. package/dist/runtime-coordinator.js +287 -62
  515. package/dist/runtime-coordinator.js.map +1 -1
  516. package/dist/runtime-credential-profiles.d.ts +78 -0
  517. package/dist/runtime-credential-profiles.js +280 -0
  518. package/dist/runtime-credential-profiles.js.map +1 -0
  519. package/dist/runtime-install.d.ts +50 -0
  520. package/dist/runtime-install.js +186 -0
  521. package/dist/runtime-install.js.map +1 -0
  522. package/dist/runtime-pane-assets.d.ts +1 -1
  523. package/dist/runtime-pane-assets.js +1 -1
  524. package/dist/runtime-pane-assets.js.map +1 -1
  525. package/dist/runtime-pane-boot.d.ts +7 -0
  526. package/dist/runtime-pane-boot.js +23 -0
  527. package/dist/runtime-pane-boot.js.map +1 -1
  528. package/dist/runtime-pane-codex-index.d.ts +10 -1
  529. package/dist/runtime-pane-codex-index.js +131 -27
  530. package/dist/runtime-pane-codex-index.js.map +1 -1
  531. package/dist/runtime-pane-host.d.ts +19 -0
  532. package/dist/runtime-pane-host.js +50 -7
  533. package/dist/runtime-pane-host.js.map +1 -1
  534. package/dist/runtime-pane-link.d.ts +7 -0
  535. package/dist/runtime-pane-link.js +93 -2
  536. package/dist/runtime-pane-link.js.map +1 -1
  537. package/dist/runtime-pane-mcp-relay.d.ts +1 -0
  538. package/dist/runtime-pane-mcp-relay.js +10 -0
  539. package/dist/runtime-pane-mcp-relay.js.map +1 -1
  540. package/dist/runtime-pane-mcp.d.ts +4 -0
  541. package/dist/runtime-pane-mcp.js +6 -3
  542. package/dist/runtime-pane-mcp.js.map +1 -1
  543. package/dist/runtime-pane-native.d.ts +32 -2
  544. package/dist/runtime-pane-native.js +55 -6
  545. package/dist/runtime-pane-native.js.map +1 -1
  546. package/dist/runtime-pane-port.d.ts +5 -0
  547. package/dist/runtime-pane-port.js +17 -1
  548. package/dist/runtime-pane-port.js.map +1 -1
  549. package/dist/runtime-pane-process.d.ts +1 -1
  550. package/dist/runtime-pane-process.js +11 -2
  551. package/dist/runtime-pane-process.js.map +1 -1
  552. package/dist/runtime-pane-protocol.d.ts +11 -1
  553. package/dist/runtime-pane-protocol.js +27 -5
  554. package/dist/runtime-pane-protocol.js.map +1 -1
  555. package/dist/runtime-pane-quota-history.d.ts +6 -0
  556. package/dist/runtime-pane-quota-history.js +44 -0
  557. package/dist/runtime-pane-quota-history.js.map +1 -0
  558. package/dist/runtime-pane-service.d.ts +5 -0
  559. package/dist/runtime-pane-service.js +47 -12
  560. package/dist/runtime-pane-service.js.map +1 -1
  561. package/dist/runtime-pane-transcript.d.ts +5 -0
  562. package/dist/runtime-pane-transcript.js +66 -5
  563. package/dist/runtime-pane-transcript.js.map +1 -1
  564. package/dist/runtime-pane-worker.js +19 -6
  565. package/dist/runtime-pane-worker.js.map +1 -1
  566. package/dist/runtime-protocol.d.ts +30 -2
  567. package/dist/runtime-protocol.js +30 -0
  568. package/dist/runtime-protocol.js.map +1 -1
  569. package/dist/runtime-update-activity.d.ts +21 -0
  570. package/dist/runtime-update-activity.js +51 -0
  571. package/dist/runtime-update-activity.js.map +1 -0
  572. package/dist/runtime-update-artifact.d.ts +31 -0
  573. package/dist/runtime-update-artifact.js +252 -0
  574. package/dist/runtime-update-artifact.js.map +1 -0
  575. package/dist/runtime-update-install.d.ts +31 -0
  576. package/dist/runtime-update-install.js +205 -0
  577. package/dist/runtime-update-install.js.map +1 -0
  578. package/dist/runtime-update-journal.d.ts +44 -0
  579. package/dist/runtime-update-journal.js +121 -0
  580. package/dist/runtime-update-journal.js.map +1 -0
  581. package/dist/runtime-update-lock.d.ts +48 -0
  582. package/dist/runtime-update-lock.js +217 -0
  583. package/dist/runtime-update-lock.js.map +1 -0
  584. package/dist/runtime-update-owners.d.ts +29 -0
  585. package/dist/runtime-update-owners.js +240 -0
  586. package/dist/runtime-update-owners.js.map +1 -0
  587. package/dist/runtime-update-protocol.d.ts +15 -0
  588. package/dist/runtime-update-protocol.js +13 -0
  589. package/dist/runtime-update-protocol.js.map +1 -0
  590. package/dist/runtime-windows-bridge.d.ts +2 -1
  591. package/dist/runtime-windows-bridge.js +11 -1
  592. package/dist/runtime-windows-bridge.js.map +1 -1
  593. package/dist/scheduler/db.d.ts +2 -1
  594. package/dist/scheduler/db.js +23 -9
  595. package/dist/scheduler/db.js.map +1 -1
  596. package/dist/scheduler/task-board-text.js +1 -1
  597. package/dist/scheduler/task-board-text.js.map +1 -1
  598. package/dist/scheduler/task-board.d.ts +13 -0
  599. package/dist/scheduler/task-board.js +28 -3
  600. package/dist/scheduler/task-board.js.map +1 -1
  601. package/dist/service-drain.d.ts +19 -0
  602. package/dist/service-drain.js +64 -0
  603. package/dist/service-drain.js.map +1 -0
  604. package/dist/service-installer.d.ts +7 -2
  605. package/dist/service-installer.js +13 -10
  606. package/dist/service-installer.js.map +1 -1
  607. package/dist/settings-api.d.ts +20 -0
  608. package/dist/settings-api.js +414 -4
  609. package/dist/settings-api.js.map +1 -1
  610. package/dist/settings-baseline.d.ts +6 -0
  611. package/dist/settings-baseline.js +3 -1
  612. package/dist/settings-baseline.js.map +1 -1
  613. package/dist/settings-change.js +29 -6
  614. package/dist/settings-change.js.map +1 -1
  615. package/dist/settings-effect.d.ts +13 -0
  616. package/dist/settings-effect.js +98 -1
  617. package/dist/settings-effect.js.map +1 -1
  618. package/dist/settings-http-confirmation.d.ts +3 -0
  619. package/dist/settings-http-confirmation.js +28 -1
  620. package/dist/settings-http-confirmation.js.map +1 -1
  621. package/dist/settings-instance-patch.js +27 -0
  622. package/dist/settings-instance-patch.js.map +1 -1
  623. package/dist/setup-wizard.js +6 -2
  624. package/dist/setup-wizard.js.map +1 -1
  625. package/dist/task-board-tool.d.ts +13 -1
  626. package/dist/task-board-tool.js +104 -4
  627. package/dist/task-board-tool.js.map +1 -1
  628. package/dist/task-board-waker.d.ts +6 -0
  629. package/dist/task-board-waker.js +4 -0
  630. package/dist/task-board-waker.js.map +1 -1
  631. package/dist/tmux-binary.d.ts +59 -0
  632. package/dist/tmux-binary.js +200 -0
  633. package/dist/tmux-binary.js.map +1 -0
  634. package/dist/tmux-control.js +4 -4
  635. package/dist/tmux-control.js.map +1 -1
  636. package/dist/tmux-exec-helper.js +32 -1
  637. package/dist/tmux-exec-helper.js.map +1 -1
  638. package/dist/tmux-manager.d.ts +1 -1
  639. package/dist/tmux-manager.js +45 -36
  640. package/dist/tmux-manager.js.map +1 -1
  641. package/dist/tmux-read.js +4 -3
  642. package/dist/tmux-read.js.map +1 -1
  643. package/dist/tool-permissions.d.ts +5 -2
  644. package/dist/tool-permissions.js +6 -5
  645. package/dist/tool-permissions.js.map +1 -1
  646. package/dist/tool-progress.d.ts +2 -0
  647. package/dist/tool-progress.js +19 -3
  648. package/dist/tool-progress.js.map +1 -1
  649. package/dist/topic-commands.js +10 -5
  650. package/dist/topic-commands.js.map +1 -1
  651. package/dist/transcript-monitor.d.ts +20 -0
  652. package/dist/transcript-monitor.js +47 -3
  653. package/dist/transcript-monitor.js.map +1 -1
  654. package/dist/transcript-needles.d.ts +7 -0
  655. package/dist/transcript-needles.js +9 -1
  656. package/dist/transcript-needles.js.map +1 -1
  657. package/dist/transcript-records.d.ts +1 -1
  658. package/dist/transcript-records.js +2 -1
  659. package/dist/transcript-records.js.map +1 -1
  660. package/dist/types.d.ts +6 -0
  661. package/dist/ui/account-identity.js +27 -0
  662. package/dist/ui/accounts-view.js +214 -0
  663. package/dist/ui/agent-runtime-settings.js +68 -0
  664. package/dist/ui/agent-runtime.js +20 -2
  665. package/dist/ui/backend-signin.js +39 -21
  666. package/dist/ui/chat-ambient.js +1 -1
  667. package/dist/ui/chat-md-strings.js +3 -0
  668. package/dist/ui/chat-render.js +3 -670
  669. package/dist/ui/chat-store.js +1 -1
  670. package/dist/ui/chat-strings.js +3 -333
  671. package/dist/ui/chat-thread.js +3 -919
  672. package/dist/ui/chat-time.js +3 -80
  673. package/dist/ui/chat-tour.js +1 -1
  674. package/dist/ui/credential-profile-select.js +54 -0
  675. package/dist/ui/fleet-office-dialogue.d.ts +30 -0
  676. package/dist/ui/fleet-office-dialogue.js +382 -0
  677. package/dist/ui/fleet-office-pack.d.ts +9 -0
  678. package/dist/ui/fleet-office-pack.js +76 -4
  679. package/dist/ui/fleet-office-skin.js +23 -5
  680. package/dist/ui/fleet-office-store.js +5 -0
  681. package/dist/ui/fleet-office.js +173 -6
  682. package/dist/ui/fleet-task-card.js +10 -6
  683. package/dist/ui/fleet-task-common.js +12 -2
  684. package/dist/ui/fleet-task-menu.js +188 -0
  685. package/dist/ui/fleet-task-stats.js +124 -0
  686. package/dist/ui/fleet-task-view.js +340 -0
  687. package/dist/ui/fleet-tasks.js +131 -43
  688. package/dist/ui/fleet-usage.js +90 -27
  689. package/dist/ui/html-attachment.js +3 -40
  690. package/dist/ui/hub-agent-copy.js +23 -0
  691. package/dist/ui/hub-button-actions.js +65 -0
  692. package/dist/ui/hub-chat-store.js +158 -0
  693. package/dist/ui/hub-chat-strings.js +77 -0
  694. package/dist/ui/hub-client.js +329 -0
  695. package/dist/ui/hub-owner-strings.js +51 -0
  696. package/dist/ui/hub-provider-guidance.js +31 -0
  697. package/dist/ui/hub-quota.js +85 -0
  698. package/dist/ui/hub-strings.js +162 -0
  699. package/dist/ui/hub-switcher.js +27 -0
  700. package/dist/ui/native-instance-settings.js +5 -8
  701. package/dist/ui/native-pane-recovery.js +1 -1
  702. package/dist/ui/office-share-page.js +4 -2
  703. package/dist/ui/panel-chat.js +36 -49
  704. package/dist/ui/panel-details.js +1 -1
  705. package/dist/ui/panel-fleet.js +82 -12
  706. package/dist/ui/panel-hub.js +619 -0
  707. package/dist/ui/panel-log.js +2 -2
  708. package/dist/ui/panel-needs.js +24 -8
  709. package/dist/ui/panel-runtimes.js +239 -45
  710. package/dist/ui/panel-settings.js +266 -192
  711. package/dist/ui/panel-windows-cli.js +88 -42
  712. package/dist/ui/preview-pages.js +1 -1
  713. package/dist/ui/preview-panel.js +92 -6
  714. package/dist/ui/preview-versions.js +1 -1
  715. package/dist/ui/preview.js +3 -476
  716. package/dist/ui/runtime-commands.d.ts +4 -0
  717. package/dist/ui/runtime-commands.js +59 -0
  718. package/dist/ui/search-dialog.js +2 -0
  719. package/dist/ui/settings-access-strings.js +42 -0
  720. package/dist/ui/settings-agent-filters.js +94 -0
  721. package/dist/ui/settings-appearance.js +122 -23
  722. package/dist/ui/settings-build-version.js +35 -0
  723. package/dist/ui/settings-codex-copy.js +19 -0
  724. package/dist/ui/settings-devices.js +253 -0
  725. package/dist/ui/settings-dialogs.js +33 -12
  726. package/dist/ui/settings-guests.js +25 -11
  727. package/dist/ui/settings-hub-browser-certificate.js +74 -0
  728. package/dist/ui/settings-hub-membership.js +72 -0
  729. package/dist/ui/settings-hub-retention.js +52 -0
  730. package/dist/ui/settings-hub.js +196 -0
  731. package/dist/ui/settings-layout.js +242 -0
  732. package/dist/ui/settings-model.js +24 -2
  733. package/dist/ui/settings-outside.js +104 -38
  734. package/dist/ui/settings-registry.js +9 -4
  735. package/dist/ui/settings-shared-strings.js +143 -0
  736. package/dist/ui/settings-shared.js +345 -0
  737. package/dist/ui/settings-shortcuts.js +65 -0
  738. package/dist/ui/settings-strings.js +89 -32
  739. package/dist/ui/settings-web-authority.js +17 -11
  740. package/dist/ui/shared/accounts.css +15 -0
  741. package/dist/ui/shared/app-i18n.js +2 -2
  742. package/dist/ui/shared/app-needs.js +1 -1
  743. package/dist/ui/shared/app-route.js +6 -1
  744. package/dist/ui/shared/app-session.js +6 -2
  745. package/dist/ui/shared/app-shell.js +81 -15
  746. package/dist/ui/shared/app.css +1114 -13
  747. package/dist/ui/shared/app.js +12 -3
  748. package/dist/ui/shared/chat-composer.js +68 -0
  749. package/dist/ui/shared/chat-md-strings.js +9 -0
  750. package/dist/ui/shared/chat-render.js +682 -0
  751. package/dist/ui/shared/chat-strings.js +333 -0
  752. package/dist/ui/shared/chat-thread.js +969 -0
  753. package/dist/ui/shared/chat-time.js +80 -0
  754. package/dist/ui/shared/header-tools.js +3 -5
  755. package/dist/ui/shared/html-attachment.js +42 -0
  756. package/dist/ui/shared/hub-chat.css +85 -0
  757. package/dist/ui/shared/hub-owner.css +33 -0
  758. package/dist/ui/shared/hub-quota.css +8 -0
  759. package/dist/ui/shared/hub.css +71 -0
  760. package/dist/ui/shared/instance-nav.js +3 -2
  761. package/dist/ui/shared/machine-name.js +13 -0
  762. package/dist/ui/shared/page-head.js +8 -0
  763. package/dist/ui/shared/panel-view.js +77 -4
  764. package/dist/ui/shared/preview.js +476 -0
  765. package/dist/ui/shared/runtime-install.css +26 -0
  766. package/dist/ui/shared/settings-width.js +31 -0
  767. package/dist/ui/shared/side-rail.js +15 -0
  768. package/dist/ui/shared/signin.js +172 -0
  769. package/dist/ui/shared/task-board.css +95 -0
  770. package/dist/ui/shared/tokens.css +1 -0
  771. package/dist/ui/shared/ui-dialog.js +2 -2
  772. package/dist/ui/shared/ui-menu.js +3 -3
  773. package/dist/ui/shared/usage-dialog.js +18 -1
  774. package/dist/ui/shared/view-history.css +17 -0
  775. package/dist/ui/shared/view-history.js +438 -0
  776. package/dist/ui/shared/view-strings.js +16 -0
  777. package/dist/ui/shared/workspace.css +20 -0
  778. package/dist/ui/signin.html +21 -0
  779. package/dist/ui/windows-cli-catalog.js +41 -12
  780. package/dist/ui/workspace-panel.js +189 -0
  781. package/dist/ui/workspace-strings.js +35 -0
  782. package/dist/unattended-install.d.ts +11 -2
  783. package/dist/unattended-install.js +41 -6
  784. package/dist/unattended-install.js.map +1 -1
  785. package/dist/usage/capacity.d.ts +69 -0
  786. package/dist/usage/capacity.js +119 -0
  787. package/dist/usage/capacity.js.map +1 -0
  788. package/dist/usage/i18n-keys.d.ts +1 -1
  789. package/dist/usage/i18n-keys.js +3 -0
  790. package/dist/usage/i18n-keys.js.map +1 -1
  791. package/dist/usage/profile-timeline.d.ts +16 -0
  792. package/dist/usage/profile-timeline.js +58 -0
  793. package/dist/usage/profile-timeline.js.map +1 -0
  794. package/dist/usage/providers.js +2 -1
  795. package/dist/usage/providers.js.map +1 -1
  796. package/dist/usage/quota-history.d.ts +43 -0
  797. package/dist/usage/quota-history.js +168 -0
  798. package/dist/usage/quota-history.js.map +1 -0
  799. package/dist/usage/usage-api.d.ts +31 -0
  800. package/dist/usage/usage-api.js +80 -1
  801. package/dist/usage/usage-api.js.map +1 -1
  802. package/dist/usage-report.d.ts +25 -1
  803. package/dist/usage-report.js +55 -4
  804. package/dist/usage-report.js.map +1 -1
  805. package/dist/usage-rollup.d.ts +6 -1
  806. package/dist/usage-rollup.js +16 -4
  807. package/dist/usage-rollup.js.map +1 -1
  808. package/dist/view-api.d.ts +16 -0
  809. package/dist/view-api.js +105 -23
  810. package/dist/view-api.js.map +1 -1
  811. package/dist/view-history.d.ts +84 -0
  812. package/dist/view-history.js +116 -0
  813. package/dist/view-history.js.map +1 -0
  814. package/dist/web-api.d.ts +49 -2
  815. package/dist/web-api.js +279 -8
  816. package/dist/web-api.js.map +1 -1
  817. package/dist/web-backend-login.d.ts +23 -1
  818. package/dist/web-backend-login.js +11 -17
  819. package/dist/web-backend-login.js.map +1 -1
  820. package/dist/web-device-login.d.ts +153 -0
  821. package/dist/web-device-login.js +278 -0
  822. package/dist/web-device-login.js.map +1 -0
  823. package/dist/web-office-skins.d.ts +9 -0
  824. package/dist/web-office-skins.js +16 -1
  825. package/dist/web-office-skins.js.map +1 -1
  826. package/dist/web-session-authorization.d.ts +13 -2
  827. package/dist/web-session-authorization.js +10 -3
  828. package/dist/web-session-authorization.js.map +1 -1
  829. package/dist/web-session.d.ts +35 -0
  830. package/dist/web-session.js +7 -0
  831. package/dist/web-session.js.map +1 -1
  832. package/dist/web-settings-patch.d.ts +3 -1
  833. package/dist/web-settings-patch.js +4 -1
  834. package/dist/web-settings-patch.js.map +1 -1
  835. package/dist/web-shell-routes.d.ts +6 -0
  836. package/dist/web-shell-routes.js +7 -0
  837. package/dist/web-shell-routes.js.map +1 -1
  838. package/dist/web-terminal.d.ts +4 -30
  839. package/dist/web-terminal.js +9 -6
  840. package/dist/web-terminal.js.map +1 -1
  841. package/dist/windows-agents.d.ts +4 -1
  842. package/dist/windows-agents.js +14 -10
  843. package/dist/windows-agents.js.map +1 -1
  844. package/dist/windows-cli-api.d.ts +12 -1
  845. package/dist/windows-cli-api.js +29 -2
  846. package/dist/windows-cli-api.js.map +1 -1
  847. package/dist/windows-cli-providers.d.ts +0 -9
  848. package/dist/windows-cli-providers.js +4 -9
  849. package/dist/windows-cli-providers.js.map +1 -1
  850. package/dist/windows-cli-worker.d.ts +25 -0
  851. package/dist/windows-cli-worker.js +98 -7
  852. package/dist/windows-cli-worker.js.map +1 -1
  853. package/dist/windows-cli.d.ts +11 -0
  854. package/dist/windows-cli.js +17 -3
  855. package/dist/windows-cli.js.map +1 -1
  856. package/dist/yaml-shared-nodes.js +8 -3
  857. package/dist/yaml-shared-nodes.js.map +1 -1
  858. package/launcher/agend-install.cjs +8 -1
  859. package/launcher/launchd-session-type.cjs +14 -0
  860. package/launcher/postinstall.cjs +27 -2
  861. package/launcher/private-home.cjs +1 -1
  862. package/launcher/tmux-bundle.cjs +247 -0
  863. package/package.json +3 -2
  864. package/templates/launchd.plist.ejs +3 -1
  865. package/templates/runtime-install/install.ps1 +270 -0
@@ -1,19 +1,23 @@
1
+ import { readInstalledBuild } from "./build-provenance.js";
1
2
  import { buildNativeFleetInstructions } from "./instructions.js";
2
3
  import { GENERAL_ROLE_INSTRUCTIONS } from "./general-role.js";
3
4
  import { NativePaneRecoveryError } from "./native-pane-recovery.js";
4
5
  import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
5
6
  import { windowsAgentsFor } from "./windows-agents.js";
7
+ import { AgentRuntimeChanges } from "./agent-runtime-change.js";
6
8
  import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
7
9
  import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
8
10
  import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
9
11
  import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
10
12
  import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
13
+ import { maskSecrets } from "./log-steps.js";
11
14
  import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
12
15
  import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
13
16
  import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
14
17
  import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
15
18
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
16
19
  import { SettingsBaselines } from "./settings-baseline.js";
20
+ import { hubMembershipSettings } from "./hub-membership-settings.js";
17
21
  import { prepareSettingsEffect } from "./settings-effect.js";
18
22
  import { settingsDisplay } from "./settings-change.js";
19
23
  import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
@@ -38,11 +42,13 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
38
42
  import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
39
43
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
40
44
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
41
- import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
45
+ import { installExitMarker, unattendedInstallCommand, loginShell } from "./unattended-install.js";
42
46
  import { InstallStream } from "./install-stream.js";
43
47
  import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
44
48
  import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
45
49
  import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
50
+ import { OfficeTalk } from "./office-talk.js";
51
+ import { pairingSummary } from "./hub-pairing-summary.js";
46
52
  import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
47
53
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
48
54
  import { ManagedTunnel } from "./tunnel/manager.js";
@@ -79,6 +85,11 @@ import { tmuxExecHelper } from "./tmux-exec-helper.js";
79
85
  import { kiroLogGate } from "./kiro-log-gate.js";
80
86
  const __filename = fileURLToPath(import.meta.url);
81
87
  const __dirname = dirname(__filename);
88
+ // Describe this running program, not files an updater may replace later. No reads on dashboard polls.
89
+ const RUNNING_BUILD = readInstalledBuild();
90
+ if (RUNNING_BUILD.source)
91
+ Object.freeze(RUNNING_BUILD.source);
92
+ Object.freeze(RUNNING_BUILD);
82
93
  /** Fallback access policy for a channel with no `access:` block — open (no gate). */
83
94
  const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
84
95
  import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
@@ -86,6 +97,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
86
97
  import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
87
98
  import { EventLog } from "./event-log.js";
88
99
  import { binaryProbe } from "./binary-probe.js";
100
+ import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
89
101
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
90
102
  import { classifySqliteOpenError } from "./sqlite-open-errors.js";
91
103
  import { OutboxOpenError } from "./outbox-open-error.js";
@@ -102,6 +114,7 @@ import { formatFleetTime } from "./tz-utils.js";
102
114
  import { createAdapter } from "./channel/factory.js";
103
115
  import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
104
116
  import { TelegramAdapter } from "./channel/adapters/telegram.js";
117
+ import { MissedOfflineStore } from "./missed-offline.js";
105
118
  import { createBackend } from "./backend/factory.js";
106
119
  import { readEffortMetadata } from "./backend/effort-metadata.js";
107
120
  import { codexEffort, flagEffort } from "./backend/launch-effort.js";
@@ -127,6 +140,7 @@ import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, che
127
140
  import { TopicArchiver } from "./topic-archiver.js";
128
141
  import { StatuslineWatcher } from "./statusline-watcher.js";
129
142
  import { applyInstanceConfigPatch, outboundHandlers } from "./outbound-handlers.js";
143
+ import { HUB_REPORT_TARGET } from "./hub-report-target.js";
130
144
  import { backendArgs, backendStep, joinBackendArgs, modelNameCarried, planDisplay } from "./backend-command.js";
131
145
  import { DeliveryOutbox, DEFAULT_REPLY_OBLIGATION_TTL_MS, isSilentScheduleDelivery } from "./delivery-outbox.js";
132
146
  // The target Daemon can legitimately wait 30 minutes for a busy pane before
@@ -153,6 +167,20 @@ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked", "review"];
153
167
  * " \t " and [" ", "\t"] both normalize to undefined (not treated as explicit
154
168
  * filters). The same result drives the live-only default and the cap decision.
155
169
  */
170
+ /**
171
+ * #3195: the saved value of each plain-update field the call named (assignee, priority, result), read back from the
172
+ * task — so the answer shows what was persisted, not what was asked. Status is always in the ack already.
173
+ */
174
+ export function savedFields(t, named) {
175
+ const out = {};
176
+ if (named.assignee !== undefined)
177
+ out.assignee = t.assignee;
178
+ if (named.priority !== undefined)
179
+ out.priority = t.priority;
180
+ if (named.result !== undefined)
181
+ out.result = t.result;
182
+ return out;
183
+ }
156
184
  export function normalizeStatusFilter(v) {
157
185
  if (typeof v === "string") {
158
186
  const s = v.trim();
@@ -182,19 +210,23 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
182
210
  }
183
211
  import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
184
212
  import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
213
+ import { nativeCliVersionState } from "./runtime-pane-native.js";
185
214
  import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
215
+ import { RuntimeInstallService } from "./runtime-install.js";
216
+ import { RUNTIME_CREDENTIAL_CAPABILITY } from "./runtime-credential-profiles.js";
186
217
  import { RuntimeCoordinator } from "./runtime-coordinator.js";
187
218
  import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
188
219
  import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
189
220
  import { NativeMcpSessions } from "./native-mcp.js";
190
221
  import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
191
- import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED } from "./native-instance-registry.js";
222
+ import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED, nativeCredentialProfile } from "./native-instance-registry.js";
192
223
  import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
193
224
  import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
194
225
  import { LogService } from "./log-service.js";
226
+ import { viewHistory } from "./view-history.js";
195
227
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
196
228
  import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
197
- import { WebChatDiskStore } from "./web-chat-store.js";
229
+ import { WebChatDiskStore, isSafeInstanceDirName } from "./web-chat-store.js";
198
230
  import { SeenUsers, resolveMentions } from "./web-mentions.js";
199
231
  import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
200
232
  import { agendVersion } from "./agent-whats-new.js";
@@ -224,8 +256,8 @@ import { envFileKeys } from "./token-env-name.js";
224
256
  import { discordGeneralChannelId } from "./discord-general.js";
225
257
  import { parseEnvText } from "./env-file.js";
226
258
  import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
227
- import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
228
- import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
259
+ import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
260
+ import { LOGIN_FLOWS, checkAuthStatus, checkAuthIdentity, authIdentitySupported, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
229
261
  import { LoginSession } from "./login-manager.js";
230
262
  import { runOnLoginShell } from "./login-shell.js";
231
263
  import { tightenInstanceDirs } from "./private-dir.js";
@@ -255,6 +287,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
255
287
  import { recentChatContext } from "./log-tail.js";
256
288
  import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
257
289
  import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
290
+ import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
291
+ import { writeSecretFile } from "./secret-file.js";
258
292
  import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
259
293
  import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
260
294
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
@@ -285,13 +319,14 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
285
319
  import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
286
320
  import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
287
321
  import { sharedRolloutIndex } from "./rollout-index.js";
288
- import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
322
+ import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardOffIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
289
323
  import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
290
324
  import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
291
325
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
292
326
  import { isWebPageNavigation } from "./web-shell-routes.js";
293
- import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
327
+ import { guestSessionPolicy, localSessionPolicy, sessionVia, tokenEpoch, WebSessionStore } from "./web-session.js";
294
328
  import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
329
+ import { DeviceLoginRequests } from "./web-device-login.js";
295
330
  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";
296
331
  import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
297
332
  import { AuthorityStateStore } from "./authority/authority-state.js";
@@ -314,9 +349,10 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
314
349
  const SELF_RESTART_DEADLINE_MS = 300_000;
315
350
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
316
351
  import { kiroHome } from "./backend/kiro-kas-store.js";
317
- import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
352
+ import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileEnv, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
318
353
  import { carryClaudeConversation } from "./backend/claude-carry.js";
319
- import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
354
+ import { classicProfile, mergeBackendOptions, profileKey, profileName, profileOf, readClassicBindings } from "./classic-bindings.js";
355
+ import { ACCOUNT_CLIS, PROFILE_CLIS, buildAccountsView, profilesOnDisk, weeklyUsage } from "./accounts-view.js";
320
356
  import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
321
357
  import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
322
358
  import { FleetStartup } from "./fleet-startup.js";
@@ -330,6 +366,12 @@ import { BackendOutageTracker } from "./backend-outage.js";
330
366
  import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
331
367
  import { getTmuxSession } from "./config.js";
332
368
  import { OwnStatusMessages, discordMessageKey } from "./chat-log-ids.js";
369
+ import { PROFILE_TIMELINE_FILE, PROFILE_UNRECORDED, recordProfileLaunch } from "./usage/profile-timeline.js";
370
+ import { readQuotaHistory } from "./usage/quota-history.js";
371
+ import { estimateCapacity } from "./usage/capacity.js";
372
+ import { covered, hourSources } from "./usage-rollup.js";
373
+ import { KEEP_DAYS } from "./cache-ledger.js";
374
+ import { ratesFor } from "./cache-prices.js";
333
375
  // Closed migration cohort: these 11 names existed immediately before the first
334
376
  // managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
335
377
  // adding a bundled skill: its new name is not proof of old AgEnD ownership.
@@ -481,6 +523,8 @@ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
481
523
  * while anything real shows signs of life well before the old two minutes.
482
524
  */
483
525
  const PROGRESS_MIN_ELAPSED_MS = 30_000;
526
+ /** M-2391: the threshold tick fires this long after it, never a millisecond before (timers can fire early). */
527
+ const PROGRESS_THRESHOLD_MARGIN_MS = 50;
484
528
  /** How much of a tool summary the progress line will show before eliding. */
485
529
  const PROGRESS_ACTIVITY_MAX_CHARS = 48;
486
530
  /**
@@ -767,6 +811,9 @@ export class FleetManager {
767
811
  localPaneRelay = null;
768
812
  openingLocalPane = null;
769
813
  runtimeHub = null;
814
+ runtimeInstall;
815
+ /** One service for the public installer routes and the administrator API: the hash the dashboard shows is the hash of what is sent. */
816
+ runtimeInstaller() { return this.runtimeInstall ??= new RuntimeInstallService(); }
770
817
  runtimeExecutor = null;
771
818
  runtimeStatusScheduled = false;
772
819
  reconciledRemoteResults = new Set();
@@ -777,6 +824,7 @@ export class FleetManager {
777
824
  return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
778
825
  handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
779
826
  handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
827
+ warn: (fields, message) => this.logger.warn(fields, message),
780
828
  changed: event => {
781
829
  if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
782
830
  for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
@@ -817,6 +865,8 @@ export class FleetManager {
817
865
  nativeMcpSessions() {
818
866
  return this.nativeMcpStore ??= new NativeMcpSessions(this);
819
867
  }
868
+ /** Retire only existing native capabilities; ordinary lifecycle changes do not instantiate the Windows path. */
869
+ invalidateNativeMcp(name) { this.nativeMcpStore?.closeInstance(name); }
820
870
  /** The same instance registry and delivery entry point, with a native Windows execution adapter. */
821
871
  nativeInstanceRuntime() {
822
872
  return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
@@ -827,13 +877,25 @@ export class FleetManager {
827
877
  removed: name => this.nativeInstanceRemoved(name),
828
878
  paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
829
879
  machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
830
- validateMachine: (id, cli, sandbox) => {
880
+ machineGeneration: id => this.runtimeCoordinator().machine(id)?.bootId ?? null,
881
+ paneAvailable: id => this.runtimeCoordinator().machine(id)?.paneProtocol === 1,
882
+ validateMachine: (id, cli, sandbox, credentialProfile) => {
831
883
  const machine = this.runtimeCoordinator().machine(id);
832
884
  const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
833
885
  if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
834
886
  throw new Error("Choose an online runtime and an available CLI");
835
887
  if (sandbox === "read-only" && !provider.readOnlySupport)
836
888
  throw new Error("This runtime CLI cannot enforce read-only execution");
889
+ if (credentialProfile !== undefined) {
890
+ // Fail closed at selection time, not at the first message: an old Runtime would ignore the name and run another account.
891
+ if (!machine.runtimeInfo?.capabilities.includes(RUNTIME_CREDENTIAL_CAPABILITY))
892
+ throw new Error("This computer's Runtime is too old to choose a Claude account. Update the Runtime on that computer, then try again.");
893
+ const row = machine.inventory?.credentialProfiles?.profiles.find(profile => profile.name === credentialProfile);
894
+ if (!row)
895
+ throw new Error(`This computer has not reported a credential profile named "${credentialProfile}". On that computer run: agend-runtime login claude-code ${credentialProfile}, then choose Detect again in that computer's CLI diagnostics (Settings → Runtime).`);
896
+ if (row.loggedIn === false)
897
+ throw new Error(`Credential profile "${credentialProfile}" is not signed in on that computer. Sign in there: agend-runtime login claude-code ${credentialProfile}`);
898
+ }
837
899
  },
838
900
  emit: (event, data) => {
839
901
  this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
@@ -842,6 +904,152 @@ export class FleetManager {
842
904
  },
843
905
  }, { bridge: this.runtimeWindowsExecutor() });
844
906
  }
907
+ agentRuntimeChangeService;
908
+ agentRuntimeChanges() {
909
+ if (this.agentRuntimeChangeService)
910
+ return this.agentRuntimeChangeService;
911
+ const windowPins = new Map();
912
+ return this.agentRuntimeChangeService ??= new AgentRuntimeChanges({
913
+ config: name => Object.hasOwn(this.fleetConfig?.instances ?? {}, name) ? this.fleetConfig.instances[name] : undefined,
914
+ defaults: () => this.fleetConfig?.defaults,
915
+ generation: () => [this.managerBootId, this.settingsGeneration],
916
+ epoch: name => this.lifecycle.epochOf(name),
917
+ backendState: (name, backend) => !isWindowsInstance(this.fleetConfig?.instances[name]) ? this.backendAvailabilityFor(name).find(row => row.backend === backend)?.state : undefined,
918
+ transition: (name, run) => this.lifecycle.runTransition(name, async (transition) => {
919
+ const config = this.fleetConfig?.instances[name];
920
+ if (!isWindowsInstance(config)) {
921
+ const path = join(this.getInstanceDir(name), "window-id");
922
+ let pin;
923
+ if (existsSync(path)) {
924
+ if (statSync(path).size > 128)
925
+ throw Error("Invalid saved CLI window identity");
926
+ pin = readFileSync(path, "utf8").trim();
927
+ if (!/^@\d+$/.test(pin))
928
+ throw Error("Invalid saved CLI window identity");
929
+ }
930
+ else if (this.daemons.has(name))
931
+ throw Error("The current CLI window identity is unavailable");
932
+ windowPins.set(name, pin);
933
+ }
934
+ try {
935
+ return await run(async () => {
936
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
937
+ this.lifecycle.invalidate(name);
938
+ this.nativeInstanceRuntime().setPaused(name, true);
939
+ }
940
+ else
941
+ await this.stopInstance(name, transition);
942
+ });
943
+ }
944
+ finally {
945
+ windowPins.delete(name);
946
+ }
947
+ }),
948
+ idle: name => {
949
+ if (this.shuttingDown || this.lifecycle.hasWorkLease(name) || this.deliveryOutbox?.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending", "uncertain"]))
950
+ return false;
951
+ const config = this.fleetConfig?.instances[name];
952
+ if (!config)
953
+ return false;
954
+ if (isWindowsInstance(config)) {
955
+ try {
956
+ const agent = this.nativeInstanceRuntime().get(name);
957
+ if (!agent || agent.blocked || agent.activeId && !isNativePaneInstance(config))
958
+ return false;
959
+ }
960
+ catch {
961
+ return false;
962
+ }
963
+ }
964
+ if (!this.daemons.has(name) && !isWindowsInstance(config) && existsSync(join(this.getInstanceDir(name), "window-id")))
965
+ return false;
966
+ return !this.daemons.has(name) || this.getInstanceExecutionState(name) === "idle";
967
+ },
968
+ prepare: async (name, before, target, current) => {
969
+ if (!current())
970
+ throw Error("Runtime change authority expired");
971
+ if (isWindowsInstance(before))
972
+ this.nativeInstanceRuntime().importHistory(name);
973
+ if (target.runtime === "windows")
974
+ return this.nativeInstanceRuntime().prepareRuntimeChange(name, before, target, current);
975
+ const sameAccount = !isWindowsInstance(before) && target.backend === (before.backend || this.fleetConfig?.defaults?.backend || "claude-code");
976
+ const availability = (sameAccount ? this.backendAvailabilityFor(name) : this.backendAvailabilityRows()).find(row => row.backend === target.backend);
977
+ if (availability && ["not_installed", "not_signed_in", "too_old"].includes(availability.state))
978
+ throw Error("Choose an available CLI on the Fleet host");
979
+ const supplied = target.directory.trim(), cwd = supplied || join(this.dataDir, "workspaces", name);
980
+ if (!isAbsolute(cwd) || /^[A-Za-z]:[\\/]/.test(cwd) || /[\x00-\x1f]/.test(cwd))
981
+ throw Error("Enter an absolute Linux working directory or leave it empty");
982
+ if (!current())
983
+ throw Error("Runtime change authority expired");
984
+ if (supplied) {
985
+ if (!statSync(cwd).isDirectory())
986
+ throw Error("The Linux working directory is not a directory");
987
+ }
988
+ else
989
+ mkdirSync(cwd, { recursive: true, mode: 0o700 });
990
+ const config = { ...before, runtime: "wsl", working_directory: cwd, backend: target.backend, model: target.model };
991
+ if (before.backend !== target.backend)
992
+ config.model_failover = [];
993
+ for (const key of ["machine_id", "native_agent_id", "native_adapter", "native_sandbox"])
994
+ delete config[key];
995
+ if (!sameAccount)
996
+ delete config.backend_options;
997
+ return { config, discard: () => { } };
998
+ },
999
+ closed: async (name, before) => {
1000
+ if (this.daemons.has(name))
1001
+ return false;
1002
+ if (isWindowsInstance(before))
1003
+ return !this.nativeInstanceRuntime().get(name)?.activeId;
1004
+ // An unreadable tmux state is not evidence that the old process exited.
1005
+ const session = getTmuxSession();
1006
+ if (!await TmuxManager.sessionExistsStrict(session))
1007
+ return true;
1008
+ const pin = windowPins.get(name);
1009
+ return !(await TmuxManager.listWindowsStrict(session)).some(window => window.name === name || pin && window.id === pin);
1010
+ },
1011
+ fresh: (name, requestId) => {
1012
+ const dir = this.getInstanceDir(name), moved = [];
1013
+ const restore = () => { for (const [source, saved] of moved.reverse())
1014
+ if (!existsSync(source) && existsSync(saved))
1015
+ renameSync(saved, source); };
1016
+ try {
1017
+ for (const file of ["session-id", "session-id.backend"]) {
1018
+ const source = join(dir, file), saved = join(dir, `${file}.runtime-${requestId}`);
1019
+ if (existsSync(source)) {
1020
+ if (existsSync(saved))
1021
+ throw Error("Session backup already exists");
1022
+ renameSync(source, saved);
1023
+ moved.push([source, saved]);
1024
+ }
1025
+ }
1026
+ return restore;
1027
+ }
1028
+ catch (error) {
1029
+ restore();
1030
+ throw error;
1031
+ }
1032
+ },
1033
+ save: (name, before, next) => {
1034
+ if (this.fleetConfig?.instances[name] !== before)
1035
+ throw Error("Agent settings changed before saving Runtime");
1036
+ // Mark the replacement stopped before committing its config. A marker failure must not leave a saved
1037
+ // Windows config referring to a profile the preparation rollback would then remove.
1038
+ writePausedMarker(this.getInstanceDir(name), Date.now(), "operator");
1039
+ const fleet = this.fleetConfig;
1040
+ fleet.instances[name] = next;
1041
+ try {
1042
+ this.saveFleetConfig();
1043
+ }
1044
+ catch (error) {
1045
+ fleet.instances[name] = before;
1046
+ throw error;
1047
+ }
1048
+ // A move deliberately leaves the replacement stopped; its next explicit Start opens the fresh CLI.
1049
+ },
1050
+ changed: name => { this.instanceStateCache.delete(name); this.instanceProcessStatus.delete(name); this.emitSseEvent("status", this.getUiStatus()); },
1051
+ });
1052
+ }
845
1053
  /** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
846
1054
  async createNativePaneInstance(profile, channelId, authorized, execution) {
847
1055
  const { createNativePane } = await import("./native-pane-create.js");
@@ -915,6 +1123,26 @@ export class FleetManager {
915
1123
  return result;
916
1124
  });
917
1125
  }
1126
+ /** The local machine's inventory as last used to open its pane relay (status only; never a launch decision). */
1127
+ localPaneInventory = null;
1128
+ nativeCliStatus(cli, machineId, daemon) {
1129
+ if (cli !== "codex" && cli !== "claude-code")
1130
+ return {};
1131
+ // The version the running pane actually launched; only without one, the machine's last inventory.
1132
+ let running;
1133
+ try {
1134
+ running = daemon?.usesNativePane?.() ? daemon.nativePaneCliVersion() : undefined;
1135
+ }
1136
+ catch {
1137
+ running = undefined;
1138
+ }
1139
+ const inventory = machineId ? this.runtimeHub?.machine(machineId)?.inventory : this.localPaneInventory;
1140
+ const version = running ?? inventory?.clis?.find(item => item.id === cli && item.runnable)?.version;
1141
+ if (version === undefined || version === null)
1142
+ return {};
1143
+ const result = nativeCliVersionState(cli, version);
1144
+ return { ...(result.version ? { native_cli_version: result.version } : {}), native_cli_verification: result.state };
1145
+ }
918
1146
  async localNativePaneEndpoint() {
919
1147
  const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
920
1148
  throw Error("Local Windows runtime is switched off or draining"); };
@@ -931,6 +1159,7 @@ export class FleetManager {
931
1159
  ]);
932
1160
  admit();
933
1161
  this.localPaneRelay?.stop();
1162
+ this.localPaneInventory = inventory;
934
1163
  const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
935
1164
  diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
936
1165
  version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
@@ -979,8 +1208,12 @@ export class FleetManager {
979
1208
  if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
980
1209
  || now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
981
1210
  || now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
982
- || now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding)
1211
+ || now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding
1212
+ || nativeCredentialProfile(now) !== nativeCredentialProfile(config))
983
1213
  throw Error("Native pane configuration or lifetime changed");
1214
+ // Fail closed: a Runtime that does not report the capability would start the terminal on the computer's own account.
1215
+ if (nativeCredentialProfile(config) && !this.runtimeCoordinator().machine(config.machine_id)?.runtimeInfo?.capabilities.includes(RUNTIME_CREDENTIAL_CAPABILITY))
1216
+ throw Error("This computer's Runtime is too old to choose a Claude account. Update the Runtime on that computer, then try again.");
984
1217
  if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
985
1218
  throw Error("Native runtime is switched off");
986
1219
  },
@@ -1039,12 +1272,12 @@ export class FleetManager {
1039
1272
  throw Error("General or its paired runtime is unavailable");
1040
1273
  const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
1041
1274
  && this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
1042
- const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertAuditedNativePaneVersion }] = await Promise.all([
1275
+ const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertRunnableNativePaneVersion }] = await Promise.all([
1043
1276
  import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
1044
1277
  ]);
1045
1278
  if (!current())
1046
1279
  throw Error("General workspace authority expired");
1047
- assertAuditedNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
1280
+ assertRunnableNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
1048
1281
  const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
1049
1282
  const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
1050
1283
  if (!current())
@@ -1058,6 +1291,14 @@ export class FleetManager {
1058
1291
  throw new NativePaneRecoveryError({ kind: "host-upgrade" });
1059
1292
  return validateGeneralWorkspaceReceipt(result, request);
1060
1293
  }
1294
+ /** S3c (S-55): the member's exposure API, attached by S5's member composition (null: no Hub member here). */
1295
+ hubMemberExposureApi = null;
1296
+ hubMemberExposures() { return this.hubMemberExposureApi; }
1297
+ attachHubMemberExposures(api) { this.hubMemberExposureApi = api; }
1298
+ /** S3c PR B: the member's pairing API, attached by S5's member composition (null: no Hub member here). */
1299
+ hubMemberPairingApi = null;
1300
+ hubMemberPairing() { return this.hubMemberPairingApi; }
1301
+ attachHubMemberPairing(api) { this.hubMemberPairingApi = api; }
1061
1302
  generalLocationFlights = new Map();
1062
1303
  generalAttentionEpisodes = new Map();
1063
1304
  generalBindings() { return new GeneralBindings(this.dataDir); }
@@ -1513,6 +1754,364 @@ export class FleetManager {
1513
1754
  * them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
1514
1755
  * sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
1515
1756
  */
1757
+ /** S-51: the Hub switches follow the configuration in force (load, save, reload rollback), like the runtime ones. */
1758
+ observeHubSwitches() {
1759
+ this.observeHubAgentIncarnations();
1760
+ if (!this.hubEffectsReady || this.hubEffectsScheduled || this.shuttingDown)
1761
+ return;
1762
+ this.hubEffectsScheduled = true;
1763
+ queueMicrotask(() => { this.hubEffectsScheduled = false; void this.applyHubSwitches(); });
1764
+ }
1765
+ /** Reads the raw configuration at run time (the newest wins); off and never started: returns before any import. */
1766
+ async applyHubSwitches() {
1767
+ if (this.shuttingDown || !this.hubEffectsReady)
1768
+ return;
1769
+ const raw = this.rawFleetConfig;
1770
+ const on = raw?.hub?.enabled === true || raw?.hub_member?.enabled === true;
1771
+ if (!on && !this.hubRuntime && !this.hubRuntimeInit)
1772
+ return;
1773
+ try {
1774
+ const runtime = this.hubRuntime ?? (await this.hubRuntimeCreated());
1775
+ if (this.shuttingDown || !this.hubEffectsReady)
1776
+ return; // Stop owns it now (fenced in stopAll, closed in doStopAll)
1777
+ await this.observeHubMemberPairing(raw?.hub_member?.enabled === true);
1778
+ const current = this.rawFleetConfig;
1779
+ await runtime.apply({ hub: current?.hub, hub_member: current?.hub_member });
1780
+ }
1781
+ catch (err) {
1782
+ this.logger.warn({ err: err?.message ?? String(err) }, "Hub switches could not be applied");
1783
+ }
1784
+ }
1785
+ /** Single flight: one import, one runtime, assigned once; every caller (and Stop) gets that same runtime. */
1786
+ hubRuntimeCreated() {
1787
+ this.hubRuntimeInit ??= (async () => {
1788
+ const { HubRuntime } = await import("./hub/runtime.js");
1789
+ const runtime = new HubRuntime({
1790
+ home: this.dataDir,
1791
+ log: (level, message, meta) => this.logger[level](meta ?? {}, message),
1792
+ onHoldsChanged: holds => { this.hubHolds = holds; this.needsYou?.poke(); },
1793
+ // S-51 PR 2b: the fleet's side of a Hub membership (pairing binding, local agents, attachment points).
1794
+ member: {
1795
+ // S3c's currentBinding: a fresh, fail-closed read of the pairing state (null unless consistent and connectable).
1796
+ boundHub: () => this.hubMemberBinding?.(this.dataDir)?.hubId ?? null,
1797
+ agents: () => this.hubMemberAgents(),
1798
+ // P1 representative mode (docs/design/hub-representative-agent.md): ordinary delivery + the report_result hook.
1799
+ representative: {
1800
+ quota: agent => this.hubAgentQuota(agent),
1801
+ deliver: input => this.deliverHubQuestion(input),
1802
+ agentToken: agent => this.hubAgentToken(agent),
1803
+ // attachments-v1: a question's files go into the agent's inbox; an answer's come from its own paths.
1804
+ files: { stateDir: this.dataDir,
1805
+ inboxDir: agent => isSafeInstanceDirName(agent) ? join(this.dataDir, "workspaces", agent, "inbox") : null,
1806
+ scratchDir: agent => isSafeInstanceDirName(agent) ? join(this.dataDir, "workspaces", agent, "hub-files") : null },
1807
+ attachReports: (handler, failed) => { this.hubReports = handler; this.hubDeliveryFailed = failed; },
1808
+ },
1809
+ attachExposures: api => this.attachHubMemberExposures(api),
1810
+ attachAttention: read => this.attachHubExecutionAttention(read),
1811
+ pokeAttention: () => this.needsYou?.poke(),
1812
+ linkState: state => { this.hubMemberLink = state; },
1813
+ },
1814
+ });
1815
+ // Stop began during the import: it holds this promise and closes the runtime; do not hand it out again.
1816
+ if (this.shuttingDown || !this.hubEffectsReady)
1817
+ runtime.fence();
1818
+ else
1819
+ this.hubRuntime = runtime;
1820
+ return runtime;
1821
+ })();
1822
+ return this.hubRuntimeInit;
1823
+ }
1824
+ /**
1825
+ * S-51 PR 2b: while the member switch is on, the local owner's "Connect to a Hub" backend (S3c's pairing API) is
1826
+ * attached, paired or not; switched off, it is detached. A binding change re-composes the member side.
1827
+ */
1828
+ async observeHubMemberPairing(on) {
1829
+ if (!on) {
1830
+ this.attachHubMemberPairing(null);
1831
+ return;
1832
+ }
1833
+ if (this.hubMemberPairingApi)
1834
+ return;
1835
+ const { createHubMemberPairingApi, currentBinding } = await import("./hub/member/pairing-api.js");
1836
+ if (this.shuttingDown || !this.hubEffectsReady || this.hubMemberPairingApi)
1837
+ return;
1838
+ this.hubMemberBinding = currentBinding;
1839
+ this.attachHubMemberPairing(createHubMemberPairingApi({
1840
+ home: this.dataDir,
1841
+ enabled: () => this.rawFleetConfig?.hub_member?.enabled === true,
1842
+ link: () => (this.hubMemberLink === "online" ? "online" : "offline"),
1843
+ hold: () => (this.hubMemberLink === "revoked" ? "revoked" : null),
1844
+ // Disconnect: stop the member client now (the runtime re-composes from the saved binding, fencing first).
1845
+ fence: () => { void this.hubRuntime?.refresh(); },
1846
+ // A claim saved, a disconnect tombstone or a cancel: re-compose the member side for the new binding.
1847
+ bindingChanged: () => { void this.hubRuntime?.refresh(); },
1848
+ // Disconnect withdraws every published exposure locally (each saved before this returns).
1849
+ withdrawAll: () => this.hubMemberWithdrawAll(),
1850
+ }));
1851
+ }
1852
+ /**
1853
+ * Disconnect: every published exposure is withdrawn and saved before the disconnect reports success. Through the
1854
+ * running member's API, or — held or not composed — directly in the member's own store; a failure here makes the
1855
+ * disconnect report "unsaved" (S3c), so consent never outlives the disconnect.
1856
+ */
1857
+ async hubMemberWithdrawAll() {
1858
+ const api = this.hubMemberExposureApi;
1859
+ if (api) {
1860
+ for (const view of api.list()) {
1861
+ if (view.exposureId && view.state === "published")
1862
+ await api.withdraw(view.exposureId, view.revision);
1863
+ }
1864
+ return;
1865
+ }
1866
+ const hubId = this.hubMemberPairingApi?.boundHub()?.hubId ?? null;
1867
+ if (!hubId)
1868
+ return;
1869
+ const { withdrawMemberExposuresOffline } = await import("./hub/compose/member.js");
1870
+ await withdrawMemberExposuresOffline(this.dataDir, hubId);
1871
+ }
1872
+ /**
1873
+ * The Hub exposure picker's agents (docs/design/hub-representative-agent.md): every configured instance normal
1874
+ * delivery addresses, General included, shown by its display name. The dedicated Hub daemon is not a fleet instance.
1875
+ */
1876
+ hubMemberAgents() {
1877
+ return Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({ name, display: this.shownInstanceName(name) }));
1878
+ }
1879
+ /** Same cached usage source as the owner's rail, scoped to the representative's CLI and credential profile. */
1880
+ async hubAgentQuota(agent) {
1881
+ const binding = () => {
1882
+ const config = this.fleetConfig?.instances[agent];
1883
+ if (!config || isWindowsInstance(config))
1884
+ return null; // runner usage never belongs to a Linux login
1885
+ const backend = this.backendNameForInstance(agent), provider = usageProviderIdForBackend(backend);
1886
+ if (!provider)
1887
+ return null;
1888
+ const profile = this.credentialProfileOf(agent, backend);
1889
+ return profile ? `${provider}:${profile}` : provider;
1890
+ };
1891
+ const id = binding();
1892
+ const { quotaBand } = await import("./hub/member/quota.js");
1893
+ const paused = () => this.daemons.get(agent)?.quotaPaused?.() === true
1894
+ || this.lifecycle.isPaused(agent) && readPauseReason(this.getInstanceDir(agent)) === "quota";
1895
+ if (!id)
1896
+ return { band: "unknown" };
1897
+ try {
1898
+ const snapshot = await getUsageSnapshot(false, new Set([id]));
1899
+ if (binding() !== id)
1900
+ return { band: "unknown" };
1901
+ return quotaBand(snapshot.providers.find(p => p.id === id), paused());
1902
+ }
1903
+ catch {
1904
+ return quotaBand(undefined, paused());
1905
+ }
1906
+ }
1907
+ /**
1908
+ * A published representative's lifecycle token (#3145, #3147 review): a DURABLE incarnation id per agent name, rotated
1909
+ * whenever its backend, folder or runtime changes and dropped when the instance is removed. A replacement (remove and
1910
+ * re-add, even with the same name and config, across restarts) or any retarget — including A→B→A — gets a new id, so
1911
+ * earlier consent never matches again without an explicit republish. Stopping or pausing changes nothing.
1912
+ */
1913
+ hubAgentToken(agent) {
1914
+ const instances = this.fleetConfig?.instances ?? {};
1915
+ if (!Object.prototype.hasOwnProperty.call(instances, agent))
1916
+ return null;
1917
+ if (this.hubIncarnationsBroken)
1918
+ return null;
1919
+ let id;
1920
+ try {
1921
+ this.settleHubIncarnations();
1922
+ id = this.hubIncarnationsBroken ? null : this.hubAgentIncarnation(agent, instances[agent], true);
1923
+ }
1924
+ catch (err) {
1925
+ this.logger.warn({ err: err?.message, agent }, "Hub agent incarnation could not be saved; the agent is not a deliverable representative");
1926
+ return null;
1927
+ }
1928
+ return id ? createHash("sha256").update(JSON.stringify(["agend-hub-agent-v2", agent, id])).digest("hex") : null;
1929
+ }
1930
+ hubIncarnationsPath() { return join(this.dataDir, "hub-member", "agent-incarnations.json"); }
1931
+ /**
1932
+ * M-2451 (#3147 residual): written beside hub-member/, in the data directory itself, when an invalidation could not be
1933
+ * made durable in hub-member/. A later process that finds it invalidates every incarnation before handing out any
1934
+ * token, so a restart never revives consent the earlier process had to withdraw (A→B→A, or a removal, while hub-member/
1935
+ * was unwritable).
1936
+ */
1937
+ hubIncarnationsMarkerPath() { return join(this.dataDir, "hub-member-invalidate.pending"); }
1938
+ /** Whether this process has settled the incarnations on disk (the marker handled, the file proven writable). */
1939
+ hubIncarnationsSettled = false;
1940
+ /**
1941
+ * Once per process, before the first token: an invalidation an earlier process could not persist (the marker) clears
1942
+ * every incarnation; and an existing file must be rewritable here, else this process is broken (no token at all) —
1943
+ * an incarnation this process could not rotate must never be handed out. Fails closed: any doubt marks it broken.
1944
+ */
1945
+ settleHubIncarnations() {
1946
+ if (this.hubIncarnationsSettled || this.hubIncarnationsBroken || !this.dataDir)
1947
+ return;
1948
+ const path = this.hubIncarnationsPath(), marker = this.hubIncarnationsMarkerPath();
1949
+ if (existsSync(marker)) {
1950
+ try {
1951
+ unlinkSync(path);
1952
+ }
1953
+ catch (err) {
1954
+ if (err?.code !== "ENOENT") {
1955
+ this.hubIncarnationsBroken = true;
1956
+ this.logger.warn({ err: err?.message }, "An earlier Hub consent invalidation is still pending and cannot be applied; no Hub representative token is handed out");
1957
+ return;
1958
+ }
1959
+ }
1960
+ // The clear is durable; a marker that cannot be removed only clears again next time (over-invalidation, never revival).
1961
+ try {
1962
+ unlinkSync(marker);
1963
+ }
1964
+ catch { /* left: harmless */ }
1965
+ this.logger.warn("An earlier Hub consent invalidation was applied: every Hub representative needs a republish");
1966
+ }
1967
+ if (existsSync(path))
1968
+ this.writeHubIncarnations(this.readHubIncarnations()); // throws (broken) when hub-member/ is not writable
1969
+ this.hubIncarnationsSettled = true;
1970
+ } /**
1971
+ * #3147 r2: set when an invalidation could not be made durable in any way (neither the atomic write nor removing the
1972
+ * file worked). From then on this process hands out no representative token at all, and the transitions the fleet
1973
+ * controls (a config save, a removal) are refused instead of applied.
1974
+ */
1975
+ hubIncarnationsBroken = false;
1976
+ readHubIncarnations() {
1977
+ try {
1978
+ const value = JSON.parse(readFileSync(this.hubIncarnationsPath(), "utf8"));
1979
+ return value && typeof value === "object" && !Array.isArray(value) ? Object.assign(Object.create(null), value) : Object.create(null);
1980
+ }
1981
+ catch {
1982
+ return Object.create(null);
1983
+ }
1984
+ }
1985
+ /**
1986
+ * Persist the incarnation map: "written", or — when the atomic write fails — "cleared": the whole file is removed, which
1987
+ * durably invalidates EVERY earlier incarnation (all consent then needs a republish; never a revival). Throws (and
1988
+ * marks the process broken) only when neither is possible.
1989
+ */
1990
+ writeHubIncarnations(map) {
1991
+ const path = this.hubIncarnationsPath(), tmp = `${path}.${process.pid}.tmp`;
1992
+ try {
1993
+ mkdirSync(join(this.dataDir, "hub-member"), { recursive: true, mode: 0o700 });
1994
+ writeFileSync(tmp, JSON.stringify(map), { mode: 0o600 });
1995
+ renameSync(tmp, path);
1996
+ return "written";
1997
+ }
1998
+ catch (err) {
1999
+ try {
2000
+ rmSync(tmp, { recursive: false, force: true });
2001
+ }
2002
+ catch { /* a directory planted there: left alone */ }
2003
+ try {
2004
+ unlinkSync(path);
2005
+ }
2006
+ catch (gone) {
2007
+ if (gone?.code !== "ENOENT") {
2008
+ this.hubIncarnationsBroken = true;
2009
+ // M-2451: record the withdrawal outside hub-member/, so a restart does not revive what this process withdrew.
2010
+ try {
2011
+ writeFileSync(this.hubIncarnationsMarkerPath(), "", { mode: 0o600 });
2012
+ }
2013
+ catch { /* the data directory too: nothing durable is left */ }
2014
+ throw err;
2015
+ }
2016
+ }
2017
+ this.logger.warn({ err: err?.message }, "Hub agent incarnations could not be saved; every earlier Hub consent is invalidated (republish needed)");
2018
+ return "cleared";
2019
+ }
2020
+ }
2021
+ /** The agent's durable incarnation id (create: make one if none; else null). A changed fingerprint rotates it. */
2022
+ hubAgentIncarnation(agent, cfg, create) {
2023
+ const fp = createHash("sha256").update(JSON.stringify([cfg.backend ?? "claude-code", cfg.working_directory, cfg.runtime ?? "", cfg.machine_id ?? ""])).digest("hex");
2024
+ const map = this.readHubIncarnations(), entry = Object.prototype.hasOwnProperty.call(map, agent) ? map[agent] : undefined;
2025
+ if (entry && entry.fp === fp && typeof entry.id === "string")
2026
+ return entry.id;
2027
+ if (!entry && !create)
2028
+ return null;
2029
+ map[agent] = { id: randomUUID(), fp };
2030
+ // A rotation that could only clear the file still invalidated the old id durably; the new one is made next time.
2031
+ return this.writeHubIncarnations(map) === "written" ? map[agent].id : null;
2032
+ }
2033
+ /**
2034
+ * Every configuration in force: rotate a recorded agent's incarnation when its backend/folder/runtime changed; drop
2035
+ * removed ones. strict (a config save the fleet is about to write): a failure to make that durable throws, so the save
2036
+ * is refused instead of applied. Otherwise (a load or reload of a hand-edited file) a failure is logged — the
2037
+ * invalidation then is the cleared file, or, if nothing could be written, this process hands out no token at all.
2038
+ */
2039
+ observeHubAgentIncarnations(strict = false) {
2040
+ // Only against a loaded configuration: never forget every agent because none is loaded yet.
2041
+ if (!this.dataDir || !this.fleetConfig?.instances)
2042
+ return;
2043
+ try {
2044
+ this.settleHubIncarnations();
2045
+ if (this.hubIncarnationsBroken)
2046
+ throw new Error("Hub agent incarnations cannot be written here");
2047
+ if (!existsSync(this.hubIncarnationsPath()))
2048
+ return;
2049
+ const instances = this.fleetConfig.instances;
2050
+ for (const agent of Object.keys(this.readHubIncarnations())) {
2051
+ // Gone from the configuration in force (removed by hand in fleet.yaml, too): forgotten, never revived by a re-add.
2052
+ if (Object.prototype.hasOwnProperty.call(instances, agent))
2053
+ this.hubAgentIncarnation(agent, instances[agent], false);
2054
+ else
2055
+ this.forgetHubAgentIncarnation(agent, true);
2056
+ }
2057
+ }
2058
+ catch (err) {
2059
+ this.logger?.warn({ err: err?.message }, "Hub agent incarnations could not be checked");
2060
+ if (strict)
2061
+ throw new Error(`Hub consent for a changed agent could not be invalidated durably; the change is not saved (${err?.message})`);
2062
+ }
2063
+ }
2064
+ /** A removed instance's incarnation goes with it (a later instance of the same name is a new one). strict: throw if not durable. */
2065
+ forgetHubAgentIncarnation(agent, strict = false) {
2066
+ try {
2067
+ if (!this.dataDir || !existsSync(this.hubIncarnationsPath()))
2068
+ return;
2069
+ const map = this.readHubIncarnations();
2070
+ if (!Object.prototype.hasOwnProperty.call(map, agent))
2071
+ return;
2072
+ delete map[agent];
2073
+ this.writeHubIncarnations(map);
2074
+ }
2075
+ catch (err) {
2076
+ this.logger.warn({ err: err?.message, agent }, "Hub agent incarnation could not be dropped");
2077
+ if (strict)
2078
+ throw new Error(`Hub consent for ${agent} could not be invalidated durably; refusing the change (${err?.message})`);
2079
+ }
2080
+ }
2081
+ /**
2082
+ * A Hub question to its representative agent: ordinary cross-instance delivery (idle gate, wake, retries), from the
2083
+ * reserved Hub sender with the Hub-issued correlation id. beforeInput runs immediately before the IPC hand-off.
2084
+ */
2085
+ async deliverHubQuestion(input) {
2086
+ const { HUB_REPORT_TARGET } = await import("./hub/representative.js");
2087
+ if (this.shuttingDown || !this.fleetConfig?.instances[input.agent])
2088
+ return false;
2089
+ // Windows agents run through their own runtime, which has no pre-input hook: not a P1 representative.
2090
+ if (isNativeExecInstance(this.fleetConfig.instances[input.agent]))
2091
+ return false;
2092
+ const ipcMeta = {
2093
+ chat_id: "",
2094
+ message_id: `hubmsg-${input.correlationId.slice(4)}`,
2095
+ user: `instance:${HUB_REPORT_TARGET}`,
2096
+ user_id: `instance:${HUB_REPORT_TARGET}`,
2097
+ ts: new Date().toISOString(),
2098
+ thread_id: "",
2099
+ from_instance: HUB_REPORT_TARGET,
2100
+ from_display: "Hub",
2101
+ correlation_id: input.correlationId,
2102
+ request_kind: "query",
2103
+ requires_reply: "true",
2104
+ task_summary: "A question from the Hub",
2105
+ };
2106
+ const result = await this.deliverToInstance(input.agent, { type: "fleet_inbound", targetSession: input.agent, content: input.text, meta: ipcMeta }, { isCrossInstance: true, stillCurrent: input.stillCurrent, beforeInput: input.beforeInput });
2107
+ return result !== false;
2108
+ }
2109
+ /** S-51: why the Hub (or this fleet's Hub membership) is not running, if it is held. Empty while off or running. */
2110
+ getHubHolds() {
2111
+ return this.hubHolds;
2112
+ }
2113
+ hubBrowserCertificate() { return this.hubRuntime?.browserCertificateSettings() ?? null; }
2114
+ hubRetention() { return this.hubRuntime?.retentionSettings() ?? null; }
1516
2115
  observeRuntimeSwitches() {
1517
2116
  setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
1518
2117
  if (!this.runtimeEffects || this.runtimeEffectsScheduled)
@@ -1581,6 +2180,28 @@ export class FleetManager {
1581
2180
  return this.daemons.get(name)?.bootId ?? null;
1582
2181
  }
1583
2182
  rawFleetConfig = {};
2183
+ /**
2184
+ * S-51 (#3095, experimental, off by default): the Hub runtime. src/hub/runtime.js is imported only once a Hub switch
2185
+ * (hub.enabled / hub_member.enabled) is on; while both are off nothing Hub-related loads, opens or listens.
2186
+ */
2187
+ hubRuntime = null;
2188
+ /** S3c's currentBinding reader, loaded with the pairing API (only while the member switch is on). */
2189
+ hubMemberBinding = null;
2190
+ /** The member link as the "Connect to a Hub" view shows it (S5's client lifecycle). */
2191
+ hubMemberLink = "offline";
2192
+ /** OutboundContext: the Hub representative's answer hook (null: no member service running, or not a Hub answer). */
2193
+ hubReport(input) {
2194
+ return this.hubReports?.(input) ?? null;
2195
+ }
2196
+ /** The member side's pane-failure hook for a Hub-issued correlation id (else null). */
2197
+ hubDeliveryFailed = null;
2198
+ /** The member side's report_result hook for HUB_REPORT_TARGET while a representative service runs (else null). */
2199
+ hubReports = null;
2200
+ /** The one in-flight creation (single flight): overlapping first observations share it, Stop waits for it. */
2201
+ hubRuntimeInit = null;
2202
+ hubEffectsReady = false;
2203
+ hubEffectsScheduled = false;
2204
+ hubHolds = [];
1584
2205
  rawFleetDocument = null;
1585
2206
  /** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
1586
2207
  fleetDocumentWrites = null;
@@ -1730,6 +2351,7 @@ export class FleetManager {
1730
2351
  startupRetryNotices = new Map();
1731
2352
  /** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
1732
2353
  unsupportedCliNotices = new Map();
2354
+ kiroCompatibilityHolds = new Map();
1733
2355
  kiroIncompatNotices = new Map();
1734
2356
  /** Backoff between automatic startup retries; the last step repeats while the backend is down. */
1735
2357
  static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
@@ -1758,6 +2380,8 @@ export class FleetManager {
1758
2380
  kiroMigrationRuns = new Map();
1759
2381
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
1760
2382
  needsYou = null;
2383
+ hubExecutionAttention = null;
2384
+ missedOfflineStore = null;
1761
2385
  configHistory = null;
1762
2386
  configHistoryTimer = null;
1763
2387
  loadedFleetSource = null;
@@ -1873,6 +2497,9 @@ export class FleetManager {
1873
2497
  discordPresenceEagerPending = false;
1874
2498
  discordPresenceInFlight = null;
1875
2499
  static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
2500
+ /** #3066 §1d: how often a fleet with no Discord bot reads the quota for its history (the user's choice: 30 min). */
2501
+ static QUOTA_BACKGROUND_READ_MS = 30 * 60_000;
2502
+ quotaReadTimer = null;
1876
2503
  /** Days of event/activity history to keep. */
1877
2504
  static EVENT_LOG_RETENTION_DAYS = 30;
1878
2505
  watchdogTimer = null;
@@ -2068,6 +2695,8 @@ export class FleetManager {
2068
2695
  /** #2855: a trusted guest's searches, per account. */
2069
2696
  guestSearches = new OfficeWriteLimiter(undefined, { perWindow: GUEST_SEARCHES.perWindow, windowMs: GUEST_SEARCHES.windowMs });
2070
2697
  officeSharesInstance;
2698
+ /** #2861: when each instance was last talked to and answered (no words), for share links' dialogue box. */
2699
+ officeTalk = new OfficeTalk();
2071
2700
  /**
2072
2701
  * #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
2073
2702
  * their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
@@ -2084,6 +2713,7 @@ export class FleetManager {
2084
2713
  hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
2085
2714
  skins: this.officeSkins,
2086
2715
  levels: () => this.officeLevelsSnapshot(),
2716
+ talk: scope => this.officeTalk.recent(scope),
2087
2717
  },
2088
2718
  manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
2089
2719
  ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
@@ -2440,6 +3070,26 @@ export class FleetManager {
2440
3070
  const baselines = this.settingsBaselineReader = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
2441
3071
  config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
2442
3072
  generalLocation: input => this.previewGeneralLocation(input),
3073
+ hubMembership: raw => hubMembershipSettings(raw),
3074
+ // S3c (S-55): a Hub exposure publish's confirmation summary, while a member composition is attached.
3075
+ hubBrowserCertificate: (input, binding) => {
3076
+ const control = this.hubBrowserCertificate();
3077
+ if (!control)
3078
+ throw Error("unavailable");
3079
+ return control.preview(input, binding);
3080
+ },
3081
+ hubExposure: (input) => {
3082
+ const api = this.hubMemberExposureApi;
3083
+ if (!api)
3084
+ throw Object.assign(new Error("hub_member_unavailable"), { code: "hub_member_unavailable" });
3085
+ return api.preview(input);
3086
+ },
3087
+ hubPairing: (action, input) => {
3088
+ const api = this.hubMemberPairingApi;
3089
+ if (!api)
3090
+ throw Object.assign(new Error("hub_member_unavailable"), { code: "hub_member_unavailable" });
3091
+ return pairingSummary(action, input, api.view(), api.boundHub());
3092
+ },
2443
3093
  proof: (kind, target, id, binding, key) => {
2444
3094
  const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
2445
3095
  const proof = map.get(id);
@@ -2484,6 +3134,7 @@ export class FleetManager {
2484
3134
  && sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
2485
3135
  }, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
2486
3136
  selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
3137
+ directAgentCreation: true,
2487
3138
  job: async (id) => {
2488
3139
  const settlement = this.settingsJobSettlements.get(id);
2489
3140
  if (!settlement)
@@ -2588,6 +3239,132 @@ export class FleetManager {
2588
3239
  // while the request is applied fences it.
2589
3240
  return this.liveOwner(req, { hostOnly: false });
2590
3241
  }
3242
+ /**
3243
+ * Refs #3061 (P2a): the new-device approval's one authority — the same one that decides an agent's request: this web-only
3244
+ * fleet's owner, asked of the LIVE session each time (a sign-out, an expiry, a revocation, the authority switch or
3245
+ * "Only this computer can authorize" turning a session into a non-owner is seen at once). A LAN session that signed in
3246
+ * with a one-time code, a session a device approval made, a guest, a chat fleet's admin and the CLI's header token are
3247
+ * none of them this: they are told so, and learn nothing about any request.
3248
+ */
3249
+ deviceApprovalOwner(req) {
3250
+ return this.liveOwner(req, { hostOnly: false });
3251
+ }
3252
+ /**
3253
+ * Refs #3061 (auth-api): device-code sign-in is offered — a web-only fleet with web authorization on. With
3254
+ * `web.verified_authority.enabled: false` no one is an owner (liveOwner → null), so no one could approve: the three
3255
+ * `/auth/device-login*` routes answer 404 and the sign-in page shows no button. "Only this computer can authorize"
3256
+ * does not close it: this computer's own session can still approve.
3257
+ */
3258
+ deviceLoginAvailable() {
3259
+ return this.isWebOnlyFleet() && this.webAuthorityState().enabled;
3260
+ }
3261
+ /** Refs #3061 (P2b): `web.login_qr` (default off), read per page served — the sign-in page then draws the approve link's QR code. */
3262
+ loginQrEnabled() {
3263
+ return this.fleetConfig?.web?.login_qr === true;
3264
+ }
3265
+ /**
3266
+ * GET /ui/devices: is this a web-only fleet, may THIS session approve a new device and end another (is it the verified
3267
+ * owner), and — to an admin session, never to a guest or the CLI's header token — the signed-in devices (Refs #3061, P2b).
3268
+ * `authority`: web authorization is on. With it off nobody is an owner (liveOwner → null), so nobody may add a device or
3269
+ * sign another out; the page says why and points to `agend web-token rotate` on this computer instead of "use the AgEnD
3270
+ * computer", which would not help.
3271
+ */
3272
+ webDevicesStatus(req) {
3273
+ const webOnly = this.isWebOnlyFleet();
3274
+ const owner = webOnly && !!this.deviceApprovalOwner(req);
3275
+ return { webOnly, owner, authority: webOnly && this.webAuthorityState().enabled, devices: webOnly ? this.webDeviceRows(req) : [] };
3276
+ }
3277
+ /**
3278
+ * Refs #3061 (P2b): the device list — this fleet's admin sessions, newest activity first (a guest's sessions are the
3279
+ * Guests page's, and a read-only tier has none): how each was approved, whether it carries owner authority, and which
3280
+ * one is the reader's own. The same people may read this as may read `GET /auth/sessions` (any admin session).
3281
+ *
3282
+ * `ownerVerified` is the §3 verdict for the session as it would be used from where it signs in (a host session from
3283
+ * this computer, a session from outside over the link): one that signed in with a one-time code or a device approval is
3284
+ * not an owner until the authenticator vouches for it. Display only — the checks that act on it run again at each use.
3285
+ */
3286
+ webDeviceRows(req) {
3287
+ const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
3288
+ if (auth.kind !== "ok" || auth.session.tier !== "admin" || !this.webSessions)
3289
+ return [];
3290
+ const authorizations = this.sessionAuthorizationStore();
3291
+ return this.webSessions.list(auth.session.idHash, r => r.tier === "admin").map(summary => {
3292
+ const record = this.webSessions.findByHandle(summary.handle);
3293
+ const gateway = summary.surface === "gateway";
3294
+ const peer = gateway ? authorizations?.get(summary.handle)?.peer ?? "quick_link" : record?.host === true ? "loopback" : "lan";
3295
+ return { handle: summary.handle, label: summary.label, via: summary.via, current: summary.current, surface: summary.surface,
3296
+ created: summary.created, lastSeen: summary.lastSeen, expiresAt: Math.min(summary.absoluteExpiry, summary.idleExpiry),
3297
+ ownerVerified: !!record && !this.webAuthority({ session: record, gateway, peer }).refusal };
3298
+ });
3299
+ }
3300
+ /**
3301
+ * Refs #3061 (P2b): may this request's session end ANOTHER session — one signed in elsewhere, or all of them (auth-api
3302
+ * `DELETE /auth/sessions…`, which an admin session could always do). On a web-only fleet that is the verified owner's:
3303
+ * a session a code or a device approval made is signed in but not an owner, and could otherwise sign out the owner's
3304
+ * own. A fleet with a chat connection is as it was (its admins are the chat's). Ending one's OWN session is never this.
3305
+ */
3306
+ mayEndOtherSessions(req) {
3307
+ return !this.isWebOnlyFleet() || !!this.deviceApprovalOwner(req);
3308
+ }
3309
+ /**
3310
+ * Refs #3061 (P2b): POST /ui/devices/signout {handle} — the owner ends another signed-in device (the settings list's
3311
+ * button). Only the verified owner, asked of the LIVE session each time (a non-owner learns nothing about any handle:
3312
+ * 403 before anything is looked up); only an admin session of this fleet (a guest's is the Guests page's; an unknown
3313
+ * handle and a guest's are the same 404); never the owner's own session here (Sign out is /auth/logout). The device's
3314
+ * authorization goes with it, so a copy of its cookie is nobody's.
3315
+ */
3316
+ webDeviceSignOut(req, handle) {
3317
+ if (!this.isWebOnlyFleet() || !this.webSessions)
3318
+ return { status: 404, body: { error: "not_found" } };
3319
+ const owner = this.deviceApprovalOwner(req);
3320
+ if (!owner)
3321
+ return { status: 403, body: { error: "owner_required" } };
3322
+ if (handle === owner.handle)
3323
+ return { status: 400, body: { error: "this_device" } };
3324
+ const target = this.webSessions.findByHandle(handle);
3325
+ if (!target || target.tier !== "admin")
3326
+ return { status: 404, body: { error: "not_found" } };
3327
+ const { found, durable } = this.webSessions.revokeByHandle(handle, r => r.tier === "admin");
3328
+ if (!found)
3329
+ return { status: 404, body: { error: "not_found" } };
3330
+ this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
3331
+ this.logger.info({ by: owner.handle.slice(0, 8), handle, label: target.label, via: sessionVia(target), durable }, "Device signed out by the owner");
3332
+ if (!durable)
3333
+ return { status: 500, body: { ok: false, durable: false, error: "not_durable" } };
3334
+ return { status: 200, body: { ok: true } };
3335
+ }
3336
+ /**
3337
+ * Refs #3061 (P2a): look a new device's code up (for the confirm screen), approve it, or deny it — the verified owner's,
3338
+ * on a web-only fleet. The answer is the request as it now stands. Approving only marks the request approved: the new
3339
+ * browser makes its own session when it next asks (auth-api POST /auth/device-login/poll), so no credential passes from
3340
+ * here to there — not in the answer, not through the fleet. A wrong code counts against one fleet-wide breaker; while
3341
+ * it is open every code (a right one too) waits.
3342
+ */
3343
+ webDeviceLoginAct(req, op, code) {
3344
+ const owner = this.deviceApprovalOwner(req);
3345
+ if (!owner)
3346
+ return { status: 403, body: { error: "owner_required" } };
3347
+ const logins = this.webDeviceLogins, token = this.webToken;
3348
+ if (!logins || !token)
3349
+ return { status: 404, body: { error: "not_found" } };
3350
+ const epoch = tokenEpoch(token);
3351
+ const shown = (v) => ({ code: v.code, label: v.label, peer: v.peer, address: v.address, expiresAt: v.expiresAt, state: v.state });
3352
+ if (op === "lookup") {
3353
+ const r = logins.lookup(code, epoch);
3354
+ if (r.kind === "ok")
3355
+ return { status: 200, body: { request: shown(r.view) } };
3356
+ return r.kind === "paused" ? { status: 429, body: { error: "paused", retryAfterMs: r.retryAfterMs } } : { status: 404, body: { error: "not_found" } };
3357
+ }
3358
+ // (The owner above was asked of the live session just now, after the body was read; nothing is awaited since.)
3359
+ const r = logins.decide(code, op === "approve" ? "approved" : "denied", owner.handle, epoch);
3360
+ if (r.kind === "ok") {
3361
+ this.logger.info({ approvedBy: owner.handle.slice(0, 8), label: r.view.label, peer: r.view.peer, decision: r.view.state }, op === "approve" ? "New device sign-in approved" : "New device sign-in denied");
3362
+ return { status: 200, body: { request: shown(r.view) } };
3363
+ }
3364
+ if (r.kind === "decided")
3365
+ return { status: 409, body: { error: "already_decided", state: r.state } };
3366
+ return r.kind === "paused" ? { status: 429, body: { error: "paused", retryAfterMs: r.retryAfterMs } } : { status: 404, body: { error: "not_found" } };
3367
+ }
2591
3368
  /** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
2592
3369
  webAgentProposals(req) {
2593
3370
  if (!this.agentProposalOwner(req))
@@ -3145,6 +3922,8 @@ export class FleetManager {
3145
3922
  sseHeartbeatMs = SSE_HEARTBEAT_MS;
3146
3923
  /** The dashboard's login codes. Memory only: a code that outlives the process is a code nobody can prove was not copied. */
3147
3924
  webLoginCodes = null;
3925
+ /** Refs #3061 (P2a): device-code sign-in requests (in memory; web-device-login.ts). Read by auth-api as `webDeviceLogins`. */
3926
+ webDeviceLogins = null;
3148
3927
  /**
3149
3928
  * #1920 step 4: guests' codes, apart from the admins' — one per account, so no guest's `/web` withdraws another's or
3150
3929
  * an admin's code (#2093 review), at most 32 at once. Local sign-in only for now (audience "local").
@@ -3183,6 +3962,22 @@ export class FleetManager {
3183
3962
  * startup-only key at startup. See checkStartupSignatureConsistency(). */
3184
3963
  fleetSignatureMismatch = null;
3185
3964
  healthServerListening = false;
3965
+ agentCredentialsCache;
3966
+ get agentCredentials() {
3967
+ return this.agentCredentialsCache ??= new AgentCredentials(name => agentCredentialPolicy(this.fleetConfig, name, this.isClassicInstance(name)), name => {
3968
+ try {
3969
+ const token = randomBytes(32).toString("hex");
3970
+ const result = writeSecretFile(join(this.getInstanceDir(name), "agent.token"), token);
3971
+ if (!result.ok)
3972
+ this.logger.error({ name, mode: result.mode, reason: result.reason }, "Agent token file is not owner-only");
3973
+ return token;
3974
+ }
3975
+ catch {
3976
+ this.logger.error({ name }, "Agent credential renewal failed; prior generation remains retired");
3977
+ return null;
3978
+ }
3979
+ });
3980
+ }
3186
3981
  constructor(dataDir) {
3187
3982
  this.dataDir = dataDir;
3188
3983
  FleetManager.signalTarget = this;
@@ -3205,6 +4000,7 @@ export class FleetManager {
3205
4000
  this.topicCommands = new TopicCommands(this);
3206
4001
  this.topicArchiver = new TopicArchiver(this);
3207
4002
  this.statuslineWatcher = new StatuslineWatcher(this);
4003
+ this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
3208
4004
  }
3209
4005
  /** #1266: a reply's buttons — the store (reply-buttons.db) and what a click does. Created on first use. */
3210
4006
  replyButtonsCtl = null;
@@ -3585,6 +4381,10 @@ export class FleetManager {
3585
4381
  }
3586
4382
  }
3587
4383
  }
4384
+ // #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
4385
+ if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
4386
+ void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
4387
+ }
3588
4388
  // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
3589
4389
  // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
3590
4390
  this.scheduleDeliveryOutboxPump();
@@ -4462,6 +5262,7 @@ export class FleetManager {
4462
5262
  this.enforceGuestSwitch();
4463
5263
  this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
4464
5264
  this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
5265
+ this.observeHubSwitches(); // S-51: and the Hub's (no-op until the fleet has started)
4465
5266
  // Child tool/guidance readers must follow this fleet's actual config, including --config.
4466
5267
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
4467
5268
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
@@ -4515,6 +5316,8 @@ export class FleetManager {
4515
5316
  * guild channel.
4516
5317
  */
4517
5318
  reregisterClassicChannels() {
5319
+ this.nativeMcpStore?.observePolicies();
5320
+ this.agentCredentialsCache?.observePolicies();
4518
5321
  if (!this.classicChannels)
4519
5322
  return;
4520
5323
  const channels = this.classicChannels.getAll();
@@ -4567,13 +5370,9 @@ export class FleetManager {
4567
5370
  }
4568
5371
  /** AgEnD package version (for the Settings "current version" / What's New). */
4569
5372
  get currentVersion() {
4570
- try {
4571
- return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version ?? "unknown";
4572
- }
4573
- catch {
4574
- return "unknown";
4575
- }
5373
+ return RUNNING_BUILD.version;
4576
5374
  }
5375
+ get currentBuild() { return RUNNING_BUILD; }
4577
5376
  /**
4578
5377
  * Resolve a slash-command target in a channel. Classic channels are looked up
4579
5378
  * per-bot (same-channel multi-bot); a fleet-topic instance is found via the
@@ -5246,6 +6045,13 @@ export class FleetManager {
5246
6045
  changed: () => this.emitSseEvent("status", this.getUiStatus()),
5247
6046
  });
5248
6047
  }
6048
+ /**
6049
+ * Claude history in the View: an instance's earlier conversation for its View, from the transcript the Log reads —
6050
+ * the View's route applied its own access rules first (view-api.ts); the Log's switch plays no part.
6051
+ */
6052
+ viewHistory(name, q) {
6053
+ return viewHistory((n, rq) => this.log.readBack(n, rq), name, q);
6054
+ }
5249
6055
  /** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
5250
6056
  logSteps(name, q) { return this.log.steps(name, q); }
5251
6057
  /**
@@ -5512,7 +6318,7 @@ export class FleetManager {
5512
6318
  finish(false);
5513
6319
  });
5514
6320
  }
5515
- async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold) {
6321
+ async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold, beforeInput) {
5516
6322
  if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
5517
6323
  return false;
5518
6324
  await this.holdDeliveryForStorm(instanceName, deliveryEpoch);
@@ -5551,7 +6357,8 @@ export class FleetManager {
5551
6357
  const log = idleGateTimeoutLog(instanceName, timeoutMs, this.instanceStateCache.get(instanceName), Date.now());
5552
6358
  this.logger[log.level](log.fields, log.message);
5553
6359
  }
5554
- const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
6360
+ // The Hub representative's begin lease + input intent run inside the final connected hand-off (sendWhenConnected).
6361
+ const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold, beforeInput);
5555
6362
  if (!sent)
5556
6363
  return false;
5557
6364
  if (this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent)) {
@@ -5584,21 +6391,25 @@ export class FleetManager {
5584
6391
  * `onStartRetryHold` (#2484, a person's message): an instance in its own start-retry backoff is waited for until that
5585
6392
  * retry brings it up, however long the backoff, and fails only once the retries give up.
5586
6393
  */
5587
- async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
6394
+ async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold, beforeInput) {
5588
6395
  if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
5589
6396
  return false;
5590
6397
  const queued = this.ipcWaitTails.get(instanceName);
5591
6398
  if (!queued) {
5592
6399
  const ipc = this.instanceIpcClients.get(instanceName);
5593
- if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
5594
- return true;
6400
+ if (ipc?.connected) {
6401
+ if (beforeInput)
6402
+ return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
6403
+ if (ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
6404
+ return true;
6405
+ }
5595
6406
  }
5596
6407
  const attempt = (queued ?? Promise.resolve())
5597
6408
  .catch(() => { })
5598
6409
  .then(() => {
5599
6410
  if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
5600
6411
  return false;
5601
- return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
6412
+ return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold, beforeInput);
5602
6413
  });
5603
6414
  // The chain stores a settled-either-way promise so one failed delivery cannot
5604
6415
  // wedge every later one, and so `queued` above is safe to await unguarded.
@@ -5615,8 +6426,21 @@ export class FleetManager {
5615
6426
  }
5616
6427
  }
5617
6428
  }
6429
+ /**
6430
+ * The Hub representative's hand-off (#3145): only once a connected client exists, after every reconnect/order wait,
6431
+ * the begin lease + input intent (beforeInput), then the fence and the LIVE client are checked again and the payload
6432
+ * goes in one attempt. After the intent is spent nothing here retries: false is the caller's uncertain outcome.
6433
+ */
6434
+ async handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput) {
6435
+ if (!(await beforeInput()))
6436
+ return false;
6437
+ if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
6438
+ return false;
6439
+ const ipc = this.instanceIpcClients.get(instanceName);
6440
+ return !!ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload));
6441
+ }
5618
6442
  /** Poll for the instance's IPC to come back, then send. Throws if it does not. */
5619
- async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
6443
+ async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold, beforeInput) {
5620
6444
  let deadline = Date.now() + IPC_RECONNECT_GRACE_MS;
5621
6445
  let warned = false;
5622
6446
  let held = false;
@@ -5626,6 +6450,8 @@ export class FleetManager {
5626
6450
  // Re-read every round: a reconnect replaces the IpcClient object entirely,
5627
6451
  // so a cached reference would stay dead forever.
5628
6452
  const ipc = this.instanceIpcClients.get(instanceName);
6453
+ if (ipc?.connected && beforeInput)
6454
+ return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
5629
6455
  if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
5630
6456
  return true;
5631
6457
  // #2484: while the instance waits for (or runs) its own start retry, the grace runs from now — so the message is
@@ -6065,6 +6891,8 @@ export class FleetManager {
6065
6891
  if (this.generalLocationDeliveryHold(instanceName))
6066
6892
  return false;
6067
6893
  if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
6894
+ if (options.beforeInput)
6895
+ return false; // no pre-input hook on the Windows runtime
6068
6896
  return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
6069
6897
  ...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
6070
6898
  }
@@ -6089,7 +6917,7 @@ export class FleetManager {
6089
6917
  return false;
6090
6918
  this.enforceWarmCap(instanceName); // woke one → evict a different LRU idle if over cap
6091
6919
  }
6092
- const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold);
6920
+ const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold, options.beforeInput);
6093
6921
  if (!sent)
6094
6922
  return false;
6095
6923
  // A cross-instance item arriving before the daemon observes this turn as
@@ -6100,7 +6928,7 @@ export class FleetManager {
6100
6928
  return true;
6101
6929
  }
6102
6930
  const previous = this.idleGatedDeliveryTails.get(instanceName) ?? Promise.resolve();
6103
- const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold));
6931
+ const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold, options.beforeInput));
6104
6932
  this.idleGatedDeliveryTails.set(instanceName, delivery);
6105
6933
  try {
6106
6934
  return await delivery;
@@ -7371,6 +8199,17 @@ export class FleetManager {
7371
8199
  this.queueKiroIncompatNotice(name, err.message);
7372
8200
  return true;
7373
8201
  }
8202
+ /** No pane is needed for a compatibility refusal to remain in Needs you. */
8203
+ setKiroCompatibilityHold(name, message) {
8204
+ const holds = this.kiroCompatibilityHolds ??= new Map();
8205
+ if ((message === null && !holds.has(name)) || (message !== null && holds.get(name)?.message === message))
8206
+ return;
8207
+ if (message === null)
8208
+ holds.delete(name);
8209
+ else
8210
+ holds.set(name, { message, since: Date.now() });
8211
+ this.needsYou?.poke();
8212
+ }
7374
8213
  /**
7375
8214
  * LifecycleContext: a kiro instance the installed kiro-cli cannot run as
7376
8215
  * configured (#1109) — at start, or at a respawn after kiro-cli replaced
@@ -7877,6 +8716,12 @@ export class FleetManager {
7877
8716
  },
7878
8717
  });
7879
8718
  }
8719
+ if (!this.webDeviceLogins) {
8720
+ this.webDeviceLogins = new DeviceLoginRequests({
8721
+ onEvent: event => { if (event === "breaker-open")
8722
+ this.logger.warn("Device sign-in approval paused: too many wrong codes"); },
8723
+ });
8724
+ }
7880
8725
  if (!this.webGuestLoginCodes) {
7881
8726
  this.webGuestLoginCodes = new GuestLoginCodes({
7882
8727
  onEvent: event => {
@@ -8491,6 +9336,7 @@ export class FleetManager {
8491
9336
  void this.publicWebLink?.close("dashboard revoke");
8492
9337
  this.forgetPublicLinkOwner();
8493
9338
  this.webLoginCodes.revoke();
9339
+ this.webDeviceLogins?.revokeAll(); // …and every new device still waiting to be approved (#3061)
8494
9340
  this.webGuestLoginCodes.revokeAll();
8495
9341
  this.guestRevokes++; // and every guest /web still on its way
8496
9342
  // #1570: no browser may ask the sign-in page for a code any more.
@@ -9454,8 +10300,9 @@ export class FleetManager {
9454
10300
  // #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
9455
10301
  const topicMode = daemonTopicMode(fleet);
9456
10302
  // Set tmux socket isolation for custom AGEND_HOME
9457
- const { getTmuxSocketName: getSocket } = await import("./paths.js");
9458
- TmuxManager.setSocketName(getSocket());
10303
+ // M-2457: the bundled tmux's socket while it is on; getTmuxSocketName() as before while off.
10304
+ const { tmuxSocket } = await import("./tmux-binary.js");
10305
+ TmuxManager.setSocketName(tmuxSocket());
9459
10306
  await TmuxManager.ensureSession(getTmuxSession());
9460
10307
  this.tmuxSessionEnsured = true;
9461
10308
  // #2072: what this host can run — probed in the background, published for every picker.
@@ -9720,6 +10567,9 @@ export class FleetManager {
9720
10567
  await this.startHealthServer(fleet.health_port ?? 19280);
9721
10568
  if (this.shuttingDown)
9722
10569
  return;
10570
+ // S-51: from here on the Hub switches apply (an enabled Hub mounts now, beside the fleet; it never blocks startup).
10571
+ this.hubEffectsReady = true;
10572
+ this.observeHubSwitches();
9723
10573
  if (generals.length > 0) {
9724
10574
  for (const [name, cfg] of generals) {
9725
10575
  try {
@@ -9822,6 +10672,9 @@ export class FleetManager {
9822
10672
  if (others.length > 0 || classicAtBoot.length > 0) {
9823
10673
  await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
9824
10674
  }
10675
+ // #3066 §1d (Fable r1): on every fleet — a web-only one (#2910) has no chat connection at all, and is the clearest
10676
+ // case with no Discord bot reading. Each tick decides for itself whether to read (backgroundQuotaRead).
10677
+ this.startQuotaBackgroundReads();
9825
10678
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
9826
10679
  await adapterStartup;
9827
10680
  this.startDiscordUsagePresence();
@@ -9997,6 +10850,178 @@ export class FleetManager {
9997
10850
  this.logger.warn({ health }, "Fleet started with problems — see /health");
9998
10851
  }
9999
10852
  }
10853
+ /**
10854
+ * #3066 §1d (the user: yes, every 30 min): the quota history (usage/quota-history.ts) records only the readings AgEnD
10855
+ * makes. A Discord bot's presence refresh reads every 15 min; without one, nothing reads while nobody looks. So, with no
10856
+ * Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
10857
+ * still bounds the vendor calls), recorded like any other.
10858
+ */
10859
+ startQuotaBackgroundReads() {
10860
+ if (this.quotaReadTimer)
10861
+ clearInterval(this.quotaReadTimer);
10862
+ this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
10863
+ this.quotaReadTimer.unref?.();
10864
+ // #3066 §1: the measured capacity, published for the usage reads (get_usage, the page) — refreshed every 10 min.
10865
+ setCapacityLookup((provider, metric) => this.capacityEstimates.get(`${provider}\u0000${metric}`) ?? null);
10866
+ if (this.capacityTimer)
10867
+ clearInterval(this.capacityTimer);
10868
+ void this.refreshCapacity();
10869
+ this.capacityTimer = setInterval(() => { void this.refreshCapacity(); }, FleetManager.CAPACITY_REFRESH_MS);
10870
+ this.capacityTimer.unref?.();
10871
+ }
10872
+ /** #3066 §1: the latest estimates, by `provider\0metric` (what usage-api's lookup reads, synchronously). */
10873
+ capacityEstimates = new Map();
10874
+ capacityTimer = null;
10875
+ static CAPACITY_REFRESH_MS = 10 * 60_000;
10876
+ /**
10877
+ * #3066 §1: estimate each account's weekly 100% from the quota history (§1d) and the usage ledger's hours — both read
10878
+ * asynchronously, the arithmetic small (usage/capacity.ts). An account is its usage provider id ("claude",
10879
+ * "claude:hsinlin", "codex:yanxianchen"): its hours are the ledger sources of that backend under that credential profile
10880
+ * (Codex's "unrecorded" hours are no account's). A per-model weekly ("Sonnet (weekly)") counts only that model's tokens.
10881
+ *
10882
+ * Prism #3073: no estimate rather than a wrong one —
10883
+ * - a window is measured only where the ledger has its hours: kept (31 days) and scanned (`days.coverage`) for every
10884
+ * instance of the account's backend, and every instance whose hours count for it; the quota history goes back 12 weeks;
10885
+ * - a profile literally named "default", "unknown" or "unrecorded" (configured, on disk, or in the quota history) makes
10886
+ * those hours ambiguous: neither the host
10887
+ * account nor the named one is estimated (each would be credited the other's use, or lose its own);
10888
+ * - only a metric the vendor marks per-model (`scope: "model"`) is filtered by model; another named weekly pool
10889
+ * ("Luna Reserve (weekly)", Antigravity pools) is no model name and is not estimated.
10890
+ */
10891
+ async refreshCapacity() {
10892
+ try {
10893
+ const readings = await readQuotaHistory();
10894
+ const groups = new Map();
10895
+ for (const r of readings) {
10896
+ const k = `${r.provider}\u0000${r.metric}`;
10897
+ (groups.get(k) ?? groups.set(k, []).get(k)).push(r);
10898
+ }
10899
+ if (!groups.size) {
10900
+ this.capacityEstimates = new Map();
10901
+ return 0;
10902
+ }
10903
+ const data = await this.cacheServiceFor().usageData();
10904
+ const now = Date.now(), DAY = 24 * 3_600_000, HOUR = 3_600_000;
10905
+ const keptFrom = now - KEEP_DAYS * DAY + HOUR; // the oldest hour still in every ledger (an hour of margin)
10906
+ // Profiles whose names are also the ledger's own tags: their hours cannot be told from the host's / unattributed.
10907
+ // Any trace of such a profile counts (Prism #3073 r2), not only today's config — a binding removed or switched
10908
+ // leaves its hours in the ledger: configured now, its directory on this host, or its account in the quota history.
10909
+ const RESERVED = new Set(["default", "unknown", PROFILE_UNRECORDED]);
10910
+ const historyProviders = new Set(readings.map((r) => r.provider));
10911
+ const reservedNamed = new Map();
10912
+ const reservedFor = (backend) => {
10913
+ const known = reservedNamed.get(backend);
10914
+ if (known)
10915
+ return known;
10916
+ const found = new Set(listConfiguredProfiles(this.fleetConfig, backend).filter((n) => RESERVED.has(n)));
10917
+ const base = usageProviderIdForBackend(backend);
10918
+ for (const n of RESERVED) {
10919
+ if (base && historyProviders.has(`${base}:${n}`))
10920
+ found.add(n);
10921
+ if (credentialHomeSpec(backend) && existsSync(credentialProfileHome(this.dataDir, backend, n)))
10922
+ found.add(n);
10923
+ }
10924
+ return reservedNamed.set(backend, found).get(backend);
10925
+ };
10926
+ // Every instance of a backend may have used any of its accounts (bindings change): an account is measured only
10927
+ // where all of them were scanned (Prism #3073 r2) — one still waiting for its first scan has no hours yet, not none.
10928
+ const daysOfBase = new Map();
10929
+ for (const inst of data.instances) {
10930
+ const base = usageProviderIdForBackend(inst.inst.backend);
10931
+ if (base)
10932
+ (daysOfBase.get(base) ?? daysOfBase.set(base, []).get(base)).push(inst.days);
10933
+ }
10934
+ // provider id → hour → model key → totals; and the instances whose hours count for it (their scan coverage).
10935
+ const byProvider = new Map();
10936
+ const daysOf = new Map();
10937
+ for (const inst of data.instances) {
10938
+ for (const [hk, hour] of Object.entries(inst.ledger.hours)) {
10939
+ for (const src of hourSources(hour)) {
10940
+ const base = usageProviderIdForBackend(src.backend);
10941
+ if (!base)
10942
+ continue;
10943
+ // Ambiguous (a named profile with the tag's name), or no account's: counted for no one, so an account whose
10944
+ // hours these may be has none — and gets no estimate, never a low one.
10945
+ if (reservedFor(src.backend).has(src.profile) || src.profile === "unknown" || src.profile === PROFILE_UNRECORDED)
10946
+ continue;
10947
+ const provider = src.profile === "default" ? base : `${base}:${src.profile}`;
10948
+ const list = daysOf.get(provider) ?? daysOf.set(provider, []).get(provider);
10949
+ if (!list.includes(inst.days))
10950
+ list.push(inst.days);
10951
+ const hours = byProvider.get(provider) ?? byProvider.set(provider, new Map()).get(provider);
10952
+ const into = hours.get(Number(hk)) ?? hours.set(Number(hk), Object.create(null)).get(Number(hk));
10953
+ for (const [mk, tot] of Object.entries(src.m)) {
10954
+ const cur = into[mk] ??= [0, 0, 0, 0, 0, 0, 0];
10955
+ for (let i = 0; i < 7; i++)
10956
+ cur[i] = (cur[i] ?? 0) + (tot[i] ?? 0);
10957
+ }
10958
+ }
10959
+ }
10960
+ }
10961
+ const next = new Map();
10962
+ for (const [key, list] of groups) {
10963
+ const [provider, metric] = key.split("\u0000");
10964
+ // A per-model weekly ("Sonnet (weekly)", the vendor's scope "model"): that model's tokens only. Another named
10965
+ // pool is not a model: no estimate. "Weekly", "Weekly limit": everything.
10966
+ const named = /^(.+) \((?:weekly|monthly|\d+d)\)$/i.exec(metric)?.[1]?.toLowerCase() ?? null;
10967
+ const perModel = list.some((r) => r.scope === "model");
10968
+ if (named && !perModel)
10969
+ continue;
10970
+ const hours = [];
10971
+ for (const [hour, models] of byProvider.get(provider) ?? []) {
10972
+ let api = 0, output = 0, all = 0;
10973
+ for (const [mk, tot] of Object.entries(models)) {
10974
+ const bar = mk.lastIndexOf("|"), model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
10975
+ if (named && !model.toLowerCase().includes(named))
10976
+ continue;
10977
+ const [, uncached, read, w5, w1h, w, out] = tot;
10978
+ const r = ratesFor(model, long ? Number.MAX_SAFE_INTEGER : 0);
10979
+ 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;
10980
+ output += out;
10981
+ all += uncached + read + w5 + w1h + w + out;
10982
+ }
10983
+ if (all > 0)
10984
+ hours.push({ hour, api, output, all });
10985
+ }
10986
+ const base = provider.split(":")[0];
10987
+ const scanned = [...new Set([...(daysOf.get(provider) ?? []), ...(daysOfBase.get(base) ?? [])])];
10988
+ const available = (from, to) => {
10989
+ if (from < keptFrom || !scanned.length)
10990
+ return false;
10991
+ for (let h = from; h <= to; h += HOUR)
10992
+ if (!scanned.every((d) => covered(d, h)))
10993
+ return false;
10994
+ return true;
10995
+ };
10996
+ const est = estimateCapacity(list, hours, available);
10997
+ if (est)
10998
+ next.set(key, est);
10999
+ }
11000
+ this.capacityEstimates = next;
11001
+ return next.size;
11002
+ }
11003
+ catch (err) {
11004
+ this.logger.debug({ err }, "capacity estimate refresh failed");
11005
+ return -1;
11006
+ }
11007
+ }
11008
+ /** One background quota read, when nothing else reads (no Discord bot) and an account has a running agent. */
11009
+ async backgroundQuotaRead() {
11010
+ if (this.shuttingDown)
11011
+ return false;
11012
+ if ([...this.adapters.values()].some(a => a.type === "discord" && typeof a.setActivity === "function"))
11013
+ return false;
11014
+ const active = this.getActiveUsageProviderIds();
11015
+ if (active.size === 0)
11016
+ return false;
11017
+ try {
11018
+ await getUsageSnapshot(false, active);
11019
+ return true;
11020
+ }
11021
+ catch {
11022
+ return false;
11023
+ }
11024
+ }
10000
11025
  /** Keep Discord profile activity aligned with the same cached usage source as /usage. */
10001
11026
  startDiscordUsagePresence() {
10002
11027
  if (this.discordPresenceTimer)
@@ -10334,8 +11359,8 @@ export class FleetManager {
10334
11359
  * that should be running is running
10335
11360
  * - `degraded` — reachable, but something the operator should look at (an
10336
11361
  * adapter retrying, an instance crashed or stopped)
10337
- * - `down` — the fleet cannot do its job: no adapter is connected, so no
10338
- * message can arrive or be answered
11362
+ * - `down` — no configured adapter is connected, or a dedicated Hub's
11363
+ * server mount is unavailable
10339
11364
  *
10340
11365
  * Deliberately does NOT gate the systemd watchdog — see the comment at the
10341
11366
  * WATCHDOG timer for why.
@@ -10368,10 +11393,20 @@ export class FleetManager {
10368
11393
  if (effectiveStatus === "connected")
10369
11394
  connected++;
10370
11395
  }
10371
- const problems = [];
11396
+ const problems = [], warnings = [];
11397
+ const rawHub = this.rawFleetConfig?.hub;
11398
+ const hub = rawHub?.enabled === true ? { server: this.shuttingDown ? "stopped"
11399
+ : this.hubRuntime?.serverHealth?.(rawHub) ?? "unavailable" } : undefined;
11400
+ const hubOnly = !!hub && names.length === 0 && this.adapterState.size === 0;
11401
+ if (hub && hub.server !== "ready")
11402
+ problems.push(`Hub server is ${hub.server}`);
10372
11403
  const hostMemory = this.memoryPressure.snapshot();
10373
- if (!this.memoryPressure.advisoryOnly() && (hostMemory.level === "critical" || hostMemory.level === "elevated")) {
10374
- problems.push(`host memory pressure is ${hostMemory.level}`);
11404
+ if (hostMemory.level === "critical" || hostMemory.level === "elevated") {
11405
+ const pressure = `host memory pressure is ${hostMemory.level}`;
11406
+ if (hubOnly)
11407
+ warnings.push(pressure);
11408
+ else if (!this.memoryPressure.advisoryOnly())
11409
+ problems.push(pressure);
10375
11410
  }
10376
11411
  if (this.adapterState.size > 0 && connected === 0)
10377
11412
  problems.push("no channel adapter is connected");
@@ -10383,9 +11418,8 @@ export class FleetManager {
10383
11418
  }
10384
11419
  if (!this.startupComplete)
10385
11420
  problems.push("startup has not completed");
10386
- // "down" is reserved for "cannot receive or answer a message at all". A fleet
10387
- // with adapters configured but none connected is exactly that.
10388
- const status = this.adapterState.size > 0 && connected === 0
11421
+ // Preserve adapter scoring. A dedicated Hub also cannot serve its workload before its server mount is ready.
11422
+ const status = (this.adapterState.size > 0 && connected === 0) || (hubOnly && hub?.server !== "ready")
10389
11423
  ? "down"
10390
11424
  : problems.length > 0 ? "degraded" : "ok";
10391
11425
  return {
@@ -10397,7 +11431,7 @@ export class FleetManager {
10397
11431
  memory: readFleetMemory(),
10398
11432
  hostMemory: this.memoryTop && (hostMemory.level === "elevated" || hostMemory.level === "critical")
10399
11433
  ? { ...hostMemory, topProcesses: this.memoryTop.processes, topProcessesAt: this.memoryTop.at } : hostMemory,
10400
- problems,
11434
+ problems, warnings, ...(hub ? { hub } : {}),
10401
11435
  };
10402
11436
  }
10403
11437
  /** Start the primary adapter (backward-compatible, sets this.adapter) */
@@ -10472,6 +11506,11 @@ export class FleetManager {
10472
11506
  adapter.on("callback_query", safeHandler(async (data) => {
10473
11507
  await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
10474
11508
  }, this.logger, "adapter.callback_query"));
11509
+ adapter.on("missed_offline", (ev) => {
11510
+ if (!isCurrentAdapter())
11511
+ return;
11512
+ this.recordMissedOffline(ev);
11513
+ });
10475
11514
  this.bindTopicClosedHandler(adapter, adapterId, "adapter.topic_closed");
10476
11515
  // Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
10477
11516
  adapter.on("slash_command", safeHandler(async (data) => {
@@ -10625,6 +11664,11 @@ export class FleetManager {
10625
11664
  adapter.on("callback_query", safeHandler(async (data) => {
10626
11665
  await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
10627
11666
  }, this.logger, `adapter[${adapterId}].callback_query`));
11667
+ adapter.on("missed_offline", (ev) => {
11668
+ if (!isCurrentAdapter())
11669
+ return;
11670
+ this.recordMissedOffline(ev);
11671
+ });
10628
11672
  this.bindTopicClosedHandler(adapter, adapterId, `adapter[${adapterId}].topic_closed`);
10629
11673
  // Slash commands: classic bot + admin commands
10630
11674
  adapter.on("slash_command", safeHandler(async (data) => {
@@ -10784,6 +11828,9 @@ export class FleetManager {
10784
11828
  this.eventLog?.insert(targetInstance, "cross_instance_delivery_failed", {
10785
11829
  from: senderSession, correlation_id: correlationId, error,
10786
11830
  });
11831
+ // A Hub question the pane never took: the representative holds it uncertain (never resent).
11832
+ if (senderSession === HUB_REPORT_TARGET && !senderInstance)
11833
+ this.hubDeliveryFailed?.(correlationId, targetInstance);
10787
11834
  if (senderInstance) {
10788
11835
  this.notifyInstanceTopic(senderInstance, t("cross_instance.delivery_failed_notice", targetInstance, displayError, correlationId));
10789
11836
  void this.deliverToInstance(senderInstance, {
@@ -10902,13 +11949,12 @@ export class FleetManager {
10902
11949
  try {
10903
11950
  const { execFile } = await import("./child-process.js");
10904
11951
  const { promisify } = await import("node:util");
10905
- const { getTmuxSocketName } = await import("./paths.js");
11952
+ const { tmuxArgs, tmuxBinary, tmuxEnvOption } = await import("./tmux-binary.js");
10906
11953
  // Honour socket isolation: without -L this queried the user's default
10907
11954
  // tmux server instead of the fleet's, so under a custom AGEND_HOME the
10908
11955
  // check was meaningless (it reported every pane dead).
10909
- const socket = getTmuxSocketName();
10910
- const args = socket ? ["-L", socket, "list-panes", "-t", windowId] : ["list-panes", "-t", windowId];
10911
- await promisify(execFile)("tmux", args, { timeout: 5_000 });
11956
+ const args = tmuxArgs(["list-panes", "-t", windowId]);
11957
+ await promisify(execFile)(tmuxBinary(), args, { timeout: 5_000, ...tmuxEnvOption() });
10912
11958
  }
10913
11959
  catch {
10914
11960
  // Pane dead — respawn
@@ -11365,6 +12411,34 @@ export class FleetManager {
11365
12411
  }
11366
12412
  return true;
11367
12413
  }
12414
+ /** File a Telegram update that was too old to inject; Needs you lists it until Acknowledge. */
12415
+ recordMissedOffline(ev) {
12416
+ if (!this.missedOfflineStore)
12417
+ this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
12418
+ const stub = {
12419
+ source: "telegram", adapterId: ev.adapterId, chatId: ev.chatId, threadId: ev.threadId,
12420
+ messageId: ev.messageId, userId: ev.userId, username: ev.username, text: ev.preview,
12421
+ timestamp: ev.timestamp, isBotMessage: false,
12422
+ };
12423
+ const threadId = this.inboundRouteThreadId(stub);
12424
+ const target = threadId ? this.resolveInboundTarget(stub, threadId) : undefined;
12425
+ let instance = target?.name ?? "";
12426
+ if (!instance) {
12427
+ instance = Object.entries(this.fleetConfig?.instances ?? {}).find(([name, cfg]) => cfg.general_topic && this.getInstanceAdapterId(name) === ev.adapterId)?.[0] ?? "";
12428
+ }
12429
+ const place = [ev.chatTitle, ev.threadId ? `#${ev.threadId}` : ""].filter(Boolean).join(" · ") || ev.chatId;
12430
+ this.missedOfflineStore.add({
12431
+ id: `missed:${ev.adapterId}:${ev.updateId}`,
12432
+ adapterId: ev.adapterId,
12433
+ instance,
12434
+ username: ev.username,
12435
+ place,
12436
+ preview: ev.preview,
12437
+ since: ev.timestamp.getTime(),
12438
+ });
12439
+ 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");
12440
+ this.needsYou?.poke();
12441
+ }
11368
12442
  /**
11369
12443
  * #1085: coincident topic numbers must not route across Telegram groups.
11370
12444
  */
@@ -13001,6 +14075,11 @@ export class FleetManager {
13001
14075
  let result;
13002
14076
  switch (msg.type) {
13003
14077
  case "fleet_decision_create": {
14078
+ const supersedes = this.decisionSupersedes("ipc-typed", instanceName, payload.supersedes);
14079
+ if ("error" in supersedes) {
14080
+ ipc.send({ type: "fleet_decision_response", fleetRequestId, error: supersedes.error });
14081
+ return;
14082
+ }
13004
14083
  // Prune expired decisions on create
13005
14084
  db.pruneExpiredDecisions();
13006
14085
  result = db.createDecision({
@@ -13011,7 +14090,7 @@ export class FleetManager {
13011
14090
  tags: payload.tags,
13012
14091
  ttl_days: payload.ttl_days,
13013
14092
  created_by: instanceName,
13014
- supersedes: payload.supersedes,
14093
+ supersedes: supersedes.id,
13015
14094
  });
13016
14095
  break;
13017
14096
  }
@@ -13188,6 +14267,21 @@ export class FleetManager {
13188
14267
  return "ok";
13189
14268
  }
13190
14269
  }
14270
+ /**
14271
+ * `post_decision` with `supersedes` changes a decision someone else may have recorded (it stops being active): the
14272
+ * same permission as `update_decision`, asked through the same live check (tool set + dashboard switches), on both
14273
+ * create paths. An empty or absent value supersedes nothing and needs nothing; anything else must be an id.
14274
+ */
14275
+ decisionSupersedes(sink, instanceName, value) {
14276
+ if (value === undefined || value === null || value === "")
14277
+ return { id: undefined };
14278
+ if (typeof value !== "string")
14279
+ return { error: "supersedes must be a decision id" };
14280
+ const permitted = this.checkToolPermission(sink, instanceName, "update_decision");
14281
+ if (!permitted.allowed)
14282
+ return { error: `Superseding a decision needs update_decision. ${permitted.message}` };
14283
+ return { id: value };
14284
+ }
13191
14285
  async handleDecisionCrudHttp(instance, op, args) {
13192
14286
  if (!this.scheduler)
13193
14287
  return { error: "Scheduler not available" };
@@ -13203,16 +14297,24 @@ export class FleetManager {
13203
14297
  if (!title || !content)
13204
14298
  return { error: "title and content are required" };
13205
14299
  const scope = args.scope === "fleet" ? "fleet" : "project";
13206
- return db.createDecision({
13207
- project_root: projectRoot,
13208
- scope,
13209
- title,
13210
- content,
13211
- tags: asStrArr(args.tags),
13212
- ttl_days: asNum(args.ttl_days),
13213
- supersedes: asStr(args.supersedes),
13214
- created_by: instance,
13215
- });
14300
+ const supersedes = this.decisionSupersedes("agent-endpoint", instance, args.supersedes);
14301
+ if ("error" in supersedes)
14302
+ return { error: supersedes.error };
14303
+ try {
14304
+ return db.createDecision({
14305
+ project_root: projectRoot,
14306
+ scope,
14307
+ title,
14308
+ content,
14309
+ tags: asStrArr(args.tags),
14310
+ ttl_days: asNum(args.ttl_days),
14311
+ supersedes: supersedes.id,
14312
+ created_by: instance,
14313
+ });
14314
+ }
14315
+ catch (err) {
14316
+ return { error: err.message }; // a refused predecessor: nothing was inserted
14317
+ }
13216
14318
  }
13217
14319
  // #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
13218
14320
  // The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
@@ -13255,6 +14357,8 @@ export class FleetManager {
13255
14357
  const asStatusFilter = normalizeStatusFilter;
13256
14358
  // #1336: small write ack — identifying fields only.
13257
14359
  const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
14360
+ // #3195: an update's ack also carries what was SAVED for each field it named, read back from the task.
14361
+ const updatedAck = (t, named) => ({ ...ack(t), ...savedFields(t, named) });
13258
14362
  // The board's notes for the write (a send-back reason, a comment the assignee had not seen): with the ack when any.
13259
14363
  const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
13260
14364
  try {
@@ -13312,12 +14416,12 @@ export class FleetManager {
13312
14416
  const id = asStr(args.id);
13313
14417
  if (!id)
13314
14418
  return { error: "id is required" };
13315
- return noted(ack(db.updateTask(db.getTaskByPrefix(id).id, {
14419
+ return noted(updatedAck(db.updateTask(db.getTaskByPrefix(id).id, {
13316
14420
  status: asStatus(args.status),
13317
14421
  assignee: asStr(args.assignee),
13318
14422
  result: asStr(args.result),
13319
14423
  priority: asPriority(args.priority),
13320
- }, { kind: "instance", name: instance })));
14424
+ }, { kind: "instance", name: instance }), args));
13321
14425
  }
13322
14426
  default: return { error: `Unknown task action: ${action}` };
13323
14427
  }
@@ -13471,7 +14575,8 @@ export class FleetManager {
13471
14575
  result: payload.result,
13472
14576
  priority: payload.priority,
13473
14577
  }, { kind: "instance", name: meta.instance_name || instanceName });
13474
- result = noted(ack(logTask));
14578
+ // #3195: with what was SAVED for each field the update named, read back from the task.
14579
+ result = noted({ ...ack(logTask), ...savedFields(logTask, payload) });
13475
14580
  break;
13476
14581
  default:
13477
14582
  throw new Error(`Unknown task action: ${action}`);
@@ -13501,16 +14606,101 @@ export class FleetManager {
13501
14606
  }
13502
14607
  return adapter.createTopic(topicName);
13503
14608
  }
13504
- async deleteForumTopic(topicId, adapterId) {
13505
- try {
13506
- const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
13507
- if (!adapter?.deleteTopic)
13508
- return;
13509
- await adapter.deleteTopic(topicId);
13510
- }
13511
- catch (err) {
13512
- this.logger.warn({ err, topicId }, "Failed to delete forum topic during rollback");
13513
- }
14609
+ /**
14610
+ * Delete an agent and, unless asked not to, its own chat topic (user decision: by default). The topic is planned
14611
+ * before the removal (its id and bot go with the fleet.yaml entry) and deleted through the agent's OWN bot after it;
14612
+ * the outcome is the platform's answer (instance-topic-delete.ts). A topic that cannot go never fails the removal.
14613
+ */
14614
+ async removeInstanceAndTopic(name, authorization, opts) {
14615
+ const plan = opts.deleteTopic ? this.planInstanceTopicDelete(name) : null;
14616
+ if (opts.execution)
14617
+ await this.removeInstance(name, authorization, opts.execution);
14618
+ else
14619
+ await this.removeInstance(name, authorization);
14620
+ // #3055 review (Prism P1): asked again right before the platform call — the fleet as it is NOW, not as planned
14621
+ // before the removal was awaited (a new owner of the topic, or the bot rebuilt).
14622
+ return deletePlannedTopic(plan, this.logger, (p) => {
14623
+ if (this.instanceTopicAdapter(p.adapterId) !== p.adapter)
14624
+ return "the agent's bot changed while it was being deleted, so its topic was left";
14625
+ return topicMustStay(p.destination, this.topicOwnership(), null); // the original is gone: every owner now counts
14626
+ }, { begin: (d) => this.topicDeletionsInFlight.add(d), end: (d) => this.topicDeletionsInFlight.delete(d) });
14627
+ }
14628
+ /** #3055: destinations whose platform deletion is running now; a binding to one waits until it is over. */
14629
+ topicDeletionsInFlight = new Set();
14630
+ /** True while a topic with this id is being deleted on any connection (a new binding of it is refused meanwhile). */
14631
+ isTopicDeletionInFlight(topicId) {
14632
+ const tail = `:${topicId}`;
14633
+ return [...this.topicDeletionsInFlight].some(d => d.endsWith(tail));
14634
+ }
14635
+ /**
14636
+ * #3055 review r2 (Prism P1-3): would this instance config bind a topic whose deletion is running now? Compared by
14637
+ * physical destination, so the same thread number in another Telegram group is not held. Settings asks it at commit.
14638
+ */
14639
+ topicBindingInFlight(inst) {
14640
+ if (this.topicDeletionsInFlight.size === 0 || inst.topic_id == null || inst.topic_id === "")
14641
+ return false;
14642
+ const adapterId = typeof inst.channel_id === "string" && inst.channel_id ? inst.channel_id : this.getPrimaryAdapterId();
14643
+ return this.topicDeletionsInFlight.has(this.destinationFor(adapterId, inst.topic_id));
14644
+ }
14645
+ /** Any topic deletion running now (a connection's group or General may not be re-pointed meanwhile). */
14646
+ topicDeletionPending() { return this.topicDeletionsInFlight.size > 0; }
14647
+ /** The physical destination of `topicId` on the bot `adapterId`. */
14648
+ destinationFor(adapterId, topicId) {
14649
+ const world = adapterId ? this.worlds.get(adapterId) : undefined;
14650
+ const platform = world?.adapter?.type ?? this.getChannelConfig(adapterId)?.type;
14651
+ const groupId = world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
14652
+ return topicDestination(platform, groupId, adapterId, topicId);
14653
+ }
14654
+ /** The adapter of the bot `adapterId` names — never a substitute for a bot that is not there. */
14655
+ instanceTopicAdapter(adapterId) {
14656
+ return (adapterId ? this.worlds.get(adapterId)?.adapter : this.worlds.size === 0 ? this.adapter ?? undefined : undefined) ?? undefined;
14657
+ }
14658
+ /**
14659
+ * Topic ownership by physical destination (#3055 review, Prism P1): an instance's topic is where its bot's platform
14660
+ * puts it (a Telegram group + thread, a Discord channel), so two bots in one group, or two instances on one
14661
+ * channel, are seen to share it; and every connection's own General is protected, with or without a General agent.
14662
+ */
14663
+ topicOwnership() {
14664
+ const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
14665
+ const destinationOf = (n) => {
14666
+ const cfg = this.fleetConfig?.instances?.[n];
14667
+ if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
14668
+ return null;
14669
+ return this.destinationFor(this.topicAdapterIdOf(n), cfg.topic_id);
14670
+ };
14671
+ const generalDestinations = () => {
14672
+ const out = new Set();
14673
+ for (const ch of channels) {
14674
+ const id = ch.id ?? ch.type;
14675
+ const groupId = this.worlds.get(id)?.groupId ?? (ch.group_id != null ? String(ch.group_id) : undefined);
14676
+ if (ch.type === "telegram")
14677
+ out.add(topicDestination("telegram", groupId, id, 1));
14678
+ if (ch.type === "discord") {
14679
+ const gcid = discordGeneralChannelId(ch);
14680
+ if (gcid)
14681
+ out.add(topicDestination("discord", groupId, id, gcid));
14682
+ }
14683
+ }
14684
+ return out;
14685
+ };
14686
+ return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
14687
+ }
14688
+ /**
14689
+ * The bot an agent's configured topic is on (#3105 review, Prism P1): a fleet.yaml agent's `channel_id` (else the
14690
+ * primary connection) whatever its runtime — a per-turn Windows agent has no execution adapter
14691
+ * (getInstanceAdapterId), yet its topic is still on that bot, and must be counted as an owner and deleted through it.
14692
+ * Topic planning and ownership only; execution routing is getInstanceAdapterId's and unchanged.
14693
+ */
14694
+ topicAdapterIdOf(name) {
14695
+ const cfg = this.fleetConfig?.instances?.[name];
14696
+ if (cfg)
14697
+ return cfg.channel_id ?? this.getPrimaryAdapterId();
14698
+ return this.getInstanceAdapterId(name);
14699
+ }
14700
+ /** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
14701
+ planInstanceTopicDelete(name) {
14702
+ const adapterId = this.topicAdapterIdOf(name);
14703
+ return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
13514
14704
  }
13515
14705
  getForumTopicDeleter(adapterId) {
13516
14706
  const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
@@ -13789,6 +14979,8 @@ export class FleetManager {
13789
14979
  this.observeWebAuthority(); // #2453 review: so is a web-authority switch (off is always seen)
13790
14980
  if (!this.fleetConfig || !this.configPath)
13791
14981
  return;
14982
+ // #3147 r2: a changed or removed Hub representative's consent is invalidated durably BEFORE the change is written.
14983
+ this.observeHubAgentIncarnations(true);
13792
14984
  if (!this.savedFleetConfigSnapshot)
13793
14985
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
13794
14986
  normalizeNoticeDelivery(this.fleetConfig);
@@ -13911,9 +15103,12 @@ export class FleetManager {
13911
15103
  Object.assign(current, resolved);
13912
15104
  }
13913
15105
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
15106
+ this.nativeMcpStore?.observePolicies();
15107
+ this.agentCredentialsCache?.observePolicies();
13914
15108
  this.refreshSavedDashboardPermissions();
13915
15109
  this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
13916
15110
  this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
15111
+ this.observeHubSwitches(); // S-51: so does a saved Hub switch
13917
15112
  }
13918
15113
  /** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
13919
15114
  refreshSavedDashboardPermissions() {
@@ -14223,6 +15418,7 @@ export class FleetManager {
14223
15418
  const instanceDirToClean = this.getInstanceDir(name);
14224
15419
  // #1774: replies sent from here on are a later instance's (a replacement registered while this one goes); the ones
14225
15420
  // sent before wait for the outcome before they are shown.
15421
+ this.forgetHubAgentIncarnation(name, true); // #3147: a re-added instance never inherits Hub consent; not durable → refused
14226
15422
  const removedBelow = this.webChatGeneration(name) + 1, removedDaemon = this.daemons.get(name);
14227
15423
  this.webChatGenerations.set(name, removedBelow);
14228
15424
  let settleRemoval;
@@ -16207,6 +17403,11 @@ export class FleetManager {
16207
17403
  needsYouItems() {
16208
17404
  return this.needsYou?.webItems() ?? [];
16209
17405
  }
17406
+ /** Enabled member composition supplies only a local attention reader, never an approval/execution callback. */
17407
+ attachHubExecutionAttention(read) {
17408
+ this.hubExecutionAttention = read;
17409
+ this.needsYou?.poke();
17410
+ }
16210
17411
  /**
16211
17412
  * #1389: the org chart's structure for the web — the instances the dashboard lists (getUiStatus's names), General,
16212
17413
  * fleet.yaml's teams, each instance's description and thread link. Read on demand; the live state rides the stream.
@@ -16270,6 +17471,7 @@ export class FleetManager {
16270
17471
  const fleet = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => !isWindowsInstance(cfg)).map(([name, cfg]) => ({
16271
17472
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
16272
17473
  claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
17474
+ ...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
16273
17475
  kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
16274
17476
  }));
16275
17477
  const taken = new Set(fleet.map(i => i.name));
@@ -16281,6 +17483,9 @@ export class FleetManager {
16281
17483
  claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
16282
17484
  profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
16283
17485
  kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
17486
+ // #3066 §1d (Fable r1 #2): a ClassicBot Codex room records its launches too (onDaemonReady) — read them the same way.
17487
+ ...((this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend) === "codex"
17488
+ ? { profileTimelinePath: join(this.getInstanceDir(ch.instanceName), PROFILE_TIMELINE_FILE) } : {}),
16284
17489
  }));
16285
17490
  return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
16286
17491
  }
@@ -16441,7 +17646,8 @@ export class FleetManager {
16441
17646
  out.push({
16442
17647
  name, state: p.state ?? undefined,
16443
17648
  interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
16444
- interactionSummary: p.interaction_summary ?? null,
17649
+ interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
17650
+ kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
16445
17651
  pauseReason, pausedAt,
16446
17652
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
16447
17653
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
@@ -16533,8 +17739,18 @@ export class FleetManager {
16533
17739
  throw new Error("the delivery outbox is not open");
16534
17740
  return this.deliveryOutbox.acknowledge(deliveryId, by);
16535
17741
  },
17742
+ missedOffline: () => (this.missedOfflineStore?.list(Date.now()) ?? []).map(m => ({
17743
+ id: m.id, instance: m.instance, owner: m.adapterId, username: m.username, place: m.place, preview: m.preview, since: m.since,
17744
+ })),
17745
+ acknowledgeMissed: (id, by) => this.missedOfflineStore?.acknowledge(id, by) ?? false,
16536
17746
  tasks: () => this.needsYouTaskItems(),
16537
17747
  backendSignIns: () => this.needsYouBackendSignIns(),
17748
+ hubExecution: () => this.hubExecutionAttention?.() ?? [],
17749
+ hubHolds: () => this.hubHolds.map(h => {
17750
+ // A provider hold carries a bounded setup reason; its guidance (S4b's copy) when present, else the code's text.
17751
+ const guidance = h.reason ? t(`hub.provider.${h.reason}`) : "";
17752
+ return { side: h.side, code: h.code, since: h.since, detail: guidance && guidance !== `hub.provider.${h.reason}` ? guidance : t(`hub.hold.${h.code}`) };
17753
+ }),
16538
17754
  configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
16539
17755
  ...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
16540
17756
  })),
@@ -17268,7 +18484,12 @@ export class FleetManager {
17268
18484
  if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
17269
18485
  return;
17270
18486
  this.outsideOpening = true;
17271
- void this.getPublicWebLink().openUnattended(OUTSIDE_LINK_OWNER).catch(() => "failed").then(outcome => {
18487
+ // alpha CI 4abe58cd: an On right after an Off finds the old link still closing (its tunnel stopping): wait for that
18488
+ // close, then open — never "busy" and a 30 s retry. The switch is asked again after the wait by openUnattended
18489
+ // itself (permitted → publicOwnerCurrent: the fleet running, the switch on): an Off meanwhile opens nothing.
18490
+ const link = this.getPublicWebLink();
18491
+ void link.whenClosed().then(() => link.openUnattended(OUTSIDE_LINK_OWNER))
18492
+ .catch(() => "failed").then(outcome => {
17272
18493
  this.outsideOpening = false;
17273
18494
  if (outcome === "opened") {
17274
18495
  this.logger.info({ attempt: n + 1 }, "Outside link opened");
@@ -17446,7 +18667,7 @@ export class FleetManager {
17446
18667
  // Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
17447
18668
  ...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
17448
18669
  const host = this.fleetConfig?.web?.public_link?.hostname;
17449
- return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null };
18670
+ return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null, webOnly: this.isWebOnlyFleet() };
17450
18671
  }
17451
18672
  authorizationRequests = null;
17452
18673
  /**
@@ -17509,8 +18730,9 @@ export class FleetManager {
17509
18730
  return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
17510
18731
  },
17511
18732
  signOut: (handle) => {
17512
- this.webSessions?.revokeByHandle(handle);
17513
- this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
18733
+ const session = this.webSessions?.revokeByHandle(handle) ?? { found: false, durable: true };
18734
+ const authorization = this.sessionAuthorizationStore()?.prune(a => a.handle !== handle) ?? { dropped: 0, durable: true };
18735
+ return { durable: session.durable, authorizationDurable: authorization.durable };
17514
18736
  },
17515
18737
  event: (type, payload) => { try {
17516
18738
  this.eventLog?.insert("web", type, payload);
@@ -17523,6 +18745,7 @@ export class FleetManager {
17523
18745
  expired: (c) => t("web_auth.card_expired", c.label),
17524
18746
  refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
17525
18747
  revoked: (c) => t("web_auth.card_revoked", c.label),
18748
+ notSaved: (c) => t("web_auth.card_not_saved", c.label),
17526
18749
  ended: (c) => t("web_auth.card_ended", c.label),
17527
18750
  generalHint: () => t("web_auth.general_hint"),
17528
18751
  confirmLabel: () => t("web_auth.confirm"),
@@ -18445,7 +19668,14 @@ export class FleetManager {
18445
19668
  return;
18446
19669
  }
18447
19670
  const publication = this.beginCancelButtonPublication(instanceName, correlationId);
18448
- const initialProgressText = FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
19671
+ // M-2391: a re-post below a reply (preserveProgress) continues the same turn — its elapsed time continues from
19672
+ // the live bubble's start, as its tool list does. Restarting it at 0 meant an agent replying more often than the
19673
+ // threshold never showed elapsed time at all. A fresh turn (no preserve) starts at 0 as before.
19674
+ const carriedStart = preserveProgress ? this.liveButtonStart(instanceName) : undefined;
19675
+ const carriedMs = carriedStart !== undefined ? Math.max(0, Date.now() - carriedStart) : 0;
19676
+ const initialProgressText = carriedStart !== undefined && carriedMs >= this.progressMinElapsedMs()
19677
+ ? FleetManager.progressText(carriedMs, null, this.progressMinElapsedMs(), this.progressPrefixFor(instanceName, carriedMs))
19678
+ : FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
18449
19679
  try {
18450
19680
  const sent = await adapter.notifyAlert(chatId, {
18451
19681
  type: "cancel",
@@ -18468,7 +19698,7 @@ export class FleetManager {
18468
19698
  // Elapsed time is measured from when this button was posted — i.e. from
18469
19699
  // when the work was handed over — not from the pane's working transition,
18470
19700
  // which resets if the CLI blips idle mid-turn.
18471
- startedAt: Date.now(),
19701
+ startedAt: carriedStart ?? Date.now(),
18472
19702
  // Matches the text notifyAlert just posted, so the first 60s tick does
18473
19703
  // not re-edit identical text — which put a "(edited)" mark on Discord
18474
19704
  // with nothing visibly changed.
@@ -18591,14 +19821,16 @@ export class FleetManager {
18591
19821
  * Make a tool summary safe to paste into a channel message.
18592
19822
  *
18593
19823
  * The text is agent-controlled (it is built from tool inputs — file paths,
18594
- * shell commands), so it gets flattened to one line, capped, and stripped of
18595
- * the two Discord mass-mention triggers. Neither channel renders it with a
18596
- * parse mode, so no markup escaping is needed beyond that.
19824
+ * shell commands), so it gets flattened to one line, secrets masked, capped,
19825
+ * and stripped of the two Discord mass-mention triggers. Masking comes BEFORE
19826
+ * the cap: a cap can cut a mask's context but must never be what hides (or
19827
+ * fails to hide) a secret. Neither channel renders it with a parse mode, so no
19828
+ * markup escaping is needed beyond that.
18597
19829
  */
18598
19830
  static sanitizeActivity(activity) {
18599
19831
  if (!activity)
18600
19832
  return null;
18601
- const flat = activity.replace(/\s+/g, " ").replace(/@(everyone|here)/g, "@​$1").trim();
19833
+ const flat = maskSecrets(activity.replace(/\s+/g, " ")).replace(/@(everyone|here)/g, "@​$1").trim();
18602
19834
  if (!flat)
18603
19835
  return null;
18604
19836
  return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
@@ -18717,6 +19949,12 @@ export class FleetManager {
18717
19949
  }
18718
19950
  return PROGRESS_MIN_ELAPSED_MS;
18719
19951
  }
19952
+ /** M-2391: when the instance's live (not retiring) bubble started — the current turn's start — or undefined. */
19953
+ liveButtonStart(instanceName) {
19954
+ const starts = [...this.cancelButtons.values()].filter((e) => e.instanceName === instanceName && !e.retiring && typeof e.startedAt === "number")
19955
+ .map((e) => e.startedAt);
19956
+ return starts.length ? Math.min(...starts) : undefined;
19957
+ }
18720
19958
  startProgressTicker(entry) {
18721
19959
  const tick = () => this.refreshBubble(entry);
18722
19960
  entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
@@ -18726,7 +19964,9 @@ export class FleetManager {
18726
19964
  // one additional edit per turn that lives past the threshold.
18727
19965
  const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
18728
19966
  if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
18729
- const firstTick = setTimeout(tick, firstAt);
19967
+ // M-2391: a little past the threshold — a timer can fire a millisecond before Date.now() reaches it, which
19968
+ // rendered the unchanged "處理中…" text and skipped the edit until the next 60 s interval.
19969
+ const firstTick = setTimeout(tick, firstAt + PROGRESS_THRESHOLD_MARGIN_MS);
18730
19970
  firstTick.unref?.();
18731
19971
  }
18732
19972
  }
@@ -19065,6 +20305,8 @@ export class FleetManager {
19065
20305
  }
19066
20306
  catch { /* safe metadata only */ }
19067
20307
  broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
20308
+ if (event.phase === "done")
20309
+ this.forgetSignInIdentities(); // a sign-in may be a different account now
19068
20310
  this.needsYou?.poke();
19069
20311
  if (!this.browserSignInOwnerCurrent(owner))
19070
20312
  return;
@@ -19103,7 +20345,10 @@ export class FleetManager {
19103
20345
  cancel: () => this.cancelLoginSession(),
19104
20346
  notice: ({ shown, label, handle }) => this.notifyFleetError(t("login.web_dashboard_notice", shown, label, handle.slice(0, 8))),
19105
20347
  // No details in the event: each page reads its own view (GET /ui/backend-login), as its session may see it.
19106
- changed: () => broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")),
20348
+ // The accounts are forgotten when ANY sign-in ends, even one a later one replaced (it may be a different account now), and not at every step of a
20349
+ // running one: `changed` only tells the pages to read again.
20350
+ changed: () => { broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")); },
20351
+ loginEnded: () => this.forgetSignInIdentities(),
19107
20352
  });
19108
20353
  return this.webBackendLoginsInstance;
19109
20354
  }
@@ -19117,6 +20362,135 @@ export class FleetManager {
19117
20362
  * #2233: Settings → Agents' sign-in list — every backend's default login as the availability snapshot has it, then
19118
20363
  * each credential profile an agent uses (its own login state, read offline).
19119
20364
  */
20365
+ /** Which account each sign-in is, as the host's own `claude auth status` said (masked; see checkAuthIdentity). Keys are `backend\0profile` in a Map: a name is data, never a property. */
20366
+ signInIdentities = new Map();
20367
+ /** Reads in flight, by the same key: a second request for a sign-in that is already being read joins it instead of starting another process. */
20368
+ signInIdentityReads = new Map();
20369
+ /** Raised each time the accounts are forgotten: a read that began before is never allowed to write what it found. */
20370
+ signInIdentityGeneration = 0;
20371
+ signInIdentitySlots = 0;
20372
+ signInIdentityWaiters = [];
20373
+ signInIdentity(backend, profile) { return this.signInIdentities.get(`${backend}\0${profile ?? ""}`)?.identity ?? undefined; }
20374
+ /** A sign-in finished (or changed): forget what was read and let no read that was already running put its answer back. */
20375
+ forgetSignInIdentities() { this.signInIdentityGeneration++; this.signInIdentities.clear(); this.signInIdentityReads.clear(); }
20376
+ /** At most three `claude auth status` at once across the whole host, however many requests ask. */
20377
+ async withSignInIdentitySlot(task) {
20378
+ while (this.signInIdentitySlots >= 3)
20379
+ await new Promise(resolve => this.signInIdentityWaiters.push(resolve));
20380
+ this.signInIdentitySlots++;
20381
+ try {
20382
+ return await task();
20383
+ }
20384
+ finally {
20385
+ this.signInIdentitySlots--;
20386
+ this.signInIdentityWaiters.shift()?.();
20387
+ }
20388
+ }
20389
+ /** One `claude auth status` for a sign-in (the default one, or a credential profile's folder). */
20390
+ async readSignInIdentity(target) {
20391
+ let env;
20392
+ if (target.profile)
20393
+ env = credentialProfileEnv(this.dataDir, target.backend, target.profile) ?? undefined;
20394
+ return checkAuthIdentity(target.backend, env);
20395
+ }
20396
+ /**
20397
+ * Start reading, in the background, the account of each Claude Code sign-in that is signed in (the default and every profile an agent uses) and
20398
+ * not read in the last five minutes. It never waits and never blocks a request: the list shows what is already known, and the pages are told to
20399
+ * read it again when something new was learned. A sign-in already being read is not read twice, and a read that began before the accounts were
20400
+ * forgotten (a sign-in finished meanwhile) is dropped. No login, no network request of ours, nothing logged.
20401
+ */
20402
+ refreshSignInIdentities() {
20403
+ const now = Date.now(), fresh = (target) => { const entry = this.signInIdentities.get(`${target.backend}\0${target.profile ?? ""}`); return !!entry && now - entry.at < 5 * 60_000; };
20404
+ const started = [];
20405
+ for (const target of this.backendSignInTargets()) {
20406
+ const key = `${target.backend}\0${target.profile ?? ""}`;
20407
+ if (target.state !== "usable" || !authIdentitySupported(target.backend) || fresh(target) || this.signInIdentityReads.has(key))
20408
+ continue;
20409
+ const generation = this.signInIdentityGeneration;
20410
+ const read = this.withSignInIdentitySlot(() => this.readSignInIdentity(target)).then(identity => {
20411
+ if (generation !== this.signInIdentityGeneration)
20412
+ return false; // forgotten while it ran: its answer may be the old account
20413
+ this.signInIdentities.set(key, { identity: identity ?? null, at: Date.now() });
20414
+ return true;
20415
+ }).catch(() => {
20416
+ // A read that threw is an answer too: remembered as "no account" for the same five minutes, so every GET does not try again.
20417
+ if (generation === this.signInIdentityGeneration)
20418
+ this.signInIdentities.set(key, { identity: null, at: Date.now() });
20419
+ return false;
20420
+ }).finally(() => { if (this.signInIdentityReads.get(key) === read)
20421
+ this.signInIdentityReads.delete(key); });
20422
+ this.signInIdentityReads.set(key, read);
20423
+ started.push(read);
20424
+ }
20425
+ if (started.length)
20426
+ void Promise.all(started).then(results => {
20427
+ if (results.some(Boolean))
20428
+ broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")); // read the list again: it knows more now
20429
+ });
20430
+ }
20431
+ /**
20432
+ * #3212 PR 1: the Accounts view (accounts-view.ts) — every CLI's default sign-in and named profiles (used or only on
20433
+ * disk), who uses each and its weekly usage. Host rows only; a paired computer's rows come with #3190 Part 3c. Its
20434
+ * usage is the usage panel's cached read (never forced), given at most `usageWaitMs` before the view is sent without it.
20435
+ */
20436
+ async accountsView(usageWaitMs = 1500) {
20437
+ const fleet = this.fleetConfig;
20438
+ const defaultBackend = fleet?.defaults?.backend ?? "claude-code";
20439
+ this.refreshSignInIdentities();
20440
+ const targets = this.backendSignInTargets();
20441
+ const diskProfiles = new Map();
20442
+ for (const backend of PROFILE_CLIS) {
20443
+ const rows = [];
20444
+ for (const profile of profilesOnDisk(this.dataDir, backend)) {
20445
+ let login = "unknown";
20446
+ try {
20447
+ login = credentialProfileLogin(this.dataDir, backend, profile).state;
20448
+ }
20449
+ catch { /* unknown */ }
20450
+ const signIn = dashboardSignInSupported(backend, profile);
20451
+ const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
20452
+ const identity = this.signInIdentity(backend, profile);
20453
+ rows.push({ profile, state: (login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown"), signIn,
20454
+ ...(hostCommand ? { hostCommand } : {}), ...(identity ? { identity } : {}) });
20455
+ }
20456
+ diskProfiles.set(backend, rows);
20457
+ }
20458
+ // Host agents only: an agent on a paired Windows computer runs on that computer's accounts (its own scope, later).
20459
+ const agents = Object.keys(fleet?.instances ?? {}).filter(name => !isWindowsInstance(fleet.instances[name])).sort()
20460
+ .map(name => { const backend = this.backendNameForInstance(name); return { name, backend, profile: this.credentialProfileOf(name, backend) }; });
20461
+ const classic = readClassicBindings(this.dataDir).map(b => {
20462
+ const backend = b.backend ?? defaultBackend;
20463
+ return { key: b.key, backend, profile: profileName(classicProfile(b.backend_options, fleet?.defaults?.backend_options, backend)) };
20464
+ });
20465
+ const fleetDefaults = new Map();
20466
+ for (const backend of PROFILE_CLIS) {
20467
+ const p = profileName(profileOf(fleet?.defaults?.backend_options, backend));
20468
+ if (p)
20469
+ fleetDefaults.set(backend, p);
20470
+ }
20471
+ // Weekly usage, from the usage panel's own (cached, never forced) read.
20472
+ const usage = new Map();
20473
+ const ids = new Map(); // provider id → backend\0profile
20474
+ for (const backend of ACCOUNT_CLIS) {
20475
+ const pid = usageProviderIdForBackend(backend);
20476
+ if (!pid)
20477
+ continue;
20478
+ ids.set(pid, `${backend}\0`);
20479
+ for (const p of new Set([...(diskProfiles.get(backend) ?? []).map(d => d.profile), ...targets.filter(t => t.backend === backend && t.profile).map(t => t.profile)]))
20480
+ ids.set(`${pid}:${p}`, `${backend}\0${p}`);
20481
+ }
20482
+ try {
20483
+ const payload = await Promise.race([getUsageSnapshot(false, undefined, { allowStale: true }), new Promise(r => { const tm = setTimeout(() => r(null), usageWaitMs); tm.unref?.(); })]);
20484
+ for (const provider of payload?.providers ?? []) {
20485
+ const at = ids.get(provider.id);
20486
+ const w = at ? weeklyUsage(provider) : undefined;
20487
+ if (at && w)
20488
+ usage.set(at, w);
20489
+ }
20490
+ }
20491
+ catch { /* the view without usage */ }
20492
+ return buildAccountsView({ targets, diskProfiles, agents, classic, fleetDefaults, usage });
20493
+ }
19120
20494
  backendSignInTargets() {
19121
20495
  const out = [];
19122
20496
  for (const row of this.backendAvailabilityRows()) {
@@ -19128,6 +20502,7 @@ export class FleetManager {
19128
20502
  ...(flow?.remoteLogin === "unsupported" ? { hostCommand: flow.command } : {}),
19129
20503
  ...(row.state === "not_installed" && install ? { install } : {}),
19130
20504
  ...(row.state === "too_old" && install ? { update: install } : {}),
20505
+ ...(this.signInIdentity(row.backend) ? { identity: this.signInIdentity(row.backend) } : {}),
19131
20506
  });
19132
20507
  }
19133
20508
  const seen = new Set();
@@ -19145,7 +20520,7 @@ export class FleetManager {
19145
20520
  const signIn = dashboardSignInSupported(backend, profile);
19146
20521
  const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
19147
20522
  out.push({ backend, profile, state: login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown", signIn,
19148
- ...(hostCommand ? { hostCommand } : {}) });
20523
+ ...(hostCommand ? { hostCommand } : {}), ...(this.signInIdentity(backend, profile) ? { identity: this.signInIdentity(backend, profile) } : {}) });
19149
20524
  }
19150
20525
  return out;
19151
20526
  }
@@ -19664,9 +21039,12 @@ export class FleetManager {
19664
21039
  this.loginWindow.release(claim);
19665
21040
  return t("login.web_shutting_down");
19666
21041
  }
21042
+ // The wrapper says its own exit code after this marker: tmux may never record it (unattended-install.ts).
21043
+ const exitMarker = installExitMarker();
19667
21044
  const flow = {
19668
21045
  backend,
19669
- command: unattendedInstallCommand(info.unattended ?? info.install, shell),
21046
+ command: unattendedInstallCommand(info.unattended ?? info.install, shell, exitMarker),
21047
+ exitMarker,
19670
21048
  successPattern: /$^/,
19671
21049
  timeoutMs: this.installTimeoutMs,
19672
21050
  };
@@ -19967,6 +21345,8 @@ export class FleetManager {
19967
21345
  this.needsYou?.poke();
19968
21346
  const onError = (err) => this.logger.debug({ err }, "SSE client write failed; evicting");
19969
21347
  if (event === "message" && data && typeof data === "object") {
21348
+ // #2861: an Office share's dialogue box hears that someone talked to an instance (never what was said).
21349
+ this.officeTalk.noteMessage(data);
19970
21350
  // A chat message: record it, and send it WITH its id so a reconnecting stream can ask for what it missed.
19971
21351
  const m = data;
19972
21352
  const recorded = this.webChatHistory.record({
@@ -20011,9 +21391,12 @@ export class FleetManager {
20011
21391
  this.taskBoardWaker?.stop();
20012
21392
  // #2303: each instance's cards at once, read at every claim (a fleet.yaml edit applies to the next one).
20013
21393
  db.board.setCardLimit((name) => boardCardLimit(this.fleetConfig, name));
21394
+ // An instance with the task board switched off is handed no cards and sent no notices (read at each write and wake).
21395
+ db.board.setBoardOff((name) => taskBoardOffIn(this.fleetConfig, name));
20014
21396
  const waker = this.taskBoardWaker = new TaskBoardWaker({
20015
21397
  deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
20016
21398
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
21399
+ boardOff: (name) => taskBoardOffIn(this.fleetConfig, name),
20017
21400
  // #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
20018
21401
  messagedAbout: (from, to, codes, sinceMs) => {
20019
21402
  const outbox = this.deliveryOutbox;
@@ -23077,6 +24460,9 @@ export class FleetManager {
23077
24460
  * settles, so a later genuine stop (after a restart) still does the work.
23078
24461
  */
23079
24462
  stopAll() {
24463
+ // S-51: no new Hub admission, dispatch or listener work from this tick on; doStopAll awaits the close.
24464
+ this.hubEffectsReady = false;
24465
+ this.hubRuntime?.fence();
23080
24466
  this.lifecycle.cancelNativeRecovery?.();
23081
24467
  this.nativeMcpStore?.closeAll();
23082
24468
  this.runtimeExecutor?.close();
@@ -23149,6 +24535,22 @@ export class FleetManager {
23149
24535
  // because we asked it to. Set synchronously — doStopAll runs to its first
23150
24536
  // await in the same tick as the signal handler, so no event can slip in.
23151
24537
  this.shuttingDown = true;
24538
+ // Stop receive admission before the first await or any instance shutdown; outbound
24539
+ // adapters remain available below to retire notices and pending prompts.
24540
+ for (const state of this.adapterState.values()) {
24541
+ if (state.retryTimer) {
24542
+ clearTimeout(state.retryTimer);
24543
+ state.retryTimer = undefined;
24544
+ }
24545
+ }
24546
+ const inboundStopped = Promise.all([...this.adapters.values()].map(async (adapter) => {
24547
+ try {
24548
+ await adapter.stopReceiving?.();
24549
+ }
24550
+ catch (err) {
24551
+ this.logger.warn({ adapterId: adapter.id, err }, "Adapter receive stop failed; inbound remains fenced");
24552
+ }
24553
+ }));
23152
24554
  this.workingRecovery?.reset();
23153
24555
  if (this.workingRecoveryTimer)
23154
24556
  clearInterval(this.workingRecoveryTimer);
@@ -23179,6 +24581,17 @@ export class FleetManager {
23179
24581
  // #1565: the web chat's last changes reach the disk (bounded: a slow disk does not hold the shutdown).
23180
24582
  const webChatStopped = this.webChatStore ? Promise.race([this.webChatStore.flush().catch(() => { }), new Promise(r => setTimeout(r, 3_000).unref?.())]) : Promise.resolve();
23181
24583
  const profileStopped = this.runtimeCpuProfiler?.shutdown("fleet shutdown");
24584
+ // S-51: the Hub closes in the same drain, bounded so an unanswering listener does not hold the shutdown.
24585
+ const hubInit = this.hubRuntimeInit, hubNow = this.hubRuntime;
24586
+ this.hubRuntime = null;
24587
+ this.hubRuntimeInit = null;
24588
+ const hubClose = async () => {
24589
+ const runtime = hubNow ?? (hubInit ? await hubInit.catch(() => null) : null);
24590
+ await runtime?.close();
24591
+ };
24592
+ const hubStopped = hubNow || hubInit
24593
+ ? Promise.race([hubClose().catch(err => this.logger.warn({ err: err?.message }, "Hub did not close cleanly")), new Promise(r => setTimeout(r, 10_000).unref?.())])
24594
+ : Promise.resolve();
23182
24595
  this.ipcStoppingInstances.add("__fleet_stopping__");
23183
24596
  // Release held delivery promises before awaiting daemon shutdown, then
23184
24597
  // reject spawn work which has not started. Otherwise a storm backoff could
@@ -23209,9 +24622,11 @@ export class FleetManager {
23209
24622
  // A login/install window is a dedicated tmux server with its own TTL
23210
24623
  // timer and HTTP listener living in THIS process: without an explicit
23211
24624
  // shutdown it would outlive us as an owner-less login CLI (sol B3).
24625
+ await inboundStopped;
23212
24626
  await publicStopped;
23213
24627
  await sharesStopped?.catch(() => { });
23214
24628
  await profileStopped;
24629
+ await hubStopped;
23215
24630
  await needsStopped;
23216
24631
  await settingsControlStopped;
23217
24632
  await cacheStopped;
@@ -23219,13 +24634,6 @@ export class FleetManager {
23219
24634
  await this.cpuProfileControl?.close();
23220
24635
  this.cpuProfileControl = null;
23221
24636
  await this.shutdownLoginWindows();
23222
- // Cancel adapter retry timers
23223
- for (const state of this.adapterState.values()) {
23224
- if (state.retryTimer) {
23225
- clearTimeout(state.retryTimer);
23226
- state.retryTimer = undefined;
23227
- }
23228
- }
23229
24637
  this.clearStatuslineWatchers();
23230
24638
  this.costGuard?.stop();
23231
24639
  this.dailySummary?.stop();
@@ -23263,6 +24671,15 @@ export class FleetManager {
23263
24671
  clearInterval(this.discordPresenceTimer);
23264
24672
  this.discordPresenceTimer = null;
23265
24673
  }
24674
+ if (this.quotaReadTimer) {
24675
+ clearInterval(this.quotaReadTimer);
24676
+ this.quotaReadTimer = null;
24677
+ }
24678
+ if (this.capacityTimer) {
24679
+ clearInterval(this.capacityTimer);
24680
+ this.capacityTimer = null;
24681
+ }
24682
+ setCapacityLookup(null);
23266
24683
  if (this.discordPresenceEagerTimer) {
23267
24684
  clearTimeout(this.discordPresenceEagerTimer);
23268
24685
  this.discordPresenceEagerTimer = null;
@@ -23549,6 +24966,7 @@ export class FleetManager {
23549
24966
  this.loadedFleetSource = previousSource;
23550
24967
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
23551
24968
  this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
24969
+ this.observeHubSwitches(); // S-51: and the Hub's
23552
24970
  this.logger.error({
23553
24971
  oldCount,
23554
24972
  newCount,
@@ -23568,6 +24986,8 @@ export class FleetManager {
23568
24986
  // mark every row done and tell the user "changes applied".
23569
24987
  return { rejected: why };
23570
24988
  }
24989
+ this.nativeMcpStore?.observePolicies();
24990
+ this.agentCredentialsCache?.observePolicies();
23571
24991
  if (this.loadedFleetSource)
23572
24992
  this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
23573
24993
  this.migrateOfficeStorage();
@@ -24468,6 +25888,9 @@ export class FleetManager {
24468
25888
  }
24469
25889
  async runConnectionBindingApply(job, binding, execution) {
24470
25890
  try {
25891
+ // #3055 review r2: a connection is not re-pointed while one of its topics is being deleted (nothing is changed).
25892
+ if (this.topicDeletionPending())
25893
+ throw new Error("a topic deletion is running; try again in a moment");
24471
25894
  await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
24472
25895
  job.result = "applied";
24473
25896
  }
@@ -25638,8 +27061,13 @@ export class FleetManager {
25638
27061
  return;
25639
27062
  }
25640
27063
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
27064
+ const publicUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
25641
27065
  if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
25642
- && this.runtimeCoordinator().handlePublicRequest(req, res, new URL(req.url ?? "/", `http://localhost:${port}`)))
27066
+ && this.runtimeCoordinator().handlePublicRequest(req, res, publicUrl))
27067
+ return;
27068
+ // The read-only installer files (/runtime/install.ps1 and the bundle ZIP) grant nothing. They answer 404 while remote runtimes
27069
+ // are off or on a public gateway link, and touch no other path.
27070
+ if (this.runtimeInstaller().handle(req, res, publicUrl))
25643
27071
  return;
25644
27072
  // Branding is public, after Host admission: automatic favicon requests
25645
27073
  // must not become noisy web-token 401s in the browser console.
@@ -25668,7 +27096,7 @@ export class FleetManager {
25668
27096
  && String(req.headers.accept ?? "").includes("text/html")
25669
27097
  && isWebPageNavigation(requestPath)) {
25670
27098
  const link = gatewayRequestContext(req);
25671
- serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId) });
27099
+ serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId), deviceLogin: !link && this.deviceLoginAvailable(), loginQr: this.loginQrEnabled() });
25672
27100
  return;
25673
27101
  }
25674
27102
  res.writeHead(decision.status);
@@ -25840,6 +27268,7 @@ export class FleetManager {
25840
27268
  res.end(JSON.stringify({
25841
27269
  ...sysInfo,
25842
27270
  version: this.currentVersion,
27271
+ build: gatewayRequestContext(req) ? undefined : this.currentBuild,
25843
27272
  instances: enriched,
25844
27273
  publicLink: this.getPublicWebStatus(),
25845
27274
  }));
@@ -26337,6 +27766,8 @@ export class FleetManager {
26337
27766
  })(),
26338
27767
  capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
26339
27768
  commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
27769
+ // cc3ce4fe: the version this machine's inventory reports for the agent's CLI, and whether AgEnD has audited it.
27770
+ ...this.nativeCliStatus(nativeConfig.backend, nativeConfig.machine_id, this.daemons.get(name)),
26340
27771
  ...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
26341
27772
  machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
26342
27773
  display_name: identity.display_name || undefined,