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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/README.md +2 -0
  2. package/README.zh-TW.md +2 -0
  3. package/THIRD_PARTY_NOTICES.md +53 -0
  4. package/dist/agent-endpoint.d.ts +5 -1
  5. package/dist/agent-endpoint.js +12 -2
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/auth-api.d.ts +21 -0
  8. package/dist/auth-api.js +89 -2
  9. package/dist/auth-api.js.map +1 -1
  10. package/dist/authority/web-authority.d.ts +12 -2
  11. package/dist/authority/web-authority.js +25 -4
  12. package/dist/authority/web-authority.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +15 -12
  14. package/dist/backend/claude-code.js +152 -133
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-package.d.ts +5 -2
  17. package/dist/backend/codex-package.js +1 -5
  18. package/dist/backend/codex-package.js.map +1 -1
  19. package/dist/backend/codex.d.ts +136 -113
  20. package/dist/backend/codex.js +1281 -1219
  21. package/dist/backend/codex.js.map +1 -1
  22. package/dist/backend/grok-model-cache.d.ts +7 -0
  23. package/dist/backend/grok-model-cache.js +65 -0
  24. package/dist/backend/grok-model-cache.js.map +1 -0
  25. package/dist/backend/grok.d.ts +2 -0
  26. package/dist/backend/grok.js +22 -0
  27. package/dist/backend/grok.js.map +1 -1
  28. package/dist/backend/native-pane.d.ts +9 -0
  29. package/dist/backend/native-pane.js +59 -0
  30. package/dist/backend/native-pane.js.map +1 -0
  31. package/dist/backend/types.d.ts +7 -0
  32. package/dist/backend/types.js.map +1 -1
  33. package/dist/channel/ipc-bridge.d.ts +2 -0
  34. package/dist/channel/mcp-tools.js +1 -1
  35. package/dist/channel/mcp-tools.js.map +1 -1
  36. package/dist/cli.js +3 -0
  37. package/dist/cli.js.map +1 -1
  38. package/dist/config-validator.js +71 -1
  39. package/dist/config-validator.js.map +1 -1
  40. package/dist/config.js +16 -0
  41. package/dist/config.js.map +1 -1
  42. package/dist/daemon.d.ts +67 -5
  43. package/dist/daemon.js +823 -266
  44. package/dist/daemon.js.map +1 -1
  45. package/dist/delivery-consumed-watch.d.ts +15 -3
  46. package/dist/delivery-consumed-watch.js +23 -1
  47. package/dist/delivery-consumed-watch.js.map +1 -1
  48. package/dist/delivery-outbox.d.ts +5 -0
  49. package/dist/delivery-outbox.js +32 -4
  50. package/dist/delivery-outbox.js.map +1 -1
  51. package/dist/delivery-reconciliation.d.ts +2 -35
  52. package/dist/delivery-reconciliation.js +2 -156
  53. package/dist/delivery-reconciliation.js.map +1 -1
  54. package/dist/fleet-context.d.ts +2 -2
  55. package/dist/fleet-level-config.d.ts +1 -1
  56. package/dist/fleet-level-config.js +2 -0
  57. package/dist/fleet-level-config.js.map +1 -1
  58. package/dist/fleet-manager.d.ts +238 -3
  59. package/dist/fleet-manager.js +1826 -104
  60. package/dist/fleet-manager.js.map +1 -1
  61. package/dist/fleet-windows-runtime.d.ts +92 -0
  62. package/dist/fleet-windows-runtime.js +380 -0
  63. package/dist/fleet-windows-runtime.js.map +1 -0
  64. package/dist/general-binding.d.ts +85 -0
  65. package/dist/general-binding.js +298 -0
  66. package/dist/general-binding.js.map +1 -0
  67. package/dist/general-host-owner.d.ts +8 -0
  68. package/dist/general-host-owner.js +26 -0
  69. package/dist/general-host-owner.js.map +1 -0
  70. package/dist/general-instance.d.ts +7 -0
  71. package/dist/general-instance.js +20 -3
  72. package/dist/general-instance.js.map +1 -1
  73. package/dist/general-role.d.ts +5 -0
  74. package/dist/general-role.js +26 -0
  75. package/dist/general-role.js.map +1 -0
  76. package/dist/instance-config-impact.js +4 -0
  77. package/dist/instance-config-impact.js.map +1 -1
  78. package/dist/instance-lifecycle.d.ts +21 -3
  79. package/dist/instance-lifecycle.js +378 -162
  80. package/dist/instance-lifecycle.js.map +1 -1
  81. package/dist/instructions.d.ts +2 -0
  82. package/dist/instructions.js +6 -0
  83. package/dist/instructions.js.map +1 -1
  84. package/dist/local-pane-endpoint.d.ts +46 -0
  85. package/dist/local-pane-endpoint.js +189 -0
  86. package/dist/local-pane-endpoint.js.map +1 -0
  87. package/dist/local-pane-launcher.d.ts +2 -0
  88. package/dist/local-pane-launcher.js +38 -0
  89. package/dist/local-pane-launcher.js.map +1 -0
  90. package/dist/local-pane-stage.ps1 +101 -0
  91. package/dist/locale.js +85 -0
  92. package/dist/locale.js.map +1 -1
  93. package/dist/model-catalog-defaults.d.ts +14 -0
  94. package/dist/model-catalog-defaults.js +20 -0
  95. package/dist/model-catalog-defaults.js.map +1 -0
  96. package/dist/native-agent-admission.d.ts +7 -0
  97. package/dist/native-agent-admission.js +26 -0
  98. package/dist/native-agent-admission.js.map +1 -0
  99. package/dist/native-daemon-client.d.ts +13 -0
  100. package/dist/native-daemon-client.js +28 -0
  101. package/dist/native-daemon-client.js.map +1 -0
  102. package/dist/native-instance-api.d.ts +14 -0
  103. package/dist/native-instance-api.js +205 -0
  104. package/dist/native-instance-api.js.map +1 -0
  105. package/dist/native-instance-input.d.ts +84 -0
  106. package/dist/native-instance-input.js +54 -0
  107. package/dist/native-instance-input.js.map +1 -0
  108. package/dist/native-instance-registry.d.ts +32 -0
  109. package/dist/native-instance-registry.js +105 -0
  110. package/dist/native-instance-registry.js.map +1 -0
  111. package/dist/native-mcp.d.ts +40 -0
  112. package/dist/native-mcp.js +278 -0
  113. package/dist/native-mcp.js.map +1 -0
  114. package/dist/native-model-catalog.d.ts +5 -0
  115. package/dist/native-model-catalog.js +76 -0
  116. package/dist/native-model-catalog.js.map +1 -0
  117. package/dist/native-pane-crash-budget.d.ts +20 -0
  118. package/dist/native-pane-crash-budget.js +60 -0
  119. package/dist/native-pane-crash-budget.js.map +1 -0
  120. package/dist/native-pane-create.d.ts +26 -0
  121. package/dist/native-pane-create.js +53 -0
  122. package/dist/native-pane-create.js.map +1 -0
  123. package/dist/native-pane-lifecycle.d.ts +65 -0
  124. package/dist/native-pane-lifecycle.js +230 -0
  125. package/dist/native-pane-lifecycle.js.map +1 -0
  126. package/dist/native-pane-manager.d.ts +59 -0
  127. package/dist/native-pane-manager.js +584 -0
  128. package/dist/native-pane-manager.js.map +1 -0
  129. package/dist/native-pane-output-log.d.ts +15 -0
  130. package/dist/native-pane-output-log.js +108 -0
  131. package/dist/native-pane-output-log.js.map +1 -0
  132. package/dist/native-pane-reconciliation.d.ts +4 -0
  133. package/dist/native-pane-reconciliation.js +40 -0
  134. package/dist/native-pane-reconciliation.js.map +1 -0
  135. package/dist/native-pane-recovery.d.ts +12 -0
  136. package/dist/native-pane-recovery.js +10 -0
  137. package/dist/native-pane-recovery.js.map +1 -0
  138. package/dist/native-pane-tools.d.ts +2 -0
  139. package/dist/native-pane-tools.js +14 -0
  140. package/dist/native-pane-tools.js.map +1 -0
  141. package/dist/needs-you-hub.d.ts +2 -0
  142. package/dist/needs-you-hub.js +2 -1
  143. package/dist/needs-you-hub.js.map +1 -1
  144. package/dist/needs-you.d.ts +19 -1
  145. package/dist/needs-you.js +12 -2
  146. package/dist/needs-you.js.map +1 -1
  147. package/dist/outbound-handlers.d.ts +16 -2
  148. package/dist/outbound-handlers.js +67 -3
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +38 -0
  151. package/dist/outbound-schemas.js +12 -0
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/pane-port.d.ts +118 -0
  154. package/dist/pane-port.js +2 -0
  155. package/dist/pane-port.js.map +1 -0
  156. package/dist/public-web-gateway.js +9 -0
  157. package/dist/public-web-gateway.js.map +1 -1
  158. package/dist/public-web-link.d.ts +5 -0
  159. package/dist/public-web-link.js +4 -0
  160. package/dist/public-web-link.js.map +1 -1
  161. package/dist/quickstart-api.js +9 -5
  162. package/dist/quickstart-api.js.map +1 -1
  163. package/dist/quickstart.d.ts +1 -0
  164. package/dist/quickstart.js +4 -1
  165. package/dist/quickstart.js.map +1 -1
  166. package/dist/runtime-api.d.ts +8 -0
  167. package/dist/runtime-api.js +96 -0
  168. package/dist/runtime-api.js.map +1 -0
  169. package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +21 -0
  170. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
  171. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
  172. package/dist/runtime-assets/@xterm/addon-serialize/package.json +29 -0
  173. package/dist/runtime-assets/@xterm/headless/LICENSE +21 -0
  174. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +2 -0
  175. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
  176. package/dist/runtime-assets/@xterm/headless/package.json +29 -0
  177. package/dist/runtime-assets/node-pty/CONPTY-LICENSE +21 -0
  178. package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +503 -0
  179. package/dist/runtime-assets/node-pty/LICENSE +69 -0
  180. package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +16 -0
  181. package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +47 -0
  182. package/dist/runtime-assets/node-pty/lib/index.js +52 -0
  183. package/dist/runtime-assets/node-pty/lib/interfaces.js +7 -0
  184. package/dist/runtime-assets/node-pty/lib/shared/conout.js +11 -0
  185. package/dist/runtime-assets/node-pty/lib/terminal.js +190 -0
  186. package/dist/runtime-assets/node-pty/lib/types.js +7 -0
  187. package/dist/runtime-assets/node-pty/lib/unixTerminal.js +346 -0
  188. package/dist/runtime-assets/node-pty/lib/utils.js +39 -0
  189. package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +125 -0
  190. package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +320 -0
  191. package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +199 -0
  192. package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  193. package/dist/runtime-assets/node-pty/package.json +64 -0
  194. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  195. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  196. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  197. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  198. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  199. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  200. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  201. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  202. package/dist/runtime-bundle.d.ts +17 -0
  203. package/dist/runtime-bundle.js +150 -0
  204. package/dist/runtime-bundle.js.map +1 -0
  205. package/dist/runtime-cli.d.ts +2 -0
  206. package/dist/runtime-cli.js +117 -0
  207. package/dist/runtime-cli.js.map +1 -0
  208. package/dist/runtime-client-store.d.ts +49 -0
  209. package/dist/runtime-client-store.js +292 -0
  210. package/dist/runtime-client-store.js.map +1 -0
  211. package/dist/runtime-client.d.ts +100 -0
  212. package/dist/runtime-client.js +700 -0
  213. package/dist/runtime-client.js.map +1 -0
  214. package/dist/runtime-coordinator.d.ts +82 -0
  215. package/dist/runtime-coordinator.js +864 -0
  216. package/dist/runtime-coordinator.js.map +1 -0
  217. package/dist/runtime-general-protocol.d.ts +22 -0
  218. package/dist/runtime-general-protocol.js +57 -0
  219. package/dist/runtime-general-protocol.js.map +1 -0
  220. package/dist/runtime-general-workspace.d.ts +17 -0
  221. package/dist/runtime-general-workspace.js +213 -0
  222. package/dist/runtime-general-workspace.js.map +1 -0
  223. package/dist/runtime-pane-assets.d.ts +17 -0
  224. package/dist/runtime-pane-assets.js +23 -0
  225. package/dist/runtime-pane-assets.js.map +1 -0
  226. package/dist/runtime-pane-boot.d.ts +2 -0
  227. package/dist/runtime-pane-boot.js +24 -0
  228. package/dist/runtime-pane-boot.js.map +1 -0
  229. package/dist/runtime-pane-codex-index.d.ts +1 -0
  230. package/dist/runtime-pane-codex-index.js +47 -0
  231. package/dist/runtime-pane-codex-index.js.map +1 -0
  232. package/dist/runtime-pane-config.d.ts +16 -0
  233. package/dist/runtime-pane-config.js +54 -0
  234. package/dist/runtime-pane-config.js.map +1 -0
  235. package/dist/runtime-pane-host.d.ts +44 -0
  236. package/dist/runtime-pane-host.js +424 -0
  237. package/dist/runtime-pane-host.js.map +1 -0
  238. package/dist/runtime-pane-job-owner.d.ts +1 -0
  239. package/dist/runtime-pane-job-owner.js +29 -0
  240. package/dist/runtime-pane-job-owner.js.map +1 -0
  241. package/dist/runtime-pane-job.ps1 +116 -0
  242. package/dist/runtime-pane-link.d.ts +40 -0
  243. package/dist/runtime-pane-link.js +246 -0
  244. package/dist/runtime-pane-link.js.map +1 -0
  245. package/dist/runtime-pane-mcp-protocol.d.ts +32 -0
  246. package/dist/runtime-pane-mcp-protocol.js +39 -0
  247. package/dist/runtime-pane-mcp-protocol.js.map +1 -0
  248. package/dist/runtime-pane-mcp-relay.d.ts +17 -0
  249. package/dist/runtime-pane-mcp-relay.js +70 -0
  250. package/dist/runtime-pane-mcp-relay.js.map +1 -0
  251. package/dist/runtime-pane-mcp.d.ts +51 -0
  252. package/dist/runtime-pane-mcp.js +150 -0
  253. package/dist/runtime-pane-mcp.js.map +1 -0
  254. package/dist/runtime-pane-native.d.ts +45 -0
  255. package/dist/runtime-pane-native.js +65 -0
  256. package/dist/runtime-pane-native.js.map +1 -0
  257. package/dist/runtime-pane-output.d.ts +19 -0
  258. package/dist/runtime-pane-output.js +53 -0
  259. package/dist/runtime-pane-output.js.map +1 -0
  260. package/dist/runtime-pane-port.d.ts +139 -0
  261. package/dist/runtime-pane-port.js +558 -0
  262. package/dist/runtime-pane-port.js.map +1 -0
  263. package/dist/runtime-pane-private.d.ts +14 -0
  264. package/dist/runtime-pane-private.js +51 -0
  265. package/dist/runtime-pane-private.js.map +1 -0
  266. package/dist/runtime-pane-private.ps1 +14 -0
  267. package/dist/runtime-pane-process.d.ts +4 -0
  268. package/dist/runtime-pane-process.js +108 -0
  269. package/dist/runtime-pane-process.js.map +1 -0
  270. package/dist/runtime-pane-protocol.d.ts +172 -0
  271. package/dist/runtime-pane-protocol.js +195 -0
  272. package/dist/runtime-pane-protocol.js.map +1 -0
  273. package/dist/runtime-pane-screen.d.ts +19 -0
  274. package/dist/runtime-pane-screen.js +76 -0
  275. package/dist/runtime-pane-screen.js.map +1 -0
  276. package/dist/runtime-pane-service.d.ts +11 -0
  277. package/dist/runtime-pane-service.js +221 -0
  278. package/dist/runtime-pane-service.js.map +1 -0
  279. package/dist/runtime-pane-stdio-protocol.d.ts +54 -0
  280. package/dist/runtime-pane-stdio-protocol.js +94 -0
  281. package/dist/runtime-pane-stdio-protocol.js.map +1 -0
  282. package/dist/runtime-pane-stdio.d.ts +3 -0
  283. package/dist/runtime-pane-stdio.js +123 -0
  284. package/dist/runtime-pane-stdio.js.map +1 -0
  285. package/dist/runtime-pane-store.d.ts +33 -0
  286. package/dist/runtime-pane-store.js +164 -0
  287. package/dist/runtime-pane-store.js.map +1 -0
  288. package/dist/runtime-pane-transcript-protocol.d.ts +43 -0
  289. package/dist/runtime-pane-transcript-protocol.js +58 -0
  290. package/dist/runtime-pane-transcript-protocol.js.map +1 -0
  291. package/dist/runtime-pane-transcript.d.ts +45 -0
  292. package/dist/runtime-pane-transcript.js +605 -0
  293. package/dist/runtime-pane-transcript.js.map +1 -0
  294. package/dist/runtime-pane-worker.d.ts +1 -0
  295. package/dist/runtime-pane-worker.js +154 -0
  296. package/dist/runtime-pane-worker.js.map +1 -0
  297. package/dist/runtime-protocol.d.ts +149 -0
  298. package/dist/runtime-protocol.js +19 -0
  299. package/dist/runtime-protocol.js.map +1 -0
  300. package/dist/runtime-switches.d.ts +79 -0
  301. package/dist/runtime-switches.js +90 -0
  302. package/dist/runtime-switches.js.map +1 -0
  303. package/dist/runtime-transcript-delta-reader.d.ts +23 -0
  304. package/dist/runtime-transcript-delta-reader.js +74 -0
  305. package/dist/runtime-transcript-delta-reader.js.map +1 -0
  306. package/dist/runtime-transcript-source.d.ts +36 -0
  307. package/dist/runtime-transcript-source.js +189 -0
  308. package/dist/runtime-transcript-source.js.map +1 -0
  309. package/dist/runtime-windows-bridge.d.ts +41 -0
  310. package/dist/runtime-windows-bridge.js +157 -0
  311. package/dist/runtime-windows-bridge.js.map +1 -0
  312. package/dist/settings-api.d.ts +5 -0
  313. package/dist/settings-api.js +95 -1
  314. package/dist/settings-api.js.map +1 -1
  315. package/dist/settings-baseline.d.ts +1 -0
  316. package/dist/settings-baseline.js +1 -1
  317. package/dist/settings-baseline.js.map +1 -1
  318. package/dist/settings-change.js +28 -0
  319. package/dist/settings-change.js.map +1 -1
  320. package/dist/settings-confirmation.d.ts +16 -1
  321. package/dist/settings-confirmation.js +47 -9
  322. package/dist/settings-confirmation.js.map +1 -1
  323. package/dist/settings-effect.d.ts +4 -0
  324. package/dist/settings-effect.js +58 -1
  325. package/dist/settings-effect.js.map +1 -1
  326. package/dist/settings-http-confirmation.d.ts +1 -0
  327. package/dist/settings-http-confirmation.js +15 -2
  328. package/dist/settings-http-confirmation.js.map +1 -1
  329. package/dist/settings-request-capability.d.ts +2 -0
  330. package/dist/settings-request-capability.js +4 -0
  331. package/dist/settings-request-capability.js.map +1 -1
  332. package/dist/settings-route-lifetime.d.ts +14 -0
  333. package/dist/settings-route-lifetime.js +48 -0
  334. package/dist/settings-route-lifetime.js.map +1 -0
  335. package/dist/settings-transaction.d.ts +4 -0
  336. package/dist/settings-transaction.js +9 -0
  337. package/dist/settings-transaction.js.map +1 -1
  338. package/dist/setup-form.js +6 -3
  339. package/dist/setup-form.js.map +1 -1
  340. package/dist/setup-i18n.js +3 -0
  341. package/dist/setup-i18n.js.map +1 -1
  342. package/dist/tmux-control.d.ts +2 -1
  343. package/dist/tmux-control.js.map +1 -1
  344. package/dist/tmux-manager.d.ts +2 -1
  345. package/dist/tmux-manager.js.map +1 -1
  346. package/dist/tmux-pane-host.d.ts +26 -0
  347. package/dist/tmux-pane-host.js +25 -0
  348. package/dist/tmux-pane-host.js.map +1 -0
  349. package/dist/tool-permissions.js +2 -1
  350. package/dist/tool-permissions.js.map +1 -1
  351. package/dist/topic-commands.js +3 -0
  352. package/dist/topic-commands.js.map +1 -1
  353. package/dist/transcript-delta-reader.d.ts +1 -1
  354. package/dist/transcript-delta-reader.js +1 -1
  355. package/dist/transcript-delta-reader.js.map +1 -1
  356. package/dist/transcript-monitor.d.ts +3 -0
  357. package/dist/transcript-monitor.js +22 -46
  358. package/dist/transcript-monitor.js.map +1 -1
  359. package/dist/transcript-needles.d.ts +35 -0
  360. package/dist/transcript-needles.js +158 -0
  361. package/dist/transcript-needles.js.map +1 -0
  362. package/dist/transcript-records.d.ts +21 -0
  363. package/dist/transcript-records.js +140 -0
  364. package/dist/transcript-records.js.map +1 -0
  365. package/dist/transcript-sources.d.ts +5 -0
  366. package/dist/transcript-sources.js +8 -79
  367. package/dist/transcript-sources.js.map +1 -1
  368. package/dist/types.d.ts +23 -0
  369. package/dist/ui/agent-proposals.js +90 -0
  370. package/dist/ui/agent-runtime.js +106 -0
  371. package/dist/ui/chat-store.js +13 -2
  372. package/dist/ui/chat-strings.js +48 -0
  373. package/dist/ui/cli-picker.js +42 -0
  374. package/dist/ui/general-location.js +71 -0
  375. package/dist/ui/native-instance-settings.js +47 -0
  376. package/dist/ui/native-pane-recovery.js +45 -0
  377. package/dist/ui/panel-chat.js +63 -32
  378. package/dist/ui/panel-details.js +2 -1
  379. package/dist/ui/panel-fleet.js +99 -18
  380. package/dist/ui/panel-needs.js +154 -3
  381. package/dist/ui/panel-runtimes.js +231 -0
  382. package/dist/ui/panel-settings.js +19 -9
  383. package/dist/ui/panel-windows-agent.js +86 -0
  384. package/dist/ui/panel-windows-cli.js +143 -0
  385. package/dist/ui/provider-marks.js +83 -0
  386. package/dist/ui/runtime-catalog.js +55 -0
  387. package/dist/ui/settings-catalog.js +37 -10
  388. package/dist/ui/settings-confirm.js +3 -1
  389. package/dist/ui/settings-dialogs.js +22 -7
  390. package/dist/ui/settings-model.js +2 -1
  391. package/dist/ui/settings-outside.js +176 -0
  392. package/dist/ui/settings-registry.js +6 -0
  393. package/dist/ui/settings-strings.js +16 -0
  394. package/dist/ui/shared/app-i18n.js +2 -2
  395. package/dist/ui/shared/app-route.js +6 -2
  396. package/dist/ui/shared/app-shell.js +3 -2
  397. package/dist/ui/shared/app-store.js +1 -0
  398. package/dist/ui/shared/app-stream.js +3 -1
  399. package/dist/ui/shared/app.css +301 -0
  400. package/dist/ui/shared/app.js +4 -0
  401. package/dist/ui/shared/header-tools.js +6 -4
  402. package/dist/ui/shared/instance-nav.js +33 -7
  403. package/dist/ui/shared/instance-switch.js +4 -3
  404. package/dist/ui/shared/qrcode.module.js +2237 -0
  405. package/dist/ui/shared/signin.js +39 -5
  406. package/dist/ui/shared/ui-menu.js +28 -8
  407. package/dist/ui/shared/vendor/qrcode-generator.LICENSE +27 -0
  408. package/dist/ui/shared/vendor/vendor.json +19 -2
  409. package/dist/ui/shared/view-strings.js +8 -0
  410. package/dist/ui/windows-agents-nav.js +2 -0
  411. package/dist/ui/windows-cli-catalog.js +79 -0
  412. package/dist/web-api.d.ts +37 -0
  413. package/dist/web-api.js +248 -6
  414. package/dist/web-api.js.map +1 -1
  415. package/dist/web-chat-history.d.ts +19 -2
  416. package/dist/web-chat-history.js +72 -3
  417. package/dist/web-chat-history.js.map +1 -1
  418. package/dist/web-only-fleet.d.ts +20 -0
  419. package/dist/web-only-fleet.js +21 -0
  420. package/dist/web-only-fleet.js.map +1 -1
  421. package/dist/web-remote-access.d.ts +152 -0
  422. package/dist/web-remote-access.js +368 -0
  423. package/dist/web-remote-access.js.map +1 -0
  424. package/dist/web-session.d.ts +7 -0
  425. package/dist/web-session.js +2 -0
  426. package/dist/web-session.js.map +1 -1
  427. package/dist/web-shell-routes.d.ts +4 -1
  428. package/dist/web-shell-routes.js +7 -2
  429. package/dist/web-shell-routes.js.map +1 -1
  430. package/dist/windows-agents.d.ts +124 -0
  431. package/dist/windows-agents.js +487 -0
  432. package/dist/windows-agents.js.map +1 -0
  433. package/dist/windows-cli-api.d.ts +23 -0
  434. package/dist/windows-cli-api.js +158 -0
  435. package/dist/windows-cli-api.js.map +1 -0
  436. package/dist/windows-cli-providers.d.ts +115 -0
  437. package/dist/windows-cli-providers.js +429 -0
  438. package/dist/windows-cli-providers.js.map +1 -0
  439. package/dist/windows-cli-worker.d.ts +58 -0
  440. package/dist/windows-cli-worker.js +479 -0
  441. package/dist/windows-cli-worker.js.map +1 -0
  442. package/dist/windows-cli.d.ts +115 -0
  443. package/dist/windows-cli.js +402 -0
  444. package/dist/windows-cli.js.map +1 -0
  445. package/dist/windows-native-mcp.d.ts +19 -0
  446. package/dist/windows-native-mcp.js +143 -0
  447. package/dist/windows-native-mcp.js.map +1 -0
  448. package/dist/windows-pane-mcp.d.ts +26 -0
  449. package/dist/windows-pane-mcp.js +187 -0
  450. package/dist/windows-pane-mcp.js.map +1 -0
  451. package/package.json +9 -3
  452. package/templates/runtime-assets/conpty-LICENSE +21 -0
  453. package/templates/runtime-assets/conpty-NOTICE.md +503 -0
  454. package/templates/runtime-assets/local-pane-stage.ps1 +101 -0
  455. package/templates/runtime-assets/runtime-pane-job.ps1 +116 -0
  456. package/templates/runtime-assets/runtime-pane-private.ps1 +14 -0
  457. package/templates/runtime-assets/xterm-LICENSE +21 -0
package/dist/daemon.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { nativePaneToolRefusal } from "./native-pane-tools.js";
2
+ import { generalByName } from "./general-instance.js";
1
3
  import { dialogScreenText } from "./dialog-screen-text.js";
2
4
  import { readActiveKiroModel } from "./backend/kiro.js";
3
5
  import { dashboardAccessOf } from "./dashboard-permissions.js";
@@ -20,7 +22,7 @@ import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, Sessio
20
22
  import { mcpServerState } from "./mcp-liveness.js";
21
23
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
22
24
  import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
23
- import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
25
+ import { resolveTmuxLogicalSize } from "./tmux-manager.js";
24
26
  import { TranscriptMonitor } from "./transcript-monitor.js";
25
27
  import { createTranscriptSource } from "./transcript-sources.js";
26
28
  import { TranscriptTurnLedger } from "./transcript-turns.js";
@@ -48,6 +50,7 @@ import { HangDetector } from "./hang-detector.js";
48
50
  import { writeSecretFile } from "./secret-file.js";
49
51
  import { PaneWriteLock } from "./pane-write-lock.js";
50
52
  import { answerRemoteCard } from "./remote-card-answer.js";
53
+ import { tmuxPaneHost } from "./tmux-pane-host.js";
51
54
  import { buildFleetInstructions } from "./instructions.js";
52
55
  import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
53
56
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
@@ -57,6 +60,7 @@ import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT
57
60
  import { t } from "./locale.js";
58
61
  import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
59
62
  import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
63
+ import { reconcileNativePaneTarget } from "./native-pane-reconciliation.js";
60
64
  import { deliveryContentDigest } from "./delivery-outbox.js";
61
65
  import { consumedWatches } from "./delivery-consumed-watch.js";
62
66
  import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
@@ -1112,6 +1116,7 @@ export class Daemon extends EventEmitter {
1112
1116
  spawnGate;
1113
1117
  stormWindow;
1114
1118
  backendOutage;
1119
+ paneTransport;
1115
1120
  /** #1314: fleet-directory fallbacks already reported to the topic (once per daemon, per field and path). */
1116
1121
  promptFileFallbackNoticed = new Set();
1117
1122
  /** Identity of this live Daemon object; changes on object stop/restart. */
@@ -1943,9 +1948,87 @@ export class Daemon extends EventEmitter {
1943
1948
  this.errorRecoveryDeadlineAt = 0;
1944
1949
  this.activeErrorPatternKey = null;
1945
1950
  }
1951
+ /** Lazy default also preserves bare-prototype diagnostic/test construction. */
1952
+ get paneHost() { return this.paneTransport ?? tmuxPaneHost; }
1953
+ nativePaneLifecycle;
1954
+ nativeFleetBridge;
1955
+ nativeTranscriptSource;
1956
+ usesNativePane() { return !!this.nativePaneLifecycle; }
1957
+ logNativeInputRefusal(details) {
1958
+ this.logger.warn({ component: "runtime-pane-host", reason: "expected_seq_changed", ...details }, "Native pane input refused before write");
1959
+ }
1960
+ logNativeToolFailure(details) {
1961
+ const turn = this.turnReplyGuard.snapshot();
1962
+ this.logger.warn({ ...details, currentMessageId: turn?.target.messageId ?? null,
1963
+ guardGeneration: turn?.generation ?? null, guardPhase: turn?.phase ?? null }, "Native pane tool operation failed");
1964
+ }
1965
+ nativePaneCurrent() { return this.nativePaneLifecycle?.current() ?? false; }
1966
+ nativeEnterHold = null;
1967
+ /** Per-launch attention for a native delivery whose bounded input attempts were refused or uncertain, including non-outbox input. */
1968
+ nativeDeliveryHold() {
1969
+ const hold = this.nativeEnterHold;
1970
+ return hold && this.nativePaneLifecycle && !this.deliveryWritesStopping && hold.spawn === this.spawnGeneration && hold.fence === this.launchFenceEpoch
1971
+ ? { key: hold.key, since: hold.since } : null;
1972
+ }
1973
+ nativePaneHasExited() {
1974
+ const snapshot = this.nativePaneLifecycle?.snapshot?.();
1975
+ return snapshot?.state === "exited" && snapshot.instance === this.name && this.tmux?.getWindowId() === snapshot.paneId;
1976
+ }
1977
+ /** Positive native exit evidence only, fenced to this exact launch and Daemon owner. */
1978
+ nativePaneExitCurrent(snapshot, owner) {
1979
+ const actual = this.nativePaneLifecycle?.snapshot?.();
1980
+ return !!actual && actual.state === "exited" && snapshot.state === "exited"
1981
+ && sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.startupAborted
1982
+ && ["hostBootId", "paneId", "launchId", "agentId", "instance", "sessionId", "commandSequence", "exitCode"].every(key => actual[key] === snapshot[key])
1983
+ && this.tmux?.getWindowId() === snapshot.paneId;
1984
+ }
1985
+ async reconcileNativeDeliveries(outbox, current) {
1986
+ if (!this.nativePaneLifecycle)
1987
+ return false;
1988
+ return reconcileNativePaneTarget(outbox, this.name, this.nativePaneLifecycle, current);
1989
+ }
1990
+ /** Same-process transport for the owned native daemon; never exposed as a runner RPC. */
1991
+ setNativeFleetBridge(bridge) {
1992
+ if (!this.nativePaneLifecycle || this.nativeFleetBridge?.current())
1993
+ throw Error("Native Fleet bridge requires one bound pane owner");
1994
+ this.nativeFleetBridge = bridge;
1995
+ }
1996
+ /** Fleet admission and generation checks happen before this synchronous handoff. */
1997
+ acceptNativeFleetMessage(message) {
1998
+ if (!this.nativePaneLifecycle || !this.nativeFleetBridge?.current() || this.deliveryWritesStopping || this.runtimeMonitorsFrozen)
1999
+ return false;
2000
+ if (typeof message.type === "string" && FLEET_RESPONSE_TYPES.has(message.type)) {
2001
+ this.routeFleetResponse(message);
2002
+ return true;
2003
+ }
2004
+ const bridge = this.nativeFleetBridge;
2005
+ const current = () => this.nativeFleetBridge === bridge && bridge.current() && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen;
2006
+ return this.handleFleetMessage(message, reply => { if (current())
2007
+ this.broadcastFleetMessage(reply); }, current);
2008
+ }
2009
+ broadcastFleetMessage(message) {
2010
+ if (this.nativePaneLifecycle) {
2011
+ if (this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen && !this.deliveryWritesStopping)
2012
+ this.nativeFleetBridge.send(message);
2013
+ }
2014
+ else
2015
+ this.ipcServer?.broadcast(message);
2016
+ }
2017
+ fleetTransportAvailable() {
2018
+ return this.nativePaneLifecycle ? !!this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen : !!this.ipcServer;
2019
+ }
2020
+ /** Internal lifecycle injection, before start. Native paths and provider files stay on their runner. */
2021
+ setNativePaneLifecycle(native) {
2022
+ if (this.tmux || this.ipcServer || this.nativePaneLifecycle)
2023
+ throw Error("Native pane adapter must be bound before daemon startup");
2024
+ this.nativePaneLifecycle = native;
2025
+ this.backend = native.backend;
2026
+ this.paneTransport = native.host;
2027
+ this.controlClient = native.observation;
2028
+ }
1946
2029
  constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity, spawnGate, stormWindow,
1947
2030
  /** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
1948
- backendOutage) {
2031
+ backendOutage, paneTransport) {
1949
2032
  super();
1950
2033
  this.name = name;
1951
2034
  this.config = config;
@@ -1957,6 +2040,7 @@ export class Daemon extends EventEmitter {
1957
2040
  this.spawnGate = spawnGate;
1958
2041
  this.stormWindow = stormWindow;
1959
2042
  this.backendOutage = backendOutage;
2043
+ this.paneTransport = paneTransport;
1960
2044
  // The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
1961
2045
  // sharing that object would activate cold changes before the normal restart/apply boundary.
1962
2046
  this.config = structuredClone(config);
@@ -1979,7 +2063,8 @@ export class Daemon extends EventEmitter {
1979
2063
  this.messageBus.setLogger(this.logger);
1980
2064
  // General is the dispatcher — it must stay warm to route messages, so it is
1981
2065
  // never auto-paused regardless of auto_pause_after.
1982
- const isGeneral = config.general_topic === true || name === "general";
2066
+ // #3035 review: the one rule (an agent named general unticked — general_topic: false — is a worker).
2067
+ const isGeneral = config.general_topic === true || generalByName(name, config);
1983
2068
  const autoPauseMinutes = isGeneral ? 0 : (typeof config.auto_pause_after === "number" ? config.auto_pause_after : 0); // default: disabled
1984
2069
  this.autoPauseController = new AutoPauseController(Math.max(0, autoPauseMinutes) * 60_000, readLastInboundAt(instanceDir) ?? Date.now());
1985
2070
  // #1209: every real completion clears the in-flight marker, so a
@@ -2038,12 +2123,19 @@ export class Daemon extends EventEmitter {
2038
2123
  return outcome === "begun";
2039
2124
  }
2040
2125
  async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
2041
- let checkpoint = null;
2042
- try {
2043
- checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2126
+ let checkpoint = null, runtimeTranscript = null;
2127
+ if (this.nativePaneLifecycle) {
2128
+ runtimeTranscript = await this.transcriptMonitor?.reconciliationRuntimeCheckpoint() ?? null;
2129
+ if (!runtimeTranscript || !this.nativePaneLifecycle.current(runtimeTranscript))
2130
+ throw Error("Native transcript checkpoint is not current; no delivery was written");
2044
2131
  }
2045
- catch (err) {
2046
- this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
2132
+ else {
2133
+ try {
2134
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2135
+ }
2136
+ catch (err) {
2137
+ this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
2138
+ }
2047
2139
  }
2048
2140
  const submissionMode = rawPaste ? "raw_paste" : steer
2049
2141
  ? "steer"
@@ -2059,8 +2151,9 @@ export class Daemon extends EventEmitter {
2059
2151
  backendVersion,
2060
2152
  windowId: windowId ?? this.tmux?.getWindowId() ?? null,
2061
2153
  transcriptPath: checkpoint?.path ?? null,
2062
- transcriptOffset: checkpoint?.offset ?? null,
2063
- transcriptSessionId: checkpoint?.sessionId ?? null,
2154
+ transcriptOffset: runtimeTranscript?.cursor.offset ?? checkpoint?.offset ?? null,
2155
+ transcriptSessionId: runtimeTranscript?.cursor.sessionId ?? checkpoint?.sessionId ?? null,
2156
+ ...(runtimeTranscript ? { runtimeTranscript } : {}),
2064
2157
  submissionMode,
2065
2158
  queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
2066
2159
  };
@@ -2103,17 +2196,25 @@ export class Daemon extends EventEmitter {
2103
2196
  const outbox = this.deliveryOutbox;
2104
2197
  if (!checkpoint || !outbox?.markConsumed)
2105
2198
  return;
2199
+ if (this.nativePaneLifecycle && !checkpoint.runtime)
2200
+ return;
2106
2201
  if (verdict.submissionMode !== "steer" && verdict.submissionMode !== "native_queue_handoff")
2107
2202
  return;
2108
2203
  if (!["claude-code", "codex"].includes(checkpoint.backend))
2109
2204
  return;
2110
2205
  const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, boot = this.bootId;
2206
+ let location;
2207
+ try {
2208
+ location = checkpoint.runtime ? { reader: this.nativeTranscriptReader(checkpoint.runtime, delivery.deliveryId) } : { path: checkpoint.path, offset: checkpoint.offset };
2209
+ }
2210
+ catch {
2211
+ return;
2212
+ } // The owner changed; an unavailable consumed watch proves nothing.
2111
2213
  consumedWatches.start({
2112
2214
  deliveryId: delivery.deliveryId,
2113
2215
  attemptNo: delivery.attemptNo,
2114
2216
  backend: checkpoint.backend,
2115
- path: checkpoint.path,
2116
- offset: checkpoint.offset,
2217
+ ...location,
2117
2218
  current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch,
2118
2219
  consumed: (via, evidence) => {
2119
2220
  try {
@@ -2194,24 +2295,34 @@ export class Daemon extends EventEmitter {
2194
2295
  const spawn = this.spawnGeneration;
2195
2296
  const fence = this.launchFenceEpoch;
2196
2297
  let last = "unavailable";
2197
- let outcome = "delivered";
2198
- let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
2298
+ const native = !!checkpoint.runtime || !!this.nativePaneLifecycle;
2299
+ let outcome = native ? "uncertain" : "delivered";
2300
+ let evidence = native ? "runtime-transcript-unconfirmed" : OUTPUT_EDGE_ONLY_EVIDENCE;
2199
2301
  // #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
2200
2302
  // in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
2201
2303
  const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
2202
2304
  let found = false;
2203
2305
  try {
2306
+ if (this.nativePaneLifecycle && !checkpoint.runtime)
2307
+ throw Error("Native proof cannot read a coordinator-local transcript path");
2308
+ const nativeReader = checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : null;
2204
2309
  for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
2205
2310
  if (this.deliveryWritesStopping)
2206
2311
  return; // reconciliation owns the row now
2207
- const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
2312
+ const look = nativeReader ? await nativeReader.look() : await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
2313
+ if (this.deliveryWritesStopping)
2314
+ return;
2315
+ if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
2316
+ last = "unavailable";
2317
+ break;
2318
+ }
2208
2319
  if (look === "user" || look === "absorbed" || look === "queued") {
2209
2320
  found = true;
2210
2321
  outcome = "delivered";
2211
2322
  evidence = transcriptMarkerEvidence(look, !!rawPaste);
2212
2323
  break;
2213
2324
  }
2214
- last = look;
2325
+ last = look === "pending" ? "unavailable" : look;
2215
2326
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
2216
2327
  last = "unavailable";
2217
2328
  break;
@@ -2223,13 +2334,13 @@ export class Daemon extends EventEmitter {
2223
2334
  // `uncertain`: re-check the same fixed epochs after the last await, before settling (#1207 P2).
2224
2335
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch)
2225
2336
  last = "unavailable";
2226
- if (last === "no-match" && evidence === OUTPUT_EDGE_ONLY_EVIDENCE) {
2337
+ if (last === "no-match" && !found) {
2227
2338
  outcome = "uncertain";
2228
2339
  evidence = "unverifiable-no-transcript-marker";
2229
2340
  }
2230
2341
  }
2231
2342
  catch (err) {
2232
- this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
2343
+ this.logger.debug({ err }, "Transcript proof unavailable — preserving the conservative outcome");
2233
2344
  }
2234
2345
  if (rawPaste && !found) {
2235
2346
  outcome = "uncertain";
@@ -2416,128 +2527,130 @@ export class Daemon extends EventEmitter {
2416
2527
  }
2417
2528
  }
2418
2529
  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);
2530
+ if (!this.nativePaneLifecycle) {
2531
+ // 1. IPC server — bridge between MCP server (Claude's child) and daemon
2532
+ const sockPath = join(this.instanceDir, "channel.sock");
2533
+ this.ipcServer = new IpcServer(sockPath, this.logger);
2534
+ // Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
2535
+ // Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
2536
+ let ipcListening = false;
2537
+ this.ipcServer.on("error", (err) => {
2538
+ if (!ipcListening)
2539
+ return; // startup errors handled by listen() rejection
2540
+ this.logger.error({ err, name: this.name }, "IPC server error");
2541
+ this.emit("error", err);
2542
+ });
2543
+ await this.ipcServer.listen();
2544
+ this.startupAdmission?.();
2545
+ ipcListening = true;
2546
+ // Permanent IPC dispatcher: routes responses to pending requests by type+id key
2547
+ this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
2548
+ // IPC message relay: when daemon wants to push a channel message to Claude,
2549
+ // it broadcasts to all IPC clients (the MCP server is one of them).
2550
+ // When MCP server sends a tool_call, daemon handles it via the messageBus.
2551
+ this.ipcServer.on("message", (msg, socket) => {
2552
+ if (msg.type === "tool_call") {
2553
+ // MCP server forwarding a Claude tool call (reply, react, edit, download)
2554
+ this.handleToolCall(msg, socket);
2555
+ }
2556
+ else if (msg.type === "mcp_ready") {
2557
+ this.handleMcpReady(msg, socket);
2558
+ }
2559
+ else if (msg.type === "pong") {
2560
+ this.handleMcpPong(msg, socket);
2561
+ }
2562
+ else if (msg.type === "query_sessions") {
2563
+ // Fleet manager asks for all registered session names (catches sessions
2564
+ // that sent mcp_ready before fleet manager connected).
2565
+ const sessions = [];
2566
+ for (const [s, sessionName] of this.socketSessionNames) {
2567
+ if (!s.destroyed && sessionName !== this.name) {
2568
+ // Individual mcp_ready for initial registration path
2569
+ this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
2570
+ sessions.push(sessionName);
2571
+ }
2459
2572
  }
2573
+ // Batch response for prune path
2574
+ this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
2460
2575
  }
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();
2576
+ else
2577
+ this.handleFleetMessage(msg, reply => { this.ipcServer?.send(socket, reply); });
2578
+ });
2579
+ }
2580
+ let nativeDisposition;
2581
+ if (this.nativePaneLifecycle) {
2582
+ this.beginSpawn();
2583
+ try {
2584
+ const acquired = await this.nativePaneLifecycle.acquire(() => {
2516
2585
  this.startupAdmission?.();
2517
- await oldTmux.killWindow();
2586
+ if (this.startupAborted)
2587
+ throw Error("Native launch was superseded");
2588
+ });
2589
+ this.tmux = acquired.pane;
2590
+ this.nativeTranscriptSource = acquired.source;
2591
+ nativeDisposition = acquired.disposition;
2592
+ this.lastSpawnAt = Date.now();
2593
+ this.setProcessStatus("running");
2594
+ this.bindInstanceStateOutputListener(this.tmux.getWindowId());
2595
+ this.saveSessionId();
2596
+ // New/resumed native processes can still be painting their first frame. Keep deliveries behind the same
2597
+ // bounded dialog/readiness scan as local startup. An already-running reattachment may be busy: do not
2598
+ // wait for that work to finish or inject a startup key into it.
2599
+ if (acquired.disposition !== "reattached") {
2600
+ if (!await this.finishStartupScan(120_000))
2601
+ throw Error("Native CLI exited during startup; retained session was not replaced");
2518
2602
  this.startupAdmission?.();
2519
- this.logger.info({ savedId }, "Killed old tmux window for fresh start");
2603
+ if (this.startupAborted || this.deliveryWritesStopping || !this.nativePaneLifecycle.current())
2604
+ throw Error("Native launch was superseded");
2520
2605
  }
2521
2606
  }
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?.();
2607
+ finally {
2608
+ this.endSpawn();
2609
+ }
2529
2610
  }
2530
2611
  else {
2531
- // Clean up stale snapshot file — resume restored full context, snapshot not needed
2532
- try {
2533
- unlinkSync(join(this.instanceDir, "rotation-state.json"));
2612
+ // 2. Tmux — ensure session, create window if not alive
2613
+ await this.paneHost.ensureSession(this.tmuxSessionName);
2614
+ this.startupAdmission?.();
2615
+ this.tmux = this.paneHost.bind(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
2616
+ // Strategy A: always start fresh Claude window (MCP server has no reconnection)
2617
+ // Kill any existing window from previous run
2618
+ const windowIdFile = join(this.instanceDir, "window-id");
2619
+ if (existsSync(windowIdFile)) {
2620
+ const savedId = readFileSync(windowIdFile, "utf-8").trim();
2621
+ if (savedId) {
2622
+ const oldTmux = this.paneHost.bind(this.tmuxSessionName, savedId);
2623
+ if (await oldTmux.isWindowAlive()) {
2624
+ this.startupAdmission?.();
2625
+ await this.checkpointSessionId();
2626
+ this.startupAdmission?.();
2627
+ await oldTmux.killWindow();
2628
+ this.startupAdmission?.();
2629
+ this.logger.info({ savedId }, "Killed old tmux window for fresh start");
2630
+ }
2631
+ }
2632
+ }
2633
+ const resumed = await this.spawnClaudeWindow();
2634
+ this.startupAdmission?.();
2635
+ this.isNewSession = !resumed;
2636
+ if (!resumed) {
2637
+ await this.injectSnapshotMessage(this.startupAdmission);
2638
+ this.startupAdmission?.();
2639
+ }
2640
+ else {
2641
+ // Clean up stale snapshot file — resume restored full context, snapshot not needed
2642
+ try {
2643
+ unlinkSync(join(this.instanceDir, "rotation-state.json"));
2644
+ }
2645
+ catch { /* may not exist */ }
2534
2646
  }
2535
- catch { /* may not exist */ }
2536
2647
  }
2648
+ if (nativeDisposition)
2649
+ this.isNewSession = nativeDisposition === "fresh";
2537
2650
  // #1209: conditional continuation for the turn interrupted by the last
2538
2651
  // restart, if the gates allow. Detached; the gate
2539
2652
  // consumes its one-shot marker first and never throws into boot.
2540
- const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
2653
+ const legacyResume = this.nativePaneLifecycle ? Promise.resolve("unknown") : this.maybeResumeInterruptedTurn().catch(() => "unknown");
2541
2654
  const recoveryOwner = JSON.stringify(this.interactionOwner());
2542
2655
  void Promise.allSettled([legacyResume]).then(() => {
2543
2656
  if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
@@ -2556,7 +2669,7 @@ export class Daemon extends EventEmitter {
2556
2669
  // (statusline transcript); codex/kiro/opencode read their CLI's own
2557
2670
  // conversation store via a pluggable source. Backends with no known
2558
2671
  // 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));
2672
+ this.transcriptMonitor = this.buildTranscriptMonitor(this.nativeTranscriptSource ?? createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2560
2673
  const transcriptMonitor = this.transcriptMonitor;
2561
2674
  const transcriptFence = this.launchFenceEpoch;
2562
2675
  // Baseline in the isolate before this daemon accepts new work. A
@@ -2612,13 +2725,15 @@ export class Daemon extends EventEmitter {
2612
2725
  if (hangConfig?.enabled !== false) {
2613
2726
  this.hangDetector = new HangDetector();
2614
2727
  }
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
- });
2728
+ if (!this.nativePaneLifecycle) {
2729
+ // 8. Context guardian
2730
+ const statusFile = join(this.instanceDir, "statusline.json");
2731
+ this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
2732
+ this.guardian.startWatching();
2733
+ this.guardian.on("status_update", () => {
2734
+ this.saveSessionId();
2735
+ });
2736
+ }
2622
2737
  // Context rotation removed: all CLI backends have built-in auto-compact.
2623
2738
  // Crash recovery (health check + respawn with snapshot) is retained below.
2624
2739
  }
@@ -2669,7 +2784,7 @@ export class Daemon extends EventEmitter {
2669
2784
  this.socketPids.delete(socket);
2670
2785
  // Notify fleet manager so it can clean up sessionRegistry
2671
2786
  if (sessionName !== this.name) {
2672
- this.ipcServer?.broadcast({ type: "session_disconnected", sessionName });
2787
+ this.broadcastFleetMessage({ type: "session_disconnected", sessionName });
2673
2788
  }
2674
2789
  });
2675
2790
  }
@@ -2685,11 +2800,16 @@ export class Daemon extends EventEmitter {
2685
2800
  // sub-agent's server may have left a dead pid in the slot).
2686
2801
  this.noteMcpProofOfLife("mcp_ready", typeof msg.pid === "number" ? msg.pid : undefined);
2687
2802
  }
2688
- this.ipcServer?.broadcast({ type: "mcp_ready", sessionName });
2803
+ this.broadcastFleetMessage({ type: "mcp_ready", sessionName });
2689
2804
  }
2690
2805
  checkMcpServerAlive() {
2691
2806
  if (this.isPaused)
2692
2807
  return;
2808
+ if (this.nativePaneLifecycle) {
2809
+ if (this.nativePaneLifecycle.mcpProof() === "alive")
2810
+ this.noteMcpProofOfLife("connection");
2811
+ return; // No native PID/socket guessing or restart from an unknown helper observation.
2812
+ }
2693
2813
  const status = mcpServerState(this.instanceDir);
2694
2814
  if (status.state === "alive") {
2695
2815
  this.mcpServedGeneration = this.spawnGeneration;
@@ -2773,6 +2893,8 @@ export class Daemon extends EventEmitter {
2773
2893
  * server from a third-party one, and failed the whole launch for either.
2774
2894
  */
2775
2895
  checkFleetMcpNeverConnected() {
2896
+ if (this.nativePaneLifecycle)
2897
+ return; // Native helper presence owns this observation.
2776
2898
  if (this.config.agent_mode === "cli")
2777
2899
  return; // no MCP server by design
2778
2900
  if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
@@ -2983,6 +3105,8 @@ export class Daemon extends EventEmitter {
2983
3105
  * proof that tools are being served, whatever channel.mcp.pid says.
2984
3106
  */
2985
3107
  mcpServerAlive() {
3108
+ if (this.nativePaneLifecycle)
3109
+ return this.nativePaneLifecycle.mcpProof() === "alive" ? { alive: true, source: "connection" } : { alive: false, source: "none" };
2986
3110
  const status = mcpServerState(this.instanceDir);
2987
3111
  if (status.state === "alive")
2988
3112
  return { alive: true, source: "pid", pid: status.pid };
@@ -3047,7 +3171,7 @@ export class Daemon extends EventEmitter {
3047
3171
  this.mcpDeathNotifiedForPid = null;
3048
3172
  this.mcpDeathDeferredForPid = null;
3049
3173
  this.clearMcpRestartRequest();
3050
- if (pid && mcpServerState(this.instanceDir).state !== "alive")
3174
+ if (!this.nativePaneLifecycle && pid && mcpServerState(this.instanceDir).state !== "alive")
3051
3175
  this.repairMcpPidSlot(pid, source);
3052
3176
  }
3053
3177
  emitMcpRecovered(source, pid) {
@@ -3094,6 +3218,26 @@ export class Daemon extends EventEmitter {
3094
3218
  // ended this instance's health loop because scheduleNext() sat after the
3095
3219
  // throwing code. Catch, log, and keep checking.
3096
3220
  try {
3221
+ if (this.nativePaneLifecycle) {
3222
+ if (this.healthCheckPaused) {
3223
+ scheduleNext();
3224
+ return;
3225
+ }
3226
+ const native = this.nativePaneLifecycle, pane = this.tmux;
3227
+ const status = await pane?.getPaneStatus();
3228
+ if (this.runtimeMonitorsFrozen || native !== this.nativePaneLifecycle || pane !== this.tmux)
3229
+ return;
3230
+ const snapshot = native.snapshot?.();
3231
+ if (status?.alive === false && snapshot?.state === "exited") {
3232
+ this.healthCheckPaused = true;
3233
+ this.setProcessStatus(snapshot.exitCode === 0 ? "stopped" : "crashed");
3234
+ this.emit("native_pane_exited", { name: this.name, snapshot, owner: this.interactionOwner() });
3235
+ }
3236
+ else if (status?.alive)
3237
+ this.checkMcpServerAlive();
3238
+ scheduleNext();
3239
+ return;
3240
+ }
3097
3241
  // Instance directory removed externally (e.g. `rm -rf ~/.agend/instances/<name>`).
3098
3242
  // Stop the loop permanently — otherwise every tick triggers a respawn, whose
3099
3243
  // writeRotationSnapshot fails with ENOENT and gets caught as "Failed to respawn",
@@ -3205,13 +3349,13 @@ export class Daemon extends EventEmitter {
3205
3349
  let inWindowLossBurst = false;
3206
3350
  let nullReason;
3207
3351
  if (!paneStatus) {
3208
- const serverAlive = await TmuxManager.sessionExists(this.tmuxSessionName);
3352
+ const serverAlive = await this.paneHost.sessionExists(this.tmuxSessionName);
3209
3353
  if (discardStaleHealth())
3210
3354
  return;
3211
3355
  // A server may have already restarted before this daemon's health
3212
3356
  // tick. The new PID is the durable generation boundary: treat it as
3213
3357
  // a fleet storm even though `has-session` is true again.
3214
- const serverPid = serverAlive ? await TmuxManager.getServerPid(this.tmuxSessionName) : null;
3358
+ const serverPid = serverAlive ? await this.paneHost.getServerPid(this.tmuxSessionName) : null;
3215
3359
  if (discardStaleHealth())
3216
3360
  return;
3217
3361
  const generationChanged = serverAlive && this.stormWindow?.observeServerAlive(serverPid) === true;
@@ -3240,7 +3384,7 @@ export class Daemon extends EventEmitter {
3240
3384
  // the window truly no longer exists vs a transient query glitch.
3241
3385
  nullReason = "no_window";
3242
3386
  try {
3243
- const windows = await TmuxManager.listWindowsStrict(this.tmuxSessionName, this.controlClient);
3387
+ const windows = await this.paneHost.listWindowsStrict(this.tmuxSessionName, this.controlClient);
3244
3388
  if (discardStaleHealth())
3245
3389
  return;
3246
3390
  this.windowQueryFailureTicks = 0;
@@ -3485,10 +3629,10 @@ export class Daemon extends EventEmitter {
3485
3629
  // Wrapped in try-catch: if tmux server is dead, listWindows throws —
3486
3630
  // must not block spawnClaudeWindow (which calls ensureSession).
3487
3631
  try {
3488
- const windows = await TmuxManager.listWindows(this.tmuxSessionName);
3632
+ const windows = await this.paneHost.listWindows(this.tmuxSessionName);
3489
3633
  for (const w of windows) {
3490
3634
  if (w.name === this.name) {
3491
- const tm = new TmuxManager(this.tmuxSessionName, w.id);
3635
+ const tm = this.paneHost.bind(this.tmuxSessionName, w.id);
3492
3636
  await tm.killWindow();
3493
3637
  }
3494
3638
  }
@@ -3631,7 +3775,7 @@ export class Daemon extends EventEmitter {
3631
3775
  // Force the next ready capture to emit a fresh transition after respawn.
3632
3776
  this.instanceState = "working";
3633
3777
  }
3634
- this.ipcServer?.broadcast({
3778
+ this.broadcastFleetMessage({
3635
3779
  type: "instance_process_state",
3636
3780
  instanceName: this.name,
3637
3781
  status,
@@ -3827,12 +3971,15 @@ export class Daemon extends EventEmitter {
3827
3971
  for (const key of dialog.keys) {
3828
3972
  if (stale())
3829
3973
  return;
3974
+ const keyScreen = this.nativePaneLifecycle ? await pollTmux.capturePane() : currentPane;
3975
+ if (stale() || !Daemon.dialogMatches(dialog, keyScreen))
3976
+ return;
3830
3977
  let sent = false;
3831
3978
  if (SPECIAL_KEYS.has(key)) {
3832
- sent = await pollTmux.sendSpecialKey(key);
3979
+ sent = await (this.nativePaneLifecycle ? pollTmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : pollTmux.sendSpecialKey(key));
3833
3980
  }
3834
3981
  else if (dialog.literalKeys) {
3835
- sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
3982
+ sent = await (this.nativePaneLifecycle ? pollTmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : pollTmux.sendKeys(key)); // typed, no Enter (#2872)
3836
3983
  }
3837
3984
  else {
3838
3985
  sent = await pollTmux.pasteText(key, this.systemPasteOptions());
@@ -4195,7 +4342,20 @@ export class Daemon extends EventEmitter {
4195
4342
  */
4196
4343
  async sendEscape() {
4197
4344
  const cancelKey = this.backend?.getCancelKey() ?? "Escape";
4198
- await this.tmux?.sendSpecialKey(cancelKey);
4345
+ const tmux = this.tmux;
4346
+ if (!tmux)
4347
+ return;
4348
+ if (this.nativePaneLifecycle) {
4349
+ // A native pane's composer proof cannot see a permission dialog, so an unproved key is withheld there. The cancel
4350
+ // is the person's own interrupt, keyed on the screen read just before (as the dialog answer path is): what Esc
4351
+ // does on Linux's tmux pane, on the same screen, with the same key.
4352
+ const screen = await tmux.capturePane().catch(() => null);
4353
+ if (screen === null)
4354
+ return;
4355
+ await tmux.sendSpecialKey(cancelKey, undefined, { kind: "dialog", screen });
4356
+ return;
4357
+ }
4358
+ await tmux.sendSpecialKey(cancelKey);
4199
4359
  }
4200
4360
  /**
4201
4361
  * Drop every not-yet-started pane delivery which predates a user cancel.
@@ -4253,7 +4413,7 @@ export class Daemon extends EventEmitter {
4253
4413
  return;
4254
4414
  const outcome = follower.look(pane);
4255
4415
  if (outcome) {
4256
- this.ipcServer?.broadcast({ type: "compact_pane_result", id, owner, outcome });
4416
+ this.broadcastFleetMessage({ type: "compact_pane_result", id, owner, outcome });
4257
4417
  return;
4258
4418
  }
4259
4419
  }
@@ -4273,7 +4433,7 @@ export class Daemon extends EventEmitter {
4273
4433
  const reportSwitch = (result) => {
4274
4434
  if (!ownSwitch)
4275
4435
  return;
4276
- this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
4436
+ this.broadcastFleetMessage({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
4277
4437
  };
4278
4438
  // Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
4279
4439
  // (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
@@ -4288,7 +4448,7 @@ export class Daemon extends EventEmitter {
4288
4448
  if (!notifyWritten || told)
4289
4449
  return;
4290
4450
  told = true;
4291
- this.ipcServer?.broadcast({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
4451
+ this.broadcastFleetMessage({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
4292
4452
  ...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
4293
4453
  if (written && paneFollower)
4294
4454
  void this.followCompactPane(notifyWritten, owner, paneFollower);
@@ -4450,10 +4610,11 @@ export class Daemon extends EventEmitter {
4450
4610
  const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
4451
4611
  if (this.tmux !== tmux)
4452
4612
  return false;
4453
- const now = claudeSwitchDialog(await read());
4613
+ const screen = await read();
4614
+ const now = claudeSwitchDialog(screen);
4454
4615
  if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
4455
4616
  return false;
4456
- return tmux.sendSpecialKey(key);
4617
+ return this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen }) : tmux.sendSpecialKey(key);
4457
4618
  });
4458
4619
  const gone = async () => {
4459
4620
  const until = performance.now() + OWN_SWITCH_SETTLE_MS;
@@ -4542,9 +4703,12 @@ export class Daemon extends EventEmitter {
4542
4703
  return false;
4543
4704
  const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
4544
4705
  for (const key of dialog.keys) {
4706
+ const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : currentPane;
4707
+ if (!matches(keyScreen))
4708
+ return false;
4545
4709
  const ok = specialKeys.has(key)
4546
- ? await this.tmux.sendSpecialKey(key)
4547
- : await this.tmux.sendKeys(key);
4710
+ ? await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key))
4711
+ : await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key));
4548
4712
  if (!ok)
4549
4713
  return false;
4550
4714
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -4787,7 +4951,7 @@ export class Daemon extends EventEmitter {
4787
4951
  await this.tmux.sendSpecialKey("Escape").catch(() => false);
4788
4952
  return true;
4789
4953
  }
4790
- async stop() {
4954
+ async stop(options = {}) {
4791
4955
  this.publishWorkingState("before_stop");
4792
4956
  this.logger.info("Stopping daemon instance");
4793
4957
  this.clearQuitRelaunchWatch();
@@ -4809,8 +4973,18 @@ export class Daemon extends EventEmitter {
4809
4973
  }
4810
4974
  if (this.adapter)
4811
4975
  await this.adapter.stop();
4976
+ if (this.nativePaneLifecycle) {
4977
+ this.healthCheckPaused = true;
4978
+ this.saveSessionId();
4979
+ await this.nativePaneLifecycle.release(options.nativePane ?? "detach");
4980
+ try {
4981
+ unlinkSync(join(this.instanceDir, "daemon.pid"));
4982
+ }
4983
+ catch { /* metadata only */ }
4984
+ return;
4985
+ }
4812
4986
  // Notify MCP servers of graceful shutdown (prevents reconnect attempts)
4813
- this.ipcServer?.broadcast({ type: "shutdown" });
4987
+ this.broadcastFleetMessage({ type: "shutdown" });
4814
4988
  // Quit CLI FIRST — this kills MCP server child processes cleanly.
4815
4989
  // IPC must stay open during quit so MCP servers receive the shutdown message.
4816
4990
  if (this.tmux) {
@@ -4956,6 +5130,24 @@ export class Daemon extends EventEmitter {
4956
5130
  freshChoice: this.recoveryFreshChoice || this.isNewSession && !this.bootSkippedResume, crashLoopFresh: this.recoveryCrashLoopFresh,
4957
5131
  cancelled: this.recoveryCancelled, background: this.workingBackgroundObservation };
4958
5132
  }
5133
+ /** Observe this host General before its approved switch. No keys, process signals or provider writes. */
5134
+ async generalHostOwner() {
5135
+ const pane = this.tmux, owner = JSON.stringify(this.interactionOwner());
5136
+ if (!this.config.general_topic || this.nativePaneLifecycle || !pane || !/^@\d+$/.test(pane.getWindowId()))
5137
+ throw Error("General host owner is unavailable");
5138
+ const status = await pane.getPaneStatus();
5139
+ if (!status?.alive || !status.pid || status.pid <= 1)
5140
+ throw Error("General host process cannot be proven");
5141
+ const checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint();
5142
+ if (this.tmux !== pane || owner !== JSON.stringify(this.interactionOwner()))
5143
+ throw Error("General host owner changed during observation");
5144
+ const backend = this.config.backend ?? "claude-code";
5145
+ let sessionId = this.backend?.getSessionId() ?? undefined;
5146
+ if (backend === "codex" && checkpoint)
5147
+ sessionId = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(checkpoint.path)?.[1];
5148
+ return { kind: "tmux", windowId: pane.getWindowId(), pid: status.pid,
5149
+ ...(typeof sessionId === "string" && sessionId.length > 0 && sessionId.length <= 256 && !/[\p{Cc}\p{Cf}]/u.test(sessionId) ? { pin: { backend, sessionId } } : {}) };
5150
+ }
4959
5151
  /** Local system work: no user turn, channel reply target, wake or replay of an original request. */
4960
5152
  async deliverWorkingRecovery(content, delivery, admitted) {
4961
5153
  const tmux = this.tmux, owner = JSON.stringify(this.interactionOwner()), epoch = this.deliveryEpoch;
@@ -4976,7 +5168,11 @@ export class Daemon extends EventEmitter {
4976
5168
  || !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
4977
5169
  || this.backend?.backgroundWorkRunning?.(pane) === true)
4978
5170
  return;
4979
- if (await tmux.getPaneInputMode?.() !== "raw" || !current())
5171
+ if (this.nativePaneLifecycle ? !this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"
5172
+ || !!this.backend?.isDeliveryInputReadyPane && !this.backend.isDeliveryInputReadyPane(pane)
5173
+ : await tmux.getPaneInputMode?.() !== "raw")
5174
+ return;
5175
+ if (!current())
4980
5176
  return;
4981
5177
  const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
4982
5178
  if (!current())
@@ -5067,7 +5263,7 @@ export class Daemon extends EventEmitter {
5067
5263
  this.logger.debug({ err }, "Interaction listener failed");
5068
5264
  }
5069
5265
  try {
5070
- this.ipcServer?.broadcast({ type: "instance_interaction", instanceName: this.name, interaction });
5266
+ this.broadcastFleetMessage({ type: "instance_interaction", instanceName: this.name, interaction });
5071
5267
  }
5072
5268
  catch (err) {
5073
5269
  this.logger.debug({ err }, "Interaction broadcast failed");
@@ -5155,6 +5351,56 @@ export class Daemon extends EventEmitter {
5155
5351
  }
5156
5352
  return true;
5157
5353
  }
5354
+ /** Shared Fleet inputs: socket and native transports retain the same wake, epoch and raw-paste semantics. */
5355
+ handleFleetMessage(msg, send, current = () => true) {
5356
+ if (msg.type === "query_sessions") {
5357
+ send({ type: "query_sessions_response", sessions: [] });
5358
+ }
5359
+ else if (msg.type === "fleet_inbound") {
5360
+ const meta = msg.meta;
5361
+ const targetSession = msg.targetSession;
5362
+ const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
5363
+ void this.wake().then(() => {
5364
+ if (!current()) {
5365
+ this.deferDurableDelivery(meta, "native channel owner changed before pane submission");
5366
+ return;
5367
+ }
5368
+ this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
5369
+ }).catch(err => {
5370
+ this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
5371
+ this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
5372
+ });
5373
+ }
5374
+ else if (msg.type === "raw_paste") {
5375
+ this.handleRawPasteIpcMessage(msg);
5376
+ }
5377
+ else if (msg.type === "config_update") {
5378
+ this.applyConfigUpdate(msg.config);
5379
+ }
5380
+ else if (msg.type === "steer") {
5381
+ const meta = (msg.meta ?? {});
5382
+ this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
5383
+ }
5384
+ else if (msg.type === "btw") {
5385
+ const meta = (msg.meta ?? {});
5386
+ this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
5387
+ }
5388
+ else if (msg.type === "fleet_schedule_trigger") {
5389
+ const payload = msg.payload;
5390
+ const meta = msg.meta;
5391
+ const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
5392
+ void this.wake().then(() => { if (current())
5393
+ this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch); }).catch(err => {
5394
+ this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
5395
+ });
5396
+ }
5397
+ else if (msg.type === "query_instance_state") {
5398
+ void this.respondToInstanceStateQuery(msg, undefined, send);
5399
+ }
5400
+ else
5401
+ return false;
5402
+ return true;
5403
+ }
5158
5404
  /**
5159
5405
  * Answer a FleetManager state query. Most callers only need the cached state,
5160
5406
  * but lifecycle decisions such as post-reply Cancel retirement need an
@@ -5162,7 +5408,7 @@ export class Daemon extends EventEmitter {
5162
5408
  * the last transition can stay "working" even though the CLI has since
5163
5409
  * settled without emitting another control-mode output record.
5164
5410
  */
5165
- async respondToInstanceStateQuery(msg, socket) {
5411
+ async respondToInstanceStateQuery(msg, socket, send) {
5166
5412
  try {
5167
5413
  if (msg.deliveryIdle === true) {
5168
5414
  await this.captureAndEvaluateInstanceState("delivery_idle_gate");
@@ -5171,7 +5417,7 @@ export class Daemon extends EventEmitter {
5171
5417
  await this.captureAndEvaluateInstanceState("state_query");
5172
5418
  }
5173
5419
  const snapshot = this.getInstanceStateSnapshot();
5174
- this.ipcServer?.send(socket, {
5420
+ const reply = {
5175
5421
  type: "instance_state_response",
5176
5422
  requestId: msg.requestId,
5177
5423
  instanceName: this.name,
@@ -5179,7 +5425,11 @@ export class Daemon extends EventEmitter {
5179
5425
  state: this.isPaused ? "paused" : snapshot.state,
5180
5426
  processStatus: this.processStatus,
5181
5427
  pausedAt: this.lastPausedAt,
5182
- });
5428
+ };
5429
+ if (send)
5430
+ send(reply);
5431
+ else if (socket)
5432
+ this.ipcServer?.send(socket, reply);
5183
5433
  }
5184
5434
  catch (err) {
5185
5435
  this.logger.debug({ err: err.message }, "Instance state query failed");
@@ -5219,7 +5469,49 @@ export class Daemon extends EventEmitter {
5219
5469
  static AUTH_RECONFIRM_ROWS = 15;
5220
5470
  /** Why the deferred pause was requested; what its marker records (Phase 2a). */
5221
5471
  pendingPauseReason = "auth";
5472
+ nativePauseProof = null;
5473
+ /** Consume the exact idle proof synchronously after the lifecycle's await, before fencing and closing. */
5474
+ nativePauseStillReady() {
5475
+ const proof = this.nativePauseProof;
5476
+ this.nativePauseProof = null;
5477
+ if (proof?.())
5478
+ return true;
5479
+ this.pauseRequested = false;
5480
+ return false;
5481
+ }
5482
+ /** Native lifecycle pause uses the ordinary idle/background/admission proof before closing its owned Job. */
5483
+ async nativePauseReady(admission) {
5484
+ this.nativePauseProof = null;
5485
+ const native = this.nativePaneLifecycle, pane = this.tmux, owner = this.interactionOwner();
5486
+ const allowStuck = this.pauseAllowStuck;
5487
+ this.pauseAllowStuck = false;
5488
+ const ready = () => !!native && native === this.nativePaneLifecycle && native.current() && this.tmux === pane
5489
+ && sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.spawning
5490
+ && this.processStatus === "running" && this.pasteQueueDepth === 0
5491
+ && (this.instanceState === "idle" || allowStuck && this.instanceState === "stuck") && (!admission || admission.current());
5492
+ if (!pane || !ready()) {
5493
+ this.pauseRequested = false;
5494
+ return false;
5495
+ }
5496
+ let text;
5497
+ try {
5498
+ text = await pane.capturePane();
5499
+ }
5500
+ catch {
5501
+ this.pauseRequested = false;
5502
+ return false;
5503
+ }
5504
+ if (!ready() || admission && !admission.accepts(text) || this.backend?.backgroundWorkRunning?.(text)
5505
+ || !allowStuck && !this.paneAuthoritativelyIdle(text)) {
5506
+ this.pauseRequested = false;
5507
+ return false;
5508
+ }
5509
+ this.nativePauseProof = ready;
5510
+ return true;
5511
+ }
5222
5512
  async pause(reason = "operator", admission) {
5513
+ if (this.nativePaneLifecycle)
5514
+ throw Error("Native pause is owned by its Fleet lifecycle transition");
5223
5515
  if (admission && !admission.current())
5224
5516
  return;
5225
5517
  if (this.pauseWakeState === "paused")
@@ -5315,7 +5607,7 @@ export class Daemon extends EventEmitter {
5315
5607
  writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
5316
5608
  this.publishWorkingState("execution");
5317
5609
  this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
5318
- this.ipcServer?.broadcast({
5610
+ this.broadcastFleetMessage({
5319
5611
  type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
5320
5612
  });
5321
5613
  this.emit("auto_paused", { name: this.name, pausedAt: this.lastPausedAt });
@@ -5410,7 +5702,7 @@ export class Daemon extends EventEmitter {
5410
5702
  this.resumeRuntimeMonitors();
5411
5703
  this.publishWorkingState("execution");
5412
5704
  this.logger.info("Instance auto-woke");
5413
- this.ipcServer?.broadcast({
5705
+ this.broadcastFleetMessage({
5414
5706
  type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
5415
5707
  });
5416
5708
  this.emit("auto_woke", { name: this.name });
@@ -5570,7 +5862,7 @@ export class Daemon extends EventEmitter {
5570
5862
  this.logStateEdge(previous, snapshot);
5571
5863
  this.publishWorkingState("execution");
5572
5864
  this.emit("instance_state", { name: this.name, ...snapshot });
5573
- this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
5865
+ this.broadcastFleetMessage({ type: "instance_state", instanceName: this.name, ...snapshot });
5574
5866
  if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
5575
5867
  this.handleStuckTransition(pane, snapshot, this.instanceStateReadyPattern);
5576
5868
  }
@@ -5670,7 +5962,7 @@ export class Daemon extends EventEmitter {
5670
5962
  // during the continuation boots clean instead of chaining resumes.
5671
5963
  // #2544: a reply that settled before this arm already answered the person —
5672
5964
  // writing a marker would re-inject a continuation for a finished turn.
5673
- if (!meta.resumedContinuationOf && !armed.completionDelivered) {
5965
+ if (!this.nativePaneLifecycle && !meta.resumedContinuationOf && !armed.completionDelivered) {
5674
5966
  const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
5675
5967
  const resumeCwd = this.config.working_directory ?? "";
5676
5968
  writeInFlightTurnMarker(this.instanceDir, {
@@ -5902,7 +6194,7 @@ export class Daemon extends EventEmitter {
5902
6194
  // outbound action suppresses a duplicate proxy; only a delivered
5903
6195
  // human-facing channel action satisfies the completion guard below.
5904
6196
  if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
5905
- && mcpServerState(this.instanceDir).state === "dead") {
6197
+ && (this.nativePaneLifecycle ? this.nativePaneLifecycle.mcpProof() === "dead" : mcpServerState(this.instanceDir).state === "dead")) {
5906
6198
  this.turnReplyGuard.complete(turn.generation);
5907
6199
  void this.sendProxyReply(pane, turn.target);
5908
6200
  return;
@@ -6279,7 +6571,7 @@ export class Daemon extends EventEmitter {
6279
6571
  }
6280
6572
  });
6281
6573
  }
6282
- if (!this.ipcServer) {
6574
+ if (!this.fleetTransportAvailable()) {
6283
6575
  this.logger.error(`${logLabel} failed — no adapter or fleet IPC route`);
6284
6576
  return Promise.resolve(false);
6285
6577
  }
@@ -6303,7 +6595,7 @@ export class Daemon extends EventEmitter {
6303
6595
  resolve(delivered);
6304
6596
  });
6305
6597
  try {
6306
- this.ipcServer.broadcast({
6598
+ this.broadcastFleetMessage({
6307
6599
  type: "fleet_outbound",
6308
6600
  tool: "reply",
6309
6601
  args,
@@ -6874,7 +7166,7 @@ export class Daemon extends EventEmitter {
6874
7166
  if (next === this.currentActivity)
6875
7167
  return;
6876
7168
  this.currentActivity = next;
6877
- this.ipcServer?.broadcast({
7169
+ this.broadcastFleetMessage({
6878
7170
  type: "instance_activity",
6879
7171
  instanceName: this.name,
6880
7172
  activity: next,
@@ -6951,7 +7243,7 @@ export class Daemon extends EventEmitter {
6951
7243
  else
6952
7244
  this.config.auto_pause_after = nextAutoPause;
6953
7245
  if (nextAutoPause !== previousAutoPause) {
6954
- const isGeneral = this.config.general_topic === true || this.name === "general";
7246
+ const isGeneral = this.config.general_topic === true || generalByName(this.name, this.config);
6955
7247
  this.autoPauseController.reconfigure((isGeneral ? 0 : nextAutoPause) * 60_000);
6956
7248
  }
6957
7249
  }
@@ -7031,7 +7323,7 @@ export class Daemon extends EventEmitter {
7031
7323
  return;
7032
7324
  this.lastProgressBroadcast = rendered;
7033
7325
  this.lastProgressBroadcastAt = Date.now();
7034
- this.ipcServer?.broadcast({
7326
+ this.broadcastFleetMessage({
7035
7327
  type: "instance_progress",
7036
7328
  instanceName: this.name,
7037
7329
  progress: rendered,
@@ -7047,7 +7339,7 @@ export class Daemon extends EventEmitter {
7047
7339
  if (this.lastProgressBroadcast !== "") {
7048
7340
  this.lastProgressBroadcast = "";
7049
7341
  this.lastProgressBroadcastAt = Date.now();
7050
- this.ipcServer?.broadcast({ type: "instance_progress", instanceName: this.name, progress: "" });
7342
+ this.broadcastFleetMessage({ type: "instance_progress", instanceName: this.name, progress: "" });
7051
7343
  }
7052
7344
  }
7053
7345
  /**
@@ -7212,7 +7504,7 @@ export class Daemon extends EventEmitter {
7212
7504
  reportCrossInstanceDeliveryFailure(meta, verdict, error) {
7213
7505
  if (!meta.from_instance)
7214
7506
  return;
7215
- this.ipcServer?.broadcast({
7507
+ this.broadcastFleetMessage({
7216
7508
  type: "cross_instance_delivery_failed",
7217
7509
  senderSession: meta.from_instance,
7218
7510
  targetInstance: this.name,
@@ -8572,13 +8864,14 @@ export class Daemon extends EventEmitter {
8572
8864
  if (this.remoteCardAnswering)
8573
8865
  return { outcome: "refused", reason: "another answer to this card is being keyed in" };
8574
8866
  this.remoteCardAnswering = true;
8867
+ let answerScreen = null;
8575
8868
  try {
8576
8869
  const outcome = await answerRemoteCard({
8577
8870
  locked: fn => this.paneWriteLock.run(fn),
8578
8871
  // 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),
8872
+ read: async () => answerScreen = this.tmux === tmux ? await tmux.capturePane().catch(() => null) : null,
8873
+ 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)),
8874
+ paste: text => this.tmux !== tmux || answerScreen === null ? Promise.resolve(false) : this.nativePaneLifecycle ? tmux.pasteBuffer(text, { proof: { kind: "dialog", screen: answerScreen } }) : tmux.pasteBuffer(text),
8582
8875
  sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
8583
8876
  now: () => performance.now(),
8584
8877
  }, pane => {
@@ -8849,12 +9142,19 @@ export class Daemon extends EventEmitter {
8849
9142
  if (!check || !this.tmux)
8850
9143
  return !check;
8851
9144
  try {
8852
- const mode = await this.tmux.getPaneInputMode?.();
8853
- if (mode !== "raw") {
9145
+ const inputActive = this.nativePaneLifecycle
9146
+ ? this.nativePaneLifecycle.current() && this.nativePaneLifecycle.mcpProof() === "alive"
9147
+ : await this.tmux.getPaneInputMode?.() === "raw";
9148
+ // ConPTY has no POSIX termios mode. Its closed launch plus current helper handshake is the input barrier;
9149
+ // the same strict composer detector below is still required. Never grant native unknown-layout fallback.
9150
+ if (!inputActive) {
8854
9151
  this.resetFooterFallback();
8855
9152
  return false;
8856
9153
  }
8857
- return this.deliveryInputReadyPane(await this.tmux.capturePane());
9154
+ const pane = await this.tmux.capturePane();
9155
+ if (this.nativePaneLifecycle && (!this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"))
9156
+ return false;
9157
+ return this.deliveryInputReadyPane(pane);
8858
9158
  }
8859
9159
  catch {
8860
9160
  this.resetFooterFallback();
@@ -9628,34 +9928,86 @@ export class Daemon extends EventEmitter {
9628
9928
  // tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
9629
9929
  // the CLI this paste went to is never sent to its successor.
9630
9930
  const tmux = this.tmux;
9931
+ const backend = this.backend;
9932
+ const native = !!this.nativePaneLifecycle;
9631
9933
  const generation = this.spawnGeneration;
9632
9934
  const fence = this.launchFenceEpoch;
9935
+ const windowId = native ? tmux?.getWindowId() : undefined;
9936
+ // M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
9937
+ // tmux object and window — so the spawn generation and launch fence are part of every pane's fence: an Enter meant
9938
+ // for the CLI this paste went to is never sent to its successor. A native Windows pane keeps #2958's full fence too
9939
+ // (its launch can be replaced under the same handle).
9633
9940
  const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
9634
- && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
9941
+ && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
9942
+ && (!native || (!!windowId && !!backend && this.backend === backend && tmux.getWindowId() === windowId));
9635
9943
  if (!current())
9636
9944
  return false;
9637
9945
  // 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");
9946
+ // S-35: say which check withheld Enter.
9947
+ const hold = (reason) => {
9948
+ onInputHeld?.(reason);
9949
+ if (reason === "output_changed" && current() && !this.nativeDeliveryHold())
9950
+ this.nativeEnterHold = {
9951
+ key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence,
9952
+ };
9953
+ if (reason === "pane-in-mode")
9954
+ this.logger.warn({ phase, reason }, "The pane stayed in a tmux mode; Enter withheld");
9955
+ else
9956
+ this.logger.warn({ ...tmux?.getLastInputDiagnostic?.(), phase, reason, retryCount: positiveRefusals }, "Native delivery Enter withheld by its safety check");
9641
9957
  return false;
9642
9958
  };
9643
9959
  // 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
9960
  // 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.)
9961
+ // past the bound.
9962
+ // Native delivery follows main #2990: only this backend's recognised runtime dialog vetoes Enter.
9963
+ // The fresh capture still binds the native host output sequence; ownership and positive-ACK-only retries remain.
9647
9964
  const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9965
+ let pane;
9966
+ let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity, linuxDialogRead = false;
9648
9967
  for (;;) {
9649
- const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
9968
+ const mode = await this.waitWhilePaneInMode(phase, current, Math.min(modeDeadline, positiveRetryDeadline));
9650
9969
  if (mode === "stale")
9651
9970
  return false;
9652
9971
  if (mode === "held")
9653
- return hold();
9972
+ return hold("pane-in-mode");
9973
+ if (native) {
9974
+ // S-35: a native pane's capture refuses while output is still arriving ("Pane changed during capture; observe it
9975
+ // again"). Right after a turn the CLI repaints for a moment; one refused read left the message unsent for ever.
9976
+ // Read again a few times — reading types nothing — and still hold if it never settles.
9977
+ let captured;
9978
+ for (let attempt = 0; captured === undefined; attempt++) {
9979
+ try {
9980
+ captured = await tmux.capturePane();
9981
+ }
9982
+ catch {
9983
+ if (!current())
9984
+ return false;
9985
+ if (attempt >= 4)
9986
+ return hold("capture_failed");
9987
+ await new Promise(done => setTimeout(done, 150 * (attempt + 1)));
9988
+ if (!current())
9989
+ return false;
9990
+ }
9991
+ }
9992
+ const screen = captured;
9993
+ pane = screen;
9994
+ if (!current())
9995
+ return false;
9996
+ try {
9997
+ if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
9998
+ return hold("dialog");
9999
+ }
10000
+ catch {
10001
+ return hold("check_failed");
10002
+ }
10003
+ if (!current())
10004
+ return false;
10005
+ }
9654
10006
  if (await this.paneInTmuxMode(phase)) {
9655
10007
  if (!current())
9656
10008
  return false;
9657
10009
  if (performance.now() >= modeDeadline)
9658
- return hold();
10010
+ return hold("pane-in-mode");
9659
10011
  await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9660
10012
  if (!current())
9661
10013
  return false;
@@ -9663,54 +10015,143 @@ export class Daemon extends EventEmitter {
9663
10015
  }
9664
10016
  if (!current())
9665
10017
  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;
10018
+ // M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468; main): on a Linux pane, a dialog the
10019
+ // backend RECOGNISES (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the
10020
+ // paste and this Enter would take the key as its answer. Read the pane once more: such a dialog withholds the
10021
+ // Enter (the caller keeps the delivery uncertain → Needs-you; the dialog scanner parks it for a person). Nothing
10022
+ // else is judged — an unknown or unreadable screen sends: #2958's composer proof (decision f57474aa) is NOT back.
10023
+ // Only a caller that can hold asks (onInputHeld); a native pane read its own screen above, every round.
10024
+ if (!native && onInputHeld && !linuxDialogRead) {
10025
+ linuxDialogRead = true;
10026
+ let finalPane = null;
10027
+ try {
10028
+ finalPane = await tmux.capturePane();
10029
+ }
10030
+ catch {
10031
+ finalPane = null;
10032
+ }
10033
+ if (!current())
10034
+ return false;
10035
+ let dialog;
10036
+ try {
10037
+ dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
10038
+ }
10039
+ catch {
10040
+ dialog = undefined;
10041
+ }
10042
+ if (dialog !== undefined) {
10043
+ onInputHeld("dialog");
10044
+ this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
10045
+ return false;
10046
+ }
10047
+ }
10048
+ if (positiveRefusals && performance.now() >= positiveRetryDeadline)
10049
+ return hold("output_changed");
10050
+ // W1: persist the "Enter may have happened" boundary before tmux receives
10051
+ // the key. A crash after this commit is never auto-replayed without a
10052
+ // positive transcript marker, even if the stale pane still shows composer.
10053
+ if (!enterMarked && durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
10054
+ this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
10055
+ return false;
10056
+ }
10057
+ enterMarked = true;
10058
+ const sent = await (native && pane !== undefined ? tmux.sendSpecialKey("Enter", undefined, { kind: "delivery", screen: pane, retryCount: positiveRefusals }) : tmux.sendSpecialKey("Enter"));
10059
+ // A busy renderer can change seq after our capture. Only a positive zero-input ACK admits a NEW proof/key;
10060
+ // An Enter retry never re-pastes, retries an uncertain write, reuses stale proof or re-commits the durable boundary.
10061
+ if (!sent && native && tmux.lastInputWasNotSent?.() === true
10062
+ && tmux.getLastSendSpecialKeyError() === "Pane changed after observation; input was not sent"
10063
+ && current()) {
10064
+ if (!positiveRefusals)
10065
+ positiveRetryDeadline = Math.min(modeDeadline, performance.now() + 2000);
10066
+ if (positiveRefusals >= 4 || performance.now() >= positiveRetryDeadline)
10067
+ return hold("output_changed");
10068
+ positiveRefusals++;
10069
+ await new Promise(done => setTimeout(done, 100));
10070
+ if (!current())
10071
+ return false;
10072
+ if (performance.now() >= positiveRetryDeadline)
10073
+ return hold("output_changed");
10074
+ continue;
10075
+ }
10076
+ if (!sent) {
10077
+ const details = { phase, tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure" };
10078
+ if (native)
10079
+ this.logger.warn({ ...tmux?.getLastInputDiagnostic?.(), ...details, retryCount: positiveRefusals }, "Native delivery Enter gave up");
10080
+ else
10081
+ this.logger.error(details, "tmux send-keys Enter failed during message delivery");
10082
+ }
10083
+ if (sent && native && current())
10084
+ this.nativeEnterHold = null;
10085
+ return sent;
10086
+ }
10087
+ }
10088
+ /** A pre-existing native busy-paste race: retry bytes only after an exact, owned zero-input acknowledgement. */
10089
+ async sendDeliveryPaste(text, stillCurrent) {
10090
+ const pane = this.tmux, native = this.nativePaneLifecycle;
10091
+ if (!native)
10092
+ return pane.pasteBuffer(text);
10093
+ const backend = this.backend, generation = this.spawnGeneration, fence = this.launchFenceEpoch, windowId = pane.getWindowId();
10094
+ const current = () => !!windowId && this.tmux === pane && this.nativePaneLifecycle === native && this.backend === backend
10095
+ && this.spawnGeneration === generation && this.launchFenceEpoch === fence && pane.getWindowId() === windowId
10096
+ && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
10097
+ let retryCount = 0;
10098
+ const hold = (reason) => {
10099
+ if (current() && !this.nativeDeliveryHold())
10100
+ this.nativeEnterHold = { key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence };
10101
+ this.logger.warn({ ...pane.getLastInputDiagnostic?.(), reason, retryCount }, "Native delivery paste held by its safety check");
10102
+ return false;
10103
+ };
10104
+ const deadline = performance.now() + 2000;
10105
+ for (let attempt = 0; attempt < 5; attempt++) {
10106
+ retryCount = attempt;
10107
+ if (!current())
10108
+ return false;
10109
+ if (performance.now() >= deadline)
10110
+ return hold("output_changed");
10111
+ let screen;
10112
+ for (let read = 0; read < 5 && screen === undefined; read++) {
10113
+ try {
10114
+ screen = await pane.capturePane();
10115
+ }
10116
+ catch { /* Reads do not construct an input operation. */ }
10117
+ if (!current())
10118
+ return false;
10119
+ if (performance.now() >= deadline)
10120
+ return hold("capture_failed");
10121
+ if (screen === undefined && read < 4)
10122
+ await new Promise(done => setTimeout(done, 100));
10123
+ }
10124
+ if (screen === undefined)
10125
+ return hold("capture_failed");
9678
10126
  try {
9679
- finalPane = await tmux.capturePane();
10127
+ if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
10128
+ return hold("dialog");
9680
10129
  }
9681
10130
  catch {
9682
- finalPane = null;
10131
+ return hold("check_failed");
9683
10132
  }
9684
10133
  if (!current())
9685
10134
  return false;
9686
- let dialog;
10135
+ if (performance.now() >= deadline)
10136
+ return hold("output_changed");
10137
+ let sent;
9687
10138
  try {
9688
- dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
10139
+ sent = await pane.pasteBuffer(text, { guard: current, proof: { kind: "composer", screen, retryCount: attempt } });
9689
10140
  }
9690
10141
  catch {
9691
- dialog = undefined;
10142
+ return hold("unconfirmed");
9692
10143
  }
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)");
10144
+ if (sent)
10145
+ return true; // Exactly one successful paste ends the loop, before any Enter.
10146
+ if (!current())
9696
10147
  return false;
9697
- }
9698
- }
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");
10148
+ if (pane.lastInputWasNotSent?.() !== true || pane.getLastPasteError?.() !== "Pane changed after observation; input was not sent")
10149
+ return hold("unconfirmed");
10150
+ if (attempt >= 4 || performance.now() >= deadline)
10151
+ return hold("output_changed");
10152
+ await new Promise(done => setTimeout(done, 100));
9712
10153
  }
9713
- return sent;
10154
+ return hold("output_changed");
9714
10155
  }
9715
10156
  async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
9716
10157
  // Last and defaulted so the positional callers that ignore the outcome stay
@@ -9748,6 +10189,9 @@ export class Daemon extends EventEmitter {
9748
10189
  const tmuxAtWrite = this.tmux;
9749
10190
  const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
9750
10191
  && this.tmux === tmuxAtWrite && this.spawnGeneration === spawnAtWrite;
10192
+ // Alpha (S-36, Prism #2989 P2): a NATIVE pane keeps #2958's pre-paste owner fence through the initial Enter and every
10193
+ // retry Enter (a pane replaced while the paste awaits never takes this delivery's Enter); a tmux pane follows main.
10194
+ const nativeOwner = this.nativePaneLifecycle ? steerCurrent : undefined;
9751
10195
  // Bug A: paste with backoff. Transient failures are usually a stale window id
9752
10196
  // after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
9753
10197
  const maxAttempts = 3;
@@ -9815,8 +10259,8 @@ export class Daemon extends EventEmitter {
9815
10259
  }
9816
10260
  verdict.durableBeginCommitted = true;
9817
10261
  verdict.submissionMode = attemptEvidence.submissionMode;
9818
- if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
9819
- verdict.transcriptCheckpoint = {
10262
+ if (attemptEvidence.runtimeTranscript || attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
10263
+ verdict.transcriptCheckpoint = attemptEvidence.runtimeTranscript ? { backend: attemptEvidence.backend, runtime: attemptEvidence.runtimeTranscript } : {
9820
10264
  backend: attemptEvidence.backend,
9821
10265
  path: attemptEvidence.transcriptPath,
9822
10266
  offset: attemptEvidence.transcriptOffset,
@@ -9881,7 +10325,15 @@ export class Daemon extends EventEmitter {
9881
10325
  }
9882
10326
  const pasteGeneration = this.spawnGeneration;
9883
10327
  beforePaste?.();
9884
- const pasted = await this.tmux.pasteBuffer(formatted);
10328
+ const pasted = await this.sendDeliveryPaste(formatted, nativeOwner);
10329
+ if (!pasted && this.nativePaneLifecycle) {
10330
+ // The native write attempt is retained as uncertain, including exhausted positive refusals. Never route an
10331
+ // unknown/partial write through legacy window recovery or abort it back to an automatically replayable row.
10332
+ verdict.paneWriteStarted = true;
10333
+ verdict.phase = "paste";
10334
+ verdict.proof = "native-paste-held";
10335
+ return false;
10336
+ }
9885
10337
  if (pasted)
9886
10338
  this.rememberPaste(formatted, pasteGeneration);
9887
10339
  if (!pasted) {
@@ -9968,15 +10420,18 @@ export class Daemon extends EventEmitter {
9968
10420
  const enterMono = performance.now();
9969
10421
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
9970
10422
  let inputHeld = null;
9971
- if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
10423
+ if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof || nativeOwner ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
9972
10424
  if (inputHeld) {
9973
10425
  // #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
9974
10426
  // the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
9975
10427
  verdict.phase = "submit-enter";
9976
- verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
10428
+ // The merge of #2957/M-2290 (main) and the alpha's native holds: a tmux mode, a recognised dialog on a Linux pane,
10429
+ // and each native safety check say which.
10430
+ verdict.proof = inputHeld === "pane-in-mode" ? "pane-in-mode-held" : nativeOwner ? `native-input-${inputHeld}`
10431
+ : inputHeld === "dialog" ? "dialog-on-pane" : `input-held-${inputHeld}`;
9977
10432
  return false;
9978
10433
  }
9979
- if (steerComposerProof && !steerCurrent()) {
10434
+ if ((steerComposerProof || nativeOwner) && !steerCurrent()) {
9980
10435
  verdict.phase = "submit-enter";
9981
10436
  verdict.proof = "fenced-before-enter";
9982
10437
  return false;
@@ -10005,7 +10460,7 @@ export class Daemon extends EventEmitter {
10005
10460
  if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
10006
10461
  await new Promise(r => setTimeout(r, 1_000));
10007
10462
  const retryAt = Date.now();
10008
- if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
10463
+ if (await this.sendDeliveryEnter("queue-less-defensive-retry", nativeOwner)) {
10009
10464
  enterAt = retryAt;
10010
10465
  enterRetry = true;
10011
10466
  this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
@@ -10138,7 +10593,7 @@ export class Daemon extends EventEmitter {
10138
10593
  // again. The prompt is back now, so an Enter can land.
10139
10594
  this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
10140
10595
  const strandedAt = Date.now();
10141
- if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
10596
+ if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", nativeOwner))) {
10142
10597
  return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
10143
10598
  }
10144
10599
  // The recovery Enter can be accepted before Codex paints its new
@@ -10191,7 +10646,7 @@ export class Daemon extends EventEmitter {
10191
10646
  }
10192
10647
  await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
10193
10648
  const retryAt = Date.now();
10194
- if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
10649
+ if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", nativeOwner))) {
10195
10650
  return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
10196
10651
  }
10197
10652
  if (windowId && this.controlClient) {
@@ -10243,7 +10698,7 @@ export class Daemon extends EventEmitter {
10243
10698
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
10244
10699
  const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
10245
10700
  const retryAt = Date.now();
10246
- if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
10701
+ if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", nativeOwner))) {
10247
10702
  return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
10248
10703
  }
10249
10704
  submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
@@ -10331,7 +10786,7 @@ export class Daemon extends EventEmitter {
10331
10786
  }
10332
10787
  // No control client to observe output: fall back to the legacy double-Enter.
10333
10788
  await new Promise(r => setTimeout(r, 1000));
10334
- await this.sendDeliveryEnter("unobserved-defensive-retry");
10789
+ await this.sendDeliveryEnter("unobserved-defensive-retry", nativeOwner);
10335
10790
  if (status)
10336
10791
  this.emit("message_confirmed", status); // ✅ (best-effort)
10337
10792
  }
@@ -10551,14 +11006,22 @@ export class Daemon extends EventEmitter {
10551
11006
  * and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
10552
11007
  */
10553
11008
  transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
11009
+ if (this.nativePaneLifecycle && !checkpoint.runtime)
11010
+ throw Error("Native proof cannot read a coordinator-local transcript path");
10554
11011
  const launchAtWrite = this.launchFenceEpoch;
10555
11012
  return {
10556
- reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
11013
+ reader: checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
10557
11014
  current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
10558
- && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
11015
+ && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
11016
+ && (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
10559
11017
  onProof,
10560
11018
  };
10561
11019
  }
11020
+ nativeTranscriptReader(reference, needle) {
11021
+ if (!this.nativePaneLifecycle || !this.nativePaneLifecycle.current(reference))
11022
+ throw Error("Native delivery transcript owner is unavailable");
11023
+ return this.nativePaneLifecycle.proofReader(reference, needle);
11024
+ }
10562
11025
  /** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
10563
11026
  cliLabel() {
10564
11027
  return this.config.backend ?? this.backend?.binaryName ?? "CLI";
@@ -10802,7 +11265,14 @@ export class Daemon extends EventEmitter {
10802
11265
  * (the retries included). Without a guard it is exactly the old unconditional path.
10803
11266
  */
10804
11267
  async submitSystemPaste(text, label, guard, admission) {
10805
- const current = guard || admission ? () => { admission?.(); return guard?.current() ?? true; } : undefined;
11268
+ // Alpha (S-36, Prism #2989 P2): a NATIVE pane's system paste keeps the pane owner from before its first await
11269
+ // (#2958's fence, reverted on main for tmux panes) — a pane replaced while the paste awaits never takes its Enter.
11270
+ const native = !!this.nativePaneLifecycle;
11271
+ const ownerTmux = this.tmux, ownerGeneration = this.spawnGeneration, ownerFence = this.launchFenceEpoch;
11272
+ const ownerWindow = native ? ownerTmux?.getWindowId() : undefined;
11273
+ const owner = () => !native || (!!ownerTmux && !!ownerWindow && this.tmux === ownerTmux && ownerTmux.getWindowId() === ownerWindow
11274
+ && this.spawnGeneration === ownerGeneration && this.launchFenceEpoch === ownerFence && !this.deliveryWritesStopping);
11275
+ const current = guard || admission || native ? () => { admission?.(); return owner() && (guard?.current() ?? true); } : undefined;
10806
11276
  if (current && !current())
10807
11277
  return false;
10808
11278
  if (!this.tmux)
@@ -10922,13 +11392,13 @@ export class Daemon extends EventEmitter {
10922
11392
  && sameInteractionOwner(recoveryOwner, this.interactionOwner());
10923
11393
  const previousWindowId = this.tmux?.getWindowId();
10924
11394
  try {
10925
- const windows = await TmuxManager.listWindows(this.tmuxSessionName, this.controlClient);
11395
+ const windows = await this.paneHost.listWindows(this.tmuxSessionName, this.controlClient);
10926
11396
  if (!recoveryCurrent() || this.tmux !== previousTmux)
10927
11397
  return undefined;
10928
11398
  const match = windows.find(w => w.name === this.name);
10929
11399
  if (!match)
10930
11400
  return undefined;
10931
- this.tmux = new TmuxManager(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
11401
+ this.tmux = this.paneHost.bind(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
10932
11402
  const recoveredTmux = this.tmux;
10933
11403
  writeFileSync(join(this.instanceDir, "window-id"), match.id);
10934
11404
  // The window we were talking to is gone; leaving it registered means the
@@ -10991,6 +11461,8 @@ export class Daemon extends EventEmitter {
10991
11461
  return false;
10992
11462
  }
10993
11463
  getWindowId() {
11464
+ if (this.nativePaneLifecycle)
11465
+ return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
10994
11466
  try {
10995
11467
  return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
10996
11468
  }
@@ -11022,7 +11494,55 @@ export class Daemon extends EventEmitter {
11022
11494
  * Handle a tool call from the MCP server (forwarded by Claude).
11023
11495
  * Routes to the channel adapter via MessageBus.
11024
11496
  */
11025
- handleToolCall(msg, socket) {
11497
+ /** Internal native-MCP admission. The pane/machine/launch owner supplies a current, dynamically checked tool lease. */
11498
+ handlePaneToolCall(input, lease) {
11499
+ const refusal = nativePaneToolRefusal(input.tool, input.args);
11500
+ if (refusal)
11501
+ return Promise.resolve({ result: null, error: refusal });
11502
+ if (!lease.current() || !lease.allowed(input.tool, input.args) || this.deliveryWritesStopping)
11503
+ return Promise.resolve({ result: null, error: "Pane tool permission is no longer current" });
11504
+ const fence = this.launchFenceEpoch;
11505
+ const current = () => lease.current() && lease.allowed(input.tool, input.args) && !this.deliveryWritesStopping && fence === this.launchFenceEpoch;
11506
+ return new Promise(resolve => {
11507
+ let settled = false;
11508
+ const finish = (value) => { if (settled)
11509
+ return; settled = true; clearTimeout(timer); resolve(value); };
11510
+ const timer = setTimeout(() => finish({ result: null, error: "Pane tool result is unconfirmed; inspect its operation before retrying" }), daemonBudgetMs(input.tool) + 1000);
11511
+ timer.unref?.();
11512
+ try {
11513
+ this.handleToolCall({ ...input, requestId: ++this.fleetRequestSeq }, undefined, {
11514
+ current,
11515
+ dispatch: (tool, args, respond) => {
11516
+ if (!current()) {
11517
+ respond(null, "Pane tool permission was revoked before dispatch");
11518
+ return true;
11519
+ }
11520
+ const dispatched = lease.dispatch?.(tool, args);
11521
+ if (!dispatched)
11522
+ return false;
11523
+ void dispatched.then(result => respond(result), () => respond(null, "Pane tool operation was refused"));
11524
+ return true;
11525
+ },
11526
+ // Resolving the relay is not proof that its HTTP response reached the provider. No response-written receipt yet.
11527
+ send: value => {
11528
+ finish({ result: value.result, ...(typeof value.error === "string" ? { error: value.error } : {}),
11529
+ ...(typeof value.errorCode === "string" || typeof value.errorCode === "number" ? { errorCode: value.errorCode } : {}) });
11530
+ return false;
11531
+ },
11532
+ });
11533
+ }
11534
+ catch {
11535
+ finish({ result: null, error: "Pane tool operation failed" });
11536
+ }
11537
+ });
11538
+ }
11539
+ /** Only the authority lease with a verified helper stdout ACK calls this internal barrier. */
11540
+ paneToolResponseWritten(operationId, current) {
11541
+ if (!current() || this.deliveryWritesStopping || !/^[a-f0-9-]{36}$/i.test(operationId))
11542
+ return;
11543
+ this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
11544
+ }
11545
+ handleToolCall(msg, socket, peer) {
11026
11546
  const tool = msg.tool;
11027
11547
  const args = (msg.args ?? {});
11028
11548
  const requestId = msg.requestId;
@@ -11030,20 +11550,20 @@ export class Daemon extends EventEmitter {
11030
11550
  this.logger.debug({ tool, requestId }, "Tool call from MCP server");
11031
11551
  // A tool call can only come from a live MCP server of ours — proof of life
11032
11552
  // that outranks whatever the pid slot says.
11033
- if (this.socketSessionNames.get(socket) === this.name) {
11034
- this.noteMcpProofOfLife("tool_call", this.socketPids.get(socket));
11553
+ if (peer?.current() || socket && this.socketSessionNames.get(socket) === this.name) {
11554
+ this.noteMcpProofOfLife("tool_call", socket ? this.socketPids.get(socket) : undefined);
11035
11555
  }
11036
11556
  // A tool invocation is not delivery evidence. Capture which obligation it
11037
11557
  // belongs to now, then settle it only after the adapter/fleet responds.
11038
11558
  // Explicitly mapped sibling sessions must not satisfy this instance's turn;
11039
11559
  // an unmapped socket is retained for legacy/tests where mcp_ready was not
11040
11560
  // observed before the first tool call.
11041
- const sourceSession = this.socketSessionNames.get(socket);
11561
+ const sourceSession = peer ? this.name : socket ? this.socketSessionNames.get(socket) : undefined;
11042
11562
  const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
11043
11563
  ? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
11044
11564
  : null;
11045
11565
  // For now, log and respond. Full adapter routing will be wired in fleet manager.
11046
- const respond = (result, error) => {
11566
+ const respond = (result, error, errorCode) => {
11047
11567
  // A message that verifiably went out stands down the dead-MCP proxy reply
11048
11568
  // for this turn: the agent proved it can still speak for itself.
11049
11569
  if (!error && result != null && TURN_OUTBOUND_TOOLS.has(tool)) {
@@ -11084,14 +11604,21 @@ export class Daemon extends EventEmitter {
11084
11604
  // failed" and report unknown immediately rather than starting recovery.
11085
11605
  this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
11086
11606
  }
11087
- const sent = this.ipcServer?.send(socket, { requestId, result, error, operationId }) ?? false;
11607
+ const packet = { requestId, result, error, operationId, ...(errorCode !== undefined ? { errorCode } : {}) };
11608
+ const sent = peer ? peer.current() && peer.send(packet) : socket ? this.ipcServer?.send(socket, packet) ?? false : false;
11088
11609
  if (!sent) {
11089
- this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
11610
+ if (peer)
11611
+ this.logger.debug({ tool, requestId }, "Native MCP result awaits transport-written confirmation");
11612
+ else
11613
+ this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
11090
11614
  }
11091
11615
  else if (operationId) {
11092
11616
  this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
11093
11617
  }
11094
11618
  };
11619
+ // Native Fleet/task operations carry their lease to the existing dispatcher mutation admission points.
11620
+ if (peer?.dispatch?.(tool, args, respond))
11621
+ return;
11095
11622
  // Repo checkout — handled locally in daemon (no fleet-manager)
11096
11623
  if (tool === "checkout_repo") {
11097
11624
  this.handleCheckoutRepo(args, respond);
@@ -11120,7 +11647,7 @@ export class Daemon extends EventEmitter {
11120
11647
  clearTimeout(timeout);
11121
11648
  respond(respMsg.result, respMsg.error);
11122
11649
  });
11123
- this.ipcServer?.broadcast({
11650
+ this.broadcastFleetMessage({
11124
11651
  type,
11125
11652
  payload: args,
11126
11653
  meta: { instance_name: this.name },
@@ -11146,7 +11673,7 @@ export class Daemon extends EventEmitter {
11146
11673
  clearTimeout(timeout);
11147
11674
  resolve({ result: respMsg.result, error: respMsg.error });
11148
11675
  });
11149
- this.ipcServer?.broadcast({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
11676
+ this.broadcastFleetMessage({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
11150
11677
  });
11151
11678
  void (async () => {
11152
11679
  if (tool === "preview_web_appearance") {
@@ -11223,7 +11750,7 @@ export class Daemon extends EventEmitter {
11223
11750
  clearTimeout(timeout);
11224
11751
  respond(respMsg.result, respMsg.error);
11225
11752
  });
11226
- this.ipcServer?.broadcast({
11753
+ this.broadcastFleetMessage({
11227
11754
  type: "fleet_task",
11228
11755
  payload: args,
11229
11756
  meta: { instance_name: this.name },
@@ -11246,7 +11773,7 @@ export class Daemon extends EventEmitter {
11246
11773
  clearTimeout(timeout);
11247
11774
  respond(respMsg.result, respMsg.error);
11248
11775
  });
11249
- this.ipcServer?.broadcast({
11776
+ this.broadcastFleetMessage({
11250
11777
  type: typeMap[tool],
11251
11778
  payload: args,
11252
11779
  meta: { instance_name: this.name, working_directory: this.config.working_directory },
@@ -11273,7 +11800,7 @@ export class Daemon extends EventEmitter {
11273
11800
  clearTimeout(timeout);
11274
11801
  respond(respMsg.result, respMsg.error);
11275
11802
  });
11276
- this.ipcServer?.broadcast({
11803
+ this.broadcastFleetMessage({
11277
11804
  type: typeMap[tool],
11278
11805
  payload: args,
11279
11806
  meta: {
@@ -11288,11 +11815,11 @@ export class Daemon extends EventEmitter {
11288
11815
  }
11289
11816
  if (CROSS_INSTANCE_TOOLS.has(tool)) {
11290
11817
  // Route to fleet manager via IPC (topic mode only)
11291
- if (this.topicMode && this.ipcServer) {
11818
+ if (this.topicMode && this.fleetTransportAvailable()) {
11292
11819
  // Use fleetRequestId (not requestId) to avoid MCP server resolving the
11293
11820
  // pending tool call prematurely when it receives the broadcast.
11294
11821
  const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
11295
- const senderSessionName = this.socketSessionNames.get(socket);
11822
+ const senderSessionName = sourceSession;
11296
11823
  // Shared table, so delete_instance and the deployment tools get the same
11297
11824
  // budget here as the MCP client expects (the ad-hoc list omitted them).
11298
11825
  const crossTimeoutMs = daemonBudgetMs(tool);
@@ -11304,7 +11831,7 @@ export class Daemon extends EventEmitter {
11304
11831
  clearTimeout(timeout);
11305
11832
  respond(respMsg.result, respMsg.error);
11306
11833
  });
11307
- this.ipcServer.broadcast({
11834
+ this.broadcastFleetMessage({
11308
11835
  type: "fleet_outbound",
11309
11836
  tool,
11310
11837
  args,
@@ -11358,12 +11885,12 @@ export class Daemon extends EventEmitter {
11358
11885
  }, daemonBudgetMs(tool));
11359
11886
  this.pendingIpcRequests.set(outboundKey, (respMsg) => {
11360
11887
  clearTimeout(timeout);
11361
- respond(respMsg.result, respMsg.error);
11888
+ respond(respMsg.result, respMsg.error, respMsg.errorCode);
11362
11889
  });
11363
11890
  // Register the waiter before broadcasting. A fast/mock adapter can settle
11364
11891
  // immediately; broadcasting first left a response-before-registration
11365
11892
  // window that degraded into a timeout despite a completed POST.
11366
- this.ipcServer?.broadcast({
11893
+ this.broadcastFleetMessage({
11367
11894
  type: "fleet_outbound",
11368
11895
  tool,
11369
11896
  args,
@@ -11494,7 +12021,7 @@ export class Daemon extends EventEmitter {
11494
12021
  // The daemon used to read `general_topic` alone while `isGeneralInstance`
11495
12022
  // also accepted the name, so an instance called `general` without the flag
11496
12023
  // was a worker here and a general everywhere else.
11497
- const isGeneral = this.config.general_topic === true || this.name === "general";
12024
+ const isGeneral = this.config.general_topic === true || generalByName(this.name, this.config);
11498
12025
  // No third argument for an ordinary instance: the default is `worker` now,
11499
12026
  // and naming it here would be a second place to change it.
11500
12027
  const toolSet = isGeneral
@@ -11870,6 +12397,8 @@ export class Daemon extends EventEmitter {
11870
12397
  catch { /* best effort */ }
11871
12398
  }
11872
12399
  async spawnClaudeWindow() {
12400
+ if (this.nativePaneLifecycle)
12401
+ throw Error("Native panes never enter local resume/fresh fallback");
11873
12402
  this.assertSessionOomAdmission();
11874
12403
  this.beginSpawn();
11875
12404
  let resumedSuccessfully = false;
@@ -12456,6 +12985,14 @@ export class Daemon extends EventEmitter {
12456
12985
  await this.museUsageRelay.stop().catch(() => { });
12457
12986
  this.museUsageRelay = null;
12458
12987
  }
12988
+ if (this.nativePaneLifecycle) {
12989
+ await this.nativePaneLifecycle.release("detach");
12990
+ try {
12991
+ unlinkSync(join(this.instanceDir, "daemon.pid"));
12992
+ }
12993
+ catch { /* metadata only */ }
12994
+ return;
12995
+ }
12459
12996
  try {
12460
12997
  await this.killProcessTree("SIGTERM", "startup aborted");
12461
12998
  }
@@ -12485,13 +13022,15 @@ export class Daemon extends EventEmitter {
12485
13022
  }
12486
13023
  /** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
12487
13024
  async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
13025
+ if (this.nativePaneLifecycle)
13026
+ throw Error("Native processes can only be closed through their owned runtime pane");
12488
13027
  const windowId = this.tmux?.getWindowId();
12489
13028
  // Before createWindow succeeds there is no pane owned by this daemon.
12490
13029
  // tmux interprets an empty selector as the session's active window.
12491
13030
  if (!windowId?.trim())
12492
13031
  return;
12493
13032
  try {
12494
- const pid = await TmuxManager.getPanePid(this.tmuxSessionName, windowId);
13033
+ const pid = await this.paneHost.getPanePid(this.tmuxSessionName, windowId);
12495
13034
  if (pid) {
12496
13035
  this.noteStopRequest(signal, reason);
12497
13036
  process.kill(-pid, signal);
@@ -12705,7 +13244,7 @@ export class Daemon extends EventEmitter {
12705
13244
  whatsNewRoles() {
12706
13245
  if (this.runtimeIdentity?.kind === "classic")
12707
13246
  return ["classic"];
12708
- if (this.config.general_topic === true || this.name === "general")
13247
+ if (this.config.general_topic === true || generalByName(this.name, this.config))
12709
13248
  return ["general"];
12710
13249
  const set = resolveToolSet(this.config, this.name);
12711
13250
  return set === "coordinator" || set === "full" ? ["worker", "coordinator"] : ["worker"];
@@ -12732,6 +13271,8 @@ export class Daemon extends EventEmitter {
12732
13271
  }
12733
13272
  }
12734
13273
  async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
13274
+ if (this.nativePaneLifecycle)
13275
+ throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
12735
13276
  this.assertSessionOomAdmission();
12736
13277
  this.startupAdmission?.();
12737
13278
  const launchFence = this.launchFenceEpoch;
@@ -12836,8 +13377,8 @@ export class Daemon extends EventEmitter {
12836
13377
  this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
12837
13378
  }
12838
13379
  // Ensure tmux session exists (may have been destroyed if all windows died)
12839
- await TmuxManager.ensureSession(this.tmuxSessionName);
12840
- if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
13380
+ await this.paneHost.ensureSession(this.tmuxSessionName);
13381
+ if (this.stormWindow?.observeServerAlive(await this.paneHost.getServerPid(this.tmuxSessionName))) {
12841
13382
  this.emit("tmux_server_crash", this.name);
12842
13383
  }
12843
13384
  this.startupAdmission?.();
@@ -13111,12 +13652,15 @@ export class Daemon extends EventEmitter {
13111
13652
  // each have seen a respawn, pause or stop.
13112
13653
  if (!owned())
13113
13654
  return false;
13655
+ const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
13656
+ if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
13657
+ return false;
13114
13658
  if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
13115
- if (!await this.tmux.sendSpecialKey(key))
13659
+ if (!await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key)))
13116
13660
  return false;
13117
13661
  }
13118
13662
  else {
13119
- if (!await this.tmux.sendKeys(key))
13663
+ if (!await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key)))
13120
13664
  return false;
13121
13665
  }
13122
13666
  await new Promise(r => setTimeout(r, 200));
@@ -13130,7 +13674,8 @@ export class Daemon extends EventEmitter {
13130
13674
  // Wait for next screen to render — bounded by what is left of the budget.
13131
13675
  const renderWait = Math.max(0, Math.min(10_000, remaining()));
13132
13676
  if (this.controlClient) {
13133
- const wid = readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
13677
+ const wid = this.nativePaneLifecycle ? ownerTmux.getWindowId()
13678
+ : readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
13134
13679
  await this.controlClient.waitForIdle(wid, renderWait);
13135
13680
  }
13136
13681
  else {
@@ -13166,7 +13711,9 @@ export class Daemon extends EventEmitter {
13166
13711
  // scan parked on the old limit text.
13167
13712
  const codexReady = this.backend.binaryName !== "codex"
13168
13713
  || this.isCodexLivePaneSnapshot(pane);
13169
- if (readyPatternMatches && codexReady) {
13714
+ const nativeComposerReady = !this.nativePaneLifecycle || (this.backend?.readInputRow
13715
+ ? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
13716
+ if (readyPatternMatches && codexReady && nativeComposerReady) {
13170
13717
  cleanReadyPolls++;
13171
13718
  if (cleanReadyPolls >= 2 || remaining() <= 0) {
13172
13719
  // A real ready prompt (with no fatal dialog on screen — those are
@@ -13291,6 +13838,16 @@ export class Daemon extends EventEmitter {
13291
13838
  void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
13292
13839
  }
13293
13840
  saveSessionId() {
13841
+ if (this.nativePaneLifecycle) {
13842
+ try {
13843
+ this.nativePaneLifecycle.checkpointSession();
13844
+ this.ownedWorkingSession = this.backend?.getSessionId() ?? null;
13845
+ }
13846
+ catch (err) {
13847
+ this.logger.warn({ err }, "Native conversation checkpoint is unconfirmed");
13848
+ }
13849
+ return;
13850
+ }
13294
13851
  // When a resume failure has forced a fresh start, don't persist the stale id
13295
13852
  // back from statusline.json — that would re-arm --continue and re-loop.
13296
13853
  if (this.skipResume)