@songsid/agend 2.3.5-alpha.4 → 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 (838) 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-change.d.ts +3 -1
  15. package/dist/agent-runtime-change.js +15 -3
  16. package/dist/agent-runtime-change.js.map +1 -1
  17. package/dist/agent-workspace.d.ts +93 -0
  18. package/dist/agent-workspace.js +353 -0
  19. package/dist/agent-workspace.js.map +1 -0
  20. package/dist/auth-api.d.ts +27 -0
  21. package/dist/auth-api.js +164 -3
  22. package/dist/auth-api.js.map +1 -1
  23. package/dist/authority/session-authorizations.d.ts +28 -4
  24. package/dist/authority/session-authorizations.js +63 -17
  25. package/dist/authority/session-authorizations.js.map +1 -1
  26. package/dist/backend/antigravity.js +3 -1
  27. package/dist/backend/antigravity.js.map +1 -1
  28. package/dist/backend/codex-footer-hints.d.ts +3 -0
  29. package/dist/backend/codex-footer-hints.js +14 -0
  30. package/dist/backend/codex-footer-hints.js.map +1 -1
  31. package/dist/backend/codex-metadata.d.ts +14 -0
  32. package/dist/backend/codex-metadata.js +45 -0
  33. package/dist/backend/codex-metadata.js.map +1 -1
  34. package/dist/backend/codex-quota-history.d.ts +7 -0
  35. package/dist/backend/codex-quota-history.js +57 -0
  36. package/dist/backend/codex-quota-history.js.map +1 -0
  37. package/dist/backend/codex-session-lookup.d.ts +6 -0
  38. package/dist/backend/codex-session-lookup.js +1 -1
  39. package/dist/backend/codex-session-lookup.js.map +1 -1
  40. package/dist/backend/codex-speed.d.ts +14 -0
  41. package/dist/backend/codex-speed.js +35 -0
  42. package/dist/backend/codex-speed.js.map +1 -0
  43. package/dist/backend/codex.d.ts +7 -0
  44. package/dist/backend/codex.js +78 -15
  45. package/dist/backend/codex.js.map +1 -1
  46. package/dist/backend/grok-weekly-reset.d.ts +24 -0
  47. package/dist/backend/grok-weekly-reset.js +95 -0
  48. package/dist/backend/grok-weekly-reset.js.map +1 -0
  49. package/dist/backend/grok.d.ts +30 -1
  50. package/dist/backend/grok.js +301 -54
  51. package/dist/backend/grok.js.map +1 -1
  52. package/dist/backend/kiro.d.ts +3 -0
  53. package/dist/backend/kiro.js +50 -2
  54. package/dist/backend/kiro.js.map +1 -1
  55. package/dist/backend/muse.js +5 -3
  56. package/dist/backend/muse.js.map +1 -1
  57. package/dist/backend/native-pane.d.ts +3 -1
  58. package/dist/backend/native-pane.js +2 -2
  59. package/dist/backend/native-pane.js.map +1 -1
  60. package/dist/backend/types.d.ts +13 -1
  61. package/dist/backend/types.js +3 -0
  62. package/dist/backend/types.js.map +1 -1
  63. package/dist/build-provenance-cli.d.ts +3 -0
  64. package/dist/build-provenance-cli.js +11 -0
  65. package/dist/build-provenance-cli.js.map +1 -0
  66. package/dist/build-provenance.d.ts +20 -0
  67. package/dist/build-provenance.js +67 -0
  68. package/dist/build-provenance.js.map +1 -0
  69. package/dist/build-provenance.json +1 -0
  70. package/dist/cache-ledger.d.ts +95 -0
  71. package/dist/cache-ledger.js +276 -4
  72. package/dist/cache-ledger.js.map +1 -1
  73. package/dist/cache-service.d.ts +5 -0
  74. package/dist/cache-service.js +16 -4
  75. package/dist/cache-service.js.map +1 -1
  76. package/dist/channel/adapters/discord.js +11 -3
  77. package/dist/channel/adapters/discord.js.map +1 -1
  78. package/dist/channel/adapters/telegram.d.ts +51 -0
  79. package/dist/channel/adapters/telegram.js +235 -17
  80. package/dist/channel/adapters/telegram.js.map +1 -1
  81. package/dist/channel/mcp-tools.js +3 -3
  82. package/dist/channel/mcp-tools.js.map +1 -1
  83. package/dist/channel/types.d.ts +2 -0
  84. package/dist/claude-auth-status.d.ts +48 -0
  85. package/dist/claude-auth-status.js +149 -0
  86. package/dist/claude-auth-status.js.map +1 -0
  87. package/dist/cli.js +138 -37
  88. package/dist/cli.js.map +1 -1
  89. package/dist/config-validator.js +21 -1
  90. package/dist/config-validator.js.map +1 -1
  91. package/dist/cross-instance-notice.d.ts +10 -0
  92. package/dist/cross-instance-notice.js +67 -5
  93. package/dist/cross-instance-notice.js.map +1 -1
  94. package/dist/daemon.d.ts +70 -1
  95. package/dist/daemon.js +547 -25
  96. package/dist/daemon.js.map +1 -1
  97. package/dist/dashboard-permissions.d.ts +8 -0
  98. package/dist/dashboard-permissions.js +16 -0
  99. package/dist/dashboard-permissions.js.map +1 -1
  100. package/dist/delivery-consumed-watch.d.ts +23 -1
  101. package/dist/delivery-consumed-watch.js +30 -3
  102. package/dist/delivery-consumed-watch.js.map +1 -1
  103. package/dist/delivery-reconciliation.d.ts +1 -1
  104. package/dist/delivery-reconciliation.js +1 -1
  105. package/dist/delivery-reconciliation.js.map +1 -1
  106. package/dist/doctor.js +20 -9
  107. package/dist/doctor.js.map +1 -1
  108. package/dist/export-import.d.ts +7 -0
  109. package/dist/export-import.js +44 -10
  110. package/dist/export-import.js.map +1 -1
  111. package/dist/fleet-context.d.ts +2 -0
  112. package/dist/fleet-manager.d.ts +359 -7
  113. package/dist/fleet-manager.js +1365 -90
  114. package/dist/fleet-manager.js.map +1 -1
  115. package/dist/fleet-windows-runtime.d.ts +2 -1
  116. package/dist/fleet-windows-runtime.js +25 -7
  117. package/dist/fleet-windows-runtime.js.map +1 -1
  118. package/dist/general-knowledge/skills/concepts/SKILL.md +425 -0
  119. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  120. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  121. package/dist/general-knowledge/skills/fleet-health/SKILL.md +2 -2
  122. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  123. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  124. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
  125. package/dist/general-knowledge/skills/tui-effort/SKILL.md +10 -10
  126. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +31 -15
  127. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  128. package/dist/general-role.d.ts +1 -1
  129. package/dist/general-role.js +1 -0
  130. package/dist/general-role.js.map +1 -1
  131. package/dist/hub/admission.d.ts +28 -0
  132. package/dist/hub/admission.js +62 -0
  133. package/dist/hub/admission.js.map +1 -0
  134. package/dist/hub/attachments/limits.d.ts +20 -0
  135. package/dist/hub/attachments/limits.js +48 -0
  136. package/dist/hub/attachments/limits.js.map +1 -0
  137. package/dist/hub/attachments/store.d.ts +116 -0
  138. package/dist/hub/attachments/store.js +246 -0
  139. package/dist/hub/attachments/store.js.map +1 -0
  140. package/dist/hub/audit/index.d.ts +62 -0
  141. package/dist/hub/audit/index.js +344 -0
  142. package/dist/hub/audit/index.js.map +1 -0
  143. package/dist/hub/browser-certificate.d.ts +38 -0
  144. package/dist/hub/browser-certificate.js +247 -0
  145. package/dist/hub/browser-certificate.js.map +1 -0
  146. package/dist/hub/buttons-model.d.ts +9 -0
  147. package/dist/hub/buttons-model.js +42 -0
  148. package/dist/hub/buttons-model.js.map +1 -0
  149. package/dist/hub/buttons.d.ts +54 -0
  150. package/dist/hub/buttons.js +346 -0
  151. package/dist/hub/buttons.js.map +1 -0
  152. package/dist/hub/catalog.d.ts +74 -0
  153. package/dist/hub/catalog.js +195 -0
  154. package/dist/hub/catalog.js.map +1 -0
  155. package/dist/hub/client.d.ts +69 -0
  156. package/dist/hub/client.js +300 -0
  157. package/dist/hub/client.js.map +1 -0
  158. package/dist/hub/compose/member.d.ts +139 -0
  159. package/dist/hub/compose/member.js +450 -0
  160. package/dist/hub/compose/member.js.map +1 -0
  161. package/dist/hub/compose/server.d.ts +77 -0
  162. package/dist/hub/compose/server.js +349 -0
  163. package/dist/hub/compose/server.js.map +1 -0
  164. package/dist/hub/compose/views.d.ts +12 -0
  165. package/dist/hub/compose/views.js +4 -0
  166. package/dist/hub/compose/views.js.map +1 -0
  167. package/dist/hub/db.d.ts +56 -0
  168. package/dist/hub/db.js +188 -0
  169. package/dist/hub/db.js.map +1 -0
  170. package/dist/hub/directory/config.d.ts +39 -0
  171. package/dist/hub/directory/config.js +108 -0
  172. package/dist/hub/directory/config.js.map +1 -0
  173. package/dist/hub/directory/directory.d.ts +18 -0
  174. package/dist/hub/directory/directory.js +320 -0
  175. package/dist/hub/directory/directory.js.map +1 -0
  176. package/dist/hub/directory/index.d.ts +7 -0
  177. package/dist/hub/directory/index.js +4 -0
  178. package/dist/hub/directory/index.js.map +1 -0
  179. package/dist/hub/directory/profiles.d.ts +41 -0
  180. package/dist/hub/directory/profiles.js +90 -0
  181. package/dist/hub/directory/profiles.js.map +1 -0
  182. package/dist/hub/directory/types.d.ts +71 -0
  183. package/dist/hub/directory/types.js +14 -0
  184. package/dist/hub/directory/types.js.map +1 -0
  185. package/dist/hub/execution/cli-provider.d.ts +23 -0
  186. package/dist/hub/execution/cli-provider.js +341 -0
  187. package/dist/hub/execution/cli-provider.js.map +1 -0
  188. package/dist/hub/execution/index.d.ts +13 -0
  189. package/dist/hub/execution/index.js +8 -0
  190. package/dist/hub/execution/index.js.map +1 -0
  191. package/dist/hub/execution/model-broker.d.ts +24 -0
  192. package/dist/hub/execution/model-broker.js +208 -0
  193. package/dist/hub/execution/model-broker.js.map +1 -0
  194. package/dist/hub/execution/provider-setup.d.ts +11 -0
  195. package/dist/hub/execution/provider-setup.js +21 -0
  196. package/dist/hub/execution/provider-setup.js.map +1 -0
  197. package/dist/hub/execution/sandbox-runner.d.ts +1 -0
  198. package/dist/hub/execution/sandbox-runner.js +221 -0
  199. package/dist/hub/execution/sandbox-runner.js.map +1 -0
  200. package/dist/hub/execution/service.d.ts +136 -0
  201. package/dist/hub/execution/service.js +459 -0
  202. package/dist/hub/execution/service.js.map +1 -0
  203. package/dist/hub/execution/store.d.ts +61 -0
  204. package/dist/hub/execution/store.js +182 -0
  205. package/dist/hub/execution/store.js.map +1 -0
  206. package/dist/hub/execution/views.d.ts +20 -0
  207. package/dist/hub/execution/views.js +48 -0
  208. package/dist/hub/execution/views.js.map +1 -0
  209. package/dist/hub/http/compose.d.ts +20 -0
  210. package/dist/hub/http/compose.js +518 -0
  211. package/dist/hub/http/compose.js.map +1 -0
  212. package/dist/hub/http/core.d.ts +49 -0
  213. package/dist/hub/http/core.js +124 -0
  214. package/dist/hub/http/core.js.map +1 -0
  215. package/dist/hub/http/deps.d.ts +61 -0
  216. package/dist/hub/http/deps.js +2 -0
  217. package/dist/hub/http/deps.js.map +1 -0
  218. package/dist/hub/http/schema.d.ts +14 -0
  219. package/dist/hub/http/schema.js +80 -0
  220. package/dist/hub/http/schema.js.map +1 -0
  221. package/dist/hub/identity.d.ts +32 -0
  222. package/dist/hub/identity.js +80 -0
  223. package/dist/hub/identity.js.map +1 -0
  224. package/dist/hub/init/browser-certificate.d.ts +9 -0
  225. package/dist/hub/init/browser-certificate.js +78 -0
  226. package/dist/hub/init/browser-certificate.js.map +1 -0
  227. package/dist/hub/init/browser-tls.d.ts +18 -0
  228. package/dist/hub/init/browser-tls.js +62 -0
  229. package/dist/hub/init/browser-tls.js.map +1 -0
  230. package/dist/hub/init/cli.d.ts +3 -0
  231. package/dist/hub/init/cli.js +83 -0
  232. package/dist/hub/init/cli.js.map +1 -0
  233. package/dist/hub/init/directory-certificate.d.ts +24 -0
  234. package/dist/hub/init/directory-certificate.js +222 -0
  235. package/dist/hub/init/directory-certificate.js.map +1 -0
  236. package/dist/hub/init/existing-fleet.d.ts +6 -0
  237. package/dist/hub/init/existing-fleet.js +58 -0
  238. package/dist/hub/init/existing-fleet.js.map +1 -0
  239. package/dist/hub/init/handoff.d.ts +8 -0
  240. package/dist/hub/init/handoff.js +36 -0
  241. package/dist/hub/init/handoff.js.map +1 -0
  242. package/dist/hub/init/identity.d.ts +5 -0
  243. package/dist/hub/init/identity.js +24 -0
  244. package/dist/hub/init/identity.js.map +1 -0
  245. package/dist/hub/init/initializer.d.ts +24 -0
  246. package/dist/hub/init/initializer.js +385 -0
  247. package/dist/hub/init/initializer.js.map +1 -0
  248. package/dist/hub/init/inventory.d.ts +17 -0
  249. package/dist/hub/init/inventory.js +33 -0
  250. package/dist/hub/init/inventory.js.map +1 -0
  251. package/dist/hub/init/layout.d.ts +32 -0
  252. package/dist/hub/init/layout.js +53 -0
  253. package/dist/hub/init/layout.js.map +1 -0
  254. package/dist/hub/init/network.d.ts +18 -0
  255. package/dist/hub/init/network.js +104 -0
  256. package/dist/hub/init/network.js.map +1 -0
  257. package/dist/hub/init/plan-digest.d.ts +5 -0
  258. package/dist/hub/init/plan-digest.js +47 -0
  259. package/dist/hub/init/plan-digest.js.map +1 -0
  260. package/dist/hub/init/prompt.d.ts +26 -0
  261. package/dist/hub/init/prompt.js +110 -0
  262. package/dist/hub/init/prompt.js.map +1 -0
  263. package/dist/hub/init/secret-file.d.ts +1 -0
  264. package/dist/hub/init/secret-file.js +59 -0
  265. package/dist/hub/init/secret-file.js.map +1 -0
  266. package/dist/hub/init/text.d.ts +127 -0
  267. package/dist/hub/init/text.js +247 -0
  268. package/dist/hub/init/text.js.map +1 -0
  269. package/dist/hub/init/types.d.ts +114 -0
  270. package/dist/hub/init/types.js +9 -0
  271. package/dist/hub/init/types.js.map +1 -0
  272. package/dist/hub/init/wizard.d.ts +34 -0
  273. package/dist/hub/init/wizard.js +554 -0
  274. package/dist/hub/init/wizard.js.map +1 -0
  275. package/dist/hub/initialization.d.ts +23 -0
  276. package/dist/hub/initialization.js +28 -0
  277. package/dist/hub/initialization.js.map +1 -0
  278. package/dist/hub/member/agent-mode.d.ts +6 -0
  279. package/dist/hub/member/agent-mode.js +23 -0
  280. package/dist/hub/member/agent-mode.js.map +1 -0
  281. package/dist/hub/member/attachments.d.ts +29 -0
  282. package/dist/hub/member/attachments.js +107 -0
  283. package/dist/hub/member/attachments.js.map +1 -0
  284. package/dist/hub/member/exposure-api.d.ts +87 -0
  285. package/dist/hub/member/exposure-api.js +64 -0
  286. package/dist/hub/member/exposure-api.js.map +1 -0
  287. package/dist/hub/member/exposures.d.ts +123 -0
  288. package/dist/hub/member/exposures.js +282 -0
  289. package/dist/hub/member/exposures.js.map +1 -0
  290. package/dist/hub/member/pairing-api.d.ts +85 -0
  291. package/dist/hub/member/pairing-api.js +385 -0
  292. package/dist/hub/member/pairing-api.js.map +1 -0
  293. package/dist/hub/member/quota.d.ts +20 -0
  294. package/dist/hub/member/quota.js +98 -0
  295. package/dist/hub/member/quota.js.map +1 -0
  296. package/dist/hub/member-journal.d.ts +104 -0
  297. package/dist/hub/member-journal.js +336 -0
  298. package/dist/hub/member-journal.js.map +1 -0
  299. package/dist/hub/member-reconciliation.d.ts +14 -0
  300. package/dist/hub/member-reconciliation.js +97 -0
  301. package/dist/hub/member-reconciliation.js.map +1 -0
  302. package/dist/hub/mount.d.ts +39 -0
  303. package/dist/hub/mount.js +123 -0
  304. package/dist/hub/mount.js.map +1 -0
  305. package/dist/hub/pairing-client.d.ts +17 -0
  306. package/dist/hub/pairing-client.js +108 -0
  307. package/dist/hub/pairing-client.js.map +1 -0
  308. package/dist/hub/pairing.d.ts +65 -0
  309. package/dist/hub/pairing.js +211 -0
  310. package/dist/hub/pairing.js.map +1 -0
  311. package/dist/hub/policy.d.ts +63 -0
  312. package/dist/hub/policy.js +147 -0
  313. package/dist/hub/policy.js.map +1 -0
  314. package/dist/hub/preview.d.ts +32 -0
  315. package/dist/hub/preview.js +64 -0
  316. package/dist/hub/preview.js.map +1 -0
  317. package/dist/hub/private-files.d.ts +5 -0
  318. package/dist/hub/private-files.js +44 -0
  319. package/dist/hub/private-files.js.map +1 -0
  320. package/dist/hub/profile.d.ts +59 -0
  321. package/dist/hub/profile.js +73 -0
  322. package/dist/hub/profile.js.map +1 -0
  323. package/dist/hub/protocol.d.ts +779 -0
  324. package/dist/hub/protocol.js +93 -0
  325. package/dist/hub/protocol.js.map +1 -0
  326. package/dist/hub/quota.d.ts +24 -0
  327. package/dist/hub/quota.js +112 -0
  328. package/dist/hub/quota.js.map +1 -0
  329. package/dist/hub/representative.d.ts +141 -0
  330. package/dist/hub/representative.js +528 -0
  331. package/dist/hub/representative.js.map +1 -0
  332. package/dist/hub/retention.d.ts +17 -0
  333. package/dist/hub/retention.js +45 -0
  334. package/dist/hub/retention.js.map +1 -0
  335. package/dist/hub/rooms.d.ts +120 -0
  336. package/dist/hub/rooms.js +535 -0
  337. package/dist/hub/rooms.js.map +1 -0
  338. package/dist/hub/runtime.d.ts +106 -0
  339. package/dist/hub/runtime.js +298 -0
  340. package/dist/hub/runtime.js.map +1 -0
  341. package/dist/hub/server.d.ts +56 -0
  342. package/dist/hub/server.js +202 -0
  343. package/dist/hub/server.js.map +1 -0
  344. package/dist/hub/service-start.d.ts +17 -0
  345. package/dist/hub/service-start.js +109 -0
  346. package/dist/hub/service-start.js.map +1 -0
  347. package/dist/hub/sessions.d.ts +53 -0
  348. package/dist/hub/sessions.js +196 -0
  349. package/dist/hub/sessions.js.map +1 -0
  350. package/dist/hub/socket.d.ts +24 -0
  351. package/dist/hub/socket.js +79 -0
  352. package/dist/hub/socket.js.map +1 -0
  353. package/dist/hub/store.d.ts +77 -0
  354. package/dist/hub/store.js +88 -0
  355. package/dist/hub/store.js.map +1 -0
  356. package/dist/hub/tls.d.ts +31 -0
  357. package/dist/hub/tls.js +135 -0
  358. package/dist/hub/tls.js.map +1 -0
  359. package/dist/hub/types.d.ts +401 -0
  360. package/dist/hub/types.js +2 -0
  361. package/dist/hub/types.js.map +1 -0
  362. package/dist/hub/validation.d.ts +33 -0
  363. package/dist/hub/validation.js +136 -0
  364. package/dist/hub/validation.js.map +1 -0
  365. package/dist/hub/version-floor.d.ts +10 -0
  366. package/dist/hub/version-floor.js +45 -0
  367. package/dist/hub/version-floor.js.map +1 -0
  368. package/dist/hub-browser-certificate-policy.d.ts +31 -0
  369. package/dist/hub-browser-certificate-policy.js +25 -0
  370. package/dist/hub-browser-certificate-policy.js.map +1 -0
  371. package/dist/hub-membership-settings.d.ts +6 -0
  372. package/dist/hub-membership-settings.js +21 -0
  373. package/dist/hub-membership-settings.js.map +1 -0
  374. package/dist/hub-mode.d.ts +15 -0
  375. package/dist/hub-mode.js +17 -0
  376. package/dist/hub-mode.js.map +1 -0
  377. package/dist/hub-pairing-summary.d.ts +48 -0
  378. package/dist/hub-pairing-summary.js +36 -0
  379. package/dist/hub-pairing-summary.js.map +1 -0
  380. package/dist/hub-report-target.d.ts +32 -0
  381. package/dist/hub-report-target.js +33 -0
  382. package/dist/hub-report-target.js.map +1 -0
  383. package/dist/hub-retention-policy.d.ts +14 -0
  384. package/dist/hub-retention-policy.js +15 -0
  385. package/dist/hub-retention-policy.js.map +1 -0
  386. package/dist/instance-config-impact.js +3 -0
  387. package/dist/instance-config-impact.js.map +1 -1
  388. package/dist/instance-lifecycle.d.ts +17 -2
  389. package/dist/instance-lifecycle.js +58 -8
  390. package/dist/instance-lifecycle.js.map +1 -1
  391. package/dist/instance-topic-delete.d.ts +77 -0
  392. package/dist/instance-topic-delete.js +123 -0
  393. package/dist/instance-topic-delete.js.map +1 -0
  394. package/dist/instructions.d.ts +1 -1
  395. package/dist/instructions.js +1 -1
  396. package/dist/instructions.js.map +1 -1
  397. package/dist/kiro-engine-status.d.ts +11 -0
  398. package/dist/kiro-engine-status.js +6 -0
  399. package/dist/kiro-engine-status.js.map +1 -1
  400. package/dist/launchd-service-domain.d.ts +5 -0
  401. package/dist/launchd-service-domain.js +21 -0
  402. package/dist/launchd-service-domain.js.map +1 -0
  403. package/dist/locale.js +125 -3
  404. package/dist/locale.js.map +1 -1
  405. package/dist/log-service.d.ts +28 -0
  406. package/dist/log-service.js +29 -0
  407. package/dist/log-service.js.map +1 -1
  408. package/dist/log-steps.d.ts +4 -3
  409. package/dist/log-steps.js +32 -7
  410. package/dist/log-steps.js.map +1 -1
  411. package/dist/logger.d.ts +16 -2
  412. package/dist/logger.js +346 -66
  413. package/dist/logger.js.map +1 -1
  414. package/dist/login-flows.d.ts +15 -7
  415. package/dist/login-flows.js +29 -3
  416. package/dist/login-flows.js.map +1 -1
  417. package/dist/login-manager.js +25 -4
  418. package/dist/login-manager.js.map +1 -1
  419. package/dist/ls-rows.js +3 -4
  420. package/dist/ls-rows.js.map +1 -1
  421. package/dist/missed-offline.d.ts +25 -0
  422. package/dist/missed-offline.js +79 -0
  423. package/dist/missed-offline.js.map +1 -0
  424. package/dist/native-instance-api.d.ts +0 -1
  425. package/dist/native-instance-api.js +15 -3
  426. package/dist/native-instance-api.js.map +1 -1
  427. package/dist/native-instance-input.d.ts +3 -0
  428. package/dist/native-instance-input.js +6 -1
  429. package/dist/native-instance-input.js.map +1 -1
  430. package/dist/native-instance-registry.d.ts +6 -0
  431. package/dist/native-instance-registry.js +24 -0
  432. package/dist/native-instance-registry.js.map +1 -1
  433. package/dist/native-mcp.d.ts +3 -0
  434. package/dist/native-mcp.js +15 -2
  435. package/dist/native-mcp.js.map +1 -1
  436. package/dist/native-pane-lifecycle.d.ts +8 -0
  437. package/dist/native-pane-lifecycle.js +13 -4
  438. package/dist/native-pane-lifecycle.js.map +1 -1
  439. package/dist/native-pane-manager.js +17 -9
  440. package/dist/native-pane-manager.js.map +1 -1
  441. package/dist/needs-you-hub.d.ts +9 -1
  442. package/dist/needs-you-hub.js +46 -8
  443. package/dist/needs-you-hub.js.map +1 -1
  444. package/dist/needs-you.d.ts +47 -2
  445. package/dist/needs-you.js +31 -3
  446. package/dist/needs-you.js.map +1 -1
  447. package/dist/office-share-page.js +2 -2
  448. package/dist/office-share-page.js.map +1 -1
  449. package/dist/office-share.d.ts +11 -0
  450. package/dist/office-share.js +14 -0
  451. package/dist/office-share.js.map +1 -1
  452. package/dist/office-talk.d.ts +26 -0
  453. package/dist/office-talk.js +50 -0
  454. package/dist/office-talk.js.map +1 -0
  455. package/dist/outbound-handlers.d.ts +14 -0
  456. package/dist/outbound-handlers.js +49 -6
  457. package/dist/outbound-handlers.js.map +1 -1
  458. package/dist/outbound-schemas.d.ts +33 -1
  459. package/dist/outbound-schemas.js +29 -16
  460. package/dist/outbound-schemas.js.map +1 -1
  461. package/dist/pane-quota-history.d.ts +9 -0
  462. package/dist/pane-quota-history.js +67 -0
  463. package/dist/pane-quota-history.js.map +1 -0
  464. package/dist/pause-marker.d.ts +1 -1
  465. package/dist/pause-marker.js +1 -1
  466. package/dist/pause-marker.js.map +1 -1
  467. package/dist/platform-general.d.ts +46 -0
  468. package/dist/platform-general.js +91 -0
  469. package/dist/platform-general.js.map +1 -0
  470. package/dist/public-web-gateway.js +7 -0
  471. package/dist/public-web-gateway.js.map +1 -1
  472. package/dist/queued-handoffs.d.ts +33 -0
  473. package/dist/queued-handoffs.js +75 -0
  474. package/dist/queued-handoffs.js.map +1 -0
  475. package/dist/quickstart.d.ts +13 -0
  476. package/dist/quickstart.js +81 -5
  477. package/dist/quickstart.js.map +1 -1
  478. package/dist/quota-notice.d.ts +7 -0
  479. package/dist/quota-notice.js +40 -0
  480. package/dist/quota-notice.js.map +1 -1
  481. package/dist/reply-buttons-controller.js +10 -6
  482. package/dist/reply-buttons-controller.js.map +1 -1
  483. package/dist/reply-polls.d.ts +2 -2
  484. package/dist/reply-polls.js +3 -3
  485. package/dist/reply-polls.js.map +1 -1
  486. package/dist/runtime-api.d.ts +2 -0
  487. package/dist/runtime-api.js +25 -7
  488. package/dist/runtime-api.js.map +1 -1
  489. package/dist/runtime-build-info.js +5 -3
  490. package/dist/runtime-build-info.js.map +1 -1
  491. package/dist/runtime-bundle.d.ts +3 -0
  492. package/dist/runtime-bundle.js +3 -1
  493. package/dist/runtime-bundle.js.map +1 -1
  494. package/dist/runtime-cli.d.ts +8 -1
  495. package/dist/runtime-cli.js +127 -8
  496. package/dist/runtime-cli.js.map +1 -1
  497. package/dist/runtime-client-store.d.ts +7 -0
  498. package/dist/runtime-client-store.js +30 -2
  499. package/dist/runtime-client-store.js.map +1 -1
  500. package/dist/runtime-client.d.ts +63 -1
  501. package/dist/runtime-client.js +295 -21
  502. package/dist/runtime-client.js.map +1 -1
  503. package/dist/runtime-coordinator.d.ts +7 -0
  504. package/dist/runtime-coordinator.js +213 -35
  505. package/dist/runtime-coordinator.js.map +1 -1
  506. package/dist/runtime-credential-profiles.d.ts +78 -0
  507. package/dist/runtime-credential-profiles.js +280 -0
  508. package/dist/runtime-credential-profiles.js.map +1 -0
  509. package/dist/runtime-install.d.ts +50 -0
  510. package/dist/runtime-install.js +186 -0
  511. package/dist/runtime-install.js.map +1 -0
  512. package/dist/runtime-pane-assets.d.ts +1 -1
  513. package/dist/runtime-pane-assets.js +1 -1
  514. package/dist/runtime-pane-assets.js.map +1 -1
  515. package/dist/runtime-pane-codex-index.d.ts +10 -1
  516. package/dist/runtime-pane-codex-index.js +131 -27
  517. package/dist/runtime-pane-codex-index.js.map +1 -1
  518. package/dist/runtime-pane-host.d.ts +19 -0
  519. package/dist/runtime-pane-host.js +50 -7
  520. package/dist/runtime-pane-host.js.map +1 -1
  521. package/dist/runtime-pane-link.d.ts +7 -0
  522. package/dist/runtime-pane-link.js +93 -2
  523. package/dist/runtime-pane-link.js.map +1 -1
  524. package/dist/runtime-pane-mcp-relay.d.ts +1 -0
  525. package/dist/runtime-pane-mcp-relay.js +10 -0
  526. package/dist/runtime-pane-mcp-relay.js.map +1 -1
  527. package/dist/runtime-pane-mcp.d.ts +4 -0
  528. package/dist/runtime-pane-mcp.js +6 -3
  529. package/dist/runtime-pane-mcp.js.map +1 -1
  530. package/dist/runtime-pane-native.d.ts +32 -2
  531. package/dist/runtime-pane-native.js +55 -6
  532. package/dist/runtime-pane-native.js.map +1 -1
  533. package/dist/runtime-pane-port.d.ts +5 -0
  534. package/dist/runtime-pane-port.js +17 -1
  535. package/dist/runtime-pane-port.js.map +1 -1
  536. package/dist/runtime-pane-process.d.ts +1 -1
  537. package/dist/runtime-pane-process.js +11 -2
  538. package/dist/runtime-pane-process.js.map +1 -1
  539. package/dist/runtime-pane-protocol.d.ts +11 -1
  540. package/dist/runtime-pane-protocol.js +27 -5
  541. package/dist/runtime-pane-protocol.js.map +1 -1
  542. package/dist/runtime-pane-quota-history.d.ts +6 -0
  543. package/dist/runtime-pane-quota-history.js +44 -0
  544. package/dist/runtime-pane-quota-history.js.map +1 -0
  545. package/dist/runtime-pane-service.d.ts +5 -0
  546. package/dist/runtime-pane-service.js +47 -12
  547. package/dist/runtime-pane-service.js.map +1 -1
  548. package/dist/runtime-pane-transcript.d.ts +5 -0
  549. package/dist/runtime-pane-transcript.js +66 -5
  550. package/dist/runtime-pane-transcript.js.map +1 -1
  551. package/dist/runtime-pane-worker.js +19 -6
  552. package/dist/runtime-pane-worker.js.map +1 -1
  553. package/dist/runtime-protocol.d.ts +17 -2
  554. package/dist/runtime-protocol.js +12 -0
  555. package/dist/runtime-protocol.js.map +1 -1
  556. package/dist/runtime-update-activity.d.ts +21 -0
  557. package/dist/runtime-update-activity.js +51 -0
  558. package/dist/runtime-update-activity.js.map +1 -0
  559. package/dist/runtime-update-artifact.d.ts +31 -0
  560. package/dist/runtime-update-artifact.js +252 -0
  561. package/dist/runtime-update-artifact.js.map +1 -0
  562. package/dist/runtime-update-install.d.ts +31 -0
  563. package/dist/runtime-update-install.js +205 -0
  564. package/dist/runtime-update-install.js.map +1 -0
  565. package/dist/runtime-update-journal.d.ts +44 -0
  566. package/dist/runtime-update-journal.js +121 -0
  567. package/dist/runtime-update-journal.js.map +1 -0
  568. package/dist/runtime-update-lock.d.ts +48 -0
  569. package/dist/runtime-update-lock.js +217 -0
  570. package/dist/runtime-update-lock.js.map +1 -0
  571. package/dist/runtime-update-owners.d.ts +29 -0
  572. package/dist/runtime-update-owners.js +240 -0
  573. package/dist/runtime-update-owners.js.map +1 -0
  574. package/dist/runtime-update-protocol.d.ts +15 -0
  575. package/dist/runtime-update-protocol.js +13 -0
  576. package/dist/runtime-update-protocol.js.map +1 -0
  577. package/dist/runtime-windows-bridge.d.ts +2 -1
  578. package/dist/runtime-windows-bridge.js +11 -1
  579. package/dist/runtime-windows-bridge.js.map +1 -1
  580. package/dist/scheduler/db.d.ts +2 -1
  581. package/dist/scheduler/db.js +23 -9
  582. package/dist/scheduler/db.js.map +1 -1
  583. package/dist/scheduler/task-board-text.js +1 -1
  584. package/dist/scheduler/task-board-text.js.map +1 -1
  585. package/dist/scheduler/task-board.d.ts +13 -0
  586. package/dist/scheduler/task-board.js +28 -3
  587. package/dist/scheduler/task-board.js.map +1 -1
  588. package/dist/service-drain.d.ts +19 -0
  589. package/dist/service-drain.js +64 -0
  590. package/dist/service-drain.js.map +1 -0
  591. package/dist/service-installer.d.ts +7 -2
  592. package/dist/service-installer.js +13 -10
  593. package/dist/service-installer.js.map +1 -1
  594. package/dist/settings-api.d.ts +20 -0
  595. package/dist/settings-api.js +414 -4
  596. package/dist/settings-api.js.map +1 -1
  597. package/dist/settings-baseline.d.ts +6 -0
  598. package/dist/settings-baseline.js +3 -1
  599. package/dist/settings-baseline.js.map +1 -1
  600. package/dist/settings-change.js +29 -6
  601. package/dist/settings-change.js.map +1 -1
  602. package/dist/settings-effect.d.ts +13 -0
  603. package/dist/settings-effect.js +97 -0
  604. package/dist/settings-effect.js.map +1 -1
  605. package/dist/settings-http-confirmation.js +13 -1
  606. package/dist/settings-http-confirmation.js.map +1 -1
  607. package/dist/settings-instance-patch.js +27 -0
  608. package/dist/settings-instance-patch.js.map +1 -1
  609. package/dist/setup-wizard.js +6 -2
  610. package/dist/setup-wizard.js.map +1 -1
  611. package/dist/task-board-tool.d.ts +13 -1
  612. package/dist/task-board-tool.js +104 -4
  613. package/dist/task-board-tool.js.map +1 -1
  614. package/dist/task-board-waker.d.ts +6 -0
  615. package/dist/task-board-waker.js +4 -0
  616. package/dist/task-board-waker.js.map +1 -1
  617. package/dist/tmux-binary.d.ts +59 -0
  618. package/dist/tmux-binary.js +200 -0
  619. package/dist/tmux-binary.js.map +1 -0
  620. package/dist/tmux-control.js +4 -4
  621. package/dist/tmux-control.js.map +1 -1
  622. package/dist/tmux-exec-helper.js +32 -1
  623. package/dist/tmux-exec-helper.js.map +1 -1
  624. package/dist/tmux-manager.d.ts +1 -1
  625. package/dist/tmux-manager.js +45 -36
  626. package/dist/tmux-manager.js.map +1 -1
  627. package/dist/tmux-read.js +4 -3
  628. package/dist/tmux-read.js.map +1 -1
  629. package/dist/tool-permissions.d.ts +5 -2
  630. package/dist/tool-permissions.js +6 -5
  631. package/dist/tool-permissions.js.map +1 -1
  632. package/dist/tool-progress.d.ts +2 -0
  633. package/dist/tool-progress.js +19 -3
  634. package/dist/tool-progress.js.map +1 -1
  635. package/dist/topic-commands.js +10 -5
  636. package/dist/topic-commands.js.map +1 -1
  637. package/dist/transcript-monitor.d.ts +20 -0
  638. package/dist/transcript-monitor.js +47 -3
  639. package/dist/transcript-monitor.js.map +1 -1
  640. package/dist/transcript-needles.d.ts +7 -0
  641. package/dist/transcript-needles.js +9 -1
  642. package/dist/transcript-needles.js.map +1 -1
  643. package/dist/transcript-records.d.ts +1 -1
  644. package/dist/transcript-records.js +2 -1
  645. package/dist/transcript-records.js.map +1 -1
  646. package/dist/types.d.ts +6 -0
  647. package/dist/ui/account-identity.js +27 -0
  648. package/dist/ui/accounts-view.js +214 -0
  649. package/dist/ui/agent-runtime-settings.js +9 -8
  650. package/dist/ui/backend-signin.js +4 -1
  651. package/dist/ui/chat-ambient.js +1 -1
  652. package/dist/ui/chat-md-strings.js +3 -0
  653. package/dist/ui/chat-render.js +3 -670
  654. package/dist/ui/chat-store.js +1 -1
  655. package/dist/ui/chat-strings.js +3 -333
  656. package/dist/ui/chat-thread.js +3 -919
  657. package/dist/ui/chat-time.js +3 -80
  658. package/dist/ui/chat-tour.js +1 -1
  659. package/dist/ui/credential-profile-select.js +54 -0
  660. package/dist/ui/fleet-office-dialogue.d.ts +30 -0
  661. package/dist/ui/fleet-office-dialogue.js +382 -0
  662. package/dist/ui/fleet-office-pack.d.ts +9 -0
  663. package/dist/ui/fleet-office-pack.js +76 -4
  664. package/dist/ui/fleet-office-skin.js +23 -5
  665. package/dist/ui/fleet-office-store.js +5 -0
  666. package/dist/ui/fleet-office.js +173 -6
  667. package/dist/ui/fleet-task-card.js +10 -6
  668. package/dist/ui/fleet-task-common.js +12 -2
  669. package/dist/ui/fleet-task-menu.js +188 -0
  670. package/dist/ui/fleet-task-stats.js +124 -0
  671. package/dist/ui/fleet-task-view.js +340 -0
  672. package/dist/ui/fleet-tasks.js +131 -43
  673. package/dist/ui/fleet-usage.js +90 -27
  674. package/dist/ui/html-attachment.js +3 -40
  675. package/dist/ui/hub-agent-copy.js +23 -0
  676. package/dist/ui/hub-button-actions.js +65 -0
  677. package/dist/ui/hub-chat-store.js +158 -0
  678. package/dist/ui/hub-chat-strings.js +77 -0
  679. package/dist/ui/hub-client.js +329 -0
  680. package/dist/ui/hub-owner-strings.js +51 -0
  681. package/dist/ui/hub-provider-guidance.js +31 -0
  682. package/dist/ui/hub-quota.js +85 -0
  683. package/dist/ui/hub-strings.js +162 -0
  684. package/dist/ui/hub-switcher.js +27 -0
  685. package/dist/ui/native-pane-recovery.js +1 -1
  686. package/dist/ui/office-share-page.js +4 -2
  687. package/dist/ui/panel-chat.js +34 -48
  688. package/dist/ui/panel-details.js +1 -1
  689. package/dist/ui/panel-fleet.js +78 -10
  690. package/dist/ui/panel-hub.js +619 -0
  691. package/dist/ui/panel-log.js +2 -2
  692. package/dist/ui/panel-needs.js +24 -8
  693. package/dist/ui/panel-runtimes.js +114 -23
  694. package/dist/ui/panel-settings.js +69 -80
  695. package/dist/ui/panel-windows-cli.js +12 -2
  696. package/dist/ui/preview-pages.js +1 -1
  697. package/dist/ui/preview-panel.js +92 -6
  698. package/dist/ui/preview-versions.js +1 -1
  699. package/dist/ui/preview.js +3 -476
  700. package/dist/ui/runtime-commands.d.ts +4 -0
  701. package/dist/ui/runtime-commands.js +59 -0
  702. package/dist/ui/search-dialog.js +2 -0
  703. package/dist/ui/settings-access-strings.js +42 -0
  704. package/dist/ui/settings-build-version.js +35 -0
  705. package/dist/ui/settings-codex-copy.js +19 -0
  706. package/dist/ui/settings-devices.js +253 -0
  707. package/dist/ui/settings-dialogs.js +28 -7
  708. package/dist/ui/settings-hub-browser-certificate.js +74 -0
  709. package/dist/ui/settings-hub-membership.js +72 -0
  710. package/dist/ui/settings-hub-retention.js +52 -0
  711. package/dist/ui/settings-hub.js +196 -0
  712. package/dist/ui/settings-layout.js +8 -11
  713. package/dist/ui/settings-model.js +16 -2
  714. package/dist/ui/settings-outside.js +104 -38
  715. package/dist/ui/settings-registry.js +4 -1
  716. package/dist/ui/settings-shared-strings.js +143 -0
  717. package/dist/ui/settings-shared.js +345 -0
  718. package/dist/ui/settings-strings.js +10 -46
  719. package/dist/ui/settings-web-authority.js +5 -2
  720. package/dist/ui/shared/accounts.css +15 -0
  721. package/dist/ui/shared/app-i18n.js +2 -2
  722. package/dist/ui/shared/app-needs.js +1 -1
  723. package/dist/ui/shared/app-route.js +6 -1
  724. package/dist/ui/shared/app-session.js +6 -2
  725. package/dist/ui/shared/app-shell.js +64 -8
  726. package/dist/ui/shared/app.css +121 -30
  727. package/dist/ui/shared/app.js +12 -3
  728. package/dist/ui/shared/chat-composer.js +68 -0
  729. package/dist/ui/shared/chat-md-strings.js +9 -0
  730. package/dist/ui/shared/chat-render.js +682 -0
  731. package/dist/ui/shared/chat-strings.js +333 -0
  732. package/dist/ui/shared/chat-thread.js +969 -0
  733. package/dist/ui/shared/chat-time.js +80 -0
  734. package/dist/ui/shared/header-tools.js +3 -5
  735. package/dist/ui/shared/html-attachment.js +42 -0
  736. package/dist/ui/shared/hub-chat.css +85 -0
  737. package/dist/ui/shared/hub-owner.css +33 -0
  738. package/dist/ui/shared/hub-quota.css +8 -0
  739. package/dist/ui/shared/hub.css +71 -0
  740. package/dist/ui/shared/page-head.js +8 -0
  741. package/dist/ui/shared/panel-view.js +77 -4
  742. package/dist/ui/shared/preview.js +476 -0
  743. package/dist/ui/shared/runtime-install.css +26 -0
  744. package/dist/ui/shared/side-rail.js +15 -0
  745. package/dist/ui/shared/signin.js +172 -0
  746. package/dist/ui/shared/task-board.css +95 -0
  747. package/dist/ui/shared/usage-dialog.js +18 -1
  748. package/dist/ui/shared/view-history.css +17 -0
  749. package/dist/ui/shared/view-history.js +438 -0
  750. package/dist/ui/shared/view-strings.js +16 -0
  751. package/dist/ui/shared/workspace.css +20 -0
  752. package/dist/ui/signin.html +21 -0
  753. package/dist/ui/workspace-panel.js +189 -0
  754. package/dist/ui/workspace-strings.js +35 -0
  755. package/dist/unattended-install.d.ts +11 -2
  756. package/dist/unattended-install.js +41 -6
  757. package/dist/unattended-install.js.map +1 -1
  758. package/dist/usage/capacity.d.ts +69 -0
  759. package/dist/usage/capacity.js +119 -0
  760. package/dist/usage/capacity.js.map +1 -0
  761. package/dist/usage/i18n-keys.d.ts +1 -1
  762. package/dist/usage/i18n-keys.js +3 -0
  763. package/dist/usage/i18n-keys.js.map +1 -1
  764. package/dist/usage/profile-timeline.d.ts +16 -0
  765. package/dist/usage/profile-timeline.js +58 -0
  766. package/dist/usage/profile-timeline.js.map +1 -0
  767. package/dist/usage/providers.js +2 -1
  768. package/dist/usage/providers.js.map +1 -1
  769. package/dist/usage/quota-history.d.ts +43 -0
  770. package/dist/usage/quota-history.js +168 -0
  771. package/dist/usage/quota-history.js.map +1 -0
  772. package/dist/usage/usage-api.d.ts +31 -0
  773. package/dist/usage/usage-api.js +80 -1
  774. package/dist/usage/usage-api.js.map +1 -1
  775. package/dist/usage-report.d.ts +25 -1
  776. package/dist/usage-report.js +55 -4
  777. package/dist/usage-report.js.map +1 -1
  778. package/dist/usage-rollup.d.ts +6 -1
  779. package/dist/usage-rollup.js +16 -4
  780. package/dist/usage-rollup.js.map +1 -1
  781. package/dist/view-api.d.ts +16 -0
  782. package/dist/view-api.js +105 -23
  783. package/dist/view-api.js.map +1 -1
  784. package/dist/view-history.d.ts +84 -0
  785. package/dist/view-history.js +116 -0
  786. package/dist/view-history.js.map +1 -0
  787. package/dist/web-api.d.ts +48 -2
  788. package/dist/web-api.js +276 -8
  789. package/dist/web-api.js.map +1 -1
  790. package/dist/web-backend-login.d.ts +23 -1
  791. package/dist/web-backend-login.js +11 -17
  792. package/dist/web-backend-login.js.map +1 -1
  793. package/dist/web-device-login.d.ts +153 -0
  794. package/dist/web-device-login.js +278 -0
  795. package/dist/web-device-login.js.map +1 -0
  796. package/dist/web-office-skins.d.ts +9 -0
  797. package/dist/web-office-skins.js +16 -1
  798. package/dist/web-office-skins.js.map +1 -1
  799. package/dist/web-session-authorization.d.ts +13 -2
  800. package/dist/web-session-authorization.js +10 -3
  801. package/dist/web-session-authorization.js.map +1 -1
  802. package/dist/web-session.d.ts +35 -0
  803. package/dist/web-session.js +7 -0
  804. package/dist/web-session.js.map +1 -1
  805. package/dist/web-settings-patch.d.ts +3 -1
  806. package/dist/web-settings-patch.js +4 -1
  807. package/dist/web-settings-patch.js.map +1 -1
  808. package/dist/web-shell-routes.d.ts +6 -0
  809. package/dist/web-shell-routes.js +7 -0
  810. package/dist/web-shell-routes.js.map +1 -1
  811. package/dist/web-terminal.d.ts +4 -30
  812. package/dist/web-terminal.js +9 -6
  813. package/dist/web-terminal.js.map +1 -1
  814. package/dist/windows-agents.d.ts +1 -0
  815. package/dist/windows-agents.js +1 -0
  816. package/dist/windows-agents.js.map +1 -1
  817. package/dist/windows-cli-api.d.ts +12 -1
  818. package/dist/windows-cli-api.js +27 -1
  819. package/dist/windows-cli-api.js.map +1 -1
  820. package/dist/windows-cli-providers.d.ts +0 -9
  821. package/dist/windows-cli-providers.js +4 -9
  822. package/dist/windows-cli-providers.js.map +1 -1
  823. package/dist/windows-cli-worker.d.ts +24 -0
  824. package/dist/windows-cli-worker.js +96 -5
  825. package/dist/windows-cli-worker.js.map +1 -1
  826. package/dist/windows-cli.d.ts +10 -0
  827. package/dist/windows-cli.js +10 -1
  828. package/dist/windows-cli.js.map +1 -1
  829. package/dist/yaml-shared-nodes.js +8 -3
  830. package/dist/yaml-shared-nodes.js.map +1 -1
  831. package/launcher/agend-install.cjs +8 -1
  832. package/launcher/launchd-session-type.cjs +14 -0
  833. package/launcher/postinstall.cjs +27 -2
  834. package/launcher/private-home.cjs +1 -1
  835. package/launcher/tmux-bundle.cjs +247 -0
  836. package/package.json +3 -2
  837. package/templates/launchd.plist.ejs +3 -1
  838. package/templates/runtime-install/install.ps1 +270 -0
@@ -1,3 +1,4 @@
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";
@@ -9,12 +10,14 @@ import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identit
9
10
  import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
10
11
  import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
11
12
  import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
13
+ import { maskSecrets } from "./log-steps.js";
12
14
  import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
13
15
  import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
14
16
  import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
15
17
  import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
16
18
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
17
19
  import { SettingsBaselines } from "./settings-baseline.js";
20
+ import { hubMembershipSettings } from "./hub-membership-settings.js";
18
21
  import { prepareSettingsEffect } from "./settings-effect.js";
19
22
  import { settingsDisplay } from "./settings-change.js";
20
23
  import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
@@ -39,11 +42,13 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
39
42
  import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
40
43
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
41
44
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
42
- import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
45
+ import { installExitMarker, unattendedInstallCommand, loginShell } from "./unattended-install.js";
43
46
  import { InstallStream } from "./install-stream.js";
44
47
  import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
45
48
  import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
46
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";
47
52
  import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
48
53
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
49
54
  import { ManagedTunnel } from "./tunnel/manager.js";
@@ -80,6 +85,11 @@ import { tmuxExecHelper } from "./tmux-exec-helper.js";
80
85
  import { kiroLogGate } from "./kiro-log-gate.js";
81
86
  const __filename = fileURLToPath(import.meta.url);
82
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);
83
93
  /** Fallback access policy for a channel with no `access:` block — open (no gate). */
84
94
  const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
85
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";
@@ -87,6 +97,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
87
97
  import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
88
98
  import { EventLog } from "./event-log.js";
89
99
  import { binaryProbe } from "./binary-probe.js";
100
+ import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
90
101
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
91
102
  import { classifySqliteOpenError } from "./sqlite-open-errors.js";
92
103
  import { OutboxOpenError } from "./outbox-open-error.js";
@@ -103,6 +114,7 @@ import { formatFleetTime } from "./tz-utils.js";
103
114
  import { createAdapter } from "./channel/factory.js";
104
115
  import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
105
116
  import { TelegramAdapter } from "./channel/adapters/telegram.js";
117
+ import { MissedOfflineStore } from "./missed-offline.js";
106
118
  import { createBackend } from "./backend/factory.js";
107
119
  import { readEffortMetadata } from "./backend/effort-metadata.js";
108
120
  import { codexEffort, flagEffort } from "./backend/launch-effort.js";
@@ -128,6 +140,7 @@ import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, che
128
140
  import { TopicArchiver } from "./topic-archiver.js";
129
141
  import { StatuslineWatcher } from "./statusline-watcher.js";
130
142
  import { applyInstanceConfigPatch, outboundHandlers } from "./outbound-handlers.js";
143
+ import { HUB_REPORT_TARGET } from "./hub-report-target.js";
131
144
  import { backendArgs, backendStep, joinBackendArgs, modelNameCarried, planDisplay } from "./backend-command.js";
132
145
  import { DeliveryOutbox, DEFAULT_REPLY_OBLIGATION_TTL_MS, isSilentScheduleDelivery } from "./delivery-outbox.js";
133
146
  // The target Daemon can legitimately wait 30 minutes for a busy pane before
@@ -154,6 +167,20 @@ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked", "review"];
154
167
  * " \t " and [" ", "\t"] both normalize to undefined (not treated as explicit
155
168
  * filters). The same result drives the live-only default and the cap decision.
156
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
+ }
157
184
  export function normalizeStatusFilter(v) {
158
185
  if (typeof v === "string") {
159
186
  const s = v.trim();
@@ -183,19 +210,23 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
183
210
  }
184
211
  import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
185
212
  import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
213
+ import { nativeCliVersionState } from "./runtime-pane-native.js";
186
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";
187
217
  import { RuntimeCoordinator } from "./runtime-coordinator.js";
188
218
  import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
189
219
  import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
190
220
  import { NativeMcpSessions } from "./native-mcp.js";
191
221
  import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
192
- 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";
193
223
  import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
194
224
  import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
195
225
  import { LogService } from "./log-service.js";
226
+ import { viewHistory } from "./view-history.js";
196
227
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
197
228
  import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
198
- import { WebChatDiskStore } from "./web-chat-store.js";
229
+ import { WebChatDiskStore, isSafeInstanceDirName } from "./web-chat-store.js";
199
230
  import { SeenUsers, resolveMentions } from "./web-mentions.js";
200
231
  import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
201
232
  import { agendVersion } from "./agent-whats-new.js";
@@ -225,8 +256,8 @@ import { envFileKeys } from "./token-env-name.js";
225
256
  import { discordGeneralChannelId } from "./discord-general.js";
226
257
  import { parseEnvText } from "./env-file.js";
227
258
  import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
228
- import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
229
- 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";
230
261
  import { LoginSession } from "./login-manager.js";
231
262
  import { runOnLoginShell } from "./login-shell.js";
232
263
  import { tightenInstanceDirs } from "./private-dir.js";
@@ -256,6 +287,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
256
287
  import { recentChatContext } from "./log-tail.js";
257
288
  import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
258
289
  import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
290
+ import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
291
+ import { writeSecretFile } from "./secret-file.js";
259
292
  import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
260
293
  import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
261
294
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
@@ -286,13 +319,14 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
286
319
  import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
287
320
  import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
288
321
  import { sharedRolloutIndex } from "./rollout-index.js";
289
- import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
322
+ import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardOffIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
290
323
  import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
291
324
  import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
292
325
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
293
326
  import { isWebPageNavigation } from "./web-shell-routes.js";
294
- import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
327
+ import { guestSessionPolicy, localSessionPolicy, sessionVia, tokenEpoch, WebSessionStore } from "./web-session.js";
295
328
  import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
329
+ import { DeviceLoginRequests } from "./web-device-login.js";
296
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";
297
331
  import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
298
332
  import { AuthorityStateStore } from "./authority/authority-state.js";
@@ -315,9 +349,10 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
315
349
  const SELF_RESTART_DEADLINE_MS = 300_000;
316
350
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
317
351
  import { kiroHome } from "./backend/kiro-kas-store.js";
318
- import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
352
+ import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileEnv, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
319
353
  import { carryClaudeConversation } from "./backend/claude-carry.js";
320
- 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";
321
356
  import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
322
357
  import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
323
358
  import { FleetStartup } from "./fleet-startup.js";
@@ -331,6 +366,12 @@ import { BackendOutageTracker } from "./backend-outage.js";
331
366
  import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
332
367
  import { getTmuxSession } from "./config.js";
333
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";
334
375
  // Closed migration cohort: these 11 names existed immediately before the first
335
376
  // managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
336
377
  // adding a bundled skill: its new name is not proof of old AgEnD ownership.
@@ -482,6 +523,8 @@ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
482
523
  * while anything real shows signs of life well before the old two minutes.
483
524
  */
484
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;
485
528
  /** How much of a tool summary the progress line will show before eliding. */
486
529
  const PROGRESS_ACTIVITY_MAX_CHARS = 48;
487
530
  /**
@@ -768,6 +811,9 @@ export class FleetManager {
768
811
  localPaneRelay = null;
769
812
  openingLocalPane = null;
770
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(); }
771
817
  runtimeExecutor = null;
772
818
  runtimeStatusScheduled = false;
773
819
  reconciledRemoteResults = new Set();
@@ -819,6 +865,8 @@ export class FleetManager {
819
865
  nativeMcpSessions() {
820
866
  return this.nativeMcpStore ??= new NativeMcpSessions(this);
821
867
  }
868
+ /** Retire only existing native capabilities; ordinary lifecycle changes do not instantiate the Windows path. */
869
+ invalidateNativeMcp(name) { this.nativeMcpStore?.closeInstance(name); }
822
870
  /** The same instance registry and delivery entry point, with a native Windows execution adapter. */
823
871
  nativeInstanceRuntime() {
824
872
  return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
@@ -831,13 +879,23 @@ export class FleetManager {
831
879
  machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
832
880
  machineGeneration: id => this.runtimeCoordinator().machine(id)?.bootId ?? null,
833
881
  paneAvailable: id => this.runtimeCoordinator().machine(id)?.paneProtocol === 1,
834
- validateMachine: (id, cli, sandbox) => {
882
+ validateMachine: (id, cli, sandbox, credentialProfile) => {
835
883
  const machine = this.runtimeCoordinator().machine(id);
836
884
  const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
837
885
  if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
838
886
  throw new Error("Choose an online runtime and an available CLI");
839
887
  if (sandbox === "read-only" && !provider.readOnlySupport)
840
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
+ }
841
899
  },
842
900
  emit: (event, data) => {
843
901
  this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
@@ -1065,6 +1123,26 @@ export class FleetManager {
1065
1123
  return result;
1066
1124
  });
1067
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
+ }
1068
1146
  async localNativePaneEndpoint() {
1069
1147
  const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
1070
1148
  throw Error("Local Windows runtime is switched off or draining"); };
@@ -1081,6 +1159,7 @@ export class FleetManager {
1081
1159
  ]);
1082
1160
  admit();
1083
1161
  this.localPaneRelay?.stop();
1162
+ this.localPaneInventory = inventory;
1084
1163
  const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
1085
1164
  diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
1086
1165
  version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
@@ -1129,8 +1208,12 @@ export class FleetManager {
1129
1208
  if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
1130
1209
  || now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
1131
1210
  || now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
1132
- || now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding)
1211
+ || now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding
1212
+ || nativeCredentialProfile(now) !== nativeCredentialProfile(config))
1133
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.");
1134
1217
  if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
1135
1218
  throw Error("Native runtime is switched off");
1136
1219
  },
@@ -1189,12 +1272,12 @@ export class FleetManager {
1189
1272
  throw Error("General or its paired runtime is unavailable");
1190
1273
  const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
1191
1274
  && this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
1192
- const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertAuditedNativePaneVersion }] = await Promise.all([
1275
+ const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertRunnableNativePaneVersion }] = await Promise.all([
1193
1276
  import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
1194
1277
  ]);
1195
1278
  if (!current())
1196
1279
  throw Error("General workspace authority expired");
1197
- 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);
1198
1281
  const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
1199
1282
  const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
1200
1283
  if (!current())
@@ -1208,6 +1291,14 @@ export class FleetManager {
1208
1291
  throw new NativePaneRecoveryError({ kind: "host-upgrade" });
1209
1292
  return validateGeneralWorkspaceReceipt(result, request);
1210
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; }
1211
1302
  generalLocationFlights = new Map();
1212
1303
  generalAttentionEpisodes = new Map();
1213
1304
  generalBindings() { return new GeneralBindings(this.dataDir); }
@@ -1663,6 +1754,364 @@ export class FleetManager {
1663
1754
  * them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
1664
1755
  * sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
1665
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; }
1666
2115
  observeRuntimeSwitches() {
1667
2116
  setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
1668
2117
  if (!this.runtimeEffects || this.runtimeEffectsScheduled)
@@ -1731,6 +2180,28 @@ export class FleetManager {
1731
2180
  return this.daemons.get(name)?.bootId ?? null;
1732
2181
  }
1733
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 = [];
1734
2205
  rawFleetDocument = null;
1735
2206
  /** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
1736
2207
  fleetDocumentWrites = null;
@@ -1880,6 +2351,7 @@ export class FleetManager {
1880
2351
  startupRetryNotices = new Map();
1881
2352
  /** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
1882
2353
  unsupportedCliNotices = new Map();
2354
+ kiroCompatibilityHolds = new Map();
1883
2355
  kiroIncompatNotices = new Map();
1884
2356
  /** Backoff between automatic startup retries; the last step repeats while the backend is down. */
1885
2357
  static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
@@ -1908,6 +2380,8 @@ export class FleetManager {
1908
2380
  kiroMigrationRuns = new Map();
1909
2381
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
1910
2382
  needsYou = null;
2383
+ hubExecutionAttention = null;
2384
+ missedOfflineStore = null;
1911
2385
  configHistory = null;
1912
2386
  configHistoryTimer = null;
1913
2387
  loadedFleetSource = null;
@@ -2023,6 +2497,9 @@ export class FleetManager {
2023
2497
  discordPresenceEagerPending = false;
2024
2498
  discordPresenceInFlight = null;
2025
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;
2026
2503
  /** Days of event/activity history to keep. */
2027
2504
  static EVENT_LOG_RETENTION_DAYS = 30;
2028
2505
  watchdogTimer = null;
@@ -2218,6 +2695,8 @@ export class FleetManager {
2218
2695
  /** #2855: a trusted guest's searches, per account. */
2219
2696
  guestSearches = new OfficeWriteLimiter(undefined, { perWindow: GUEST_SEARCHES.perWindow, windowMs: GUEST_SEARCHES.windowMs });
2220
2697
  officeSharesInstance;
2698
+ /** #2861: when each instance was last talked to and answered (no words), for share links' dialogue box. */
2699
+ officeTalk = new OfficeTalk();
2221
2700
  /**
2222
2701
  * #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
2223
2702
  * their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
@@ -2234,6 +2713,7 @@ export class FleetManager {
2234
2713
  hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
2235
2714
  skins: this.officeSkins,
2236
2715
  levels: () => this.officeLevelsSnapshot(),
2716
+ talk: scope => this.officeTalk.recent(scope),
2237
2717
  },
2238
2718
  manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
2239
2719
  ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
@@ -2590,6 +3070,26 @@ export class FleetManager {
2590
3070
  const baselines = this.settingsBaselineReader = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
2591
3071
  config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
2592
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
+ },
2593
3093
  proof: (kind, target, id, binding, key) => {
2594
3094
  const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
2595
3095
  const proof = map.get(id);
@@ -2739,6 +3239,132 @@ export class FleetManager {
2739
3239
  // while the request is applied fences it.
2740
3240
  return this.liveOwner(req, { hostOnly: false });
2741
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
+ }
2742
3368
  /** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
2743
3369
  webAgentProposals(req) {
2744
3370
  if (!this.agentProposalOwner(req))
@@ -3296,6 +3922,8 @@ export class FleetManager {
3296
3922
  sseHeartbeatMs = SSE_HEARTBEAT_MS;
3297
3923
  /** The dashboard's login codes. Memory only: a code that outlives the process is a code nobody can prove was not copied. */
3298
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;
3299
3927
  /**
3300
3928
  * #1920 step 4: guests' codes, apart from the admins' — one per account, so no guest's `/web` withdraws another's or
3301
3929
  * an admin's code (#2093 review), at most 32 at once. Local sign-in only for now (audience "local").
@@ -3334,6 +3962,22 @@ export class FleetManager {
3334
3962
  * startup-only key at startup. See checkStartupSignatureConsistency(). */
3335
3963
  fleetSignatureMismatch = null;
3336
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
+ }
3337
3981
  constructor(dataDir) {
3338
3982
  this.dataDir = dataDir;
3339
3983
  FleetManager.signalTarget = this;
@@ -3356,6 +4000,7 @@ export class FleetManager {
3356
4000
  this.topicCommands = new TopicCommands(this);
3357
4001
  this.topicArchiver = new TopicArchiver(this);
3358
4002
  this.statuslineWatcher = new StatuslineWatcher(this);
4003
+ this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
3359
4004
  }
3360
4005
  /** #1266: a reply's buttons — the store (reply-buttons.db) and what a click does. Created on first use. */
3361
4006
  replyButtonsCtl = null;
@@ -3736,6 +4381,10 @@ export class FleetManager {
3736
4381
  }
3737
4382
  }
3738
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
+ }
3739
4388
  // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
3740
4389
  // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
3741
4390
  this.scheduleDeliveryOutboxPump();
@@ -4613,6 +5262,7 @@ export class FleetManager {
4613
5262
  this.enforceGuestSwitch();
4614
5263
  this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
4615
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)
4616
5266
  // Child tool/guidance readers must follow this fleet's actual config, including --config.
4617
5267
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
4618
5268
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
@@ -4666,6 +5316,8 @@ export class FleetManager {
4666
5316
  * guild channel.
4667
5317
  */
4668
5318
  reregisterClassicChannels() {
5319
+ this.nativeMcpStore?.observePolicies();
5320
+ this.agentCredentialsCache?.observePolicies();
4669
5321
  if (!this.classicChannels)
4670
5322
  return;
4671
5323
  const channels = this.classicChannels.getAll();
@@ -4718,13 +5370,9 @@ export class FleetManager {
4718
5370
  }
4719
5371
  /** AgEnD package version (for the Settings "current version" / What's New). */
4720
5372
  get currentVersion() {
4721
- try {
4722
- return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version ?? "unknown";
4723
- }
4724
- catch {
4725
- return "unknown";
4726
- }
5373
+ return RUNNING_BUILD.version;
4727
5374
  }
5375
+ get currentBuild() { return RUNNING_BUILD; }
4728
5376
  /**
4729
5377
  * Resolve a slash-command target in a channel. Classic channels are looked up
4730
5378
  * per-bot (same-channel multi-bot); a fleet-topic instance is found via the
@@ -5397,6 +6045,13 @@ export class FleetManager {
5397
6045
  changed: () => this.emitSseEvent("status", this.getUiStatus()),
5398
6046
  });
5399
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
+ }
5400
6055
  /** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
5401
6056
  logSteps(name, q) { return this.log.steps(name, q); }
5402
6057
  /**
@@ -5663,7 +6318,7 @@ export class FleetManager {
5663
6318
  finish(false);
5664
6319
  });
5665
6320
  }
5666
- async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold) {
6321
+ async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold, beforeInput) {
5667
6322
  if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
5668
6323
  return false;
5669
6324
  await this.holdDeliveryForStorm(instanceName, deliveryEpoch);
@@ -5702,7 +6357,8 @@ export class FleetManager {
5702
6357
  const log = idleGateTimeoutLog(instanceName, timeoutMs, this.instanceStateCache.get(instanceName), Date.now());
5703
6358
  this.logger[log.level](log.fields, log.message);
5704
6359
  }
5705
- 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);
5706
6362
  if (!sent)
5707
6363
  return false;
5708
6364
  if (this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent)) {
@@ -5735,21 +6391,25 @@ export class FleetManager {
5735
6391
  * `onStartRetryHold` (#2484, a person's message): an instance in its own start-retry backoff is waited for until that
5736
6392
  * retry brings it up, however long the backoff, and fails only once the retries give up.
5737
6393
  */
5738
- async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
6394
+ async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold, beforeInput) {
5739
6395
  if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
5740
6396
  return false;
5741
6397
  const queued = this.ipcWaitTails.get(instanceName);
5742
6398
  if (!queued) {
5743
6399
  const ipc = this.instanceIpcClients.get(instanceName);
5744
- if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
5745
- 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
+ }
5746
6406
  }
5747
6407
  const attempt = (queued ?? Promise.resolve())
5748
6408
  .catch(() => { })
5749
6409
  .then(() => {
5750
6410
  if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
5751
6411
  return false;
5752
- return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
6412
+ return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold, beforeInput);
5753
6413
  });
5754
6414
  // The chain stores a settled-either-way promise so one failed delivery cannot
5755
6415
  // wedge every later one, and so `queued` above is safe to await unguarded.
@@ -5766,8 +6426,21 @@ export class FleetManager {
5766
6426
  }
5767
6427
  }
5768
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
+ }
5769
6442
  /** Poll for the instance's IPC to come back, then send. Throws if it does not. */
5770
- async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
6443
+ async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold, beforeInput) {
5771
6444
  let deadline = Date.now() + IPC_RECONNECT_GRACE_MS;
5772
6445
  let warned = false;
5773
6446
  let held = false;
@@ -5777,6 +6450,8 @@ export class FleetManager {
5777
6450
  // Re-read every round: a reconnect replaces the IpcClient object entirely,
5778
6451
  // so a cached reference would stay dead forever.
5779
6452
  const ipc = this.instanceIpcClients.get(instanceName);
6453
+ if (ipc?.connected && beforeInput)
6454
+ return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
5780
6455
  if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
5781
6456
  return true;
5782
6457
  // #2484: while the instance waits for (or runs) its own start retry, the grace runs from now — so the message is
@@ -6216,6 +6891,8 @@ export class FleetManager {
6216
6891
  if (this.generalLocationDeliveryHold(instanceName))
6217
6892
  return false;
6218
6893
  if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
6894
+ if (options.beforeInput)
6895
+ return false; // no pre-input hook on the Windows runtime
6219
6896
  return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
6220
6897
  ...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
6221
6898
  }
@@ -6240,7 +6917,7 @@ export class FleetManager {
6240
6917
  return false;
6241
6918
  this.enforceWarmCap(instanceName); // woke one → evict a different LRU idle if over cap
6242
6919
  }
6243
- 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);
6244
6921
  if (!sent)
6245
6922
  return false;
6246
6923
  // A cross-instance item arriving before the daemon observes this turn as
@@ -6251,7 +6928,7 @@ export class FleetManager {
6251
6928
  return true;
6252
6929
  }
6253
6930
  const previous = this.idleGatedDeliveryTails.get(instanceName) ?? Promise.resolve();
6254
- 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));
6255
6932
  this.idleGatedDeliveryTails.set(instanceName, delivery);
6256
6933
  try {
6257
6934
  return await delivery;
@@ -7522,6 +8199,17 @@ export class FleetManager {
7522
8199
  this.queueKiroIncompatNotice(name, err.message);
7523
8200
  return true;
7524
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
+ }
7525
8213
  /**
7526
8214
  * LifecycleContext: a kiro instance the installed kiro-cli cannot run as
7527
8215
  * configured (#1109) — at start, or at a respawn after kiro-cli replaced
@@ -8028,6 +8716,12 @@ export class FleetManager {
8028
8716
  },
8029
8717
  });
8030
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
+ }
8031
8725
  if (!this.webGuestLoginCodes) {
8032
8726
  this.webGuestLoginCodes = new GuestLoginCodes({
8033
8727
  onEvent: event => {
@@ -8642,6 +9336,7 @@ export class FleetManager {
8642
9336
  void this.publicWebLink?.close("dashboard revoke");
8643
9337
  this.forgetPublicLinkOwner();
8644
9338
  this.webLoginCodes.revoke();
9339
+ this.webDeviceLogins?.revokeAll(); // …and every new device still waiting to be approved (#3061)
8645
9340
  this.webGuestLoginCodes.revokeAll();
8646
9341
  this.guestRevokes++; // and every guest /web still on its way
8647
9342
  // #1570: no browser may ask the sign-in page for a code any more.
@@ -9605,8 +10300,9 @@ export class FleetManager {
9605
10300
  // #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
9606
10301
  const topicMode = daemonTopicMode(fleet);
9607
10302
  // Set tmux socket isolation for custom AGEND_HOME
9608
- const { getTmuxSocketName: getSocket } = await import("./paths.js");
9609
- 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());
9610
10306
  await TmuxManager.ensureSession(getTmuxSession());
9611
10307
  this.tmuxSessionEnsured = true;
9612
10308
  // #2072: what this host can run — probed in the background, published for every picker.
@@ -9871,6 +10567,9 @@ export class FleetManager {
9871
10567
  await this.startHealthServer(fleet.health_port ?? 19280);
9872
10568
  if (this.shuttingDown)
9873
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();
9874
10573
  if (generals.length > 0) {
9875
10574
  for (const [name, cfg] of generals) {
9876
10575
  try {
@@ -9973,6 +10672,9 @@ export class FleetManager {
9973
10672
  if (others.length > 0 || classicAtBoot.length > 0) {
9974
10673
  await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
9975
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();
9976
10678
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
9977
10679
  await adapterStartup;
9978
10680
  this.startDiscordUsagePresence();
@@ -10148,6 +10850,178 @@ export class FleetManager {
10148
10850
  this.logger.warn({ health }, "Fleet started with problems — see /health");
10149
10851
  }
10150
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
+ }
10151
11025
  /** Keep Discord profile activity aligned with the same cached usage source as /usage. */
10152
11026
  startDiscordUsagePresence() {
10153
11027
  if (this.discordPresenceTimer)
@@ -10485,8 +11359,8 @@ export class FleetManager {
10485
11359
  * that should be running is running
10486
11360
  * - `degraded` — reachable, but something the operator should look at (an
10487
11361
  * adapter retrying, an instance crashed or stopped)
10488
- * - `down` — the fleet cannot do its job: no adapter is connected, so no
10489
- * message can arrive or be answered
11362
+ * - `down` — no configured adapter is connected, or a dedicated Hub's
11363
+ * server mount is unavailable
10490
11364
  *
10491
11365
  * Deliberately does NOT gate the systemd watchdog — see the comment at the
10492
11366
  * WATCHDOG timer for why.
@@ -10519,10 +11393,20 @@ export class FleetManager {
10519
11393
  if (effectiveStatus === "connected")
10520
11394
  connected++;
10521
11395
  }
10522
- 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}`);
10523
11403
  const hostMemory = this.memoryPressure.snapshot();
10524
- if (!this.memoryPressure.advisoryOnly() && (hostMemory.level === "critical" || hostMemory.level === "elevated")) {
10525
- 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);
10526
11410
  }
10527
11411
  if (this.adapterState.size > 0 && connected === 0)
10528
11412
  problems.push("no channel adapter is connected");
@@ -10534,9 +11418,8 @@ export class FleetManager {
10534
11418
  }
10535
11419
  if (!this.startupComplete)
10536
11420
  problems.push("startup has not completed");
10537
- // "down" is reserved for "cannot receive or answer a message at all". A fleet
10538
- // with adapters configured but none connected is exactly that.
10539
- 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")
10540
11423
  ? "down"
10541
11424
  : problems.length > 0 ? "degraded" : "ok";
10542
11425
  return {
@@ -10548,7 +11431,7 @@ export class FleetManager {
10548
11431
  memory: readFleetMemory(),
10549
11432
  hostMemory: this.memoryTop && (hostMemory.level === "elevated" || hostMemory.level === "critical")
10550
11433
  ? { ...hostMemory, topProcesses: this.memoryTop.processes, topProcessesAt: this.memoryTop.at } : hostMemory,
10551
- problems,
11434
+ problems, warnings, ...(hub ? { hub } : {}),
10552
11435
  };
10553
11436
  }
10554
11437
  /** Start the primary adapter (backward-compatible, sets this.adapter) */
@@ -10623,6 +11506,11 @@ export class FleetManager {
10623
11506
  adapter.on("callback_query", safeHandler(async (data) => {
10624
11507
  await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
10625
11508
  }, this.logger, "adapter.callback_query"));
11509
+ adapter.on("missed_offline", (ev) => {
11510
+ if (!isCurrentAdapter())
11511
+ return;
11512
+ this.recordMissedOffline(ev);
11513
+ });
10626
11514
  this.bindTopicClosedHandler(adapter, adapterId, "adapter.topic_closed");
10627
11515
  // Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
10628
11516
  adapter.on("slash_command", safeHandler(async (data) => {
@@ -10776,6 +11664,11 @@ export class FleetManager {
10776
11664
  adapter.on("callback_query", safeHandler(async (data) => {
10777
11665
  await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
10778
11666
  }, this.logger, `adapter[${adapterId}].callback_query`));
11667
+ adapter.on("missed_offline", (ev) => {
11668
+ if (!isCurrentAdapter())
11669
+ return;
11670
+ this.recordMissedOffline(ev);
11671
+ });
10779
11672
  this.bindTopicClosedHandler(adapter, adapterId, `adapter[${adapterId}].topic_closed`);
10780
11673
  // Slash commands: classic bot + admin commands
10781
11674
  adapter.on("slash_command", safeHandler(async (data) => {
@@ -10935,6 +11828,9 @@ export class FleetManager {
10935
11828
  this.eventLog?.insert(targetInstance, "cross_instance_delivery_failed", {
10936
11829
  from: senderSession, correlation_id: correlationId, error,
10937
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);
10938
11834
  if (senderInstance) {
10939
11835
  this.notifyInstanceTopic(senderInstance, t("cross_instance.delivery_failed_notice", targetInstance, displayError, correlationId));
10940
11836
  void this.deliverToInstance(senderInstance, {
@@ -11053,13 +11949,12 @@ export class FleetManager {
11053
11949
  try {
11054
11950
  const { execFile } = await import("./child-process.js");
11055
11951
  const { promisify } = await import("node:util");
11056
- const { getTmuxSocketName } = await import("./paths.js");
11952
+ const { tmuxArgs, tmuxBinary, tmuxEnvOption } = await import("./tmux-binary.js");
11057
11953
  // Honour socket isolation: without -L this queried the user's default
11058
11954
  // tmux server instead of the fleet's, so under a custom AGEND_HOME the
11059
11955
  // check was meaningless (it reported every pane dead).
11060
- const socket = getTmuxSocketName();
11061
- const args = socket ? ["-L", socket, "list-panes", "-t", windowId] : ["list-panes", "-t", windowId];
11062
- 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() });
11063
11958
  }
11064
11959
  catch {
11065
11960
  // Pane dead — respawn
@@ -11516,6 +12411,34 @@ export class FleetManager {
11516
12411
  }
11517
12412
  return true;
11518
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
+ }
11519
12442
  /**
11520
12443
  * #1085: coincident topic numbers must not route across Telegram groups.
11521
12444
  */
@@ -13152,6 +14075,11 @@ export class FleetManager {
13152
14075
  let result;
13153
14076
  switch (msg.type) {
13154
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
+ }
13155
14083
  // Prune expired decisions on create
13156
14084
  db.pruneExpiredDecisions();
13157
14085
  result = db.createDecision({
@@ -13162,7 +14090,7 @@ export class FleetManager {
13162
14090
  tags: payload.tags,
13163
14091
  ttl_days: payload.ttl_days,
13164
14092
  created_by: instanceName,
13165
- supersedes: payload.supersedes,
14093
+ supersedes: supersedes.id,
13166
14094
  });
13167
14095
  break;
13168
14096
  }
@@ -13339,6 +14267,21 @@ export class FleetManager {
13339
14267
  return "ok";
13340
14268
  }
13341
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
+ }
13342
14285
  async handleDecisionCrudHttp(instance, op, args) {
13343
14286
  if (!this.scheduler)
13344
14287
  return { error: "Scheduler not available" };
@@ -13354,16 +14297,24 @@ export class FleetManager {
13354
14297
  if (!title || !content)
13355
14298
  return { error: "title and content are required" };
13356
14299
  const scope = args.scope === "fleet" ? "fleet" : "project";
13357
- return db.createDecision({
13358
- project_root: projectRoot,
13359
- scope,
13360
- title,
13361
- content,
13362
- tags: asStrArr(args.tags),
13363
- ttl_days: asNum(args.ttl_days),
13364
- supersedes: asStr(args.supersedes),
13365
- created_by: instance,
13366
- });
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
+ }
13367
14318
  }
13368
14319
  // #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
13369
14320
  // The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
@@ -13406,6 +14357,8 @@ export class FleetManager {
13406
14357
  const asStatusFilter = normalizeStatusFilter;
13407
14358
  // #1336: small write ack — identifying fields only.
13408
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) });
13409
14362
  // The board's notes for the write (a send-back reason, a comment the assignee had not seen): with the ack when any.
13410
14363
  const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
13411
14364
  try {
@@ -13463,12 +14416,12 @@ export class FleetManager {
13463
14416
  const id = asStr(args.id);
13464
14417
  if (!id)
13465
14418
  return { error: "id is required" };
13466
- return noted(ack(db.updateTask(db.getTaskByPrefix(id).id, {
14419
+ return noted(updatedAck(db.updateTask(db.getTaskByPrefix(id).id, {
13467
14420
  status: asStatus(args.status),
13468
14421
  assignee: asStr(args.assignee),
13469
14422
  result: asStr(args.result),
13470
14423
  priority: asPriority(args.priority),
13471
- }, { kind: "instance", name: instance })));
14424
+ }, { kind: "instance", name: instance }), args));
13472
14425
  }
13473
14426
  default: return { error: `Unknown task action: ${action}` };
13474
14427
  }
@@ -13622,7 +14575,8 @@ export class FleetManager {
13622
14575
  result: payload.result,
13623
14576
  priority: payload.priority,
13624
14577
  }, { kind: "instance", name: meta.instance_name || instanceName });
13625
- 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) });
13626
14580
  break;
13627
14581
  default:
13628
14582
  throw new Error(`Unknown task action: ${action}`);
@@ -13652,16 +14606,101 @@ export class FleetManager {
13652
14606
  }
13653
14607
  return adapter.createTopic(topicName);
13654
14608
  }
13655
- async deleteForumTopic(topicId, adapterId) {
13656
- try {
13657
- const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
13658
- if (!adapter?.deleteTopic)
13659
- return;
13660
- await adapter.deleteTopic(topicId);
13661
- }
13662
- catch (err) {
13663
- this.logger.warn({ err, topicId }, "Failed to delete forum topic during rollback");
13664
- }
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() });
13665
14704
  }
13666
14705
  getForumTopicDeleter(adapterId) {
13667
14706
  const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
@@ -13940,6 +14979,8 @@ export class FleetManager {
13940
14979
  this.observeWebAuthority(); // #2453 review: so is a web-authority switch (off is always seen)
13941
14980
  if (!this.fleetConfig || !this.configPath)
13942
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);
13943
14984
  if (!this.savedFleetConfigSnapshot)
13944
14985
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
13945
14986
  normalizeNoticeDelivery(this.fleetConfig);
@@ -14062,9 +15103,12 @@ export class FleetManager {
14062
15103
  Object.assign(current, resolved);
14063
15104
  }
14064
15105
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
15106
+ this.nativeMcpStore?.observePolicies();
15107
+ this.agentCredentialsCache?.observePolicies();
14065
15108
  this.refreshSavedDashboardPermissions();
14066
15109
  this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
14067
15110
  this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
15111
+ this.observeHubSwitches(); // S-51: so does a saved Hub switch
14068
15112
  }
14069
15113
  /** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
14070
15114
  refreshSavedDashboardPermissions() {
@@ -14374,6 +15418,7 @@ export class FleetManager {
14374
15418
  const instanceDirToClean = this.getInstanceDir(name);
14375
15419
  // #1774: replies sent from here on are a later instance's (a replacement registered while this one goes); the ones
14376
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
14377
15422
  const removedBelow = this.webChatGeneration(name) + 1, removedDaemon = this.daemons.get(name);
14378
15423
  this.webChatGenerations.set(name, removedBelow);
14379
15424
  let settleRemoval;
@@ -16358,6 +17403,11 @@ export class FleetManager {
16358
17403
  needsYouItems() {
16359
17404
  return this.needsYou?.webItems() ?? [];
16360
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
+ }
16361
17411
  /**
16362
17412
  * #1389: the org chart's structure for the web — the instances the dashboard lists (getUiStatus's names), General,
16363
17413
  * fleet.yaml's teams, each instance's description and thread link. Read on demand; the live state rides the stream.
@@ -16421,6 +17471,7 @@ export class FleetManager {
16421
17471
  const fleet = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => !isWindowsInstance(cfg)).map(([name, cfg]) => ({
16422
17472
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
16423
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) } : {}),
16424
17475
  kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
16425
17476
  }));
16426
17477
  const taken = new Set(fleet.map(i => i.name));
@@ -16432,6 +17483,9 @@ export class FleetManager {
16432
17483
  claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
16433
17484
  profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
16434
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) } : {}),
16435
17489
  }));
16436
17490
  return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
16437
17491
  }
@@ -16592,7 +17646,8 @@ export class FleetManager {
16592
17646
  out.push({
16593
17647
  name, state: p.state ?? undefined,
16594
17648
  interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
16595
- interactionSummary: p.interaction_summary ?? null,
17649
+ interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
17650
+ kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
16596
17651
  pauseReason, pausedAt,
16597
17652
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
16598
17653
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
@@ -16684,8 +17739,18 @@ export class FleetManager {
16684
17739
  throw new Error("the delivery outbox is not open");
16685
17740
  return this.deliveryOutbox.acknowledge(deliveryId, by);
16686
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,
16687
17746
  tasks: () => this.needsYouTaskItems(),
16688
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
+ }),
16689
17754
  configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
16690
17755
  ...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
16691
17756
  })),
@@ -17602,7 +18667,7 @@ export class FleetManager {
17602
18667
  // Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
17603
18668
  ...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
17604
18669
  const host = this.fleetConfig?.web?.public_link?.hostname;
17605
- 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() };
17606
18671
  }
17607
18672
  authorizationRequests = null;
17608
18673
  /**
@@ -17665,8 +18730,9 @@ export class FleetManager {
17665
18730
  return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
17666
18731
  },
17667
18732
  signOut: (handle) => {
17668
- this.webSessions?.revokeByHandle(handle);
17669
- 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 };
17670
18736
  },
17671
18737
  event: (type, payload) => { try {
17672
18738
  this.eventLog?.insert("web", type, payload);
@@ -17679,6 +18745,7 @@ export class FleetManager {
17679
18745
  expired: (c) => t("web_auth.card_expired", c.label),
17680
18746
  refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
17681
18747
  revoked: (c) => t("web_auth.card_revoked", c.label),
18748
+ notSaved: (c) => t("web_auth.card_not_saved", c.label),
17682
18749
  ended: (c) => t("web_auth.card_ended", c.label),
17683
18750
  generalHint: () => t("web_auth.general_hint"),
17684
18751
  confirmLabel: () => t("web_auth.confirm"),
@@ -18601,7 +19668,14 @@ export class FleetManager {
18601
19668
  return;
18602
19669
  }
18603
19670
  const publication = this.beginCancelButtonPublication(instanceName, correlationId);
18604
- 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));
18605
19679
  try {
18606
19680
  const sent = await adapter.notifyAlert(chatId, {
18607
19681
  type: "cancel",
@@ -18624,7 +19698,7 @@ export class FleetManager {
18624
19698
  // Elapsed time is measured from when this button was posted — i.e. from
18625
19699
  // when the work was handed over — not from the pane's working transition,
18626
19700
  // which resets if the CLI blips idle mid-turn.
18627
- startedAt: Date.now(),
19701
+ startedAt: carriedStart ?? Date.now(),
18628
19702
  // Matches the text notifyAlert just posted, so the first 60s tick does
18629
19703
  // not re-edit identical text — which put a "(edited)" mark on Discord
18630
19704
  // with nothing visibly changed.
@@ -18747,14 +19821,16 @@ export class FleetManager {
18747
19821
  * Make a tool summary safe to paste into a channel message.
18748
19822
  *
18749
19823
  * The text is agent-controlled (it is built from tool inputs — file paths,
18750
- * shell commands), so it gets flattened to one line, capped, and stripped of
18751
- * the two Discord mass-mention triggers. Neither channel renders it with a
18752
- * parse mode, so no markup escaping is needed beyond that.
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.
18753
19829
  */
18754
19830
  static sanitizeActivity(activity) {
18755
19831
  if (!activity)
18756
19832
  return null;
18757
- 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();
18758
19834
  if (!flat)
18759
19835
  return null;
18760
19836
  return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
@@ -18873,6 +19949,12 @@ export class FleetManager {
18873
19949
  }
18874
19950
  return PROGRESS_MIN_ELAPSED_MS;
18875
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
+ }
18876
19958
  startProgressTicker(entry) {
18877
19959
  const tick = () => this.refreshBubble(entry);
18878
19960
  entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
@@ -18882,7 +19964,9 @@ export class FleetManager {
18882
19964
  // one additional edit per turn that lives past the threshold.
18883
19965
  const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
18884
19966
  if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
18885
- 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);
18886
19970
  firstTick.unref?.();
18887
19971
  }
18888
19972
  }
@@ -19221,6 +20305,8 @@ export class FleetManager {
19221
20305
  }
19222
20306
  catch { /* safe metadata only */ }
19223
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
19224
20310
  this.needsYou?.poke();
19225
20311
  if (!this.browserSignInOwnerCurrent(owner))
19226
20312
  return;
@@ -19259,7 +20345,10 @@ export class FleetManager {
19259
20345
  cancel: () => this.cancelLoginSession(),
19260
20346
  notice: ({ shown, label, handle }) => this.notifyFleetError(t("login.web_dashboard_notice", shown, label, handle.slice(0, 8))),
19261
20347
  // No details in the event: each page reads its own view (GET /ui/backend-login), as its session may see it.
19262
- 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(),
19263
20352
  });
19264
20353
  return this.webBackendLoginsInstance;
19265
20354
  }
@@ -19273,6 +20362,135 @@ export class FleetManager {
19273
20362
  * #2233: Settings → Agents' sign-in list — every backend's default login as the availability snapshot has it, then
19274
20363
  * each credential profile an agent uses (its own login state, read offline).
19275
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
+ }
19276
20494
  backendSignInTargets() {
19277
20495
  const out = [];
19278
20496
  for (const row of this.backendAvailabilityRows()) {
@@ -19284,6 +20502,7 @@ export class FleetManager {
19284
20502
  ...(flow?.remoteLogin === "unsupported" ? { hostCommand: flow.command } : {}),
19285
20503
  ...(row.state === "not_installed" && install ? { install } : {}),
19286
20504
  ...(row.state === "too_old" && install ? { update: install } : {}),
20505
+ ...(this.signInIdentity(row.backend) ? { identity: this.signInIdentity(row.backend) } : {}),
19287
20506
  });
19288
20507
  }
19289
20508
  const seen = new Set();
@@ -19301,7 +20520,7 @@ export class FleetManager {
19301
20520
  const signIn = dashboardSignInSupported(backend, profile);
19302
20521
  const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
19303
20522
  out.push({ backend, profile, state: login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown", signIn,
19304
- ...(hostCommand ? { hostCommand } : {}) });
20523
+ ...(hostCommand ? { hostCommand } : {}), ...(this.signInIdentity(backend, profile) ? { identity: this.signInIdentity(backend, profile) } : {}) });
19305
20524
  }
19306
20525
  return out;
19307
20526
  }
@@ -19820,9 +21039,12 @@ export class FleetManager {
19820
21039
  this.loginWindow.release(claim);
19821
21040
  return t("login.web_shutting_down");
19822
21041
  }
21042
+ // The wrapper says its own exit code after this marker: tmux may never record it (unattended-install.ts).
21043
+ const exitMarker = installExitMarker();
19823
21044
  const flow = {
19824
21045
  backend,
19825
- command: unattendedInstallCommand(info.unattended ?? info.install, shell),
21046
+ command: unattendedInstallCommand(info.unattended ?? info.install, shell, exitMarker),
21047
+ exitMarker,
19826
21048
  successPattern: /$^/,
19827
21049
  timeoutMs: this.installTimeoutMs,
19828
21050
  };
@@ -20123,6 +21345,8 @@ export class FleetManager {
20123
21345
  this.needsYou?.poke();
20124
21346
  const onError = (err) => this.logger.debug({ err }, "SSE client write failed; evicting");
20125
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);
20126
21350
  // A chat message: record it, and send it WITH its id so a reconnecting stream can ask for what it missed.
20127
21351
  const m = data;
20128
21352
  const recorded = this.webChatHistory.record({
@@ -20167,9 +21391,12 @@ export class FleetManager {
20167
21391
  this.taskBoardWaker?.stop();
20168
21392
  // #2303: each instance's cards at once, read at every claim (a fleet.yaml edit applies to the next one).
20169
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));
20170
21396
  const waker = this.taskBoardWaker = new TaskBoardWaker({
20171
21397
  deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
20172
21398
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
21399
+ boardOff: (name) => taskBoardOffIn(this.fleetConfig, name),
20173
21400
  // #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
20174
21401
  messagedAbout: (from, to, codes, sinceMs) => {
20175
21402
  const outbox = this.deliveryOutbox;
@@ -23233,6 +24460,9 @@ export class FleetManager {
23233
24460
  * settles, so a later genuine stop (after a restart) still does the work.
23234
24461
  */
23235
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();
23236
24466
  this.lifecycle.cancelNativeRecovery?.();
23237
24467
  this.nativeMcpStore?.closeAll();
23238
24468
  this.runtimeExecutor?.close();
@@ -23305,6 +24535,22 @@ export class FleetManager {
23305
24535
  // because we asked it to. Set synchronously — doStopAll runs to its first
23306
24536
  // await in the same tick as the signal handler, so no event can slip in.
23307
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
+ }));
23308
24554
  this.workingRecovery?.reset();
23309
24555
  if (this.workingRecoveryTimer)
23310
24556
  clearInterval(this.workingRecoveryTimer);
@@ -23335,6 +24581,17 @@ export class FleetManager {
23335
24581
  // #1565: the web chat's last changes reach the disk (bounded: a slow disk does not hold the shutdown).
23336
24582
  const webChatStopped = this.webChatStore ? Promise.race([this.webChatStore.flush().catch(() => { }), new Promise(r => setTimeout(r, 3_000).unref?.())]) : Promise.resolve();
23337
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();
23338
24595
  this.ipcStoppingInstances.add("__fleet_stopping__");
23339
24596
  // Release held delivery promises before awaiting daemon shutdown, then
23340
24597
  // reject spawn work which has not started. Otherwise a storm backoff could
@@ -23365,9 +24622,11 @@ export class FleetManager {
23365
24622
  // A login/install window is a dedicated tmux server with its own TTL
23366
24623
  // timer and HTTP listener living in THIS process: without an explicit
23367
24624
  // shutdown it would outlive us as an owner-less login CLI (sol B3).
24625
+ await inboundStopped;
23368
24626
  await publicStopped;
23369
24627
  await sharesStopped?.catch(() => { });
23370
24628
  await profileStopped;
24629
+ await hubStopped;
23371
24630
  await needsStopped;
23372
24631
  await settingsControlStopped;
23373
24632
  await cacheStopped;
@@ -23375,13 +24634,6 @@ export class FleetManager {
23375
24634
  await this.cpuProfileControl?.close();
23376
24635
  this.cpuProfileControl = null;
23377
24636
  await this.shutdownLoginWindows();
23378
- // Cancel adapter retry timers
23379
- for (const state of this.adapterState.values()) {
23380
- if (state.retryTimer) {
23381
- clearTimeout(state.retryTimer);
23382
- state.retryTimer = undefined;
23383
- }
23384
- }
23385
24637
  this.clearStatuslineWatchers();
23386
24638
  this.costGuard?.stop();
23387
24639
  this.dailySummary?.stop();
@@ -23419,6 +24671,15 @@ export class FleetManager {
23419
24671
  clearInterval(this.discordPresenceTimer);
23420
24672
  this.discordPresenceTimer = null;
23421
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);
23422
24683
  if (this.discordPresenceEagerTimer) {
23423
24684
  clearTimeout(this.discordPresenceEagerTimer);
23424
24685
  this.discordPresenceEagerTimer = null;
@@ -23705,6 +24966,7 @@ export class FleetManager {
23705
24966
  this.loadedFleetSource = previousSource;
23706
24967
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
23707
24968
  this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
24969
+ this.observeHubSwitches(); // S-51: and the Hub's
23708
24970
  this.logger.error({
23709
24971
  oldCount,
23710
24972
  newCount,
@@ -23724,6 +24986,8 @@ export class FleetManager {
23724
24986
  // mark every row done and tell the user "changes applied".
23725
24987
  return { rejected: why };
23726
24988
  }
24989
+ this.nativeMcpStore?.observePolicies();
24990
+ this.agentCredentialsCache?.observePolicies();
23727
24991
  if (this.loadedFleetSource)
23728
24992
  this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
23729
24993
  this.migrateOfficeStorage();
@@ -24624,6 +25888,9 @@ export class FleetManager {
24624
25888
  }
24625
25889
  async runConnectionBindingApply(job, binding, execution) {
24626
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");
24627
25894
  await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
24628
25895
  job.result = "applied";
24629
25896
  }
@@ -25794,8 +27061,13 @@ export class FleetManager {
25794
27061
  return;
25795
27062
  }
25796
27063
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
27064
+ const publicUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
25797
27065
  if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
25798
- && 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))
25799
27071
  return;
25800
27072
  // Branding is public, after Host admission: automatic favicon requests
25801
27073
  // must not become noisy web-token 401s in the browser console.
@@ -25824,7 +27096,7 @@ export class FleetManager {
25824
27096
  && String(req.headers.accept ?? "").includes("text/html")
25825
27097
  && isWebPageNavigation(requestPath)) {
25826
27098
  const link = gatewayRequestContext(req);
25827
- 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() });
25828
27100
  return;
25829
27101
  }
25830
27102
  res.writeHead(decision.status);
@@ -25996,6 +27268,7 @@ export class FleetManager {
25996
27268
  res.end(JSON.stringify({
25997
27269
  ...sysInfo,
25998
27270
  version: this.currentVersion,
27271
+ build: gatewayRequestContext(req) ? undefined : this.currentBuild,
25999
27272
  instances: enriched,
26000
27273
  publicLink: this.getPublicWebStatus(),
26001
27274
  }));
@@ -26493,6 +27766,8 @@ export class FleetManager {
26493
27766
  })(),
26494
27767
  capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
26495
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)),
26496
27771
  ...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
26497
27772
  machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
26498
27773
  display_name: identity.display_name || undefined,