@songsid/agend 2.3.4-beta.7 → 2.3.5-alpha.3

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 (457) hide show
  1. package/README.md +2 -0
  2. package/README.zh-TW.md +2 -0
  3. package/THIRD_PARTY_NOTICES.md +53 -0
  4. package/dist/agent-endpoint.d.ts +5 -1
  5. package/dist/agent-endpoint.js +12 -2
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/auth-api.d.ts +21 -0
  8. package/dist/auth-api.js +89 -2
  9. package/dist/auth-api.js.map +1 -1
  10. package/dist/authority/web-authority.d.ts +12 -2
  11. package/dist/authority/web-authority.js +25 -4
  12. package/dist/authority/web-authority.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +15 -12
  14. package/dist/backend/claude-code.js +152 -133
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-package.d.ts +5 -2
  17. package/dist/backend/codex-package.js +1 -5
  18. package/dist/backend/codex-package.js.map +1 -1
  19. package/dist/backend/codex.d.ts +136 -113
  20. package/dist/backend/codex.js +1281 -1219
  21. package/dist/backend/codex.js.map +1 -1
  22. package/dist/backend/grok-model-cache.d.ts +7 -0
  23. package/dist/backend/grok-model-cache.js +65 -0
  24. package/dist/backend/grok-model-cache.js.map +1 -0
  25. package/dist/backend/grok.d.ts +2 -0
  26. package/dist/backend/grok.js +22 -0
  27. package/dist/backend/grok.js.map +1 -1
  28. package/dist/backend/native-pane.d.ts +9 -0
  29. package/dist/backend/native-pane.js +59 -0
  30. package/dist/backend/native-pane.js.map +1 -0
  31. package/dist/backend/types.d.ts +7 -0
  32. package/dist/backend/types.js.map +1 -1
  33. package/dist/channel/ipc-bridge.d.ts +2 -0
  34. package/dist/channel/mcp-tools.js +1 -1
  35. package/dist/channel/mcp-tools.js.map +1 -1
  36. package/dist/cli.js +3 -0
  37. package/dist/cli.js.map +1 -1
  38. package/dist/config-validator.js +71 -1
  39. package/dist/config-validator.js.map +1 -1
  40. package/dist/config.js +16 -0
  41. package/dist/config.js.map +1 -1
  42. package/dist/daemon.d.ts +67 -5
  43. package/dist/daemon.js +823 -266
  44. package/dist/daemon.js.map +1 -1
  45. package/dist/delivery-consumed-watch.d.ts +15 -3
  46. package/dist/delivery-consumed-watch.js +23 -1
  47. package/dist/delivery-consumed-watch.js.map +1 -1
  48. package/dist/delivery-outbox.d.ts +5 -0
  49. package/dist/delivery-outbox.js +32 -4
  50. package/dist/delivery-outbox.js.map +1 -1
  51. package/dist/delivery-reconciliation.d.ts +2 -35
  52. package/dist/delivery-reconciliation.js +2 -156
  53. package/dist/delivery-reconciliation.js.map +1 -1
  54. package/dist/fleet-context.d.ts +2 -2
  55. package/dist/fleet-level-config.d.ts +1 -1
  56. package/dist/fleet-level-config.js +2 -0
  57. package/dist/fleet-level-config.js.map +1 -1
  58. package/dist/fleet-manager.d.ts +238 -3
  59. package/dist/fleet-manager.js +1826 -104
  60. package/dist/fleet-manager.js.map +1 -1
  61. package/dist/fleet-windows-runtime.d.ts +92 -0
  62. package/dist/fleet-windows-runtime.js +380 -0
  63. package/dist/fleet-windows-runtime.js.map +1 -0
  64. package/dist/general-binding.d.ts +85 -0
  65. package/dist/general-binding.js +298 -0
  66. package/dist/general-binding.js.map +1 -0
  67. package/dist/general-host-owner.d.ts +8 -0
  68. package/dist/general-host-owner.js +26 -0
  69. package/dist/general-host-owner.js.map +1 -0
  70. package/dist/general-instance.d.ts +7 -0
  71. package/dist/general-instance.js +20 -3
  72. package/dist/general-instance.js.map +1 -1
  73. package/dist/general-role.d.ts +5 -0
  74. package/dist/general-role.js +26 -0
  75. package/dist/general-role.js.map +1 -0
  76. package/dist/instance-config-impact.js +4 -0
  77. package/dist/instance-config-impact.js.map +1 -1
  78. package/dist/instance-lifecycle.d.ts +21 -3
  79. package/dist/instance-lifecycle.js +378 -162
  80. package/dist/instance-lifecycle.js.map +1 -1
  81. package/dist/instructions.d.ts +2 -0
  82. package/dist/instructions.js +6 -0
  83. package/dist/instructions.js.map +1 -1
  84. package/dist/local-pane-endpoint.d.ts +46 -0
  85. package/dist/local-pane-endpoint.js +189 -0
  86. package/dist/local-pane-endpoint.js.map +1 -0
  87. package/dist/local-pane-launcher.d.ts +2 -0
  88. package/dist/local-pane-launcher.js +38 -0
  89. package/dist/local-pane-launcher.js.map +1 -0
  90. package/dist/local-pane-stage.ps1 +101 -0
  91. package/dist/locale.js +85 -0
  92. package/dist/locale.js.map +1 -1
  93. package/dist/model-catalog-defaults.d.ts +14 -0
  94. package/dist/model-catalog-defaults.js +20 -0
  95. package/dist/model-catalog-defaults.js.map +1 -0
  96. package/dist/native-agent-admission.d.ts +7 -0
  97. package/dist/native-agent-admission.js +26 -0
  98. package/dist/native-agent-admission.js.map +1 -0
  99. package/dist/native-daemon-client.d.ts +13 -0
  100. package/dist/native-daemon-client.js +28 -0
  101. package/dist/native-daemon-client.js.map +1 -0
  102. package/dist/native-instance-api.d.ts +14 -0
  103. package/dist/native-instance-api.js +205 -0
  104. package/dist/native-instance-api.js.map +1 -0
  105. package/dist/native-instance-input.d.ts +84 -0
  106. package/dist/native-instance-input.js +54 -0
  107. package/dist/native-instance-input.js.map +1 -0
  108. package/dist/native-instance-registry.d.ts +32 -0
  109. package/dist/native-instance-registry.js +105 -0
  110. package/dist/native-instance-registry.js.map +1 -0
  111. package/dist/native-mcp.d.ts +40 -0
  112. package/dist/native-mcp.js +278 -0
  113. package/dist/native-mcp.js.map +1 -0
  114. package/dist/native-model-catalog.d.ts +5 -0
  115. package/dist/native-model-catalog.js +76 -0
  116. package/dist/native-model-catalog.js.map +1 -0
  117. package/dist/native-pane-crash-budget.d.ts +20 -0
  118. package/dist/native-pane-crash-budget.js +60 -0
  119. package/dist/native-pane-crash-budget.js.map +1 -0
  120. package/dist/native-pane-create.d.ts +26 -0
  121. package/dist/native-pane-create.js +53 -0
  122. package/dist/native-pane-create.js.map +1 -0
  123. package/dist/native-pane-lifecycle.d.ts +65 -0
  124. package/dist/native-pane-lifecycle.js +230 -0
  125. package/dist/native-pane-lifecycle.js.map +1 -0
  126. package/dist/native-pane-manager.d.ts +59 -0
  127. package/dist/native-pane-manager.js +584 -0
  128. package/dist/native-pane-manager.js.map +1 -0
  129. package/dist/native-pane-output-log.d.ts +15 -0
  130. package/dist/native-pane-output-log.js +108 -0
  131. package/dist/native-pane-output-log.js.map +1 -0
  132. package/dist/native-pane-reconciliation.d.ts +4 -0
  133. package/dist/native-pane-reconciliation.js +40 -0
  134. package/dist/native-pane-reconciliation.js.map +1 -0
  135. package/dist/native-pane-recovery.d.ts +12 -0
  136. package/dist/native-pane-recovery.js +10 -0
  137. package/dist/native-pane-recovery.js.map +1 -0
  138. package/dist/native-pane-tools.d.ts +2 -0
  139. package/dist/native-pane-tools.js +14 -0
  140. package/dist/native-pane-tools.js.map +1 -0
  141. package/dist/needs-you-hub.d.ts +2 -0
  142. package/dist/needs-you-hub.js +2 -1
  143. package/dist/needs-you-hub.js.map +1 -1
  144. package/dist/needs-you.d.ts +19 -1
  145. package/dist/needs-you.js +12 -2
  146. package/dist/needs-you.js.map +1 -1
  147. package/dist/outbound-handlers.d.ts +16 -2
  148. package/dist/outbound-handlers.js +67 -3
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +38 -0
  151. package/dist/outbound-schemas.js +12 -0
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/pane-port.d.ts +118 -0
  154. package/dist/pane-port.js +2 -0
  155. package/dist/pane-port.js.map +1 -0
  156. package/dist/public-web-gateway.js +9 -0
  157. package/dist/public-web-gateway.js.map +1 -1
  158. package/dist/public-web-link.d.ts +5 -0
  159. package/dist/public-web-link.js +4 -0
  160. package/dist/public-web-link.js.map +1 -1
  161. package/dist/quickstart-api.js +9 -5
  162. package/dist/quickstart-api.js.map +1 -1
  163. package/dist/quickstart.d.ts +1 -0
  164. package/dist/quickstart.js +4 -1
  165. package/dist/quickstart.js.map +1 -1
  166. package/dist/runtime-api.d.ts +8 -0
  167. package/dist/runtime-api.js +96 -0
  168. package/dist/runtime-api.js.map +1 -0
  169. package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +21 -0
  170. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
  171. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
  172. package/dist/runtime-assets/@xterm/addon-serialize/package.json +29 -0
  173. package/dist/runtime-assets/@xterm/headless/LICENSE +21 -0
  174. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +2 -0
  175. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
  176. package/dist/runtime-assets/@xterm/headless/package.json +29 -0
  177. package/dist/runtime-assets/node-pty/CONPTY-LICENSE +21 -0
  178. package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +503 -0
  179. package/dist/runtime-assets/node-pty/LICENSE +69 -0
  180. package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +16 -0
  181. package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +47 -0
  182. package/dist/runtime-assets/node-pty/lib/index.js +52 -0
  183. package/dist/runtime-assets/node-pty/lib/interfaces.js +7 -0
  184. package/dist/runtime-assets/node-pty/lib/shared/conout.js +11 -0
  185. package/dist/runtime-assets/node-pty/lib/terminal.js +190 -0
  186. package/dist/runtime-assets/node-pty/lib/types.js +7 -0
  187. package/dist/runtime-assets/node-pty/lib/unixTerminal.js +346 -0
  188. package/dist/runtime-assets/node-pty/lib/utils.js +39 -0
  189. package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +125 -0
  190. package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +320 -0
  191. package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +199 -0
  192. package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  193. package/dist/runtime-assets/node-pty/package.json +64 -0
  194. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  195. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  196. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  197. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  198. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  199. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  200. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  201. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  202. package/dist/runtime-bundle.d.ts +17 -0
  203. package/dist/runtime-bundle.js +150 -0
  204. package/dist/runtime-bundle.js.map +1 -0
  205. package/dist/runtime-cli.d.ts +2 -0
  206. package/dist/runtime-cli.js +117 -0
  207. package/dist/runtime-cli.js.map +1 -0
  208. package/dist/runtime-client-store.d.ts +49 -0
  209. package/dist/runtime-client-store.js +292 -0
  210. package/dist/runtime-client-store.js.map +1 -0
  211. package/dist/runtime-client.d.ts +100 -0
  212. package/dist/runtime-client.js +700 -0
  213. package/dist/runtime-client.js.map +1 -0
  214. package/dist/runtime-coordinator.d.ts +82 -0
  215. package/dist/runtime-coordinator.js +864 -0
  216. package/dist/runtime-coordinator.js.map +1 -0
  217. package/dist/runtime-general-protocol.d.ts +22 -0
  218. package/dist/runtime-general-protocol.js +57 -0
  219. package/dist/runtime-general-protocol.js.map +1 -0
  220. package/dist/runtime-general-workspace.d.ts +17 -0
  221. package/dist/runtime-general-workspace.js +213 -0
  222. package/dist/runtime-general-workspace.js.map +1 -0
  223. package/dist/runtime-pane-assets.d.ts +17 -0
  224. package/dist/runtime-pane-assets.js +23 -0
  225. package/dist/runtime-pane-assets.js.map +1 -0
  226. package/dist/runtime-pane-boot.d.ts +2 -0
  227. package/dist/runtime-pane-boot.js +24 -0
  228. package/dist/runtime-pane-boot.js.map +1 -0
  229. package/dist/runtime-pane-codex-index.d.ts +1 -0
  230. package/dist/runtime-pane-codex-index.js +47 -0
  231. package/dist/runtime-pane-codex-index.js.map +1 -0
  232. package/dist/runtime-pane-config.d.ts +16 -0
  233. package/dist/runtime-pane-config.js +54 -0
  234. package/dist/runtime-pane-config.js.map +1 -0
  235. package/dist/runtime-pane-host.d.ts +44 -0
  236. package/dist/runtime-pane-host.js +424 -0
  237. package/dist/runtime-pane-host.js.map +1 -0
  238. package/dist/runtime-pane-job-owner.d.ts +1 -0
  239. package/dist/runtime-pane-job-owner.js +29 -0
  240. package/dist/runtime-pane-job-owner.js.map +1 -0
  241. package/dist/runtime-pane-job.ps1 +116 -0
  242. package/dist/runtime-pane-link.d.ts +40 -0
  243. package/dist/runtime-pane-link.js +246 -0
  244. package/dist/runtime-pane-link.js.map +1 -0
  245. package/dist/runtime-pane-mcp-protocol.d.ts +32 -0
  246. package/dist/runtime-pane-mcp-protocol.js +39 -0
  247. package/dist/runtime-pane-mcp-protocol.js.map +1 -0
  248. package/dist/runtime-pane-mcp-relay.d.ts +17 -0
  249. package/dist/runtime-pane-mcp-relay.js +70 -0
  250. package/dist/runtime-pane-mcp-relay.js.map +1 -0
  251. package/dist/runtime-pane-mcp.d.ts +51 -0
  252. package/dist/runtime-pane-mcp.js +150 -0
  253. package/dist/runtime-pane-mcp.js.map +1 -0
  254. package/dist/runtime-pane-native.d.ts +45 -0
  255. package/dist/runtime-pane-native.js +65 -0
  256. package/dist/runtime-pane-native.js.map +1 -0
  257. package/dist/runtime-pane-output.d.ts +19 -0
  258. package/dist/runtime-pane-output.js +53 -0
  259. package/dist/runtime-pane-output.js.map +1 -0
  260. package/dist/runtime-pane-port.d.ts +139 -0
  261. package/dist/runtime-pane-port.js +558 -0
  262. package/dist/runtime-pane-port.js.map +1 -0
  263. package/dist/runtime-pane-private.d.ts +14 -0
  264. package/dist/runtime-pane-private.js +51 -0
  265. package/dist/runtime-pane-private.js.map +1 -0
  266. package/dist/runtime-pane-private.ps1 +14 -0
  267. package/dist/runtime-pane-process.d.ts +4 -0
  268. package/dist/runtime-pane-process.js +108 -0
  269. package/dist/runtime-pane-process.js.map +1 -0
  270. package/dist/runtime-pane-protocol.d.ts +172 -0
  271. package/dist/runtime-pane-protocol.js +195 -0
  272. package/dist/runtime-pane-protocol.js.map +1 -0
  273. package/dist/runtime-pane-screen.d.ts +19 -0
  274. package/dist/runtime-pane-screen.js +76 -0
  275. package/dist/runtime-pane-screen.js.map +1 -0
  276. package/dist/runtime-pane-service.d.ts +11 -0
  277. package/dist/runtime-pane-service.js +221 -0
  278. package/dist/runtime-pane-service.js.map +1 -0
  279. package/dist/runtime-pane-stdio-protocol.d.ts +54 -0
  280. package/dist/runtime-pane-stdio-protocol.js +94 -0
  281. package/dist/runtime-pane-stdio-protocol.js.map +1 -0
  282. package/dist/runtime-pane-stdio.d.ts +3 -0
  283. package/dist/runtime-pane-stdio.js +123 -0
  284. package/dist/runtime-pane-stdio.js.map +1 -0
  285. package/dist/runtime-pane-store.d.ts +33 -0
  286. package/dist/runtime-pane-store.js +164 -0
  287. package/dist/runtime-pane-store.js.map +1 -0
  288. package/dist/runtime-pane-transcript-protocol.d.ts +43 -0
  289. package/dist/runtime-pane-transcript-protocol.js +58 -0
  290. package/dist/runtime-pane-transcript-protocol.js.map +1 -0
  291. package/dist/runtime-pane-transcript.d.ts +45 -0
  292. package/dist/runtime-pane-transcript.js +605 -0
  293. package/dist/runtime-pane-transcript.js.map +1 -0
  294. package/dist/runtime-pane-worker.d.ts +1 -0
  295. package/dist/runtime-pane-worker.js +154 -0
  296. package/dist/runtime-pane-worker.js.map +1 -0
  297. package/dist/runtime-protocol.d.ts +149 -0
  298. package/dist/runtime-protocol.js +19 -0
  299. package/dist/runtime-protocol.js.map +1 -0
  300. package/dist/runtime-switches.d.ts +79 -0
  301. package/dist/runtime-switches.js +90 -0
  302. package/dist/runtime-switches.js.map +1 -0
  303. package/dist/runtime-transcript-delta-reader.d.ts +23 -0
  304. package/dist/runtime-transcript-delta-reader.js +74 -0
  305. package/dist/runtime-transcript-delta-reader.js.map +1 -0
  306. package/dist/runtime-transcript-source.d.ts +36 -0
  307. package/dist/runtime-transcript-source.js +189 -0
  308. package/dist/runtime-transcript-source.js.map +1 -0
  309. package/dist/runtime-windows-bridge.d.ts +41 -0
  310. package/dist/runtime-windows-bridge.js +157 -0
  311. package/dist/runtime-windows-bridge.js.map +1 -0
  312. package/dist/settings-api.d.ts +5 -0
  313. package/dist/settings-api.js +95 -1
  314. package/dist/settings-api.js.map +1 -1
  315. package/dist/settings-baseline.d.ts +1 -0
  316. package/dist/settings-baseline.js +1 -1
  317. package/dist/settings-baseline.js.map +1 -1
  318. package/dist/settings-change.js +28 -0
  319. package/dist/settings-change.js.map +1 -1
  320. package/dist/settings-confirmation.d.ts +16 -1
  321. package/dist/settings-confirmation.js +47 -9
  322. package/dist/settings-confirmation.js.map +1 -1
  323. package/dist/settings-effect.d.ts +4 -0
  324. package/dist/settings-effect.js +58 -1
  325. package/dist/settings-effect.js.map +1 -1
  326. package/dist/settings-http-confirmation.d.ts +1 -0
  327. package/dist/settings-http-confirmation.js +15 -2
  328. package/dist/settings-http-confirmation.js.map +1 -1
  329. package/dist/settings-request-capability.d.ts +2 -0
  330. package/dist/settings-request-capability.js +4 -0
  331. package/dist/settings-request-capability.js.map +1 -1
  332. package/dist/settings-route-lifetime.d.ts +14 -0
  333. package/dist/settings-route-lifetime.js +48 -0
  334. package/dist/settings-route-lifetime.js.map +1 -0
  335. package/dist/settings-transaction.d.ts +4 -0
  336. package/dist/settings-transaction.js +9 -0
  337. package/dist/settings-transaction.js.map +1 -1
  338. package/dist/setup-form.js +6 -3
  339. package/dist/setup-form.js.map +1 -1
  340. package/dist/setup-i18n.js +3 -0
  341. package/dist/setup-i18n.js.map +1 -1
  342. package/dist/tmux-control.d.ts +2 -1
  343. package/dist/tmux-control.js.map +1 -1
  344. package/dist/tmux-manager.d.ts +2 -1
  345. package/dist/tmux-manager.js.map +1 -1
  346. package/dist/tmux-pane-host.d.ts +26 -0
  347. package/dist/tmux-pane-host.js +25 -0
  348. package/dist/tmux-pane-host.js.map +1 -0
  349. package/dist/tool-permissions.js +2 -1
  350. package/dist/tool-permissions.js.map +1 -1
  351. package/dist/topic-commands.js +3 -0
  352. package/dist/topic-commands.js.map +1 -1
  353. package/dist/transcript-delta-reader.d.ts +1 -1
  354. package/dist/transcript-delta-reader.js +1 -1
  355. package/dist/transcript-delta-reader.js.map +1 -1
  356. package/dist/transcript-monitor.d.ts +3 -0
  357. package/dist/transcript-monitor.js +22 -46
  358. package/dist/transcript-monitor.js.map +1 -1
  359. package/dist/transcript-needles.d.ts +35 -0
  360. package/dist/transcript-needles.js +158 -0
  361. package/dist/transcript-needles.js.map +1 -0
  362. package/dist/transcript-records.d.ts +21 -0
  363. package/dist/transcript-records.js +140 -0
  364. package/dist/transcript-records.js.map +1 -0
  365. package/dist/transcript-sources.d.ts +5 -0
  366. package/dist/transcript-sources.js +8 -79
  367. package/dist/transcript-sources.js.map +1 -1
  368. package/dist/types.d.ts +23 -0
  369. package/dist/ui/agent-proposals.js +90 -0
  370. package/dist/ui/agent-runtime.js +106 -0
  371. package/dist/ui/chat-store.js +13 -2
  372. package/dist/ui/chat-strings.js +48 -0
  373. package/dist/ui/cli-picker.js +42 -0
  374. package/dist/ui/general-location.js +71 -0
  375. package/dist/ui/native-instance-settings.js +47 -0
  376. package/dist/ui/native-pane-recovery.js +45 -0
  377. package/dist/ui/panel-chat.js +63 -32
  378. package/dist/ui/panel-details.js +2 -1
  379. package/dist/ui/panel-fleet.js +99 -18
  380. package/dist/ui/panel-needs.js +154 -3
  381. package/dist/ui/panel-runtimes.js +231 -0
  382. package/dist/ui/panel-settings.js +19 -9
  383. package/dist/ui/panel-windows-agent.js +86 -0
  384. package/dist/ui/panel-windows-cli.js +143 -0
  385. package/dist/ui/provider-marks.js +83 -0
  386. package/dist/ui/runtime-catalog.js +55 -0
  387. package/dist/ui/settings-catalog.js +37 -10
  388. package/dist/ui/settings-confirm.js +3 -1
  389. package/dist/ui/settings-dialogs.js +22 -7
  390. package/dist/ui/settings-model.js +2 -1
  391. package/dist/ui/settings-outside.js +176 -0
  392. package/dist/ui/settings-registry.js +6 -0
  393. package/dist/ui/settings-strings.js +16 -0
  394. package/dist/ui/shared/app-i18n.js +2 -2
  395. package/dist/ui/shared/app-route.js +6 -2
  396. package/dist/ui/shared/app-shell.js +3 -2
  397. package/dist/ui/shared/app-store.js +1 -0
  398. package/dist/ui/shared/app-stream.js +3 -1
  399. package/dist/ui/shared/app.css +301 -0
  400. package/dist/ui/shared/app.js +4 -0
  401. package/dist/ui/shared/header-tools.js +6 -4
  402. package/dist/ui/shared/instance-nav.js +33 -7
  403. package/dist/ui/shared/instance-switch.js +4 -3
  404. package/dist/ui/shared/qrcode.module.js +2237 -0
  405. package/dist/ui/shared/signin.js +39 -5
  406. package/dist/ui/shared/ui-menu.js +28 -8
  407. package/dist/ui/shared/vendor/qrcode-generator.LICENSE +27 -0
  408. package/dist/ui/shared/vendor/vendor.json +19 -2
  409. package/dist/ui/shared/view-strings.js +8 -0
  410. package/dist/ui/windows-agents-nav.js +2 -0
  411. package/dist/ui/windows-cli-catalog.js +79 -0
  412. package/dist/web-api.d.ts +37 -0
  413. package/dist/web-api.js +248 -6
  414. package/dist/web-api.js.map +1 -1
  415. package/dist/web-chat-history.d.ts +19 -2
  416. package/dist/web-chat-history.js +72 -3
  417. package/dist/web-chat-history.js.map +1 -1
  418. package/dist/web-only-fleet.d.ts +20 -0
  419. package/dist/web-only-fleet.js +21 -0
  420. package/dist/web-only-fleet.js.map +1 -1
  421. package/dist/web-remote-access.d.ts +152 -0
  422. package/dist/web-remote-access.js +368 -0
  423. package/dist/web-remote-access.js.map +1 -0
  424. package/dist/web-session.d.ts +7 -0
  425. package/dist/web-session.js +2 -0
  426. package/dist/web-session.js.map +1 -1
  427. package/dist/web-shell-routes.d.ts +4 -1
  428. package/dist/web-shell-routes.js +7 -2
  429. package/dist/web-shell-routes.js.map +1 -1
  430. package/dist/windows-agents.d.ts +124 -0
  431. package/dist/windows-agents.js +487 -0
  432. package/dist/windows-agents.js.map +1 -0
  433. package/dist/windows-cli-api.d.ts +23 -0
  434. package/dist/windows-cli-api.js +158 -0
  435. package/dist/windows-cli-api.js.map +1 -0
  436. package/dist/windows-cli-providers.d.ts +115 -0
  437. package/dist/windows-cli-providers.js +429 -0
  438. package/dist/windows-cli-providers.js.map +1 -0
  439. package/dist/windows-cli-worker.d.ts +58 -0
  440. package/dist/windows-cli-worker.js +479 -0
  441. package/dist/windows-cli-worker.js.map +1 -0
  442. package/dist/windows-cli.d.ts +115 -0
  443. package/dist/windows-cli.js +402 -0
  444. package/dist/windows-cli.js.map +1 -0
  445. package/dist/windows-native-mcp.d.ts +19 -0
  446. package/dist/windows-native-mcp.js +143 -0
  447. package/dist/windows-native-mcp.js.map +1 -0
  448. package/dist/windows-pane-mcp.d.ts +26 -0
  449. package/dist/windows-pane-mcp.js +187 -0
  450. package/dist/windows-pane-mcp.js.map +1 -0
  451. package/package.json +9 -3
  452. package/templates/runtime-assets/conpty-LICENSE +21 -0
  453. package/templates/runtime-assets/conpty-NOTICE.md +503 -0
  454. package/templates/runtime-assets/local-pane-stage.ps1 +101 -0
  455. package/templates/runtime-assets/runtime-pane-job.ps1 +116 -0
  456. package/templates/runtime-assets/runtime-pane-private.ps1 +14 -0
  457. package/templates/runtime-assets/xterm-LICENSE +21 -0
@@ -1,3 +1,8 @@
1
+ import { buildNativeFleetInstructions } from "./instructions.js";
2
+ import { GENERAL_ROLE_INSTRUCTIONS } from "./general-role.js";
3
+ import { NativePaneRecoveryError } from "./native-pane-recovery.js";
4
+ import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
5
+ import { windowsAgentsFor } from "./windows-agents.js";
1
6
  import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
2
7
  import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
3
8
  import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
@@ -9,10 +14,16 @@ import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecovery
9
14
  import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
10
15
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
11
16
  import { SettingsBaselines } from "./settings-baseline.js";
12
- import { SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
17
+ import { prepareSettingsEffect } from "./settings-effect.js";
18
+ import { settingsDisplay } from "./settings-change.js";
19
+ import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
13
20
  import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
14
21
  import { SettingsControlServer } from "./settings-control.js";
15
- import { SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
22
+ import { SettingsRouteLifetime } from "./settings-route-lifetime.js";
23
+ import { GeneralManagedFileError } from "./runtime-general-protocol.js";
24
+ import { GeneralBindings, generalBindingRevision, parseGeneralSwitchInput, switchGeneralLocation } from "./general-binding.js";
25
+ import { confirmGeneralHostExit } from "./general-host-owner.js";
26
+ import { settingsFingerprint, SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
16
27
  import { performance } from "node:perf_hooks";
17
28
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
18
29
  import { gatewayRequestContext } from "./web-request-context.js";
@@ -42,7 +53,7 @@ import { ProfileControlServer } from "./profile-control.js";
42
53
  import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
43
54
  import { atomicWriteFileSync } from "./atomic-write.js";
44
55
  import { createHash, randomBytes, randomUUID } from "node:crypto";
45
- import { freemem, totalmem, homedir } from "node:os";
56
+ import { freemem, totalmem, homedir, hostname } from "node:os";
46
57
  import { effectiveCpuCount } from "./cpu-quota.js";
47
58
  import { createServer } from "node:http";
48
59
  import { Worker } from "node:worker_threads";
@@ -84,7 +95,8 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
84
95
  import { TmuxManager } from "./tmux-manager.js";
85
96
  import { AccessManager } from "./channel/access-manager.js";
86
97
  import { IpcClient } from "./channel/ipc-bridge.js";
87
- import { usesChannelAddress } from "./channel/mcp-tools.js";
98
+ import { NativeDaemonClient } from "./native-daemon-client.js";
99
+ import { usesChannelAddress, TOOLS } from "./channel/mcp-tools.js";
88
100
  import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
89
101
  import { formatFleetTime } from "./tz-utils.js";
90
102
  import { createAdapter } from "./channel/factory.js";
@@ -94,6 +106,7 @@ import { createBackend } from "./backend/factory.js";
94
106
  import { readEffortMetadata } from "./backend/effort-metadata.js";
95
107
  import { codexEffort, flagEffort } from "./backend/launch-effort.js";
96
108
  import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
109
+ import { readGrokModelCache } from "./backend/grok-model-cache.js";
97
110
  import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
98
111
  import { createLogger, rotateLogIfNeeded } from "./logger.js";
99
112
  import { processAttachments } from "./channel/attachment-handler.js";
@@ -168,6 +181,16 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
168
181
  return tasks;
169
182
  }
170
183
  import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
184
+ import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
185
+ import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
186
+ import { RuntimeCoordinator } from "./runtime-coordinator.js";
187
+ import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
188
+ import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
189
+ import { NativeMcpSessions } from "./native-mcp.js";
190
+ import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
191
+ import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED } from "./native-instance-registry.js";
192
+ import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
193
+ import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
171
194
  import { LogService } from "./log-service.js";
172
195
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
173
196
  import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
@@ -269,6 +292,7 @@ import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninP
269
292
  import { isWebPageNavigation } from "./web-shell-routes.js";
270
293
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
271
294
  import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
295
+ 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";
272
296
  import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
273
297
  import { AuthorityStateStore } from "./authority/authority-state.js";
274
298
  import { SessionAuthorizations } from "./authority/session-authorizations.js";
@@ -737,6 +761,825 @@ export class FleetManager {
737
761
  /** Live view of lifecycle.daemons — used throughout; not deprecated. */
738
762
  get daemons() { return this.lifecycle.daemons; }
739
763
  fleetConfig = null;
764
+ fleetWindowsRuntime = null;
765
+ nativeMcpStore = null;
766
+ paneManager = null;
767
+ localPaneRelay = null;
768
+ openingLocalPane = null;
769
+ runtimeHub = null;
770
+ runtimeExecutor = null;
771
+ runtimeStatusScheduled = false;
772
+ reconciledRemoteResults = new Set();
773
+ /** Machine credentials are separate from dashboard sessions and per-turn agent capabilities. */
774
+ runtimeCoordinator() {
775
+ if (!remoteRuntimeEnabled())
776
+ throw new Error("Remote runtimes are not enabled");
777
+ return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
778
+ handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
779
+ handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
780
+ changed: event => {
781
+ if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
782
+ for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
783
+ if (config.machine_id === event.machineId) {
784
+ this.nativeMcpStore?.closeInstance(name);
785
+ if (config.general_topic) {
786
+ this.cancelPendingDeliveries(name);
787
+ this.holdGeneralOldDeliveries(name);
788
+ }
789
+ }
790
+ this.needsYou?.poke();
791
+ }
792
+ if (this.runtimeStatusScheduled || this.shuttingDown)
793
+ return;
794
+ this.runtimeStatusScheduled = true;
795
+ queueMicrotask(() => {
796
+ this.runtimeStatusScheduled = false;
797
+ if (this.shuttingDown)
798
+ return;
799
+ try {
800
+ this.fleetWindowsRuntime?.importHistory();
801
+ for (const snapshot of this.runtimeHub?.tasks() ?? [])
802
+ this.reconcileRemoteDelivery(snapshot.machineId, snapshot.task);
803
+ this.emitSseEvent("status", this.getUiStatus());
804
+ this.scheduleDeliveryOutboxPump();
805
+ }
806
+ catch (error) {
807
+ this.logger.warn({ err: error }, "Runtime status reconciliation failed");
808
+ }
809
+ });
810
+ },
811
+ });
812
+ }
813
+ runtimeWindowsExecutor() {
814
+ return this.runtimeExecutor ??= new RuntimeWindowsBridge(this, () => remoteRuntimeEnabled() && !this.shuttingDown ? this.runtimeCoordinator() : null);
815
+ }
816
+ /** Per-turn capabilities for native MCP, separate from browser and Linux-agent credentials. */
817
+ nativeMcpSessions() {
818
+ return this.nativeMcpStore ??= new NativeMcpSessions(this);
819
+ }
820
+ /** The same instance registry and delivery entry point, with a native Windows execution adapter. */
821
+ nativeInstanceRuntime() {
822
+ return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
823
+ owner: this, fleet: () => this.fleetConfig, save: () => this.saveFleetConfig(), stopping: () => this.shuttingDown, history: this.webChatHistory,
824
+ openMcp: (instance, requestId, sandbox, stillCurrent) => this.healthServerListening
825
+ ? this.nativeMcpSessions().grant({ instance, requestId, sandbox, stillCurrent, endpoint: `http://127.0.0.1:${this.fleetConfig?.health_port ?? 19280}/agent` }) : null,
826
+ warn: (message, error) => this.logger.warn({ err: error }, message),
827
+ removed: name => this.nativeInstanceRemoved(name),
828
+ paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
829
+ machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
830
+ validateMachine: (id, cli, sandbox) => {
831
+ const machine = this.runtimeCoordinator().machine(id);
832
+ const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
833
+ if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
834
+ throw new Error("Choose an online runtime and an available CLI");
835
+ if (sandbox === "read-only" && !provider.readOnlySupport)
836
+ throw new Error("This runtime CLI cannot enforce read-only execution");
837
+ },
838
+ emit: (event, data) => {
839
+ this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
840
+ if (event === "activity")
841
+ this.scheduleDeliveryOutboxPump();
842
+ },
843
+ }, { bridge: this.runtimeWindowsExecutor() });
844
+ }
845
+ /** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
846
+ async createNativePaneInstance(profile, channelId, authorized, execution) {
847
+ const { createNativePane } = await import("./native-pane-create.js");
848
+ return createNativePane({ fleet: () => this.fleetConfig, runtime: this.nativeInstanceRuntime(),
849
+ topic: target => {
850
+ const world = this.worlds.get(target.id), adapter = world?.adapter;
851
+ if (!world || !adapter?.createTopic || !adapter.deleteTopic)
852
+ throw Error("The approved connection cannot create and compensate a topic");
853
+ const same = () => {
854
+ const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
855
+ const matches = channels.filter(row => (row.id ?? row.type) === target.id);
856
+ return !this.shuttingDown && this.worlds.get(target.id) === world && world.adapter === adapter && matches.length === 1 && matches[0].type === target.type && matches[0].group_id === target.group_id;
857
+ };
858
+ return { current: same, create: name => adapter.createTopic(name), remove: id => adapter.deleteTopic(id) };
859
+ },
860
+ epoch: name => this.lifecycle.epochOf(name),
861
+ start: async (name, config, current) => {
862
+ if (!current())
863
+ throw Error("Native creation expired before start");
864
+ await this.startInstance(name, config, true);
865
+ if (!current())
866
+ throw Error("Native creation expired during start");
867
+ await this.connectIpcToInstance(name);
868
+ if (!current())
869
+ throw Error("Native creation expired during channel connection");
870
+ },
871
+ close: (name, config, epoch) => this.lifecycle.runTransition(name, async (transition) => {
872
+ if (this.fleetConfig?.instances[name] !== config || this.lifecycle.epochOf(name) !== epoch)
873
+ return false;
874
+ await this.stopInstance(name, transition);
875
+ return this.fleetConfig?.instances[name] === config && this.lifecycle.epochOf(name) === epoch + 1;
876
+ }),
877
+ routes: () => { if (this.fleetConfig)
878
+ this.routing.rebuild(this.fleetConfig); },
879
+ warn: error => this.logger.warn({ err: error }, "Native creation cleanup is held; retained identity and topic require attention"), }, profile, channelId, () => !this.shuttingDown && authorized(), execution);
880
+ }
881
+ /** Admin-only settings path: validate first, then close the exact old launch before changing cold fields. */
882
+ async updateNativePaneConfig(name, patch, authorized, transaction) {
883
+ return this.lifecycle.runTransition(name, async (transition) => {
884
+ const before = this.fleetConfig?.instances[name];
885
+ if (!isNativePaneInstance(before))
886
+ throw Error("Persistent Windows instance not found");
887
+ const runtime = this.nativeInstanceRuntime(), generation = this.explicitStopGeneration.get(name) ?? 0;
888
+ let expectedGeneration = generation;
889
+ const current = () => authorized() && !this.shuttingDown && this.fleetConfig?.instances[name] === before
890
+ && (this.explicitStopGeneration.get(name) ?? 0) === expectedGeneration
891
+ && (before.machine_id ? remoteRuntimeEnabled() : localWindowsCliEnabled());
892
+ if (!current())
893
+ throw Error("Native settings authority expired");
894
+ runtime.previewUpdate(name, patch);
895
+ const cold = ["working_directory", "model", "systemPrompt", "native_sandbox"].some(key => Object.hasOwn(patch, key) && (patch[key] ?? "") !== (before[key] ?? ""));
896
+ const running = this.daemons.get(name);
897
+ if (cold) {
898
+ if (running && !running.nativePaneHasExited() && (!await running.nativePauseReady({ current, accepts: () => true, revalidate: async () => current() }) || !running.nativePauseStillReady()))
899
+ throw Error("Wait for this Windows agent to be idle before changing its launch settings");
900
+ if (!current())
901
+ throw Error("Native settings authority expired before closing its old pane");
902
+ await this.stopInstance(name, transition);
903
+ expectedGeneration = generation + 1;
904
+ if (!current())
905
+ throw Error("Native settings authority expired after closing its old pane");
906
+ if (runtime.get(name)?.activeId)
907
+ throw Error("Native pane exit is unconfirmed; its settings were not changed");
908
+ }
909
+ const result = runtime.update(name, patch, current, transaction), next = this.fleetConfig.instances[name];
910
+ if (cold && running) {
911
+ if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== next || (this.explicitStopGeneration.get(name) ?? 0) !== expectedGeneration)
912
+ throw Error("Native settings saved; restart authority expired");
913
+ await this.startInstance(name, next, true, "fleet-topic", false, transition);
914
+ }
915
+ return result;
916
+ });
917
+ }
918
+ async localNativePaneEndpoint() {
919
+ const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
920
+ throw Error("Local Windows runtime is switched off or draining"); };
921
+ admit();
922
+ if (this.localPaneDrain)
923
+ throw Error("Local Windows runtime cleanup is pending");
924
+ if (this.localPaneRelay?.online())
925
+ return this.localPaneRelay;
926
+ if (this.openingLocalPane)
927
+ return this.openingLocalPane;
928
+ const opening = (async () => {
929
+ const [{ LocalPaneEndpoint }, { launchLocalPaneRelay, localPaneScope }, inventory] = await Promise.all([
930
+ import("./local-pane-endpoint.js"), import("./local-pane-launcher.js"), this.runtimeWindowsExecutor().probe({ force: false }),
931
+ ]);
932
+ admit();
933
+ this.localPaneRelay?.stop();
934
+ const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
935
+ diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
936
+ version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
937
+ launch: () => launchLocalPaneRelay(this.dataDir, admit),
938
+ mcp: async (body, current) => (await this.nativePanes()).handle("local", body, current),
939
+ });
940
+ try {
941
+ admit();
942
+ this.localPaneRelay = relay;
943
+ return relay;
944
+ }
945
+ catch (error) {
946
+ relay.stop();
947
+ throw error;
948
+ }
949
+ })();
950
+ this.openingLocalPane = opening;
951
+ try {
952
+ return await opening;
953
+ }
954
+ finally {
955
+ if (this.openingLocalPane === opening)
956
+ this.openingLocalPane = null;
957
+ }
958
+ }
959
+ /** Persistent ownership is independent of the exec worker's per-turn capabilities. */
960
+ async nativePanes() {
961
+ if (this.paneManager)
962
+ return this.paneManager;
963
+ const { NativePaneManager } = await import("./native-pane-manager.js");
964
+ if (this.paneManager)
965
+ return this.paneManager;
966
+ const store = windowsAgentsFor(this, this.runtimeWindowsExecutor());
967
+ const allowed = (name, tool) => nativePaneToolAvailable(tool) && mayUseTool(resolveToolSet(this.fleetConfig?.instances[name], name), tool)
968
+ && !dashboardRefusalIn(this.fleetConfig, name, tool);
969
+ return this.paneManager = new NativePaneManager({
970
+ home: this.dataDir, store, config: name => this.fleetConfig?.instances[name],
971
+ freshNotice: async (name, provider) => {
972
+ const message = t(provider === "codex" ? "native_pane.codex_fresh_started" : "native_pane.fresh_started");
973
+ this.eventLog?.insert(name, "native_pane_fresh_started", {});
974
+ this.logger.info({ name }, message);
975
+ await this.notifyInstanceTopic(name, message);
976
+ },
977
+ admit: (name, config) => {
978
+ const now = this.fleetConfig?.instances[name];
979
+ if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
980
+ || now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
981
+ || now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
982
+ || now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding)
983
+ throw Error("Native pane configuration or lifetime changed");
984
+ if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
985
+ throw Error("Native runtime is switched off");
986
+ },
987
+ endpoint: async (agent, cleanup = false) => {
988
+ if (!agent.machineId) {
989
+ if (!cleanup)
990
+ return this.localNativePaneEndpoint();
991
+ if (!this.localPaneRelay)
992
+ throw Error("Local cleanup transport is unavailable; pane remains reserved");
993
+ return this.localPaneRelay;
994
+ }
995
+ const hub = cleanup ? this.runtimeHub : this.runtimeCoordinator(), machineId = agent.machineId;
996
+ if (!hub)
997
+ throw Error("Remote cleanup transport is unavailable; pane remains reserved");
998
+ const draining = () => !!this.remotePaneDrain;
999
+ return { machineId, online: () => !this.shuttingDown && (draining() || remoteRuntimeEnabled()) && this.runtimeHub === hub && hub.machine(machineId)?.status === "online",
1000
+ call: request => {
1001
+ if (this.runtimeHub !== hub || !(draining() || remoteRuntimeEnabled()) || draining() && request.action !== "snapshot" && request.action !== "close")
1002
+ return Promise.reject(Error("Remote pane transport only permits owned cleanup"));
1003
+ return hub.callPane(machineId, request, maxAgentsPerMachine());
1004
+ }, subscribe: listener => hub.subscribe(listener),
1005
+ epoch: () => hub.paneTransportEpoch(machineId), version: provider => hub.machine(machineId)?.inventory?.clis.find(row => row.id === provider && row.runnable)?.version ?? null };
1006
+ },
1007
+ instructions: (name, config) => buildNativeFleetInstructions({ instanceName: name, workingDirectory: config.working_directory,
1008
+ displayName: config.display_name, description: config.description, customPrompt: config.systemPrompt,
1009
+ workflow: config.workflow, taskBoard: !dashboardRefusalIn(this.fleetConfig, name, "task"),
1010
+ runtimeIdentity: { kind: "fleet-topic", backend: config.backend ?? "codex", model: config.model || "default" } }, config.general_topic === true),
1011
+ menu: name => TOOLS.filter(tool => allowed(name, tool.name)).map(tool => ({ ...tool })),
1012
+ allowed: (name, tool, args) => allowed(name, tool) && !nativePaneToolRefusal(tool, args), published: (name, daemon) => this.daemons.get(name) === daemon,
1013
+ invoke: async (daemon, input, current, name) => {
1014
+ if (input.tool === "list_models") {
1015
+ const { ListModelsArgs } = await import("./outbound-schemas.js");
1016
+ const parsed = ListModelsArgs.safeParse(input.args);
1017
+ if (!parsed.success || !current())
1018
+ return { result: null, error: "Invalid native model request or expired owner" };
1019
+ const target = parsed.data.instance_name ?? name;
1020
+ if (!isWindowsInstance(this.fleetConfig?.instances[target]))
1021
+ return { result: null, error: "Native panes can only read Windows runtime model inventories" };
1022
+ return { result: await this.listModelCatalog({ instanceName: target }) };
1023
+ }
1024
+ const ops = { list_instances: "list", describe_instance: "describe", task: "task", send_to_instance: "send",
1025
+ delegate_task: "delegate", request_information: "ask", report_result: "report", delivery_status: "delivery-status" };
1026
+ const { nativeAgentAdmissionContext } = await import("./native-agent-admission.js");
1027
+ const { dispatchAgentOperation } = await import("./agent-endpoint.js");
1028
+ return daemon.handlePaneToolCall(input, { current, allowed: tool => allowed(name, tool),
1029
+ dispatch: (tool, args) => ops[tool] ? dispatchAgentOperation(nativeAgentAdmissionContext(this, {
1030
+ instance: name, current: () => current() && allowed(name, tool), taskAllowed: () => current() && allowed(name, "task"),
1031
+ }), name, ops[tool], args, input.operationId) : undefined });
1032
+ },
1033
+ });
1034
+ }
1035
+ /** Prepare only bundled role files; this neither switches General nor starts a provider process. */
1036
+ async prepareWindowsGeneralWorkspace(name, machineId, cli, bindingId, authorized) {
1037
+ const config = this.fleetConfig?.instances[name], hub = this.runtimeCoordinator(), epoch = hub.paneTransportEpoch(machineId);
1038
+ if (!config?.general_topic || !epoch)
1039
+ throw Error("General or its paired runtime is unavailable");
1040
+ const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
1041
+ && this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
1042
+ const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertAuditedNativePaneVersion }] = await Promise.all([
1043
+ import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
1044
+ ]);
1045
+ if (!current())
1046
+ throw Error("General workspace authority expired");
1047
+ assertAuditedNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
1048
+ const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
1049
+ const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
1050
+ if (!current())
1051
+ throw Error("General workspace authority expired");
1052
+ if (inventory?.capabilities?.generalWorkspace !== true)
1053
+ throw new NativePaneRecoveryError({ kind: "host-upgrade" });
1054
+ const result = await hub.callPane(machineId, request, maxAgentsPerMachine());
1055
+ if (!current())
1056
+ throw Error("General workspace authority expired");
1057
+ if (generalWorkspaceUpgrade(result, request))
1058
+ throw new NativePaneRecoveryError({ kind: "host-upgrade" });
1059
+ return validateGeneralWorkspaceReceipt(result, request);
1060
+ }
1061
+ generalLocationFlights = new Map();
1062
+ generalAttentionEpisodes = new Map();
1063
+ generalBindings() { return new GeneralBindings(this.dataDir); }
1064
+ generalLocations() {
1065
+ const store = this.generalBindings();
1066
+ return { generals: Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => cfg.general_topic).map(([name, cfg]) => {
1067
+ const record = store.read(name), machine = cfg.machine_id ? this.runtimeHub?.machine(cfg.machine_id) : null;
1068
+ return { name, displayName: cfg.display_name ?? name, location: isWindowsInstance(cfg) ? "windows" : "host",
1069
+ machineId: cfg.machine_id ?? null, cli: cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code", model: cfg.model ?? "",
1070
+ revision: generalBindingRevision(record, cfg), canSwitchBack: !!record && record.active !== "host",
1071
+ offline: isWindowsInstance(cfg) && (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(cfg.machine_id)),
1072
+ retryTarget: record?.pending?.phase === "selected" ? (record.pending.to === "host" ? { location: "host" } : (() => { const row = record.windows.find(item => item.id === record.pending.to); return { location: "windows", machineId: row.machineId, cli: row.cli, model: row.config.model ?? "" }; })()) : null,
1073
+ hold: record?.hold ?? (record?.pending ? record.pending.phase === "selected" ? "start" : "exit" : null), switching: this.generalLocationFlights.has(name) };
1074
+ }), machines: (this.runtimeHub?.listMachines() ?? []).filter(row => row.status !== "pending" && row.status !== "revoked").map(row => ({
1075
+ id: row.id, name: row.name, online: row.status === "online" && !!this.runtimeHub?.paneTransportEpoch(row.id),
1076
+ clis: row.inventory?.clis.filter(cli => cli.runnable && (cli.id === "claude-code" || cli.id === "codex")).map(cli => ({ id: cli.id, version: cli.version })) ?? [],
1077
+ })) };
1078
+ }
1079
+ /** Pure preflight for the confirmation diff and stale-tab refusal; never prepares files or starts a CLI. */
1080
+ previewGeneralLocation(raw) {
1081
+ const input = parseGeneralSwitchInput(raw), cfg = this.fleetConfig?.instances[input.name];
1082
+ if (!cfg?.general_topic || this.shuttingDown)
1083
+ throw Error("General is unavailable");
1084
+ const record = this.generalBindings().read(input.name);
1085
+ if (input.revision !== generalBindingRevision(record, cfg))
1086
+ throw Error("General location changed; read its settings again");
1087
+ const connections = cfg.channel_id ? [cfg.channel_id] : [];
1088
+ const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
1089
+ if (channels.length && (connections.length !== 1 || channels.filter(row => (row.id ?? row.type) === connections[0]).length !== 1))
1090
+ throw Error("General's connection cannot be resolved");
1091
+ if (input.target.location === "host") {
1092
+ if (!record)
1093
+ throw Error("General's saved host location is unavailable");
1094
+ return { input, connections, summary: [t("general_location.confirm_host"), t("general_location.history")] };
1095
+ }
1096
+ const active = record?.windows.find(row => row.id === record.active);
1097
+ if (!record?.pending && record?.hold !== "start" && active?.machineId === input.target.machineId && active.cli === input.target.cli)
1098
+ throw Error("General already runs at this location");
1099
+ const machine = this.runtimeHub?.machine(input.target.machineId);
1100
+ if (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(machine.id))
1101
+ throw Error(t("general_location.offline"));
1102
+ const provider = input.target.cli;
1103
+ if (!machine.inventory?.clis.some(cli => cli.id === provider && cli.runnable))
1104
+ throw Error("The chosen Windows CLI is unavailable");
1105
+ return { input, connections, summary: [t("general_location.confirm_windows", settingsDisplay(machine.name)), t("general_location.history")] };
1106
+ }
1107
+ /** All entry points, including automatic native recovery, must respect an interrupted binding switch. */
1108
+ assertGeneralBindingStart(name, config) {
1109
+ if (!config.general_topic)
1110
+ return;
1111
+ try {
1112
+ this.generalBindings().assertStart(name, config);
1113
+ }
1114
+ catch (err) {
1115
+ this.logger.warn({ name, err }, "General location start held by its saved binding");
1116
+ this.needsYou?.poke();
1117
+ throw new UnsupportedCliError(t("general_location.exit_unconfirmed"));
1118
+ }
1119
+ }
1120
+ /** Only the Settings replay's confirmed admin can enter; the whole switch owns one lifecycle transition. */
1121
+ async switchGeneralLocation(raw, authorized, execution) {
1122
+ const { input } = this.previewGeneralLocation(raw), name = input.name;
1123
+ return this.lifecycle.runTransition(name, async (transition) => {
1124
+ execution.assert();
1125
+ if (!authorized() || this.generalLocationFlights.has(name))
1126
+ throw Error("General location authority expired or a switch is already running");
1127
+ const token = Symbol(name), generation = this.settingsGeneration;
1128
+ const destinationEpoch = input.target.location === "windows" ? this.runtimeHub?.paneTransportEpoch(input.target.machineId) : null;
1129
+ let expected = this.fleetConfig.instances[name], stopGeneration = this.explicitStopGeneration.get(name) ?? 0, epoch = this.lifecycle.epochOf(name);
1130
+ const hostSnapshot = !isWindowsInstance(expected) ? structuredClone(this.daemons.get(name)?.getConfigSnapshot() ?? expected) : undefined;
1131
+ const current = () => !this.shuttingDown && this.settingsGeneration === generation && authorized()
1132
+ && this.generalLocationFlights.get(name) === token && this.fleetConfig?.instances[name] === expected
1133
+ && (input.target.location !== "windows" || remoteRuntimeEnabled() && !!destinationEpoch && this.runtimeHub?.paneTransportEpoch(input.target.machineId) === destinationEpoch)
1134
+ && (this.explicitStopGeneration.get(name) ?? 0) === stopGeneration && this.lifecycle.epochOf(name) === epoch;
1135
+ const check = () => { if (!current())
1136
+ throw Error("General location switch was superseded"); };
1137
+ this.generalLocationFlights.set(name, token);
1138
+ // Old ephemeral waiters carry this epoch; they cannot wake into the selected conversation.
1139
+ this.cancelPendingDeliveries(name);
1140
+ let lease = null;
1141
+ try {
1142
+ lease = await waitSettingsLease([settingsFileResource(this.getSettingsConfigPath()), `general:${this.dataDir}:${name}`], () => current() && execution.current(), performance.now() + execution.remainingMs, execution.owner);
1143
+ const store = this.generalBindings();
1144
+ const result = await switchGeneralLocation({ store, current: () => expected, assert: check,
1145
+ prepare: async (target, retained) => {
1146
+ if (target.location !== "windows")
1147
+ return null;
1148
+ const old = retained.windows.find(row => row.machineId === target.machineId && row.cli === target.cli);
1149
+ const id = old?.id ?? randomUUID(), agentId = old?.agentId ?? randomUUID();
1150
+ const publication = await this.prepareWindowsGeneralWorkspace(name, target.machineId, target.cli, id, () => current() && execution.current());
1151
+ check();
1152
+ execution.assert();
1153
+ const config = { ...structuredClone(expected), runtime: "windows", machine_id: target.machineId,
1154
+ native_adapter: "pane", native_agent_id: agentId, native_general_binding: id, native_sandbox: "workspace-write",
1155
+ backend: target.cli, model: target.model, working_directory: publication.cwd, systemPrompt: "", agent_mode: "cli" };
1156
+ // Host credentials, shell hooks and provider-specific options never become Windows launch configuration.
1157
+ for (const key of ["backend_options", "credential_profile", "pre_task_command"])
1158
+ delete config[key];
1159
+ windowsAgentsFor(this, this.runtimeWindowsExecutor()).ensureGeneralBinding({ id: agentId, instance: name, machineId: target.machineId,
1160
+ cli: target.cli, cwd: publication.cwd, name: (expected.display_name ?? name).slice(0, 64), model: target.model, instructions: "", description: expected.description ?? "" }, () => current() && execution.current());
1161
+ return { id, agentId, machineId: target.machineId, cli: target.cli, config };
1162
+ },
1163
+ idle: async () => {
1164
+ check();
1165
+ const daemon = this.daemons.get(name);
1166
+ return !daemon || daemon.getInstanceStateSnapshot().state === "idle" && daemon.getWorkingRecoveryState().idle
1167
+ && daemon.getInteractionSnapshot().phase === "clear" && !this.lifecycle.hasWorkLease(name)
1168
+ && Date.now() - (this.lastDeliveryAt.get(name) ?? 0) >= 2000;
1169
+ },
1170
+ hostSnapshot: () => hostSnapshot,
1171
+ capture: async () => {
1172
+ if (isWindowsInstance(expected))
1173
+ return undefined;
1174
+ const daemon = this.daemons.get(name);
1175
+ if (!daemon)
1176
+ throw Error(t("general_location.exit_unconfirmed"));
1177
+ const owner = await daemon.generalHostOwner();
1178
+ check();
1179
+ execution.assert();
1180
+ return owner;
1181
+ },
1182
+ stop: async (owner) => {
1183
+ check();
1184
+ const stopping = this.stopInstance(name, transition);
1185
+ stopGeneration++;
1186
+ epoch++;
1187
+ await stopping;
1188
+ check();
1189
+ if (isWindowsInstance(expected)) {
1190
+ const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(expected.native_agent_id);
1191
+ if (profile.activeId || profile.paneLaunchId)
1192
+ throw Error(t("general_location.exit_unconfirmed"));
1193
+ execution.mutate(() => this.holdGeneralOldDeliveries(name));
1194
+ return undefined;
1195
+ }
1196
+ if (!owner || !await confirmGeneralHostExit(owner, getTmuxSession()))
1197
+ throw Error(t("general_location.exit_unconfirmed"));
1198
+ check();
1199
+ execution.mutate(() => this.holdGeneralOldDeliveries(name));
1200
+ return owner.pin;
1201
+ },
1202
+ save: next => {
1203
+ check();
1204
+ const previous = expected;
1205
+ try {
1206
+ this.fleetConfig.instances[name] = next;
1207
+ this.saveFleetConfig([{ path: ["instances", name], value: next }]);
1208
+ expected = next;
1209
+ this.routing.rebuild(this.fleetConfig);
1210
+ }
1211
+ catch (err) {
1212
+ this.fleetConfig.instances[name] = previous;
1213
+ throw err;
1214
+ }
1215
+ },
1216
+ start: async () => {
1217
+ check();
1218
+ if (!execution.completed)
1219
+ throw Error("General's selected binding has no committed approval");
1220
+ // The committed receipt permits only this selected startup. Live authority still fences every awaited spawn boundary.
1221
+ const startup = new SettingsExecution({ current: () => current() && execution.completed && execution.remainingMs > 0,
1222
+ snapshot: () => this.fleetConfig });
1223
+ try {
1224
+ await this.startInstance(name, expected, true, "fleet-topic", false, transition, startup);
1225
+ }
1226
+ finally {
1227
+ startup.close();
1228
+ }
1229
+ check();
1230
+ const daemon = this.daemons.get(name);
1231
+ if (!daemon || daemon.getProcessStatus() !== "running" || isNativePaneInstance(expected) && !daemon.nativePaneCurrent())
1232
+ throw Error(t("general_location.start_failed"));
1233
+ await this.connectIpcToInstance(name);
1234
+ check();
1235
+ },
1236
+ notify: (kind, err) => {
1237
+ const message = t(kind === "exit" ? "general_location.exit_unconfirmed" : kind === "start" ? "general_location.start_failed" : "general_location.complete", isWindowsInstance(expected) ? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host"));
1238
+ this.eventLog?.insert(name, "general_location", { phase: kind, message });
1239
+ if (err)
1240
+ this.logger.warn({ name, phase: kind, err }, message);
1241
+ else
1242
+ this.logger.info({ name, phase: kind }, message);
1243
+ this.needsYou?.poke();
1244
+ this.emitSseEvent("status", this.getUiStatus());
1245
+ this.notifyInstanceTopic(name, message);
1246
+ },
1247
+ }, input, execution);
1248
+ return { ...result, message: result.started ? t("general_location.complete", isWindowsInstance(expected)
1249
+ ? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host")) : t("general_location.start_failed") };
1250
+ }
1251
+ finally {
1252
+ lease?.release();
1253
+ if (this.generalLocationFlights.get(name) === token)
1254
+ this.generalLocationFlights.delete(name);
1255
+ this.needsYou?.poke();
1256
+ }
1257
+ });
1258
+ }
1259
+ /** An offline PC or a confirmed binding boundary holds old waiting input; it never becomes another conversation's queue. */
1260
+ holdGeneralOldDeliveries(name) {
1261
+ const outbox = this.deliveryOutbox;
1262
+ if (!outbox)
1263
+ return;
1264
+ for (const row of outbox.listPending().filter(row => row.targetInstance === name)) {
1265
+ if (row.state !== "submission_started")
1266
+ outbox.failBeforeBegin(row.deliveryId, t("general_location.input_not_replayed"));
1267
+ else if (row.targetDaemonBootId) {
1268
+ outbox.markTargetReconciliationPending(name, row.targetDaemonBootId);
1269
+ if (!outbox.reconcileAttempt(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "uncertain", t("general_location.input_not_replayed")))
1270
+ throw Error("General's old delivery could not be held");
1271
+ }
1272
+ else
1273
+ throw Error("General's old delivery has no confirmed owner");
1274
+ }
1275
+ this.needsYou?.poke();
1276
+ }
1277
+ generalLocationDeliveryHold(name) {
1278
+ const config = this.fleetConfig?.instances[name];
1279
+ if (!config?.general_topic)
1280
+ return null;
1281
+ if (this.generalLocationFlights?.has(name))
1282
+ return t("general_location.exit_unconfirmed");
1283
+ let record;
1284
+ try {
1285
+ record = this.generalBindings().read(name);
1286
+ }
1287
+ catch {
1288
+ return t("general_location.start_failed");
1289
+ }
1290
+ if (record?.pending || record?.hold)
1291
+ return t(record.pending?.phase === "selected" || record.hold === "start" ? "general_location.start_failed" : "general_location.exit_unconfirmed");
1292
+ if (isWindowsInstance(config) && config.machine_id && (!remoteRuntimeEnabled() || this.runtimeHub?.machine(config.machine_id)?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(config.machine_id)))
1293
+ return t("general_location.offline");
1294
+ return null;
1295
+ }
1296
+ generalLocationAttention(name) {
1297
+ const cfg = this.fleetConfig?.instances[name];
1298
+ if (!cfg?.general_topic)
1299
+ return null;
1300
+ const detail = this.generalLocationDeliveryHold(name);
1301
+ if (!detail) {
1302
+ this.generalAttentionEpisodes.delete(name);
1303
+ return null;
1304
+ }
1305
+ let record = null;
1306
+ try {
1307
+ record = this.generalBindings().read(name);
1308
+ }
1309
+ catch { /* a corrupt journal is a visible hold too */ }
1310
+ const offline = !record?.pending && !record?.hold && isWindowsInstance(cfg) && detail === t("general_location.offline");
1311
+ const key = record?.pending?.id ?? `${record?.revision ?? "unknown"}:${offline ? "offline" : "hold"}`;
1312
+ let episode = this.generalAttentionEpisodes.get(name);
1313
+ if (!episode || episode.key !== key) {
1314
+ episode = { key, since: record?.pending?.at ?? Date.now() };
1315
+ this.generalAttentionEpisodes.set(name, episode);
1316
+ }
1317
+ return { ...episode, detail, offline };
1318
+ }
1319
+ async nativePaneFor(name, config, assertCurrent) {
1320
+ try {
1321
+ return await (await this.nativePanes()).prepare(name, config, assertCurrent);
1322
+ }
1323
+ catch (err) {
1324
+ if (config.general_topic && err instanceof GeneralManagedFileError) {
1325
+ assertCurrent();
1326
+ const store = this.generalBindings(), record = store.read(name);
1327
+ if (record && record.active === config.native_general_binding) {
1328
+ record.hold = "start";
1329
+ store.write(record);
1330
+ }
1331
+ this.logger.warn({ name, managedFile: err.file }, t("general_location.start_failed"));
1332
+ this.needsYou?.poke();
1333
+ throw new UnsupportedCliError(t("general_location.start_failed"));
1334
+ }
1335
+ throw err;
1336
+ }
1337
+ }
1338
+ async recoverNativePane(name, launchId, authorized) {
1339
+ const config = this.fleetConfig?.instances[name];
1340
+ if (!isNativePaneInstance(config))
1341
+ throw Error("Persistent Windows pane is not configured");
1342
+ const generation = this.explicitStopGeneration.get(name) ?? 0;
1343
+ await this.lifecycle.runTransition(name, async (transition) => {
1344
+ const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
1345
+ throw Error(t("native_pane.authority_expired")); };
1346
+ current();
1347
+ if (this.daemons.get(name)?.nativePaneCurrent())
1348
+ throw Error(t("native_pane.still_running"));
1349
+ this.cancelStartupRetry(name);
1350
+ this.lifecycle.cancelNativeRecovery(name);
1351
+ await (await this.nativePanes()).recover(name, config, launchId, true, current);
1352
+ current();
1353
+ // The host supplied a positive exit before ordinary lifecycle cleanup. No signal or input is required.
1354
+ await this.lifecycle.stop(name, transition);
1355
+ current();
1356
+ this.instanceProcessStatus.set(name, "stopped");
1357
+ this.eventLog?.insert(name, "native_pane_recovered", { launchId, confirmedStopped: true });
1358
+ this.logger.info({ name, launchId }, "Windows saved pane recovered by an administrator; waiting for Start");
1359
+ this.emitSseEvent("status", this.getUiStatus());
1360
+ });
1361
+ }
1362
+ holdNativePaneRecovery(name, error) {
1363
+ if (!(error instanceof NativePaneRecoveryError))
1364
+ return false;
1365
+ this.instanceProcessStatus.set(name, "stopped");
1366
+ this.logger.warn({ name, recovery: error.recovery }, error.message);
1367
+ this.eventLog?.insert(name, "native_pane_recovery_needed", { ...error.recovery });
1368
+ this.needsYou?.poke();
1369
+ this.emitSseEvent("status", this.getUiStatus());
1370
+ return true;
1371
+ }
1372
+ async startFreshNativePane(name, launchId, sessionId, authorized) {
1373
+ const config = this.fleetConfig?.instances[name];
1374
+ if (!isNativePaneInstance(config))
1375
+ throw Error("Persistent Windows pane is not configured");
1376
+ const generation = this.explicitStopGeneration.get(name) ?? 0;
1377
+ await this.lifecycle.runTransition(name, async (transition) => {
1378
+ const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
1379
+ throw Error(t("native_pane.authority_expired")); };
1380
+ current();
1381
+ const held = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id).paneRecovery;
1382
+ if (held?.kind !== "session-unconfirmed" || held.launchId !== launchId || (held.sessionId ?? null) !== sessionId)
1383
+ throw Error(t("native_pane.changed"));
1384
+ if (this.daemons.get(name)?.nativePaneCurrent())
1385
+ throw Error(t("native_pane.still_running"));
1386
+ this.cancelStartupRetry(name);
1387
+ this.lifecycle.cancelNativeRecovery(name);
1388
+ await this.lifecycle.stop(name, transition);
1389
+ current();
1390
+ try {
1391
+ await (await this.nativePanes()).startFresh(name, config, launchId, sessionId, current);
1392
+ current();
1393
+ }
1394
+ catch (error) {
1395
+ current();
1396
+ this.needsYou?.poke();
1397
+ this.emitSseEvent("status", this.getUiStatus());
1398
+ throw error;
1399
+ }
1400
+ this.eventLog?.insert(name, "native_pane_new_conversation", { launchId, sessionId });
1401
+ await this.startInstance(name, config, true, "fleet-topic", true, transition);
1402
+ current();
1403
+ this.needsYou?.poke();
1404
+ this.emitSseEvent("status", this.getUiStatus());
1405
+ });
1406
+ }
1407
+ nativePanePinnedSession(name) {
1408
+ const config = this.fleetConfig?.instances[name];
1409
+ if (!isNativePaneInstance(config) || !config.native_agent_id)
1410
+ return null;
1411
+ const agent = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
1412
+ return agent.fleet_instance === name && agent.machineId === config.machine_id ? agent.threadId ?? null : null;
1413
+ }
1414
+ onNativePaneRecoverySettled(name) {
1415
+ if (this.daemons.get(name)?.nativePaneCurrent())
1416
+ this.instanceProcessStatus.delete(name);
1417
+ this.onAttentionChanged(name);
1418
+ this.scheduleDeliveryOutboxPump();
1419
+ }
1420
+ setNativePanePaused(name, paused) {
1421
+ const id = this.fleetConfig?.instances[name]?.native_agent_id;
1422
+ if (!id)
1423
+ throw Error("Native profile is missing");
1424
+ windowsAgentsFor(this, this.runtimeWindowsExecutor()).setPaused(id, paused);
1425
+ }
1426
+ async closeRetainedNativePane(name) {
1427
+ const config = this.fleetConfig?.instances[name];
1428
+ if (!config)
1429
+ throw Error("Native instance configuration is missing");
1430
+ await (await this.nativePanes()).closeRetained(name, config, () => { if (this.shuttingDown)
1431
+ throw Error("Fleet is stopping"); });
1432
+ }
1433
+ async reconcileNativePaneBeforeStart(name, daemon) {
1434
+ const outbox = this.deliveryOutbox;
1435
+ if (!outbox)
1436
+ return true;
1437
+ const epoch = this.lifecycle.epochOf(name);
1438
+ return daemon.reconcileNativeDeliveries(outbox, () => !this.shuttingDown && this.lifecycle.epochOf(name) === epoch);
1439
+ }
1440
+ /** M-2255: the Windows runtimes' switches the fleet's own state reflects (null until the start-up has applied them). */
1441
+ runtimeEffects = null;
1442
+ runtimeEffectsScheduled = false;
1443
+ localPaneDrain = null;
1444
+ remotePaneDrain = null;
1445
+ /** Revoke admissions synchronously; keep the captured transport only long enough for exact owned closes. */
1446
+ drainNativeRuntime(remote) {
1447
+ const existing = remote ? this.remotePaneDrain : this.localPaneDrain;
1448
+ if (existing)
1449
+ return existing;
1450
+ const hub = this.runtimeHub, relay = this.localPaneRelay;
1451
+ if (!remote)
1452
+ relay?.beginCleanup();
1453
+ const targets = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, config]) => isWindowsInstance(config) && !!config.machine_id === remote);
1454
+ for (const [name, config] of targets) {
1455
+ this.nativeMcpStore?.closeInstance(name);
1456
+ if (isNativePaneInstance(config)) {
1457
+ this.lifecycle.cancelNativeRecovery(name);
1458
+ this.lifecycle.invalidate(name);
1459
+ this.daemons.get(name)?.fenceDeliveryWritesForStop();
1460
+ }
1461
+ }
1462
+ const run = Promise.resolve().then(async () => {
1463
+ const results = await Promise.allSettled(targets.filter(([, config]) => isNativePaneInstance(config)).map(([name]) => this.stopInstance(name)));
1464
+ results.forEach((result, index) => { if (result.status === "rejected")
1465
+ this.logger.warn({ err: result.reason, remote, index }, "Windows pane cleanup unconfirmed; its reservation is retained"); });
1466
+ try {
1467
+ if (remote)
1468
+ await hub?.shutdownPaneHosts();
1469
+ else
1470
+ await relay?.shutdownHost();
1471
+ }
1472
+ catch (error) {
1473
+ this.logger.warn({ err: error, remote }, "Windows pane host shutdown unconfirmed; owned state is retained");
1474
+ }
1475
+ }).finally(() => {
1476
+ if (remote) {
1477
+ hub?.close();
1478
+ if (this.runtimeHub === hub)
1479
+ this.runtimeHub = null;
1480
+ }
1481
+ else {
1482
+ relay?.stop();
1483
+ if (this.localPaneRelay === relay)
1484
+ this.localPaneRelay = null;
1485
+ closeWindowsBridge(this);
1486
+ }
1487
+ });
1488
+ if (remote)
1489
+ this.remotePaneDrain = run;
1490
+ else
1491
+ this.localPaneDrain = run;
1492
+ void run.finally(() => {
1493
+ if (remote) {
1494
+ if (this.remotePaneDrain === run)
1495
+ this.remotePaneDrain = null;
1496
+ }
1497
+ else if (this.localPaneDrain === run)
1498
+ this.localPaneDrain = null;
1499
+ if (!this.shuttingDown && (remote ? remoteRuntimeEnabled() : localWindowsCliEnabled())) {
1500
+ this.initializeNativeInstances();
1501
+ this.initializeNativeInstances(true);
1502
+ }
1503
+ try {
1504
+ this.emitSseEvent("status", this.getUiStatus());
1505
+ }
1506
+ catch { }
1507
+ }).catch(error => this.logger.warn({ err: error }, "Windows runtime cleanup failed; retained ownership requires reconciliation"));
1508
+ return run;
1509
+ }
1510
+ /**
1511
+ * M-2255: Settings → Runtime's switches (fleet.yaml `runtimes`, or their environment override) in force — set each time
1512
+ * the configuration in force changes (a load, a save, a reload's rollback), next to the web authority's. Every gate reads
1513
+ * them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
1514
+ * sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
1515
+ */
1516
+ observeRuntimeSwitches() {
1517
+ setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
1518
+ if (!this.runtimeEffects || this.runtimeEffectsScheduled)
1519
+ return;
1520
+ this.runtimeEffectsScheduled = true;
1521
+ queueMicrotask(() => { this.runtimeEffectsScheduled = false; this.applyRuntimeSwitches(); });
1522
+ }
1523
+ /**
1524
+ * Turned off: what it allowed ends now — the paired computers' connections (their agents show as offline) or this PC's
1525
+ * Windows process (a turn in progress is cancelled, as when the fleet stops), and those agents' tool sessions.
1526
+ * Turned on: the Windows agents are registered and their chat history read, as at start-up.
1527
+ */
1528
+ applyRuntimeSwitches() {
1529
+ const was = this.runtimeEffects;
1530
+ if (!was || this.shuttingDown)
1531
+ return;
1532
+ const now = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
1533
+ if (was.windows_cli === now.windows_cli && was.remote_runtime === now.remote_runtime)
1534
+ return;
1535
+ this.runtimeEffects = now;
1536
+ try {
1537
+ if (was.remote_runtime && !now.remote_runtime) {
1538
+ void this.drainNativeRuntime(true);
1539
+ }
1540
+ if (was.windows_cli && !now.windows_cli) {
1541
+ void this.drainNativeRuntime(false);
1542
+ }
1543
+ if (!was.windows_cli && !was.remote_runtime && !this.localPaneDrain && !this.remotePaneDrain) {
1544
+ this.initializeNativeInstances();
1545
+ this.initializeNativeInstances(true);
1546
+ }
1547
+ this.logger.info({ windows_cli: now.windows_cli, remote_runtime: now.remote_runtime }, "Windows runtime switches applied");
1548
+ }
1549
+ catch (error) {
1550
+ this.logger.warn({ err: error }, "Windows runtime switches could not be applied in full");
1551
+ }
1552
+ try {
1553
+ this.emitSseEvent("status", this.getUiStatus());
1554
+ }
1555
+ catch { /* the next status frame carries it */ }
1556
+ }
1557
+ /** An unavailable opt-in runtime must not prevent the Linux fleet from starting or restoring its own history. */
1558
+ initializeNativeInstances(history = false) {
1559
+ if (!windowsCliEnabled())
1560
+ return;
1561
+ try {
1562
+ const runtime = this.nativeInstanceRuntime();
1563
+ if (history)
1564
+ runtime.importHistory();
1565
+ else
1566
+ runtime.register();
1567
+ }
1568
+ catch (error) {
1569
+ this.logger.warn({ err: error }, "Windows runtime state could not be loaded; affected native instances remain unavailable");
1570
+ }
1571
+ }
1572
+ /** Native CLIs are headless: the manager owns the durable delivery generation instead of a tmux daemon. */
1573
+ deliveryBootId(name) {
1574
+ const config = this.fleetConfig?.instances[name];
1575
+ if (isNativeExecInstance(config)) {
1576
+ if (!this.nativeInstanceRuntime().available(name))
1577
+ return null;
1578
+ const machine = config.machine_id ? this.runtimeCoordinator().machine(config.machine_id) : null;
1579
+ return machine ? `runtime:${machine.id}:${machine.bootId ?? machine.generation}` : this.managerBootId;
1580
+ }
1581
+ return this.daemons.get(name)?.bootId ?? null;
1582
+ }
740
1583
  rawFleetConfig = {};
741
1584
  rawFleetDocument = null;
742
1585
  /** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
@@ -1536,6 +2379,8 @@ export class FleetManager {
1536
2379
  * authorizing revoked links and cookies until the fleet restarted.
1537
2380
  */
1538
2381
  settingsGeneration = 0;
2382
+ /** #2942: the settings baseline the agent-requested Windows changes are checked against (set with the gate). */
2383
+ settingsBaselineReader = null;
1539
2384
  settingsRuntimeRevision = 0;
1540
2385
  settingsConfirmation = null;
1541
2386
  settingsControl = null;
@@ -1592,8 +2437,9 @@ export class FleetManager {
1592
2437
  return this.settingsConfirmation;
1593
2438
  const generation = this.settingsGeneration;
1594
2439
  const current = () => !this.shuttingDown && this.settingsGeneration === generation;
1595
- const baselines = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
2440
+ const baselines = this.settingsBaselineReader = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
1596
2441
  config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
2442
+ generalLocation: input => this.previewGeneralLocation(input),
1597
2443
  proof: (kind, target, id, binding, key) => {
1598
2444
  const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
1599
2445
  const proof = map.get(id);
@@ -1614,6 +2460,17 @@ export class FleetManager {
1614
2460
  const store = new SettingsConfirmationStore({
1615
2461
  audit: (event, fields) => this.logger.info({ event: `settings_confirmation.${event}`, ...fields }, "Settings confirmation audit"),
1616
2462
  notify: view => view.state === "pending" ? this.promptSettingsChange(store, view) : Promise.resolve(),
2463
+ // alpha.2 (M-2300): an agent's request came, was decided or ran out — the open pages read the list again (a ping
2464
+ // with nothing in it: only a web-only fleet's verified owner may read it, GET /ui/agent-proposals).
2465
+ changed: view => {
2466
+ if (view.source !== "agent_request")
2467
+ return;
2468
+ this.agentProposalsRev++;
2469
+ broadcastSseEvent(this.sseClients, "agent_proposals", {}, err => this.logger.debug({ err }, "agent_proposals event failed"));
2470
+ // …and a status frame with the new revision: a tab from before the upgrade (its stream knows no `agent_proposals`)
2471
+ // applies status frames, and a polling page reads the same frame at its next /ui/poll.
2472
+ this.emitSseEvent("status", this.getUiStatus());
2473
+ },
1617
2474
  });
1618
2475
  const gate = new SettingsHttpConfirmation(store, {
1619
2476
  principal: req => {
@@ -1661,8 +2518,10 @@ export class FleetManager {
1661
2518
  const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
1662
2519
  if (auth.kind !== "ok")
1663
2520
  return null;
1664
- // #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
1665
- if (gatewayRequestContext(req))
2521
+ // #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there. #3008: except
2522
+ // this computer's admin signed in from outside (the authenticator's code, over the link connections from outside
2523
+ // opened): the web-only fleet's owner, judged below by the same authority as here (hostOperator).
2524
+ if (gatewayRequestContext(req) && auth.session.remote !== "totp")
1666
2525
  return null;
1667
2526
  // …and never a change that turns the named-link option on: that one needs a General's card or the host.
1668
2527
  if (!store.selfConfirmable(id))
@@ -1695,30 +2554,97 @@ export class FleetManager {
1695
2554
  if (own.length !== 1 || own[0].type !== chat.platform || !chat.admin(actor.user_id))
1696
2555
  return null;
1697
2556
  const affected = [...store.affectedConnections(id)]; // read now: a finished change no longer has them
1698
- if (!this.settingsNoticeGenerals(affected, chat.platform).length)
2557
+ const destinations = this.settingsNoticeGenerals(affected, chat.platform);
2558
+ if (!destinations.length)
1699
2559
  return null;
1700
2560
  const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
1701
2561
  return {
1702
2562
  label: t("settings_confirm.web_verified_label", name),
1703
2563
  // admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
1704
- current: () => !!verified() && chat.admin(actor.user_id) && this.settingsNoticeGenerals(affected, chat.platform).length > 0,
2564
+ current: () => !!verified() && chat.admin(actor.user_id) && destinations.some(route => route.current()),
2565
+ followStop: async (name, begin) => { const route = destinations.find(item => item.current()); if (!route)
2566
+ throw Error("Settings route expired"); await route.followStop(name, begin); },
1705
2567
  done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
1706
2568
  };
1707
2569
  }
1708
- /** #2170: the Generals a chat card could be posted in (promptSettingsChange's rule): connected, on the platform, unaffected. */
1709
- settingsNoticeGenerals(affected, platform) {
1710
- const excluded = new Set(affected), found = [];
1711
- for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
1712
- if (!config.general_topic || !this.daemons.get(name))
1713
- continue;
1714
- const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
1715
- if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform || this.adapterState.get(adapterId)?.status !== "connected")
1716
- continue;
1717
- const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
1718
- if (chatId && topic)
1719
- found.push({ adapterId, adapter, chatId, topic });
2570
+ /**
2571
+ * alpha.2 (M-2300; #2973's agent requests, junsion's §6): on a fleet with no chat connection there is no General to
2572
+ * post an agent's Windows request in — it waited for `agend settings confirm` on the host. Its owner (decision
2573
+ * 28c2d3a3: the `agend web --code` session on this computer, web-verified) now sees and decides it from the web's
2574
+ * Needs you. The authority is asked from the request each time, and again at execution by the store (`current`):
2575
+ * still that owner now (this computer, not revoked, signed out or run out), still a fleet with no chat connection,
2576
+ * never over the public link. Anyone else — a guest, a LAN or public-link session, a chat-connected fleet's admin —
2577
+ * gets nothing (a chat fleet keeps General's card as its way).
2578
+ */
2579
+ /**
2580
+ * #2997 review (Prism P2): how many times an agent's request came or was decided — in a web-only fleet's status frame,
2581
+ * so a page that never hears the stream's ping (one polling /ui/poll, one reconnecting, a tab from before the upgrade)
2582
+ * reads the list again when it moves. A count, nothing more.
2583
+ */
2584
+ agentProposalsRev = 0;
2585
+ agentProposalOwner(req) {
2586
+ // #3008: this computer's admin here, or signed in from outside (over the link connections from outside opened) —
2587
+ // asked of the LIVE session each time (#2997 review): a sign-out, an expiry, a revocation or the switch going off
2588
+ // while the request is applied fences it.
2589
+ return this.liveOwner(req, { hostOnly: false });
2590
+ }
2591
+ /** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
2592
+ webAgentProposals(req) {
2593
+ if (!this.agentProposalOwner(req))
2594
+ return null;
2595
+ return this.settingsGate().store.listBySource("agent_request");
2596
+ }
2597
+ /**
2598
+ * alpha.2: the owner's Approve or Reject of an agent's request. The store claims it before its first await (a second
2599
+ * click, from here or the host, finds it no longer pending) and asks the owner, the request (its fleet generation and
2600
+ * Windows switch) and the target machine again before and while it runs the same creation code the web uses. The
2601
+ * answer is always the request as it now stands: applied (with the new agent's name), rejected, expired, stale or
2602
+ * failed (with its reason), or already decided.
2603
+ */
2604
+ async webDecideAgentProposal(req, id, decision) {
2605
+ const owner = this.agentProposalOwner(req);
2606
+ if (!owner)
2607
+ return { status: 403, body: { error: "owner_required", message: t("settings_confirm.web_owner_required") } };
2608
+ const store = this.settingsGate().store, before = store.inspect(id);
2609
+ if (!before || before.source !== "agent_request")
2610
+ return { status: 404, body: { error: "not_found", message: t("settings_confirm.no_longer_pending") } };
2611
+ try {
2612
+ const view = await store.decide(id, decision, { ...owner, via: "web" });
2613
+ this.logger.info({ id, decision, state: view.state }, "Agent request decided from the web by this computer's admin");
2614
+ return { status: 200, body: { view, agent: agentNameOf(view) } };
2615
+ }
2616
+ catch (err) {
2617
+ const code = err instanceof SettingsConfirmationError ? err.code : "decide_failed";
2618
+ const status = err instanceof SettingsConfirmationError ? err.status : 500;
2619
+ // Already decided (a double click, the host's command), run out or withdrawn: say what it is now.
2620
+ return { status, body: { error: code, message: t(code === "admin_required" ? "settings_confirm.web_owner_required" : "settings_confirm.no_longer_pending"), view: store.inspect(id) } };
1720
2621
  }
1721
- return found;
2622
+ }
2623
+ /** A confirmation belongs to the server route/lifetime, not the availability of General's model. */
2624
+ settingsGeneralRoute(name, platform, excluded) {
2625
+ const config = this.fleetConfig?.instances[name];
2626
+ if (!config?.general_topic || this.shuttingDown || this.ipcStoppingInstances.has(name))
2627
+ return null;
2628
+ const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
2629
+ if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform)
2630
+ return null;
2631
+ const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
2632
+ if (!chatId || !topic)
2633
+ return null;
2634
+ const route = () => !this.shuttingDown && this.fleetConfig?.instances[name]?.general_topic === true
2635
+ && this.adapterState.get(adapterId)?.status === "connected"
2636
+ && this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
2637
+ && this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic;
2638
+ const lifetime = new SettingsRouteLifetime(name, route, () => this.lifecycle.epochOf(name), () => this.ipcStoppingInstances.has(name));
2639
+ return lifetime.current() ? { adapterId, adapter, chatId, topic, current: lifetime.current, followStop: lifetime.followStop.bind(lifetime) } : null;
2640
+ }
2641
+ /** Connected, same-platform, unaffected server routes can post cards even while the model is offline. */
2642
+ settingsNoticeGenerals(affected, platform) {
2643
+ const excluded = new Set(affected);
2644
+ return Object.keys(this.fleetConfig?.instances ?? {}).flatMap(name => {
2645
+ const route = this.settingsGeneralRoute(name, platform, excluded);
2646
+ return route ? [route] : [];
2647
+ });
1722
2648
  }
1723
2649
  /**
1724
2650
  * #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
@@ -1777,6 +2703,119 @@ export class FleetManager {
1777
2703
  return { platform, current, admin: userId => current() && !!userId
1778
2704
  && identities.every(owner => this.isFleetAdmin(userId, owner.id)) };
1779
2705
  }
2706
+ /**
2707
+ * #2942: a Windows create or settings change an AGENT asked for. Nothing is created or written here: it becomes a
2708
+ * pending settings change whose General card a fleet admin must confirm (promptSettingsChange — the same admin rule
2709
+ * as every card). Its apply runs the same creation/patch code the web uses, inside the store's SettingsExecution, with
2710
+ * the confirming admin, the Windows switch and the target machine asked again at that moment.
2711
+ */
2712
+ async proposeAgentWindowsChange(input) {
2713
+ const gate = this.settingsGate(), baselines = this.settingsBaselineReader;
2714
+ if (!baselines || !this.fleetConfig)
2715
+ throw new Error("Fleet configuration is unavailable");
2716
+ const generation = this.settingsGeneration, machineId = input.machineId;
2717
+ const switchOn = () => machineId ? remoteRuntimeEnabled() : localWindowsCliEnabled();
2718
+ const online = () => !machineId || (remoteRuntimeEnabled() && this.runtimeCoordinator().machine(machineId)?.status === "online");
2719
+ if (!switchOn())
2720
+ throw new Error(machineId ? "Remote runtime is not enabled" : LOCAL_WINDOWS_CLI_OFF);
2721
+ const baseline = await baselines.read(), snapshot = settingsFingerprint(baselines.snapshot());
2722
+ const { view } = gate.store.propose({ session: `agent:${input.requester}`, key: `agent-windows:${input.key}`,
2723
+ requestFingerprint: input.fingerprint, fingerprint: input.fingerprint, section: "channel", requestedBy: input.requester,
2724
+ source: "agent_request", summary: input.summary, bytes: input.bytes, authority: input.authority, affectedConnections: [],
2725
+ current: () => !this.shuttingDown && this.settingsGeneration === generation && switchOn(),
2726
+ snapshot: () => baselines.snapshot(),
2727
+ unchanged: async () => (await baselines.read()).fingerprint === baseline.fingerprint && settingsFingerprint(baselines.snapshot()) === snapshot,
2728
+ // The store's own fence (this confirming admin, the session, the state) plus the switch and machine now.
2729
+ apply: async (actorCurrent, execution) => {
2730
+ execution.assert();
2731
+ // alpha.2 polish: a paired machine whose CLI inventory has not arrived yet (just paired) is asked for its own
2732
+ // report first (it runs on that PC), so the creation validates against what is installed there — never a refusal
2733
+ // that only a manual Refresh would undo. Its answer or failure changes nothing else; the fence is asked again.
2734
+ if (machineId && online() && !inventoryProbed(this.runtimeCoordinator().machine(machineId)?.inventory)) {
2735
+ // `force`: the runner's cached answer on a fresh profile is the placeholder itself (#3005 rig recheck).
2736
+ try {
2737
+ await this.runtimeCoordinator().call(machineId, "probe", { force: true });
2738
+ }
2739
+ catch { /* validation says what is missing */ }
2740
+ execution.assert();
2741
+ }
2742
+ return input.apply(() => actorCurrent() && online(), execution);
2743
+ } });
2744
+ return view;
2745
+ }
2746
+ /** #2942: create_instance with runtime "windows" from an agent: validated like the web, then held for General. */
2747
+ async requestWindowsCreate(requester, rawArgs) {
2748
+ const cfg = this.fleetConfig;
2749
+ if (!cfg)
2750
+ throw new Error("Fleet configuration is unavailable");
2751
+ let parsed;
2752
+ try {
2753
+ parsed = parseNativeInstanceCreate(rawArgs);
2754
+ }
2755
+ catch {
2756
+ throw new Error("Invalid Windows agent request: check the directory, sandbox, backend and model");
2757
+ }
2758
+ // The web's own projection: the same validation, topic target and diff the admin's dashboard confirmation shows.
2759
+ let diff;
2760
+ try {
2761
+ diff = prepareSettingsEffect("POST", "/ui/instances", rawArgs, { config: cfg, classic: {} }).diff;
2762
+ }
2763
+ catch {
2764
+ throw new Error("Invalid Windows agent request: its topic connection is not one this fleet can use");
2765
+ }
2766
+ if (!diff)
2767
+ throw new Error("Invalid Windows agent request");
2768
+ return this.proposeAgentWindowsChange({ requester, key: diff.fingerprint, fingerprint: diff.fingerprint, summary: diff.summary,
2769
+ machineId: parsed.input.machineId, bytes: JSON.stringify(rawArgs).length,
2770
+ // The General card is the confirmation for an agent's request, whatever the requested connection (a topic or none).
2771
+ authority: { connections: diff.authority?.connections ?? [], primaryGeneral: true, unknown: false },
2772
+ apply: (allowed, execution) => createNativeWindowsInstance(this, parsed, allowed, execution) });
2773
+ }
2774
+ /** #2942: update_instance_config on a Windows agent: only the fields the web editor allows, held for General. */
2775
+ async requestWindowsConfig(requester, name, patch) {
2776
+ const inst = this.fleetConfig?.instances[name];
2777
+ if (!isWindowsInstance(inst))
2778
+ throw new Error("Windows agent not found");
2779
+ let edit;
2780
+ try {
2781
+ edit = NativeInstanceConfigEdit.parse(patch);
2782
+ }
2783
+ catch {
2784
+ throw new Error("Invalid Windows agent settings: display_name, description and model only");
2785
+ }
2786
+ const before = { display_name: inst.display_name ?? "", description: inst.description ?? "", model: inst.model ?? "" };
2787
+ const changed = Object.keys(edit).filter(key => edit[key] !== before[key]);
2788
+ if (!changed.length)
2789
+ throw new Error("Nothing to change: these settings already have these values");
2790
+ const summary = changed.map(key => `${name} ${key}: ${settingsDisplay(before[key])} → ${settingsDisplay(edit[key])}`);
2791
+ const machineId = typeof inst.machine_id === "string" ? inst.machine_id : undefined;
2792
+ // #2973 review (Prism P1): a persistent pane is bound to a chat connection; changing it (a model change restarts it)
2793
+ // needs that connection's admins as well as the primary General's, exactly as its creation did. An instance whose
2794
+ // binding names no single configured connection is refused (fail closed), never confirmed by the General alone.
2795
+ const connections = [];
2796
+ if (isNativePaneInstance(inst) || inst.channel_id !== undefined) {
2797
+ const bound = inst.channel_id, channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
2798
+ if (typeof bound !== "string" || channels.filter(ch => (ch.id ?? ch.type) === bound).length !== 1)
2799
+ throw new Error("This Windows agent's chat connection cannot be resolved; change its settings in the web dashboard");
2800
+ connections.push(bound);
2801
+ }
2802
+ return this.proposeAgentWindowsChange({ requester, key: `${name}:${settingsFingerprint(edit)}`, fingerprint: settingsFingerprint([name, edit]),
2803
+ summary, machineId, bytes: JSON.stringify(edit).length, authority: { connections, primaryGeneral: true, unknown: false },
2804
+ // #2973 review (Prism P2): the update may wait for its lifecycle turn; if the agent is rebound meanwhile (B → C), the
2805
+ // approval for B must not stop or change it on C. Pin the approved target and require the execution's snapshot to be
2806
+ // unchanged before every pre-commit effect; the post-commit restart keeps its own (refreshed) checks.
2807
+ apply: (allowed, execution) => {
2808
+ const pinned = { channel: inst.channel_id, machine: inst.machine_id, adapter: inst.native_adapter, runtime: inst.runtime };
2809
+ const sameTarget = () => {
2810
+ const now = this.fleetConfig?.instances[name];
2811
+ return !!now && now.channel_id === pinned.channel && now.machine_id === pinned.machine && now.native_adapter === pinned.adapter && now.runtime === pinned.runtime;
2812
+ };
2813
+ // Before the commit: the exact target and an unchanged snapshot. After runtime.update completes the transaction,
2814
+ // current() is false by design; the committed operation's restart receipt then needs the same actor, target and
2815
+ // switch checks plus `completed` — a completed execution can never authorize another write (mutate refuses).
2816
+ return updateNativeWindowsInstance(this, name, edit, () => allowed() && sameTarget() && (execution.completed || execution.current()), execution);
2817
+ } });
2818
+ }
1780
2819
  async promptSettingsChange(store, view) {
1781
2820
  const generation = this.settingsGeneration;
1782
2821
  const authority = this.settingsChatAuthority(store, view.id);
@@ -1785,25 +2824,15 @@ export class FleetManager {
1785
2824
  return;
1786
2825
  }
1787
2826
  const excluded = new Set(store.affectedConnections(view.id));
1788
- for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
1789
- if (!config.general_topic)
1790
- continue;
1791
- const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
1792
- const general = this.daemons.get(name);
1793
- if (!adapterId || !adapter || !general || excluded.has(adapterId) || adapter.type !== authority.platform)
1794
- continue;
1795
- const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
1796
- if (!chatId || !topic)
2827
+ for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
2828
+ const route = this.settingsGeneralRoute(name, authority.platform, excluded);
2829
+ if (!route)
1797
2830
  continue;
2831
+ const { adapterId, adapter, chatId, topic } = route;
1798
2832
  let retired = false, attached = false;
1799
2833
  const postingDeadline = performance.now() + Math.min(5000, view.remaining_ms);
1800
- const current = () => !retired && (attached || performance.now() < postingDeadline) && !this.shuttingDown && this.settingsGeneration === generation
1801
- && this.fleetConfig?.instances[name]?.general_topic === true
1802
- && this.daemons.get(name) === general && !this.ipcStoppingInstances.has(name)
1803
- && this.adapterState.get(adapterId)?.status === "connected"
1804
- && this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
1805
- && this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic
1806
- && authority.current();
2834
+ const current = () => !retired && (attached || performance.now() < postingDeadline) && this.settingsGeneration === generation
2835
+ && route.current() && authority.current();
1807
2836
  if (!current())
1808
2837
  continue;
1809
2838
  try {
@@ -1813,7 +2842,7 @@ export class FleetManager {
1813
2842
  t("settings_confirm.requester", view.requested_by), ...view.summary].join("\n"),
1814
2843
  choices: [{ action: "confirm", label: t("settings_confirm.confirm_button") }, { action: "reject", label: t("settings_confirm.reject_button") }],
1815
2844
  expiredText: t("settings_confirm.no_longer_pending"), timeoutMs: view.remaining_ms,
1816
- extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId) },
2845
+ extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId), confirmationStop: route.followStop },
1817
2846
  });
1818
2847
  void posted.then(nonce => {
1819
2848
  if (!retired)
@@ -2081,6 +3110,7 @@ export class FleetManager {
2081
3110
  try {
2082
3111
  const view = await this.settingsGate().store.decide(entry.pendingChangeId, action, {
2083
3112
  label: t("settings_confirm.admin_label", data.userId ?? ""),
3113
+ followStop: entry.confirmationStop,
2084
3114
  current: () => entry.confirmationCurrent?.() === true && this.getAdapterForInstance(entry.instanceName) === entry.adapter
2085
3115
  && !!data.userId && entry.confirmationAdmin?.(data.userId) === true,
2086
3116
  });
@@ -2455,12 +3485,15 @@ export class FleetManager {
2455
3485
  }
2456
3486
  /** Synchronous SQLite admission; the caller may acknowledge only after this returns. */
2457
3487
  admitDurableDelivery(input) {
3488
+ const bindingHold = this.generalLocationDeliveryHold(input.targetInstance);
3489
+ if (bindingHold)
3490
+ throw Error(bindingHold);
2458
3491
  const outbox = this.deliveryOutbox;
2459
3492
  if (!outbox)
2460
3493
  throw new Error("Durable delivery store is unavailable");
2461
- const source = this.daemons.get(input.sourceInstance);
2462
- const sourceDaemonBootId = input.sourceDaemonBootId ?? source?.bootId;
2463
- if (!source || !sourceDaemonBootId || source.bootId !== sourceDaemonBootId) {
3494
+ const currentSourceBootId = this.getDaemonBootId(input.sourceInstance);
3495
+ const sourceDaemonBootId = input.sourceDaemonBootId ?? currentSourceBootId;
3496
+ if (!currentSourceBootId || !sourceDaemonBootId || currentSourceBootId !== sourceDaemonBootId) {
2464
3497
  throw new Error("Source daemon generation is no longer current; outcome is unknown");
2465
3498
  }
2466
3499
  const sourceKey = `mcp:${input.sourceInstance}:${input.operationId}:${input.targetInstance}:${input.targetSession ?? ""}:${input.kind}`;
@@ -2494,7 +3527,7 @@ export class FleetManager {
2494
3527
  const human = (item) => {
2495
3528
  if (!item.authority || !state)
2496
3529
  return item;
2497
- const authority = authorityNow(item.authority, state);
3530
+ const authority = this.receiptAuthorityNow(item.authority, state);
2498
3531
  return { ...item, authority, ...(authority.downgraded ? { source_instance: "human:web:unknown" } : {}) };
2499
3532
  };
2500
3533
  // #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
@@ -2523,6 +3556,11 @@ export class FleetManager {
2523
3556
  return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
2524
3557
  }
2525
3558
  getDaemonBootId(instanceName) {
3559
+ if (this.generalLocationDeliveryHold(instanceName))
3560
+ return undefined;
3561
+ if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
3562
+ return !this.shuttingDown && windowsCliEnabled() && this.nativeMcpStore?.hasActive(instanceName) ? this.managerBootId : undefined;
3563
+ }
2526
3564
  return this.daemons.get(instanceName)?.bootId;
2527
3565
  }
2528
3566
  markDurableResponseDelivered(sourceInstance, operationId) {
@@ -2531,8 +3569,22 @@ export class FleetManager {
2531
3569
  /** Called after a replacement Daemon object is installed in lifecycle.daemons. */
2532
3570
  onDaemonReady(name, daemonBootId) {
2533
3571
  const daemon = this.daemons.get(name);
2534
- if (daemon?.bootId === daemonBootId)
3572
+ if (daemon?.bootId === daemonBootId) {
2535
3573
  this.listenForWorkingState(name, daemon);
3574
+ const cfg = this.fleetConfig?.instances[name];
3575
+ if (cfg?.general_topic && !this.generalLocationFlights.has(name) && daemon.getProcessStatus() === "running"
3576
+ && (!isNativePaneInstance(cfg) || daemon.nativePaneCurrent())) {
3577
+ try {
3578
+ if (this.generalBindings().completeSelected(name, cfg)) {
3579
+ this.needsYou?.poke();
3580
+ this.logger.info({ name }, "General location's selected start receipt settled after recovery");
3581
+ }
3582
+ }
3583
+ catch (err) {
3584
+ this.logger.warn({ name, err }, "General location start receipt remains held");
3585
+ }
3586
+ }
3587
+ }
2536
3588
  // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
2537
3589
  // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
2538
3590
  this.scheduleDeliveryOutboxPump();
@@ -2676,6 +3728,15 @@ export class FleetManager {
2676
3728
  this.deliveryPumpRunning = true;
2677
3729
  this.deliveryPumpScheduled = false;
2678
3730
  try {
3731
+ // Native workers have no resident daemon to wake. Pending durable work resumes their logical instance;
3732
+ // the Windows bridge still owns the single physical execution slot.
3733
+ for (const target of new Set(outbox.listPending().map(row => row.targetInstance))) {
3734
+ if (isNativeExecInstance(this.fleetConfig?.instances[target])) {
3735
+ const runtime = this.nativeInstanceRuntime(), agent = runtime.get(target);
3736
+ if (agent?.paused && !agent.blocked)
3737
+ runtime.setPaused(target, false);
3738
+ }
3739
+ }
2679
3740
  outbox.expireStale(undefined, undefined, target => {
2680
3741
  const failure = this.wakeCoordinator?.wakeFailure(target);
2681
3742
  return failure ? `target could not be woken (${failure})` : undefined;
@@ -2688,7 +3749,7 @@ export class FleetManager {
2688
3749
  // Phase 2c: a lane a queue worker owns is never claimed here.
2689
3750
  target => this.queueWorkers.has(target) || this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target)
2690
3751
  ? null
2691
- : this.daemons.get(target)?.bootId ?? null, this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
3752
+ : this.deliveryBootId(target), this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
2692
3753
  if (!claimed)
2693
3754
  break;
2694
3755
  this.activeDurableTargets.add(claimed.targetInstance);
@@ -2749,7 +3810,7 @@ export class FleetManager {
2749
3810
  wanted: () => this.deliveryWorkerMode(target) === "on",
2750
3811
  available: () => !this.shuttingDown && this.deliveryOutbox?.isOpen === true,
2751
3812
  blocked: () => this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target) === true,
2752
- daemonBootId: () => this.daemons.get(target)?.bootId ?? null,
3813
+ daemonBootId: () => this.deliveryBootId(target),
2753
3814
  claim: bootId => this.deliveryOutbox?.claimNext(this.managerBootId, candidate => candidate === target ? bootId : null, new Set(), row => this.recoveryDeliveryEligible(row)),
2754
3815
  tryAcquireBudget: () => {
2755
3816
  if (this.activeDurableTargets.size >= 8)
@@ -2780,6 +3841,10 @@ export class FleetManager {
2780
3841
  const outbox = this.deliveryOutbox;
2781
3842
  if (!outbox)
2782
3843
  return;
3844
+ if (isNativeExecInstance(this.fleetConfig?.instances[claimed.targetInstance])) {
3845
+ await this.dispatchNativeDurableDelivery(claimed);
3846
+ return;
3847
+ }
2783
3848
  const target = claimed.targetInstance;
2784
3849
  const attempt = claimed.attemptNo;
2785
3850
  const payload = { ...claimed.payload };
@@ -2861,6 +3926,85 @@ export class FleetManager {
2861
3926
  }
2862
3927
  await laneReleased;
2863
3928
  }
3929
+ /** The ordinary durable lane, with provider completion proof instead of terminal/IPC submission proof. */
3930
+ async dispatchNativeDurableDelivery(claimed) {
3931
+ const outbox = this.deliveryOutbox;
3932
+ if (!outbox)
3933
+ return;
3934
+ const { deliveryId, targetDaemonBootId, attemptNo, targetInstance: target } = claimed;
3935
+ let begun = false;
3936
+ try {
3937
+ if (["raw_paste", "steer"].includes(claimed.kind)) {
3938
+ if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) === "begun")
3939
+ outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "failed", "Windows runtime does not support terminal input or steering");
3940
+ return;
3941
+ }
3942
+ const meta = claimed.payload.meta && typeof claimed.payload.meta === "object" ? claimed.payload.meta : {};
3943
+ const task = await this.nativeInstanceRuntime().execute(target, { ...claimed.payload, requestId: deliveryId, meta: { ...meta, from_instance: claimed.sourceInstance, correlation_id: claimed.correlationId } }, {
3944
+ stillCurrent: () => !this.shuttingDown,
3945
+ beforeStart: () => { if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) !== "begun")
3946
+ throw new Error("Windows delivery attempt is no longer current"); begun = true; },
3947
+ });
3948
+ const outcome = task.outcomeConfirmed === false ? "uncertain" : task.status === "completed" ? "delivered" : "failed";
3949
+ outbox.complete(deliveryId, targetDaemonBootId, attemptNo, outcome, task.error || task.status);
3950
+ // Return a requested peer report when the provider did not explicitly report it through scoped MCP.
3951
+ if (meta.requires_reply === "true" && claimed.correlationId && !task.reportedCorrelationIds?.includes(claimed.correlationId) && this.fleetConfig?.instances[claimed.sourceInstance]) {
3952
+ const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
3953
+ outbox.admit({ sourceKey: `native-result:${deliveryId}`, operationId: `native-result:${deliveryId}`, sourceInstance: target, sourceDaemonBootId: this.managerBootId,
3954
+ targetInstance: claimed.sourceInstance, kind: "fleet_inbound", correlationId: claimed.correlationId,
3955
+ payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: target, correlation_id: claimed.correlationId, requires_reply: "false", source: "native_runtime" } } });
3956
+ this.scheduleDeliveryOutboxPump();
3957
+ }
3958
+ }
3959
+ catch (error) {
3960
+ const reason = error instanceof Error ? error.message : String(error);
3961
+ if (begun)
3962
+ outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "uncertain", reason);
3963
+ else
3964
+ outbox.retryBeforeBegin(deliveryId, targetDaemonBootId, attemptNo, reason, 5000);
3965
+ this.logger.warn({ target, deliveryId, err: reason }, "Windows durable delivery did not complete");
3966
+ }
3967
+ }
3968
+ /** A runtime's persisted completion can outlive this manager. Resolve its original delivery fence, never send
3969
+ * the prompt again, and reconstruct a missing peer report using the same durable operation ID. */
3970
+ reconcileRemoteDelivery(machineId, task) {
3971
+ const outbox = this.deliveryOutbox;
3972
+ if (!outbox?.isOpen || !task.finishedAt || task.outcomeConfirmed !== true || !["completed", "failed", "cancelled"].includes(task.status))
3973
+ return;
3974
+ const key = `${task.id}:${task.finishedAt}:${task.status}`;
3975
+ if (this.reconciledRemoteResults.has(key))
3976
+ return;
3977
+ const row = outbox.get(task.id);
3978
+ if (!row || !isNativeExecInstance(this.fleetConfig?.instances[row.targetInstance]) || this.fleetConfig?.instances[row.targetInstance]?.machine_id !== machineId
3979
+ || !row.targetDaemonBootId || this.activeDurableTargets.has(row.targetInstance))
3980
+ return;
3981
+ if (row.state === "submission_started" || row.state === "reconciliation_pending") {
3982
+ if (!outbox.complete(row.deliveryId, row.targetDaemonBootId, row.attemptNo, task.status === "completed" ? "delivered" : "failed", task.error || "Runtime completion recovered after reconnect"))
3983
+ return;
3984
+ }
3985
+ else if (row.state === "uncertain" && task.status === "completed") {
3986
+ if (outbox.markConsumed(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "turn", "Paired runtime journal confirmed this exact execution completed") === "ignored")
3987
+ return;
3988
+ }
3989
+ else if (row.state !== "delivered" && row.state !== "failed")
3990
+ return;
3991
+ const meta = row.payload.meta && typeof row.payload.meta === "object" ? row.payload.meta : {};
3992
+ if (meta.requires_reply !== "true" || !row.correlationId || !this.fleetConfig?.instances[row.sourceInstance]) {
3993
+ this.reconciledRemoteResults.add(key);
3994
+ return;
3995
+ }
3996
+ const reports = outbox.queryStatusForInstance(row.targetInstance, { correlationId: row.correlationId, limit: 100 }).items;
3997
+ if (reports.some(report => report.source_instance === row.targetInstance && report.target_instance === row.sourceInstance
3998
+ && report.kind === "report" && !["failed", "cancelled"].includes(report.state))) {
3999
+ this.reconciledRemoteResults.add(key);
4000
+ return;
4001
+ }
4002
+ const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
4003
+ outbox.admit({ sourceKey: `native-result:${row.deliveryId}`, operationId: `native-result:${row.deliveryId}`, sourceInstance: row.targetInstance, sourceDaemonBootId: this.managerBootId,
4004
+ targetInstance: row.sourceInstance, kind: "fleet_inbound", correlationId: row.correlationId,
4005
+ payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: row.targetInstance, correlation_id: row.correlationId, requires_reply: "false", source: "native_runtime" } } });
4006
+ this.reconciledRemoteResults.add(key);
4007
+ }
2864
4008
  async waitForDurableLaneRelease(claimed) {
2865
4009
  const outbox = this.deliveryOutbox;
2866
4010
  if (!outbox)
@@ -3317,6 +4461,7 @@ export class FleetManager {
3317
4461
  this.warnAboutRemovedLoginMode(loaded);
3318
4462
  this.enforceGuestSwitch();
3319
4463
  this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
4464
+ this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
3320
4465
  // Child tool/guidance readers must follow this fleet's actual config, including --config.
3321
4466
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
3322
4467
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
@@ -3745,6 +4890,8 @@ export class FleetManager {
3745
4890
  }
3746
4891
  /** Get the adapter bound to an instance, falling back to primary adapter */
3747
4892
  getAdapterForInstance(name) {
4893
+ if (isNativeExecInstance(this.fleetConfig?.instances[name]))
4894
+ return null;
3748
4895
  const worldId = this.getInstanceAdapterId(name);
3749
4896
  if (worldId)
3750
4897
  return this.worlds.get(worldId)?.adapter ?? this.adapter;
@@ -3807,6 +4954,8 @@ export class FleetManager {
3807
4954
  }
3808
4955
  /** Get the group_id for an instance's bound adapter */
3809
4956
  getGroupIdForInstance(name) {
4957
+ if (isNativeExecInstance(this.fleetConfig?.instances[name]))
4958
+ return "";
3810
4959
  const adapterId = this.getInstanceAdapterId(name);
3811
4960
  const world = adapterId ? this.worlds.get(adapterId) : undefined;
3812
4961
  return world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
@@ -3843,6 +4992,8 @@ export class FleetManager {
3843
4992
  */
3844
4993
  getInstanceAdapterId(name) {
3845
4994
  const cfg = this.fleetConfig?.instances[name];
4995
+ if (isNativeExecInstance(cfg))
4996
+ return undefined;
3846
4997
  if (cfg)
3847
4998
  return cfg.channel_id ?? this.getPrimaryAdapterId();
3848
4999
  if (this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
@@ -3870,9 +5021,32 @@ export class FleetManager {
3870
5021
  this.instanceWorldBinding.set(name, adapterId);
3871
5022
  }
3872
5023
  getInstanceStatus(name) {
5024
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
5025
+ try {
5026
+ return this.nativeInstanceRuntime().status(name);
5027
+ }
5028
+ catch {
5029
+ return "crashed";
5030
+ }
5031
+ }
3873
5032
  if (this.lifecycle.isPaused(name))
3874
5033
  return "paused";
3875
5034
  const daemon = this.lifecycle.daemons.get(name);
5035
+ if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
5036
+ const config = this.fleetConfig.instances[name];
5037
+ if (config.machine_id ? !remoteRuntimeEnabled() : !localWindowsCliEnabled())
5038
+ return "stopped";
5039
+ if (daemon) {
5040
+ const native = daemon, status = native.getProcessStatus?.() ?? "crashed";
5041
+ return status === "running" && native.nativePaneCurrent?.() !== true ? "crashed" : status;
5042
+ }
5043
+ try {
5044
+ return this.nativeInstanceRuntime().get(name)?.activeId ? "crashed" : "stopped";
5045
+ }
5046
+ catch {
5047
+ return "crashed";
5048
+ }
5049
+ }
3876
5050
  const processStatus = this.instanceProcessStatus.get(name);
3877
5051
  // IPC can be disconnected during a respawn, so the event which announces
3878
5052
  // the new live pane may be missed. The in-process daemon is authoritative
@@ -3906,6 +5080,14 @@ export class FleetManager {
3906
5080
  }
3907
5081
  }
3908
5082
  getInstanceExecutionState(name) {
5083
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
5084
+ try {
5085
+ return this.nativeInstanceRuntime().execution(name);
5086
+ }
5087
+ catch {
5088
+ return null;
5089
+ }
5090
+ }
3909
5091
  if (this.lifecycle.isPaused(name))
3910
5092
  return null;
3911
5093
  // Process status wins over a stale pane snapshot. A dead remain-on-exit pane
@@ -3916,6 +5098,8 @@ export class FleetManager {
3916
5098
  }
3917
5099
  /** Pure read of the single daemon-owned observation; no probe/capture or second tracker. */
3918
5100
  getInstanceInteraction(name) {
5101
+ if (isNativeExecInstance(this.fleetConfig?.instances[name]))
5102
+ return null;
3919
5103
  if (this.lifecycle.isPaused(name) || this.instanceProcessStatus.has(name))
3920
5104
  return null;
3921
5105
  const daemon = this.lifecycle.daemons.get(name);
@@ -4003,6 +5187,8 @@ export class FleetManager {
4003
5187
  bindings.push([name, backend, profile]);
4004
5188
  };
4005
5189
  for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
5190
+ if (isWindowsInstance(config))
5191
+ continue; // Windows sign-ins and usage do not belong to the Linux account.
4006
5192
  // loadFleetConfig() has already merged the fleet default into each row.
4007
5193
  const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
4008
5194
  add(name, backend, instanceCredentialProfile(config, this.fleetConfig?.defaults, backend));
@@ -4609,6 +5795,8 @@ export class FleetManager {
4609
5795
  * id is no platform message, and the echo is the fleet's own post. The web send calls this on the echo's lane.
4610
5796
  */
4611
5797
  startWebChatProgress(instanceName) {
5798
+ if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
5799
+ return Promise.resolve();
4612
5800
  if (this.shuttingDown)
4613
5801
  return Promise.resolve();
4614
5802
  return this.sendCancelButton(instanceName);
@@ -4874,6 +6062,12 @@ export class FleetManager {
4874
6062
  }
4875
6063
  /** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
4876
6064
  async deliverToInstance(instanceName, payload, options = {}) {
6065
+ if (this.generalLocationDeliveryHold(instanceName))
6066
+ return false;
6067
+ if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
6068
+ return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
6069
+ ...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
6070
+ }
4877
6071
  const deliveryEpoch = this.getDeliveryEpoch(instanceName);
4878
6072
  const deliveryPayload = { ...payload, delivery_epoch: deliveryEpoch };
4879
6073
  const meta = payload.meta && typeof payload.meta === "object"
@@ -5431,6 +6625,10 @@ export class FleetManager {
5431
6625
  * slot admission and joins any wake already in flight for the target.
5432
6626
  */
5433
6627
  async explicitWake(name, timeoutMs = 30_000, retryStoppedSession = false) {
6628
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
6629
+ this.nativeInstanceRuntime().setPaused(name, false);
6630
+ return;
6631
+ }
5434
6632
  if (this.sessionOomStopped(name)) {
5435
6633
  if (!retryStoppedSession)
5436
6634
  throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
@@ -5455,6 +6653,10 @@ export class FleetManager {
5455
6653
  .sort((a, b) => (readLastInboundAt(this.getInstanceDir(a)) ?? 0) - (readLastInboundAt(this.getInstanceDir(b)) ?? 0))[0];
5456
6654
  }
5457
6655
  async changeInstancePauseState(name, action) {
6656
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
6657
+ this.nativeInstanceRuntime().setPaused(name, action === "pause");
6658
+ return action === "pause" ? "paused" : "awake";
6659
+ }
5458
6660
  if (action === "wake") {
5459
6661
  await this.explicitWake(name, 30_000, true);
5460
6662
  this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
@@ -5632,6 +6834,14 @@ export class FleetManager {
5632
6834
  resumePaused = false,
5633
6835
  /** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
5634
6836
  transition, execution, kiroMigration) {
6837
+ if (isNativeExecInstance(config)) {
6838
+ execution?.assert();
6839
+ if (!windowsCliEnabled())
6840
+ throw new Error("Windows runtime is disabled");
6841
+ if (resumePaused)
6842
+ this.nativeInstanceRuntime().setPaused(name, false);
6843
+ return;
6844
+ }
5635
6845
  const migration = this.kiroMigrationRuns?.get(name);
5636
6846
  if (migration && migration.token !== kiroMigration)
5637
6847
  throw new Error(t("kiro_migrate.in_progress"));
@@ -5682,27 +6892,29 @@ export class FleetManager {
5682
6892
  if (resumePaused)
5683
6893
  this.lifecycle.markActivitySeedNow(name);
5684
6894
  const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
5685
- // #1490 row 67: off-loop resolve before skill/instruction publication
6895
+ // #1490 row 67 (main): off-loop resolve before skill/instruction publication
5686
6896
  // constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
5687
6897
  // ownership first: a Stop during this wait finishes and bumps the epoch,
5688
6898
  // and the inner lifecycle check would otherwise capture the new epoch and
5689
6899
  // accept the stale start. Lifecycle start looks up again before its own
5690
- // createBackend; the probe cache makes that cheap.
6900
+ // createBackend; the probe cache makes that cheap. (alpha: a native Windows
6901
+ // agent's CLI is on that PC — nothing to resolve on this one.)
5691
6902
  const epoch = this.lifecycle.epochOf(name);
5692
6903
  const stopGen = this.explicitStopGeneration.get(name) ?? 0;
5693
- const resolved = await resolveBackendBinary(backend);
6904
+ const windows = isWindowsInstance(config);
6905
+ const resolved = windows ? null : await resolveBackendBinary(backend);
5694
6906
  execution?.assert();
5695
6907
  if (this.lifecycle.epochOf(name) !== epoch
5696
6908
  || (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
5697
6909
  throw new SupersededStartError(name);
5698
6910
  }
5699
6911
  const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
5700
- if (binary)
6912
+ if (binary && !windows)
5701
6913
  noteResolvedBinary(binary, resolved ?? binary);
5702
- if (config.general_topic) {
6914
+ if (!windows && config.general_topic) {
5703
6915
  this.ensureGeneralInstructions(config.working_directory, backend, name);
5704
6916
  }
5705
- else if (kind === "fleet-topic") {
6917
+ else if (!isWindowsInstance(config) && kind === "fleet-topic") {
5706
6918
  // Workers get only role-eligible on-demand skills. Classic instances are
5707
6919
  // deliberately excluded: their workspace and conversation lifecycle are
5708
6920
  // managed independently from fleet-topic workers.
@@ -5717,7 +6929,7 @@ export class FleetManager {
5717
6929
  this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
5718
6930
  }
5719
6931
  }
5720
- else if (kind === "classic") {
6932
+ else if (!isWindowsInstance(config) && kind === "classic") {
5721
6933
  // #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
5722
6934
  // coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
5723
6935
  // room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
@@ -5734,7 +6946,7 @@ export class FleetManager {
5734
6946
  }
5735
6947
  }
5736
6948
  const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
5737
- const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
6949
+ const identity = { kind, backend, model: isWindowsInstance(config) ? config.model || "default" : this.resolveInstanceModel(name).display };
5738
6950
  if (receipt) {
5739
6951
  await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
5740
6952
  // Only this transition's actual publication can acquire its replacement.
@@ -5804,7 +7016,7 @@ export class FleetManager {
5804
7016
  // them before grouping/staggering: startInstance() retains its own guard as
5805
7017
  // a final backstop, but putting a no-op entry in this queue still consumes a
5806
7018
  // full stagger slot for every distinct working directory.
5807
- const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
7019
+ const runnableEntries = entries.filter(([name, config]) => !isNativeExecInstance(config) && !this.lifecycle.isPaused(name));
5808
7020
  // ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
5809
7021
  // only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
5810
7022
  const fleetNames = new Set(entries.map(([name]) => name));
@@ -5870,6 +7082,8 @@ export class FleetManager {
5870
7082
  return false;
5871
7083
  if (err instanceof SessionOomStoppedError)
5872
7084
  await this.notifySessionOomStop(name);
7085
+ if (this.holdNativePaneRecovery(name, err))
7086
+ return false;
5873
7087
  if (this.stopOnUnsupportedCli(name, err, what))
5874
7088
  return false;
5875
7089
  this.logger.error({ err, name }, `Failed to start ${what}`);
@@ -6011,6 +7225,8 @@ export class FleetManager {
6011
7225
  if (err instanceof SupersededStartError)
6012
7226
  return;
6013
7227
  // An earlier attempt could not tell (unknown probe); this one could.
7228
+ if (this.holdNativePaneRecovery(name, err))
7229
+ return;
6014
7230
  if (this.stopOnUnsupportedCli(name, err, "instance"))
6015
7231
  return;
6016
7232
  this.logger.error({ err, name, attempt: attempt + 1 }, "Automatic startup retry failed");
@@ -6261,7 +7477,7 @@ export class FleetManager {
6261
7477
  return count;
6262
7478
  }
6263
7479
  configuredStartupInstanceNames(fleet, includeClassic) {
6264
- const names = new Set(Object.keys(fleet.instances));
7480
+ const names = new Set(Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config)).map(([name]) => name));
6265
7481
  if (includeClassic) {
6266
7482
  for (const channel of this.classicChannels?.getAll() ?? [])
6267
7483
  names.add(channel.instanceName);
@@ -6312,6 +7528,10 @@ export class FleetManager {
6312
7528
  return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
6313
7529
  }
6314
7530
  async stopInstance(name, transition) {
7531
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
7532
+ this.nativeInstanceRuntime().setPaused(name, true);
7533
+ return { backgroundWorkEnded: false };
7534
+ }
6315
7535
  this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
6316
7536
  this.cancelStartupRetry(name);
6317
7537
  this.failoverActive.delete(name);
@@ -6351,6 +7571,16 @@ export class FleetManager {
6351
7571
  async restartSingleInstance(name, opts) {
6352
7572
  if (opts?.ownerCurrent && !opts.ownerCurrent())
6353
7573
  throw new RestartSupersededError(this.shownInstanceName(name));
7574
+ if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
7575
+ const runtime = this.nativeInstanceRuntime();
7576
+ if (runtime.get(name)?.blocked)
7577
+ throw new Error("Verify the disconnected Windows process stopped and recover this agent before restarting");
7578
+ runtime.cancel(name);
7579
+ runtime.setPaused(name, false);
7580
+ return;
7581
+ }
7582
+ if (opts?.explicit && isNativePaneInstance(this.fleetConfig?.instances[name]))
7583
+ this.lifecycle.resetNativeCrashBudget(name);
6354
7584
  const migration = this.kiroMigrationRuns?.get(name);
6355
7585
  if (migration && migration.token !== opts?.kiroMigration)
6356
7586
  throw new Error(t("kiro_migrate.in_progress"));
@@ -6414,7 +7644,7 @@ export class FleetManager {
6414
7644
  return run;
6415
7645
  }
6416
7646
  isInstanceRestarting(name) {
6417
- return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
7647
+ return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true || this.lifecycle?.nativeRecoveryPending?.(name) === true;
6418
7648
  }
6419
7649
  /** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
6420
7650
  restartInFlight(name) {
@@ -6709,6 +7939,9 @@ export class FleetManager {
6709
7939
  log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
6710
7940
  }
6711
7941
  publicOwnerCurrent(owner) {
7942
+ // #3008: the outside link is this computer's admin's — while "Allow connections from outside" is on, web-only.
7943
+ if (isOutsideLinkOwner(owner))
7944
+ return !this.shuttingDown && this.outsideOn();
6712
7945
  if (this.shuttingDown || !this.hasFleetAdmins(owner.adapterId) || !this.isFleetAdmin(owner.userId, owner.adapterId))
6713
7946
  return false;
6714
7947
  const configured = (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).find(ch => (ch.id ?? ch.type) === owner.adapterId);
@@ -6798,7 +8031,11 @@ export class FleetManager {
6798
8031
  }
6799
8032
  getPublicWebLink() {
6800
8033
  this.initializeWebSessions();
6801
- return this.publicWebLink ??= new PublicWebLink({
8034
+ return this.publicWebLink ??= new PublicWebLink(this.publicWebLinkDeps());
8035
+ }
8036
+ /** The public link's wiring to this fleet (a test builds one with the tunnel itself stubbed: `ensure`, `provider`). */
8037
+ publicWebLinkDeps() {
8038
+ return {
6802
8039
  dataDir: this.dataDir, web: () => this.fleetConfig?.web,
6803
8040
  permitted: owner => this.publicOwnerCurrent(owner),
6804
8041
  reserve: id => this.getTunnelLane().reserve("dashboard", id),
@@ -6831,7 +8068,9 @@ export class FleetManager {
6831
8068
  if (world && world.adapter === owner.binding)
6832
8069
  void world.adapter.sendText(owner.chatId, t("dashboard.public_cleanup"), { threadId: owner.threadId }).catch(() => { });
6833
8070
  },
6834
- });
8071
+ onClose: (owner, reason) => { if (isOutsideLinkOwner(owner))
8072
+ this.outsideLinkClosed(reason); },
8073
+ };
6835
8074
  }
6836
8075
  publicLinkAutoOpen = null;
6837
8076
  /**
@@ -8190,6 +9429,9 @@ export class FleetManager {
8190
9429
  const fleet = this.loadConfig(configPath, { startup: true });
8191
9430
  this.slimFleetConfigAtStartup();
8192
9431
  this.logConfigWarningsAtStartup();
9432
+ this.initializeNativeInstances();
9433
+ // M-2255: from here on a change to the Windows runtimes' switches applies live (observeRuntimeSwitches).
9434
+ this.runtimeEffects = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
8193
9435
  setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
8194
9436
  // #1737 review: the marker this boot adopts — the startup clear may only ever clear this one.
8195
9437
  const bootUpdateMarker = updateMarkerIdentity(this.dataDir);
@@ -8221,7 +9463,7 @@ export class FleetManager {
8221
9463
  // Pre-flight (advisory, fire-and-forget): warm the per-backend auth cache
8222
9464
  // before instances spawn, so a CLI that comes up on its sign-in screen gets
8223
9465
  // an instant 🔑 verdict instead of decaying into crash/MCP-died noise.
8224
- this.lifecycle.primeAuthVerification(Object.values(fleet.instances).map(config => {
9466
+ this.lifecycle.primeAuthVerification(Object.values(fleet.instances).filter(config => !isWindowsInstance(config)).map(config => {
8225
9467
  const backend = config.backend ?? fleet.defaults?.backend ?? "claude-code";
8226
9468
  // #1699 follow-up: each instance's own login — a profile is probed with its own environment.
8227
9469
  return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, fleet.defaults, backend) : null };
@@ -8283,6 +9525,7 @@ export class FleetManager {
8283
9525
  // #1565: the web chat as it was before this process — the configured instances' only (fleet.yaml and ClassicBot
8284
9526
  // rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
8285
9527
  this.restoreWebChat(fleet);
9528
+ this.initializeNativeInstances(true);
8286
9529
  this.startSearchIndex(fleet);
8287
9530
  // Parse-only source monitoring; it does not apply hand-edited Fleet settings.
8288
9531
  this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
@@ -8465,7 +9708,7 @@ export class FleetManager {
8465
9708
  }
8466
9709
  }
8467
9710
  // Phase 1: Start general instances first and wait for them
8468
- const allEntries = Object.entries(fleet.instances);
9711
+ const allEntries = Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config));
8469
9712
  const generals = allEntries.filter(([_, cfg]) => cfg.general_topic);
8470
9713
  const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
8471
9714
  const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
@@ -8704,6 +9947,10 @@ export class FleetManager {
8704
9947
  // #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
8705
9948
  this.startPublicLinkAutoOpen();
8706
9949
  }
9950
+ // #3008: connections from outside were on when the fleet stopped: the link opens again (its address is new for a
9951
+ // quick link — Settings shows it on this computer).
9952
+ if (this.outsideOn())
9953
+ this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
8707
9954
  // #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
8708
9955
  // #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
8709
9956
  void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
@@ -9467,10 +10714,14 @@ export class FleetManager {
9467
10714
  }
9468
10715
  }
9469
10716
  }
10717
+ const daemon = this.daemons.get(name);
10718
+ const native = daemon?.usesNativePane?.();
9470
10719
  const sockPath = join(this.getInstanceDir(name), "channel.sock");
9471
- if (!existsSync(sockPath))
10720
+ if (!native && !existsSync(sockPath))
9472
10721
  return;
9473
- const ipc = new IpcClient(sockPath);
10722
+ const ipc = native
10723
+ ? new NativeDaemonClient(daemon, () => this.daemons.get(name) === daemon && (!this.instanceIpcClients.has(name) || this.instanceIpcClients.get(name) === ipc))
10724
+ : new IpcClient(sockPath);
9474
10725
  try {
9475
10726
  await ipc.connect();
9476
10727
  this.instanceIpcClients.set(name, ipc);
@@ -9586,7 +10837,7 @@ export class FleetManager {
9586
10837
  // connected, so seed the cache instead of waiting for another transition.
9587
10838
  ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
9588
10839
  this.logger.debug({ name }, "Connected to instance IPC");
9589
- if (!this.statuslineWatcher.has(name)) {
10840
+ if (!native && !this.statuslineWatcher.has(name)) {
9590
10841
  this.statuslineWatcher.watch(name);
9591
10842
  }
9592
10843
  // Auto-reconnect on disconnect (unless intentionally stopping)
@@ -10633,6 +11884,12 @@ export class FleetManager {
10633
11884
  if (msg.adapterId)
10634
11885
  this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
10635
11886
  const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
11887
+ const locationHold = this.generalLocationDeliveryHold(generalInstance);
11888
+ if (locationHold) {
11889
+ await inboundAdapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
11890
+ this.needsYou?.poke();
11891
+ return;
11892
+ }
10636
11893
  // React immediately — before any other API calls — through the status
10637
11894
  // path, which tracks Telegram slot ownership and Discord status adds.
10638
11895
  if (msg.chatId && msg.messageId) {
@@ -10720,6 +11977,13 @@ export class FleetManager {
10720
11977
  if (instanceConfig?.general_topic && await this.topicCommands.handleGeneralCommand(msg, instanceName)) {
10721
11978
  return;
10722
11979
  }
11980
+ const locationHold = this.generalLocationDeliveryHold(instanceName);
11981
+ if (locationHold) {
11982
+ const adapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
11983
+ await adapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
11984
+ this.needsYou?.poke();
11985
+ return;
11986
+ }
10723
11987
  // Bind instance to the adapter that delivered this message
10724
11988
  if (msg.adapterId)
10725
11989
  this.bindInstanceAdapter(instanceName, msg.adapterId, true);
@@ -12649,6 +13913,7 @@ export class FleetManager {
12649
13913
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
12650
13914
  this.refreshSavedDashboardPermissions();
12651
13915
  this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
13916
+ this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
12652
13917
  }
12653
13918
  /** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
12654
13919
  refreshSavedDashboardPermissions() {
@@ -12882,10 +14147,43 @@ export class FleetManager {
12882
14147
  }
12883
14148
  getSettingsConfigPath() { return this.configPath; }
12884
14149
  async removeInstance(name, authorization, execution) {
14150
+ if (isWindowsInstance(this.fleetConfig?.instances[name])) {
14151
+ execution?.assert();
14152
+ assertExplicitInstanceRemoval(authorization);
14153
+ if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
14154
+ await this.stopInstance(name);
14155
+ execution?.assert();
14156
+ }
14157
+ this.nativeInstanceRuntime().remove(name, () => !this.shuttingDown && (!execution || execution.current()), execution);
14158
+ return;
14159
+ }
12885
14160
  if (execution)
12886
14161
  return this.lifecycle.runTransition(name, transition => this.removeInstanceOwned(name, authorization, execution, transition));
12887
14162
  return this.removeInstanceOwned(name, authorization);
12888
14163
  }
14164
+ /** Shared instance metadata goes with a native identity; its Windows workspace and provider sessions stay intact. */
14165
+ nativeInstanceRemoved(name) {
14166
+ const clean = (what, run) => { try {
14167
+ run();
14168
+ }
14169
+ catch (error) {
14170
+ this.logger.warn({ name, err: error }, `Native instance ${what} cleanup failed`);
14171
+ } };
14172
+ clean("delivery", () => { this.cancelPendingDeliveries(name); });
14173
+ forgetInstanceContext(name);
14174
+ clean("schedules", () => { for (const schedule of this.scheduler?.list(name) ?? [])
14175
+ this.scheduler.delete(schedule.id); });
14176
+ this.lastActivity.delete(name);
14177
+ this.lastInboundUser.delete(name);
14178
+ this.rateLimitWarnedAt.delete(name);
14179
+ this.lastInboundMsg.delete(name);
14180
+ clean("history", () => this.webChatHistory.forget(name));
14181
+ clean("search", () => this.searchIndex?.removeInstance(name));
14182
+ clean("files", () => this.webFiles.forget(name));
14183
+ clean("compact", () => this.compactWatchService?.forget(name));
14184
+ clean("preferences", () => this.webPrefs.forgetInstance(name));
14185
+ clean("appearance", () => { this.themeStore.forget(name); this.webAppearance.forget(name); this.appearanceTools.forget(name); this.mods.forgetInstance(name); this.officeActsInstance?.forget(name); this.officePackToolsInstance?.forget(name); });
14186
+ }
12889
14187
  async removeInstanceOwned(name, authorization, execution, transition) {
12890
14188
  execution?.assert();
12891
14189
  assertExplicitInstanceRemoval(authorization);
@@ -13011,6 +14309,8 @@ export class FleetManager {
13011
14309
  execution?.complete();
13012
14310
  }
13013
14311
  startStatuslineWatcher(name) {
14312
+ if (isWindowsInstance(this.fleetConfig?.instances[name]))
14313
+ return;
13014
14314
  if (this.lifecycle.isPaused(name))
13015
14315
  return;
13016
14316
  this.statuslineWatcher.watch(name);
@@ -14459,6 +15759,10 @@ export class FleetManager {
14459
15759
  * would have to make this async and change every caller.
14460
15760
  */
14461
15761
  notifyInstanceTopic(instanceName, text, extraOpts) {
15762
+ if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
15763
+ this.emitSseEvent("message", { instance: instanceName, role: "status", sender: "AgEnD", text, ts: new Date().toISOString() });
15764
+ return true;
15765
+ }
14462
15766
  // #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
14463
15767
  if (this.webOnlyPromptPlace())
14464
15768
  return this.recordWebNotice("instance", text, instanceName);
@@ -14939,6 +16243,8 @@ export class FleetManager {
14939
16243
  * (also for a backend that has no profiles, where the option is ignored).
14940
16244
  */
14941
16245
  credentialProfileOf(name, backend) {
16246
+ if (isWindowsInstance(this.fleetConfig?.instances[name]))
16247
+ return null;
14942
16248
  if (!credentialHomeSpec(backend))
14943
16249
  return null;
14944
16250
  const cfg = this.fleetConfig?.instances?.[name];
@@ -14961,7 +16267,7 @@ export class FleetManager {
14961
16267
  }
14962
16268
  /** The instances the cache analysis reads: fleet.yaml's, then the ClassicBot rooms (their workspace under the AgEnD home, their own backend). */
14963
16269
  cacheInstances() {
14964
- const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
16270
+ const fleet = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => !isWindowsInstance(cfg)).map(([name, cfg]) => ({
14965
16271
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
14966
16272
  claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
14967
16273
  kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
@@ -15140,6 +16446,20 @@ export class FleetManager {
15140
16446
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
15141
16447
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
15142
16448
  usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
16449
+ nativeDeliveryHold: this.daemons.get(name)?.nativeDeliveryHold?.() ?? null,
16450
+ generalLocation: this.generalLocationAttention(name),
16451
+ paneRecovery: (() => {
16452
+ const config = this.fleetConfig?.instances[name];
16453
+ if (!isNativePaneInstance(config) || !config.native_agent_id)
16454
+ return null;
16455
+ try {
16456
+ const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
16457
+ return profile.paneRecovery ? { key: `${profile.paneRecovery.kind}:${profile.paneRecovery.launchId ?? "-"}`, detail: new NativePaneRecoveryError(profile.paneRecovery).message, since: profile.paneRecoveryAt ?? profile.createdAt } : null;
16458
+ }
16459
+ catch {
16460
+ return null;
16461
+ }
16462
+ })(),
15143
16463
  });
15144
16464
  }
15145
16465
  return out;
@@ -15195,6 +16515,8 @@ export class FleetManager {
15195
16515
  const target = this.fleetNoticeTarget(world);
15196
16516
  return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
15197
16517
  },
16518
+ // The name the web page shows for the instance (its View alias, else its own name), never the raw id alone.
16519
+ instanceName: instance => resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[instance], profile: profileIdentities(this.dataDir).get(instance) }).display_name || instance,
15198
16520
  instanceTopic: instance => {
15199
16521
  const topic = this.fleetConfig?.instances[instance]?.topic_id;
15200
16522
  if (topic != null)
@@ -15739,7 +17061,7 @@ export class FleetManager {
15739
17061
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
15740
17062
  authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
15741
17063
  policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
15742
- hostOperator: this.isWebOnlyFleet(),
17064
+ hostOperator: this.isWebOnlyFleet(), outside: this.outsideLink(),
15743
17065
  });
15744
17066
  }
15745
17067
  sessionAuthorizations = null;
@@ -15799,6 +17121,264 @@ export class FleetManager {
15799
17121
  const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
15800
17122
  this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
15801
17123
  }
17124
+ // ── #3008: connections from outside (a web-only fleet; decision 28c2d3a3 rule 3, the user's simplified form) ──
17125
+ webRemoteAccessState = null;
17126
+ /** The authenticator, the outside switch and the wrong-code limiter (web-remote-access.json), read on first use. */
17127
+ webRemoteAccess() {
17128
+ if (!this.dataDir)
17129
+ return null;
17130
+ this.webRemoteAccessState ??= new WebRemoteAccess(join(this.dataDir, WEB_REMOTE_ACCESS_FILE), this.logger);
17131
+ return this.webRemoteAccessState;
17132
+ }
17133
+ /** "Allow connections from outside" is on (an app set up) and this is a web-only fleet. */
17134
+ outsideOn() {
17135
+ return this.isWebOnlyFleet() && (this.webRemoteAccess()?.outside ?? false);
17136
+ }
17137
+ /** The link connections from outside opened, while it is open and still theirs: its exposure. Else null. */
17138
+ outsideLink() {
17139
+ if (!this.outsideOn())
17140
+ return null;
17141
+ const open = this.publicWebLink?.openSignin();
17142
+ return open && isOutsideLinkOwner(open.owner) ? { exposureId: open.exposureId } : null;
17143
+ }
17144
+ /**
17145
+ * The request's live session, asked again each time `current()` is called (its own cookie authenticated anew, the same
17146
+ * handle): this web-only fleet's owner — this computer's admin, on this computer (`hostOnly`), or also signed in from
17147
+ * outside — and web-verified now. Null when it is not (a guest, a LAN session, a chat fleet's admin, nobody).
17148
+ */
17149
+ liveOwner(req, opts) {
17150
+ if (!this.isWebOnlyFleet())
17151
+ return null;
17152
+ if (opts.hostOnly && gatewayRequestContext(req))
17153
+ return null;
17154
+ const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
17155
+ if (auth.kind !== "ok")
17156
+ return null;
17157
+ const handle = auth.session.handle, peer = webPeerOf(req), gateway = !!gatewayRequestContext(req);
17158
+ const owner = () => {
17159
+ if (!this.isWebOnlyFleet())
17160
+ return false;
17161
+ const live = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
17162
+ if (live.kind !== "ok" || live.session.handle !== handle)
17163
+ return false;
17164
+ if (opts.hostOnly && (live.session.host !== true || peer !== "loopback"))
17165
+ return false;
17166
+ const now = this.webAuthority({ session: live.session, gateway, peer });
17167
+ const a = now.authority.kind === "web-verified" && now.authority.session?.handle === handle ? now.authority.actor : null;
17168
+ return !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId;
17169
+ };
17170
+ return owner() ? { label: t("settings_confirm.host_operator_label"), handle, current: owner } : null;
17171
+ }
17172
+ /** Every session signed in from outside ends (and the authorizations kept for them). */
17173
+ endOutsideSessions() {
17174
+ const ended = new Set(this.webSessions?.list(undefined, r => r.remote === "totp").map(r => r.handle) ?? []);
17175
+ if (!ended.size)
17176
+ return;
17177
+ this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
17178
+ const result = this.webSessions?.revokeAll(r => r.remote === "totp");
17179
+ if (result && !result.durable)
17180
+ this.logger.warn("Outside sessions ended, but that could not be saved");
17181
+ this.logger.info({ count: ended.size }, "Sessions signed in from outside ended");
17182
+ }
17183
+ /**
17184
+ * #1939 §10 / #3008: a stored receipt's authority as it reads now — the web authority state, and for this computer's
17185
+ * admin from outside the switch and the last revocation of remote authority (Prism P2 2).
17186
+ */
17187
+ receiptAuthorityNow(stored, state = this.webAuthorityState()) {
17188
+ return authorityNow(stored, { ...state, outside: this.outsideOn(), outsideHandles: this.webRemoteAccess()?.admitted ?? new Set() });
17189
+ }
17190
+ /** #3008 (the sign-in page): this exposure is the outside link, open now. */
17191
+ isOutsideLink(exposureId) {
17192
+ return this.outsideLink()?.exposureId === exposureId;
17193
+ }
17194
+ /** What Settings shows: set up or not, the switch, a wait in force, the link — its address only to this computer. */
17195
+ webOutsideStatus(req) {
17196
+ const store = this.webRemoteAccess(), status = store?.status();
17197
+ const here = !!this.liveOwner(req, { hostOnly: true });
17198
+ const open = this.outsideOn() ? this.publicWebLink?.openSignin() : null;
17199
+ const mine = !!open && isOutsideLinkOwner(open.owner);
17200
+ const linkStatus = this.outsideOn() ? this.getPublicWebStatus() : null;
17201
+ return { webOnly: this.isWebOnlyFleet(), here, enrolled: !!status?.enrolled, enrolledAt: status?.enrolledAt ?? null,
17202
+ outside: this.outsideOn(), pausedForMs: status?.pausedForMs ?? 0,
17203
+ link: linkStatus && linkStatus.state !== "closed" ? { ...linkStatus, ...(here && mine ? { url: open.url } : {}), quick: !publicLinkNamedHostname(this.fleetConfig?.web) } : null };
17204
+ }
17205
+ /** Set up (or set up again): a new secret, shown once to this computer's admin. Null: not them. */
17206
+ webOutsideBeginEnrollment(req) {
17207
+ const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17208
+ if (!owner || !store)
17209
+ return null;
17210
+ return store.beginEnrollment(owner.handle, hostname() || "fleet");
17211
+ }
17212
+ /** The new app's first code: on "ok" it replaces the old one, whose sessions from outside end. */
17213
+ webOutsideConfirmEnrollment(req, code) {
17214
+ const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17215
+ if (!owner || !store || !owner.current())
17216
+ return "not_here";
17217
+ const had = store.enrolled;
17218
+ const r = store.confirmEnrollment(owner.handle, code);
17219
+ if (r === "ok") {
17220
+ this.logger.info({ again: had }, "Authenticator app set up for connections from outside");
17221
+ if (had)
17222
+ this.endOutsideSessions();
17223
+ }
17224
+ return r;
17225
+ }
17226
+ /** Forget the app: the switch goes off, the link closes, every session from outside ends. */
17227
+ webOutsideRemove(req) {
17228
+ const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17229
+ if (!owner || !store || !owner.current())
17230
+ return "not_here";
17231
+ // #3008 review (Prism P2 1): shut at once whatever the disk says; say so when it could not be saved (retried).
17232
+ const saved = store.remove();
17233
+ this.closeOutsideLink("outside removed");
17234
+ this.logger.info({ saved }, "Authenticator app removed; connections from outside off");
17235
+ return saved ? "ok" : "unsaved";
17236
+ }
17237
+ /** The switch. On: needs an app set up; the link opens. Off: it closes and every session from outside ends. */
17238
+ webOutsideSwitch(req, on) {
17239
+ const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
17240
+ if (!owner || !store || !owner.current())
17241
+ return "not_here";
17242
+ if (on && !publicLinkSettings(this.fleetConfig?.web).allowed)
17243
+ return "not_allowed";
17244
+ const r = store.setOutside(on);
17245
+ if (r === "not_enrolled")
17246
+ return r;
17247
+ this.logger.info({ on, saved: r === "ok" }, "Connections from outside switched");
17248
+ // #3008 review (Prism P2 1): Off shuts at once even when it could not be saved (the store retries); On opens only
17249
+ // once saved — a switch the disk does not hold would not survive a restart.
17250
+ if (!on)
17251
+ this.closeOutsideLink("outside off");
17252
+ else if (r === "ok")
17253
+ this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
17254
+ return r;
17255
+ }
17256
+ outsideAttempt = null;
17257
+ outsideOpening = false;
17258
+ /**
17259
+ * Open the outside link: an attempt after each delay in turn until one opens (#3008 B: twice more at most — the quick
17260
+ * link's address service limits how often it may be asked). Every attempt asks again: the switch still on, the fleet
17261
+ * running. A final failure: one Notice; the switch stays on, so the next start tries again.
17262
+ */
17263
+ openOutsideLink(delays) {
17264
+ this.outsideAttempt?.cancel();
17265
+ this.outsideAttempt = null;
17266
+ const attempt = (n) => {
17267
+ this.outsideAttempt = null;
17268
+ if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
17269
+ return;
17270
+ this.outsideOpening = true;
17271
+ void this.getPublicWebLink().openUnattended(OUTSIDE_LINK_OWNER).catch(() => "failed").then(outcome => {
17272
+ this.outsideOpening = false;
17273
+ if (outcome === "opened") {
17274
+ this.logger.info({ attempt: n + 1 }, "Outside link opened");
17275
+ return;
17276
+ }
17277
+ if (outcome === "busy" && this.outsideLink())
17278
+ return; // already open (ours)
17279
+ if (this.shuttingDown || !this.outsideOn())
17280
+ return;
17281
+ if (n + 1 < delays.length) {
17282
+ this.outsideAttempt = outsideTimer(() => attempt(n + 1), delays[n + 1]);
17283
+ return;
17284
+ }
17285
+ this.logger.warn({ attempts: n + 1 }, "Outside link could not be opened; giving up until the next start or switch");
17286
+ this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_failed")));
17287
+ });
17288
+ };
17289
+ this.outsideAttempt = outsideTimer(() => attempt(0), delays[0] ?? 0);
17290
+ }
17291
+ closeOutsideLink(reason) {
17292
+ this.outsideAttempt?.cancel();
17293
+ this.outsideAttempt = null;
17294
+ const open = this.publicWebLink?.openSignin();
17295
+ if (this.publicWebLink && (!open || isOutsideLinkOwner(open.owner)))
17296
+ void this.publicWebLink.close(reason);
17297
+ this.endOutsideSessions();
17298
+ }
17299
+ /** PublicWebLink.onClose for the outside link: what closed it decides what happens next (#3008 B). */
17300
+ outsideLinkClosed(reason) {
17301
+ if (this.shuttingDown || OUTSIDE_OWN_CLOSES.has(reason) || !this.outsideOn())
17302
+ return;
17303
+ // #3008 review (Prism P2 3): expiry first — its lifetime counts from the opening request, so it can run out while the
17304
+ // opening is still under way: the switch goes off then too, and that attempt's end (outsideOn false) retries nothing.
17305
+ if (reason === "expired") {
17306
+ // Its lifetime ran out: the switch goes off (only this computer turns it on again — a session from outside can't).
17307
+ this.outsideAttempt?.cancel();
17308
+ this.outsideAttempt = null;
17309
+ if (this.webRemoteAccess()?.setOutside(false) === "unsaved")
17310
+ this.logger.warn("Outside link expired; the switch's Off could not be saved yet (retried)");
17311
+ this.endOutsideSessions();
17312
+ this.logger.info("Outside link expired; connections from outside off");
17313
+ this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_expired")));
17314
+ return;
17315
+ }
17316
+ if (this.outsideOpening)
17317
+ return; // that attempt's own failure: its retry
17318
+ // Dropped (the tunnel exited, the gateway failed, the configuration changed): reopened up to twice.
17319
+ this.logger.info({ reason }, "Outside link closed; reopening");
17320
+ this.openOutsideLink(OUTSIDE_LINK_RETRY_MS);
17321
+ }
17322
+ /**
17323
+ * #3008 (auth-api POST /auth/totp): a sign-in through the outside link with the authenticator's code. "ok": the caller
17324
+ * makes the session (SessionRecord.remote) and then calls authorizeOutsideSession. Each wrong code counts; a wait that
17325
+ * starts is a Notice; a code that matches a step just outside the window logs how far this clock seems to be off.
17326
+ */
17327
+ outsideSignIn(exposureId, code) {
17328
+ const link = this.outsideLink(), store = this.webRemoteAccess();
17329
+ if (!link || !store || !exposureId || link.exposureId !== exposureId)
17330
+ return { kind: "closed" };
17331
+ const v = store.verify(code);
17332
+ if (v.kind === "ok")
17333
+ return { kind: "ok" };
17334
+ if (v.kind === "paused")
17335
+ return v;
17336
+ if (v.kind === "not_enrolled")
17337
+ return { kind: "closed" };
17338
+ if (v.kind === "unavailable")
17339
+ return v;
17340
+ if (v.skewSteps !== null)
17341
+ this.logger.warn({ offsetSeconds: v.skewSteps * TOTP_STEP_SECONDS }, "A wrong authenticator code matched a nearby time step: this computer's clock may be off");
17342
+ if (v.paused) {
17343
+ this.logger.warn({ waitMs: v.retryAfterMs }, "Sign-in from outside paused after wrong codes");
17344
+ this.recordWebNotice("fleet", t("outside.notice_paused", outsideDuration(v.retryAfterMs), v.wrong));
17345
+ return { kind: "paused", retryAfterMs: v.retryAfterMs };
17346
+ }
17347
+ return { kind: "wrong" };
17348
+ }
17349
+ /**
17350
+ * #3008: a session just made by outsideSignIn's "ok" is this computer's admin for its authorization's life. False: it
17351
+ * could not be admitted (its handle not saved, #3015 review r3) — the caller ends it.
17352
+ */
17353
+ authorizeOutsideSession(session, peer, label) {
17354
+ const store = this.sessionAuthorizationStore(), link = this.outsideLink();
17355
+ if (!store || session.remote !== "totp" || session.owner || !link || session.exposureId !== link.exposureId)
17356
+ return false;
17357
+ if (!this.webRemoteAccess()?.admit(session.handle)) {
17358
+ this.logger.warn({ handle: session.handle }, "Session from outside not admitted: it could not be saved");
17359
+ return false;
17360
+ }
17361
+ const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
17362
+ const check = evaluateWebAuthority({
17363
+ session, gateway: true, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
17364
+ isAdmin: () => false,
17365
+ authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
17366
+ policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
17367
+ hostOperator: true, outside: link,
17368
+ });
17369
+ if (check.refusal) {
17370
+ this.logger.info({ handle: session.handle, refusal: check.refusal }, "Session from outside not authorized");
17371
+ return false;
17372
+ }
17373
+ {
17374
+ const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
17375
+ store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
17376
+ const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
17377
+ this.logger.info({ handle: session.handle, durable }, "Session from outside authorized (this computer's admin)");
17378
+ }
17379
+ this.recordWebNotice("fleet", t("outside.notice_signed_in", label));
17380
+ return true;
17381
+ }
15802
17382
  authorityStateStore = null;
15803
17383
  /**
15804
17384
  * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
@@ -15819,6 +17399,10 @@ export class FleetManager {
15819
17399
  return true;
15820
17400
  if (session.surface === "local")
15821
17401
  return false;
17402
+ // #3008 review (Prism P2 4): signed in from outside with the authenticator's code — its opt-in is the outside
17403
+ // switch, not these options (the authority judges it so): it keeps its authorization.
17404
+ if (session.remote === "totp")
17405
+ return false;
15822
17406
  const peer = a.peer ?? "named_link";
15823
17407
  return (!!state.namedLinkClosed && peer === "named_link") || (!!state.quickLinkClosed && peer === "quick_link");
15824
17408
  };
@@ -15968,7 +17552,7 @@ export class FleetManager {
15968
17552
  authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
15969
17553
  policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
15970
17554
  // #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
15971
- hostOperator: this.isWebOnlyFleet(),
17555
+ hostOperator: this.isWebOnlyFleet(), outside: this.outsideLink(),
15972
17556
  });
15973
17557
  return r.refusal;
15974
17558
  }
@@ -16834,6 +18418,8 @@ export class FleetManager {
16834
18418
  publication.retirePending = true;
16835
18419
  }
16836
18420
  async sendCancelButton(instanceName, correlationId, preserveProgress = false) {
18421
+ if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
18422
+ return;
16837
18423
  // Post first, retire after (see the tail of this method). Retiring up front
16838
18424
  // meant that from the delete until the new message came back — a chat API
16839
18425
  // round trip, and every reply goes through here — the instance had NO live
@@ -17366,6 +18952,14 @@ export class FleetManager {
17366
18952
  }
17367
18953
  /** Interrupt an instance's current generation (cancel button / /cancel). */
17368
18954
  cancelInstance(instanceName) {
18955
+ if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
18956
+ this.cancelPendingDeliveries(instanceName);
18957
+ const cancelled = this.nativeInstanceRuntime().cancel(instanceName);
18958
+ this.lastInboundMsg.delete(instanceName);
18959
+ for (const message of this.webChatHistory.cancelPending(instanceName))
18960
+ this.emitSseEvent("delivery", { instance: instanceName, messageId: message.messageId, delivery: message.delivery });
18961
+ return cancelled !== null;
18962
+ }
17369
18963
  const daemon = this.daemons.get(instanceName);
17370
18964
  if (!daemon)
17371
18965
  return false;
@@ -18583,36 +20177,7 @@ export class FleetManager {
18583
20177
  "antigravity": ".agents/AGENTS.md",
18584
20178
  "mock": "CLAUDE.md",
18585
20179
  };
18586
- static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
18587
-
18588
- You are the fleet coordinator — the central entry point for this AgEnD fleet.
18589
- You route tasks, manage instances, enforce policies, and synthesize results.
18590
- Do NOT modify project files directly — delegate file changes to the project's instance.
18591
- You CAN write code snippets, explain code, and answer technical questions directly.
18592
-
18593
- ## Task Routing
18594
-
18595
- - **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
18596
- - **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
18597
- - **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
18598
-
18599
- Instance discovery: start with list_instances(); follow the guidance in its response. Prefer reuse; never duplicate a running instance.
18600
-
18601
- ## Reply Contract
18602
-
18603
- Every final response to the user contains: the result (the actual answer or deliverable) and gaps (anything incomplete — omit if none). Summarize instance reports; omit internal coordination noise.
18604
-
18605
- ## After Restart
18606
-
18607
- BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
18608
-
18609
- ## Playbooks (on-demand skills)
18610
-
18611
- Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
18612
- - **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
18613
- - **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
18614
- Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
18615
- `;
20180
+ static GENERAL_INSTRUCTIONS = GENERAL_ROLE_INSTRUCTIONS;
18616
20181
  /** Ensure the general instance has its project instructions file + knowledge */
18617
20182
  ensureGeneralInstructions(workDir, backendName, instanceName) {
18618
20183
  measureSyncWork("fleet.generalInstructions", () => this.ensureGeneralInstructionsSync(workDir, backendName, instanceName));
@@ -19634,6 +21199,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19634
21199
  : model,
19635
21200
  });
19636
21201
  const fleetInstance = this.fleetConfig?.instances[instanceName];
21202
+ if (isWindowsInstance(fleetInstance))
21203
+ return fleetInstance.model?.trim() ? done(fleetInstance.model.trim(), "instance") : done("default", "unresolved", "this CLI does not report a default");
19637
21204
  if (fleetInstance) {
19638
21205
  if (fleetInstance.model?.trim())
19639
21206
  return done(fleetInstance.model.trim(), "instance");
@@ -19715,7 +21282,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19715
21282
  if (this.fleetConfig?.defaults?.backend)
19716
21283
  backends.add(this.fleetConfig.defaults.backend);
19717
21284
  for (const inst of Object.values(this.fleetConfig?.instances ?? {}))
19718
- if (inst.backend)
21285
+ if (!isWindowsInstance(inst) && inst.backend)
19719
21286
  backends.add(inst.backend);
19720
21287
  for (const ch of this.classicChannels?.getAll() ?? [])
19721
21288
  if (ch.backend)
@@ -19730,6 +21297,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19730
21297
  // here; a ClassicBot room on a profile is probed on its first /model (catalogProfile knows rooms too).
19731
21298
  const profiles = new Set();
19732
21299
  for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
21300
+ if (isWindowsInstance(this.fleetConfig?.instances[name]))
21301
+ continue;
19733
21302
  if (this.backendNameForInstance(name) !== "claude-code")
19734
21303
  continue;
19735
21304
  const profile = this.catalogProfile("claude-code", name);
@@ -19750,6 +21319,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19750
21319
  * instead of presenting an old list as a fresh one.
19751
21320
  */
19752
21321
  async getModelOptionsWithSource(instanceName, opts = {}) {
21322
+ if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
21323
+ return { models: (await this.listModelCatalog({ instanceName })).models, source: "cache" };
19753
21324
  const backendName = this.backendNameForInstance(instanceName);
19754
21325
  const profile = this.catalogProfile(backendName, instanceName);
19755
21326
  const cached = this.readCliEnv(backendName, profile);
@@ -19805,6 +21376,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19805
21376
  */
19806
21377
  async listModelCatalog(opts = {}) {
19807
21378
  const { instanceName } = opts;
21379
+ if (instanceName && isWindowsInstance(this.fleetConfig?.instances[instanceName])) {
21380
+ const config = this.fleetConfig.instances[instanceName], backend = config.backend ?? "codex";
21381
+ // Detection is admin-owned. A read never launches a provider or falls back to this host's account.
21382
+ let inventory = null;
21383
+ try {
21384
+ inventory = await this.runtimeWindowsExecutor().probe({ force: false, machineId: config.machine_id });
21385
+ }
21386
+ catch { /* offline remains unavailable */ }
21387
+ const row = inventory?.clis.find(value => value.id === backend);
21388
+ return { backend, scope: "instance", instance: instanceName, current_model: config.model || null,
21389
+ models: (row?.models ?? []).map(value => ({ id: value.id, label: value.name ?? value.id, ...(value.description ? { description: value.description } : {}) })),
21390
+ source: row?.models?.length ? "cache" : "fallback",
21391
+ ...(inventory?.probedAt ? { probed_at: new Date(inventory.probedAt).toISOString() } : {}),
21392
+ note: "Windows runtime's last detected inventory; no coordinator account or live probe is used." };
21393
+ }
19808
21394
  if (instanceName) {
19809
21395
  const backend = this.backendNameForInstance(instanceName);
19810
21396
  const resolved = this.resolveInstanceModel(instanceName);
@@ -19840,6 +21426,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19840
21426
  }
19841
21427
  /** Ask a backend for its catalog using ONE instance's real config. Never throws. */
19842
21428
  async instanceScopedModels(instanceName, backend) {
21429
+ if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
21430
+ return [];
19843
21431
  try {
19844
21432
  const inst = this.fleetConfig?.instances?.[instanceName];
19845
21433
  const instanceDir = this.getInstanceDir(instanceName);
@@ -20477,6 +22065,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20477
22065
  }
20478
22066
  /** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
20479
22067
  instanceEffortMetadata(instanceName) {
22068
+ if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
22069
+ return { strategy: "unsupported", levels: [] };
20480
22070
  const backend = this.backendNameForInstance(instanceName);
20481
22071
  let model = null;
20482
22072
  if (backend === "antigravity") {
@@ -20487,6 +22077,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20487
22077
  }
20488
22078
  /** Configured effort for an instance: per-instance, else fleet default, else none. */
20489
22079
  resolveInstanceEffort(instanceName) {
22080
+ if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
22081
+ return { effort: null, source: "unset" };
20490
22082
  const own = this.fleetConfig?.instances[instanceName]?.effort;
20491
22083
  if (own)
20492
22084
  return { effort: own, source: "instance" };
@@ -20524,9 +22116,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20524
22116
  * instance respawns.
20525
22117
  */
20526
22118
  async applyEffort(instanceName, requested, opts) {
20527
- // #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
22119
+ // #1490 row 27 (main): a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
20528
22120
  if (opts?.current && !opts.current())
20529
22121
  return t("menu.instance_replaced", this.shownInstanceName(instanceName));
22122
+ if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
22123
+ return t("effort.unsupported", this.backendNameForInstance(instanceName));
20530
22124
  const level = requested.trim().toLowerCase();
20531
22125
  const backendName = this.backendNameForInstance(instanceName);
20532
22126
  let strategy = "unsupported";
@@ -20574,6 +22168,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20574
22168
  if (/[\x00-\x1f\x7f]/.test(model)) {
20575
22169
  return t("model.invalid_chars");
20576
22170
  }
22171
+ const native = this.fleetConfig?.instances[instanceName];
22172
+ if (isWindowsInstance(native)) {
22173
+ if (!isNativePaneInstance(native))
22174
+ return WINDOWS_CONFIG_REFUSED;
22175
+ const catalog = await this.listModelCatalog({ instanceName });
22176
+ if (!catalog.models.some(row => row.id === model))
22177
+ return t("model.unknown_name", model, catalog.models.map(row => row.id).join(", "));
22178
+ const id = native.native_agent_id, machine = native.machine_id;
22179
+ await this.updateNativePaneConfig(instanceName, { model }, () => {
22180
+ const now = this.fleetConfig?.instances[instanceName];
22181
+ return isNativePaneInstance(now) && now.native_agent_id === id && now.machine_id === machine;
22182
+ });
22183
+ return t("model.restart_success", instanceName, model);
22184
+ }
20577
22185
  const backendName = this.backendNameForInstance(instanceName);
20578
22186
  let strategy = "restart";
20579
22187
  try {
@@ -20762,6 +22370,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20762
22370
  * profile's, when it is named and has one), claude's account catalog included. Read only: never a probe.
20763
22371
  */
20764
22372
  modelCatalogFor(backend, instance) {
22373
+ if (instance && isWindowsInstance(this.fleetConfig?.instances[instance]))
22374
+ return [];
20765
22375
  if (!KNOWN_BACKENDS.includes(backend) || backend === "mock")
20766
22376
  return [];
20767
22377
  const own = instance && Object.prototype.hasOwnProperty.call(this.fleetConfig?.instances ?? {}, instance) ? instance
@@ -20775,6 +22385,40 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20775
22385
  }
20776
22386
  return out;
20777
22387
  }
22388
+ /** Host-default fallback only: Grok has no modeled profile home. Never construct a backend or start a probe. */
22389
+ async cliModelCatalogFor(backend, instance) {
22390
+ if (backend !== "grok")
22391
+ return [];
22392
+ const fleet = this.fleetConfig;
22393
+ const configured = instance && Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) ? fleet.instances[instance] : undefined;
22394
+ const classic = this.classicChannels;
22395
+ const room = instance && !configured ? classic?.getAll().find(channel => channel.instanceName === instance) : undefined;
22396
+ if (instance && !configured && !room)
22397
+ return [];
22398
+ const daemon = room ? this.daemons.get(instance) : undefined;
22399
+ const grokHome = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
22400
+ const stillHostDefault = () => {
22401
+ if (this.fleetConfig !== fleet || (process.env.GROK_HOME?.trim() || join(homedir(), ".grok")) !== grokHome)
22402
+ return false;
22403
+ if (configured && (isWindowsInstance(configured) || !Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) || fleet.instances[instance] !== configured))
22404
+ return false;
22405
+ if (room && (this.classicChannels !== classic || classic?.getAll().find(channel => channel.instanceName === instance) !== room || this.daemons.get(instance) !== daemon))
22406
+ return false;
22407
+ // The ordinary profile helpers deliberately ignore Grok: it has no credential-home mapping yet.
22408
+ // Check raw declarations, including invalid ones, rather than mistaking them for the host login.
22409
+ const defaults = fleet?.defaults?.backend_options;
22410
+ const profiles = configured
22411
+ ? [configured.backend_options?.grok?.credential_profile, defaults?.grok?.credential_profile]
22412
+ : room
22413
+ ? [mergeBackendOptions(defaults, room.backendOptions)?.grok?.credential_profile, daemon?.getConfigSnapshot?.().backend_options?.grok?.credential_profile]
22414
+ : [defaults?.grok?.credential_profile];
22415
+ return profiles.every(profile => profile === undefined || profile === null || profile === "");
22416
+ };
22417
+ if (!stillHostDefault())
22418
+ return [];
22419
+ const models = await readGrokModelCache(grokHome);
22420
+ return stillHostDefault() ? models : [];
22421
+ }
20778
22422
  /**
20779
22423
  * #2072 (#2078 review): the snapshot as THIS instance would see it. Its sign-in on a backend is its own credential
20780
22424
  * profile's when it has one there — the host's default login says nothing about it. A profile's login is a file check
@@ -21433,6 +23077,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21433
23077
  * settles, so a later genuine stop (after a restart) still does the work.
21434
23078
  */
21435
23079
  stopAll() {
23080
+ this.lifecycle.cancelNativeRecovery?.();
23081
+ this.nativeMcpStore?.closeAll();
23082
+ this.runtimeExecutor?.close();
23083
+ this.runtimeExecutor = null;
23084
+ this.fleetWindowsRuntime = null;
23085
+ this.runtimeHub?.close();
23086
+ this.runtimeHub = null;
23087
+ this.localPaneRelay?.stop();
23088
+ this.localPaneRelay = null;
23089
+ closeWindowsBridge(this);
21436
23090
  this.stopAllInFlight ??= this.doStopAll().finally(() => {
21437
23091
  // #1956: after the stop's own tmux work; anything later goes direct. A later startAll starts it again.
21438
23092
  if (this.tmuxExecHelperTimer) {
@@ -21511,6 +23165,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21511
23165
  const settingsControlStopped = settingsControl?.close();
21512
23166
  this.publicLinkAutoOpen?.stop();
21513
23167
  const publicStopped = this.publicWebLink?.close("fleet shutdown");
23168
+ this.webRemoteAccessState?.close(); // #3008: an unsaved switch gets its last write
21514
23169
  const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
21515
23170
  // #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
21516
23171
  // adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
@@ -21893,6 +23548,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21893
23548
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21894
23549
  this.loadedFleetSource = previousSource;
21895
23550
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
23551
+ this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
21896
23552
  this.logger.error({
21897
23553
  oldCount,
21898
23554
  newCount,
@@ -23528,7 +25184,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23528
25184
  const topicMode = daemonTopicMode(fleet);
23529
25185
  const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
23530
25186
  // Phase 1: generals first
23531
- const restartEntries = Object.entries(fleet.instances);
25187
+ const restartEntries = Object.entries(fleet.instances).filter(([, config]) => !isNativeExecInstance(config));
23532
25188
  const restartGenerals = restartEntries.filter(([_, cfg]) => cfg.general_topic);
23533
25189
  const restartOthers = restartEntries.filter(([_, cfg]) => !cfg.general_topic);
23534
25190
  for (const [name, cfg] of restartGenerals) {
@@ -23982,6 +25638,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23982
25638
  return;
23983
25639
  }
23984
25640
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
25641
+ if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
25642
+ && this.runtimeCoordinator().handlePublicRequest(req, res, new URL(req.url ?? "/", `http://localhost:${port}`)))
25643
+ return;
23985
25644
  // Branding is public, after Host admission: automatic favicon requests
23986
25645
  // must not become noisy web-token 401s in the browser console.
23987
25646
  // #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
@@ -24008,7 +25667,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24008
25667
  if (decision.reason === "no-credential" && req.method === "GET"
24009
25668
  && String(req.headers.accept ?? "").includes("text/html")
24010
25669
  && isWebPageNavigation(requestPath)) {
24011
- serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
25670
+ const link = gatewayRequestContext(req);
25671
+ serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId) });
24012
25672
  return;
24013
25673
  }
24014
25674
  res.writeHead(decision.status);
@@ -24365,6 +26025,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24365
26025
  if (!this.webToken || !this.webSessions)
24366
26026
  this.initializeWebAuthTokens();
24367
26027
  const server = createServer((req, res) => this.dispatchWebHttp(req, res, port, drain.closing));
26028
+ server.on("upgrade", (req, socket, head) => {
26029
+ if (!this.shuttingDown && remoteRuntimeEnabled() && isHostAllowed(req.headers.host, allowedHostNames(this.fleetConfig))
26030
+ && this.runtimeCoordinator().handleUpgrade(req, socket, head))
26031
+ return;
26032
+ socket.destroy();
26033
+ });
24368
26034
  // #1937: Node's default (300 s to receive a request) would cut a large upload on a slow link. The listener allows
24369
26035
  // more; the upload route keeps its own clocks (no byte for a minute, or 30 minutes in all), and every other request
24370
26036
  // keeps the old 300 s (limitRequestReceive in dispatchWebHttp).
@@ -24589,9 +26255,36 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24589
26255
  this.logger.debug({ err }, "bots unreadable (getUiStatus)");
24590
26256
  }
24591
26257
  const instances = names.map(name => {
26258
+ const nativeConfig = this.fleetConfig?.instances[name];
26259
+ if (isNativeExecInstance(nativeConfig)) {
26260
+ let runtime = null;
26261
+ let agent = null;
26262
+ try {
26263
+ runtime = this.nativeInstanceRuntime();
26264
+ agent = runtime.get(name);
26265
+ }
26266
+ catch { /* A damaged native profile is a crashed native row, never a Linux spawn. */ }
26267
+ const display_name = nativeConfig.display_name || agent?.name || name;
26268
+ const backend = nativeConfig.backend || agent?.cli || "codex";
26269
+ return { name, display_name, description: nativeConfig.description || undefined, tags: nativeConfig.tags || [],
26270
+ teams: instanceTeams(this.fleetConfig?.teams, name), runtime: "windows", native_agent_id: nativeConfig.native_agent_id,
26271
+ ...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
26272
+ machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}),
26273
+ native_blocked: !agent || agent.blocked === true,
26274
+ native_busy: runtime?.busy(name) ?? false,
26275
+ backend,
26276
+ model: nativeConfig.model || "default", model_source: nativeConfig.model ? "instance" : "cli-default",
26277
+ status: agent && runtime ? runtime.status(name) : "crashed", state: agent && runtime ? runtime.execution(name) : null, execution_state: agent && runtime ? runtime.execution(name) : null,
26278
+ interaction: null, interaction_summary: null, context_pct: null, cost: null,
26279
+ last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined,
26280
+ avatar: avatarFor({ name, displayName: display_name }),
26281
+ capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: nativeProviderMcpSupported(backend), channels: false, terminal: false, attachments: false, usage: false, send_while_busy: false },
26282
+ ...(!agent ? { runtime_error: "Windows profile is missing or invalid; restore its saved conversation before using this instance" }
26283
+ : agent.blocked ? { runtime_error: "Windows process outcome is unknown; verify it stopped before recovery" } : {}) };
26284
+ }
24592
26285
  // #1956: read once for this instance; its cost, context and model come from it.
24593
- const statusline = readStatuslineJson(this.dataDir, name);
24594
- const cost = statusline?.cost?.total_cost_usd ?? 0;
26286
+ const statusline = isNativePaneInstance(nativeConfig) ? undefined : readStatuslineJson(this.dataDir, name);
26287
+ const cost = isNativePaneInstance(nativeConfig) ? null : statusline?.cost?.total_cost_usd ?? 0;
24595
26288
  // Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
24596
26289
  const classic = classicOnly.includes(name);
24597
26290
  const backend = classic
@@ -24599,7 +26292,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24599
26292
  : (this.fleetConfig?.instances[name]?.backend
24600
26293
  ?? this.fleetConfig?.defaults?.backend
24601
26294
  ?? "claude-code");
24602
- const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
26295
+ const { context } = isNativePaneInstance(nativeConfig) ? { context: null } : resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
24603
26296
  // context_pct: null when unavailable, not 0
24604
26297
  const context_pct = context ?? null;
24605
26298
  // Model: Only Claude Code has live statusline; others use the effective resolver.
@@ -24630,6 +26323,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24630
26323
  }
24631
26324
  return {
24632
26325
  name,
26326
+ general_topic: nativeConfig?.general_topic === true,
26327
+ ...(isNativePaneInstance(nativeConfig) ? { runtime: "windows", native_adapter: "pane", native_agent_id: nativeConfig.native_agent_id,
26328
+ ...(() => {
26329
+ try {
26330
+ const profile = this.nativeInstanceRuntime().get(name), bound = this.daemons.get(name);
26331
+ return { native_recovery: profile?.paneRecovery ?? (profile?.paneLaunchId && bound?.usesNativePane?.() && !bound.nativePaneCurrent()
26332
+ ? { kind: "exit-unconfirmed", launchId: profile.paneLaunchId } : null) };
26333
+ }
26334
+ catch {
26335
+ return { native_recovery: null };
26336
+ }
26337
+ })(),
26338
+ capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
26339
+ commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
26340
+ ...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
26341
+ machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
24633
26342
  display_name: identity.display_name || undefined,
24634
26343
  avatar: image.avatar,
24635
26344
  ...(image.accent ? { accent: image.accent } : {}),
@@ -24652,6 +26361,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24652
26361
  role: identity.role || undefined,
24653
26362
  tags: identity.tags,
24654
26363
  teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
26364
+ // #3008 G2: a web-only fleet's General (the agent its owner ticked) — the sidebar pins it first.
26365
+ ...(this.isWebOnlyFleet() && this.fleetConfig?.instances[name]?.general_topic === true ? { general: true } : {}),
24655
26366
  last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined, // #2179: its last reply to a person (ISO)
24656
26367
  // #2855: its last reply that reached the channel — the only reply time a guest's sidebar gets (guestStatus).
24657
26368
  last_reply_channel: this.webChatHistory.lastAgentReply(name, { channel: true }) ?? undefined,
@@ -24681,6 +26392,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24681
26392
  uptime: Math.floor((Date.now() - this.startedAt) / 1000),
24682
26393
  // #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
24683
26394
  connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
26395
+ windows_cli: localWindowsCliEnabled(),
24684
26396
  // #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
24685
26397
  prefs_rev: this.webPrefs.rev(),
24686
26398
  // #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
@@ -24690,6 +26402,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24690
26402
  host_memory: this.hostMemoryIndicator(),
24691
26403
  // #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
24692
26404
  ...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
26405
+ // alpha.2 (M-2300, #2997 review): agents' requests changed — the owner's page reads GET /ui/agent-proposals again.
26406
+ ...(this.isWebOnlyFleet() ? { agent_proposals_rev: this.agentProposalsRev } : {}),
24693
26407
  // #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
24694
26408
  ...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
24695
26409
  };
@@ -25132,4 +26846,12 @@ load();
25132
26846
  </script>
25133
26847
  </body>
25134
26848
  </html>`;
26849
+ /** alpha.2 (M-2300): the agent an applied Windows request created (its name, for the page's "Open chat"), or null. */
26850
+ export function agentNameOf(view) {
26851
+ const r = view.state === "applied" ? view.outcome?.result : null;
26852
+ if (!r || typeof r !== "object")
26853
+ return null;
26854
+ const o = r, name = o.name ?? o.instance_name ?? (o.agent && typeof o.agent === "object" ? o.agent.name : undefined);
26855
+ return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) ? name : null;
26856
+ }
25135
26857
  //# sourceMappingURL=fleet-manager.js.map