@songsid/agend 2.3.4-beta.6 → 2.3.5-alpha.2

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 (386) 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/backend/claude-code.d.ts +15 -12
  8. package/dist/backend/claude-code.js +137 -133
  9. package/dist/backend/claude-code.js.map +1 -1
  10. package/dist/backend/codex-package.d.ts +5 -2
  11. package/dist/backend/codex-package.js +1 -5
  12. package/dist/backend/codex-package.js.map +1 -1
  13. package/dist/backend/codex.d.ts +136 -113
  14. package/dist/backend/codex.js +1271 -1219
  15. package/dist/backend/codex.js.map +1 -1
  16. package/dist/backend/grok-model-cache.d.ts +7 -0
  17. package/dist/backend/grok-model-cache.js +65 -0
  18. package/dist/backend/grok-model-cache.js.map +1 -0
  19. package/dist/backend/grok.d.ts +2 -0
  20. package/dist/backend/grok.js +22 -0
  21. package/dist/backend/grok.js.map +1 -1
  22. package/dist/backend/native-pane.d.ts +9 -0
  23. package/dist/backend/native-pane.js +59 -0
  24. package/dist/backend/native-pane.js.map +1 -0
  25. package/dist/backend/types.d.ts +7 -0
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/channel/ipc-bridge.d.ts +2 -0
  28. package/dist/channel/mcp-tools.js +1 -1
  29. package/dist/channel/mcp-tools.js.map +1 -1
  30. package/dist/cli.js +6 -2
  31. package/dist/cli.js.map +1 -1
  32. package/dist/config-validator.js +59 -0
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/config.js +16 -0
  35. package/dist/config.js.map +1 -1
  36. package/dist/daemon.d.ts +62 -5
  37. package/dist/daemon.js +749 -267
  38. package/dist/daemon.js.map +1 -1
  39. package/dist/delivery-consumed-watch.d.ts +15 -3
  40. package/dist/delivery-consumed-watch.js +23 -1
  41. package/dist/delivery-consumed-watch.js.map +1 -1
  42. package/dist/delivery-outbox.d.ts +5 -0
  43. package/dist/delivery-outbox.js +32 -4
  44. package/dist/delivery-outbox.js.map +1 -1
  45. package/dist/delivery-reconciliation.d.ts +2 -35
  46. package/dist/delivery-reconciliation.js +2 -156
  47. package/dist/delivery-reconciliation.js.map +1 -1
  48. package/dist/fleet-context.d.ts +2 -2
  49. package/dist/fleet-level-config.d.ts +1 -1
  50. package/dist/fleet-level-config.js +2 -0
  51. package/dist/fleet-level-config.js.map +1 -1
  52. package/dist/fleet-manager.d.ts +125 -2
  53. package/dist/fleet-manager.js +1157 -27
  54. package/dist/fleet-manager.js.map +1 -1
  55. package/dist/fleet-windows-runtime.d.ts +92 -0
  56. package/dist/fleet-windows-runtime.js +373 -0
  57. package/dist/fleet-windows-runtime.js.map +1 -0
  58. package/dist/instance-config-impact.js +4 -0
  59. package/dist/instance-config-impact.js.map +1 -1
  60. package/dist/instance-lifecycle.d.ts +20 -3
  61. package/dist/instance-lifecycle.js +358 -156
  62. package/dist/instance-lifecycle.js.map +1 -1
  63. package/dist/local-pane-endpoint.d.ts +46 -0
  64. package/dist/local-pane-endpoint.js +189 -0
  65. package/dist/local-pane-endpoint.js.map +1 -0
  66. package/dist/local-pane-launcher.d.ts +2 -0
  67. package/dist/local-pane-launcher.js +38 -0
  68. package/dist/local-pane-launcher.js.map +1 -0
  69. package/dist/local-pane-stage.ps1 +101 -0
  70. package/dist/locale.js +34 -0
  71. package/dist/locale.js.map +1 -1
  72. package/dist/model-catalog-defaults.d.ts +14 -0
  73. package/dist/model-catalog-defaults.js +20 -0
  74. package/dist/model-catalog-defaults.js.map +1 -0
  75. package/dist/native-agent-admission.d.ts +7 -0
  76. package/dist/native-agent-admission.js +26 -0
  77. package/dist/native-agent-admission.js.map +1 -0
  78. package/dist/native-daemon-client.d.ts +13 -0
  79. package/dist/native-daemon-client.js +28 -0
  80. package/dist/native-daemon-client.js.map +1 -0
  81. package/dist/native-instance-api.d.ts +14 -0
  82. package/dist/native-instance-api.js +205 -0
  83. package/dist/native-instance-api.js.map +1 -0
  84. package/dist/native-instance-input.d.ts +84 -0
  85. package/dist/native-instance-input.js +54 -0
  86. package/dist/native-instance-input.js.map +1 -0
  87. package/dist/native-instance-registry.d.ts +32 -0
  88. package/dist/native-instance-registry.js +105 -0
  89. package/dist/native-instance-registry.js.map +1 -0
  90. package/dist/native-mcp.d.ts +40 -0
  91. package/dist/native-mcp.js +278 -0
  92. package/dist/native-mcp.js.map +1 -0
  93. package/dist/native-model-catalog.d.ts +5 -0
  94. package/dist/native-model-catalog.js +76 -0
  95. package/dist/native-model-catalog.js.map +1 -0
  96. package/dist/native-pane-crash-budget.d.ts +20 -0
  97. package/dist/native-pane-crash-budget.js +60 -0
  98. package/dist/native-pane-crash-budget.js.map +1 -0
  99. package/dist/native-pane-create.d.ts +26 -0
  100. package/dist/native-pane-create.js +53 -0
  101. package/dist/native-pane-create.js.map +1 -0
  102. package/dist/native-pane-lifecycle.d.ts +65 -0
  103. package/dist/native-pane-lifecycle.js +227 -0
  104. package/dist/native-pane-lifecycle.js.map +1 -0
  105. package/dist/native-pane-manager.d.ts +58 -0
  106. package/dist/native-pane-manager.js +563 -0
  107. package/dist/native-pane-manager.js.map +1 -0
  108. package/dist/native-pane-output-log.d.ts +15 -0
  109. package/dist/native-pane-output-log.js +108 -0
  110. package/dist/native-pane-output-log.js.map +1 -0
  111. package/dist/native-pane-reconciliation.d.ts +4 -0
  112. package/dist/native-pane-reconciliation.js +40 -0
  113. package/dist/native-pane-reconciliation.js.map +1 -0
  114. package/dist/native-pane-recovery.d.ts +12 -0
  115. package/dist/native-pane-recovery.js +10 -0
  116. package/dist/native-pane-recovery.js.map +1 -0
  117. package/dist/native-pane-tools.d.ts +2 -0
  118. package/dist/native-pane-tools.js +14 -0
  119. package/dist/native-pane-tools.js.map +1 -0
  120. package/dist/needs-you-hub.d.ts +2 -0
  121. package/dist/needs-you-hub.js +1 -0
  122. package/dist/needs-you-hub.js.map +1 -1
  123. package/dist/needs-you.d.ts +12 -1
  124. package/dist/needs-you.js +8 -1
  125. package/dist/needs-you.js.map +1 -1
  126. package/dist/outbound-handlers.d.ts +16 -2
  127. package/dist/outbound-handlers.js +67 -3
  128. package/dist/outbound-handlers.js.map +1 -1
  129. package/dist/outbound-schemas.d.ts +38 -0
  130. package/dist/outbound-schemas.js +12 -0
  131. package/dist/outbound-schemas.js.map +1 -1
  132. package/dist/pane-port.d.ts +107 -0
  133. package/dist/pane-port.js +2 -0
  134. package/dist/pane-port.js.map +1 -0
  135. package/dist/runtime-api.d.ts +8 -0
  136. package/dist/runtime-api.js +96 -0
  137. package/dist/runtime-api.js.map +1 -0
  138. package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +21 -0
  139. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
  140. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
  141. package/dist/runtime-assets/@xterm/addon-serialize/package.json +29 -0
  142. package/dist/runtime-assets/@xterm/headless/LICENSE +21 -0
  143. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +2 -0
  144. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
  145. package/dist/runtime-assets/@xterm/headless/package.json +29 -0
  146. package/dist/runtime-assets/node-pty/CONPTY-LICENSE +21 -0
  147. package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +503 -0
  148. package/dist/runtime-assets/node-pty/LICENSE +69 -0
  149. package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +16 -0
  150. package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +47 -0
  151. package/dist/runtime-assets/node-pty/lib/index.js +52 -0
  152. package/dist/runtime-assets/node-pty/lib/interfaces.js +7 -0
  153. package/dist/runtime-assets/node-pty/lib/shared/conout.js +11 -0
  154. package/dist/runtime-assets/node-pty/lib/terminal.js +190 -0
  155. package/dist/runtime-assets/node-pty/lib/types.js +7 -0
  156. package/dist/runtime-assets/node-pty/lib/unixTerminal.js +346 -0
  157. package/dist/runtime-assets/node-pty/lib/utils.js +39 -0
  158. package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +125 -0
  159. package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +320 -0
  160. package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +199 -0
  161. package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  162. package/dist/runtime-assets/node-pty/package.json +64 -0
  163. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  164. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  165. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  166. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  167. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  168. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  169. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  170. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  171. package/dist/runtime-bundle.d.ts +17 -0
  172. package/dist/runtime-bundle.js +147 -0
  173. package/dist/runtime-bundle.js.map +1 -0
  174. package/dist/runtime-cli.d.ts +2 -0
  175. package/dist/runtime-cli.js +117 -0
  176. package/dist/runtime-cli.js.map +1 -0
  177. package/dist/runtime-client-store.d.ts +49 -0
  178. package/dist/runtime-client-store.js +292 -0
  179. package/dist/runtime-client-store.js.map +1 -0
  180. package/dist/runtime-client.d.ts +100 -0
  181. package/dist/runtime-client.js +700 -0
  182. package/dist/runtime-client.js.map +1 -0
  183. package/dist/runtime-coordinator.d.ts +82 -0
  184. package/dist/runtime-coordinator.js +864 -0
  185. package/dist/runtime-coordinator.js.map +1 -0
  186. package/dist/runtime-pane-assets.d.ts +17 -0
  187. package/dist/runtime-pane-assets.js +23 -0
  188. package/dist/runtime-pane-assets.js.map +1 -0
  189. package/dist/runtime-pane-boot.d.ts +2 -0
  190. package/dist/runtime-pane-boot.js +24 -0
  191. package/dist/runtime-pane-boot.js.map +1 -0
  192. package/dist/runtime-pane-codex-index.d.ts +1 -0
  193. package/dist/runtime-pane-codex-index.js +47 -0
  194. package/dist/runtime-pane-codex-index.js.map +1 -0
  195. package/dist/runtime-pane-config.d.ts +16 -0
  196. package/dist/runtime-pane-config.js +54 -0
  197. package/dist/runtime-pane-config.js.map +1 -0
  198. package/dist/runtime-pane-host.d.ts +43 -0
  199. package/dist/runtime-pane-host.js +403 -0
  200. package/dist/runtime-pane-host.js.map +1 -0
  201. package/dist/runtime-pane-job-owner.d.ts +1 -0
  202. package/dist/runtime-pane-job-owner.js +29 -0
  203. package/dist/runtime-pane-job-owner.js.map +1 -0
  204. package/dist/runtime-pane-job.ps1 +116 -0
  205. package/dist/runtime-pane-link.d.ts +40 -0
  206. package/dist/runtime-pane-link.js +246 -0
  207. package/dist/runtime-pane-link.js.map +1 -0
  208. package/dist/runtime-pane-mcp-protocol.d.ts +32 -0
  209. package/dist/runtime-pane-mcp-protocol.js +39 -0
  210. package/dist/runtime-pane-mcp-protocol.js.map +1 -0
  211. package/dist/runtime-pane-mcp-relay.d.ts +17 -0
  212. package/dist/runtime-pane-mcp-relay.js +70 -0
  213. package/dist/runtime-pane-mcp-relay.js.map +1 -0
  214. package/dist/runtime-pane-mcp.d.ts +43 -0
  215. package/dist/runtime-pane-mcp.js +137 -0
  216. package/dist/runtime-pane-mcp.js.map +1 -0
  217. package/dist/runtime-pane-native.d.ts +45 -0
  218. package/dist/runtime-pane-native.js +65 -0
  219. package/dist/runtime-pane-native.js.map +1 -0
  220. package/dist/runtime-pane-output.d.ts +19 -0
  221. package/dist/runtime-pane-output.js +53 -0
  222. package/dist/runtime-pane-output.js.map +1 -0
  223. package/dist/runtime-pane-port.d.ts +134 -0
  224. package/dist/runtime-pane-port.js +537 -0
  225. package/dist/runtime-pane-port.js.map +1 -0
  226. package/dist/runtime-pane-private.d.ts +14 -0
  227. package/dist/runtime-pane-private.js +51 -0
  228. package/dist/runtime-pane-private.js.map +1 -0
  229. package/dist/runtime-pane-private.ps1 +14 -0
  230. package/dist/runtime-pane-process.d.ts +4 -0
  231. package/dist/runtime-pane-process.js +108 -0
  232. package/dist/runtime-pane-process.js.map +1 -0
  233. package/dist/runtime-pane-protocol.d.ts +169 -0
  234. package/dist/runtime-pane-protocol.js +189 -0
  235. package/dist/runtime-pane-protocol.js.map +1 -0
  236. package/dist/runtime-pane-screen.d.ts +19 -0
  237. package/dist/runtime-pane-screen.js +76 -0
  238. package/dist/runtime-pane-screen.js.map +1 -0
  239. package/dist/runtime-pane-service.d.ts +11 -0
  240. package/dist/runtime-pane-service.js +221 -0
  241. package/dist/runtime-pane-service.js.map +1 -0
  242. package/dist/runtime-pane-stdio-protocol.d.ts +54 -0
  243. package/dist/runtime-pane-stdio-protocol.js +94 -0
  244. package/dist/runtime-pane-stdio-protocol.js.map +1 -0
  245. package/dist/runtime-pane-stdio.d.ts +3 -0
  246. package/dist/runtime-pane-stdio.js +123 -0
  247. package/dist/runtime-pane-stdio.js.map +1 -0
  248. package/dist/runtime-pane-store.d.ts +33 -0
  249. package/dist/runtime-pane-store.js +164 -0
  250. package/dist/runtime-pane-store.js.map +1 -0
  251. package/dist/runtime-pane-transcript-protocol.d.ts +43 -0
  252. package/dist/runtime-pane-transcript-protocol.js +58 -0
  253. package/dist/runtime-pane-transcript-protocol.js.map +1 -0
  254. package/dist/runtime-pane-transcript.d.ts +45 -0
  255. package/dist/runtime-pane-transcript.js +605 -0
  256. package/dist/runtime-pane-transcript.js.map +1 -0
  257. package/dist/runtime-pane-worker.d.ts +1 -0
  258. package/dist/runtime-pane-worker.js +154 -0
  259. package/dist/runtime-pane-worker.js.map +1 -0
  260. package/dist/runtime-protocol.d.ts +149 -0
  261. package/dist/runtime-protocol.js +19 -0
  262. package/dist/runtime-protocol.js.map +1 -0
  263. package/dist/runtime-switches.d.ts +79 -0
  264. package/dist/runtime-switches.js +90 -0
  265. package/dist/runtime-switches.js.map +1 -0
  266. package/dist/runtime-transcript-delta-reader.d.ts +23 -0
  267. package/dist/runtime-transcript-delta-reader.js +74 -0
  268. package/dist/runtime-transcript-delta-reader.js.map +1 -0
  269. package/dist/runtime-transcript-source.d.ts +36 -0
  270. package/dist/runtime-transcript-source.js +189 -0
  271. package/dist/runtime-transcript-source.js.map +1 -0
  272. package/dist/runtime-windows-bridge.d.ts +41 -0
  273. package/dist/runtime-windows-bridge.js +157 -0
  274. package/dist/runtime-windows-bridge.js.map +1 -0
  275. package/dist/settings-api.js +58 -0
  276. package/dist/settings-api.js.map +1 -1
  277. package/dist/settings-change.js +28 -0
  278. package/dist/settings-change.js.map +1 -1
  279. package/dist/settings-confirmation.d.ts +15 -1
  280. package/dist/settings-confirmation.js +46 -9
  281. package/dist/settings-confirmation.js.map +1 -1
  282. package/dist/settings-effect.js +23 -1
  283. package/dist/settings-effect.js.map +1 -1
  284. package/dist/settings-http-confirmation.js +11 -2
  285. package/dist/settings-http-confirmation.js.map +1 -1
  286. package/dist/tmux-control.d.ts +2 -1
  287. package/dist/tmux-control.js.map +1 -1
  288. package/dist/tmux-manager.d.ts +2 -1
  289. package/dist/tmux-manager.js.map +1 -1
  290. package/dist/tmux-pane-host.d.ts +26 -0
  291. package/dist/tmux-pane-host.js +25 -0
  292. package/dist/tmux-pane-host.js.map +1 -0
  293. package/dist/topic-commands.js +3 -0
  294. package/dist/topic-commands.js.map +1 -1
  295. package/dist/transcript-delta-reader.d.ts +1 -1
  296. package/dist/transcript-delta-reader.js +1 -1
  297. package/dist/transcript-delta-reader.js.map +1 -1
  298. package/dist/transcript-monitor.d.ts +3 -0
  299. package/dist/transcript-monitor.js +22 -46
  300. package/dist/transcript-monitor.js.map +1 -1
  301. package/dist/transcript-needles.d.ts +35 -0
  302. package/dist/transcript-needles.js +158 -0
  303. package/dist/transcript-needles.js.map +1 -0
  304. package/dist/transcript-records.d.ts +21 -0
  305. package/dist/transcript-records.js +140 -0
  306. package/dist/transcript-records.js.map +1 -0
  307. package/dist/transcript-sources.d.ts +5 -0
  308. package/dist/transcript-sources.js +8 -79
  309. package/dist/transcript-sources.js.map +1 -1
  310. package/dist/types.d.ts +21 -0
  311. package/dist/ui/agent-proposals.js +90 -0
  312. package/dist/ui/agent-runtime.js +106 -0
  313. package/dist/ui/chat-store.js +13 -2
  314. package/dist/ui/chat-strings.js +48 -0
  315. package/dist/ui/cli-picker.js +42 -0
  316. package/dist/ui/native-instance-settings.js +47 -0
  317. package/dist/ui/native-pane-recovery.js +45 -0
  318. package/dist/ui/panel-chat.js +61 -32
  319. package/dist/ui/panel-details.js +2 -1
  320. package/dist/ui/panel-fleet.js +99 -18
  321. package/dist/ui/panel-needs.js +152 -3
  322. package/dist/ui/panel-runtimes.js +231 -0
  323. package/dist/ui/panel-settings.js +14 -8
  324. package/dist/ui/panel-windows-agent.js +86 -0
  325. package/dist/ui/panel-windows-cli.js +143 -0
  326. package/dist/ui/provider-marks.js +83 -0
  327. package/dist/ui/runtime-catalog.js +55 -0
  328. package/dist/ui/settings-catalog.js +37 -10
  329. package/dist/ui/settings-confirm.js +3 -1
  330. package/dist/ui/settings-dialogs.js +14 -5
  331. package/dist/ui/settings-registry.js +5 -0
  332. package/dist/ui/settings-strings.js +14 -0
  333. package/dist/ui/shared/app-i18n.js +2 -2
  334. package/dist/ui/shared/app-route.js +6 -2
  335. package/dist/ui/shared/app-shell.js +3 -2
  336. package/dist/ui/shared/app-store.js +1 -0
  337. package/dist/ui/shared/app-stream.js +3 -1
  338. package/dist/ui/shared/app.css +301 -0
  339. package/dist/ui/shared/app.js +4 -0
  340. package/dist/ui/shared/header-tools.js +6 -4
  341. package/dist/ui/shared/instance-nav.js +20 -4
  342. package/dist/ui/shared/instance-switch.js +4 -3
  343. package/dist/ui/shared/ui-menu.js +28 -8
  344. package/dist/ui/shared/view-strings.js +6 -0
  345. package/dist/ui/windows-agents-nav.js +2 -0
  346. package/dist/ui/windows-cli-catalog.js +79 -0
  347. package/dist/update-check.d.ts +8 -2
  348. package/dist/update-check.js +5 -2
  349. package/dist/update-check.js.map +1 -1
  350. package/dist/web-api.d.ts +25 -0
  351. package/dist/web-api.js +171 -6
  352. package/dist/web-api.js.map +1 -1
  353. package/dist/web-chat-history.d.ts +19 -2
  354. package/dist/web-chat-history.js +72 -3
  355. package/dist/web-chat-history.js.map +1 -1
  356. package/dist/web-shell-routes.d.ts +4 -1
  357. package/dist/web-shell-routes.js +7 -2
  358. package/dist/web-shell-routes.js.map +1 -1
  359. package/dist/windows-agents.d.ts +112 -0
  360. package/dist/windows-agents.js +463 -0
  361. package/dist/windows-agents.js.map +1 -0
  362. package/dist/windows-cli-api.d.ts +23 -0
  363. package/dist/windows-cli-api.js +158 -0
  364. package/dist/windows-cli-api.js.map +1 -0
  365. package/dist/windows-cli-providers.d.ts +115 -0
  366. package/dist/windows-cli-providers.js +429 -0
  367. package/dist/windows-cli-providers.js.map +1 -0
  368. package/dist/windows-cli-worker.d.ts +58 -0
  369. package/dist/windows-cli-worker.js +479 -0
  370. package/dist/windows-cli-worker.js.map +1 -0
  371. package/dist/windows-cli.d.ts +115 -0
  372. package/dist/windows-cli.js +402 -0
  373. package/dist/windows-cli.js.map +1 -0
  374. package/dist/windows-native-mcp.d.ts +19 -0
  375. package/dist/windows-native-mcp.js +143 -0
  376. package/dist/windows-native-mcp.js.map +1 -0
  377. package/dist/windows-pane-mcp.d.ts +26 -0
  378. package/dist/windows-pane-mcp.js +187 -0
  379. package/dist/windows-pane-mcp.js.map +1 -0
  380. package/package.json +9 -3
  381. package/templates/runtime-assets/conpty-LICENSE +21 -0
  382. package/templates/runtime-assets/conpty-NOTICE.md +503 -0
  383. package/templates/runtime-assets/local-pane-stage.ps1 +101 -0
  384. package/templates/runtime-assets/runtime-pane-job.ps1 +116 -0
  385. package/templates/runtime-assets/runtime-pane-private.ps1 +14 -0
  386. package/templates/runtime-assets/xterm-LICENSE +21 -0
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { nativePaneToolRefusal } from "./native-pane-tools.js";
1
2
  import { dialogScreenText } from "./dialog-screen-text.js";
2
3
  import { readActiveKiroModel } from "./backend/kiro.js";
3
4
  import { dashboardAccessOf } from "./dashboard-permissions.js";
@@ -20,7 +21,7 @@ import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, Sessio
20
21
  import { mcpServerState } from "./mcp-liveness.js";
21
22
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
22
23
  import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
23
- import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
24
+ import { resolveTmuxLogicalSize } from "./tmux-manager.js";
24
25
  import { TranscriptMonitor } from "./transcript-monitor.js";
25
26
  import { createTranscriptSource } from "./transcript-sources.js";
26
27
  import { TranscriptTurnLedger } from "./transcript-turns.js";
@@ -48,6 +49,7 @@ import { HangDetector } from "./hang-detector.js";
48
49
  import { writeSecretFile } from "./secret-file.js";
49
50
  import { PaneWriteLock } from "./pane-write-lock.js";
50
51
  import { answerRemoteCard } from "./remote-card-answer.js";
52
+ import { tmuxPaneHost } from "./tmux-pane-host.js";
51
53
  import { buildFleetInstructions } from "./instructions.js";
52
54
  import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
53
55
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
@@ -57,6 +59,7 @@ import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT
57
59
  import { t } from "./locale.js";
58
60
  import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
59
61
  import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
62
+ import { reconcileNativePaneTarget } from "./native-pane-reconciliation.js";
60
63
  import { deliveryContentDigest } from "./delivery-outbox.js";
61
64
  import { consumedWatches } from "./delivery-consumed-watch.js";
62
65
  import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
@@ -1112,6 +1115,7 @@ export class Daemon extends EventEmitter {
1112
1115
  spawnGate;
1113
1116
  stormWindow;
1114
1117
  backendOutage;
1118
+ paneTransport;
1115
1119
  /** #1314: fleet-directory fallbacks already reported to the topic (once per daemon, per field and path). */
1116
1120
  promptFileFallbackNoticed = new Set();
1117
1121
  /** Identity of this live Daemon object; changes on object stop/restart. */
@@ -1943,9 +1947,79 @@ export class Daemon extends EventEmitter {
1943
1947
  this.errorRecoveryDeadlineAt = 0;
1944
1948
  this.activeErrorPatternKey = null;
1945
1949
  }
1950
+ /** Lazy default also preserves bare-prototype diagnostic/test construction. */
1951
+ get paneHost() { return this.paneTransport ?? tmuxPaneHost; }
1952
+ nativePaneLifecycle;
1953
+ nativeFleetBridge;
1954
+ nativeTranscriptSource;
1955
+ usesNativePane() { return !!this.nativePaneLifecycle; }
1956
+ nativePaneCurrent() { return this.nativePaneLifecycle?.current() ?? false; }
1957
+ nativeEnterHold = null;
1958
+ /** Per-launch attention for a native delivery whose bounded input attempts were refused or uncertain, including non-outbox input. */
1959
+ nativeDeliveryHold() {
1960
+ const hold = this.nativeEnterHold;
1961
+ return hold && this.nativePaneLifecycle && !this.deliveryWritesStopping && hold.spawn === this.spawnGeneration && hold.fence === this.launchFenceEpoch
1962
+ ? { key: hold.key, since: hold.since } : null;
1963
+ }
1964
+ nativePaneHasExited() {
1965
+ const snapshot = this.nativePaneLifecycle?.snapshot?.();
1966
+ return snapshot?.state === "exited" && snapshot.instance === this.name && this.tmux?.getWindowId() === snapshot.paneId;
1967
+ }
1968
+ /** Positive native exit evidence only, fenced to this exact launch and Daemon owner. */
1969
+ nativePaneExitCurrent(snapshot, owner) {
1970
+ const actual = this.nativePaneLifecycle?.snapshot?.();
1971
+ return !!actual && actual.state === "exited" && snapshot.state === "exited"
1972
+ && sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.startupAborted
1973
+ && ["hostBootId", "paneId", "launchId", "agentId", "instance", "sessionId", "commandSequence", "exitCode"].every(key => actual[key] === snapshot[key])
1974
+ && this.tmux?.getWindowId() === snapshot.paneId;
1975
+ }
1976
+ async reconcileNativeDeliveries(outbox, current) {
1977
+ if (!this.nativePaneLifecycle)
1978
+ return false;
1979
+ return reconcileNativePaneTarget(outbox, this.name, this.nativePaneLifecycle, current);
1980
+ }
1981
+ /** Same-process transport for the owned native daemon; never exposed as a runner RPC. */
1982
+ setNativeFleetBridge(bridge) {
1983
+ if (!this.nativePaneLifecycle || this.nativeFleetBridge?.current())
1984
+ throw Error("Native Fleet bridge requires one bound pane owner");
1985
+ this.nativeFleetBridge = bridge;
1986
+ }
1987
+ /** Fleet admission and generation checks happen before this synchronous handoff. */
1988
+ acceptNativeFleetMessage(message) {
1989
+ if (!this.nativePaneLifecycle || !this.nativeFleetBridge?.current() || this.deliveryWritesStopping || this.runtimeMonitorsFrozen)
1990
+ return false;
1991
+ if (typeof message.type === "string" && FLEET_RESPONSE_TYPES.has(message.type)) {
1992
+ this.routeFleetResponse(message);
1993
+ return true;
1994
+ }
1995
+ const bridge = this.nativeFleetBridge;
1996
+ const current = () => this.nativeFleetBridge === bridge && bridge.current() && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen;
1997
+ return this.handleFleetMessage(message, reply => { if (current())
1998
+ this.broadcastFleetMessage(reply); }, current);
1999
+ }
2000
+ broadcastFleetMessage(message) {
2001
+ if (this.nativePaneLifecycle) {
2002
+ if (this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen && !this.deliveryWritesStopping)
2003
+ this.nativeFleetBridge.send(message);
2004
+ }
2005
+ else
2006
+ this.ipcServer?.broadcast(message);
2007
+ }
2008
+ fleetTransportAvailable() {
2009
+ return this.nativePaneLifecycle ? !!this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen : !!this.ipcServer;
2010
+ }
2011
+ /** Internal lifecycle injection, before start. Native paths and provider files stay on their runner. */
2012
+ setNativePaneLifecycle(native) {
2013
+ if (this.tmux || this.ipcServer || this.nativePaneLifecycle)
2014
+ throw Error("Native pane adapter must be bound before daemon startup");
2015
+ this.nativePaneLifecycle = native;
2016
+ this.backend = native.backend;
2017
+ this.paneTransport = native.host;
2018
+ this.controlClient = native.observation;
2019
+ }
1946
2020
  constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity, spawnGate, stormWindow,
1947
2021
  /** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
1948
- backendOutage) {
2022
+ backendOutage, paneTransport) {
1949
2023
  super();
1950
2024
  this.name = name;
1951
2025
  this.config = config;
@@ -1957,6 +2031,7 @@ export class Daemon extends EventEmitter {
1957
2031
  this.spawnGate = spawnGate;
1958
2032
  this.stormWindow = stormWindow;
1959
2033
  this.backendOutage = backendOutage;
2034
+ this.paneTransport = paneTransport;
1960
2035
  // The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
1961
2036
  // sharing that object would activate cold changes before the normal restart/apply boundary.
1962
2037
  this.config = structuredClone(config);
@@ -2038,12 +2113,19 @@ export class Daemon extends EventEmitter {
2038
2113
  return outcome === "begun";
2039
2114
  }
2040
2115
  async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
2041
- let checkpoint = null;
2042
- try {
2043
- checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2116
+ let checkpoint = null, runtimeTranscript = null;
2117
+ if (this.nativePaneLifecycle) {
2118
+ runtimeTranscript = await this.transcriptMonitor?.reconciliationRuntimeCheckpoint() ?? null;
2119
+ if (!runtimeTranscript || !this.nativePaneLifecycle.current(runtimeTranscript))
2120
+ throw Error("Native transcript checkpoint is not current; no delivery was written");
2044
2121
  }
2045
- catch (err) {
2046
- this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
2122
+ else {
2123
+ try {
2124
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2125
+ }
2126
+ catch (err) {
2127
+ this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
2128
+ }
2047
2129
  }
2048
2130
  const submissionMode = rawPaste ? "raw_paste" : steer
2049
2131
  ? "steer"
@@ -2059,8 +2141,9 @@ export class Daemon extends EventEmitter {
2059
2141
  backendVersion,
2060
2142
  windowId: windowId ?? this.tmux?.getWindowId() ?? null,
2061
2143
  transcriptPath: checkpoint?.path ?? null,
2062
- transcriptOffset: checkpoint?.offset ?? null,
2063
- transcriptSessionId: checkpoint?.sessionId ?? null,
2144
+ transcriptOffset: runtimeTranscript?.cursor.offset ?? checkpoint?.offset ?? null,
2145
+ transcriptSessionId: runtimeTranscript?.cursor.sessionId ?? checkpoint?.sessionId ?? null,
2146
+ ...(runtimeTranscript ? { runtimeTranscript } : {}),
2064
2147
  submissionMode,
2065
2148
  queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
2066
2149
  };
@@ -2103,17 +2186,25 @@ export class Daemon extends EventEmitter {
2103
2186
  const outbox = this.deliveryOutbox;
2104
2187
  if (!checkpoint || !outbox?.markConsumed)
2105
2188
  return;
2189
+ if (this.nativePaneLifecycle && !checkpoint.runtime)
2190
+ return;
2106
2191
  if (verdict.submissionMode !== "steer" && verdict.submissionMode !== "native_queue_handoff")
2107
2192
  return;
2108
2193
  if (!["claude-code", "codex"].includes(checkpoint.backend))
2109
2194
  return;
2110
2195
  const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, boot = this.bootId;
2196
+ let location;
2197
+ try {
2198
+ location = checkpoint.runtime ? { reader: this.nativeTranscriptReader(checkpoint.runtime, delivery.deliveryId) } : { path: checkpoint.path, offset: checkpoint.offset };
2199
+ }
2200
+ catch {
2201
+ return;
2202
+ } // The owner changed; an unavailable consumed watch proves nothing.
2111
2203
  consumedWatches.start({
2112
2204
  deliveryId: delivery.deliveryId,
2113
2205
  attemptNo: delivery.attemptNo,
2114
2206
  backend: checkpoint.backend,
2115
- path: checkpoint.path,
2116
- offset: checkpoint.offset,
2207
+ ...location,
2117
2208
  current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch,
2118
2209
  consumed: (via, evidence) => {
2119
2210
  try {
@@ -2194,24 +2285,34 @@ export class Daemon extends EventEmitter {
2194
2285
  const spawn = this.spawnGeneration;
2195
2286
  const fence = this.launchFenceEpoch;
2196
2287
  let last = "unavailable";
2197
- let outcome = "delivered";
2198
- let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
2288
+ const native = !!checkpoint.runtime || !!this.nativePaneLifecycle;
2289
+ let outcome = native ? "uncertain" : "delivered";
2290
+ let evidence = native ? "runtime-transcript-unconfirmed" : OUTPUT_EDGE_ONLY_EVIDENCE;
2199
2291
  // #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
2200
2292
  // in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
2201
2293
  const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
2202
2294
  let found = false;
2203
2295
  try {
2296
+ if (this.nativePaneLifecycle && !checkpoint.runtime)
2297
+ throw Error("Native proof cannot read a coordinator-local transcript path");
2298
+ const nativeReader = checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : null;
2204
2299
  for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
2205
2300
  if (this.deliveryWritesStopping)
2206
2301
  return; // reconciliation owns the row now
2207
- const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
2302
+ const look = nativeReader ? await nativeReader.look() : await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
2303
+ if (this.deliveryWritesStopping)
2304
+ return;
2305
+ if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
2306
+ last = "unavailable";
2307
+ break;
2308
+ }
2208
2309
  if (look === "user" || look === "absorbed" || look === "queued") {
2209
2310
  found = true;
2210
2311
  outcome = "delivered";
2211
2312
  evidence = transcriptMarkerEvidence(look, !!rawPaste);
2212
2313
  break;
2213
2314
  }
2214
- last = look;
2315
+ last = look === "pending" ? "unavailable" : look;
2215
2316
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
2216
2317
  last = "unavailable";
2217
2318
  break;
@@ -2223,13 +2324,13 @@ export class Daemon extends EventEmitter {
2223
2324
  // `uncertain`: re-check the same fixed epochs after the last await, before settling (#1207 P2).
2224
2325
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch)
2225
2326
  last = "unavailable";
2226
- if (last === "no-match" && evidence === OUTPUT_EDGE_ONLY_EVIDENCE) {
2327
+ if (last === "no-match" && !found) {
2227
2328
  outcome = "uncertain";
2228
2329
  evidence = "unverifiable-no-transcript-marker";
2229
2330
  }
2230
2331
  }
2231
2332
  catch (err) {
2232
- this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
2333
+ this.logger.debug({ err }, "Transcript proof unavailable — preserving the conservative outcome");
2233
2334
  }
2234
2335
  if (rawPaste && !found) {
2235
2336
  outcome = "uncertain";
@@ -2416,128 +2517,130 @@ export class Daemon extends EventEmitter {
2416
2517
  }
2417
2518
  }
2418
2519
  catch { /* corrupt/missing — ignore */ }
2419
- // 1. IPC server — bridge between MCP server (Claude's child) and daemon
2420
- const sockPath = join(this.instanceDir, "channel.sock");
2421
- this.ipcServer = new IpcServer(sockPath, this.logger);
2422
- // Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
2423
- // Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
2424
- let ipcListening = false;
2425
- this.ipcServer.on("error", (err) => {
2426
- if (!ipcListening)
2427
- return; // startup errors handled by listen() rejection
2428
- this.logger.error({ err, name: this.name }, "IPC server error");
2429
- this.emit("error", err);
2430
- });
2431
- await this.ipcServer.listen();
2432
- this.startupAdmission?.();
2433
- ipcListening = true;
2434
- // Permanent IPC dispatcher: routes responses to pending requests by type+id key
2435
- this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
2436
- // IPC message relay: when daemon wants to push a channel message to Claude,
2437
- // it broadcasts to all IPC clients (the MCP server is one of them).
2438
- // When MCP server sends a tool_call, daemon handles it via the messageBus.
2439
- this.ipcServer.on("message", (msg, socket) => {
2440
- if (msg.type === "tool_call") {
2441
- // MCP server forwarding a Claude tool call (reply, react, edit, download)
2442
- this.handleToolCall(msg, socket);
2443
- }
2444
- else if (msg.type === "mcp_ready") {
2445
- this.handleMcpReady(msg, socket);
2446
- }
2447
- else if (msg.type === "pong") {
2448
- this.handleMcpPong(msg, socket);
2449
- }
2450
- else if (msg.type === "query_sessions") {
2451
- // Fleet manager asks for all registered session names (catches sessions
2452
- // that sent mcp_ready before fleet manager connected).
2453
- const sessions = [];
2454
- for (const [s, sessionName] of this.socketSessionNames) {
2455
- if (!s.destroyed && sessionName !== this.name) {
2456
- // Individual mcp_ready for initial registration path
2457
- this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
2458
- sessions.push(sessionName);
2520
+ if (!this.nativePaneLifecycle) {
2521
+ // 1. IPC server — bridge between MCP server (Claude's child) and daemon
2522
+ const sockPath = join(this.instanceDir, "channel.sock");
2523
+ this.ipcServer = new IpcServer(sockPath, this.logger);
2524
+ // Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
2525
+ // Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
2526
+ let ipcListening = false;
2527
+ this.ipcServer.on("error", (err) => {
2528
+ if (!ipcListening)
2529
+ return; // startup errors handled by listen() rejection
2530
+ this.logger.error({ err, name: this.name }, "IPC server error");
2531
+ this.emit("error", err);
2532
+ });
2533
+ await this.ipcServer.listen();
2534
+ this.startupAdmission?.();
2535
+ ipcListening = true;
2536
+ // Permanent IPC dispatcher: routes responses to pending requests by type+id key
2537
+ this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
2538
+ // IPC message relay: when daemon wants to push a channel message to Claude,
2539
+ // it broadcasts to all IPC clients (the MCP server is one of them).
2540
+ // When MCP server sends a tool_call, daemon handles it via the messageBus.
2541
+ this.ipcServer.on("message", (msg, socket) => {
2542
+ if (msg.type === "tool_call") {
2543
+ // MCP server forwarding a Claude tool call (reply, react, edit, download)
2544
+ this.handleToolCall(msg, socket);
2545
+ }
2546
+ else if (msg.type === "mcp_ready") {
2547
+ this.handleMcpReady(msg, socket);
2548
+ }
2549
+ else if (msg.type === "pong") {
2550
+ this.handleMcpPong(msg, socket);
2551
+ }
2552
+ else if (msg.type === "query_sessions") {
2553
+ // Fleet manager asks for all registered session names (catches sessions
2554
+ // that sent mcp_ready before fleet manager connected).
2555
+ const sessions = [];
2556
+ for (const [s, sessionName] of this.socketSessionNames) {
2557
+ if (!s.destroyed && sessionName !== this.name) {
2558
+ // Individual mcp_ready for initial registration path
2559
+ this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
2560
+ sessions.push(sessionName);
2561
+ }
2459
2562
  }
2563
+ // Batch response for prune path
2564
+ this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
2460
2565
  }
2461
- // Batch response for prune path
2462
- this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
2463
- }
2464
- else if (msg.type === "fleet_inbound") {
2465
- // Fleet manager routed a message to us (topic mode)
2466
- const meta = msg.meta;
2467
- const targetSession = msg.targetSession;
2468
- const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
2469
- void this.wake().then(() => {
2470
- this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
2471
- }).catch(err => {
2472
- this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
2473
- this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
2474
- });
2475
- }
2476
- else if (msg.type === "raw_paste") {
2477
- this.handleRawPasteIpcMessage(msg);
2478
- }
2479
- else if (msg.type === "config_update") {
2480
- this.applyConfigUpdate(msg.config);
2481
- }
2482
- else if (msg.type === "steer") {
2483
- const meta = (msg.meta ?? {});
2484
- this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
2485
- }
2486
- else if (msg.type === "btw") {
2487
- const meta = (msg.meta ?? {});
2488
- this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
2489
- }
2490
- else if (msg.type === "fleet_schedule_trigger") {
2491
- const payload = msg.payload;
2492
- const meta = msg.meta;
2493
- const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
2494
- void this.wake().then(() => this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch)).catch(err => {
2495
- this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
2496
- });
2497
- }
2498
- else if (msg.type === "query_instance_state") {
2499
- void this.respondToInstanceStateQuery(msg, socket);
2500
- }
2501
- });
2502
- // 2. Tmux — ensure session, create window if not alive
2503
- await TmuxManager.ensureSession(this.tmuxSessionName);
2504
- this.startupAdmission?.();
2505
- this.tmux = new TmuxManager(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
2506
- // Strategy A: always start fresh Claude window (MCP server has no reconnection)
2507
- // Kill any existing window from previous run
2508
- const windowIdFile = join(this.instanceDir, "window-id");
2509
- if (existsSync(windowIdFile)) {
2510
- const savedId = readFileSync(windowIdFile, "utf-8").trim();
2511
- if (savedId) {
2512
- const oldTmux = new TmuxManager(this.tmuxSessionName, savedId);
2513
- if (await oldTmux.isWindowAlive()) {
2514
- this.startupAdmission?.();
2515
- await this.checkpointSessionId();
2566
+ else
2567
+ this.handleFleetMessage(msg, reply => { this.ipcServer?.send(socket, reply); });
2568
+ });
2569
+ }
2570
+ let nativeDisposition;
2571
+ if (this.nativePaneLifecycle) {
2572
+ this.beginSpawn();
2573
+ try {
2574
+ const acquired = await this.nativePaneLifecycle.acquire(() => {
2516
2575
  this.startupAdmission?.();
2517
- await oldTmux.killWindow();
2576
+ if (this.startupAborted)
2577
+ throw Error("Native launch was superseded");
2578
+ });
2579
+ this.tmux = acquired.pane;
2580
+ this.nativeTranscriptSource = acquired.source;
2581
+ nativeDisposition = acquired.disposition;
2582
+ this.lastSpawnAt = Date.now();
2583
+ this.setProcessStatus("running");
2584
+ this.bindInstanceStateOutputListener(this.tmux.getWindowId());
2585
+ this.saveSessionId();
2586
+ // New/resumed native processes can still be painting their first frame. Keep deliveries behind the same
2587
+ // bounded dialog/readiness scan as local startup. An already-running reattachment may be busy: do not
2588
+ // wait for that work to finish or inject a startup key into it.
2589
+ if (acquired.disposition !== "reattached") {
2590
+ if (!await this.finishStartupScan(120_000))
2591
+ throw Error("Native CLI exited during startup; retained session was not replaced");
2518
2592
  this.startupAdmission?.();
2519
- this.logger.info({ savedId }, "Killed old tmux window for fresh start");
2593
+ if (this.startupAborted || this.deliveryWritesStopping || !this.nativePaneLifecycle.current())
2594
+ throw Error("Native launch was superseded");
2520
2595
  }
2521
2596
  }
2522
- }
2523
- const resumed = await this.spawnClaudeWindow();
2524
- this.startupAdmission?.();
2525
- this.isNewSession = !resumed;
2526
- if (!resumed) {
2527
- await this.injectSnapshotMessage(this.startupAdmission);
2528
- this.startupAdmission?.();
2597
+ finally {
2598
+ this.endSpawn();
2599
+ }
2529
2600
  }
2530
2601
  else {
2531
- // Clean up stale snapshot file — resume restored full context, snapshot not needed
2532
- try {
2533
- unlinkSync(join(this.instanceDir, "rotation-state.json"));
2602
+ // 2. Tmux — ensure session, create window if not alive
2603
+ await this.paneHost.ensureSession(this.tmuxSessionName);
2604
+ this.startupAdmission?.();
2605
+ this.tmux = this.paneHost.bind(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
2606
+ // Strategy A: always start fresh Claude window (MCP server has no reconnection)
2607
+ // Kill any existing window from previous run
2608
+ const windowIdFile = join(this.instanceDir, "window-id");
2609
+ if (existsSync(windowIdFile)) {
2610
+ const savedId = readFileSync(windowIdFile, "utf-8").trim();
2611
+ if (savedId) {
2612
+ const oldTmux = this.paneHost.bind(this.tmuxSessionName, savedId);
2613
+ if (await oldTmux.isWindowAlive()) {
2614
+ this.startupAdmission?.();
2615
+ await this.checkpointSessionId();
2616
+ this.startupAdmission?.();
2617
+ await oldTmux.killWindow();
2618
+ this.startupAdmission?.();
2619
+ this.logger.info({ savedId }, "Killed old tmux window for fresh start");
2620
+ }
2621
+ }
2622
+ }
2623
+ const resumed = await this.spawnClaudeWindow();
2624
+ this.startupAdmission?.();
2625
+ this.isNewSession = !resumed;
2626
+ if (!resumed) {
2627
+ await this.injectSnapshotMessage(this.startupAdmission);
2628
+ this.startupAdmission?.();
2629
+ }
2630
+ else {
2631
+ // Clean up stale snapshot file — resume restored full context, snapshot not needed
2632
+ try {
2633
+ unlinkSync(join(this.instanceDir, "rotation-state.json"));
2634
+ }
2635
+ catch { /* may not exist */ }
2534
2636
  }
2535
- catch { /* may not exist */ }
2536
2637
  }
2638
+ if (nativeDisposition)
2639
+ this.isNewSession = nativeDisposition === "fresh";
2537
2640
  // #1209: conditional continuation for the turn interrupted by the last
2538
2641
  // restart, if the gates allow. Detached; the gate
2539
2642
  // consumes its one-shot marker first and never throws into boot.
2540
- const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
2643
+ const legacyResume = this.nativePaneLifecycle ? Promise.resolve("unknown") : this.maybeResumeInterruptedTurn().catch(() => "unknown");
2541
2644
  const recoveryOwner = JSON.stringify(this.interactionOwner());
2542
2645
  void Promise.allSettled([legacyResume]).then(() => {
2543
2646
  if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
@@ -2556,7 +2659,7 @@ export class Daemon extends EventEmitter {
2556
2659
  // (statusline transcript); codex/kiro/opencode read their CLI's own
2557
2660
  // conversation store via a pluggable source. Backends with no known
2558
2661
  // source stay inert exactly as before.
2559
- this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2662
+ this.transcriptMonitor = this.buildTranscriptMonitor(this.nativeTranscriptSource ?? createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2560
2663
  const transcriptMonitor = this.transcriptMonitor;
2561
2664
  const transcriptFence = this.launchFenceEpoch;
2562
2665
  // Baseline in the isolate before this daemon accepts new work. A
@@ -2612,13 +2715,15 @@ export class Daemon extends EventEmitter {
2612
2715
  if (hangConfig?.enabled !== false) {
2613
2716
  this.hangDetector = new HangDetector();
2614
2717
  }
2615
- // 8. Context guardian
2616
- const statusFile = join(this.instanceDir, "statusline.json");
2617
- this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
2618
- this.guardian.startWatching();
2619
- this.guardian.on("status_update", () => {
2620
- this.saveSessionId();
2621
- });
2718
+ if (!this.nativePaneLifecycle) {
2719
+ // 8. Context guardian
2720
+ const statusFile = join(this.instanceDir, "statusline.json");
2721
+ this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
2722
+ this.guardian.startWatching();
2723
+ this.guardian.on("status_update", () => {
2724
+ this.saveSessionId();
2725
+ });
2726
+ }
2622
2727
  // Context rotation removed: all CLI backends have built-in auto-compact.
2623
2728
  // Crash recovery (health check + respawn with snapshot) is retained below.
2624
2729
  }
@@ -2669,7 +2774,7 @@ export class Daemon extends EventEmitter {
2669
2774
  this.socketPids.delete(socket);
2670
2775
  // Notify fleet manager so it can clean up sessionRegistry
2671
2776
  if (sessionName !== this.name) {
2672
- this.ipcServer?.broadcast({ type: "session_disconnected", sessionName });
2777
+ this.broadcastFleetMessage({ type: "session_disconnected", sessionName });
2673
2778
  }
2674
2779
  });
2675
2780
  }
@@ -2685,11 +2790,16 @@ export class Daemon extends EventEmitter {
2685
2790
  // sub-agent's server may have left a dead pid in the slot).
2686
2791
  this.noteMcpProofOfLife("mcp_ready", typeof msg.pid === "number" ? msg.pid : undefined);
2687
2792
  }
2688
- this.ipcServer?.broadcast({ type: "mcp_ready", sessionName });
2793
+ this.broadcastFleetMessage({ type: "mcp_ready", sessionName });
2689
2794
  }
2690
2795
  checkMcpServerAlive() {
2691
2796
  if (this.isPaused)
2692
2797
  return;
2798
+ if (this.nativePaneLifecycle) {
2799
+ if (this.nativePaneLifecycle.mcpProof() === "alive")
2800
+ this.noteMcpProofOfLife("connection");
2801
+ return; // No native PID/socket guessing or restart from an unknown helper observation.
2802
+ }
2693
2803
  const status = mcpServerState(this.instanceDir);
2694
2804
  if (status.state === "alive") {
2695
2805
  this.mcpServedGeneration = this.spawnGeneration;
@@ -2773,6 +2883,8 @@ export class Daemon extends EventEmitter {
2773
2883
  * server from a third-party one, and failed the whole launch for either.
2774
2884
  */
2775
2885
  checkFleetMcpNeverConnected() {
2886
+ if (this.nativePaneLifecycle)
2887
+ return; // Native helper presence owns this observation.
2776
2888
  if (this.config.agent_mode === "cli")
2777
2889
  return; // no MCP server by design
2778
2890
  if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
@@ -2983,6 +3095,8 @@ export class Daemon extends EventEmitter {
2983
3095
  * proof that tools are being served, whatever channel.mcp.pid says.
2984
3096
  */
2985
3097
  mcpServerAlive() {
3098
+ if (this.nativePaneLifecycle)
3099
+ return this.nativePaneLifecycle.mcpProof() === "alive" ? { alive: true, source: "connection" } : { alive: false, source: "none" };
2986
3100
  const status = mcpServerState(this.instanceDir);
2987
3101
  if (status.state === "alive")
2988
3102
  return { alive: true, source: "pid", pid: status.pid };
@@ -3047,7 +3161,7 @@ export class Daemon extends EventEmitter {
3047
3161
  this.mcpDeathNotifiedForPid = null;
3048
3162
  this.mcpDeathDeferredForPid = null;
3049
3163
  this.clearMcpRestartRequest();
3050
- if (pid && mcpServerState(this.instanceDir).state !== "alive")
3164
+ if (!this.nativePaneLifecycle && pid && mcpServerState(this.instanceDir).state !== "alive")
3051
3165
  this.repairMcpPidSlot(pid, source);
3052
3166
  }
3053
3167
  emitMcpRecovered(source, pid) {
@@ -3094,6 +3208,26 @@ export class Daemon extends EventEmitter {
3094
3208
  // ended this instance's health loop because scheduleNext() sat after the
3095
3209
  // throwing code. Catch, log, and keep checking.
3096
3210
  try {
3211
+ if (this.nativePaneLifecycle) {
3212
+ if (this.healthCheckPaused) {
3213
+ scheduleNext();
3214
+ return;
3215
+ }
3216
+ const native = this.nativePaneLifecycle, pane = this.tmux;
3217
+ const status = await pane?.getPaneStatus();
3218
+ if (this.runtimeMonitorsFrozen || native !== this.nativePaneLifecycle || pane !== this.tmux)
3219
+ return;
3220
+ const snapshot = native.snapshot?.();
3221
+ if (status?.alive === false && snapshot?.state === "exited") {
3222
+ this.healthCheckPaused = true;
3223
+ this.setProcessStatus(snapshot.exitCode === 0 ? "stopped" : "crashed");
3224
+ this.emit("native_pane_exited", { name: this.name, snapshot, owner: this.interactionOwner() });
3225
+ }
3226
+ else if (status?.alive)
3227
+ this.checkMcpServerAlive();
3228
+ scheduleNext();
3229
+ return;
3230
+ }
3097
3231
  // Instance directory removed externally (e.g. `rm -rf ~/.agend/instances/<name>`).
3098
3232
  // Stop the loop permanently — otherwise every tick triggers a respawn, whose
3099
3233
  // writeRotationSnapshot fails with ENOENT and gets caught as "Failed to respawn",
@@ -3205,13 +3339,13 @@ export class Daemon extends EventEmitter {
3205
3339
  let inWindowLossBurst = false;
3206
3340
  let nullReason;
3207
3341
  if (!paneStatus) {
3208
- const serverAlive = await TmuxManager.sessionExists(this.tmuxSessionName);
3342
+ const serverAlive = await this.paneHost.sessionExists(this.tmuxSessionName);
3209
3343
  if (discardStaleHealth())
3210
3344
  return;
3211
3345
  // A server may have already restarted before this daemon's health
3212
3346
  // tick. The new PID is the durable generation boundary: treat it as
3213
3347
  // a fleet storm even though `has-session` is true again.
3214
- const serverPid = serverAlive ? await TmuxManager.getServerPid(this.tmuxSessionName) : null;
3348
+ const serverPid = serverAlive ? await this.paneHost.getServerPid(this.tmuxSessionName) : null;
3215
3349
  if (discardStaleHealth())
3216
3350
  return;
3217
3351
  const generationChanged = serverAlive && this.stormWindow?.observeServerAlive(serverPid) === true;
@@ -3240,7 +3374,7 @@ export class Daemon extends EventEmitter {
3240
3374
  // the window truly no longer exists vs a transient query glitch.
3241
3375
  nullReason = "no_window";
3242
3376
  try {
3243
- const windows = await TmuxManager.listWindowsStrict(this.tmuxSessionName, this.controlClient);
3377
+ const windows = await this.paneHost.listWindowsStrict(this.tmuxSessionName, this.controlClient);
3244
3378
  if (discardStaleHealth())
3245
3379
  return;
3246
3380
  this.windowQueryFailureTicks = 0;
@@ -3485,10 +3619,10 @@ export class Daemon extends EventEmitter {
3485
3619
  // Wrapped in try-catch: if tmux server is dead, listWindows throws —
3486
3620
  // must not block spawnClaudeWindow (which calls ensureSession).
3487
3621
  try {
3488
- const windows = await TmuxManager.listWindows(this.tmuxSessionName);
3622
+ const windows = await this.paneHost.listWindows(this.tmuxSessionName);
3489
3623
  for (const w of windows) {
3490
3624
  if (w.name === this.name) {
3491
- const tm = new TmuxManager(this.tmuxSessionName, w.id);
3625
+ const tm = this.paneHost.bind(this.tmuxSessionName, w.id);
3492
3626
  await tm.killWindow();
3493
3627
  }
3494
3628
  }
@@ -3631,7 +3765,7 @@ export class Daemon extends EventEmitter {
3631
3765
  // Force the next ready capture to emit a fresh transition after respawn.
3632
3766
  this.instanceState = "working";
3633
3767
  }
3634
- this.ipcServer?.broadcast({
3768
+ this.broadcastFleetMessage({
3635
3769
  type: "instance_process_state",
3636
3770
  instanceName: this.name,
3637
3771
  status,
@@ -3827,12 +3961,15 @@ export class Daemon extends EventEmitter {
3827
3961
  for (const key of dialog.keys) {
3828
3962
  if (stale())
3829
3963
  return;
3964
+ const keyScreen = this.nativePaneLifecycle ? await pollTmux.capturePane() : currentPane;
3965
+ if (stale() || !Daemon.dialogMatches(dialog, keyScreen))
3966
+ return;
3830
3967
  let sent = false;
3831
3968
  if (SPECIAL_KEYS.has(key)) {
3832
- sent = await pollTmux.sendSpecialKey(key);
3969
+ sent = await (this.nativePaneLifecycle ? pollTmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : pollTmux.sendSpecialKey(key));
3833
3970
  }
3834
3971
  else if (dialog.literalKeys) {
3835
- sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
3972
+ sent = await (this.nativePaneLifecycle ? pollTmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : pollTmux.sendKeys(key)); // typed, no Enter (#2872)
3836
3973
  }
3837
3974
  else {
3838
3975
  sent = await pollTmux.pasteText(key, this.systemPasteOptions());
@@ -4195,7 +4332,20 @@ export class Daemon extends EventEmitter {
4195
4332
  */
4196
4333
  async sendEscape() {
4197
4334
  const cancelKey = this.backend?.getCancelKey() ?? "Escape";
4198
- await this.tmux?.sendSpecialKey(cancelKey);
4335
+ const tmux = this.tmux;
4336
+ if (!tmux)
4337
+ return;
4338
+ if (this.nativePaneLifecycle) {
4339
+ // A native pane's composer proof cannot see a permission dialog, so an unproved key is withheld there. The cancel
4340
+ // is the person's own interrupt, keyed on the screen read just before (as the dialog answer path is): what Esc
4341
+ // does on Linux's tmux pane, on the same screen, with the same key.
4342
+ const screen = await tmux.capturePane().catch(() => null);
4343
+ if (screen === null)
4344
+ return;
4345
+ await tmux.sendSpecialKey(cancelKey, undefined, { kind: "dialog", screen });
4346
+ return;
4347
+ }
4348
+ await tmux.sendSpecialKey(cancelKey);
4199
4349
  }
4200
4350
  /**
4201
4351
  * Drop every not-yet-started pane delivery which predates a user cancel.
@@ -4253,7 +4403,7 @@ export class Daemon extends EventEmitter {
4253
4403
  return;
4254
4404
  const outcome = follower.look(pane);
4255
4405
  if (outcome) {
4256
- this.ipcServer?.broadcast({ type: "compact_pane_result", id, owner, outcome });
4406
+ this.broadcastFleetMessage({ type: "compact_pane_result", id, owner, outcome });
4257
4407
  return;
4258
4408
  }
4259
4409
  }
@@ -4273,7 +4423,7 @@ export class Daemon extends EventEmitter {
4273
4423
  const reportSwitch = (result) => {
4274
4424
  if (!ownSwitch)
4275
4425
  return;
4276
- this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
4426
+ this.broadcastFleetMessage({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
4277
4427
  };
4278
4428
  // Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
4279
4429
  // (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
@@ -4288,7 +4438,7 @@ export class Daemon extends EventEmitter {
4288
4438
  if (!notifyWritten || told)
4289
4439
  return;
4290
4440
  told = true;
4291
- this.ipcServer?.broadcast({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
4441
+ this.broadcastFleetMessage({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
4292
4442
  ...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
4293
4443
  if (written && paneFollower)
4294
4444
  void this.followCompactPane(notifyWritten, owner, paneFollower);
@@ -4450,10 +4600,11 @@ export class Daemon extends EventEmitter {
4450
4600
  const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
4451
4601
  if (this.tmux !== tmux)
4452
4602
  return false;
4453
- const now = claudeSwitchDialog(await read());
4603
+ const screen = await read();
4604
+ const now = claudeSwitchDialog(screen);
4454
4605
  if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
4455
4606
  return false;
4456
- return tmux.sendSpecialKey(key);
4607
+ return this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen }) : tmux.sendSpecialKey(key);
4457
4608
  });
4458
4609
  const gone = async () => {
4459
4610
  const until = performance.now() + OWN_SWITCH_SETTLE_MS;
@@ -4542,9 +4693,12 @@ export class Daemon extends EventEmitter {
4542
4693
  return false;
4543
4694
  const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
4544
4695
  for (const key of dialog.keys) {
4696
+ const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : currentPane;
4697
+ if (!matches(keyScreen))
4698
+ return false;
4545
4699
  const ok = specialKeys.has(key)
4546
- ? await this.tmux.sendSpecialKey(key)
4547
- : await this.tmux.sendKeys(key);
4700
+ ? await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key))
4701
+ : await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key));
4548
4702
  if (!ok)
4549
4703
  return false;
4550
4704
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -4787,7 +4941,7 @@ export class Daemon extends EventEmitter {
4787
4941
  await this.tmux.sendSpecialKey("Escape").catch(() => false);
4788
4942
  return true;
4789
4943
  }
4790
- async stop() {
4944
+ async stop(options = {}) {
4791
4945
  this.publishWorkingState("before_stop");
4792
4946
  this.logger.info("Stopping daemon instance");
4793
4947
  this.clearQuitRelaunchWatch();
@@ -4809,8 +4963,18 @@ export class Daemon extends EventEmitter {
4809
4963
  }
4810
4964
  if (this.adapter)
4811
4965
  await this.adapter.stop();
4966
+ if (this.nativePaneLifecycle) {
4967
+ this.healthCheckPaused = true;
4968
+ this.saveSessionId();
4969
+ await this.nativePaneLifecycle.release(options.nativePane ?? "detach");
4970
+ try {
4971
+ unlinkSync(join(this.instanceDir, "daemon.pid"));
4972
+ }
4973
+ catch { /* metadata only */ }
4974
+ return;
4975
+ }
4812
4976
  // Notify MCP servers of graceful shutdown (prevents reconnect attempts)
4813
- this.ipcServer?.broadcast({ type: "shutdown" });
4977
+ this.broadcastFleetMessage({ type: "shutdown" });
4814
4978
  // Quit CLI FIRST — this kills MCP server child processes cleanly.
4815
4979
  // IPC must stay open during quit so MCP servers receive the shutdown message.
4816
4980
  if (this.tmux) {
@@ -4976,7 +5140,11 @@ export class Daemon extends EventEmitter {
4976
5140
  || !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
4977
5141
  || this.backend?.backgroundWorkRunning?.(pane) === true)
4978
5142
  return;
4979
- if (await tmux.getPaneInputMode?.() !== "raw" || !current())
5143
+ if (this.nativePaneLifecycle ? !this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"
5144
+ || !!this.backend?.isDeliveryInputReadyPane && !this.backend.isDeliveryInputReadyPane(pane)
5145
+ : await tmux.getPaneInputMode?.() !== "raw")
5146
+ return;
5147
+ if (!current())
4980
5148
  return;
4981
5149
  const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
4982
5150
  if (!current())
@@ -5067,7 +5235,7 @@ export class Daemon extends EventEmitter {
5067
5235
  this.logger.debug({ err }, "Interaction listener failed");
5068
5236
  }
5069
5237
  try {
5070
- this.ipcServer?.broadcast({ type: "instance_interaction", instanceName: this.name, interaction });
5238
+ this.broadcastFleetMessage({ type: "instance_interaction", instanceName: this.name, interaction });
5071
5239
  }
5072
5240
  catch (err) {
5073
5241
  this.logger.debug({ err }, "Interaction broadcast failed");
@@ -5155,6 +5323,56 @@ export class Daemon extends EventEmitter {
5155
5323
  }
5156
5324
  return true;
5157
5325
  }
5326
+ /** Shared Fleet inputs: socket and native transports retain the same wake, epoch and raw-paste semantics. */
5327
+ handleFleetMessage(msg, send, current = () => true) {
5328
+ if (msg.type === "query_sessions") {
5329
+ send({ type: "query_sessions_response", sessions: [] });
5330
+ }
5331
+ else if (msg.type === "fleet_inbound") {
5332
+ const meta = msg.meta;
5333
+ const targetSession = msg.targetSession;
5334
+ const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
5335
+ void this.wake().then(() => {
5336
+ if (!current()) {
5337
+ this.deferDurableDelivery(meta, "native channel owner changed before pane submission");
5338
+ return;
5339
+ }
5340
+ this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
5341
+ }).catch(err => {
5342
+ this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
5343
+ this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
5344
+ });
5345
+ }
5346
+ else if (msg.type === "raw_paste") {
5347
+ this.handleRawPasteIpcMessage(msg);
5348
+ }
5349
+ else if (msg.type === "config_update") {
5350
+ this.applyConfigUpdate(msg.config);
5351
+ }
5352
+ else if (msg.type === "steer") {
5353
+ const meta = (msg.meta ?? {});
5354
+ this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
5355
+ }
5356
+ else if (msg.type === "btw") {
5357
+ const meta = (msg.meta ?? {});
5358
+ this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
5359
+ }
5360
+ else if (msg.type === "fleet_schedule_trigger") {
5361
+ const payload = msg.payload;
5362
+ const meta = msg.meta;
5363
+ const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
5364
+ void this.wake().then(() => { if (current())
5365
+ this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch); }).catch(err => {
5366
+ this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
5367
+ });
5368
+ }
5369
+ else if (msg.type === "query_instance_state") {
5370
+ void this.respondToInstanceStateQuery(msg, undefined, send);
5371
+ }
5372
+ else
5373
+ return false;
5374
+ return true;
5375
+ }
5158
5376
  /**
5159
5377
  * Answer a FleetManager state query. Most callers only need the cached state,
5160
5378
  * but lifecycle decisions such as post-reply Cancel retirement need an
@@ -5162,7 +5380,7 @@ export class Daemon extends EventEmitter {
5162
5380
  * the last transition can stay "working" even though the CLI has since
5163
5381
  * settled without emitting another control-mode output record.
5164
5382
  */
5165
- async respondToInstanceStateQuery(msg, socket) {
5383
+ async respondToInstanceStateQuery(msg, socket, send) {
5166
5384
  try {
5167
5385
  if (msg.deliveryIdle === true) {
5168
5386
  await this.captureAndEvaluateInstanceState("delivery_idle_gate");
@@ -5171,7 +5389,7 @@ export class Daemon extends EventEmitter {
5171
5389
  await this.captureAndEvaluateInstanceState("state_query");
5172
5390
  }
5173
5391
  const snapshot = this.getInstanceStateSnapshot();
5174
- this.ipcServer?.send(socket, {
5392
+ const reply = {
5175
5393
  type: "instance_state_response",
5176
5394
  requestId: msg.requestId,
5177
5395
  instanceName: this.name,
@@ -5179,7 +5397,11 @@ export class Daemon extends EventEmitter {
5179
5397
  state: this.isPaused ? "paused" : snapshot.state,
5180
5398
  processStatus: this.processStatus,
5181
5399
  pausedAt: this.lastPausedAt,
5182
- });
5400
+ };
5401
+ if (send)
5402
+ send(reply);
5403
+ else if (socket)
5404
+ this.ipcServer?.send(socket, reply);
5183
5405
  }
5184
5406
  catch (err) {
5185
5407
  this.logger.debug({ err: err.message }, "Instance state query failed");
@@ -5219,7 +5441,49 @@ export class Daemon extends EventEmitter {
5219
5441
  static AUTH_RECONFIRM_ROWS = 15;
5220
5442
  /** Why the deferred pause was requested; what its marker records (Phase 2a). */
5221
5443
  pendingPauseReason = "auth";
5444
+ nativePauseProof = null;
5445
+ /** Consume the exact idle proof synchronously after the lifecycle's await, before fencing and closing. */
5446
+ nativePauseStillReady() {
5447
+ const proof = this.nativePauseProof;
5448
+ this.nativePauseProof = null;
5449
+ if (proof?.())
5450
+ return true;
5451
+ this.pauseRequested = false;
5452
+ return false;
5453
+ }
5454
+ /** Native lifecycle pause uses the ordinary idle/background/admission proof before closing its owned Job. */
5455
+ async nativePauseReady(admission) {
5456
+ this.nativePauseProof = null;
5457
+ const native = this.nativePaneLifecycle, pane = this.tmux, owner = this.interactionOwner();
5458
+ const allowStuck = this.pauseAllowStuck;
5459
+ this.pauseAllowStuck = false;
5460
+ const ready = () => !!native && native === this.nativePaneLifecycle && native.current() && this.tmux === pane
5461
+ && sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.spawning
5462
+ && this.processStatus === "running" && this.pasteQueueDepth === 0
5463
+ && (this.instanceState === "idle" || allowStuck && this.instanceState === "stuck") && (!admission || admission.current());
5464
+ if (!pane || !ready()) {
5465
+ this.pauseRequested = false;
5466
+ return false;
5467
+ }
5468
+ let text;
5469
+ try {
5470
+ text = await pane.capturePane();
5471
+ }
5472
+ catch {
5473
+ this.pauseRequested = false;
5474
+ return false;
5475
+ }
5476
+ if (!ready() || admission && !admission.accepts(text) || this.backend?.backgroundWorkRunning?.(text)
5477
+ || !allowStuck && !this.paneAuthoritativelyIdle(text)) {
5478
+ this.pauseRequested = false;
5479
+ return false;
5480
+ }
5481
+ this.nativePauseProof = ready;
5482
+ return true;
5483
+ }
5222
5484
  async pause(reason = "operator", admission) {
5485
+ if (this.nativePaneLifecycle)
5486
+ throw Error("Native pause is owned by its Fleet lifecycle transition");
5223
5487
  if (admission && !admission.current())
5224
5488
  return;
5225
5489
  if (this.pauseWakeState === "paused")
@@ -5315,7 +5579,7 @@ export class Daemon extends EventEmitter {
5315
5579
  writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
5316
5580
  this.publishWorkingState("execution");
5317
5581
  this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
5318
- this.ipcServer?.broadcast({
5582
+ this.broadcastFleetMessage({
5319
5583
  type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
5320
5584
  });
5321
5585
  this.emit("auto_paused", { name: this.name, pausedAt: this.lastPausedAt });
@@ -5410,7 +5674,7 @@ export class Daemon extends EventEmitter {
5410
5674
  this.resumeRuntimeMonitors();
5411
5675
  this.publishWorkingState("execution");
5412
5676
  this.logger.info("Instance auto-woke");
5413
- this.ipcServer?.broadcast({
5677
+ this.broadcastFleetMessage({
5414
5678
  type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
5415
5679
  });
5416
5680
  this.emit("auto_woke", { name: this.name });
@@ -5570,7 +5834,7 @@ export class Daemon extends EventEmitter {
5570
5834
  this.logStateEdge(previous, snapshot);
5571
5835
  this.publishWorkingState("execution");
5572
5836
  this.emit("instance_state", { name: this.name, ...snapshot });
5573
- this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
5837
+ this.broadcastFleetMessage({ type: "instance_state", instanceName: this.name, ...snapshot });
5574
5838
  if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
5575
5839
  this.handleStuckTransition(pane, snapshot, this.instanceStateReadyPattern);
5576
5840
  }
@@ -5670,7 +5934,7 @@ export class Daemon extends EventEmitter {
5670
5934
  // during the continuation boots clean instead of chaining resumes.
5671
5935
  // #2544: a reply that settled before this arm already answered the person —
5672
5936
  // writing a marker would re-inject a continuation for a finished turn.
5673
- if (!meta.resumedContinuationOf && !armed.completionDelivered) {
5937
+ if (!this.nativePaneLifecycle && !meta.resumedContinuationOf && !armed.completionDelivered) {
5674
5938
  const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
5675
5939
  const resumeCwd = this.config.working_directory ?? "";
5676
5940
  writeInFlightTurnMarker(this.instanceDir, {
@@ -5902,7 +6166,7 @@ export class Daemon extends EventEmitter {
5902
6166
  // outbound action suppresses a duplicate proxy; only a delivered
5903
6167
  // human-facing channel action satisfies the completion guard below.
5904
6168
  if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
5905
- && mcpServerState(this.instanceDir).state === "dead") {
6169
+ && (this.nativePaneLifecycle ? this.nativePaneLifecycle.mcpProof() === "dead" : mcpServerState(this.instanceDir).state === "dead")) {
5906
6170
  this.turnReplyGuard.complete(turn.generation);
5907
6171
  void this.sendProxyReply(pane, turn.target);
5908
6172
  return;
@@ -6279,7 +6543,7 @@ export class Daemon extends EventEmitter {
6279
6543
  }
6280
6544
  });
6281
6545
  }
6282
- if (!this.ipcServer) {
6546
+ if (!this.fleetTransportAvailable()) {
6283
6547
  this.logger.error(`${logLabel} failed — no adapter or fleet IPC route`);
6284
6548
  return Promise.resolve(false);
6285
6549
  }
@@ -6303,7 +6567,7 @@ export class Daemon extends EventEmitter {
6303
6567
  resolve(delivered);
6304
6568
  });
6305
6569
  try {
6306
- this.ipcServer.broadcast({
6570
+ this.broadcastFleetMessage({
6307
6571
  type: "fleet_outbound",
6308
6572
  tool: "reply",
6309
6573
  args,
@@ -6874,7 +7138,7 @@ export class Daemon extends EventEmitter {
6874
7138
  if (next === this.currentActivity)
6875
7139
  return;
6876
7140
  this.currentActivity = next;
6877
- this.ipcServer?.broadcast({
7141
+ this.broadcastFleetMessage({
6878
7142
  type: "instance_activity",
6879
7143
  instanceName: this.name,
6880
7144
  activity: next,
@@ -7031,7 +7295,7 @@ export class Daemon extends EventEmitter {
7031
7295
  return;
7032
7296
  this.lastProgressBroadcast = rendered;
7033
7297
  this.lastProgressBroadcastAt = Date.now();
7034
- this.ipcServer?.broadcast({
7298
+ this.broadcastFleetMessage({
7035
7299
  type: "instance_progress",
7036
7300
  instanceName: this.name,
7037
7301
  progress: rendered,
@@ -7047,7 +7311,7 @@ export class Daemon extends EventEmitter {
7047
7311
  if (this.lastProgressBroadcast !== "") {
7048
7312
  this.lastProgressBroadcast = "";
7049
7313
  this.lastProgressBroadcastAt = Date.now();
7050
- this.ipcServer?.broadcast({ type: "instance_progress", instanceName: this.name, progress: "" });
7314
+ this.broadcastFleetMessage({ type: "instance_progress", instanceName: this.name, progress: "" });
7051
7315
  }
7052
7316
  }
7053
7317
  /**
@@ -7212,7 +7476,7 @@ export class Daemon extends EventEmitter {
7212
7476
  reportCrossInstanceDeliveryFailure(meta, verdict, error) {
7213
7477
  if (!meta.from_instance)
7214
7478
  return;
7215
- this.ipcServer?.broadcast({
7479
+ this.broadcastFleetMessage({
7216
7480
  type: "cross_instance_delivery_failed",
7217
7481
  senderSession: meta.from_instance,
7218
7482
  targetInstance: this.name,
@@ -8572,13 +8836,14 @@ export class Daemon extends EventEmitter {
8572
8836
  if (this.remoteCardAnswering)
8573
8837
  return { outcome: "refused", reason: "another answer to this card is being keyed in" };
8574
8838
  this.remoteCardAnswering = true;
8839
+ let answerScreen = null;
8575
8840
  try {
8576
8841
  const outcome = await answerRemoteCard({
8577
8842
  locked: fn => this.paneWriteLock.run(fn),
8578
8843
  // An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
8579
- read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
8580
- key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
8581
- paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
8844
+ read: async () => answerScreen = this.tmux === tmux ? await tmux.capturePane().catch(() => null) : null,
8845
+ key: key => this.tmux !== tmux || answerScreen === null ? Promise.resolve(false) : key === "Space" ? (this.nativePaneLifecycle ? tmux.sendKeys(" ", { kind: "dialog", screen: answerScreen }) : tmux.sendKeys(" ")) : (this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: answerScreen }) : tmux.sendSpecialKey(key)),
8846
+ paste: text => this.tmux !== tmux || answerScreen === null ? Promise.resolve(false) : this.nativePaneLifecycle ? tmux.pasteBuffer(text, { proof: { kind: "dialog", screen: answerScreen } }) : tmux.pasteBuffer(text),
8582
8847
  sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
8583
8848
  now: () => performance.now(),
8584
8849
  }, pane => {
@@ -8849,12 +9114,19 @@ export class Daemon extends EventEmitter {
8849
9114
  if (!check || !this.tmux)
8850
9115
  return !check;
8851
9116
  try {
8852
- const mode = await this.tmux.getPaneInputMode?.();
8853
- if (mode !== "raw") {
9117
+ const inputActive = this.nativePaneLifecycle
9118
+ ? this.nativePaneLifecycle.current() && this.nativePaneLifecycle.mcpProof() === "alive"
9119
+ : await this.tmux.getPaneInputMode?.() === "raw";
9120
+ // ConPTY has no POSIX termios mode. Its closed launch plus current helper handshake is the input barrier;
9121
+ // the same strict composer detector below is still required. Never grant native unknown-layout fallback.
9122
+ if (!inputActive) {
8854
9123
  this.resetFooterFallback();
8855
9124
  return false;
8856
9125
  }
8857
- return this.deliveryInputReadyPane(await this.tmux.capturePane());
9126
+ const pane = await this.tmux.capturePane();
9127
+ if (this.nativePaneLifecycle && (!this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"))
9128
+ return false;
9129
+ return this.deliveryInputReadyPane(pane);
8858
9130
  }
8859
9131
  catch {
8860
9132
  this.resetFooterFallback();
@@ -9624,38 +9896,84 @@ export class Daemon extends EventEmitter {
9624
9896
  async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
9625
9897
  // A recovery Enter may have waited for a transient while cancel or spawn replaced the delivery: check here, and
9626
9898
  // again after every wait below, at the last point before the tmux write.
9627
- // M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
9628
- // tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
9629
- // the CLI this paste went to is never sent to its successor.
9630
9899
  const tmux = this.tmux;
9900
+ const backend = this.backend;
9901
+ const native = !!this.nativePaneLifecycle;
9631
9902
  const generation = this.spawnGeneration;
9632
9903
  const fence = this.launchFenceEpoch;
9633
- const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
9634
- && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
9904
+ const windowId = native ? tmux?.getWindowId() : undefined;
9905
+ // A native Windows pane keeps #2958's full fence (its launch can be replaced under the same handle).
9906
+ const current = () => this.tmux === tmux && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
9907
+ && (!native || (!!windowId && !!backend && this.backend === backend && this.spawnGeneration === generation
9908
+ && this.launchFenceEpoch === fence && tmux.getWindowId() === windowId));
9635
9909
  if (!current())
9636
9910
  return false;
9637
9911
  // The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
9638
- const hold = () => {
9639
- onInputHeld?.("pane-in-mode");
9640
- this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
9912
+ // S-35: say which check withheld Enter.
9913
+ const hold = (reason) => {
9914
+ onInputHeld?.(reason);
9915
+ if (reason === "output_changed" && current() && !this.nativeDeliveryHold())
9916
+ this.nativeEnterHold = {
9917
+ key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence,
9918
+ };
9919
+ if (reason === "pane_mode")
9920
+ this.logger.warn({ phase, reason }, "The pane stayed in a tmux mode; Enter withheld");
9921
+ else
9922
+ this.logger.warn({ phase, reason }, "Native delivery Enter withheld by its safety check");
9641
9923
  return false;
9642
9924
  };
9643
9925
  // M-2267 (#2971): a pane in a tmux mode (a person left it in copy-mode) would take this Enter. Wait for the mode to
9644
9926
  // end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
9645
- // past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
9646
- // f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
9927
+ // past the bound.
9928
+ // Native delivery follows main #2990: only this backend's recognised runtime dialog vetoes Enter.
9929
+ // The fresh capture still binds the native host output sequence; ownership and positive-ACK-only retries remain.
9647
9930
  const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9931
+ let pane;
9932
+ let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity;
9648
9933
  for (;;) {
9649
- const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
9934
+ const mode = await this.waitWhilePaneInMode(phase, current, Math.min(modeDeadline, positiveRetryDeadline));
9650
9935
  if (mode === "stale")
9651
9936
  return false;
9652
9937
  if (mode === "held")
9653
- return hold();
9938
+ return hold("pane_mode");
9939
+ if (native) {
9940
+ // S-35: a native pane's capture refuses while output is still arriving ("Pane changed during capture; observe it
9941
+ // again"). Right after a turn the CLI repaints for a moment; one refused read left the message unsent for ever.
9942
+ // Read again a few times — reading types nothing — and still hold if it never settles.
9943
+ let captured;
9944
+ for (let attempt = 0; captured === undefined; attempt++) {
9945
+ try {
9946
+ captured = await tmux.capturePane();
9947
+ }
9948
+ catch {
9949
+ if (!current())
9950
+ return false;
9951
+ if (attempt >= 4)
9952
+ return hold("capture_failed");
9953
+ await new Promise(done => setTimeout(done, 150 * (attempt + 1)));
9954
+ if (!current())
9955
+ return false;
9956
+ }
9957
+ }
9958
+ const screen = captured;
9959
+ pane = screen;
9960
+ if (!current())
9961
+ return false;
9962
+ try {
9963
+ if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
9964
+ return hold("dialog");
9965
+ }
9966
+ catch {
9967
+ return hold("check_failed");
9968
+ }
9969
+ if (!current())
9970
+ return false;
9971
+ }
9654
9972
  if (await this.paneInTmuxMode(phase)) {
9655
9973
  if (!current())
9656
9974
  return false;
9657
9975
  if (performance.now() >= modeDeadline)
9658
- return hold();
9976
+ return hold("pane_mode");
9659
9977
  await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9660
9978
  if (!current())
9661
9979
  return false;
@@ -9663,54 +9981,110 @@ export class Daemon extends EventEmitter {
9663
9981
  }
9664
9982
  if (!current())
9665
9983
  return false;
9666
- break;
9667
- }
9668
- // M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
9669
- // (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
9670
- // would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
9671
- // delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
9672
- // — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
9673
- // Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
9674
- // that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
9675
- // unrecognised-prompt guess — so an unknown screen still sends.
9676
- if (onInputHeld) {
9677
- let finalPane = null;
9984
+ if (positiveRefusals && performance.now() >= positiveRetryDeadline)
9985
+ return hold("output_changed");
9986
+ // W1: persist the "Enter may have happened" boundary before tmux receives
9987
+ // the key. A crash after this commit is never auto-replayed without a
9988
+ // positive transcript marker, even if the stale pane still shows composer.
9989
+ if (!enterMarked && durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
9990
+ this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
9991
+ return false;
9992
+ }
9993
+ enterMarked = true;
9994
+ const sent = await (native && pane !== undefined ? tmux.sendSpecialKey("Enter", undefined, { kind: "delivery", screen: pane }) : tmux.sendSpecialKey("Enter"));
9995
+ // A busy renderer can change seq after our capture. Only a positive zero-input ACK admits a NEW proof/key;
9996
+ // An Enter retry never re-pastes, retries an uncertain write, reuses stale proof or re-commits the durable boundary.
9997
+ if (!sent && native && tmux.lastInputWasNotSent?.() === true
9998
+ && tmux.getLastSendSpecialKeyError() === "Pane changed after observation; input was not sent"
9999
+ && current()) {
10000
+ if (!positiveRefusals)
10001
+ positiveRetryDeadline = Math.min(modeDeadline, performance.now() + 2000);
10002
+ if (positiveRefusals >= 4 || performance.now() >= positiveRetryDeadline)
10003
+ return hold("output_changed");
10004
+ positiveRefusals++;
10005
+ await new Promise(done => setTimeout(done, 100));
10006
+ if (!current())
10007
+ return false;
10008
+ if (performance.now() >= positiveRetryDeadline)
10009
+ return hold("output_changed");
10010
+ continue;
10011
+ }
10012
+ if (!sent) {
10013
+ this.logger.error({
10014
+ phase,
10015
+ tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
10016
+ }, "tmux send-keys Enter failed during message delivery");
10017
+ }
10018
+ if (sent && native && current())
10019
+ this.nativeEnterHold = null;
10020
+ return sent;
10021
+ }
10022
+ }
10023
+ /** A pre-existing native busy-paste race: retry bytes only after an exact, owned zero-input acknowledgement. */
10024
+ async sendDeliveryPaste(text, stillCurrent) {
10025
+ const pane = this.tmux, native = this.nativePaneLifecycle;
10026
+ if (!native)
10027
+ return pane.pasteBuffer(text);
10028
+ const backend = this.backend, generation = this.spawnGeneration, fence = this.launchFenceEpoch, windowId = pane.getWindowId();
10029
+ const current = () => !!windowId && this.tmux === pane && this.nativePaneLifecycle === native && this.backend === backend
10030
+ && this.spawnGeneration === generation && this.launchFenceEpoch === fence && pane.getWindowId() === windowId
10031
+ && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
10032
+ const hold = (reason) => {
10033
+ if (current() && !this.nativeDeliveryHold())
10034
+ this.nativeEnterHold = { key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence };
10035
+ this.logger.warn({ reason }, "Native delivery paste held by its safety check");
10036
+ return false;
10037
+ };
10038
+ const deadline = performance.now() + 2000;
10039
+ for (let attempt = 0; attempt < 5; attempt++) {
10040
+ if (!current())
10041
+ return false;
10042
+ if (performance.now() >= deadline)
10043
+ return hold("output_changed");
10044
+ let screen;
10045
+ for (let read = 0; read < 5 && screen === undefined; read++) {
10046
+ try {
10047
+ screen = await pane.capturePane();
10048
+ }
10049
+ catch { /* Reads do not construct an input operation. */ }
10050
+ if (!current())
10051
+ return false;
10052
+ if (performance.now() >= deadline)
10053
+ return hold("capture_failed");
10054
+ if (screen === undefined && read < 4)
10055
+ await new Promise(done => setTimeout(done, 100));
10056
+ }
10057
+ if (screen === undefined)
10058
+ return hold("capture_failed");
9678
10059
  try {
9679
- finalPane = await tmux.capturePane();
10060
+ if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
10061
+ return hold("dialog");
9680
10062
  }
9681
10063
  catch {
9682
- finalPane = null;
10064
+ return hold("check_failed");
9683
10065
  }
9684
10066
  if (!current())
9685
10067
  return false;
9686
- let dialog;
10068
+ if (performance.now() >= deadline)
10069
+ return hold("output_changed");
10070
+ let sent;
9687
10071
  try {
9688
- dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
10072
+ sent = await pane.pasteBuffer(text, { guard: current, proof: { kind: "composer", screen } });
9689
10073
  }
9690
10074
  catch {
9691
- dialog = undefined;
10075
+ return hold("unconfirmed");
9692
10076
  }
9693
- if (dialog !== undefined) {
9694
- onInputHeld?.("dialog");
9695
- this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
10077
+ if (sent)
10078
+ return true; // Exactly one successful paste ends the loop, before any Enter.
10079
+ if (!current())
9696
10080
  return false;
9697
- }
10081
+ if (pane.lastInputWasNotSent?.() !== true || pane.getLastPasteError?.() !== "Pane changed after observation; input was not sent")
10082
+ return hold("unconfirmed");
10083
+ if (attempt >= 4 || performance.now() >= deadline)
10084
+ return hold("output_changed");
10085
+ await new Promise(done => setTimeout(done, 100));
9698
10086
  }
9699
- // W1: persist the "Enter may have happened" boundary before tmux receives
9700
- // the key. A crash after this commit is never auto-replayed without a
9701
- // positive transcript marker, even if the stale pane still shows composer.
9702
- if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
9703
- this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
9704
- return false;
9705
- }
9706
- const sent = await tmux.sendSpecialKey("Enter");
9707
- if (!sent) {
9708
- this.logger.error({
9709
- phase,
9710
- tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
9711
- }, "tmux send-keys Enter failed during message delivery");
9712
- }
9713
- return sent;
10087
+ return hold("output_changed");
9714
10088
  }
9715
10089
  async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
9716
10090
  // Last and defaulted so the positional callers that ignore the outcome stay
@@ -9748,6 +10122,9 @@ export class Daemon extends EventEmitter {
9748
10122
  const tmuxAtWrite = this.tmux;
9749
10123
  const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
9750
10124
  && this.tmux === tmuxAtWrite && this.spawnGeneration === spawnAtWrite;
10125
+ // Alpha (S-36, Prism #2989 P2): a NATIVE pane keeps #2958's pre-paste owner fence through the initial Enter and every
10126
+ // retry Enter (a pane replaced while the paste awaits never takes this delivery's Enter); a tmux pane follows main.
10127
+ const nativeOwner = this.nativePaneLifecycle ? steerCurrent : undefined;
9751
10128
  // Bug A: paste with backoff. Transient failures are usually a stale window id
9752
10129
  // after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
9753
10130
  const maxAttempts = 3;
@@ -9815,8 +10192,8 @@ export class Daemon extends EventEmitter {
9815
10192
  }
9816
10193
  verdict.durableBeginCommitted = true;
9817
10194
  verdict.submissionMode = attemptEvidence.submissionMode;
9818
- if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
9819
- verdict.transcriptCheckpoint = {
10195
+ if (attemptEvidence.runtimeTranscript || attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
10196
+ verdict.transcriptCheckpoint = attemptEvidence.runtimeTranscript ? { backend: attemptEvidence.backend, runtime: attemptEvidence.runtimeTranscript } : {
9820
10197
  backend: attemptEvidence.backend,
9821
10198
  path: attemptEvidence.transcriptPath,
9822
10199
  offset: attemptEvidence.transcriptOffset,
@@ -9881,7 +10258,15 @@ export class Daemon extends EventEmitter {
9881
10258
  }
9882
10259
  const pasteGeneration = this.spawnGeneration;
9883
10260
  beforePaste?.();
9884
- const pasted = await this.tmux.pasteBuffer(formatted);
10261
+ const pasted = await this.sendDeliveryPaste(formatted, nativeOwner);
10262
+ if (!pasted && this.nativePaneLifecycle) {
10263
+ // The native write attempt is retained as uncertain, including exhausted positive refusals. Never route an
10264
+ // unknown/partial write through legacy window recovery or abort it back to an automatically replayable row.
10265
+ verdict.paneWriteStarted = true;
10266
+ verdict.phase = "paste";
10267
+ verdict.proof = "native-paste-held";
10268
+ return false;
10269
+ }
9885
10270
  if (pasted)
9886
10271
  this.rememberPaste(formatted, pasteGeneration);
9887
10272
  if (!pasted) {
@@ -9967,16 +10352,15 @@ export class Daemon extends EventEmitter {
9967
10352
  let enterAt = Date.now();
9968
10353
  const enterMono = performance.now();
9969
10354
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
9970
- let inputHeld = null;
9971
- if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
10355
+ let inputHeld = "";
10356
+ if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof || nativeOwner ? steerEnterCurrent : undefined, durableAttempt, reason => { inputHeld = reason; }))) {
9972
10357
  if (inputHeld) {
9973
- // #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
9974
- // the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
10358
+ // #2971: held in a tmux mode past the bound. The paste may still be in the CLI: keep it uncertain, never re-paste.
9975
10359
  verdict.phase = "submit-enter";
9976
- verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
10360
+ verdict.proof = inputHeld === "pane_mode" ? "pane-in-mode-held" : `native-input-${inputHeld}`;
9977
10361
  return false;
9978
10362
  }
9979
- if (steerComposerProof && !steerCurrent()) {
10363
+ if ((steerComposerProof || nativeOwner) && !steerCurrent()) {
9980
10364
  verdict.phase = "submit-enter";
9981
10365
  verdict.proof = "fenced-before-enter";
9982
10366
  return false;
@@ -10005,7 +10389,7 @@ export class Daemon extends EventEmitter {
10005
10389
  if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
10006
10390
  await new Promise(r => setTimeout(r, 1_000));
10007
10391
  const retryAt = Date.now();
10008
- if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
10392
+ if (await this.sendDeliveryEnter("queue-less-defensive-retry", nativeOwner)) {
10009
10393
  enterAt = retryAt;
10010
10394
  enterRetry = true;
10011
10395
  this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
@@ -10136,7 +10520,7 @@ export class Daemon extends EventEmitter {
10136
10520
  // again. The prompt is back now, so an Enter can land.
10137
10521
  this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
10138
10522
  const strandedAt = Date.now();
10139
- if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
10523
+ if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", nativeOwner))) {
10140
10524
  return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
10141
10525
  }
10142
10526
  // The recovery Enter can be accepted before Codex paints its new
@@ -10189,7 +10573,7 @@ export class Daemon extends EventEmitter {
10189
10573
  }
10190
10574
  await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
10191
10575
  const retryAt = Date.now();
10192
- if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
10576
+ if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", nativeOwner))) {
10193
10577
  return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
10194
10578
  }
10195
10579
  if (windowId && this.controlClient) {
@@ -10241,7 +10625,7 @@ export class Daemon extends EventEmitter {
10241
10625
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
10242
10626
  const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
10243
10627
  const retryAt = Date.now();
10244
- if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
10628
+ if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", nativeOwner))) {
10245
10629
  return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
10246
10630
  }
10247
10631
  submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
@@ -10329,7 +10713,7 @@ export class Daemon extends EventEmitter {
10329
10713
  }
10330
10714
  // No control client to observe output: fall back to the legacy double-Enter.
10331
10715
  await new Promise(r => setTimeout(r, 1000));
10332
- await this.sendDeliveryEnter("unobserved-defensive-retry");
10716
+ await this.sendDeliveryEnter("unobserved-defensive-retry", nativeOwner);
10333
10717
  if (status)
10334
10718
  this.emit("message_confirmed", status); // ✅ (best-effort)
10335
10719
  }
@@ -10549,14 +10933,22 @@ export class Daemon extends EventEmitter {
10549
10933
  * and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
10550
10934
  */
10551
10935
  transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
10936
+ if (this.nativePaneLifecycle && !checkpoint.runtime)
10937
+ throw Error("Native proof cannot read a coordinator-local transcript path");
10552
10938
  const launchAtWrite = this.launchFenceEpoch;
10553
10939
  return {
10554
- reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
10940
+ reader: checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
10555
10941
  current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
10556
- && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
10942
+ && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
10943
+ && (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
10557
10944
  onProof,
10558
10945
  };
10559
10946
  }
10947
+ nativeTranscriptReader(reference, needle) {
10948
+ if (!this.nativePaneLifecycle || !this.nativePaneLifecycle.current(reference))
10949
+ throw Error("Native delivery transcript owner is unavailable");
10950
+ return this.nativePaneLifecycle.proofReader(reference, needle);
10951
+ }
10560
10952
  /** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
10561
10953
  cliLabel() {
10562
10954
  return this.config.backend ?? this.backend?.binaryName ?? "CLI";
@@ -10800,7 +11192,14 @@ export class Daemon extends EventEmitter {
10800
11192
  * (the retries included). Without a guard it is exactly the old unconditional path.
10801
11193
  */
10802
11194
  async submitSystemPaste(text, label, guard, admission) {
10803
- const current = guard || admission ? () => { admission?.(); return guard?.current() ?? true; } : undefined;
11195
+ // Alpha (S-36, Prism #2989 P2): a NATIVE pane's system paste keeps the pane owner from before its first await
11196
+ // (#2958's fence, reverted on main for tmux panes) — a pane replaced while the paste awaits never takes its Enter.
11197
+ const native = !!this.nativePaneLifecycle;
11198
+ const ownerTmux = this.tmux, ownerGeneration = this.spawnGeneration, ownerFence = this.launchFenceEpoch;
11199
+ const ownerWindow = native ? ownerTmux?.getWindowId() : undefined;
11200
+ const owner = () => !native || (!!ownerTmux && !!ownerWindow && this.tmux === ownerTmux && ownerTmux.getWindowId() === ownerWindow
11201
+ && this.spawnGeneration === ownerGeneration && this.launchFenceEpoch === ownerFence && !this.deliveryWritesStopping);
11202
+ const current = guard || admission || native ? () => { admission?.(); return owner() && (guard?.current() ?? true); } : undefined;
10804
11203
  if (current && !current())
10805
11204
  return false;
10806
11205
  if (!this.tmux)
@@ -10920,13 +11319,13 @@ export class Daemon extends EventEmitter {
10920
11319
  && sameInteractionOwner(recoveryOwner, this.interactionOwner());
10921
11320
  const previousWindowId = this.tmux?.getWindowId();
10922
11321
  try {
10923
- const windows = await TmuxManager.listWindows(this.tmuxSessionName, this.controlClient);
11322
+ const windows = await this.paneHost.listWindows(this.tmuxSessionName, this.controlClient);
10924
11323
  if (!recoveryCurrent() || this.tmux !== previousTmux)
10925
11324
  return undefined;
10926
11325
  const match = windows.find(w => w.name === this.name);
10927
11326
  if (!match)
10928
11327
  return undefined;
10929
- this.tmux = new TmuxManager(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
11328
+ this.tmux = this.paneHost.bind(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
10930
11329
  const recoveredTmux = this.tmux;
10931
11330
  writeFileSync(join(this.instanceDir, "window-id"), match.id);
10932
11331
  // The window we were talking to is gone; leaving it registered means the
@@ -10989,6 +11388,8 @@ export class Daemon extends EventEmitter {
10989
11388
  return false;
10990
11389
  }
10991
11390
  getWindowId() {
11391
+ if (this.nativePaneLifecycle)
11392
+ return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
10992
11393
  try {
10993
11394
  return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
10994
11395
  }
@@ -11020,7 +11421,51 @@ export class Daemon extends EventEmitter {
11020
11421
  * Handle a tool call from the MCP server (forwarded by Claude).
11021
11422
  * Routes to the channel adapter via MessageBus.
11022
11423
  */
11023
- handleToolCall(msg, socket) {
11424
+ /** Internal native-MCP admission. The pane/machine/launch owner supplies a current, dynamically checked tool lease. */
11425
+ handlePaneToolCall(input, lease) {
11426
+ const refusal = nativePaneToolRefusal(input.tool, input.args);
11427
+ if (refusal)
11428
+ return Promise.resolve({ result: null, error: refusal });
11429
+ if (!lease.current() || !lease.allowed(input.tool, input.args) || this.deliveryWritesStopping)
11430
+ return Promise.resolve({ result: null, error: "Pane tool permission is no longer current" });
11431
+ const fence = this.launchFenceEpoch;
11432
+ const current = () => lease.current() && lease.allowed(input.tool, input.args) && !this.deliveryWritesStopping && fence === this.launchFenceEpoch;
11433
+ return new Promise(resolve => {
11434
+ let settled = false;
11435
+ const finish = (value) => { if (settled)
11436
+ return; settled = true; clearTimeout(timer); resolve(value); };
11437
+ const timer = setTimeout(() => finish({ result: null, error: "Pane tool result is unconfirmed; inspect its operation before retrying" }), daemonBudgetMs(input.tool) + 1000);
11438
+ timer.unref?.();
11439
+ try {
11440
+ this.handleToolCall({ ...input, requestId: ++this.fleetRequestSeq }, undefined, {
11441
+ current,
11442
+ dispatch: (tool, args, respond) => {
11443
+ if (!current()) {
11444
+ respond(null, "Pane tool permission was revoked before dispatch");
11445
+ return true;
11446
+ }
11447
+ const dispatched = lease.dispatch?.(tool, args);
11448
+ if (!dispatched)
11449
+ return false;
11450
+ void dispatched.then(result => respond(result), () => respond(null, "Pane tool operation was refused"));
11451
+ return true;
11452
+ },
11453
+ // Resolving the relay is not proof that its HTTP response reached the provider. No response-written receipt yet.
11454
+ send: value => { finish({ result: value.result, ...(typeof value.error === "string" ? { error: value.error } : {}) }); return false; },
11455
+ });
11456
+ }
11457
+ catch {
11458
+ finish({ result: null, error: "Pane tool operation failed" });
11459
+ }
11460
+ });
11461
+ }
11462
+ /** Only the authority lease with a verified helper stdout ACK calls this internal barrier. */
11463
+ paneToolResponseWritten(operationId, current) {
11464
+ if (!current() || this.deliveryWritesStopping || !/^[a-f0-9-]{36}$/i.test(operationId))
11465
+ return;
11466
+ this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
11467
+ }
11468
+ handleToolCall(msg, socket, peer) {
11024
11469
  const tool = msg.tool;
11025
11470
  const args = (msg.args ?? {});
11026
11471
  const requestId = msg.requestId;
@@ -11028,15 +11473,15 @@ export class Daemon extends EventEmitter {
11028
11473
  this.logger.debug({ tool, requestId }, "Tool call from MCP server");
11029
11474
  // A tool call can only come from a live MCP server of ours — proof of life
11030
11475
  // that outranks whatever the pid slot says.
11031
- if (this.socketSessionNames.get(socket) === this.name) {
11032
- this.noteMcpProofOfLife("tool_call", this.socketPids.get(socket));
11476
+ if (peer?.current() || socket && this.socketSessionNames.get(socket) === this.name) {
11477
+ this.noteMcpProofOfLife("tool_call", socket ? this.socketPids.get(socket) : undefined);
11033
11478
  }
11034
11479
  // A tool invocation is not delivery evidence. Capture which obligation it
11035
11480
  // belongs to now, then settle it only after the adapter/fleet responds.
11036
11481
  // Explicitly mapped sibling sessions must not satisfy this instance's turn;
11037
11482
  // an unmapped socket is retained for legacy/tests where mcp_ready was not
11038
11483
  // observed before the first tool call.
11039
- const sourceSession = this.socketSessionNames.get(socket);
11484
+ const sourceSession = peer ? this.name : socket ? this.socketSessionNames.get(socket) : undefined;
11040
11485
  const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
11041
11486
  ? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
11042
11487
  : null;
@@ -11082,14 +11527,21 @@ export class Daemon extends EventEmitter {
11082
11527
  // failed" and report unknown immediately rather than starting recovery.
11083
11528
  this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
11084
11529
  }
11085
- const sent = this.ipcServer?.send(socket, { requestId, result, error, operationId }) ?? false;
11530
+ const packet = { requestId, result, error, operationId };
11531
+ const sent = peer ? peer.current() && peer.send(packet) : socket ? this.ipcServer?.send(socket, packet) ?? false : false;
11086
11532
  if (!sent) {
11087
- this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
11533
+ if (peer)
11534
+ this.logger.debug({ tool, requestId }, "Native MCP result awaits transport-written confirmation");
11535
+ else
11536
+ this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
11088
11537
  }
11089
11538
  else if (operationId) {
11090
11539
  this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
11091
11540
  }
11092
11541
  };
11542
+ // Native Fleet/task operations carry their lease to the existing dispatcher mutation admission points.
11543
+ if (peer?.dispatch?.(tool, args, respond))
11544
+ return;
11093
11545
  // Repo checkout — handled locally in daemon (no fleet-manager)
11094
11546
  if (tool === "checkout_repo") {
11095
11547
  this.handleCheckoutRepo(args, respond);
@@ -11118,7 +11570,7 @@ export class Daemon extends EventEmitter {
11118
11570
  clearTimeout(timeout);
11119
11571
  respond(respMsg.result, respMsg.error);
11120
11572
  });
11121
- this.ipcServer?.broadcast({
11573
+ this.broadcastFleetMessage({
11122
11574
  type,
11123
11575
  payload: args,
11124
11576
  meta: { instance_name: this.name },
@@ -11144,7 +11596,7 @@ export class Daemon extends EventEmitter {
11144
11596
  clearTimeout(timeout);
11145
11597
  resolve({ result: respMsg.result, error: respMsg.error });
11146
11598
  });
11147
- this.ipcServer?.broadcast({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
11599
+ this.broadcastFleetMessage({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
11148
11600
  });
11149
11601
  void (async () => {
11150
11602
  if (tool === "preview_web_appearance") {
@@ -11221,7 +11673,7 @@ export class Daemon extends EventEmitter {
11221
11673
  clearTimeout(timeout);
11222
11674
  respond(respMsg.result, respMsg.error);
11223
11675
  });
11224
- this.ipcServer?.broadcast({
11676
+ this.broadcastFleetMessage({
11225
11677
  type: "fleet_task",
11226
11678
  payload: args,
11227
11679
  meta: { instance_name: this.name },
@@ -11244,7 +11696,7 @@ export class Daemon extends EventEmitter {
11244
11696
  clearTimeout(timeout);
11245
11697
  respond(respMsg.result, respMsg.error);
11246
11698
  });
11247
- this.ipcServer?.broadcast({
11699
+ this.broadcastFleetMessage({
11248
11700
  type: typeMap[tool],
11249
11701
  payload: args,
11250
11702
  meta: { instance_name: this.name, working_directory: this.config.working_directory },
@@ -11271,7 +11723,7 @@ export class Daemon extends EventEmitter {
11271
11723
  clearTimeout(timeout);
11272
11724
  respond(respMsg.result, respMsg.error);
11273
11725
  });
11274
- this.ipcServer?.broadcast({
11726
+ this.broadcastFleetMessage({
11275
11727
  type: typeMap[tool],
11276
11728
  payload: args,
11277
11729
  meta: {
@@ -11286,11 +11738,11 @@ export class Daemon extends EventEmitter {
11286
11738
  }
11287
11739
  if (CROSS_INSTANCE_TOOLS.has(tool)) {
11288
11740
  // Route to fleet manager via IPC (topic mode only)
11289
- if (this.topicMode && this.ipcServer) {
11741
+ if (this.topicMode && this.fleetTransportAvailable()) {
11290
11742
  // Use fleetRequestId (not requestId) to avoid MCP server resolving the
11291
11743
  // pending tool call prematurely when it receives the broadcast.
11292
11744
  const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
11293
- const senderSessionName = this.socketSessionNames.get(socket);
11745
+ const senderSessionName = sourceSession;
11294
11746
  // Shared table, so delete_instance and the deployment tools get the same
11295
11747
  // budget here as the MCP client expects (the ad-hoc list omitted them).
11296
11748
  const crossTimeoutMs = daemonBudgetMs(tool);
@@ -11302,7 +11754,7 @@ export class Daemon extends EventEmitter {
11302
11754
  clearTimeout(timeout);
11303
11755
  respond(respMsg.result, respMsg.error);
11304
11756
  });
11305
- this.ipcServer.broadcast({
11757
+ this.broadcastFleetMessage({
11306
11758
  type: "fleet_outbound",
11307
11759
  tool,
11308
11760
  args,
@@ -11361,7 +11813,7 @@ export class Daemon extends EventEmitter {
11361
11813
  // Register the waiter before broadcasting. A fast/mock adapter can settle
11362
11814
  // immediately; broadcasting first left a response-before-registration
11363
11815
  // window that degraded into a timeout despite a completed POST.
11364
- this.ipcServer?.broadcast({
11816
+ this.broadcastFleetMessage({
11365
11817
  type: "fleet_outbound",
11366
11818
  tool,
11367
11819
  args,
@@ -11868,6 +12320,8 @@ export class Daemon extends EventEmitter {
11868
12320
  catch { /* best effort */ }
11869
12321
  }
11870
12322
  async spawnClaudeWindow() {
12323
+ if (this.nativePaneLifecycle)
12324
+ throw Error("Native panes never enter local resume/fresh fallback");
11871
12325
  this.assertSessionOomAdmission();
11872
12326
  this.beginSpawn();
11873
12327
  let resumedSuccessfully = false;
@@ -12454,6 +12908,14 @@ export class Daemon extends EventEmitter {
12454
12908
  await this.museUsageRelay.stop().catch(() => { });
12455
12909
  this.museUsageRelay = null;
12456
12910
  }
12911
+ if (this.nativePaneLifecycle) {
12912
+ await this.nativePaneLifecycle.release("detach");
12913
+ try {
12914
+ unlinkSync(join(this.instanceDir, "daemon.pid"));
12915
+ }
12916
+ catch { /* metadata only */ }
12917
+ return;
12918
+ }
12457
12919
  try {
12458
12920
  await this.killProcessTree("SIGTERM", "startup aborted");
12459
12921
  }
@@ -12483,13 +12945,15 @@ export class Daemon extends EventEmitter {
12483
12945
  }
12484
12946
  /** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
12485
12947
  async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
12948
+ if (this.nativePaneLifecycle)
12949
+ throw Error("Native processes can only be closed through their owned runtime pane");
12486
12950
  const windowId = this.tmux?.getWindowId();
12487
12951
  // Before createWindow succeeds there is no pane owned by this daemon.
12488
12952
  // tmux interprets an empty selector as the session's active window.
12489
12953
  if (!windowId?.trim())
12490
12954
  return;
12491
12955
  try {
12492
- const pid = await TmuxManager.getPanePid(this.tmuxSessionName, windowId);
12956
+ const pid = await this.paneHost.getPanePid(this.tmuxSessionName, windowId);
12493
12957
  if (pid) {
12494
12958
  this.noteStopRequest(signal, reason);
12495
12959
  process.kill(-pid, signal);
@@ -12730,6 +13194,8 @@ export class Daemon extends EventEmitter {
12730
13194
  }
12731
13195
  }
12732
13196
  async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
13197
+ if (this.nativePaneLifecycle)
13198
+ throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
12733
13199
  this.assertSessionOomAdmission();
12734
13200
  this.startupAdmission?.();
12735
13201
  const launchFence = this.launchFenceEpoch;
@@ -12834,8 +13300,8 @@ export class Daemon extends EventEmitter {
12834
13300
  this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
12835
13301
  }
12836
13302
  // Ensure tmux session exists (may have been destroyed if all windows died)
12837
- await TmuxManager.ensureSession(this.tmuxSessionName);
12838
- if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
13303
+ await this.paneHost.ensureSession(this.tmuxSessionName);
13304
+ if (this.stormWindow?.observeServerAlive(await this.paneHost.getServerPid(this.tmuxSessionName))) {
12839
13305
  this.emit("tmux_server_crash", this.name);
12840
13306
  }
12841
13307
  this.startupAdmission?.();
@@ -13109,12 +13575,15 @@ export class Daemon extends EventEmitter {
13109
13575
  // each have seen a respawn, pause or stop.
13110
13576
  if (!owned())
13111
13577
  return false;
13578
+ const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
13579
+ if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
13580
+ return false;
13112
13581
  if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
13113
- if (!await this.tmux.sendSpecialKey(key))
13582
+ if (!await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key)))
13114
13583
  return false;
13115
13584
  }
13116
13585
  else {
13117
- if (!await this.tmux.sendKeys(key))
13586
+ if (!await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key)))
13118
13587
  return false;
13119
13588
  }
13120
13589
  await new Promise(r => setTimeout(r, 200));
@@ -13128,7 +13597,8 @@ export class Daemon extends EventEmitter {
13128
13597
  // Wait for next screen to render — bounded by what is left of the budget.
13129
13598
  const renderWait = Math.max(0, Math.min(10_000, remaining()));
13130
13599
  if (this.controlClient) {
13131
- const wid = readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
13600
+ const wid = this.nativePaneLifecycle ? ownerTmux.getWindowId()
13601
+ : readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
13132
13602
  await this.controlClient.waitForIdle(wid, renderWait);
13133
13603
  }
13134
13604
  else {
@@ -13164,7 +13634,9 @@ export class Daemon extends EventEmitter {
13164
13634
  // scan parked on the old limit text.
13165
13635
  const codexReady = this.backend.binaryName !== "codex"
13166
13636
  || this.isCodexLivePaneSnapshot(pane);
13167
- if (readyPatternMatches && codexReady) {
13637
+ const nativeComposerReady = !this.nativePaneLifecycle || (this.backend?.readInputRow
13638
+ ? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
13639
+ if (readyPatternMatches && codexReady && nativeComposerReady) {
13168
13640
  cleanReadyPolls++;
13169
13641
  if (cleanReadyPolls >= 2 || remaining() <= 0) {
13170
13642
  // A real ready prompt (with no fatal dialog on screen — those are
@@ -13289,6 +13761,16 @@ export class Daemon extends EventEmitter {
13289
13761
  void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
13290
13762
  }
13291
13763
  saveSessionId() {
13764
+ if (this.nativePaneLifecycle) {
13765
+ try {
13766
+ this.nativePaneLifecycle.checkpointSession();
13767
+ this.ownedWorkingSession = this.backend?.getSessionId() ?? null;
13768
+ }
13769
+ catch (err) {
13770
+ this.logger.warn({ err }, "Native conversation checkpoint is unconfirmed");
13771
+ }
13772
+ return;
13773
+ }
13292
13774
  // When a resume failure has forced a fresh start, don't persist the stale id
13293
13775
  // back from statusline.json — that would re-arm --continue and re-loop.
13294
13776
  if (this.skipResume)