@songsid/agend 2.3.5-alpha.4 → 2.3.5-beta.1

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 (670) hide show
  1. package/README.md +0 -2
  2. package/README.zh-TW.md +0 -2
  3. package/THIRD_PARTY_NOTICES.md +12 -28
  4. package/dist/agent-cli-instructions.md +1 -1
  5. package/dist/agent-credentials.d.ts +24 -0
  6. package/dist/agent-credentials.js +66 -0
  7. package/dist/agent-credentials.js.map +1 -0
  8. package/dist/agent-endpoint.d.ts +4 -6
  9. package/dist/agent-endpoint.js +15 -12
  10. package/dist/agent-endpoint.js.map +1 -1
  11. package/dist/auth-api.d.ts +0 -21
  12. package/dist/auth-api.js +8 -93
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/session-authorizations.d.ts +30 -4
  15. package/dist/authority/session-authorizations.js +69 -17
  16. package/dist/authority/session-authorizations.js.map +1 -1
  17. package/dist/authority/web-authority.d.ts +2 -12
  18. package/dist/authority/web-authority.js +4 -25
  19. package/dist/authority/web-authority.js.map +1 -1
  20. package/dist/backend/antigravity.js +3 -1
  21. package/dist/backend/antigravity.js.map +1 -1
  22. package/dist/backend/claude-code.d.ts +12 -15
  23. package/dist/backend/claude-code.js +133 -152
  24. package/dist/backend/claude-code.js.map +1 -1
  25. package/dist/backend/codex-metadata.d.ts +14 -0
  26. package/dist/backend/codex-metadata.js +45 -0
  27. package/dist/backend/codex-metadata.js.map +1 -1
  28. package/dist/backend/codex-package.d.ts +2 -5
  29. package/dist/backend/codex-package.js +5 -1
  30. package/dist/backend/codex-package.js.map +1 -1
  31. package/dist/backend/codex-quota-history.d.ts +7 -0
  32. package/dist/backend/codex-quota-history.js +72 -0
  33. package/dist/backend/codex-quota-history.js.map +1 -0
  34. package/dist/backend/codex-session-lookup.d.ts +6 -0
  35. package/dist/backend/codex-session-lookup.js +1 -1
  36. package/dist/backend/codex-session-lookup.js.map +1 -1
  37. package/dist/backend/codex-speed.d.ts +14 -0
  38. package/dist/backend/codex-speed.js +35 -0
  39. package/dist/backend/codex-speed.js.map +1 -0
  40. package/dist/backend/codex.d.ts +116 -136
  41. package/dist/backend/codex.js +912 -942
  42. package/dist/backend/codex.js.map +1 -1
  43. package/dist/backend/grok-weekly-reset.d.ts +24 -0
  44. package/dist/backend/grok-weekly-reset.js +95 -0
  45. package/dist/backend/grok-weekly-reset.js.map +1 -0
  46. package/dist/backend/grok.d.ts +30 -3
  47. package/dist/backend/grok.js +301 -76
  48. package/dist/backend/grok.js.map +1 -1
  49. package/dist/backend/kiro.d.ts +3 -0
  50. package/dist/backend/kiro.js +50 -2
  51. package/dist/backend/kiro.js.map +1 -1
  52. package/dist/backend/muse.js +5 -3
  53. package/dist/backend/muse.js.map +1 -1
  54. package/dist/backend/types.d.ts +15 -8
  55. package/dist/backend/types.js +3 -0
  56. package/dist/backend/types.js.map +1 -1
  57. package/dist/bundled-tmux-guard.d.ts +21 -0
  58. package/dist/bundled-tmux-guard.js +96 -0
  59. package/dist/bundled-tmux-guard.js.map +1 -0
  60. package/dist/cache-ledger.d.ts +95 -0
  61. package/dist/cache-ledger.js +276 -4
  62. package/dist/cache-ledger.js.map +1 -1
  63. package/dist/cache-service.d.ts +5 -0
  64. package/dist/cache-service.js +16 -4
  65. package/dist/cache-service.js.map +1 -1
  66. package/dist/channel/adapters/discord.js +11 -3
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +51 -0
  69. package/dist/channel/adapters/telegram.js +235 -17
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/ipc-bridge.d.ts +0 -2
  72. package/dist/channel/mcp-tools.js +3 -3
  73. package/dist/channel/mcp-tools.js.map +1 -1
  74. package/dist/channel/types.d.ts +2 -0
  75. package/dist/classic-channel-manager.d.ts +2 -0
  76. package/dist/classic-channel-manager.js +5 -1
  77. package/dist/classic-channel-manager.js.map +1 -1
  78. package/dist/cli.js +14 -10
  79. package/dist/cli.js.map +1 -1
  80. package/dist/config-validator.js +30 -71
  81. package/dist/config-validator.js.map +1 -1
  82. package/dist/config.js +3 -17
  83. package/dist/config.js.map +1 -1
  84. package/dist/cross-instance-notice.d.ts +10 -0
  85. package/dist/cross-instance-notice.js +67 -5
  86. package/dist/cross-instance-notice.js.map +1 -1
  87. package/dist/daemon.d.ts +70 -68
  88. package/dist/daemon.js +774 -846
  89. package/dist/daemon.js.map +1 -1
  90. package/dist/dashboard-permissions.d.ts +8 -0
  91. package/dist/dashboard-permissions.js +16 -0
  92. package/dist/dashboard-permissions.js.map +1 -1
  93. package/dist/delivery-consumed-watch.d.ts +26 -16
  94. package/dist/delivery-consumed-watch.js +31 -26
  95. package/dist/delivery-consumed-watch.js.map +1 -1
  96. package/dist/delivery-outbox.d.ts +0 -5
  97. package/dist/delivery-outbox.js +4 -32
  98. package/dist/delivery-outbox.js.map +1 -1
  99. package/dist/delivery-reconciliation.d.ts +42 -2
  100. package/dist/delivery-reconciliation.js +164 -2
  101. package/dist/delivery-reconciliation.js.map +1 -1
  102. package/dist/export-import.d.ts +7 -0
  103. package/dist/export-import.js +44 -10
  104. package/dist/export-import.js.map +1 -1
  105. package/dist/fleet-context.d.ts +4 -2
  106. package/dist/fleet-level-config.d.ts +1 -1
  107. package/dist/fleet-level-config.js +0 -2
  108. package/dist/fleet-level-config.js.map +1 -1
  109. package/dist/fleet-manager.d.ts +108 -245
  110. package/dist/fleet-manager.js +656 -2028
  111. package/dist/fleet-manager.js.map +1 -1
  112. package/dist/general-instance.d.ts +0 -7
  113. package/dist/general-instance.js +3 -20
  114. package/dist/general-instance.js.map +1 -1
  115. package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
  116. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  117. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  118. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  119. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +9 -3
  120. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  121. package/dist/instance-config-impact.js +6 -7
  122. package/dist/instance-config-impact.js.map +1 -1
  123. package/dist/instance-lifecycle.d.ts +19 -23
  124. package/dist/instance-lifecycle.js +216 -383
  125. package/dist/instance-lifecycle.js.map +1 -1
  126. package/dist/instance-topic-delete.d.ts +77 -0
  127. package/dist/instance-topic-delete.js +123 -0
  128. package/dist/instance-topic-delete.js.map +1 -0
  129. package/dist/instructions.d.ts +1 -3
  130. package/dist/instructions.js +1 -7
  131. package/dist/instructions.js.map +1 -1
  132. package/dist/kiro-engine-status.d.ts +11 -0
  133. package/dist/kiro-engine-status.js +6 -0
  134. package/dist/kiro-engine-status.js.map +1 -1
  135. package/dist/locale.js +63 -86
  136. package/dist/locale.js.map +1 -1
  137. package/dist/log-service.d.ts +28 -0
  138. package/dist/log-service.js +29 -0
  139. package/dist/log-service.js.map +1 -1
  140. package/dist/log-steps.d.ts +4 -3
  141. package/dist/log-steps.js +32 -7
  142. package/dist/log-steps.js.map +1 -1
  143. package/dist/logger.d.ts +16 -2
  144. package/dist/logger.js +346 -66
  145. package/dist/logger.js.map +1 -1
  146. package/dist/login-flows.d.ts +7 -0
  147. package/dist/login-flows.js +5 -2
  148. package/dist/login-flows.js.map +1 -1
  149. package/dist/login-manager.js +25 -4
  150. package/dist/login-manager.js.map +1 -1
  151. package/dist/missed-offline.d.ts +25 -0
  152. package/dist/missed-offline.js +79 -0
  153. package/dist/missed-offline.js.map +1 -0
  154. package/dist/muse-usage-relay.d.ts +2 -0
  155. package/dist/muse-usage-relay.js +5 -3
  156. package/dist/muse-usage-relay.js.map +1 -1
  157. package/dist/needs-you-hub.d.ts +6 -3
  158. package/dist/needs-you-hub.js +31 -9
  159. package/dist/needs-you-hub.js.map +1 -1
  160. package/dist/needs-you.d.ts +20 -18
  161. package/dist/needs-you.js +18 -14
  162. package/dist/needs-you.js.map +1 -1
  163. package/dist/office-share-page.js +1 -1
  164. package/dist/office-share-page.js.map +1 -1
  165. package/dist/outbound-handlers.d.ts +2 -16
  166. package/dist/outbound-handlers.js +10 -72
  167. package/dist/outbound-handlers.js.map +1 -1
  168. package/dist/outbound-schemas.d.ts +0 -38
  169. package/dist/outbound-schemas.js +5 -14
  170. package/dist/outbound-schemas.js.map +1 -1
  171. package/dist/pane-quota-history.d.ts +7 -0
  172. package/dist/pane-quota-history.js +47 -0
  173. package/dist/pane-quota-history.js.map +1 -0
  174. package/dist/platform-general.d.ts +46 -0
  175. package/dist/platform-general.js +91 -0
  176. package/dist/platform-general.js.map +1 -0
  177. package/dist/public-web-gateway.js +0 -9
  178. package/dist/public-web-gateway.js.map +1 -1
  179. package/dist/public-web-link.d.ts +0 -11
  180. package/dist/public-web-link.js +0 -13
  181. package/dist/public-web-link.js.map +1 -1
  182. package/dist/queued-handoffs.d.ts +33 -0
  183. package/dist/queued-handoffs.js +75 -0
  184. package/dist/queued-handoffs.js.map +1 -0
  185. package/dist/quickstart-api.js +5 -9
  186. package/dist/quickstart-api.js.map +1 -1
  187. package/dist/quickstart.d.ts +8 -1
  188. package/dist/quickstart.js +42 -4
  189. package/dist/quickstart.js.map +1 -1
  190. package/dist/quota-notice-config.d.ts +22 -0
  191. package/dist/quota-notice-config.js +41 -0
  192. package/dist/quota-notice-config.js.map +1 -0
  193. package/dist/quota-notice.d.ts +7 -0
  194. package/dist/quota-notice.js +40 -0
  195. package/dist/quota-notice.js.map +1 -1
  196. package/dist/reply-buttons-controller.js +10 -6
  197. package/dist/reply-buttons-controller.js.map +1 -1
  198. package/dist/reply-polls.d.ts +2 -2
  199. package/dist/reply-polls.js +3 -3
  200. package/dist/reply-polls.js.map +1 -1
  201. package/dist/scheduler/db.d.ts +2 -1
  202. package/dist/scheduler/db.js +23 -9
  203. package/dist/scheduler/db.js.map +1 -1
  204. package/dist/scheduler/task-board.d.ts +9 -0
  205. package/dist/scheduler/task-board.js +19 -0
  206. package/dist/scheduler/task-board.js.map +1 -1
  207. package/dist/settings-api.d.ts +13 -5
  208. package/dist/settings-api.js +26 -96
  209. package/dist/settings-api.js.map +1 -1
  210. package/dist/settings-baseline.d.ts +0 -1
  211. package/dist/settings-baseline.js +1 -1
  212. package/dist/settings-baseline.js.map +1 -1
  213. package/dist/settings-change.js +23 -28
  214. package/dist/settings-change.js.map +1 -1
  215. package/dist/settings-confirmation.d.ts +1 -16
  216. package/dist/settings-confirmation.js +9 -47
  217. package/dist/settings-confirmation.js.map +1 -1
  218. package/dist/settings-effect.d.ts +0 -4
  219. package/dist/settings-effect.js +1 -58
  220. package/dist/settings-effect.js.map +1 -1
  221. package/dist/settings-http-confirmation.d.ts +0 -4
  222. package/dist/settings-http-confirmation.js +3 -31
  223. package/dist/settings-http-confirmation.js.map +1 -1
  224. package/dist/settings-instance-patch.js +28 -1
  225. package/dist/settings-instance-patch.js.map +1 -1
  226. package/dist/settings-request-capability.d.ts +0 -2
  227. package/dist/settings-request-capability.js +0 -4
  228. package/dist/settings-request-capability.js.map +1 -1
  229. package/dist/settings-transaction.d.ts +0 -4
  230. package/dist/settings-transaction.js +0 -9
  231. package/dist/settings-transaction.js.map +1 -1
  232. package/dist/setup-form.js +3 -6
  233. package/dist/setup-form.js.map +1 -1
  234. package/dist/setup-i18n.js +0 -3
  235. package/dist/setup-i18n.js.map +1 -1
  236. package/dist/task-board-tool.d.ts +11 -0
  237. package/dist/task-board-tool.js +44 -0
  238. package/dist/task-board-tool.js.map +1 -1
  239. package/dist/task-board-waker.d.ts +6 -0
  240. package/dist/task-board-waker.js +4 -0
  241. package/dist/task-board-waker.js.map +1 -1
  242. package/dist/tmux-control.d.ts +1 -2
  243. package/dist/tmux-control.js.map +1 -1
  244. package/dist/tmux-manager.d.ts +1 -2
  245. package/dist/tmux-manager.js.map +1 -1
  246. package/dist/tool-permissions.d.ts +5 -2
  247. package/dist/tool-permissions.js +7 -7
  248. package/dist/tool-permissions.js.map +1 -1
  249. package/dist/tool-progress.d.ts +2 -0
  250. package/dist/tool-progress.js +19 -3
  251. package/dist/tool-progress.js.map +1 -1
  252. package/dist/topic-commands.js +4 -3
  253. package/dist/topic-commands.js.map +1 -1
  254. package/dist/transcript-delta-reader.d.ts +1 -1
  255. package/dist/transcript-delta-reader.js +1 -1
  256. package/dist/transcript-delta-reader.js.map +1 -1
  257. package/dist/transcript-monitor.d.ts +20 -3
  258. package/dist/transcript-monitor.js +92 -24
  259. package/dist/transcript-monitor.js.map +1 -1
  260. package/dist/transcript-sources.d.ts +0 -5
  261. package/dist/transcript-sources.js +79 -8
  262. package/dist/transcript-sources.js.map +1 -1
  263. package/dist/types.d.ts +3 -23
  264. package/dist/ui/app.html +8 -1
  265. package/dist/ui/backend-signin.js +21 -36
  266. package/dist/ui/chat-md-strings.js +9 -0
  267. package/dist/ui/chat-store.js +2 -13
  268. package/dist/ui/chat-strings.js +2 -50
  269. package/dist/ui/chat-thread.js +53 -3
  270. package/dist/ui/fleet-task-card.js +3 -3
  271. package/dist/ui/fleet-task-common.js +10 -0
  272. package/dist/ui/fleet-tasks.js +7 -5
  273. package/dist/ui/fleet-usage.js +90 -27
  274. package/dist/ui/panel-chat.js +37 -66
  275. package/dist/ui/panel-details.js +1 -2
  276. package/dist/ui/panel-fleet.js +88 -107
  277. package/dist/ui/panel-needs.js +21 -163
  278. package/dist/ui/panel-notices.js +4 -3
  279. package/dist/ui/panel-settings.js +149 -239
  280. package/dist/ui/preview-panel.js +84 -6
  281. package/dist/ui/search-dialog.js +3 -2
  282. package/dist/ui/settings-appearance.js +23 -122
  283. package/dist/ui/settings-catalog.js +10 -37
  284. package/dist/ui/settings-codex-copy.js +19 -0
  285. package/dist/ui/settings-confirm.js +1 -3
  286. package/dist/ui/settings-dialogs.js +20 -27
  287. package/dist/ui/settings-guests.js +14 -27
  288. package/dist/ui/settings-model.js +17 -12
  289. package/dist/ui/settings-quota-notice.js +100 -0
  290. package/dist/ui/settings-registry.js +4 -11
  291. package/dist/ui/settings-strings.js +4 -113
  292. package/dist/ui/settings-web-authority.js +13 -15
  293. package/dist/ui/shared/app-i18n.js +2 -2
  294. package/dist/ui/shared/app-needs.js +1 -1
  295. package/dist/ui/shared/app-route.js +2 -6
  296. package/dist/ui/shared/app-shell.js +18 -23
  297. package/dist/ui/shared/app-store.js +2 -3
  298. package/dist/ui/shared/app-stream.js +1 -3
  299. package/dist/ui/shared/app.css +40 -1312
  300. package/dist/ui/shared/app.js +2 -5
  301. package/dist/ui/shared/header-tools.js +4 -6
  302. package/dist/ui/shared/instance-nav.js +7 -34
  303. package/dist/ui/shared/instance-switch.js +3 -4
  304. package/dist/ui/shared/loading.css +37 -0
  305. package/dist/ui/shared/panel-view.js +77 -4
  306. package/dist/ui/shared/signin.js +6 -39
  307. package/dist/ui/shared/tokens.css +0 -1
  308. package/dist/ui/shared/ui-dialog.js +2 -2
  309. package/dist/ui/shared/ui-menu.js +10 -30
  310. package/dist/ui/shared/ui-states.js +14 -3
  311. package/dist/ui/shared/usage-dialog.js +18 -1
  312. package/dist/ui/shared/vendor/vendor.json +2 -19
  313. package/dist/ui/shared/view-history.css +17 -0
  314. package/dist/ui/shared/view-history.js +438 -0
  315. package/dist/ui/shared/view-strings.js +16 -8
  316. package/dist/ui/signin.html +5 -2
  317. package/dist/unattended-install.d.ts +11 -2
  318. package/dist/unattended-install.js +39 -5
  319. package/dist/unattended-install.js.map +1 -1
  320. package/dist/usage/capacity.d.ts +69 -0
  321. package/dist/usage/capacity.js +119 -0
  322. package/dist/usage/capacity.js.map +1 -0
  323. package/dist/usage/i18n-keys.d.ts +1 -1
  324. package/dist/usage/i18n-keys.js +3 -0
  325. package/dist/usage/i18n-keys.js.map +1 -1
  326. package/dist/usage/profile-timeline.d.ts +16 -0
  327. package/dist/usage/profile-timeline.js +58 -0
  328. package/dist/usage/profile-timeline.js.map +1 -0
  329. package/dist/usage/providers.d.ts +5 -0
  330. package/dist/usage/providers.js +5 -0
  331. package/dist/usage/providers.js.map +1 -1
  332. package/dist/usage/quota-history.d.ts +43 -0
  333. package/dist/usage/quota-history.js +168 -0
  334. package/dist/usage/quota-history.js.map +1 -0
  335. package/dist/usage/quota-notices.d.ts +38 -0
  336. package/dist/usage/quota-notices.js +160 -0
  337. package/dist/usage/quota-notices.js.map +1 -0
  338. package/dist/usage/usage-api.d.ts +32 -0
  339. package/dist/usage/usage-api.js +96 -1
  340. package/dist/usage/usage-api.js.map +1 -1
  341. package/dist/usage-report.d.ts +25 -1
  342. package/dist/usage-report.js +55 -4
  343. package/dist/usage-report.js.map +1 -1
  344. package/dist/usage-rollup.d.ts +6 -1
  345. package/dist/usage-rollup.js +16 -4
  346. package/dist/usage-rollup.js.map +1 -1
  347. package/dist/view-api.d.ts +16 -0
  348. package/dist/view-api.js +100 -16
  349. package/dist/view-api.js.map +1 -1
  350. package/dist/view-history.d.ts +84 -0
  351. package/dist/view-history.js +116 -0
  352. package/dist/view-history.js.map +1 -0
  353. package/dist/web-api.d.ts +6 -38
  354. package/dist/web-api.js +20 -255
  355. package/dist/web-api.js.map +1 -1
  356. package/dist/web-chat-history.d.ts +2 -19
  357. package/dist/web-chat-history.js +3 -72
  358. package/dist/web-chat-history.js.map +1 -1
  359. package/dist/web-only-fleet.d.ts +0 -20
  360. package/dist/web-only-fleet.js +0 -21
  361. package/dist/web-only-fleet.js.map +1 -1
  362. package/dist/web-session-authorization.d.ts +13 -2
  363. package/dist/web-session-authorization.js +10 -3
  364. package/dist/web-session-authorization.js.map +1 -1
  365. package/dist/web-session.d.ts +0 -7
  366. package/dist/web-session.js +0 -2
  367. package/dist/web-session.js.map +1 -1
  368. package/dist/web-shell-routes.d.ts +1 -4
  369. package/dist/web-shell-routes.js +2 -7
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/launcher/private-home.cjs +1 -1
  372. package/package.json +3 -9
  373. package/dist/agent-runtime-api.d.ts +0 -4
  374. package/dist/agent-runtime-api.js +0 -59
  375. package/dist/agent-runtime-api.js.map +0 -1
  376. package/dist/agent-runtime-change.d.ts +0 -74
  377. package/dist/agent-runtime-change.js +0 -102
  378. package/dist/agent-runtime-change.js.map +0 -1
  379. package/dist/backend/grok-model-cache.d.ts +0 -7
  380. package/dist/backend/grok-model-cache.js +0 -65
  381. package/dist/backend/grok-model-cache.js.map +0 -1
  382. package/dist/backend/native-pane.d.ts +0 -9
  383. package/dist/backend/native-pane.js +0 -59
  384. package/dist/backend/native-pane.js.map +0 -1
  385. package/dist/fleet-windows-runtime.d.ts +0 -97
  386. package/dist/fleet-windows-runtime.js +0 -449
  387. package/dist/fleet-windows-runtime.js.map +0 -1
  388. package/dist/general-binding.d.ts +0 -85
  389. package/dist/general-binding.js +0 -298
  390. package/dist/general-binding.js.map +0 -1
  391. package/dist/general-host-owner.d.ts +0 -8
  392. package/dist/general-host-owner.js +0 -26
  393. package/dist/general-host-owner.js.map +0 -1
  394. package/dist/general-role.d.ts +0 -5
  395. package/dist/general-role.js +0 -26
  396. package/dist/general-role.js.map +0 -1
  397. package/dist/local-pane-endpoint.d.ts +0 -46
  398. package/dist/local-pane-endpoint.js +0 -189
  399. package/dist/local-pane-endpoint.js.map +0 -1
  400. package/dist/local-pane-launcher.d.ts +0 -2
  401. package/dist/local-pane-launcher.js +0 -38
  402. package/dist/local-pane-launcher.js.map +0 -1
  403. package/dist/local-pane-stage.ps1 +0 -101
  404. package/dist/model-catalog-defaults.d.ts +0 -14
  405. package/dist/model-catalog-defaults.js +0 -20
  406. package/dist/model-catalog-defaults.js.map +0 -1
  407. package/dist/native-agent-admission.d.ts +0 -7
  408. package/dist/native-agent-admission.js +0 -26
  409. package/dist/native-agent-admission.js.map +0 -1
  410. package/dist/native-daemon-client.d.ts +0 -13
  411. package/dist/native-daemon-client.js +0 -28
  412. package/dist/native-daemon-client.js.map +0 -1
  413. package/dist/native-instance-api.d.ts +0 -14
  414. package/dist/native-instance-api.js +0 -205
  415. package/dist/native-instance-api.js.map +0 -1
  416. package/dist/native-instance-input.d.ts +0 -84
  417. package/dist/native-instance-input.js +0 -54
  418. package/dist/native-instance-input.js.map +0 -1
  419. package/dist/native-instance-registry.d.ts +0 -32
  420. package/dist/native-instance-registry.js +0 -105
  421. package/dist/native-instance-registry.js.map +0 -1
  422. package/dist/native-mcp.d.ts +0 -40
  423. package/dist/native-mcp.js +0 -278
  424. package/dist/native-mcp.js.map +0 -1
  425. package/dist/native-model-catalog.d.ts +0 -5
  426. package/dist/native-model-catalog.js +0 -76
  427. package/dist/native-model-catalog.js.map +0 -1
  428. package/dist/native-pane-crash-budget.d.ts +0 -20
  429. package/dist/native-pane-crash-budget.js +0 -60
  430. package/dist/native-pane-crash-budget.js.map +0 -1
  431. package/dist/native-pane-create.d.ts +0 -26
  432. package/dist/native-pane-create.js +0 -53
  433. package/dist/native-pane-create.js.map +0 -1
  434. package/dist/native-pane-lifecycle.d.ts +0 -65
  435. package/dist/native-pane-lifecycle.js +0 -230
  436. package/dist/native-pane-lifecycle.js.map +0 -1
  437. package/dist/native-pane-manager.d.ts +0 -59
  438. package/dist/native-pane-manager.js +0 -584
  439. package/dist/native-pane-manager.js.map +0 -1
  440. package/dist/native-pane-output-log.d.ts +0 -15
  441. package/dist/native-pane-output-log.js +0 -108
  442. package/dist/native-pane-output-log.js.map +0 -1
  443. package/dist/native-pane-reconciliation.d.ts +0 -4
  444. package/dist/native-pane-reconciliation.js +0 -40
  445. package/dist/native-pane-reconciliation.js.map +0 -1
  446. package/dist/native-pane-recovery.d.ts +0 -12
  447. package/dist/native-pane-recovery.js +0 -10
  448. package/dist/native-pane-recovery.js.map +0 -1
  449. package/dist/native-pane-tools.d.ts +0 -2
  450. package/dist/native-pane-tools.js +0 -14
  451. package/dist/native-pane-tools.js.map +0 -1
  452. package/dist/pane-port.d.ts +0 -118
  453. package/dist/pane-port.js +0 -2
  454. package/dist/pane-port.js.map +0 -1
  455. package/dist/runtime-api.d.ts +0 -8
  456. package/dist/runtime-api.js +0 -171
  457. package/dist/runtime-api.js.map +0 -1
  458. package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +0 -21
  459. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +0 -2
  460. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +0 -19
  461. package/dist/runtime-assets/@xterm/addon-serialize/package.json +0 -29
  462. package/dist/runtime-assets/@xterm/headless/LICENSE +0 -21
  463. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +0 -2
  464. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +0 -46
  465. package/dist/runtime-assets/@xterm/headless/package.json +0 -29
  466. package/dist/runtime-assets/node-pty/CONPTY-LICENSE +0 -21
  467. package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +0 -503
  468. package/dist/runtime-assets/node-pty/LICENSE +0 -69
  469. package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +0 -16
  470. package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +0 -47
  471. package/dist/runtime-assets/node-pty/lib/index.js +0 -52
  472. package/dist/runtime-assets/node-pty/lib/interfaces.js +0 -7
  473. package/dist/runtime-assets/node-pty/lib/shared/conout.js +0 -11
  474. package/dist/runtime-assets/node-pty/lib/terminal.js +0 -190
  475. package/dist/runtime-assets/node-pty/lib/types.js +0 -7
  476. package/dist/runtime-assets/node-pty/lib/unixTerminal.js +0 -346
  477. package/dist/runtime-assets/node-pty/lib/utils.js +0 -39
  478. package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +0 -125
  479. package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +0 -320
  480. package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +0 -199
  481. package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +0 -22
  482. package/dist/runtime-assets/node-pty/package.json +0 -64
  483. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  484. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  485. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  486. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  487. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  488. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  489. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  490. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  491. package/dist/runtime-build-info.d.ts +0 -9
  492. package/dist/runtime-build-info.js +0 -57
  493. package/dist/runtime-build-info.js.map +0 -1
  494. package/dist/runtime-bundle.d.ts +0 -19
  495. package/dist/runtime-bundle.js +0 -156
  496. package/dist/runtime-bundle.js.map +0 -1
  497. package/dist/runtime-cli.d.ts +0 -7
  498. package/dist/runtime-cli.js +0 -152
  499. package/dist/runtime-cli.js.map +0 -1
  500. package/dist/runtime-client-store.d.ts +0 -68
  501. package/dist/runtime-client-store.js +0 -344
  502. package/dist/runtime-client-store.js.map +0 -1
  503. package/dist/runtime-client.d.ts +0 -112
  504. package/dist/runtime-client.js +0 -736
  505. package/dist/runtime-client.js.map +0 -1
  506. package/dist/runtime-connection.d.ts +0 -22
  507. package/dist/runtime-connection.js +0 -93
  508. package/dist/runtime-connection.js.map +0 -1
  509. package/dist/runtime-coordinator.d.ts +0 -91
  510. package/dist/runtime-coordinator.js +0 -911
  511. package/dist/runtime-coordinator.js.map +0 -1
  512. package/dist/runtime-general-protocol.d.ts +0 -22
  513. package/dist/runtime-general-protocol.js +0 -57
  514. package/dist/runtime-general-protocol.js.map +0 -1
  515. package/dist/runtime-general-workspace.d.ts +0 -17
  516. package/dist/runtime-general-workspace.js +0 -213
  517. package/dist/runtime-general-workspace.js.map +0 -1
  518. package/dist/runtime-pane-assets.d.ts +0 -17
  519. package/dist/runtime-pane-assets.js +0 -23
  520. package/dist/runtime-pane-assets.js.map +0 -1
  521. package/dist/runtime-pane-boot.d.ts +0 -9
  522. package/dist/runtime-pane-boot.js +0 -47
  523. package/dist/runtime-pane-boot.js.map +0 -1
  524. package/dist/runtime-pane-codex-index.d.ts +0 -1
  525. package/dist/runtime-pane-codex-index.js +0 -47
  526. package/dist/runtime-pane-codex-index.js.map +0 -1
  527. package/dist/runtime-pane-config.d.ts +0 -16
  528. package/dist/runtime-pane-config.js +0 -54
  529. package/dist/runtime-pane-config.js.map +0 -1
  530. package/dist/runtime-pane-host.d.ts +0 -44
  531. package/dist/runtime-pane-host.js +0 -424
  532. package/dist/runtime-pane-host.js.map +0 -1
  533. package/dist/runtime-pane-job-owner.d.ts +0 -1
  534. package/dist/runtime-pane-job-owner.js +0 -29
  535. package/dist/runtime-pane-job-owner.js.map +0 -1
  536. package/dist/runtime-pane-job.ps1 +0 -116
  537. package/dist/runtime-pane-link.d.ts +0 -40
  538. package/dist/runtime-pane-link.js +0 -246
  539. package/dist/runtime-pane-link.js.map +0 -1
  540. package/dist/runtime-pane-mcp-protocol.d.ts +0 -32
  541. package/dist/runtime-pane-mcp-protocol.js +0 -39
  542. package/dist/runtime-pane-mcp-protocol.js.map +0 -1
  543. package/dist/runtime-pane-mcp-relay.d.ts +0 -17
  544. package/dist/runtime-pane-mcp-relay.js +0 -70
  545. package/dist/runtime-pane-mcp-relay.js.map +0 -1
  546. package/dist/runtime-pane-mcp.d.ts +0 -51
  547. package/dist/runtime-pane-mcp.js +0 -150
  548. package/dist/runtime-pane-mcp.js.map +0 -1
  549. package/dist/runtime-pane-native.d.ts +0 -45
  550. package/dist/runtime-pane-native.js +0 -65
  551. package/dist/runtime-pane-native.js.map +0 -1
  552. package/dist/runtime-pane-output.d.ts +0 -19
  553. package/dist/runtime-pane-output.js +0 -53
  554. package/dist/runtime-pane-output.js.map +0 -1
  555. package/dist/runtime-pane-port.d.ts +0 -139
  556. package/dist/runtime-pane-port.js +0 -558
  557. package/dist/runtime-pane-port.js.map +0 -1
  558. package/dist/runtime-pane-private.d.ts +0 -14
  559. package/dist/runtime-pane-private.js +0 -51
  560. package/dist/runtime-pane-private.js.map +0 -1
  561. package/dist/runtime-pane-private.ps1 +0 -14
  562. package/dist/runtime-pane-process.d.ts +0 -4
  563. package/dist/runtime-pane-process.js +0 -108
  564. package/dist/runtime-pane-process.js.map +0 -1
  565. package/dist/runtime-pane-protocol.d.ts +0 -172
  566. package/dist/runtime-pane-protocol.js +0 -195
  567. package/dist/runtime-pane-protocol.js.map +0 -1
  568. package/dist/runtime-pane-screen.d.ts +0 -19
  569. package/dist/runtime-pane-screen.js +0 -76
  570. package/dist/runtime-pane-screen.js.map +0 -1
  571. package/dist/runtime-pane-service.d.ts +0 -11
  572. package/dist/runtime-pane-service.js +0 -221
  573. package/dist/runtime-pane-service.js.map +0 -1
  574. package/dist/runtime-pane-stdio-protocol.d.ts +0 -54
  575. package/dist/runtime-pane-stdio-protocol.js +0 -94
  576. package/dist/runtime-pane-stdio-protocol.js.map +0 -1
  577. package/dist/runtime-pane-stdio.d.ts +0 -3
  578. package/dist/runtime-pane-stdio.js +0 -123
  579. package/dist/runtime-pane-stdio.js.map +0 -1
  580. package/dist/runtime-pane-store.d.ts +0 -33
  581. package/dist/runtime-pane-store.js +0 -164
  582. package/dist/runtime-pane-store.js.map +0 -1
  583. package/dist/runtime-pane-transcript-protocol.d.ts +0 -43
  584. package/dist/runtime-pane-transcript-protocol.js +0 -58
  585. package/dist/runtime-pane-transcript-protocol.js.map +0 -1
  586. package/dist/runtime-pane-transcript.d.ts +0 -45
  587. package/dist/runtime-pane-transcript.js +0 -605
  588. package/dist/runtime-pane-transcript.js.map +0 -1
  589. package/dist/runtime-pane-worker.d.ts +0 -1
  590. package/dist/runtime-pane-worker.js +0 -154
  591. package/dist/runtime-pane-worker.js.map +0 -1
  592. package/dist/runtime-protocol.d.ts +0 -162
  593. package/dist/runtime-protocol.js +0 -37
  594. package/dist/runtime-protocol.js.map +0 -1
  595. package/dist/runtime-switches.d.ts +0 -79
  596. package/dist/runtime-switches.js +0 -90
  597. package/dist/runtime-switches.js.map +0 -1
  598. package/dist/runtime-transcript-delta-reader.d.ts +0 -23
  599. package/dist/runtime-transcript-delta-reader.js +0 -74
  600. package/dist/runtime-transcript-delta-reader.js.map +0 -1
  601. package/dist/runtime-transcript-source.d.ts +0 -36
  602. package/dist/runtime-transcript-source.js +0 -189
  603. package/dist/runtime-transcript-source.js.map +0 -1
  604. package/dist/runtime-windows-bridge.d.ts +0 -41
  605. package/dist/runtime-windows-bridge.js +0 -157
  606. package/dist/runtime-windows-bridge.js.map +0 -1
  607. package/dist/settings-route-lifetime.d.ts +0 -14
  608. package/dist/settings-route-lifetime.js +0 -48
  609. package/dist/settings-route-lifetime.js.map +0 -1
  610. package/dist/tmux-pane-host.d.ts +0 -26
  611. package/dist/tmux-pane-host.js +0 -25
  612. package/dist/tmux-pane-host.js.map +0 -1
  613. package/dist/transcript-needles.d.ts +0 -35
  614. package/dist/transcript-needles.js +0 -158
  615. package/dist/transcript-needles.js.map +0 -1
  616. package/dist/transcript-records.d.ts +0 -21
  617. package/dist/transcript-records.js +0 -140
  618. package/dist/transcript-records.js.map +0 -1
  619. package/dist/ui/agent-proposals.js +0 -90
  620. package/dist/ui/agent-runtime-settings.js +0 -67
  621. package/dist/ui/agent-runtime.js +0 -124
  622. package/dist/ui/cli-picker.js +0 -42
  623. package/dist/ui/general-location.js +0 -71
  624. package/dist/ui/native-instance-settings.js +0 -44
  625. package/dist/ui/native-pane-recovery.js +0 -45
  626. package/dist/ui/panel-runtimes.js +0 -334
  627. package/dist/ui/panel-windows-agent.js +0 -86
  628. package/dist/ui/panel-windows-cli.js +0 -179
  629. package/dist/ui/provider-marks.js +0 -83
  630. package/dist/ui/runtime-catalog.js +0 -55
  631. package/dist/ui/settings-agent-filters.js +0 -94
  632. package/dist/ui/settings-layout.js +0 -245
  633. package/dist/ui/settings-outside.js +0 -176
  634. package/dist/ui/settings-shortcuts.js +0 -65
  635. package/dist/ui/shared/machine-name.js +0 -13
  636. package/dist/ui/shared/qrcode.module.js +0 -2237
  637. package/dist/ui/shared/settings-width.js +0 -31
  638. package/dist/ui/shared/vendor/qrcode-generator.LICENSE +0 -27
  639. package/dist/ui/windows-agents-nav.js +0 -2
  640. package/dist/ui/windows-cli-catalog.js +0 -108
  641. package/dist/web-remote-access.d.ts +0 -152
  642. package/dist/web-remote-access.js +0 -368
  643. package/dist/web-remote-access.js.map +0 -1
  644. package/dist/windows-agents.d.ts +0 -126
  645. package/dist/windows-agents.js +0 -490
  646. package/dist/windows-agents.js.map +0 -1
  647. package/dist/windows-cli-api.d.ts +0 -23
  648. package/dist/windows-cli-api.js +0 -159
  649. package/dist/windows-cli-api.js.map +0 -1
  650. package/dist/windows-cli-providers.d.ts +0 -115
  651. package/dist/windows-cli-providers.js +0 -429
  652. package/dist/windows-cli-providers.js.map +0 -1
  653. package/dist/windows-cli-worker.d.ts +0 -59
  654. package/dist/windows-cli-worker.js +0 -479
  655. package/dist/windows-cli-worker.js.map +0 -1
  656. package/dist/windows-cli.d.ts +0 -116
  657. package/dist/windows-cli.js +0 -407
  658. package/dist/windows-cli.js.map +0 -1
  659. package/dist/windows-native-mcp.d.ts +0 -19
  660. package/dist/windows-native-mcp.js +0 -143
  661. package/dist/windows-native-mcp.js.map +0 -1
  662. package/dist/windows-pane-mcp.d.ts +0 -26
  663. package/dist/windows-pane-mcp.js +0 -187
  664. package/dist/windows-pane-mcp.js.map +0 -1
  665. package/templates/runtime-assets/conpty-LICENSE +0 -21
  666. package/templates/runtime-assets/conpty-NOTICE.md +0 -503
  667. package/templates/runtime-assets/local-pane-stage.ps1 +0 -101
  668. package/templates/runtime-assets/runtime-pane-job.ps1 +0 -116
  669. package/templates/runtime-assets/runtime-pane-private.ps1 +0 -14
  670. package/templates/runtime-assets/xterm-LICENSE +0 -21
package/dist/daemon.js CHANGED
@@ -1,5 +1,5 @@
1
- import { nativePaneToolRefusal } from "./native-pane-tools.js";
2
- import { generalByName } from "./general-instance.js";
1
+ import { quotaNoticeProblem } from "./quota-notice-config.js";
2
+ import { quotaHistoryCount, quotaAfterInput } from "./pane-quota-history.js";
3
3
  import { dialogScreenText } from "./dialog-screen-text.js";
4
4
  import { readActiveKiroModel } from "./backend/kiro.js";
5
5
  import { dashboardAccessOf } from "./dashboard-permissions.js";
@@ -22,7 +22,7 @@ import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, Sessio
22
22
  import { mcpServerState } from "./mcp-liveness.js";
23
23
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
24
24
  import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
25
- import { resolveTmuxLogicalSize } from "./tmux-manager.js";
25
+ import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
26
26
  import { TranscriptMonitor } from "./transcript-monitor.js";
27
27
  import { createTranscriptSource } from "./transcript-sources.js";
28
28
  import { TranscriptTurnLedger } from "./transcript-turns.js";
@@ -35,14 +35,14 @@ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
35
35
  import { readAgentFile } from "./agent-file-read.js";
36
36
  import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
37
37
  import { LOGIN_FLOWS } from "./login-flows.js";
38
- import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
38
+ import { ProgressAccumulator, isSearchProgram, summarizeProgress } from "./tool-progress.js";
39
39
  import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
40
40
  import { isCrossInstanceVisibility } from "./cross-instance-notice.js";
41
41
  import { ContextGuardian } from "./context-guardian.js";
42
42
  import { IpcServer } from "./channel/ipc-bridge.js";
43
43
  import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
44
44
  import { MessageBus } from "./channel/message-bus.js";
45
- import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
45
+ import { shellQuote, KiroCompatibilityError, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
46
46
  import { getTmuxSession } from "./config.js";
47
47
  import { routeToolCall } from "./channel/tool-router.js";
48
48
  import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
@@ -50,7 +50,6 @@ import { HangDetector } from "./hang-detector.js";
50
50
  import { writeSecretFile } from "./secret-file.js";
51
51
  import { PaneWriteLock } from "./pane-write-lock.js";
52
52
  import { answerRemoteCard } from "./remote-card-answer.js";
53
- import { tmuxPaneHost } from "./tmux-pane-host.js";
54
53
  import { buildFleetInstructions } from "./instructions.js";
55
54
  import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
56
55
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
@@ -58,13 +57,13 @@ import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedT
58
57
  import { TurnReplyGuard, formatReplyDropDetection, turnExpectsPersonReply } from "./turn-reply-guard.js";
59
58
  import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT_PAUSE } from "./backend/claude-transcript-errors.js";
60
59
  import { t } from "./locale.js";
61
- import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
60
+ import { claudeResetInstant, claudeResetTime, quotaPausedLine } from "./quota-notice.js";
62
61
  import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
63
- import { reconcileNativePaneTarget } from "./native-pane-reconciliation.js";
64
62
  import { deliveryContentDigest } from "./delivery-outbox.js";
65
63
  import { consumedWatches } from "./delivery-consumed-watch.js";
66
64
  import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
67
- import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
65
+ import { markerConsumed, scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
66
+ import { QUEUED_HANDOFF_MAX_REDELIVERIES, QueuedHandoffStore } from "./queued-handoffs.js";
68
67
  import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
69
68
  import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
70
69
  import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
@@ -155,6 +154,8 @@ export const HANDOFF_RECHECK_MS = 1_000;
155
154
  export const WAIT_STOP_POLL_MS = 250;
156
155
  /** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
157
156
  export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
157
+ /** #3030: a queued hand-off's watch outlives a long turn (the record outlives the watch in any case). */
158
+ export const QUEUED_HANDOFF_WATCH_LIFETIME_MS = 12 * 60 * 60_000;
158
159
  /**
159
160
  * An instance publishing more than this many execution-state edges inside the
160
161
  * window is flapping; its edges stop being logged one by one (#1116). One
@@ -347,7 +348,8 @@ export function shellCommandLabel(command) {
347
348
  if (!program)
348
349
  return "";
349
350
  const isBareWord = (t) => !!t && /^[A-Za-z][A-Za-z0-9_-]*$/.test(t);
350
- const label = isBareWord(tokens[1]) ? `${program} ${tokens[1]}` : program;
351
+ // A search program's next word is what it searches for, not a subcommand: `grep <token>` keeps only `grep`.
352
+ const label = isBareWord(tokens[1]) && !isSearchProgram(program) ? `${program} ${tokens[1]}` : program;
351
353
  return label.length > 40 ? `${label.slice(0, 39)}…` : label;
352
354
  }
353
355
  export class PaneStateMachine {
@@ -1116,7 +1118,6 @@ export class Daemon extends EventEmitter {
1116
1118
  spawnGate;
1117
1119
  stormWindow;
1118
1120
  backendOutage;
1119
- paneTransport;
1120
1121
  /** #1314: fleet-directory fallbacks already reported to the topic (once per daemon, per field and path). */
1121
1122
  promptFileFallbackNoticed = new Set();
1122
1123
  /** Identity of this live Daemon object; changes on object stop/restart. */
@@ -1585,6 +1586,12 @@ export class Daemon extends EventEmitter {
1585
1586
  pendingWhatsNew = null;
1586
1587
  pasteQueueDepth = 0;
1587
1588
  /** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
1589
+ /** #3030: people's messages a CLI never took (or that a restart left unverifiable), told once per batch. */
1590
+ unsettledHandoffs = { "not-taken": [], unverifiable: [] };
1591
+ unsettledHandoffsTimer = null;
1592
+ /** #3030: since when (monotonic) the CLI has been idle without a break; null while it is not. */
1593
+ instanceIdleSinceMono = null;
1594
+ queuedHandoffStore = null;
1588
1595
  cancelledHumans = [];
1589
1596
  cancelledHumansTimer = null;
1590
1597
  firstDeliveryDelay = new FirstDeliveryDelay();
@@ -1789,6 +1796,80 @@ export class Daemon extends EventEmitter {
1789
1796
  // still booting. Its new tmux window starts empty before resume restores the
1790
1797
  // old transcript, so pre-ready scans must not lower this seeded baseline.
1791
1798
  seededErrorCountBaselines = new Map();
1799
+ quotaReplay = null;
1800
+ paneQuotaPatterns() {
1801
+ if (!["codex", "grok"].includes(this.backend?.binaryName ?? ""))
1802
+ return [];
1803
+ const patterns = this.backend?.getErrorPatterns?.() ?? [];
1804
+ return this.backend?.binaryName === "codex" ? patterns.filter(ep => Daemon.isCodexUsageLimitPause(ep)) : patterns.filter(ep => ep.type === "quota");
1805
+ }
1806
+ async prepareQuotaReplayHistory(reuseWindow) {
1807
+ this.quotaReplay = null;
1808
+ const backend = this.backend, owner = this.tmux, window = owner?.getWindowId?.();
1809
+ const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, attempt = this.launchAttempt;
1810
+ const current = () => backend === this.backend && owner === this.tmux && window === owner?.getWindowId?.()
1811
+ && spawn === this.spawnGeneration && fence === this.launchFenceEpoch && attempt === this.launchAttempt;
1812
+ const replay = backend?.quotaReplayHistory?.();
1813
+ if (replay)
1814
+ this.quotaReplay = { ...replay, spawn, fence, attempt };
1815
+ // A Grok wake keeps its exited pane. The replacement's first ready pane is not history proof.
1816
+ if (reuseWindow && backend?.resumesPreservedQuotaHistory?.()) {
1817
+ let oldPane;
1818
+ try {
1819
+ oldPane = await owner.capturePane();
1820
+ }
1821
+ catch {
1822
+ return;
1823
+ }
1824
+ this.startupAdmission?.();
1825
+ if (!current())
1826
+ return;
1827
+ const patterns = this.paneQuotaPatterns();
1828
+ const messages = oldPane.split(/\r?\n/).filter(row => patterns.some(ep => ep.pattern.test(row)));
1829
+ this.quotaReplay = { messages, current: () => true, spawn, fence, attempt };
1830
+ }
1831
+ }
1832
+ /** Before admission, this launch may absorb ONLY rows positively known before its resume. */
1833
+ takeQuotaReplayBaseline(pane) {
1834
+ const history = this.quotaReplay;
1835
+ if (!history)
1836
+ return;
1837
+ this.quotaReplay = null;
1838
+ if (history.spawn !== this.spawnGeneration || history.fence !== this.launchFenceEpoch || history.attempt !== this.launchAttempt || !history.current())
1839
+ return;
1840
+ for (const ep of this.paneQuotaPatterns()) {
1841
+ const count = Math.min(Daemon.countOccurrences(ep.pattern, pane), quotaHistoryCount(pane, ep.pattern, history.messages));
1842
+ if (!count)
1843
+ continue;
1844
+ const key = Daemon.errorPatternKey(ep);
1845
+ this.lastErrorCount.set(key, Math.max(this.lastErrorCount.get(key) ?? 0, count));
1846
+ this.logger.debug({ backend: this.backend?.binaryName, count, evidence: "before-resume" }, "Quota rows restored from history — not a new incident");
1847
+ }
1848
+ }
1849
+ // Submission makes captured rows history even when no monitor reported them yet. Keep this acknowledged input's
1850
+ // marker as well: a new error can replace an old row in the viewport without increasing the total match count.
1851
+ quotaInput = null;
1852
+ commitPreInputQuotaHistory(evidence) {
1853
+ const history = evidence?.quotaHistory;
1854
+ if (!history || history.spawn !== this.spawnGeneration || history.fence !== this.launchFenceEpoch || history.attempt !== this.launchAttempt
1855
+ || history.tmux !== this.tmux || history.window !== this.tmux?.getWindowId?.() || this.deliveryWritesStopping)
1856
+ return;
1857
+ for (const row of history.counts)
1858
+ this.lastErrorCount.set(row.key, row.count);
1859
+ this.quotaInput = history.marker && history.occurrences === 0 ? { history, reported: new Map() } : null;
1860
+ }
1861
+ quotaOccurrencesAfterInput(pane, ep) {
1862
+ if (!(this.backend?.binaryName === "codex" && Daemon.isCodexUsageLimitPause(ep)
1863
+ || this.backend?.binaryName === "grok" && ep.type === "quota"))
1864
+ return null;
1865
+ const input = this.quotaInput, history = input?.history;
1866
+ if (!input || !history || history.spawn !== this.spawnGeneration || history.fence !== this.launchFenceEpoch
1867
+ || history.attempt !== this.launchAttempt || history.tmux !== this.tmux || history.window !== this.tmux?.getWindowId?.()
1868
+ || this.deliveryWritesStopping)
1869
+ return null;
1870
+ const suffix = quotaAfterInput(pane, history.marker);
1871
+ return suffix === null ? null : { input, count: Daemon.countOccurrences(ep.pattern, suffix) };
1872
+ }
1792
1873
  lastDetectedErrorType = null;
1793
1874
  /**
1794
1875
  * #2822: Claude's per-model usage limit (`You've reached your <model> limit.`), known only from Claude's own record of
@@ -1810,12 +1891,46 @@ export class Daemon extends EventEmitter {
1810
1891
  static CREDIT_BALANCE_LOW = CLAUDE_CREDIT_BALANCE_LOW;
1811
1892
  /** #2834 (#816): Claude's usage-limit auto-continue note, as it records it (`system` / `informational`). */
1812
1893
  static USAGE_LIMIT_PAUSE = CLAUDE_USAGE_LIMIT_PAUSE;
1813
- /** #2834 (#816): Claude paused itself until its usage window opens — the same notice the screen pattern gave. */
1814
- noteClaudeInformational(text) {
1815
- const match = Daemon.USAGE_LIMIT_PAUSE.exec(text.trim());
1894
+ /** Claude's note that its usage window opened again ("Usage limit reset · continuing automatically"). */
1895
+ static USAGE_LIMIT_RESET = /^Usage limit reset\b/i;
1896
+ /** Repeats of one usage pause ("reached again" every few minutes) collapse into the first notice for this long. */
1897
+ static USAGE_PAUSE_NOTICE_WINDOW_MS = 30 * 60_000;
1898
+ /** The usage-pause notice told last: when (monotonic), Claude's words for the reset, and that reset (wall clock). */
1899
+ usagePauseNotice = null;
1900
+ /**
1901
+ * #2834 (#816): Claude paused itself until its usage window opens — the same notice the screen pattern gave. `at`: when
1902
+ * Claude wrote it (its record's timestamp). One notice per pause: a reset time already past is not this pause (an old
1903
+ * note), and its repeats ("reached again") within the window — or naming the same reset, until it comes — say nothing
1904
+ * more. Claude's own "Usage limit reset" ends it.
1905
+ */
1906
+ /** #3186 review: the lifecycle's second opinion dropped the notice (usage has capacity): nothing was told. */
1907
+ clearUsagePauseNotice() { this.usagePauseNotice = null; }
1908
+ noteClaudeInformational(text, at = null) {
1909
+ const line = text.trim();
1910
+ if (Daemon.USAGE_LIMIT_RESET.test(line)) {
1911
+ this.usagePauseNotice = null;
1912
+ return;
1913
+ }
1914
+ const match = Daemon.USAGE_LIMIT_PAUSE.exec(line);
1816
1915
  if (!match)
1817
1916
  return;
1818
1917
  const when = match[1].trim();
1918
+ const wallNow = Date.now(), now = performance.now();
1919
+ const resetAt = claudeResetInstant(when, at ?? wallNow);
1920
+ if (resetAt !== null && resetAt <= wallNow) {
1921
+ this.logger.debug({ when, writtenAt: at }, "Claude usage-limit note names a reset already past — an old note, not told");
1922
+ return;
1923
+ }
1924
+ // A pause told earlier is this one while its own reset has not come (unknown: within the window): its repeats —
1925
+ // "reached again", or a relative time drifting ("in 3h", "in 2h 55m") — within the window, or naming the same reset.
1926
+ // Once its reset has passed, a later note is a new pause even without Claude's "Usage limit reset" (#3186 review).
1927
+ const told = this.usagePauseNotice;
1928
+ const toldOpen = !!told && (told.resetAt === null || wallNow < told.resetAt);
1929
+ if (told && toldOpen && (now - told.at < Daemon.USAGE_PAUSE_NOTICE_WINDOW_MS || (told.when === when && told.resetAt !== null))) {
1930
+ this.logger.debug({ when }, "Claude usage-limit note repeats a pause already told — collapsed");
1931
+ return;
1932
+ }
1933
+ this.usagePauseNotice = { at: now, when, resetAt };
1819
1934
  this.emit("pty_error", { name: this.name, type: "quota", action: "notify", verifyQuota: true, skipCooldown: true, skipRecoveryWait: true,
1820
1935
  message: `Claude Code usage limit reached — paused; continuing automatically ${when}`,
1821
1936
  notice: quotaPausedLine("Claude", "usage", claudeResetTime(when)) });
@@ -1948,87 +2063,9 @@ export class Daemon extends EventEmitter {
1948
2063
  this.errorRecoveryDeadlineAt = 0;
1949
2064
  this.activeErrorPatternKey = null;
1950
2065
  }
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
- }
2029
2066
  constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity, spawnGate, stormWindow,
2030
2067
  /** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
2031
- backendOutage, paneTransport) {
2068
+ backendOutage) {
2032
2069
  super();
2033
2070
  this.name = name;
2034
2071
  this.config = config;
@@ -2040,7 +2077,6 @@ export class Daemon extends EventEmitter {
2040
2077
  this.spawnGate = spawnGate;
2041
2078
  this.stormWindow = stormWindow;
2042
2079
  this.backendOutage = backendOutage;
2043
- this.paneTransport = paneTransport;
2044
2080
  // The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
2045
2081
  // sharing that object would activate cold changes before the normal restart/apply boundary.
2046
2082
  this.config = structuredClone(config);
@@ -2063,8 +2099,7 @@ export class Daemon extends EventEmitter {
2063
2099
  this.messageBus.setLogger(this.logger);
2064
2100
  // General is the dispatcher — it must stay warm to route messages, so it is
2065
2101
  // never auto-paused regardless of auto_pause_after.
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);
2102
+ const isGeneral = config.general_topic === true || name === "general";
2068
2103
  const autoPauseMinutes = isGeneral ? 0 : (typeof config.auto_pause_after === "number" ? config.auto_pause_after : 0); // default: disabled
2069
2104
  this.autoPauseController = new AutoPauseController(Math.max(0, autoPauseMinutes) * 60_000, readLastInboundAt(instanceDir) ?? Date.now());
2070
2105
  // #1209: every real completion clears the in-flight marker, so a
@@ -2076,6 +2111,9 @@ export class Daemon extends EventEmitter {
2076
2111
  this.publishWorkingState("turn_closed");
2077
2112
  };
2078
2113
  }
2114
+ /** Register each issued HTTP credential with the same-process lifecycle owner. */
2115
+ agentCredentialIssuer;
2116
+ setAgentCredentialIssuer(issue) { this.agentCredentialIssuer = issue; }
2079
2117
  /** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
2080
2118
  setDeliveryOutboxPort(port) {
2081
2119
  this.deliveryOutbox = port;
@@ -2123,19 +2161,12 @@ export class Daemon extends EventEmitter {
2123
2161
  return outcome === "begun";
2124
2162
  }
2125
2163
  async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
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");
2164
+ let checkpoint = null;
2165
+ try {
2166
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2131
2167
  }
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
- }
2168
+ catch (err) {
2169
+ this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
2139
2170
  }
2140
2171
  const submissionMode = rawPaste ? "raw_paste" : steer
2141
2172
  ? "steer"
@@ -2151,9 +2182,8 @@ export class Daemon extends EventEmitter {
2151
2182
  backendVersion,
2152
2183
  windowId: windowId ?? this.tmux?.getWindowId() ?? null,
2153
2184
  transcriptPath: checkpoint?.path ?? null,
2154
- transcriptOffset: runtimeTranscript?.cursor.offset ?? checkpoint?.offset ?? null,
2155
- transcriptSessionId: runtimeTranscript?.cursor.sessionId ?? checkpoint?.sessionId ?? null,
2156
- ...(runtimeTranscript ? { runtimeTranscript } : {}),
2185
+ transcriptOffset: checkpoint?.offset ?? null,
2186
+ transcriptSessionId: checkpoint?.sessionId ?? null,
2157
2187
  submissionMode,
2158
2188
  queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
2159
2189
  };
@@ -2196,25 +2226,17 @@ export class Daemon extends EventEmitter {
2196
2226
  const outbox = this.deliveryOutbox;
2197
2227
  if (!checkpoint || !outbox?.markConsumed)
2198
2228
  return;
2199
- if (this.nativePaneLifecycle && !checkpoint.runtime)
2200
- return;
2201
2229
  if (verdict.submissionMode !== "steer" && verdict.submissionMode !== "native_queue_handoff")
2202
2230
  return;
2203
2231
  if (!["claude-code", "codex"].includes(checkpoint.backend))
2204
2232
  return;
2205
2233
  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.
2213
2234
  consumedWatches.start({
2214
2235
  deliveryId: delivery.deliveryId,
2215
2236
  attemptNo: delivery.attemptNo,
2216
2237
  backend: checkpoint.backend,
2217
- ...location,
2238
+ path: checkpoint.path,
2239
+ offset: checkpoint.offset,
2218
2240
  current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch,
2219
2241
  consumed: (via, evidence) => {
2220
2242
  try {
@@ -2228,6 +2250,243 @@ export class Daemon extends EventEmitter {
2228
2250
  logger: this.logger,
2229
2251
  });
2230
2252
  }
2253
+ handoffStore() {
2254
+ return this.queuedHandoffStore ??= new QueuedHandoffStore(this.instanceDir);
2255
+ }
2256
+ /** #3030: the CLI that holds a queued hand-off; a record is settled only by a later one. */
2257
+ handoffHolder() { return `${this.bootId}:${this.spawnGeneration}`; }
2258
+ /**
2259
+ * #3030: before the paste. A hand-off into a busy Claude Code / Codex queue gets its record — the transcript
2260
+ * checkpoint before the paste, the unique leading marker and what a redelivery needs — or, for a redelivery written
2261
+ * as an ordinary idle submission, its record moves its checkpoint here, so a crash right after this paste is still
2262
+ * judged from the transcript that will show it. A write that did not keep a record runs as before.
2263
+ */
2264
+ async recordQueuedHandoff(formatted, handingOff, verdict, durableAttempt, humanVerificationId) {
2265
+ const track = verdict.trackHandoff;
2266
+ if (!track)
2267
+ return;
2268
+ if (!handingOff && !track.id) {
2269
+ // An earlier attempt of this delivery was a hand-off; this one is an idle submission.
2270
+ if (verdict.handoff)
2271
+ this.dropQueuedHandoff(verdict);
2272
+ return;
2273
+ }
2274
+ const backend = this.config.backend ?? this.backend?.binaryName ?? "";
2275
+ if (!["claude-code", "codex"].includes(backend))
2276
+ return;
2277
+ const marker = durableAttempt ? `[agend-delivery-id:${durableAttempt.deliveryId}]`
2278
+ : humanVerificationId ? `[agend-human-id:${humanVerificationId}]` : null;
2279
+ if (!marker || !formatted.startsWith(marker))
2280
+ return;
2281
+ let checkpoint = verdict.transcriptCheckpoint ?? null;
2282
+ if (!checkpoint) {
2283
+ try {
2284
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2285
+ }
2286
+ catch (err) {
2287
+ this.logger.debug({ err }, "Could not checkpoint the transcript before a queued hand-off");
2288
+ }
2289
+ }
2290
+ if (!checkpoint)
2291
+ return; // nothing could judge it later: it runs as before (✅ at the hand-off)
2292
+ const entry = {
2293
+ id: track.id ?? randomUUID(), marker, backend, path: checkpoint.path, offset: checkpoint.offset,
2294
+ content: track.content, meta: track.meta, holder: this.handoffHolder(), createdAt: Date.now(),
2295
+ redeliveries: track.redeliveries ?? 0,
2296
+ };
2297
+ try {
2298
+ const dropped = this.handoffStore().put(entry);
2299
+ if (handingOff)
2300
+ verdict.handoff = entry;
2301
+ for (const old of dropped)
2302
+ this.logger.warn({ handoff: old.id, createdAt: old.createdAt }, "Queued hand-off record dropped: too many on record");
2303
+ }
2304
+ catch (err) {
2305
+ this.logger.warn({ err }, "Could not record a queued hand-off; it runs as before (✅ at the hand-off)");
2306
+ }
2307
+ }
2308
+ /**
2309
+ * #3030: after pushChannelMessage's write. A hand-off on record whose paste went out is watched (✅ when a turn
2310
+ * takes it); one that was never written goes. A redelivery that ended as an ordinary submission is done; one that a
2311
+ * cancel dropped goes too (a cancel is the person's); one stopped before its write stays for the next CLI.
2312
+ */
2313
+ afterTrackedWrite(meta, verdict, sent, deliveryEpoch) {
2314
+ try {
2315
+ // A stale epoch with writes still allowed is a cancel (the person's); a stop fences writes too, and keeps records.
2316
+ const cancelled = !this.isDeliveryEpochCurrent(deliveryEpoch) && !this.deliveryWritesStopping;
2317
+ if (verdict.handoff) {
2318
+ if (sent || verdict.paneWriteStarted)
2319
+ this.watchQueuedHandoff(verdict.handoff);
2320
+ else if (!verdict.trackHandoff?.id || cancelled)
2321
+ this.dropQueuedHandoff(verdict);
2322
+ return;
2323
+ }
2324
+ const id = meta.handoff_redelivery;
2325
+ if (!id)
2326
+ return;
2327
+ if (sent || verdict.paneWriteStarted || cancelled) {
2328
+ this.handoffStore().remove(id);
2329
+ this.logger.info({ handoff: id, sent, proof: verdict.proof ?? null }, "Queued hand-off redelivered as an ordinary submission");
2330
+ }
2331
+ }
2332
+ catch (err) {
2333
+ this.logger.warn({ err }, "Could not update the queued hand-off record after its write");
2334
+ }
2335
+ }
2336
+ redeliveryStillOurs(meta) {
2337
+ const holder = this.handoffHolder();
2338
+ if (meta.handoff_holder !== holder)
2339
+ return false;
2340
+ try {
2341
+ return this.handoffStore().get(meta.handoff_redelivery)?.holder === holder;
2342
+ }
2343
+ catch {
2344
+ return false;
2345
+ }
2346
+ }
2347
+ dropQueuedHandoff(verdict) {
2348
+ const entry = verdict.handoff;
2349
+ verdict.handoff = undefined;
2350
+ if (!entry)
2351
+ return;
2352
+ try {
2353
+ this.handoffStore().remove(entry.id);
2354
+ }
2355
+ catch (err) {
2356
+ this.logger.warn({ err, handoff: entry.id }, "Could not remove a queued hand-off record");
2357
+ }
2358
+ }
2359
+ /**
2360
+ * #3030: the hand-off's CLI is the current one: watch its transcript for the turn that takes it (✅ then), and for an
2361
+ * idle CLI that never did (reported). A stop ends the watch; the record stays for the next CLI to settle.
2362
+ */
2363
+ watchQueuedHandoff(entry) {
2364
+ const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
2365
+ const started = consumedWatches.start({
2366
+ deliveryId: `handoff:${entry.id}`,
2367
+ attemptNo: entry.redeliveries,
2368
+ backend: entry.backend,
2369
+ path: entry.path,
2370
+ offset: entry.offset,
2371
+ marker: entry.marker,
2372
+ // A cancel or stop moves the epoch: the watch ends quietly (a cancel cleared the record, a stop leaves it).
2373
+ current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch
2374
+ && epoch === this.deliveryEpoch,
2375
+ consumed: (via) => this.settleQueuedHandoff(entry, "taken", `transcript:${via}`),
2376
+ idleForMs: () => this.instanceIdleSinceMono === null ? null : performance.now() - this.instanceIdleSinceMono,
2377
+ notTaken: () => this.settleQueuedHandoff(entry, "not-taken", "transcript:absent-while-idle"),
2378
+ // A queued message waits for the running turn, however long that is.
2379
+ lifetimeMs: QUEUED_HANDOFF_WATCH_LIFETIME_MS,
2380
+ logger: this.logger,
2381
+ });
2382
+ if (!started) {
2383
+ // No watch (the fleet's cap): ✅ as before; the record still lets a restart judge it.
2384
+ const status = channelStatus(entry.meta);
2385
+ if (status)
2386
+ this.emit("message_confirmed", status);
2387
+ }
2388
+ }
2389
+ /** #3030: a record's end — taken (✅), or never taken / not judgeable (❌, told once per batch). */
2390
+ settleQueuedHandoff(entry, outcome, proof) {
2391
+ try {
2392
+ this.handoffStore().remove(entry.id);
2393
+ }
2394
+ catch (err) {
2395
+ this.logger.warn({ err, handoff: entry.id }, "Could not remove a settled queued hand-off record");
2396
+ }
2397
+ const meta = entry.meta;
2398
+ const status = channelStatus(meta);
2399
+ if (outcome === "taken") {
2400
+ this.logger.info({ handoff: entry.id, proof, redeliveries: entry.redeliveries }, "Queued hand-off taken by the CLI");
2401
+ if (status)
2402
+ this.emit("message_confirmed", status);
2403
+ return;
2404
+ }
2405
+ this.logger.warn({ handoff: entry.id, outcome, proof, redeliveries: entry.redeliveries }, "Queued hand-off was not taken by the CLI");
2406
+ if (meta.from_instance) {
2407
+ this.reportCrossInstanceDeliveryFailure(meta, { reached: true, phase: "handoff-not-consumed", proof: outcome === "not-taken" ? "not-taken" : "unverifiable" });
2408
+ return;
2409
+ }
2410
+ if (!status)
2411
+ return;
2412
+ this.unsettledHandoffs[outcome].push({ chatId: status.chatId, messageId: status.messageId, ...(status.threadId ? { threadId: status.threadId } : {}) });
2413
+ if (this.unsettledHandoffsTimer)
2414
+ clearTimeout(this.unsettledHandoffsTimer);
2415
+ this.unsettledHandoffsTimer = setTimeout(() => {
2416
+ this.unsettledHandoffsTimer = null;
2417
+ for (const reason of ["not-taken", "unverifiable"]) {
2418
+ const messages = this.unsettledHandoffs[reason].splice(0);
2419
+ if (messages.length)
2420
+ this.emit("human_handoffs_unsettled", { reason, messages });
2421
+ }
2422
+ }, CANCELLED_HUMANS_NOTICE_MS);
2423
+ this.unsettledHandoffsTimer.unref?.();
2424
+ }
2425
+ /**
2426
+ * #3030: right after a CLI came up — the records left by CLIs that are gone. Their queue went with them, so each is
2427
+ * judged by its own transcript from its checkpoint: taken there (✅, nothing pasted); proven not taken (the whole
2428
+ * delta read, the marker absent or only enqueued) → redelivered once, with the same marker, through the ordinary
2429
+ * queue; anything that cannot be judged → never pasted, reported. Ownership is evidence: never pasted twice.
2430
+ */
2431
+ async reconcileQueuedHandoffs() {
2432
+ let entries;
2433
+ try {
2434
+ entries = this.handoffStore().list().filter(e => e.holder !== this.handoffHolder());
2435
+ }
2436
+ catch (err) {
2437
+ this.logger.warn({ err }, "Could not read the queued hand-off records");
2438
+ return;
2439
+ }
2440
+ if (!entries.length)
2441
+ return;
2442
+ // Captured before any await: a reconciliation that outlives its CLI can never claim the next CLI's holder.
2443
+ const holder = this.handoffHolder();
2444
+ const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
2445
+ const fresh = () => spawn === this.spawnGeneration && fence === this.launchFenceEpoch && epoch === this.deliveryEpoch
2446
+ && !this.deliveryWritesStopping;
2447
+ let redelivered = 0, taken = 0, unverifiable = 0;
2448
+ for (const entry of entries) {
2449
+ const kind = await scanTranscriptForDeliveryMarker(entry.path, entry.offset, entry.backend, { marker: entry.marker });
2450
+ // This CLI is gone already, or a cancel came: the next CLI settles them (a cancel cleared them).
2451
+ if (!fresh())
2452
+ return;
2453
+ // …and the record is still the one that was read (another reconciliation or a cancel did not change it meanwhile).
2454
+ let current;
2455
+ try {
2456
+ current = this.handoffStore().get(entry.id);
2457
+ }
2458
+ catch {
2459
+ current = undefined;
2460
+ }
2461
+ if (!current || current.holder !== entry.holder || current.redeliveries !== entry.redeliveries)
2462
+ continue;
2463
+ if (markerConsumed(kind)) {
2464
+ taken++;
2465
+ this.settleQueuedHandoff(entry, "taken", `restart-transcript:${kind}`);
2466
+ continue;
2467
+ }
2468
+ if (kind === "unavailable" || entry.redeliveries >= QUEUED_HANDOFF_MAX_REDELIVERIES) {
2469
+ unverifiable++;
2470
+ this.settleQueuedHandoff(entry, "unverifiable", kind === "unavailable" ? "restart-transcript:unavailable" : "redelivery-limit");
2471
+ continue;
2472
+ }
2473
+ // Proven not taken: the CLI that queued it is gone and its transcript, read to the end, never took it.
2474
+ const next = { ...entry, holder, redeliveries: entry.redeliveries + 1 };
2475
+ try {
2476
+ this.handoffStore().put(next);
2477
+ }
2478
+ catch (err) {
2479
+ this.logger.warn({ err, handoff: entry.id }, "Could not update a queued hand-off record; not redelivered now");
2480
+ continue;
2481
+ }
2482
+ redelivered++;
2483
+ // The person's receipt was settled by the first write: the redelivery carries its marker, not its receipt binding.
2484
+ const { human_receipt_id: _r, human_owner_key: _o, human_daemon_owner: _d, human_wire_sha256: _w, ...meta } = entry.meta;
2485
+ this.pushChannelMessage(entry.content, { ...meta, handoff_redelivery: entry.id,
2486
+ handoff_redeliveries: String(next.redeliveries), handoff_holder: next.holder }, undefined, epoch);
2487
+ }
2488
+ this.logger.info({ instance: this.name, redelivered, taken, unverifiable, proof: "transcript" }, "Queued hand-offs left by the previous CLI settled");
2489
+ }
2231
2490
  finishDurableSubmission(delivery, verdict) {
2232
2491
  // Legacy backends may accept a pane write without enough pane evidence to
2233
2492
  // claim submission. Keep that row inspectable instead of recording a false
@@ -2295,34 +2554,24 @@ export class Daemon extends EventEmitter {
2295
2554
  const spawn = this.spawnGeneration;
2296
2555
  const fence = this.launchFenceEpoch;
2297
2556
  let last = "unavailable";
2298
- const native = !!checkpoint.runtime || !!this.nativePaneLifecycle;
2299
- let outcome = native ? "uncertain" : "delivered";
2300
- let evidence = native ? "runtime-transcript-unconfirmed" : OUTPUT_EDGE_ONLY_EVIDENCE;
2557
+ let outcome = "delivered";
2558
+ let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
2301
2559
  // #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
2302
2560
  // in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
2303
2561
  const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
2304
2562
  let found = false;
2305
2563
  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;
2309
2564
  for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
2310
2565
  if (this.deliveryWritesStopping)
2311
2566
  return; // reconciliation owns the row now
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
- }
2567
+ const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
2319
2568
  if (look === "user" || look === "absorbed" || look === "queued") {
2320
2569
  found = true;
2321
2570
  outcome = "delivered";
2322
2571
  evidence = transcriptMarkerEvidence(look, !!rawPaste);
2323
2572
  break;
2324
2573
  }
2325
- last = look === "pending" ? "unavailable" : look;
2574
+ last = look;
2326
2575
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
2327
2576
  last = "unavailable";
2328
2577
  break;
@@ -2334,13 +2583,13 @@ export class Daemon extends EventEmitter {
2334
2583
  // `uncertain`: re-check the same fixed epochs after the last await, before settling (#1207 P2).
2335
2584
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch)
2336
2585
  last = "unavailable";
2337
- if (last === "no-match" && !found) {
2586
+ if (last === "no-match" && evidence === OUTPUT_EDGE_ONLY_EVIDENCE) {
2338
2587
  outcome = "uncertain";
2339
2588
  evidence = "unverifiable-no-transcript-marker";
2340
2589
  }
2341
2590
  }
2342
2591
  catch (err) {
2343
- this.logger.debug({ err }, "Transcript proof unavailable — preserving the conservative outcome");
2592
+ this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
2344
2593
  }
2345
2594
  if (rawPaste && !found) {
2346
2595
  outcome = "uncertain";
@@ -2527,130 +2776,128 @@ export class Daemon extends EventEmitter {
2527
2776
  }
2528
2777
  }
2529
2778
  catch { /* corrupt/missing — ignore */ }
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
- }
2779
+ // 1. IPC server — bridge between MCP server (Claude's child) and daemon
2780
+ const sockPath = join(this.instanceDir, "channel.sock");
2781
+ this.ipcServer = new IpcServer(sockPath, this.logger);
2782
+ // Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
2783
+ // Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
2784
+ let ipcListening = false;
2785
+ this.ipcServer.on("error", (err) => {
2786
+ if (!ipcListening)
2787
+ return; // startup errors handled by listen() rejection
2788
+ this.logger.error({ err, name: this.name }, "IPC server error");
2789
+ this.emit("error", err);
2790
+ });
2791
+ await this.ipcServer.listen();
2792
+ this.startupAdmission?.();
2793
+ ipcListening = true;
2794
+ // Permanent IPC dispatcher: routes responses to pending requests by type+id key
2795
+ this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
2796
+ // IPC message relay: when daemon wants to push a channel message to Claude,
2797
+ // it broadcasts to all IPC clients (the MCP server is one of them).
2798
+ // When MCP server sends a tool_call, daemon handles it via the messageBus.
2799
+ this.ipcServer.on("message", (msg, socket) => {
2800
+ if (msg.type === "tool_call") {
2801
+ // MCP server forwarding a Claude tool call (reply, react, edit, download)
2802
+ this.handleToolCall(msg, socket);
2803
+ }
2804
+ else if (msg.type === "mcp_ready") {
2805
+ this.handleMcpReady(msg, socket);
2806
+ }
2807
+ else if (msg.type === "pong") {
2808
+ this.handleMcpPong(msg, socket);
2809
+ }
2810
+ else if (msg.type === "query_sessions") {
2811
+ // Fleet manager asks for all registered session names (catches sessions
2812
+ // that sent mcp_ready before fleet manager connected).
2813
+ const sessions = [];
2814
+ for (const [s, sessionName] of this.socketSessionNames) {
2815
+ if (!s.destroyed && sessionName !== this.name) {
2816
+ // Individual mcp_ready for initial registration path
2817
+ this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
2818
+ sessions.push(sessionName);
2572
2819
  }
2573
- // Batch response for prune path
2574
- this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
2575
2820
  }
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(() => {
2585
- this.startupAdmission?.();
2586
- if (this.startupAborted)
2587
- throw Error("Native launch was superseded");
2821
+ // Batch response for prune path
2822
+ this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
2823
+ }
2824
+ else if (msg.type === "fleet_inbound") {
2825
+ // Fleet manager routed a message to us (topic mode)
2826
+ const meta = msg.meta;
2827
+ const targetSession = msg.targetSession;
2828
+ const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
2829
+ void this.wake().then(() => {
2830
+ this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
2831
+ }).catch(err => {
2832
+ this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
2833
+ this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
2588
2834
  });
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");
2602
- this.startupAdmission?.();
2603
- if (this.startupAborted || this.deliveryWritesStopping || !this.nativePaneLifecycle.current())
2604
- throw Error("Native launch was superseded");
2605
- }
2606
2835
  }
2607
- finally {
2608
- this.endSpawn();
2836
+ else if (msg.type === "raw_paste") {
2837
+ this.handleRawPasteIpcMessage(msg);
2609
2838
  }
2610
- }
2611
- else {
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
- }
2839
+ else if (msg.type === "config_update") {
2840
+ this.applyConfigUpdate(msg.config);
2632
2841
  }
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?.();
2842
+ else if (msg.type === "steer") {
2843
+ const meta = (msg.meta ?? {});
2844
+ this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
2639
2845
  }
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"));
2846
+ else if (msg.type === "btw") {
2847
+ const meta = (msg.meta ?? {});
2848
+ this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
2849
+ }
2850
+ else if (msg.type === "fleet_schedule_trigger") {
2851
+ const payload = msg.payload;
2852
+ const meta = msg.meta;
2853
+ const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
2854
+ void this.wake().then(() => this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch)).catch(err => {
2855
+ this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
2856
+ });
2857
+ }
2858
+ else if (msg.type === "query_instance_state") {
2859
+ void this.respondToInstanceStateQuery(msg, socket);
2860
+ }
2861
+ });
2862
+ // 2. Tmux — ensure session, create window if not alive
2863
+ await TmuxManager.ensureSession(this.tmuxSessionName);
2864
+ this.startupAdmission?.();
2865
+ this.tmux = new TmuxManager(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
2866
+ // Strategy A: always start fresh Claude window (MCP server has no reconnection)
2867
+ // Kill any existing window from previous run
2868
+ const windowIdFile = join(this.instanceDir, "window-id");
2869
+ if (existsSync(windowIdFile)) {
2870
+ const savedId = readFileSync(windowIdFile, "utf-8").trim();
2871
+ if (savedId) {
2872
+ const oldTmux = new TmuxManager(this.tmuxSessionName, savedId);
2873
+ if (await oldTmux.isWindowAlive()) {
2874
+ this.startupAdmission?.();
2875
+ await this.checkpointSessionId();
2876
+ this.startupAdmission?.();
2877
+ await oldTmux.killWindow();
2878
+ this.startupAdmission?.();
2879
+ this.logger.info({ savedId }, "Killed old tmux window for fresh start");
2644
2880
  }
2645
- catch { /* may not exist */ }
2646
2881
  }
2647
2882
  }
2648
- if (nativeDisposition)
2649
- this.isNewSession = nativeDisposition === "fresh";
2883
+ const resumed = await this.spawnClaudeWindow();
2884
+ this.startupAdmission?.();
2885
+ this.isNewSession = !resumed;
2886
+ if (!resumed) {
2887
+ await this.injectSnapshotMessage(this.startupAdmission);
2888
+ this.startupAdmission?.();
2889
+ }
2890
+ else {
2891
+ // Clean up stale snapshot file — resume restored full context, snapshot not needed
2892
+ try {
2893
+ unlinkSync(join(this.instanceDir, "rotation-state.json"));
2894
+ }
2895
+ catch { /* may not exist */ }
2896
+ }
2650
2897
  // #1209: conditional continuation for the turn interrupted by the last
2651
2898
  // restart, if the gates allow. Detached; the gate
2652
2899
  // consumes its one-shot marker first and never throws into boot.
2653
- const legacyResume = this.nativePaneLifecycle ? Promise.resolve("unknown") : this.maybeResumeInterruptedTurn().catch(() => "unknown");
2900
+ const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
2654
2901
  const recoveryOwner = JSON.stringify(this.interactionOwner());
2655
2902
  void Promise.allSettled([legacyResume]).then(() => {
2656
2903
  if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
@@ -2669,7 +2916,7 @@ export class Daemon extends EventEmitter {
2669
2916
  // (statusline transcript); codex/kiro/opencode read their CLI's own
2670
2917
  // conversation store via a pluggable source. Backends with no known
2671
2918
  // source stay inert exactly as before.
2672
- this.transcriptMonitor = this.buildTranscriptMonitor(this.nativeTranscriptSource ?? createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2919
+ this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2673
2920
  const transcriptMonitor = this.transcriptMonitor;
2674
2921
  const transcriptFence = this.launchFenceEpoch;
2675
2922
  // Baseline in the isolate before this daemon accepts new work. A
@@ -2705,7 +2952,7 @@ export class Daemon extends EventEmitter {
2705
2952
  this.publishActivity(null);
2706
2953
  });
2707
2954
  this.transcriptMonitor.on("api_error", (text, status, _at, errorKind) => this.noteClaudeApiError(text, status, errorKind));
2708
- this.transcriptMonitor.on("system_informational", (text) => this.noteClaudeInformational(text));
2955
+ this.transcriptMonitor.on("system_informational", (text, at) => this.noteClaudeInformational(text, at));
2709
2956
  this.transcriptMonitor.on("user_prompt", () => this.noteClaudePrompt());
2710
2957
  this.transcriptMonitor.on("assistant_text", (text) => {
2711
2958
  this.noteClaudeOutput();
@@ -2725,15 +2972,13 @@ export class Daemon extends EventEmitter {
2725
2972
  if (hangConfig?.enabled !== false) {
2726
2973
  this.hangDetector = new HangDetector();
2727
2974
  }
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
- }
2975
+ // 8. Context guardian
2976
+ const statusFile = join(this.instanceDir, "statusline.json");
2977
+ this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
2978
+ this.guardian.startWatching();
2979
+ this.guardian.on("status_update", () => {
2980
+ this.saveSessionId();
2981
+ });
2737
2982
  // Context rotation removed: all CLI backends have built-in auto-compact.
2738
2983
  // Crash recovery (health check + respawn with snapshot) is retained below.
2739
2984
  }
@@ -2750,6 +2995,8 @@ export class Daemon extends EventEmitter {
2750
2995
  this.startErrorMonitor();
2751
2996
  }
2752
2997
  this.startInstanceStateMonitor();
2998
+ // #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
2999
+ void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
2753
3000
  this.logger.info(`${this.name} ready`);
2754
3001
  }
2755
3002
  /**
@@ -2784,7 +3031,7 @@ export class Daemon extends EventEmitter {
2784
3031
  this.socketPids.delete(socket);
2785
3032
  // Notify fleet manager so it can clean up sessionRegistry
2786
3033
  if (sessionName !== this.name) {
2787
- this.broadcastFleetMessage({ type: "session_disconnected", sessionName });
3034
+ this.ipcServer?.broadcast({ type: "session_disconnected", sessionName });
2788
3035
  }
2789
3036
  });
2790
3037
  }
@@ -2800,16 +3047,11 @@ export class Daemon extends EventEmitter {
2800
3047
  // sub-agent's server may have left a dead pid in the slot).
2801
3048
  this.noteMcpProofOfLife("mcp_ready", typeof msg.pid === "number" ? msg.pid : undefined);
2802
3049
  }
2803
- this.broadcastFleetMessage({ type: "mcp_ready", sessionName });
3050
+ this.ipcServer?.broadcast({ type: "mcp_ready", sessionName });
2804
3051
  }
2805
3052
  checkMcpServerAlive() {
2806
3053
  if (this.isPaused)
2807
3054
  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
- }
2813
3055
  const status = mcpServerState(this.instanceDir);
2814
3056
  if (status.state === "alive") {
2815
3057
  this.mcpServedGeneration = this.spawnGeneration;
@@ -2893,8 +3135,6 @@ export class Daemon extends EventEmitter {
2893
3135
  * server from a third-party one, and failed the whole launch for either.
2894
3136
  */
2895
3137
  checkFleetMcpNeverConnected() {
2896
- if (this.nativePaneLifecycle)
2897
- return; // Native helper presence owns this observation.
2898
3138
  if (this.config.agent_mode === "cli")
2899
3139
  return; // no MCP server by design
2900
3140
  if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
@@ -3105,8 +3345,6 @@ export class Daemon extends EventEmitter {
3105
3345
  * proof that tools are being served, whatever channel.mcp.pid says.
3106
3346
  */
3107
3347
  mcpServerAlive() {
3108
- if (this.nativePaneLifecycle)
3109
- return this.nativePaneLifecycle.mcpProof() === "alive" ? { alive: true, source: "connection" } : { alive: false, source: "none" };
3110
3348
  const status = mcpServerState(this.instanceDir);
3111
3349
  if (status.state === "alive")
3112
3350
  return { alive: true, source: "pid", pid: status.pid };
@@ -3171,7 +3409,7 @@ export class Daemon extends EventEmitter {
3171
3409
  this.mcpDeathNotifiedForPid = null;
3172
3410
  this.mcpDeathDeferredForPid = null;
3173
3411
  this.clearMcpRestartRequest();
3174
- if (!this.nativePaneLifecycle && pid && mcpServerState(this.instanceDir).state !== "alive")
3412
+ if (pid && mcpServerState(this.instanceDir).state !== "alive")
3175
3413
  this.repairMcpPidSlot(pid, source);
3176
3414
  }
3177
3415
  emitMcpRecovered(source, pid) {
@@ -3218,26 +3456,6 @@ export class Daemon extends EventEmitter {
3218
3456
  // ended this instance's health loop because scheduleNext() sat after the
3219
3457
  // throwing code. Catch, log, and keep checking.
3220
3458
  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
- }
3241
3459
  // Instance directory removed externally (e.g. `rm -rf ~/.agend/instances/<name>`).
3242
3460
  // Stop the loop permanently — otherwise every tick triggers a respawn, whose
3243
3461
  // writeRotationSnapshot fails with ENOENT and gets caught as "Failed to respawn",
@@ -3253,8 +3471,6 @@ export class Daemon extends EventEmitter {
3253
3471
  scheduleNext();
3254
3472
  return;
3255
3473
  }
3256
- // The CLI owns the MCP server process, so the daemon can only observe it.
3257
- measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
3258
3474
  // Human-readable backend label for logs (e.g. "claude", "kiro-cli")
3259
3475
  const cliLabel = this.backend?.binaryName ?? "CLI";
3260
3476
  const healthTmux = this.tmux;
@@ -3275,6 +3491,9 @@ export class Daemon extends EventEmitter {
3275
3491
  if (discardStaleHealth())
3276
3492
  return;
3277
3493
  if (paneStatus?.alive) {
3494
+ // A CLI shutdown also closes its MCP children. Revive tools only for
3495
+ // a positively live pane, never while its exit is being classified.
3496
+ measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
3278
3497
  // Capture only a known primary while it lives. No extra tmux call: PID comes
3279
3498
  // from this same status read. Fast/unobserved deaths remain ordinary crashes.
3280
3499
  const key = this.oomSessionKey();
@@ -3321,6 +3540,29 @@ export class Daemon extends EventEmitter {
3321
3540
  return;
3322
3541
  }
3323
3542
  }
3543
+ // tmux marks the PTY dead when its fd closes, before SIGCHLD can fill
3544
+ // pane_dead_status. Claude /exit (and any other CLI) can hit this gap.
3545
+ // Give the status four bounded re-probes; missing is never exit zero.
3546
+ if (paneStatus && !paneStatus.alive && paneStatus.exitCode === undefined) {
3547
+ for (let attempt = 0; attempt < 4; attempt++) {
3548
+ await new Promise(resolve => setTimeout(resolve, 250));
3549
+ if (discardStaleHealth())
3550
+ return;
3551
+ const observed = await healthTmux.getPaneStatus();
3552
+ if (discardStaleHealth())
3553
+ return;
3554
+ if (observed)
3555
+ paneStatus = observed;
3556
+ if (paneStatus.alive || paneStatus.exitCode !== undefined)
3557
+ break;
3558
+ }
3559
+ if (paneStatus.alive) {
3560
+ this.windowQueryFailureTicks = 0;
3561
+ this.stormWindow?.noteWindowAlive(this.name);
3562
+ scheduleNext();
3563
+ return;
3564
+ }
3565
+ }
3324
3566
  // paneStatus === null → window gone entirely (e.g. tmux server crash)
3325
3567
  // paneStatus.alive === false → pane dead, exit code available
3326
3568
  const exitCode = paneStatus?.exitCode;
@@ -3349,13 +3591,13 @@ export class Daemon extends EventEmitter {
3349
3591
  let inWindowLossBurst = false;
3350
3592
  let nullReason;
3351
3593
  if (!paneStatus) {
3352
- const serverAlive = await this.paneHost.sessionExists(this.tmuxSessionName);
3594
+ const serverAlive = await TmuxManager.sessionExists(this.tmuxSessionName);
3353
3595
  if (discardStaleHealth())
3354
3596
  return;
3355
3597
  // A server may have already restarted before this daemon's health
3356
3598
  // tick. The new PID is the durable generation boundary: treat it as
3357
3599
  // a fleet storm even though `has-session` is true again.
3358
- const serverPid = serverAlive ? await this.paneHost.getServerPid(this.tmuxSessionName) : null;
3600
+ const serverPid = serverAlive ? await TmuxManager.getServerPid(this.tmuxSessionName) : null;
3359
3601
  if (discardStaleHealth())
3360
3602
  return;
3361
3603
  const generationChanged = serverAlive && this.stormWindow?.observeServerAlive(serverPid) === true;
@@ -3384,7 +3626,7 @@ export class Daemon extends EventEmitter {
3384
3626
  // the window truly no longer exists vs a transient query glitch.
3385
3627
  nullReason = "no_window";
3386
3628
  try {
3387
- const windows = await this.paneHost.listWindowsStrict(this.tmuxSessionName, this.controlClient);
3629
+ const windows = await TmuxManager.listWindowsStrict(this.tmuxSessionName, this.controlClient);
3388
3630
  if (discardStaleHealth())
3389
3631
  return;
3390
3632
  this.windowQueryFailureTicks = 0;
@@ -3504,6 +3746,8 @@ export class Daemon extends EventEmitter {
3504
3746
  try {
3505
3747
  await this.spawnClaudeWindow();
3506
3748
  this.setProcessStatus("running");
3749
+ // #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
3750
+ void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
3507
3751
  this.logger.info("Recovered from background session conflict");
3508
3752
  this.emit("crash_respawn", this.name);
3509
3753
  }
@@ -3629,10 +3873,10 @@ export class Daemon extends EventEmitter {
3629
3873
  // Wrapped in try-catch: if tmux server is dead, listWindows throws —
3630
3874
  // must not block spawnClaudeWindow (which calls ensureSession).
3631
3875
  try {
3632
- const windows = await this.paneHost.listWindows(this.tmuxSessionName);
3876
+ const windows = await TmuxManager.listWindows(this.tmuxSessionName);
3633
3877
  for (const w of windows) {
3634
3878
  if (w.name === this.name) {
3635
- const tm = this.paneHost.bind(this.tmuxSessionName, w.id);
3879
+ const tm = new TmuxManager(this.tmuxSessionName, w.id);
3636
3880
  await tm.killWindow();
3637
3881
  }
3638
3882
  }
@@ -3663,6 +3907,8 @@ export class Daemon extends EventEmitter {
3663
3907
  catch { /* may not exist */ }
3664
3908
  }
3665
3909
  this.setProcessStatus("running");
3910
+ // #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
3911
+ void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
3666
3912
  this.logger.info({ resumed }, `Respawned ${cliLabel} window after crash`);
3667
3913
  this.emit("crash_respawn", this.name);
3668
3914
  // Emit the per-instance audit event while the storm is still active
@@ -3775,7 +4021,7 @@ export class Daemon extends EventEmitter {
3775
4021
  // Force the next ready capture to emit a fresh transition after respawn.
3776
4022
  this.instanceState = "working";
3777
4023
  }
3778
- this.broadcastFleetMessage({
4024
+ this.ipcServer?.broadcast({
3779
4025
  type: "instance_process_state",
3780
4026
  instanceName: this.name,
3781
4027
  status,
@@ -3971,15 +4217,12 @@ export class Daemon extends EventEmitter {
3971
4217
  for (const key of dialog.keys) {
3972
4218
  if (stale())
3973
4219
  return;
3974
- const keyScreen = this.nativePaneLifecycle ? await pollTmux.capturePane() : currentPane;
3975
- if (stale() || !Daemon.dialogMatches(dialog, keyScreen))
3976
- return;
3977
4220
  let sent = false;
3978
4221
  if (SPECIAL_KEYS.has(key)) {
3979
- sent = await (this.nativePaneLifecycle ? pollTmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : pollTmux.sendSpecialKey(key));
4222
+ sent = await pollTmux.sendSpecialKey(key);
3980
4223
  }
3981
4224
  else if (dialog.literalKeys) {
3982
- sent = await (this.nativePaneLifecycle ? pollTmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : pollTmux.sendKeys(key)); // typed, no Enter (#2872)
4225
+ sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
3983
4226
  }
3984
4227
  else {
3985
4228
  sent = await pollTmux.pasteText(key, this.systemPasteOptions());
@@ -4179,7 +4422,13 @@ export class Daemon extends EventEmitter {
4179
4422
  for (const ep of patterns) {
4180
4423
  const key = Daemon.errorPatternKey(ep);
4181
4424
  const count = countMatches(ep.pattern);
4182
- const seen = this.lastErrorCount.get(key) ?? 0;
4425
+ let seen = this.lastErrorCount.get(key) ?? 0;
4426
+ const afterInput = this.quotaOccurrencesAfterInput(pane, ep);
4427
+ // Only a new echo of our acknowledged input can re-arm a same-count replacement. Old rows before that echo,
4428
+ // an ambiguous/missing marker or a superseded input owner retain the ordinary count-based decision.
4429
+ const newQuotaAfterInput = !!afterInput && afterInput.count > (afterInput.input.reported.get(key) ?? 0);
4430
+ if (newQuotaAfterInput)
4431
+ seen = Math.min(seen, count - 1);
4183
4432
  // #2282: a resumed exhausted429 row is history, not even a new
4184
4433
  // notification/failover incident; so is a resumed codex workspace-limit row
4185
4434
  // (#2844: a wake must not pause again on it). Wait for the first live
@@ -4201,7 +4450,7 @@ export class Daemon extends EventEmitter {
4201
4450
  // the lifecycle, whose usage probe decides (available / reserve / exhausted).
4202
4451
  if (Daemon.isCodexUsageLimitPause(ep)) {
4203
4452
  const ack = this.codexReserveAck;
4204
- if (ack && ack.generation === this.spawnGeneration && count <= ack.count) {
4453
+ if (ack && ack.generation === this.spawnGeneration && count <= ack.count && !newQuotaAfterInput) {
4205
4454
  // The handled incident's lines can scroll out of the capture buffer as
4206
4455
  // the reserve session talks on. Lower the proof (and the occurrence
4207
4456
  // baseline) with them: otherwise `count <= ack.count` would keep
@@ -4242,6 +4491,8 @@ export class Daemon extends EventEmitter {
4242
4491
  continue;
4243
4492
  }
4244
4493
  this.lastErrorCount.set(key, count);
4494
+ if (afterInput)
4495
+ afterInput.input.reported.set(key, Math.max(afterInput.input.reported.get(key) ?? 0, afterInput.count));
4245
4496
  // Kiro's `auto` model picks a concrete model per turn and reroutes around
4246
4497
  // a temporarily unavailable one on its own — telling the user to run
4247
4498
  // /model would ask them to fix something the CLI is already handling.
@@ -4342,20 +4593,7 @@ export class Daemon extends EventEmitter {
4342
4593
  */
4343
4594
  async sendEscape() {
4344
4595
  const cancelKey = this.backend?.getCancelKey() ?? "Escape";
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);
4596
+ await this.tmux?.sendSpecialKey(cancelKey);
4359
4597
  }
4360
4598
  /**
4361
4599
  * Drop every not-yet-started pane delivery which predates a user cancel.
@@ -4375,6 +4613,14 @@ export class Daemon extends EventEmitter {
4375
4613
  // (Completion paths clear via the guard's onComplete; a cancel with no
4376
4614
  // later idle edge would otherwise leave the marker behind.)
4377
4615
  clearInFlightTurnMarker(this.instanceDir);
4616
+ // #3030: what the person cancelled is not kept for a redelivery (nor reported as not taken).
4617
+ try {
4618
+ for (const entry of this.handoffStore().list())
4619
+ this.handoffStore().remove(entry.id);
4620
+ }
4621
+ catch (err) {
4622
+ this.logger.warn({ err }, "Could not clear queued hand-off records on cancel");
4623
+ }
4378
4624
  this.deliveryEpoch = fleetEpoch === undefined
4379
4625
  ? this.deliveryEpoch + 1
4380
4626
  : Math.max(this.deliveryEpoch, fleetEpoch);
@@ -4413,7 +4659,7 @@ export class Daemon extends EventEmitter {
4413
4659
  return;
4414
4660
  const outcome = follower.look(pane);
4415
4661
  if (outcome) {
4416
- this.broadcastFleetMessage({ type: "compact_pane_result", id, owner, outcome });
4662
+ this.ipcServer?.broadcast({ type: "compact_pane_result", id, owner, outcome });
4417
4663
  return;
4418
4664
  }
4419
4665
  }
@@ -4433,7 +4679,7 @@ export class Daemon extends EventEmitter {
4433
4679
  const reportSwitch = (result) => {
4434
4680
  if (!ownSwitch)
4435
4681
  return;
4436
- this.broadcastFleetMessage({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
4682
+ this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
4437
4683
  };
4438
4684
  // Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
4439
4685
  // (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
@@ -4448,7 +4694,7 @@ export class Daemon extends EventEmitter {
4448
4694
  if (!notifyWritten || told)
4449
4695
  return;
4450
4696
  told = true;
4451
- this.broadcastFleetMessage({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
4697
+ this.ipcServer?.broadcast({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
4452
4698
  ...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
4453
4699
  if (written && paneFollower)
4454
4700
  void this.followCompactPane(notifyWritten, owner, paneFollower);
@@ -4610,11 +4856,10 @@ export class Daemon extends EventEmitter {
4610
4856
  const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
4611
4857
  if (this.tmux !== tmux)
4612
4858
  return false;
4613
- const screen = await read();
4614
- const now = claudeSwitchDialog(screen);
4859
+ const now = claudeSwitchDialog(await read());
4615
4860
  if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
4616
4861
  return false;
4617
- return this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen }) : tmux.sendSpecialKey(key);
4862
+ return tmux.sendSpecialKey(key);
4618
4863
  });
4619
4864
  const gone = async () => {
4620
4865
  const until = performance.now() + OWN_SWITCH_SETTLE_MS;
@@ -4703,12 +4948,9 @@ export class Daemon extends EventEmitter {
4703
4948
  return false;
4704
4949
  const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
4705
4950
  for (const key of dialog.keys) {
4706
- const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : currentPane;
4707
- if (!matches(keyScreen))
4708
- return false;
4709
4951
  const ok = specialKeys.has(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));
4952
+ ? await this.tmux.sendSpecialKey(key)
4953
+ : await this.tmux.sendKeys(key);
4712
4954
  if (!ok)
4713
4955
  return false;
4714
4956
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -4951,7 +5193,7 @@ export class Daemon extends EventEmitter {
4951
5193
  await this.tmux.sendSpecialKey("Escape").catch(() => false);
4952
5194
  return true;
4953
5195
  }
4954
- async stop(options = {}) {
5196
+ async stop() {
4955
5197
  this.publishWorkingState("before_stop");
4956
5198
  this.logger.info("Stopping daemon instance");
4957
5199
  this.clearQuitRelaunchWatch();
@@ -4973,18 +5215,8 @@ export class Daemon extends EventEmitter {
4973
5215
  }
4974
5216
  if (this.adapter)
4975
5217
  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
- }
4986
5218
  // Notify MCP servers of graceful shutdown (prevents reconnect attempts)
4987
- this.broadcastFleetMessage({ type: "shutdown" });
5219
+ this.ipcServer?.broadcast({ type: "shutdown" });
4988
5220
  // Quit CLI FIRST — this kills MCP server child processes cleanly.
4989
5221
  // IPC must stay open during quit so MCP servers receive the shutdown message.
4990
5222
  if (this.tmux) {
@@ -5130,24 +5362,6 @@ export class Daemon extends EventEmitter {
5130
5362
  freshChoice: this.recoveryFreshChoice || this.isNewSession && !this.bootSkippedResume, crashLoopFresh: this.recoveryCrashLoopFresh,
5131
5363
  cancelled: this.recoveryCancelled, background: this.workingBackgroundObservation };
5132
5364
  }
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
- }
5151
5365
  /** Local system work: no user turn, channel reply target, wake or replay of an original request. */
5152
5366
  async deliverWorkingRecovery(content, delivery, admitted) {
5153
5367
  const tmux = this.tmux, owner = JSON.stringify(this.interactionOwner()), epoch = this.deliveryEpoch;
@@ -5168,11 +5382,7 @@ export class Daemon extends EventEmitter {
5168
5382
  || !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
5169
5383
  || this.backend?.backgroundWorkRunning?.(pane) === true)
5170
5384
  return;
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())
5385
+ if (await tmux.getPaneInputMode?.() !== "raw" || !current())
5176
5386
  return;
5177
5387
  const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
5178
5388
  if (!current())
@@ -5263,7 +5473,7 @@ export class Daemon extends EventEmitter {
5263
5473
  this.logger.debug({ err }, "Interaction listener failed");
5264
5474
  }
5265
5475
  try {
5266
- this.broadcastFleetMessage({ type: "instance_interaction", instanceName: this.name, interaction });
5476
+ this.ipcServer?.broadcast({ type: "instance_interaction", instanceName: this.name, interaction });
5267
5477
  }
5268
5478
  catch (err) {
5269
5479
  this.logger.debug({ err }, "Interaction broadcast failed");
@@ -5351,56 +5561,6 @@ export class Daemon extends EventEmitter {
5351
5561
  }
5352
5562
  return true;
5353
5563
  }
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
- }
5404
5564
  /**
5405
5565
  * Answer a FleetManager state query. Most callers only need the cached state,
5406
5566
  * but lifecycle decisions such as post-reply Cancel retirement need an
@@ -5408,7 +5568,7 @@ export class Daemon extends EventEmitter {
5408
5568
  * the last transition can stay "working" even though the CLI has since
5409
5569
  * settled without emitting another control-mode output record.
5410
5570
  */
5411
- async respondToInstanceStateQuery(msg, socket, send) {
5571
+ async respondToInstanceStateQuery(msg, socket) {
5412
5572
  try {
5413
5573
  if (msg.deliveryIdle === true) {
5414
5574
  await this.captureAndEvaluateInstanceState("delivery_idle_gate");
@@ -5417,7 +5577,7 @@ export class Daemon extends EventEmitter {
5417
5577
  await this.captureAndEvaluateInstanceState("state_query");
5418
5578
  }
5419
5579
  const snapshot = this.getInstanceStateSnapshot();
5420
- const reply = {
5580
+ this.ipcServer?.send(socket, {
5421
5581
  type: "instance_state_response",
5422
5582
  requestId: msg.requestId,
5423
5583
  instanceName: this.name,
@@ -5425,11 +5585,7 @@ export class Daemon extends EventEmitter {
5425
5585
  state: this.isPaused ? "paused" : snapshot.state,
5426
5586
  processStatus: this.processStatus,
5427
5587
  pausedAt: this.lastPausedAt,
5428
- };
5429
- if (send)
5430
- send(reply);
5431
- else if (socket)
5432
- this.ipcServer?.send(socket, reply);
5588
+ });
5433
5589
  }
5434
5590
  catch (err) {
5435
5591
  this.logger.debug({ err: err.message }, "Instance state query failed");
@@ -5469,49 +5625,7 @@ export class Daemon extends EventEmitter {
5469
5625
  static AUTH_RECONFIRM_ROWS = 15;
5470
5626
  /** Why the deferred pause was requested; what its marker records (Phase 2a). */
5471
5627
  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
- }
5512
5628
  async pause(reason = "operator", admission) {
5513
- if (this.nativePaneLifecycle)
5514
- throw Error("Native pause is owned by its Fleet lifecycle transition");
5515
5629
  if (admission && !admission.current())
5516
5630
  return;
5517
5631
  if (this.pauseWakeState === "paused")
@@ -5607,7 +5721,7 @@ export class Daemon extends EventEmitter {
5607
5721
  writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
5608
5722
  this.publishWorkingState("execution");
5609
5723
  this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
5610
- this.broadcastFleetMessage({
5724
+ this.ipcServer?.broadcast({
5611
5725
  type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
5612
5726
  });
5613
5727
  this.emit("auto_paused", { name: this.name, pausedAt: this.lastPausedAt });
@@ -5702,7 +5816,7 @@ export class Daemon extends EventEmitter {
5702
5816
  this.resumeRuntimeMonitors();
5703
5817
  this.publishWorkingState("execution");
5704
5818
  this.logger.info("Instance auto-woke");
5705
- this.broadcastFleetMessage({
5819
+ this.ipcServer?.broadcast({
5706
5820
  type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
5707
5821
  });
5708
5822
  this.emit("auto_woke", { name: this.name });
@@ -5803,6 +5917,10 @@ export class Daemon extends EventEmitter {
5803
5917
  if (snapshot.state === "working" && this.recoveryBootSettled
5804
5918
  && this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
5805
5919
  this.recoveryNativeWork = true;
5920
+ if (snapshot.state !== "idle")
5921
+ this.instanceIdleSinceMono = null;
5922
+ else if (this.instanceState !== "idle" || this.instanceIdleSinceMono === null)
5923
+ this.instanceIdleSinceMono = performance.now();
5806
5924
  this.instanceState = snapshot.state;
5807
5925
  this.acceptedInstanceSnapshot = { ...snapshot };
5808
5926
  // Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
@@ -5862,7 +5980,7 @@ export class Daemon extends EventEmitter {
5862
5980
  this.logStateEdge(previous, snapshot);
5863
5981
  this.publishWorkingState("execution");
5864
5982
  this.emit("instance_state", { name: this.name, ...snapshot });
5865
- this.broadcastFleetMessage({ type: "instance_state", instanceName: this.name, ...snapshot });
5983
+ this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
5866
5984
  if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
5867
5985
  this.handleStuckTransition(pane, snapshot, this.instanceStateReadyPattern);
5868
5986
  }
@@ -5914,7 +6032,7 @@ export class Daemon extends EventEmitter {
5914
6032
  * message can arrive mid-turn and sit queued, and marking at arrival would let
5915
6033
  * the PREVIOUS turn's idle edge consume (and reset) the new turn's state.
5916
6034
  */
5917
- markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch) {
6035
+ markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch, queuedHandoff = false) {
5918
6036
  // Cancel may have interrupted an in-flight paste before its promise settled.
5919
6037
  // Its late success must not re-arm the turn that the user just cancelled.
5920
6038
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
@@ -5962,7 +6080,9 @@ export class Daemon extends EventEmitter {
5962
6080
  // during the continuation boots clean instead of chaining resumes.
5963
6081
  // #2544: a reply that settled before this arm already answered the person —
5964
6082
  // writing a marker would re-inject a continuation for a finished turn.
5965
- if (!this.nativePaneLifecycle && !meta.resumedContinuationOf && !armed.completionDelivered) {
6083
+ // #3030: a message only queued in the busy CLI has not started a turn: the interrupted turn is still the running one,
6084
+ // and a queued message lost to a restart is the hand-off record's to redeliver, not a continuation's.
6085
+ if (!meta.resumedContinuationOf && !armed.completionDelivered && !queuedHandoff) {
5966
6086
  const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
5967
6087
  const resumeCwd = this.config.working_directory ?? "";
5968
6088
  writeInFlightTurnMarker(this.instanceDir, {
@@ -6194,7 +6314,7 @@ export class Daemon extends EventEmitter {
6194
6314
  // outbound action suppresses a duplicate proxy; only a delivered
6195
6315
  // human-facing channel action satisfies the completion guard below.
6196
6316
  if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
6197
- && (this.nativePaneLifecycle ? this.nativePaneLifecycle.mcpProof() === "dead" : mcpServerState(this.instanceDir).state === "dead")) {
6317
+ && mcpServerState(this.instanceDir).state === "dead") {
6198
6318
  this.turnReplyGuard.complete(turn.generation);
6199
6319
  void this.sendProxyReply(pane, turn.target);
6200
6320
  return;
@@ -6571,7 +6691,7 @@ export class Daemon extends EventEmitter {
6571
6691
  }
6572
6692
  });
6573
6693
  }
6574
- if (!this.fleetTransportAvailable()) {
6694
+ if (!this.ipcServer) {
6575
6695
  this.logger.error(`${logLabel} failed — no adapter or fleet IPC route`);
6576
6696
  return Promise.resolve(false);
6577
6697
  }
@@ -6595,7 +6715,7 @@ export class Daemon extends EventEmitter {
6595
6715
  resolve(delivered);
6596
6716
  });
6597
6717
  try {
6598
- this.broadcastFleetMessage({
6718
+ this.ipcServer.broadcast({
6599
6719
  type: "fleet_outbound",
6600
6720
  tool: "reply",
6601
6721
  args,
@@ -7166,7 +7286,7 @@ export class Daemon extends EventEmitter {
7166
7286
  if (next === this.currentActivity)
7167
7287
  return;
7168
7288
  this.currentActivity = next;
7169
- this.broadcastFleetMessage({
7289
+ this.ipcServer?.broadcast({
7170
7290
  type: "instance_activity",
7171
7291
  instanceName: this.name,
7172
7292
  activity: next,
@@ -7219,6 +7339,10 @@ export class Daemon extends EventEmitter {
7219
7339
  else if (update.reply_completion_guard === null) {
7220
7340
  delete this.config.reply_completion_guard;
7221
7341
  }
7342
+ if (update.quota_notice === null)
7343
+ delete this.config.quota_notice;
7344
+ else if (update.quota_notice !== undefined && !quotaNoticeProblem(update.quota_notice))
7345
+ this.config.quota_notice = structuredClone(update.quota_notice);
7222
7346
  if (typeof update.mcp_proxy_reply === "boolean")
7223
7347
  this.config.mcp_proxy_reply = update.mcp_proxy_reply;
7224
7348
  else if (update.mcp_proxy_reply === null)
@@ -7243,7 +7367,7 @@ export class Daemon extends EventEmitter {
7243
7367
  else
7244
7368
  this.config.auto_pause_after = nextAutoPause;
7245
7369
  if (nextAutoPause !== previousAutoPause) {
7246
- const isGeneral = this.config.general_topic === true || generalByName(this.name, this.config);
7370
+ const isGeneral = this.config.general_topic === true || this.name === "general";
7247
7371
  this.autoPauseController.reconfigure((isGeneral ? 0 : nextAutoPause) * 60_000);
7248
7372
  }
7249
7373
  }
@@ -7323,7 +7447,7 @@ export class Daemon extends EventEmitter {
7323
7447
  return;
7324
7448
  this.lastProgressBroadcast = rendered;
7325
7449
  this.lastProgressBroadcastAt = Date.now();
7326
- this.broadcastFleetMessage({
7450
+ this.ipcServer?.broadcast({
7327
7451
  type: "instance_progress",
7328
7452
  instanceName: this.name,
7329
7453
  progress: rendered,
@@ -7339,7 +7463,7 @@ export class Daemon extends EventEmitter {
7339
7463
  if (this.lastProgressBroadcast !== "") {
7340
7464
  this.lastProgressBroadcast = "";
7341
7465
  this.lastProgressBroadcastAt = Date.now();
7342
- this.broadcastFleetMessage({ type: "instance_progress", instanceName: this.name, progress: "" });
7466
+ this.ipcServer?.broadcast({ type: "instance_progress", instanceName: this.name, progress: "" });
7343
7467
  }
7344
7468
  }
7345
7469
  /**
@@ -7367,10 +7491,12 @@ export class Daemon extends EventEmitter {
7367
7491
  // Command name only — never the arguments. See shellCommandLabel.
7368
7492
  if (name === "Bash")
7369
7493
  return `$ ${shellCommandLabel(String(inp.command ?? ""))}`;
7494
+ // The tool name only — never the search pattern: it can be a literal secret someone is looking for, and this label
7495
+ // reaches the channel's progress line.
7370
7496
  if (name === "Glob")
7371
- return `Glob ${inp.pattern ?? ""}`;
7497
+ return "Glob";
7372
7498
  if (name === "Grep")
7373
- return `Grep ${inp.pattern ?? ""}`;
7499
+ return "Grep";
7374
7500
  if (name === "Agent")
7375
7501
  return "Agent (subagent)";
7376
7502
  if (name.startsWith("mcp__agend__"))
@@ -7504,7 +7630,7 @@ export class Daemon extends EventEmitter {
7504
7630
  reportCrossInstanceDeliveryFailure(meta, verdict, error) {
7505
7631
  if (!meta.from_instance)
7506
7632
  return;
7507
- this.broadcastFleetMessage({
7633
+ this.ipcServer?.broadcast({
7508
7634
  type: "cross_instance_delivery_failed",
7509
7635
  senderSession: meta.from_instance,
7510
7636
  targetInstance: this.name,
@@ -7741,6 +7867,13 @@ export class Daemon extends EventEmitter {
7741
7867
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
7742
7868
  this.logger.info("Pending channel delivery dropped by user cancel");
7743
7869
  this.deferDurableDelivery(meta, "delivery epoch changed while queued");
7870
+ // #3030: a cancel is the person's — a queued redelivery it dropped is not redelivered later.
7871
+ if (meta.handoff_redelivery && !this.deliveryWritesStopping) {
7872
+ try {
7873
+ this.handoffStore().remove(meta.handoff_redelivery);
7874
+ }
7875
+ catch { /* the next CLI judges it */ }
7876
+ }
7744
7877
  return;
7745
7878
  }
7746
7879
  if (this.config.pre_task_command) {
@@ -7751,13 +7884,26 @@ export class Daemon extends EventEmitter {
7751
7884
  // #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
7752
7885
  // (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
7753
7886
  // byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
7754
- const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id ? this.currentWhatsNew(this.pendingWhatsNew) : null;
7887
+ const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id && !meta.handoff_redelivery
7888
+ ? this.currentWhatsNew(this.pendingWhatsNew) : null;
7755
7889
  const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
7756
7890
  const status = channelStatus(meta);
7757
7891
  // A fresh delivery begins a fresh turn — its bubble must not inherit
7758
7892
  // the previous turn's tool list.
7759
7893
  this.resetToolProgress();
7760
7894
  verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
7895
+ // #3030: a hand-off into the CLI's own queue stays on record until a turn takes it (redeliveries included).
7896
+ if (!content.startsWith("/raw ") || meta.from_instance) {
7897
+ const { handoff_redelivery: redeliveryOf, handoff_redeliveries: redeliveries, handoff_holder: _holder, ...kept } = meta;
7898
+ verdict.trackHandoff = { content, meta: kept,
7899
+ ...(redeliveryOf ? { id: redeliveryOf, redeliveries: Number(redeliveries) || 0 } : {}) };
7900
+ }
7901
+ // #3030: a redelivery is written only by the CLI it was issued for, and only while its record still says so —
7902
+ // one queued for a CLI that was replaced meanwhile is that next CLI's reconciliation's to judge again.
7903
+ if (meta.handoff_redelivery && !this.redeliveryStillOurs(meta)) {
7904
+ this.logger.info({ handoff: meta.handoff_redelivery }, "Queued hand-off redelivery dropped: its CLI was replaced before the write");
7905
+ return;
7906
+ }
7761
7907
  const durableAttempt = this.durableDeliveryAttempt(meta, content);
7762
7908
  if (durableAttempt === false) {
7763
7909
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
@@ -7780,10 +7926,11 @@ export class Daemon extends EventEmitter {
7780
7926
  if (whatsNew && (sent || verdict.paneWriteStarted))
7781
7927
  this.recordWhatsNewSent(whatsNew);
7782
7928
  }
7929
+ this.afterTrackedWrite(meta, verdict, !!sent, deliveryEpoch);
7783
7930
  if (sent) {
7784
7931
  delivered = true;
7785
7932
  this.finishDurableSubmission(durable, verdict);
7786
- this.markTurnStarted(meta, formatted, deliveryEpoch);
7933
+ this.markTurnStarted(meta, formatted, deliveryEpoch, !!verdict.handoff);
7787
7934
  }
7788
7935
  else if (durableAttempt && verdict.paneWriteStarted) {
7789
7936
  this.finishDurableDelivery(durable, "uncertain", `${verdict.phase ?? "post-submit"}:${verdict.proof ?? "unproven"}`, verdict);
@@ -8864,14 +9011,13 @@ export class Daemon extends EventEmitter {
8864
9011
  if (this.remoteCardAnswering)
8865
9012
  return { outcome: "refused", reason: "another answer to this card is being keyed in" };
8866
9013
  this.remoteCardAnswering = true;
8867
- let answerScreen = null;
8868
9014
  try {
8869
9015
  const outcome = await answerRemoteCard({
8870
9016
  locked: fn => this.paneWriteLock.run(fn),
8871
9017
  // An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
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),
9018
+ read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
9019
+ key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
9020
+ paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
8875
9021
  sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
8876
9022
  now: () => performance.now(),
8877
9023
  }, pane => {
@@ -9142,19 +9288,12 @@ export class Daemon extends EventEmitter {
9142
9288
  if (!check || !this.tmux)
9143
9289
  return !check;
9144
9290
  try {
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) {
9291
+ const mode = await this.tmux.getPaneInputMode?.();
9292
+ if (mode !== "raw") {
9151
9293
  this.resetFooterFallback();
9152
9294
  return false;
9153
9295
  }
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);
9296
+ return this.deliveryInputReadyPane(await this.tmux.capturePane());
9158
9297
  }
9159
9298
  catch {
9160
9299
  this.resetFooterFallback();
@@ -9928,86 +10067,34 @@ export class Daemon extends EventEmitter {
9928
10067
  // tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
9929
10068
  // the CLI this paste went to is never sent to its successor.
9930
10069
  const tmux = this.tmux;
9931
- const backend = this.backend;
9932
- const native = !!this.nativePaneLifecycle;
9933
10070
  const generation = this.spawnGeneration;
9934
10071
  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).
9940
10072
  const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
9941
- && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
9942
- && (!native || (!!windowId && !!backend && this.backend === backend && tmux.getWindowId() === windowId));
10073
+ && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
9943
10074
  if (!current())
9944
10075
  return false;
9945
10076
  // The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
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");
10077
+ const hold = () => {
10078
+ onInputHeld?.("pane-in-mode");
10079
+ this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
9957
10080
  return false;
9958
10081
  };
9959
10082
  // 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
9960
10083
  // end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
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.
10084
+ // past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
10085
+ // f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
9964
10086
  const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9965
- let pane;
9966
- let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity, linuxDialogRead = false;
9967
10087
  for (;;) {
9968
- const mode = await this.waitWhilePaneInMode(phase, current, Math.min(modeDeadline, positiveRetryDeadline));
10088
+ const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
9969
10089
  if (mode === "stale")
9970
10090
  return false;
9971
10091
  if (mode === "held")
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
- }
10092
+ return hold();
10006
10093
  if (await this.paneInTmuxMode(phase)) {
10007
10094
  if (!current())
10008
10095
  return false;
10009
10096
  if (performance.now() >= modeDeadline)
10010
- return hold("pane-in-mode");
10097
+ return hold();
10011
10098
  await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
10012
10099
  if (!current())
10013
10100
  return false;
@@ -10015,143 +10102,54 @@ export class Daemon extends EventEmitter {
10015
10102
  }
10016
10103
  if (!current())
10017
10104
  return false;
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");
10105
+ break;
10106
+ }
10107
+ // M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
10108
+ // (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
10109
+ // would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
10110
+ // delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
10111
+ // — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
10112
+ // Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
10113
+ // that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
10114
+ // unrecognised-prompt guess — so an unknown screen still sends.
10115
+ if (onInputHeld) {
10116
+ let finalPane = null;
10126
10117
  try {
10127
- if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
10128
- return hold("dialog");
10118
+ finalPane = await tmux.capturePane();
10129
10119
  }
10130
10120
  catch {
10131
- return hold("check_failed");
10121
+ finalPane = null;
10132
10122
  }
10133
10123
  if (!current())
10134
10124
  return false;
10135
- if (performance.now() >= deadline)
10136
- return hold("output_changed");
10137
- let sent;
10125
+ let dialog;
10138
10126
  try {
10139
- sent = await pane.pasteBuffer(text, { guard: current, proof: { kind: "composer", screen, retryCount: attempt } });
10127
+ dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
10140
10128
  }
10141
10129
  catch {
10142
- return hold("unconfirmed");
10130
+ dialog = undefined;
10143
10131
  }
10144
- if (sent)
10145
- return true; // Exactly one successful paste ends the loop, before any Enter.
10146
- if (!current())
10132
+ if (dialog !== undefined) {
10133
+ onInputHeld?.("dialog");
10134
+ this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
10147
10135
  return false;
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));
10136
+ }
10137
+ }
10138
+ // W1: persist the "Enter may have happened" boundary before tmux receives
10139
+ // the key. A crash after this commit is never auto-replayed without a
10140
+ // positive transcript marker, even if the stale pane still shows composer.
10141
+ if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
10142
+ this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
10143
+ return false;
10153
10144
  }
10154
- return hold("output_changed");
10145
+ const sent = await tmux.sendSpecialKey("Enter");
10146
+ if (!sent) {
10147
+ this.logger.error({
10148
+ phase,
10149
+ tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
10150
+ }, "tmux send-keys Enter failed during message delivery");
10151
+ }
10152
+ return sent;
10155
10153
  }
10156
10154
  async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
10157
10155
  // Last and defaulted so the positional callers that ignore the outcome stay
@@ -10189,9 +10187,6 @@ export class Daemon extends EventEmitter {
10189
10187
  const tmuxAtWrite = this.tmux;
10190
10188
  const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
10191
10189
  && 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;
10195
10190
  // Bug A: paste with backoff. Transient failures are usually a stale window id
10196
10191
  // after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
10197
10192
  const maxAttempts = 3;
@@ -10259,8 +10254,8 @@ export class Daemon extends EventEmitter {
10259
10254
  }
10260
10255
  verdict.durableBeginCommitted = true;
10261
10256
  verdict.submissionMode = attemptEvidence.submissionMode;
10262
- if (attemptEvidence.runtimeTranscript || attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
10263
- verdict.transcriptCheckpoint = attemptEvidence.runtimeTranscript ? { backend: attemptEvidence.backend, runtime: attemptEvidence.runtimeTranscript } : {
10257
+ if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
10258
+ verdict.transcriptCheckpoint = {
10264
10259
  backend: attemptEvidence.backend,
10265
10260
  path: attemptEvidence.transcriptPath,
10266
10261
  offset: attemptEvidence.transcriptOffset,
@@ -10274,6 +10269,17 @@ export class Daemon extends EventEmitter {
10274
10269
  }
10275
10270
  }
10276
10271
  }
10272
+ // #3030: a hand-off into a busy CLI's own queue is on record before its paste (write-ahead): that queue lives only
10273
+ // in the CLI process, and a stop before a turn takes it would lose it without a trace.
10274
+ if (verdict.trackHandoff && !rawPaste) {
10275
+ await this.recordQueuedHandoff(formatted, handingOffToNativeQueue, verdict, durableAttempt, humanVerificationId);
10276
+ if (stillCurrent && !stillCurrent()) {
10277
+ if (!verdict.trackHandoff.id)
10278
+ this.dropQueuedHandoff(verdict);
10279
+ verdict.fenced = !verdict.durableBeginCommitted;
10280
+ return false;
10281
+ }
10282
+ }
10277
10283
  if (lookBeforePaste) {
10278
10284
  await lookBeforePaste();
10279
10285
  // The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
@@ -10325,15 +10331,7 @@ export class Daemon extends EventEmitter {
10325
10331
  }
10326
10332
  const pasteGeneration = this.spawnGeneration;
10327
10333
  beforePaste?.();
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
- }
10334
+ const pasted = await this.tmux.pasteBuffer(formatted);
10337
10335
  if (pasted)
10338
10336
  this.rememberPaste(formatted, pasteGeneration);
10339
10337
  if (!pasted) {
@@ -10420,18 +10418,15 @@ export class Daemon extends EventEmitter {
10420
10418
  const enterMono = performance.now();
10421
10419
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
10422
10420
  let inputHeld = null;
10423
- if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof || nativeOwner ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
10421
+ if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
10424
10422
  if (inputHeld) {
10425
10423
  // #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
10426
10424
  // the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
10427
10425
  verdict.phase = "submit-enter";
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}`;
10426
+ verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
10432
10427
  return false;
10433
10428
  }
10434
- if ((steerComposerProof || nativeOwner) && !steerCurrent()) {
10429
+ if (steerComposerProof && !steerCurrent()) {
10435
10430
  verdict.phase = "submit-enter";
10436
10431
  verdict.proof = "fenced-before-enter";
10437
10432
  return false;
@@ -10440,6 +10435,7 @@ export class Daemon extends EventEmitter {
10440
10435
  return this.failDelivery(verdict, status, "submit-enter", "pre-enter-deadline");
10441
10436
  return this.failDelivery(verdict, status, "submit-enter", "tmux-send-keys-failed");
10442
10437
  }
10438
+ this.commitPreInputQuotaHistory(pasteBaseline);
10443
10439
  // The proof's whole budget, from the Enter: every capture and dialog probe inside it, a late answer dropped.
10444
10440
  const steerProofDeadline = performance.now() + NATIVE_QUEUE_PASTE_VERIFY_MS + POST_ENTER_PROOF_WINDOW_MS;
10445
10441
  // Kiro's legacy TUI can swallow Enter while it is still processing a large
@@ -10460,7 +10456,7 @@ export class Daemon extends EventEmitter {
10460
10456
  if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
10461
10457
  await new Promise(r => setTimeout(r, 1_000));
10462
10458
  const retryAt = Date.now();
10463
- if (await this.sendDeliveryEnter("queue-less-defensive-retry", nativeOwner)) {
10459
+ if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
10464
10460
  enterAt = retryAt;
10465
10461
  enterRetry = true;
10466
10462
  this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
@@ -10521,8 +10517,9 @@ export class Daemon extends EventEmitter {
10521
10517
  }
10522
10518
  const proof = await this.confirmSubmitted(signature, pasteBaseline);
10523
10519
  if (proof === "submitted") {
10524
- if (status)
10525
- this.emit("message_confirmed", status); // ✅ native queue accepted
10520
+ // ✅ native queue accepted — unless it is on record (#3030): then ✅ waits for the turn that takes it.
10521
+ if (status && !verdict.handoff)
10522
+ this.emit("message_confirmed", status);
10526
10523
  return true;
10527
10524
  }
10528
10525
  if (proof === "unverifiable") {
@@ -10554,8 +10551,8 @@ export class Daemon extends EventEmitter {
10554
10551
  this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
10555
10552
  verdict.phase = "best-effort-submission";
10556
10553
  verdict.proof = "unverified";
10557
- if (status)
10558
- this.emit("message_confirmed", status); // ✅ (best-effort)
10554
+ if (status && !verdict.handoff)
10555
+ this.emit("message_confirmed", status); // ✅ (best-effort; on record: when taken, #3030)
10559
10556
  return true;
10560
10557
  }
10561
10558
  // raw_paste has no unique marker and is not allowed to re-paste or
@@ -10570,6 +10567,10 @@ export class Daemon extends EventEmitter {
10570
10567
  // confirmation ladder. "stranded" is the case that used to be a silent
10571
10568
  // ✅ — the text was visible, so the old check called it delivered.
10572
10569
  this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
10570
+ // #3030: what follows is an idle submission (or a failure the caller reports): no longer a queued hand-off.
10571
+ if (verdict.handoff && !verdict.trackHandoff?.id)
10572
+ this.dropQueuedHandoff(verdict);
10573
+ verdict.handoff = undefined;
10573
10574
  if (windowId && this.controlClient && !(await this.controlClient.waitUntilIdle(windowId))) {
10574
10575
  verdict.phase = "native-queue-idle-wait";
10575
10576
  verdict.proof = "idle-wait-failed";
@@ -10593,7 +10594,7 @@ export class Daemon extends EventEmitter {
10593
10594
  // again. The prompt is back now, so an Enter can land.
10594
10595
  this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
10595
10596
  const strandedAt = Date.now();
10596
- if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", nativeOwner))) {
10597
+ if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
10597
10598
  return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
10598
10599
  }
10599
10600
  // The recovery Enter can be accepted before Codex paints its new
@@ -10646,7 +10647,7 @@ export class Daemon extends EventEmitter {
10646
10647
  }
10647
10648
  await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
10648
10649
  const retryAt = Date.now();
10649
- if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", nativeOwner))) {
10650
+ if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
10650
10651
  return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
10651
10652
  }
10652
10653
  if (windowId && this.controlClient) {
@@ -10698,7 +10699,7 @@ export class Daemon extends EventEmitter {
10698
10699
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
10699
10700
  const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
10700
10701
  const retryAt = Date.now();
10701
- if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", nativeOwner))) {
10702
+ if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
10702
10703
  return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
10703
10704
  }
10704
10705
  submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
@@ -10786,7 +10787,7 @@ export class Daemon extends EventEmitter {
10786
10787
  }
10787
10788
  // No control client to observe output: fall back to the legacy double-Enter.
10788
10789
  await new Promise(r => setTimeout(r, 1000));
10789
- await this.sendDeliveryEnter("unobserved-defensive-retry", nativeOwner);
10790
+ await this.sendDeliveryEnter("unobserved-defensive-retry");
10790
10791
  if (status)
10791
10792
  this.emit("message_confirmed", status); // ✅ (best-effort)
10792
10793
  }
@@ -11006,22 +11007,14 @@ export class Daemon extends EventEmitter {
11006
11007
  * and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
11007
11008
  */
11008
11009
  transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
11009
- if (this.nativePaneLifecycle && !checkpoint.runtime)
11010
- throw Error("Native proof cannot read a coordinator-local transcript path");
11011
11010
  const launchAtWrite = this.launchFenceEpoch;
11012
11011
  return {
11013
- reader: checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
11012
+ reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
11014
11013
  current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
11015
- && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
11016
- && (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
11014
+ && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
11017
11015
  onProof,
11018
11016
  };
11019
11017
  }
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
- }
11025
11018
  /** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
11026
11019
  cliLabel() {
11027
11020
  return this.config.backend ?? this.backend?.binaryName ?? "CLI";
@@ -11161,12 +11154,18 @@ export class Daemon extends EventEmitter {
11161
11154
  async capturePaneEvidence(signature) {
11162
11155
  if (!this.tmux)
11163
11156
  return null;
11157
+ const owner = this.tmux, window = owner.getWindowId?.(), spawn = this.spawnGeneration, fence = this.launchFenceEpoch, launch = this.launchAttempt;
11164
11158
  // Bounded re-probe: a capture that fails here costs the delivery its only
11165
11159
  // "before" picture, and a momentary failure should not decide anything.
11166
11160
  for (let attempt = 1; attempt <= BASELINE_CAPTURE_ATTEMPTS; attempt++) {
11167
11161
  try {
11168
11162
  const pane = await this.tmux.capturePane();
11169
11163
  const evidence = this.paneEvidence(pane, signature);
11164
+ if (owner === this.tmux && window === owner.getWindowId?.() && spawn === this.spawnGeneration && fence === this.launchFenceEpoch && launch === this.launchAttempt) {
11165
+ const counts = this.paneQuotaPatterns().map(ep => ({ key: Daemon.errorPatternKey(ep), count: Daemon.countOccurrences(ep.pattern, pane) }));
11166
+ if (counts.length)
11167
+ evidence.quotaHistory = { spawn, fence, attempt: launch, tmux: owner, window, counts, marker: signature.value, occurrences: evidence.payload };
11168
+ }
11170
11169
  const input = this.inputRegion(pane);
11171
11170
  if (input && agendMessageInInput(input.text)) {
11172
11171
  // Whatever we paste now lands after it, and one Enter submits both as
@@ -11265,14 +11264,7 @@ export class Daemon extends EventEmitter {
11265
11264
  * (the retries included). Without a guard it is exactly the old unconditional path.
11266
11265
  */
11267
11266
  async submitSystemPaste(text, label, guard, admission) {
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;
11267
+ const current = guard || admission ? () => { admission?.(); return guard?.current() ?? true; } : undefined;
11276
11268
  if (current && !current())
11277
11269
  return false;
11278
11270
  if (!this.tmux)
@@ -11308,6 +11300,7 @@ export class Daemon extends EventEmitter {
11308
11300
  await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
11309
11301
  if (!(await this.sendDeliveryEnter(label, current)))
11310
11302
  return false;
11303
+ this.commitPreInputQuotaHistory(baseline);
11311
11304
  let proof = await this.confirmSubmitted(signature, baseline);
11312
11305
  if (current && !current())
11313
11306
  return false;
@@ -11392,13 +11385,13 @@ export class Daemon extends EventEmitter {
11392
11385
  && sameInteractionOwner(recoveryOwner, this.interactionOwner());
11393
11386
  const previousWindowId = this.tmux?.getWindowId();
11394
11387
  try {
11395
- const windows = await this.paneHost.listWindows(this.tmuxSessionName, this.controlClient);
11388
+ const windows = await TmuxManager.listWindows(this.tmuxSessionName, this.controlClient);
11396
11389
  if (!recoveryCurrent() || this.tmux !== previousTmux)
11397
11390
  return undefined;
11398
11391
  const match = windows.find(w => w.name === this.name);
11399
11392
  if (!match)
11400
11393
  return undefined;
11401
- this.tmux = this.paneHost.bind(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
11394
+ this.tmux = new TmuxManager(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
11402
11395
  const recoveredTmux = this.tmux;
11403
11396
  writeFileSync(join(this.instanceDir, "window-id"), match.id);
11404
11397
  // The window we were talking to is gone; leaving it registered means the
@@ -11461,8 +11454,6 @@ export class Daemon extends EventEmitter {
11461
11454
  return false;
11462
11455
  }
11463
11456
  getWindowId() {
11464
- if (this.nativePaneLifecycle)
11465
- return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
11466
11457
  try {
11467
11458
  return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
11468
11459
  }
@@ -11494,55 +11485,7 @@ export class Daemon extends EventEmitter {
11494
11485
  * Handle a tool call from the MCP server (forwarded by Claude).
11495
11486
  * Routes to the channel adapter via MessageBus.
11496
11487
  */
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) {
11488
+ handleToolCall(msg, socket) {
11546
11489
  const tool = msg.tool;
11547
11490
  const args = (msg.args ?? {});
11548
11491
  const requestId = msg.requestId;
@@ -11550,20 +11493,20 @@ export class Daemon extends EventEmitter {
11550
11493
  this.logger.debug({ tool, requestId }, "Tool call from MCP server");
11551
11494
  // A tool call can only come from a live MCP server of ours — proof of life
11552
11495
  // that outranks whatever the pid slot says.
11553
- if (peer?.current() || socket && this.socketSessionNames.get(socket) === this.name) {
11554
- this.noteMcpProofOfLife("tool_call", socket ? this.socketPids.get(socket) : undefined);
11496
+ if (this.socketSessionNames.get(socket) === this.name) {
11497
+ this.noteMcpProofOfLife("tool_call", this.socketPids.get(socket));
11555
11498
  }
11556
11499
  // A tool invocation is not delivery evidence. Capture which obligation it
11557
11500
  // belongs to now, then settle it only after the adapter/fleet responds.
11558
11501
  // Explicitly mapped sibling sessions must not satisfy this instance's turn;
11559
11502
  // an unmapped socket is retained for legacy/tests where mcp_ready was not
11560
11503
  // observed before the first tool call.
11561
- const sourceSession = peer ? this.name : socket ? this.socketSessionNames.get(socket) : undefined;
11504
+ const sourceSession = this.socketSessionNames.get(socket);
11562
11505
  const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
11563
11506
  ? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
11564
11507
  : null;
11565
11508
  // For now, log and respond. Full adapter routing will be wired in fleet manager.
11566
- const respond = (result, error, errorCode) => {
11509
+ const respond = (result, error) => {
11567
11510
  // A message that verifiably went out stands down the dead-MCP proxy reply
11568
11511
  // for this turn: the agent proved it can still speak for itself.
11569
11512
  if (!error && result != null && TURN_OUTBOUND_TOOLS.has(tool)) {
@@ -11604,21 +11547,14 @@ export class Daemon extends EventEmitter {
11604
11547
  // failed" and report unknown immediately rather than starting recovery.
11605
11548
  this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
11606
11549
  }
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;
11550
+ const sent = this.ipcServer?.send(socket, { requestId, result, error, operationId }) ?? false;
11609
11551
  if (!sent) {
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");
11552
+ this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
11614
11553
  }
11615
11554
  else if (operationId) {
11616
11555
  this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
11617
11556
  }
11618
11557
  };
11619
- // Native Fleet/task operations carry their lease to the existing dispatcher mutation admission points.
11620
- if (peer?.dispatch?.(tool, args, respond))
11621
- return;
11622
11558
  // Repo checkout — handled locally in daemon (no fleet-manager)
11623
11559
  if (tool === "checkout_repo") {
11624
11560
  this.handleCheckoutRepo(args, respond);
@@ -11647,7 +11583,7 @@ export class Daemon extends EventEmitter {
11647
11583
  clearTimeout(timeout);
11648
11584
  respond(respMsg.result, respMsg.error);
11649
11585
  });
11650
- this.broadcastFleetMessage({
11586
+ this.ipcServer?.broadcast({
11651
11587
  type,
11652
11588
  payload: args,
11653
11589
  meta: { instance_name: this.name },
@@ -11673,7 +11609,7 @@ export class Daemon extends EventEmitter {
11673
11609
  clearTimeout(timeout);
11674
11610
  resolve({ result: respMsg.result, error: respMsg.error });
11675
11611
  });
11676
- this.broadcastFleetMessage({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
11612
+ this.ipcServer?.broadcast({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
11677
11613
  });
11678
11614
  void (async () => {
11679
11615
  if (tool === "preview_web_appearance") {
@@ -11750,7 +11686,7 @@ export class Daemon extends EventEmitter {
11750
11686
  clearTimeout(timeout);
11751
11687
  respond(respMsg.result, respMsg.error);
11752
11688
  });
11753
- this.broadcastFleetMessage({
11689
+ this.ipcServer?.broadcast({
11754
11690
  type: "fleet_task",
11755
11691
  payload: args,
11756
11692
  meta: { instance_name: this.name },
@@ -11773,7 +11709,7 @@ export class Daemon extends EventEmitter {
11773
11709
  clearTimeout(timeout);
11774
11710
  respond(respMsg.result, respMsg.error);
11775
11711
  });
11776
- this.broadcastFleetMessage({
11712
+ this.ipcServer?.broadcast({
11777
11713
  type: typeMap[tool],
11778
11714
  payload: args,
11779
11715
  meta: { instance_name: this.name, working_directory: this.config.working_directory },
@@ -11800,7 +11736,7 @@ export class Daemon extends EventEmitter {
11800
11736
  clearTimeout(timeout);
11801
11737
  respond(respMsg.result, respMsg.error);
11802
11738
  });
11803
- this.broadcastFleetMessage({
11739
+ this.ipcServer?.broadcast({
11804
11740
  type: typeMap[tool],
11805
11741
  payload: args,
11806
11742
  meta: {
@@ -11815,11 +11751,11 @@ export class Daemon extends EventEmitter {
11815
11751
  }
11816
11752
  if (CROSS_INSTANCE_TOOLS.has(tool)) {
11817
11753
  // Route to fleet manager via IPC (topic mode only)
11818
- if (this.topicMode && this.fleetTransportAvailable()) {
11754
+ if (this.topicMode && this.ipcServer) {
11819
11755
  // Use fleetRequestId (not requestId) to avoid MCP server resolving the
11820
11756
  // pending tool call prematurely when it receives the broadcast.
11821
11757
  const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
11822
- const senderSessionName = sourceSession;
11758
+ const senderSessionName = this.socketSessionNames.get(socket);
11823
11759
  // Shared table, so delete_instance and the deployment tools get the same
11824
11760
  // budget here as the MCP client expects (the ad-hoc list omitted them).
11825
11761
  const crossTimeoutMs = daemonBudgetMs(tool);
@@ -11831,7 +11767,7 @@ export class Daemon extends EventEmitter {
11831
11767
  clearTimeout(timeout);
11832
11768
  respond(respMsg.result, respMsg.error);
11833
11769
  });
11834
- this.broadcastFleetMessage({
11770
+ this.ipcServer.broadcast({
11835
11771
  type: "fleet_outbound",
11836
11772
  tool,
11837
11773
  args,
@@ -11885,12 +11821,12 @@ export class Daemon extends EventEmitter {
11885
11821
  }, daemonBudgetMs(tool));
11886
11822
  this.pendingIpcRequests.set(outboundKey, (respMsg) => {
11887
11823
  clearTimeout(timeout);
11888
- respond(respMsg.result, respMsg.error, respMsg.errorCode);
11824
+ respond(respMsg.result, respMsg.error);
11889
11825
  });
11890
11826
  // Register the waiter before broadcasting. A fast/mock adapter can settle
11891
11827
  // immediately; broadcasting first left a response-before-registration
11892
11828
  // window that degraded into a timeout despite a completed POST.
11893
- this.broadcastFleetMessage({
11829
+ this.ipcServer?.broadcast({
11894
11830
  type: "fleet_outbound",
11895
11831
  tool,
11896
11832
  args,
@@ -12021,7 +11957,7 @@ export class Daemon extends EventEmitter {
12021
11957
  // The daemon used to read `general_topic` alone while `isGeneralInstance`
12022
11958
  // also accepted the name, so an instance called `general` without the flag
12023
11959
  // was a worker here and a general everywhere else.
12024
- const isGeneral = this.config.general_topic === true || generalByName(this.name, this.config);
11960
+ const isGeneral = this.config.general_topic === true || this.name === "general";
12025
11961
  // No third argument for an ordinary instance: the default is `worker` now,
12026
11962
  // and naming it here would be a second place to change it.
12027
11963
  const toolSet = isGeneral
@@ -12183,6 +12119,8 @@ export class Daemon extends EventEmitter {
12183
12119
  }
12184
12120
  this.cancelCapacityNudge("the instance was stopped, paused or respawned");
12185
12121
  this.lastErrorEpisode = null;
12122
+ this.quotaReplay = null;
12123
+ this.quotaInput = null;
12186
12124
  this.nudgeBaselines = null;
12187
12125
  // A relaunch settles any outstanding quit watch: the CLI came back.
12188
12126
  this.clearQuitRelaunchWatch();
@@ -12397,8 +12335,6 @@ export class Daemon extends EventEmitter {
12397
12335
  catch { /* best effort */ }
12398
12336
  }
12399
12337
  async spawnClaudeWindow() {
12400
- if (this.nativePaneLifecycle)
12401
- throw Error("Native panes never enter local resume/fresh fallback");
12402
12338
  this.assertSessionOomAdmission();
12403
12339
  this.beginSpawn();
12404
12340
  let resumedSuccessfully = false;
@@ -12985,14 +12921,6 @@ export class Daemon extends EventEmitter {
12985
12921
  await this.museUsageRelay.stop().catch(() => { });
12986
12922
  this.museUsageRelay = null;
12987
12923
  }
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
- }
12996
12924
  try {
12997
12925
  await this.killProcessTree("SIGTERM", "startup aborted");
12998
12926
  }
@@ -13022,15 +12950,13 @@ export class Daemon extends EventEmitter {
13022
12950
  }
13023
12951
  /** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
13024
12952
  async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
13025
- if (this.nativePaneLifecycle)
13026
- throw Error("Native processes can only be closed through their owned runtime pane");
13027
12953
  const windowId = this.tmux?.getWindowId();
13028
12954
  // Before createWindow succeeds there is no pane owned by this daemon.
13029
12955
  // tmux interprets an empty selector as the session's active window.
13030
12956
  if (!windowId?.trim())
13031
12957
  return;
13032
12958
  try {
13033
- const pid = await this.paneHost.getPanePid(this.tmuxSessionName, windowId);
12959
+ const pid = await TmuxManager.getPanePid(this.tmuxSessionName, windowId);
13034
12960
  if (pid) {
13035
12961
  this.noteStopRequest(signal, reason);
13036
12962
  process.kill(-pid, signal);
@@ -13052,7 +12978,12 @@ export class Daemon extends EventEmitter {
13052
12978
  || this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
13053
12979
  throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
13054
12980
  }
13055
- return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs);
12981
+ return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs).catch(err => {
12982
+ // Includes an in-place binary replacement during asynchronous launch preparation.
12983
+ if (err instanceof KiroCompatibilityError)
12984
+ this.emit("kiro_compatibility_hold", err.message);
12985
+ throw err;
12986
+ });
13056
12987
  };
13057
12988
  const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
13058
12989
  instanceName: this.name,
@@ -13244,7 +13175,7 @@ export class Daemon extends EventEmitter {
13244
13175
  whatsNewRoles() {
13245
13176
  if (this.runtimeIdentity?.kind === "classic")
13246
13177
  return ["classic"];
13247
- if (this.config.general_topic === true || generalByName(this.name, this.config))
13178
+ if (this.config.general_topic === true || this.name === "general")
13248
13179
  return ["general"];
13249
13180
  const set = resolveToolSet(this.config, this.name);
13250
13181
  return set === "coordinator" || set === "full" ? ["worker", "coordinator"] : ["worker"];
@@ -13271,12 +13202,15 @@ export class Daemon extends EventEmitter {
13271
13202
  }
13272
13203
  }
13273
13204
  async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
13274
- if (this.nativePaneLifecycle)
13275
- throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
13276
13205
  this.assertSessionOomAdmission();
13277
13206
  this.startupAdmission?.();
13278
13207
  const launchFence = this.launchFenceEpoch;
13279
13208
  const backendConfig = this.buildBackendConfig();
13209
+ // A compatibility refusal is not a failed resume: no configuration, pin, history or pane may change.
13210
+ if (this.backend?.assertLaunchSupported) {
13211
+ this.backend.assertLaunchSupported(backendConfig);
13212
+ this.emit("kiro_compatibility_hold", null);
13213
+ }
13280
13214
  // #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
13281
13215
  this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
13282
13216
  // Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
@@ -13313,6 +13247,7 @@ export class Daemon extends EventEmitter {
13313
13247
  const agentTokenPath = join(this.instanceDir, "agent.token");
13314
13248
  const agentToken = randomBytes(32).toString("hex");
13315
13249
  const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
13250
+ this.agentCredentialIssuer?.(agentToken);
13316
13251
  if (!tokenWrite.ok) {
13317
13252
  // Do not fail the spawn over it — an instance that cannot start is worse
13318
13253
  // than one whose token is readable. But say so, loudly: the whole point of
@@ -13358,6 +13293,8 @@ export class Daemon extends EventEmitter {
13358
13293
  // Every launched command re-arms the passive-transient check, including a
13359
13294
  // retry inside the same spawn: its load is a new one.
13360
13295
  this.launchAttempt++;
13296
+ await this.prepareQuotaReplayHistory(reuseWindow);
13297
+ this.startupAdmission?.();
13361
13298
  this.interactionObservation.reset(this.interactionOwner());
13362
13299
  this.clearInteractionConfirmation();
13363
13300
  // A launch that cannot paint a passive transient (Codex: a fresh start
@@ -13377,8 +13314,8 @@ export class Daemon extends EventEmitter {
13377
13314
  this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
13378
13315
  }
13379
13316
  // Ensure tmux session exists (may have been destroyed if all windows died)
13380
- await this.paneHost.ensureSession(this.tmuxSessionName);
13381
- if (this.stormWindow?.observeServerAlive(await this.paneHost.getServerPid(this.tmuxSessionName))) {
13317
+ await TmuxManager.ensureSession(this.tmuxSessionName);
13318
+ if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
13382
13319
  this.emit("tmux_server_crash", this.name);
13383
13320
  }
13384
13321
  this.startupAdmission?.();
@@ -13514,6 +13451,7 @@ export class Daemon extends EventEmitter {
13514
13451
  let lastDialog = null;
13515
13452
  let lastTransient = null;
13516
13453
  let captureFailures = 0;
13454
+ let lastPane = null;
13517
13455
  let attempts = 0;
13518
13456
  // A safety-critical startup choice is one-shot. If the CLI has not
13519
13457
  // repainted after Enter, let the following hold-only entry report it
@@ -13537,11 +13475,13 @@ export class Daemon extends EventEmitter {
13537
13475
  try {
13538
13476
  pane = await this.tmux.capturePane();
13539
13477
  this.startupAdmission?.();
13478
+ lastPane = pane;
13540
13479
  }
13541
13480
  catch (err) {
13542
13481
  this.startupAdmission?.();
13543
13482
  // Transient tmux trouble is not evidence about the CLI. Returning false
13544
13483
  // here would clear the session; retry within the budget instead.
13484
+ lastPane = null;
13545
13485
  captureFailures++;
13546
13486
  this.logger.debug({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
13547
13487
  // Only a POSITIVE dead pane ends the scan; an unanswerable liveness
@@ -13652,15 +13592,12 @@ export class Daemon extends EventEmitter {
13652
13592
  // each have seen a respawn, pause or stop.
13653
13593
  if (!owned())
13654
13594
  return false;
13655
- const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
13656
- if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
13657
- return false;
13658
13595
  if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
13659
- if (!await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key)))
13596
+ if (!await this.tmux.sendSpecialKey(key))
13660
13597
  return false;
13661
13598
  }
13662
13599
  else {
13663
- if (!await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key)))
13600
+ if (!await this.tmux.sendKeys(key))
13664
13601
  return false;
13665
13602
  }
13666
13603
  await new Promise(r => setTimeout(r, 200));
@@ -13674,8 +13611,7 @@ export class Daemon extends EventEmitter {
13674
13611
  // Wait for next screen to render — bounded by what is left of the budget.
13675
13612
  const renderWait = Math.max(0, Math.min(10_000, remaining()));
13676
13613
  if (this.controlClient) {
13677
- const wid = this.nativePaneLifecycle ? ownerTmux.getWindowId()
13678
- : readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
13614
+ const wid = readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
13679
13615
  await this.controlClient.waitForIdle(wid, renderWait);
13680
13616
  }
13681
13617
  else {
@@ -13711,15 +13647,15 @@ export class Daemon extends EventEmitter {
13711
13647
  // scan parked on the old limit text.
13712
13648
  const codexReady = this.backend.binaryName !== "codex"
13713
13649
  || this.isCodexLivePaneSnapshot(pane);
13714
- const nativeComposerReady = !this.nativePaneLifecycle || (this.backend?.readInputRow
13715
- ? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
13716
- if (readyPatternMatches && codexReady && nativeComposerReady) {
13650
+ if (readyPatternMatches && codexReady) {
13717
13651
  cleanReadyPolls++;
13718
13652
  if (cleanReadyPolls >= 2 || remaining() <= 0) {
13719
13653
  // A real ready prompt (with no fatal dialog on screen — those are
13720
13654
  // matched above, before this check) means the fatal screen is gone,
13721
13655
  // e.g. the user fixed the config and the instance respawned.
13722
13656
  this.fatalStartupBlocked = false;
13657
+ if (owned())
13658
+ this.takeQuotaReplayBaseline(pane);
13723
13659
  return true;
13724
13660
  }
13725
13661
  await sleep();
@@ -13758,6 +13694,8 @@ export class Daemon extends EventEmitter {
13758
13694
  this.logger.warn({ attempts, budgetMs, captureFailures }, "Startup scan exhausted without a ready prompt or a known dialog — assuming ready (unknown CLI screen)");
13759
13695
  }
13760
13696
  this.startupAdmission?.();
13697
+ if (owned() && lastPane !== null && !lastDialog && !lastTransient)
13698
+ this.takeQuotaReplayBaseline(lastPane);
13761
13699
  return true;
13762
13700
  }
13763
13701
  /**
@@ -13838,16 +13776,6 @@ export class Daemon extends EventEmitter {
13838
13776
  void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
13839
13777
  }
13840
13778
  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
- }
13851
13779
  // When a resume failure has forced a fresh start, don't persist the stale id
13852
13780
  // back from statusline.json — that would re-arm --continue and re-loop.
13853
13781
  if (this.skipResume)