@adhdev/daemon-core 0.9.82-rc.50 → 0.9.82-rc.501

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 (478) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +9 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +85 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +246 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +139 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +23 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +71 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +64 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +48 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +152 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router-aggregate-status.d.ts +24 -0
  56. package/dist/commands/router-mesh-session-owner.d.ts +59 -0
  57. package/dist/commands/router-refine.d.ts +151 -0
  58. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  59. package/dist/commands/router.d.ts +203 -15
  60. package/dist/commands/upgrade-helper.d.ts +41 -1
  61. package/dist/config/chat-history.d.ts +11 -0
  62. package/dist/config/config.d.ts +19 -0
  63. package/dist/config/mesh-config.d.ts +66 -0
  64. package/dist/config/mesh-json-config.d.ts +199 -0
  65. package/dist/config/registry-resolver.d.ts +54 -0
  66. package/dist/config/repo-settings.d.ts +77 -0
  67. package/dist/config/state-store.d.ts +30 -0
  68. package/dist/daemon/dev-server.d.ts +0 -2
  69. package/dist/detection/cli-detector.d.ts +17 -0
  70. package/dist/detection/ide-detector.d.ts +13 -0
  71. package/dist/detection/win32-ide-version.d.ts +37 -0
  72. package/dist/git/change-impact-config.d.ts +159 -0
  73. package/dist/git/git-commands.d.ts +26 -2
  74. package/dist/git/git-diff.d.ts +6 -0
  75. package/dist/git/git-executor.d.ts +11 -0
  76. package/dist/git/git-status.d.ts +80 -0
  77. package/dist/git/git-types.d.ts +2 -50
  78. package/dist/git/git-worktree.d.ts +71 -1
  79. package/dist/git/index.d.ts +3 -1
  80. package/dist/index.d.ts +71 -17
  81. package/dist/index.js +56455 -21586
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +53763 -19064
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/installer.d.ts +1 -4
  86. package/dist/ipc/local-ipc-server.d.ts +91 -0
  87. package/dist/launch.d.ts +1 -1
  88. package/dist/logging/async-batch-writer.d.ts +10 -0
  89. package/dist/logging/debug-config.d.ts +16 -0
  90. package/dist/logging/log-redactor.d.ts +24 -0
  91. package/dist/logging/log-tail-reader.d.ts +81 -0
  92. package/dist/logging/logger.d.ts +1 -1
  93. package/dist/mesh/contracts.d.ts +256 -0
  94. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  95. package/dist/mesh/coordinator-registry.d.ts +59 -0
  96. package/dist/mesh/mesh-active-work.d.ts +203 -0
  97. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  98. package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
  99. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  100. package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
  101. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  102. package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
  103. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  104. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  105. package/dist/mesh/mesh-events-pending.d.ts +258 -0
  106. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  107. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  108. package/dist/mesh/mesh-events.d.ts +6 -49
  109. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  110. package/dist/mesh/mesh-host-ownership.d.ts +21 -1
  111. package/dist/mesh/mesh-idle-reminder.d.ts +68 -0
  112. package/dist/mesh/mesh-init.d.ts +128 -0
  113. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  114. package/dist/mesh/mesh-ledger.d.ts +139 -1
  115. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  116. package/dist/mesh/mesh-missions.d.ts +281 -0
  117. package/dist/mesh/mesh-node-identity.d.ts +310 -0
  118. package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
  119. package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
  120. package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
  121. package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
  122. package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
  123. package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
  124. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  125. package/dist/mesh/mesh-refine-gates.d.ts +457 -0
  126. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  127. package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
  128. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  129. package/dist/mesh/mesh-routing.d.ts +70 -0
  130. package/dist/mesh/mesh-runtime-store.d.ts +584 -0
  131. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  132. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  133. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  134. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
  135. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  136. package/dist/mesh/mesh-work-queue.d.ts +392 -1
  137. package/dist/mesh/preview-freshness.d.ts +28 -0
  138. package/dist/mesh/refine-config.d.ts +97 -0
  139. package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
  140. package/dist/providers/acp-provider-instance.d.ts +5 -0
  141. package/dist/providers/approval-utils.d.ts +46 -0
  142. package/dist/providers/chat-message-normalization.d.ts +58 -1
  143. package/dist/providers/cli-provider-effect-format.d.ts +30 -0
  144. package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
  145. package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
  146. package/dist/providers/cli-provider-instance-types.d.ts +47 -0
  147. package/dist/providers/cli-provider-instance.d.ts +461 -30
  148. package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
  149. package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
  150. package/dist/providers/contracts.d.ts +196 -6
  151. package/dist/providers/external-sources.d.ts +71 -0
  152. package/dist/providers/manual-attendance.d.ts +79 -0
  153. package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
  154. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
  155. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  156. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  157. package/dist/providers/native-history/constants.d.ts +12 -0
  158. package/dist/providers/native-history/dispatcher.d.ts +44 -0
  159. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  160. package/dist/providers/native-history/index.d.ts +13 -0
  161. package/dist/providers/provider-instance-manager.d.ts +30 -0
  162. package/dist/providers/provider-instance.d.ts +31 -1
  163. package/dist/providers/provider-loader.d.ts +42 -5
  164. package/dist/providers/provider-trust.d.ts +31 -0
  165. package/dist/providers/read-chat-contract.d.ts +29 -0
  166. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  167. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  168. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  169. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  170. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  171. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  172. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  173. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  174. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  175. package/dist/providers/sdk/v1/index.d.ts +30 -0
  176. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  177. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  178. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  179. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  180. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  181. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  182. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  183. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  184. package/dist/providers/spec/adapter.d.ts +98 -0
  185. package/dist/providers/spec/background-task-detector.d.ts +29 -0
  186. package/dist/providers/spec/cli-adapter.d.ts +224 -0
  187. package/dist/providers/spec/evaluator.d.ts +45 -0
  188. package/dist/providers/spec/fsm-driver.d.ts +449 -0
  189. package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
  190. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  191. package/dist/providers/spec/fsm-types.d.ts +202 -0
  192. package/dist/providers/spec/native-history-executor.d.ts +50 -0
  193. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  194. package/dist/providers/spec/route.d.ts +4 -0
  195. package/dist/providers/spec/types.d.ts +244 -0
  196. package/dist/providers/status-monitor.d.ts +7 -7
  197. package/dist/providers/transcript-v2.d.ts +176 -0
  198. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  199. package/dist/providers/working-dir.d.ts +17 -0
  200. package/dist/repo-mesh-types.d.ts +629 -14
  201. package/dist/runtime-defaults.d.ts +2 -0
  202. package/dist/session-host/managed-host.d.ts +64 -0
  203. package/dist/session-host/runtime-surface.d.ts +10 -16
  204. package/dist/sessions/registry.d.ts +26 -0
  205. package/dist/shared-types-extra.d.ts +2 -4
  206. package/dist/shared-types.d.ts +110 -55
  207. package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
  208. package/dist/status/normalize.d.ts +1 -1
  209. package/dist/status/normalize.js +1 -0
  210. package/dist/status/normalize.js.map +1 -1
  211. package/dist/status/normalize.mjs +1 -0
  212. package/dist/status/normalize.mjs.map +1 -1
  213. package/dist/status/reporter.d.ts +2 -0
  214. package/dist/status/snapshot.d.ts +27 -0
  215. package/dist/system/hash.d.ts +8 -0
  216. package/dist/system/load-better-sqlite3.d.ts +21 -0
  217. package/dist/types.d.ts +20 -6
  218. package/package.json +11 -4
  219. package/src/agent-stream/poller.ts +2 -3
  220. package/src/agent-stream/provider-adapter.ts +1 -1
  221. package/src/boot/daemon-lifecycle.ts +79 -14
  222. package/src/boot/process-hardening.ts +89 -0
  223. package/src/build-info.ts +73 -0
  224. package/src/chat/source-machine.ts +534 -0
  225. package/src/chat/source-resolver.ts +0 -0
  226. package/src/chat/subscription-updates.ts +20 -1
  227. package/src/cli-adapter-types.d.ts +3 -1
  228. package/src/cli-adapter-types.ts +85 -2
  229. package/src/cli-adapters/cli-script-runner.ts +421 -0
  230. package/src/cli-adapters/cli-state-engine.ts +1379 -0
  231. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  232. package/src/cli-adapters/provider-cli-adapter.ts +1059 -1143
  233. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  234. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  235. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  236. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  237. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  238. package/src/cli-adapters/pty-transport.ts +2 -1
  239. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  240. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  241. package/src/cli-adapters/resolve-executable.ts +204 -0
  242. package/src/cli-adapters/session-host-transport.ts +2 -1
  243. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  244. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  245. package/src/cli-adapters/terminal-screen.ts +16 -81
  246. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  247. package/src/commands/chat-commands-read.ts +2788 -0
  248. package/src/commands/chat-commands-scope.ts +54 -0
  249. package/src/commands/chat-commands-shared.ts +114 -0
  250. package/src/commands/chat-commands-write.ts +891 -0
  251. package/src/commands/chat-commands.ts +19 -1841
  252. package/src/commands/cli-manager.d.ts +2 -0
  253. package/src/commands/cli-manager.ts +724 -27
  254. package/src/commands/handler.ts +865 -2
  255. package/src/commands/high-family/index.ts +28 -0
  256. package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
  257. package/src/commands/high-family/mesh-events.ts +102 -0
  258. package/src/commands/high-family/mesh-status.ts +857 -0
  259. package/src/commands/high-family/types.ts +80 -0
  260. package/src/commands/low-family/coordinator-prompt.ts +125 -0
  261. package/src/commands/low-family/daemon-lifecycle.ts +120 -0
  262. package/src/commands/low-family/diagnostics.ts +57 -0
  263. package/src/commands/low-family/index.ts +37 -0
  264. package/src/commands/low-family/mesh-ledger.ts +62 -0
  265. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  266. package/src/commands/low-family/notification.ts +116 -0
  267. package/src/commands/low-family/refine-config.ts +106 -0
  268. package/src/commands/low-family/session-host.ts +282 -0
  269. package/src/commands/low-family/spec-providerdev.ts +217 -0
  270. package/src/commands/low-family/status-meta.ts +112 -0
  271. package/src/commands/low-family/types.ts +39 -0
  272. package/src/commands/med-family/cli-agent.ts +270 -0
  273. package/src/commands/med-family/fast-forward.ts +230 -0
  274. package/src/commands/med-family/ide.ts +163 -0
  275. package/src/commands/med-family/index.ts +37 -0
  276. package/src/commands/med-family/mesh-crud.ts +1309 -0
  277. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  278. package/src/commands/med-family/mesh-queue.ts +167 -0
  279. package/src/commands/med-family/mesh-restart.ts +92 -0
  280. package/src/commands/med-family/types.ts +153 -0
  281. package/src/commands/mesh-coordinator.ts +334 -124
  282. package/src/commands/router-aggregate-status.ts +209 -0
  283. package/src/commands/router-mesh-session-owner.ts +114 -0
  284. package/src/commands/router-refine.ts +1794 -0
  285. package/src/commands/router-worktree-cleanup.ts +870 -0
  286. package/src/commands/router.ts +567 -4149
  287. package/src/commands/stream-commands.ts +8 -0
  288. package/src/commands/upgrade-helper.ts +310 -45
  289. package/src/config/chat-history.ts +517 -24
  290. package/src/config/config.ts +30 -0
  291. package/src/config/mesh-config.ts +300 -24
  292. package/src/config/mesh-json-config.ts +376 -0
  293. package/src/config/recent-activity.ts +8 -2
  294. package/src/config/registry-resolver.ts +100 -0
  295. package/src/config/repo-settings.ts +111 -0
  296. package/src/config/state-store.ts +55 -0
  297. package/src/daemon/dev-auto-implement.ts +3 -2
  298. package/src/daemon/dev-cli-debug.ts +10 -1
  299. package/src/daemon/dev-server.ts +0 -541
  300. package/src/detection/cli-detector.ts +94 -9
  301. package/src/detection/ide-detector.ts +55 -16
  302. package/src/detection/win32-ide-version.ts +106 -0
  303. package/src/git/change-impact-config.ts +354 -0
  304. package/src/git/git-commands.ts +112 -16
  305. package/src/git/git-diff.ts +81 -11
  306. package/src/git/git-executor.ts +12 -0
  307. package/src/git/git-status.ts +822 -48
  308. package/src/git/git-types.ts +14 -62
  309. package/src/git/git-worktree.ts +261 -4
  310. package/src/git/index.ts +17 -0
  311. package/src/index.ts +191 -13
  312. package/src/installer.d.ts +1 -1
  313. package/src/installer.ts +8 -6
  314. package/src/ipc/local-ipc-server.ts +278 -0
  315. package/src/launch.d.ts +1 -1
  316. package/src/launch.ts +37 -28
  317. package/src/logging/async-batch-writer.ts +55 -0
  318. package/src/logging/command-log.ts +7 -5
  319. package/src/logging/debug-config.ts +25 -0
  320. package/src/logging/debug-trace.ts +7 -2
  321. package/src/logging/log-redactor.ts +100 -0
  322. package/src/logging/log-tail-reader.ts +341 -0
  323. package/src/logging/logger.ts +14 -7
  324. package/src/mesh/contracts.ts +510 -0
  325. package/src/mesh/coordinator-prompt.ts +626 -34
  326. package/src/mesh/coordinator-registry.ts +121 -0
  327. package/src/mesh/mesh-active-work.ts +706 -0
  328. package/src/mesh/mesh-clone-grace.ts +68 -0
  329. package/src/mesh/mesh-completion-synthesis.ts +416 -0
  330. package/src/mesh/mesh-coordinator-config.ts +97 -0
  331. package/src/mesh/mesh-delivery-policy.ts +284 -0
  332. package/src/mesh/mesh-event-classify.ts +78 -0
  333. package/src/mesh/mesh-event-forwarding.ts +2042 -0
  334. package/src/mesh/mesh-event-trace.ts +67 -0
  335. package/src/mesh/mesh-events-coordinator.ts +32 -0
  336. package/src/mesh/mesh-events-pending.ts +1548 -0
  337. package/src/mesh/mesh-events-stale.ts +452 -0
  338. package/src/mesh/mesh-events-utils.ts +443 -0
  339. package/src/mesh/mesh-events.ts +43 -1035
  340. package/src/mesh/mesh-fast-forward.ts +856 -0
  341. package/src/mesh/mesh-host-ownership.ts +41 -3
  342. package/src/mesh/mesh-idle-reminder.ts +153 -0
  343. package/src/mesh/mesh-init.ts +350 -0
  344. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  345. package/src/mesh/mesh-ledger.ts +897 -102
  346. package/src/mesh/mesh-magi-status.ts +223 -0
  347. package/src/mesh/mesh-missions.ts +695 -0
  348. package/src/mesh/mesh-node-identity.ts +2065 -0
  349. package/src/mesh/mesh-queue-assignment.ts +2498 -0
  350. package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
  351. package/src/mesh/mesh-reconcile-config.ts +66 -0
  352. package/src/mesh/mesh-reconcile-identity.ts +103 -0
  353. package/src/mesh/mesh-reconcile-loop.ts +1709 -0
  354. package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
  355. package/src/mesh/mesh-refine-batch.ts +205 -0
  356. package/src/mesh/mesh-refine-gates.ts +1792 -0
  357. package/src/mesh/mesh-refine-status.ts +231 -0
  358. package/src/mesh/mesh-remote-event-pull.ts +286 -0
  359. package/src/mesh/mesh-review-inbox.ts +308 -0
  360. package/src/mesh/mesh-routing.ts +291 -0
  361. package/src/mesh/mesh-runtime-store.ts +2318 -0
  362. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  363. package/src/mesh/mesh-task-inflight.ts +70 -0
  364. package/src/mesh/mesh-task-stats.ts +161 -0
  365. package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
  366. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  367. package/src/mesh/mesh-work-queue.ts +1489 -145
  368. package/src/mesh/p2p-relay-failure.ts +2 -2
  369. package/src/mesh/preview-freshness.ts +163 -0
  370. package/src/mesh/refine-config.ts +129 -12
  371. package/src/mesh/worktree-bootstrap-config.ts +599 -0
  372. package/src/providers/acp-provider-instance.ts +43 -10
  373. package/src/providers/approval-utils.d.ts +5 -0
  374. package/src/providers/approval-utils.ts +100 -6
  375. package/src/providers/chat-message-normalization.ts +126 -4
  376. package/src/providers/cli-provider-effect-format.ts +53 -0
  377. package/src/providers/cli-provider-history-dedup.ts +75 -0
  378. package/src/providers/cli-provider-input-prompt.ts +133 -0
  379. package/src/providers/cli-provider-instance-types.ts +150 -0
  380. package/src/providers/cli-provider-instance.ts +2914 -498
  381. package/src/providers/cli-provider-status-helpers.ts +123 -0
  382. package/src/providers/cli-provider-transcript-merge.ts +114 -0
  383. package/src/providers/contracts.d.ts +55 -0
  384. package/src/providers/contracts.ts +207 -6
  385. package/src/providers/extension-provider-instance.ts +12 -7
  386. package/src/providers/external-sources.ts +218 -0
  387. package/src/providers/ide-provider-instance.ts +35 -12
  388. package/src/providers/manual-attendance.ts +105 -0
  389. package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
  390. package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
  391. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  392. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  393. package/src/providers/native-history/constants.ts +19 -0
  394. package/src/providers/native-history/dispatcher.ts +617 -0
  395. package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
  396. package/src/providers/native-history/index.ts +30 -0
  397. package/src/providers/provider-instance-manager.ts +71 -0
  398. package/src/providers/provider-instance.ts +24 -1
  399. package/src/providers/provider-loader.ts +700 -55
  400. package/src/providers/provider-schema.ts +93 -14
  401. package/src/providers/provider-trust.ts +114 -0
  402. package/src/providers/read-chat-contract.ts +76 -16
  403. package/src/providers/sdk/README.md +49 -0
  404. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  405. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  406. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  407. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
  408. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  409. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  410. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  411. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  412. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  413. package/src/providers/sdk/v1/index.ts +152 -0
  414. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  415. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  416. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  417. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
  418. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  419. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  420. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  421. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  422. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  423. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  424. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  425. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  426. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  427. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  428. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  429. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  430. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  431. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  432. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  433. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  434. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  435. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  436. package/src/providers/sdk/v1/validators/index.ts +19 -0
  437. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  438. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  439. package/src/providers/spec/adapter.ts +246 -0
  440. package/src/providers/spec/background-task-detector.ts +178 -0
  441. package/src/providers/spec/cli-adapter.ts +1090 -0
  442. package/src/providers/spec/evaluator.ts +407 -0
  443. package/src/providers/spec/fsm-driver.ts +1528 -0
  444. package/src/providers/spec/fsm-evaluator.ts +290 -0
  445. package/src/providers/spec/fsm-loader.ts +128 -0
  446. package/src/providers/spec/fsm-types.ts +301 -0
  447. package/src/providers/spec/native-history-executor.ts +1256 -0
  448. package/src/providers/spec/pre-launch-trust.ts +104 -0
  449. package/src/providers/spec/route.ts +51 -0
  450. package/src/providers/spec/types.ts +284 -0
  451. package/src/providers/status-monitor.d.ts +7 -7
  452. package/src/providers/status-monitor.ts +37 -22
  453. package/src/providers/transcript-v2.ts +567 -0
  454. package/src/providers/types/interactive-prompt.ts +536 -0
  455. package/src/providers/version-archive.ts +64 -24
  456. package/src/providers/working-dir.ts +23 -0
  457. package/src/repo-mesh-types.ts +877 -16
  458. package/src/runtime-defaults.ts +39 -0
  459. package/src/session-host/managed-host.ts +218 -0
  460. package/src/session-host/runtime-surface.ts +20 -80
  461. package/src/sessions/registry.ts +44 -0
  462. package/src/shared-types-extra.ts +2 -4
  463. package/src/shared-types.d.ts +8 -0
  464. package/src/shared-types.ts +148 -55
  465. package/src/status/builders.ts +69 -6
  466. package/src/status/chat-tail-hot-sessions.ts +117 -2
  467. package/src/status/normalize.ts +2 -0
  468. package/src/status/reporter.ts +19 -1
  469. package/src/status/snapshot.ts +100 -27
  470. package/src/system/hash.ts +23 -0
  471. package/src/system/host-memory.ts +29 -12
  472. package/src/system/load-better-sqlite3.ts +68 -0
  473. package/src/types.ts +26 -6
  474. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  475. package/dist/mesh/mesh-sync.d.ts +0 -53
  476. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  477. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  478. package/src/mesh/mesh-sync.ts +0 -111
@@ -15,16 +15,24 @@
15
15
  */
16
16
 
17
17
  import * as os from 'os';
18
- import type { CliAdapter } from '../cli-adapter-types.js';
18
+ import type { CliAdapter, CliLaunchInfo } from '../cli-adapter-types.js';
19
+ import type { InteractivePromptResponse } from '../providers/types/interactive-prompt.js';
19
20
  import { LOG } from '../logging/logger.js';
20
21
  import { getDebugRuntimeConfig } from '../logging/debug-config.js';
21
22
  import { TerminalScreen } from './terminal-screen.js';
23
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
22
24
  import {
23
25
  NodePtyTransportFactory,
24
26
  type PtyRuntimeMetadata,
25
27
  type PtyRuntimeTransport,
26
28
  type PtyTransportFactory,
27
29
  } from './pty-transport.js';
30
+ import {
31
+ WIN32_PTY_WRITE_CHUNK_CHARS,
32
+ WIN32_PTY_WRITE_CHUNK_GAP_MS,
33
+ chunkPreservingSurrogates,
34
+ shouldChunkWin32Write,
35
+ } from './pty-write-chunking.js';
28
36
  import {
29
37
  buildCliScreenSnapshot,
30
38
  compactPromptText,
@@ -44,6 +52,13 @@ import {
44
52
  type CliTraceEntry,
45
53
  type ParsedSession,
46
54
  } from './provider-cli-shared.js';
55
+ import { CliScriptRunner } from './cli-script-runner.js';
56
+ import {
57
+ CliStateEngine,
58
+ type CliBufferSnapshot,
59
+ type CliTransportAccess,
60
+ type CliStateEngineCallbacks,
61
+ } from './cli-state-engine.js';
47
62
  import {
48
63
  buildCliParseInput,
49
64
  buildCliTraceParseSnapshot,
@@ -78,24 +93,6 @@ export {
78
93
  } from './provider-cli-shared.js';
79
94
 
80
95
 
81
- interface IdleFinishCandidate {
82
- armedAt: number;
83
- lastOutputAt: number;
84
- lastScreenChangeAt: number;
85
- responseEpoch: number;
86
- assistantLength: number;
87
- }
88
-
89
- interface SettledEvalContext {
90
- now: number;
91
- modal: any;
92
- status: string;
93
- parsedMessages: CliChatMessage[];
94
- lastParsedAssistant: CliChatMessage | undefined;
95
- parsedStatus: string | null;
96
- prevStatus: string;
97
- }
98
-
99
96
  interface SendMessageState {
100
97
  text: string;
101
98
  normalizedPromptSnippet: string;
@@ -103,6 +100,10 @@ interface SendMessageState {
103
100
  maxEchoWaitMs: number;
104
101
  retryDelayMs: number;
105
102
  didCommitUserTurn: boolean;
103
+ // Whether this was the session's first turn at the moment of dispatch — captured
104
+ // before commitSendUserTurn flips this.firstTurnSent, so a later stuck-retry still
105
+ // knows it is recovering the win32 premature-ready first-turn swallow.
106
+ isFirstTurn: boolean;
106
107
  }
107
108
 
108
109
  interface SendMessageCompletion {
@@ -110,6 +111,18 @@ interface SendMessageCompletion {
110
111
  rejectOnce: (error: unknown) => void;
111
112
  }
112
113
 
114
+ interface PendingOutboundMessage {
115
+ id: string;
116
+ role: 'user';
117
+ content: string;
118
+ queuedAt: number;
119
+ source: 'sendMessage';
120
+ // ARCH-REFACTOR R1: the mesh taskId this queued message belongs to. Carried so that
121
+ // when the queue is flushed (once the prior turn settles) the new turn is bound to
122
+ // its OWN task, not whatever scalar the session happens to hold at flush time.
123
+ meshTaskId?: string;
124
+ }
125
+
113
126
  export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
114
127
  if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
115
128
  if (maxChars <= 0) return '';
@@ -119,27 +132,66 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
119
132
  return current.slice(-keepFromCurrent) + chunk;
120
133
  }
121
134
 
135
+ // Force-send (mesh coordinator dispatch / reconcile redelivery) writes raw
136
+ // keystrokes straight into the PTY, bypassing the normal echo-wait submit
137
+ // pipeline.
138
+ //
139
+ // History of this path's failure modes:
140
+ // 1. Originally it wrote `text + sendKey` in a single PTY write with zero
141
+ // settle time. Injected into an idle TUI input box that was still entering
142
+ // its input-accepting state, the trailing submit key could be swallowed —
143
+ // prompt typed but never submitted ("text injected, Enter not pressed").
144
+ // 2. The first fix split inject from submit: write(text) → echo-gated settle →
145
+ // write(sendKey) as a *separate* PTY write. That broke win32: ConPTY/TUI
146
+ // does not recognize a lone '\r' that arrives in its own PTY chunk
147
+ // 150ms+ after the text as a submit key, and the win32 echo gate never
148
+ // matched (ConPTY echo/parsing differences), so the cap was burned and the
149
+ // same detached lone CR was emitted — adding latency while the Enter still
150
+ // got swallowed.
151
+ //
152
+ // Current behavior: keep a fixed settle gap so the input handler is in its
153
+ // input-accepting state, then write `content + sendKey` as a SINGLE atomic PTY
154
+ // write — the same verified mechanism the normal `immediate` submit strategy
155
+ // uses (submitImmediatePrompt). With the Enter in the same write unit as the
156
+ // text, win32 ConPTY always sees it as a submit and the race that the split was
157
+ // trying to solve cannot happen (the Enter can never be separated from the
158
+ // text). FORCE_SUBMIT_SETTLE_MS is that minimum pre-submit gap.
159
+ const FORCE_SUBMIT_SETTLE_MS = 150;
160
+
122
161
  // ─── Adapter ────────────────────────────────────────
123
162
 
124
163
  export class ProviderCliAdapter implements CliAdapter {
125
- readonly cliType: string;
164
+ cliType: string;
126
165
  readonly cliName: string;
127
166
  public workingDir: string;
128
167
 
129
168
  private provider: CliProviderModule;
130
169
  private ptyProcess: PtyRuntimeTransport | null = null;
131
170
  private transportFactory: PtyTransportFactory;
132
- private currentStatus: CliSessionStatus['status'] = 'starting';
133
171
  private onStatusChange: (() => void) | null = null;
172
+ // FALSE-IDLE (Fix 2): probe the owning instance for the post-approval resume grace.
173
+ // Null until the instance registers it; the engine treats absence as "not in grace".
174
+ private inApprovalResumeGraceProbe: (() => boolean) | null = null;
175
+
176
+ // ─── State machine engine ─────────────────────────
177
+ readonly engine: CliStateEngine;
134
178
 
135
179
  private responseBuffer = '';
136
180
  private recentOutputBuffer = '';
137
- private isWaitingForResponse = false;
138
- private activeModal: { message: string; buttons: string[] } | null = null;
139
- private parseErrorMessage: string | null = null;
181
+ private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
182
+ private providerSessionId: string | null = null;
140
183
  private responseTimeout: NodeJS.Timeout | null = null;
141
- private idleTimeout: NodeJS.Timeout | null = null;
142
184
  private ready = false;
185
+ // WIN32-READY-HOLD: the ready barrier can release on screen/spec-FSM grace
186
+ // before win32 ConPTY's input layer is live. The first split write (text, then a
187
+ // separate trailing CR via waitForEchoAndSubmit) then has its submit CR swallowed,
188
+ // and every CR-only retry re-sends a bare CR the input layer keeps dropping — the
189
+ // first message is typed-but-never-submitted and lost. Routing only the FIRST turn
190
+ // through the atomic content+sendKey single write (submitImmediatePrompt) keeps the
191
+ // Enter in the same PTY write unit as the text, the invariant win32 ConPTY needs to
192
+ // recognize a submit, so the swallow is bypassed. Subsequent turns (input layer now
193
+ // proven live) keep the normal echo-gated path. Flips true on first committed turn.
194
+ private firstTurnSent = false;
143
195
  private startupBuffer = '';
144
196
  private startupParseGate = false;
145
197
  private startupSettleTimer: NodeJS.Timeout | null = null;
@@ -159,44 +211,71 @@ export class ProviderCliAdapter implements CliAdapter {
159
211
  private lastScreenSnapshot = '';
160
212
  private lastScreenText = '';
161
213
  private lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
214
+ // (FALSEIDLE Path-C) Count of CONSECUTIVE getStatus polls that observed a
215
+ // gate-eligible static-idle screen (detect=idle, no modal, quiet, empty
216
+ // partial buffer). For a mesh/autonomous worker we require several such
217
+ // polls in a row before confirming static-idle (see getStatus), so a
218
+ // single momentarily-silent point-sample of a still-live turn cannot flip
219
+ // it. Reset to 0 the instant any poll is ineligible.
220
+ private staticIdlePollStreak = 0;
162
221
 
163
222
  // Server log forwarding
164
223
  private serverConn: any = null;
165
224
  private logBuffer: { message: string; level: string }[] = [];
166
225
 
167
- // Approval cooldown
168
- private lastApprovalResolvedAt: number = 0;
169
-
170
- // Approval state machine
171
- private approvalTransitionBuffer: string = '';
172
- private approvalExitTimeout: NodeJS.Timeout | null = null;
173
- private pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
174
- private pendingScriptStatusSince = 0;
175
- private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
176
-
177
- // Output settle debounce — fires after PTY output goes quiet
178
- private settleTimer: NodeJS.Timeout | null = null;
179
- private settledBuffer: string = '';
180
- private submitPendingUntil = 0;
181
- private responseSettleIgnoreUntil = 0;
182
- private responseEpoch = 0;
226
+ private pendingOutboundQueue: PendingOutboundMessage[] = [];
227
+ private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
228
+ private pendingOutboundFlushInFlight = false;
229
+ // Submit retry timer — PTY-level, not state machine
183
230
  private submitRetryTimer: NodeJS.Timeout | null = null;
184
- private submitRetryUsed = false;
185
- private submitRetryPromptSnippet = '';
186
- private idleFinishCandidate: IdleFinishCandidate | null = null;
187
- private finishRetryTimer: NodeJS.Timeout | null = null;
188
- private finishRetryCount = 0;
231
+
232
+ // PTY-WRITE-SERIALIZE: a single per-session tail promise that serializes every
233
+ // PTY write. Each writeToPty() call chains its actual write after the previous
234
+ // one and returns a promise that resolves only after ITS write completes, so
235
+ // two consecutive sends (e.g. mesh force-dispatch burst → forceSendMessage, or
236
+ // a retry-timer's bare CR interleaving a fresh body) can never write into the
237
+ // same input line: message A's (body + sendKey/CR) is fully written to the PTY
238
+ // before message B's body write starts. The chain is kept alive across errors
239
+ // (each link swallows/logs its own failure) so one failed write never wedges
240
+ // all later writes behind a permanently-rejected tail.
241
+ private ptyWriteChain: Promise<void> = Promise.resolve();
189
242
 
190
243
  // Resize redraw suppression
191
244
  private resizeSuppressUntil: number = 0;
192
245
 
193
- // Debug: status transition history
194
- private statusHistory: { status: string; at: number; trigger?: string }[] = [];
246
+ // (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
247
+ // ChatSourceMachine holds the lock by state, not by a mutable field on
248
+ // the adapter. Removed entirely; no callers remain after the readChat
249
+ // ladder was replaced.
250
+
251
+ // ─── Script runner (parsing isolated here, adapter stays as transport) ───
252
+ private readonly runner: CliScriptRunner;
253
+ /** @deprecated use runner.cliScripts for direct script access */
254
+ get cliScripts(): CliScripts { return this.runner.cliScripts; }
255
+
256
+ /**
257
+ * Recent script invocations (oldest → newest). Exposed via the
258
+ * `get_chat_debug_bundle` daemon command so anyone debugging a
259
+ * stuck-status regression can read what each detectStatus /
260
+ * parseSession call actually saw and returned, instead of having
261
+ * to instrument the daemon.
262
+ */
263
+ getScriptInvocationTrace() {
264
+ return this.runner.getInvocationTrace();
265
+ }
195
266
 
196
- // ─── CLI Scripts (script-based parsing) ───
197
- private cliScripts: CliScripts;
198
- /** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
199
- private scriptState: unknown = null;
267
+ /**
268
+ * Returns the full raw PTY byte stream captured since adapter start.
269
+ * Used by the `record_provider_pty` IPC command to produce fixtures.
270
+ * Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
271
+ */
272
+ getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
273
+ return {
274
+ text: this.accumulatedRawBuffer,
275
+ droppedChars: this.accumulatedRawBufferDroppedChars,
276
+ };
277
+ }
278
+ set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
200
279
  private runtimeSettings: Record<string, any> = {};
201
280
  /** Full accumulated rendered PTY transcript for parser/readback use */
202
281
  private accumulatedBuffer: string = '';
@@ -205,7 +284,7 @@ export class ProviderCliAdapter implements CliAdapter {
205
284
  /** Full accumulated raw PTY output (with ANSI) */
206
285
  private accumulatedRawBuffer: string = '';
207
286
  /** Current visible terminal screen snapshot */
208
- private terminalScreen = new TerminalScreen(24, 80);
287
+ private terminalScreen = new TerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
209
288
  private static readonly MAX_RESPONSE_BUFFER = 8000;
210
289
  private static readonly MAX_RECENT_OUTPUT_BUFFER = 1000;
211
290
  private responseBufferDroppedChars = 0;
@@ -216,10 +295,6 @@ export class ProviderCliAdapter implements CliAdapter {
216
295
  * Hermes turn (tool calls + reasoning + final bubble) without the
217
296
  * rolling window pushing the turn's ╭─ opening line out of view. */
218
297
  private static readonly MAX_ACCUMULATED_BUFFER = 262144;
219
- private currentTurnScope: TurnParseScope | null = null;
220
- private traceEntries: CliTraceEntry[] = [];
221
- private traceSeq = 0;
222
- private traceSessionId = '';
223
298
  private parsedStatusCache: {
224
299
  responseBuffer: string;
225
300
  currentTurnScope: TurnParseScope | null;
@@ -233,11 +308,15 @@ export class ProviderCliAdapter implements CliAdapter {
233
308
  result: any;
234
309
  } | null = null;
235
310
  private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
236
- private static readonly MAX_TRACE_ENTRIES = 250;
311
+ // (FALSEIDLE Path-C) Consecutive gate-eligible getStatus polls a mesh/autonomous
312
+ // session must show before the poll-static-idle confirm fires. 2 = one extra
313
+ // status tick of hysteresis: enough to reject a single momentary-silence
314
+ // point-sample of a still-live turn, cheap enough not to materially delay a
315
+ // genuine boot-wedge release (the wedge screen is stably static, so it clears
316
+ // every consecutive poll and confirms on the 2nd).
317
+ private static readonly STATIC_IDLE_POLL_CONFIRM_COUNT = 2;
237
318
 
238
319
  private readonly providerResolutionMeta: ProviderResolutionMeta;
239
- private static readonly FINISH_RETRY_DELAY_MS = 300;
240
- private static readonly MAX_FINISH_RETRIES = 2;
241
320
 
242
321
  private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
243
322
  const build = (droppedChars: number, maxChars: number) => droppedChars > 0
@@ -313,13 +392,13 @@ export class ProviderCliAdapter implements CliAdapter {
313
392
  if (
314
393
  cached
315
394
  && cached.responseBuffer === this.responseBuffer
316
- && cached.currentTurnScope === this.currentTurnScope
395
+ && cached.currentTurnScope === this.engine.currentTurnScope
317
396
  && cached.recentOutputBuffer === this.recentOutputBuffer
318
397
  && cached.accumulatedBuffer === this.accumulatedBuffer
319
398
  && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
320
399
  && cached.screenText === this.lastScreenText
321
- && cached.currentStatus === this.currentStatus
322
- && cached.activeModal === this.activeModal
400
+ && cached.currentStatus === this.engine.currentStatus
401
+ && cached.activeModal === this.engine.activeModal
323
402
  && cached.cliName === this.cliName
324
403
  ) {
325
404
  return cached.result;
@@ -343,84 +422,18 @@ export class ProviderCliAdapter implements CliAdapter {
343
422
  return this.timeouts.statusActivityHold;
344
423
  }
345
424
 
346
- private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
347
- const prev = this.currentStatus;
348
- if (prev === status) return;
349
- this.currentStatus = status;
350
- this.statusHistory.push({ status, at: Date.now(), trigger });
351
- if (this.statusHistory.length > 50) this.statusHistory.shift();
352
- this.recordTrace('status', {
353
- previousStatus: prev,
354
- trigger: trigger || null,
355
- });
356
- LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
357
- }
358
-
359
- private clearIdleFinishCandidate(reason: string): void {
360
- if (!this.idleFinishCandidate) return;
361
- this.recordTrace('idle_candidate_reset', {
362
- reason,
363
- candidate: this.idleFinishCandidate,
364
- });
365
- this.idleFinishCandidate = null;
366
- }
367
-
368
- private armIdleFinishCandidate(assistantLength: number): void {
369
- const now = Date.now();
370
- const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
371
- this.idleFinishCandidate = {
372
- armedAt: now,
373
- lastOutputAt: this.lastOutputAt,
374
- lastScreenChangeAt: this.lastScreenChangeAt,
375
- responseEpoch: this.responseEpoch,
376
- assistantLength,
377
- };
378
- this.recordTrace('idle_candidate_armed', {
379
- confirmMs: idleFinishConfirmMs,
380
- candidate: this.idleFinishCandidate,
381
- ...buildCliTraceParseSnapshot({
382
- accumulatedBuffer: this.accumulatedBuffer,
383
- accumulatedRawBuffer: this.accumulatedRawBuffer,
384
- responseBuffer: this.responseBuffer,
385
- partialResponse: this.responseBuffer,
386
- scope: this.currentTurnScope,
387
- }),
388
- });
389
- if (this.settleTimer) clearTimeout(this.settleTimer);
390
- this.settleTimer = setTimeout(() => {
391
- this.settleTimer = null;
392
- this.settledBuffer = this.recentOutputBuffer;
393
- this.evaluateSettled();
394
- }, idleFinishConfirmMs);
395
- }
396
-
397
-
398
- private recordTrace(type: string, payload: Record<string, any> = {}): void {
399
- const entry: CliTraceEntry = {
400
- id: ++this.traceSeq,
401
- at: Date.now(),
402
- type,
403
- status: this.currentStatus,
404
- isWaitingForResponse: this.isWaitingForResponse,
405
- activeModal: this.activeModal
406
- ? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] }
407
- : null,
408
- payload,
409
- };
410
- this.traceEntries.push(entry);
411
- if (this.traceEntries.length > ProviderCliAdapter.MAX_TRACE_ENTRIES) {
412
- this.traceEntries.splice(0, this.traceEntries.length - ProviderCliAdapter.MAX_TRACE_ENTRIES);
413
- }
414
- }
415
-
416
- private resetTraceSession(): void {
417
- this.traceEntries = [];
418
- this.traceSeq = 0;
419
- this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
420
- this.recordTrace('session_start', {
421
- providerType: this.cliType,
422
- workingDir: this.workingDir,
423
- });
425
+ // (FALSEIDLE Path-C) Whether this session is a mesh worker or coordinator's
426
+ // own autonomous session. Mirrors CliProviderInstance.isAutonomousMeshSession
427
+ // over the runtimeSettings the instance mirrors down via updateRuntimeSettings
428
+ // (meshNodeFor / meshActiveTaskId / meshNodeId / launchedByCoordinator =
429
+ // isMeshWorkerSession, plus meshCoordinatorFor for the coordinator's own turn).
430
+ // Such a session has no human at the keyboard to correct a premature idle, so
431
+ // the poll-static-idle confirm is debounced for it (multiple consecutive idle
432
+ // polls) rather than fired on a single point-sample.
433
+ private isAutonomousMeshSession(): boolean {
434
+ const s = this.runtimeSettings;
435
+ return !!(s?.meshNodeFor || s?.meshActiveTaskId || s?.meshNodeId
436
+ || s?.launchedByCoordinator || s?.meshCoordinatorFor);
424
437
  }
425
438
 
426
439
  // Resolved timeouts
@@ -432,7 +445,6 @@ export class ProviderCliAdapter implements CliAdapter {
432
445
  private readonly sendKey: string;
433
446
  private readonly submitStrategy: 'wait_for_echo' | 'immediate';
434
447
  private readonly requirePromptEchoBeforeSubmit: boolean;
435
- private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
436
448
 
437
449
  constructor(
438
450
  provider: CliProviderModule,
@@ -441,6 +453,7 @@ export class ProviderCliAdapter implements CliAdapter {
441
453
  private extraEnv: Record<string, string> = {},
442
454
  transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
443
455
  ) {
456
+ this.runner = new CliScriptRunner(provider.type);
444
457
  this.provider = provider;
445
458
  this.transportFactory = transportFactory;
446
459
  this.cliType = provider.type;
@@ -458,10 +471,34 @@ export class ProviderCliAdapter implements CliAdapter {
458
471
  this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
459
472
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
460
473
 
461
- // Scripts are requiredloaded by ProviderLoader via compatibility array
462
- this.cliScripts = provider.scripts || {};
463
- this.scriptState = typeof this.cliScripts.createState === 'function' ? (this.cliScripts.createState() ?? null) : null;
464
- const scriptNames = listCliScriptNames(this.cliScripts);
474
+ // State machine engineowns all status transitions
475
+ this.engine = new CliStateEngine(
476
+ provider,
477
+ this.runner,
478
+ this as unknown as CliTransportAccess,
479
+ {
480
+ onStatusChange: () => { this.onStatusChange?.(); },
481
+ onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
482
+ onTurnCompleted: () => { this.responseBuffer = ''; },
483
+ isInApprovalResumeGrace: () => this.inApprovalResumeGraceProbe?.() === true,
484
+ } satisfies CliStateEngineCallbacks,
485
+ resolvedConfig.timeouts,
486
+ );
487
+
488
+ // Scripts delegated to CliScriptRunner — adapter stays as transport.
489
+ // Pass the manifest tui block so the runner can build the
490
+ // declarativeDetectStatus / declarativeParseApproval SDK functions
491
+ // that v1 extended-tier overrides depend on (without these, scripts
492
+ // like codex-cli's detect_status v1 fail-closed with return 'idle'
493
+ // and the session sticks in `generating` forever).
494
+ this.runner.setScripts(provider.scripts || {}, provider.tui);
495
+ // Per-invocation wall-clock budget for provider scripts. Manifests may
496
+ // raise this for genuinely slow parsers, but the default (50ms) is the
497
+ // settle-loop safety net — any script that exceeds it gets flagged in
498
+ // the invocation trace and surfaced via the debug bundle so we can
499
+ // identify the offender instead of guessing why the settle loop hangs.
500
+ this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
501
+ const scriptNames = this.runner.getScriptNames();
465
502
  if (scriptNames.length > 0) {
466
503
  LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
467
504
  LOG.info(
@@ -487,14 +524,9 @@ export class ProviderCliAdapter implements CliAdapter {
487
524
 
488
525
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
489
526
  setCliScripts(scripts: CliScripts): void {
490
- this.cliScripts = scripts;
527
+ this.runner.setScripts(scripts);
491
528
  this.parsedStatusCache = null;
492
- this.parseErrorMessage = null;
493
- // Initialize per-session state: createState() is called once here and on script reload.
494
- // The returned object lives until the PTY exits (scriptState = null on exit).
495
- this.scriptState = typeof scripts.createState === 'function' ? (scripts.createState() ?? null) : null;
496
- const scriptNames = listCliScriptNames(scripts);
497
- LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
529
+ LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
498
530
  }
499
531
 
500
532
  /** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
@@ -521,6 +553,13 @@ export class ProviderCliAdapter implements CliAdapter {
521
553
  this.onStatusChange = callback;
522
554
  }
523
555
 
556
+ // FALSE-IDLE (Fix 2): the instance registers its inApprovalResumeGrace judgment so the
557
+ // engine's applyIdle hysteresis can scope itself to autonomous auto-approving sessions
558
+ // without the engine needing any mesh/auto-approve awareness of its own.
559
+ setInApprovalResumeGraceProbe(probe: () => boolean): void {
560
+ this.inApprovalResumeGraceProbe = probe;
561
+ }
562
+
524
563
  setOnPtyData(callback: (data: string) => void): void {
525
564
  this.onPtyDataCallback = callback;
526
565
  }
@@ -548,15 +587,6 @@ export class ProviderCliAdapter implements CliAdapter {
548
587
  });
549
588
 
550
589
  LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
551
- this.resetTraceSession();
552
- this.recordTrace('spawn', {
553
- shellCommand: spawnPlan.shellCmd,
554
- shellArgs: spawnPlan.shellArgs,
555
- cwd: spawnPlan.ptyOptions.cwd,
556
- cols: spawnPlan.ptyOptions.cols,
557
- rows: spawnPlan.ptyOptions.rows,
558
- providerResolution: this.providerResolutionMeta,
559
- });
560
590
 
561
591
  try {
562
592
  this.ptyProcess = this.transportFactory.spawn(
@@ -620,13 +650,12 @@ export class ProviderCliAdapter implements CliAdapter {
620
650
  this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
621
651
  LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
622
652
  this.flushPendingOutputParse();
623
- this.recordTrace('exit', { exitCode });
624
653
  this.ptyProcess = null;
625
- this.setStatus('stopped', 'pty_exit');
654
+ this.engine.onPtyExit();
626
655
  this.ready = false;
627
656
  this.startupParseGate = false;
628
657
  this.spawnAt = 0;
629
- this.scriptState = null;
658
+ this.runner.resetSessionState();
630
659
  this.onStatusChange?.();
631
660
  });
632
661
 
@@ -635,17 +664,14 @@ export class ProviderCliAdapter implements CliAdapter {
635
664
  this.startupBuffer = '';
636
665
  this.startupFirstOutputAt = 0;
637
666
  if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
638
- this.resetTerminalScreen(24, 80);
667
+ this.resetTerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
639
668
  this.pendingTerminalQueryTail = '';
640
- this.currentTurnScope = null;
641
- this.finishRetryCount = 0;
642
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
643
669
  this.ready = false;
670
+ // Each fresh spawn re-enters the premature-ready swallow window — the next
671
+ // turn is again a "first turn" and must use the win32-safe atomic send path.
672
+ this.firstTurnSent = false;
644
673
  await this.ptyProcess.ready;
645
- this.recordTrace('ready', {
646
- runtimeMeta: this.getRuntimeMetadata(),
647
- });
648
- this.setStatus('starting', 'pty_ready');
674
+ this.engine.onSpawnReady();
649
675
  this.scheduleStartupSettleCheck();
650
676
  this.onStatusChange?.();
651
677
  }
@@ -671,11 +697,11 @@ export class ProviderCliAdapter implements CliAdapter {
671
697
  if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
672
698
  this.startupFirstOutputAt = now;
673
699
  }
674
- if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
675
- this.clearIdleFinishCandidate('new_output');
700
+ if (rawData.length > 0 || cleanData.length > 0) {
701
+ this.engine.clearIdleFinishCandidate('new_output');
676
702
  }
677
703
  if (getDebugRuntimeConfig().collectDebugTrace) {
678
- this.recordTrace('output', {
704
+ this.engine.recordExternalTrace('output', {
679
705
  rawLength: rawData.length,
680
706
  cleanLength: cleanData.length,
681
707
  rawPreview: summarizeCliTraceText(rawData, 300),
@@ -687,7 +713,7 @@ export class ProviderCliAdapter implements CliAdapter {
687
713
  this.scheduleStartupSettleCheck();
688
714
  }
689
715
 
690
- if (this.isWaitingForResponse && cleanData) {
716
+ if (this.engine.isWaitingForResponse && cleanData) {
691
717
  const previousResponseLen = this.responseBuffer.length;
692
718
  this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
693
719
  this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
@@ -726,19 +752,19 @@ export class ProviderCliAdapter implements CliAdapter {
726
752
  // Keep turn-scope offsets aligned with the truncated buffer so scoped
727
753
  // parses don't lose the beginning of a long turn (e.g. the Hermes
728
754
  // ╭─ opening line) when the rolling window sheds bytes.
729
- if (this.currentTurnScope) {
755
+ if (this.engine.currentTurnScope) {
730
756
  if (droppedClean > 0) {
731
- this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
757
+ this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
732
758
  }
733
759
  if (droppedRaw > 0) {
734
- this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
760
+ this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
735
761
  }
736
762
  }
737
763
 
738
764
  this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
739
765
 
740
766
  // ─── Script-based status detection
741
- this.scheduleSettle();
767
+ this.engine.scheduleSettle();
742
768
  }
743
769
 
744
770
  private resolveStartupState(
@@ -761,6 +787,11 @@ export class ProviderCliAdapter implements CliAdapter {
761
787
  if (stableMs < 2000) return;
762
788
 
763
789
  const startupModal = this.runParseApproval(this.recentOutputBuffer);
790
+ const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
791
+ if (!startupModal && startupStatus !== 'idle') {
792
+ this.scheduleStartupSettleCheck();
793
+ return;
794
+ }
764
795
  this.startupParseGate = false;
765
796
  if (this.startupSettleTimer) {
766
797
  clearTimeout(this.startupSettleTimer);
@@ -768,20 +799,33 @@ export class ProviderCliAdapter implements CliAdapter {
768
799
  }
769
800
  this.ready = true;
770
801
  if (startupModal) {
771
- this.activeModal = startupModal;
772
- this.setStatus('waiting_approval', `startup_ready:${trigger}`);
802
+ this.engine.activeModal = startupModal;
803
+ this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
773
804
  } else {
774
- if (this.currentStatus === 'waiting_approval' || this.activeModal) {
775
- this.lastApprovalResolvedAt = Date.now();
805
+ if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
806
+ this.engine.lastApprovalResolvedAt = Date.now();
776
807
  }
777
- this.activeModal = null;
778
- this.setStatus('idle', `startup_ready:${trigger}`);
808
+ this.engine.activeModal = null;
809
+ // Clear the in-flight turn flag at the same time we declare
810
+ // startup-idle. Otherwise the next settled evaluation sees
811
+ // isWaitingForResponse=true + recent CLI welcome-screen paints
812
+ // and flips us right back to generating via the hold path
813
+ // (the "startup → generating → idle → generating → idle"
814
+ // flicker the user observed on claude-cli launch).
815
+ this.engine.isWaitingForResponse = false;
816
+ this.engine.currentTurnScope = null;
817
+ this.engine.setStatus('idle', `startup_ready:${trigger}`);
779
818
  }
780
819
  LOG.info(
781
820
  'CLI',
782
821
  `[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
783
822
  );
784
823
  this.onStatusChange?.();
824
+ // Readiness barrier flush: a message queued because the session was not yet
825
+ // ready (sendMessageNow's not_ready_pending_prompt path) has no turn-completion
826
+ // event to trigger its flush. Now that the interactive prompt is up and we are
827
+ // idle, drain it. No-op when the queue is empty or we settled to a modal.
828
+ if (!startupModal) this.schedulePendingOutboundFlush();
785
829
  }
786
830
 
787
831
  private scheduleStartupSettleCheck(): void {
@@ -801,88 +845,6 @@ export class ProviderCliAdapter implements CliAdapter {
801
845
  }, delayMs);
802
846
  }
803
847
 
804
- private scheduleSettle(): void {
805
- if (this.settleTimer) clearTimeout(this.settleTimer);
806
- const settleEpoch = this.responseEpoch;
807
- const delay = Math.max(
808
- this.timeouts.outputSettle,
809
- this.submitPendingUntil > Date.now()
810
- ? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
811
- : 0,
812
- );
813
- this.settleTimer = setTimeout(() => {
814
- this.settleTimer = null;
815
- if (settleEpoch !== this.responseEpoch) return;
816
- this.settledBuffer = this.recentOutputBuffer;
817
- this.evaluateSettled();
818
- }, delay);
819
- }
820
-
821
- private armApprovalExitTimeout(): void {
822
- if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
823
- this.approvalExitTimeout = setTimeout(() => {
824
- if (!this.hasActionableApproval()) return;
825
- const tail = this.recentOutputBuffer;
826
- const screenText = this.terminalScreen.getText() || '';
827
- const modal = this.runParseApproval(tail);
828
- const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
829
- if (stillWaiting) {
830
- if (!modal) {
831
- LOG.warn('CLI', `[${this.cliType}] approval timeout check found no actionable modal; keeping approval state fail-closed`);
832
- this.activeModal = null;
833
- this.onStatusChange?.();
834
- this.armApprovalExitTimeout();
835
- return;
836
- }
837
- this.activeModal = modal;
838
- this.onStatusChange?.();
839
- this.armApprovalExitTimeout();
840
- return;
841
- }
842
- LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
843
- this.activeModal = null;
844
- this.lastApprovalResolvedAt = Date.now();
845
- this.setStatus('idle', 'approval_timeout');
846
- this.onStatusChange?.();
847
- }, 60000);
848
- }
849
-
850
- private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
851
- if (!this.currentTurnScope) return false;
852
- if (this.hasActionableApproval()) return false;
853
- if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
854
- if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
855
-
856
- if (this.runDetectStatus(this.recentOutputBuffer) !== 'idle') return false;
857
-
858
- const now = Date.now();
859
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
860
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
861
- return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
862
- }
863
-
864
- private hasRecentInteractiveActivity(now: number): boolean {
865
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
866
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
867
- const holdMs = this.getStatusActivityHoldMs();
868
- return quietForMs < holdMs
869
- || screenStableMs < holdMs;
870
- }
871
-
872
- private shouldDeferIdleTimeoutFinish(): boolean {
873
- if (!this.isWaitingForResponse || this.hasActionableApproval()) {
874
- return false;
875
- }
876
- const latestStatus = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
877
- if (latestStatus === 'generating') {
878
- this.settledBuffer = this.recentOutputBuffer;
879
- this.evaluateSettled();
880
- return true;
881
- }
882
- return false;
883
- }
884
-
885
-
886
848
  private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
887
849
  const startedAt = Date.now();
888
850
  let loggedWait = false;
@@ -892,7 +854,7 @@ export class ProviderCliAdapter implements CliAdapter {
892
854
  const screenText = this.terminalScreen.getText() || '';
893
855
  const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
894
856
  const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
895
- const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
857
+ const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
896
858
  const interactiveReady = status === 'idle'
897
859
  && stableMs >= 700
898
860
  && recentlyOutput >= 350;
@@ -926,637 +888,84 @@ export class ProviderCliAdapter implements CliAdapter {
926
888
 
927
889
  private clearAllTimers(): void {
928
890
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
929
- if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
930
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
931
891
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
932
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
933
- if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
934
- if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
935
892
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
936
893
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
894
+ this.engine.clearAllTimers();
937
895
  }
938
896
 
939
- private clearStaleIdleResponseGuard(reason: string): boolean {
940
- const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
941
- const isIdle = this.runDetectStatus(this.recentOutputBuffer) === 'idle';
942
- if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
943
- return false;
944
- }
945
- this.clearAllTimers();
946
- this.clearIdleFinishCandidate(reason);
947
- this.responseBuffer = '';
948
- this.isWaitingForResponse = false;
949
- this.responseSettleIgnoreUntil = 0;
950
- this.submitRetryUsed = false;
951
- this.submitRetryPromptSnippet = '';
952
- this.finishRetryCount = 0;
953
- this.currentTurnScope = null;
954
- this.activeModal = null;
955
- this.recordTrace('stale_idle_response_cleared', { reason });
956
- return true;
957
- }
958
-
959
- private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
960
- const raw = String(this.responseBuffer || '').trim();
961
- if (!raw) return false;
962
- const normalizedPrompt = compactPromptText(promptSnippet);
963
- if (!normalizedPrompt) return true;
964
- const normalizedBuffer = compactPromptText(raw);
965
- if (!normalizedBuffer) return false;
966
- if (normalizedBuffer === normalizedPrompt) return false;
967
- if (normalizedBuffer.startsWith(normalizedPrompt)) {
968
- const remainder = normalizedBuffer
969
- .slice(normalizedPrompt.length)
970
- .replace(/[─═\-]+/g, '')
971
- .replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
972
- .replace(/accepteditson\([^)]*\)/gi, '')
973
- .replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
974
- .replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
975
- .replace(/esctointerrupt/gi, '')
976
- .replace(/❯/g, '')
977
- .replace(/^[\s\-–—:;,.!/?]+/, '')
978
- .trim();
979
- return remainder.length > 0;
980
- }
981
- return true;
982
- }
983
-
984
- private evaluateSettled(): void {
985
- const now = Date.now();
986
- if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
987
- const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
988
- if (this.settleTimer) clearTimeout(this.settleTimer);
989
- this.settleTimer = setTimeout(() => {
990
- this.settleTimer = null;
991
- this.settledBuffer = this.recentOutputBuffer;
992
- this.evaluateSettled();
993
- }, delayTime);
994
- return;
995
- }
996
-
997
- this.resolveStartupState('settled');
998
- if (this.startupParseGate) return;
897
+ // ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
999
898
 
1000
- if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.parseErrorMessage) {
1001
- const tail = this.settledBuffer || this.recentOutputBuffer;
1002
- const modal = this.runParseApproval(tail);
1003
- const lightweightStatus = this.cliScripts?.detectStatus
1004
- ? this.runDetectStatus(tail)
1005
- : null;
1006
- if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') {
1007
- return;
1008
- }
1009
- }
1010
-
1011
- const session = this.runParseSession();
1012
- if (!session) return;
1013
-
1014
- const { status, messages, parsedStatus } = session;
1015
- const modal = (session as any).activeModal ?? session.modal ?? null;
1016
- const parsedMessages = normalizeCliParsedMessages(messages, {
1017
- scope: null,
1018
- lastOutputAt: this.lastOutputAt,
1019
- });
1020
-
1021
- if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
1022
-
1023
- const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
1024
- const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
1025
- const screenText = this.terminalScreen.getText() || '';
1026
-
1027
- this.recordTrace('settled', {
1028
- tail: summarizeCliTraceText(this.settledBuffer, 500),
1029
- screenText: summarizeCliTraceText(screenText, 1200),
1030
- detectStatus: status,
1031
- parsedStatus: parsedStatus || null,
1032
- parsedMessageCount: parsedMessages.length,
1033
- parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
1034
- parsedActiveModal: modal,
1035
- approval: modal,
1036
- ...buildCliTraceParseSnapshot({
1037
- accumulatedBuffer: this.accumulatedBuffer,
1038
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1039
- responseBuffer: this.responseBuffer,
1040
- partialResponse: this.responseBuffer,
1041
- scope: this.currentTurnScope,
1042
- }),
899
+ runParseSession(): ParsedSession | null {
900
+ const screenText = this.terminalScreen.getText();
901
+ const parseScreenText = this.getParseScreenText(screenText);
902
+ const tail = this.recentOutputBuffer.slice(-500);
903
+ const input = buildCliParseInput({
904
+ accumulatedBuffer: this.accumulatedBuffer,
905
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
906
+ recentOutputBuffer: this.recentOutputBuffer,
907
+ terminalScreenText: parseScreenText,
908
+ workingDir: this.workingDir,
909
+ providerSessionId: this.providerSessionId || undefined,
910
+ historySessionId: this.providerSessionId || undefined,
911
+ baseMessages: [],
912
+ partialResponse: this.responseBuffer,
913
+ isWaitingForResponse: this.engine.isWaitingForResponse,
914
+ scope: this.engine.currentTurnScope,
915
+ runtimeSettings: this.runtimeSettings,
916
+ spawnAt: this.spawnAt,
1043
917
  });
1044
-
1045
- if (
1046
- this.currentTurnScope
1047
- && !lastParsedAssistant
1048
- && !this.submitRetryUsed
1049
- && this.ptyProcess
1050
- && !this.hasActionableApproval()
1051
- && promptLikelyVisible(screenText, normalizedPromptSnippet)
1052
- && !this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)
1053
- ) {
1054
- this.submitRetryUsed = true;
1055
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1056
- LOG.info('CLI', `[${this.cliType}] Retrying submit key from settled parser (no assistant yet)`);
1057
- this.recordTrace('submit_write', {
1058
- mode: 'settled_retry',
1059
- sendKey: this.sendKey,
1060
- screenText: summarizeCliTraceText(screenText, 500),
1061
- });
1062
- this.ptyProcess.write(this.sendKey);
1063
- if (this.settleTimer) clearTimeout(this.settleTimer);
1064
- this.settleTimer = setTimeout(() => {
1065
- this.settleTimer = null;
1066
- this.settledBuffer = this.recentOutputBuffer;
1067
- this.evaluateSettled();
1068
- }, this.timeouts.outputSettle + 150);
1069
- return;
1070
- }
1071
-
1072
- if (this.currentTurnScope && !lastParsedAssistant) {
1073
- LOG.debug(
1074
- 'CLI',
1075
- `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1076
- );
1077
- }
1078
-
1079
- if (!status) return;
1080
-
1081
- const prevStatus = this.currentStatus;
1082
- const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
1083
-
1084
- if (!this.applyPendingScriptStatusDebounce(ctx)) return;
1085
-
1086
- const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
1087
- LOG.debug(
1088
- 'CLI',
1089
- `[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
1090
- );
1091
-
1092
- const shouldHoldGenerating =
1093
- status === 'idle'
1094
- && this.isWaitingForResponse
1095
- && !modal
1096
- && recentInteractiveActivity
1097
- && !(parsedStatus === 'idle' && !!lastParsedAssistant);
1098
-
1099
- if (shouldHoldGenerating) { this.applyHoldGenerating(ctx, recentInteractiveActivity); return; }
1100
- if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
1101
- if (status === 'generating') { this.applyGenerating(ctx); return; }
1102
- if (status === 'idle') { this.applyIdle(ctx, now); }
1103
- }
1104
-
1105
- // Returns false if the caller should bail out (debounce pending).
1106
- private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
1107
- const { now, status, prevStatus } = ctx;
1108
- const shouldDebounce =
1109
- prevStatus === 'idle'
1110
- && !this.isWaitingForResponse
1111
- && !this.currentTurnScope
1112
- && (status === 'generating' || status === 'waiting_approval');
1113
-
1114
- if (!shouldDebounce) {
1115
- this.pendingScriptStatus = null;
1116
- this.pendingScriptStatusSince = 0;
1117
- if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
1118
- return true;
1119
- }
1120
-
1121
- const armPending = (delayMs: number) => {
1122
- if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
1123
- this.pendingScriptStatusTimer = setTimeout(() => {
1124
- this.pendingScriptStatusTimer = null;
1125
- this.settledBuffer = this.recentOutputBuffer;
1126
- this.evaluateSettled();
1127
- }, delayMs);
1128
- };
1129
-
1130
- if (this.pendingScriptStatus !== status) {
1131
- this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
1132
- this.pendingScriptStatusSince = now;
1133
- armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
1134
- return false;
1135
- }
1136
- const elapsed = now - this.pendingScriptStatusSince;
1137
- if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
1138
- armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
1139
- return false;
1140
- }
1141
- return true;
1142
- }
1143
-
1144
- private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
1145
- const { status } = ctx;
1146
- this.clearIdleFinishCandidate('hold_generating_recent_activity');
1147
- this.setStatus('generating', 'recent_activity_hold');
1148
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1149
- this.idleTimeout = setTimeout(() => {
1150
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1151
- if (this.shouldDeferIdleTimeoutFinish()) return;
1152
- this.finishResponse();
1153
- }
1154
- }, this.timeouts.generatingIdle);
1155
- this.recordTrace('hold_generating_recent_activity', {
1156
- scriptStatus: status,
1157
- recentInteractiveActivity,
1158
- lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1159
- lastScreenChangeAt: this.lastScreenChangeAt,
1160
- holdMs: this.getStatusActivityHoldMs(),
1161
- ...buildCliTraceParseSnapshot({
1162
- accumulatedBuffer: this.accumulatedBuffer,
1163
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1164
- responseBuffer: this.responseBuffer,
1165
- partialResponse: this.responseBuffer,
1166
- scope: this.currentTurnScope,
1167
- }),
918
+ const session = this.runner.parseSession({
919
+ ...input,
920
+ tail,
921
+ tailScreen: buildCliScreenSnapshot(tail),
1168
922
  });
1169
- this.onStatusChange?.();
923
+ if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
924
+ return session;
1170
925
  }
1171
926
 
1172
- private applyWaitingApproval(ctx: SettledEvalContext): void {
1173
- const { modal } = ctx;
1174
- this.clearIdleFinishCandidate('waiting_approval');
1175
- const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
1176
- if (inCooldown && !modal) {
1177
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1178
- this.activeModal = null;
1179
- if (this.isWaitingForResponse) {
1180
- this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
1181
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1182
- this.idleTimeout = setTimeout(() => {
1183
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1184
- if (this.shouldDeferIdleTimeoutFinish()) return;
1185
- this.finishResponse();
1186
- }
1187
- }, this.timeouts.generatingIdle);
1188
- } else {
1189
- this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
1190
- }
1191
- this.onStatusChange?.();
1192
- return;
1193
- }
1194
- if (!inCooldown) {
1195
- if (!modal) {
1196
- LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
1197
- return;
1198
- }
1199
- this.isWaitingForResponse = true;
1200
- this.setStatus('waiting_approval', 'script_detect');
1201
- this.activeModal = modal;
1202
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1203
- this.armApprovalExitTimeout();
1204
- this.onStatusChange?.();
1205
- }
1206
- }
1207
-
1208
- private applyGenerating(ctx: SettledEvalContext): void {
1209
- const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
1210
- this.clearIdleFinishCandidate('generating');
1211
- const screenText = this.terminalScreen.getText() || '';
1212
- const effectiveScreenText = screenText || this.accumulatedBuffer;
1213
- const noActiveTurn = !this.currentTurnScope;
1214
- const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
1215
- const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
1216
- && !!lastParsedAssistant
1217
- ;
1218
- if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
1219
- return;
1220
- }
1221
- if (prevStatus === 'waiting_approval') {
1222
- // Transitioned out of approval → generating
1223
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1224
- this.activeModal = null;
1225
- this.lastApprovalResolvedAt = Date.now();
1226
- }
1227
- if (!this.isWaitingForResponse) {
1228
- this.isWaitingForResponse = true;
1229
- this.responseBuffer = '';
1230
- }
1231
- this.setStatus('generating', 'script_detect');
1232
- // Reset idle timeout
1233
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1234
- this.idleTimeout = setTimeout(() => {
1235
- if (this.isWaitingForResponse) {
1236
- if (this.shouldDeferIdleTimeoutFinish()) return;
1237
- this.finishResponse();
1238
- }
1239
- }, this.timeouts.generatingIdle);
1240
- this.onStatusChange?.();
1241
- }
1242
-
1243
- private applyIdle(ctx: SettledEvalContext, now: number): void {
1244
- const { modal, lastParsedAssistant, prevStatus } = ctx;
1245
- if (prevStatus === 'waiting_approval') {
1246
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1247
- this.activeModal = null;
1248
- this.lastApprovalResolvedAt = Date.now();
1249
- this.setStatus('idle', 'approval_prompt_gone_script_idle');
1250
- }
1251
- if (!this.isWaitingForResponse) {
1252
- if (prevStatus !== 'idle') {
1253
- this.clearIdleFinishCandidate('idle_without_response');
1254
- this.setStatus('idle', 'script_detect');
1255
- this.onStatusChange?.();
1256
- }
1257
- return;
1258
- }
1259
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1260
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1261
- const hasAssistantTurn = !!lastParsedAssistant;
1262
- const assistantLength = lastParsedAssistant?.content?.length || 0;
1263
- const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
1264
- const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
1265
- const idleReady = !modal
1266
- && hasAssistantTurn
1267
- && quietForMs >= idleQuietThresholdMs
1268
- && screenStableMs >= idleFinishConfirmMs;
1269
- const candidate = this.idleFinishCandidate;
1270
- const candidateQuiet = !!candidate
1271
- && candidate.responseEpoch === this.responseEpoch
1272
- && candidate.lastOutputAt === this.lastOutputAt
1273
- && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1274
- && assistantLength >= candidate.assistantLength
1275
- && (now - candidate.armedAt) >= idleFinishConfirmMs;
1276
-
1277
- this.recordTrace('idle_decision', {
1278
- quietForMs,
1279
- screenStableMs,
1280
- hasAssistantTurn,
1281
- assistantLength,
1282
- hasModal: !!modal,
1283
- idleQuietThresholdMs,
1284
- idleStableThresholdMs: idleFinishConfirmMs,
1285
- idleReady,
1286
- idleFinishConfirmMs,
1287
- idleFinishCandidate: candidate,
1288
- candidateQuiet,
1289
- canFinishImmediately: idleReady && candidateQuiet,
1290
- submitPendingUntil: this.submitPendingUntil,
1291
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1292
- ...buildCliTraceParseSnapshot({
1293
- accumulatedBuffer: this.accumulatedBuffer,
1294
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1295
- responseBuffer: this.responseBuffer,
1296
- partialResponse: this.responseBuffer,
1297
- scope: this.currentTurnScope,
1298
- }),
1299
- });
1300
-
1301
- if (idleReady && candidateQuiet) {
1302
- this.clearIdleFinishCandidate('finish_response');
1303
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1304
- this.finishResponse();
1305
- return;
1306
- }
1307
-
1308
- if (idleReady) {
1309
- if (!candidate) {
1310
- this.armIdleFinishCandidate(assistantLength);
1311
- return;
1312
- }
1313
- } else {
1314
- this.clearIdleFinishCandidate('idle_not_ready');
1315
- }
1316
-
1317
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1318
- this.idleTimeout = setTimeout(() => {
1319
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1320
- if (this.shouldDeferIdleTimeoutFinish()) return;
1321
- this.clearIdleFinishCandidate('idle_timeout_finish');
1322
- this.finishResponse();
1323
- }
1324
- }, this.timeouts.idleFinish);
1325
- }
1326
-
1327
- private finishResponse(): void {
1328
- if (this.submitPendingUntil > Date.now()) return;
1329
- if (this.responseSettleIgnoreUntil > Date.now()) return;
1330
- this.clearIdleFinishCandidate('finish_response_enter');
1331
- this.recordTrace('finish_response', {
1332
- ...buildCliTraceParseSnapshot({
1333
- accumulatedBuffer: this.accumulatedBuffer,
1334
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1335
- responseBuffer: this.responseBuffer,
1336
- partialResponse: this.responseBuffer,
1337
- scope: this.currentTurnScope,
1338
- }),
927
+ runDetectStatus(text: string): string | null {
928
+ const screenText = this.terminalScreen.getText();
929
+ const tail = text.slice(-500);
930
+ return this.runner.detectStatus({
931
+ tail,
932
+ screenText,
933
+ rawBuffer: this.accumulatedRawBuffer,
934
+ isWaitingForResponse: this.engine.isWaitingForResponse,
935
+ screen: buildCliScreenSnapshot(screenText),
936
+ tailScreen: buildCliScreenSnapshot(tail),
1339
937
  });
1340
- const commitResult = this.commitCurrentTranscript();
1341
- if (this.shouldRetryFinishResponse(commitResult)) {
1342
- this.finishRetryCount += 1;
1343
- this.recordTrace('finish_response_retry', {
1344
- retryCount: this.finishRetryCount,
1345
- retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
1346
- assistantContent: summarizeCliTraceText(commitResult.assistantContent, 220),
1347
- ...buildCliTraceParseSnapshot({
1348
- accumulatedBuffer: this.accumulatedBuffer,
1349
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1350
- responseBuffer: this.responseBuffer,
1351
- partialResponse: this.responseBuffer,
1352
- scope: this.currentTurnScope,
1353
- }),
1354
- });
1355
- if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
1356
- this.finishRetryTimer = setTimeout(() => {
1357
- this.finishRetryTimer = null;
1358
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1359
- this.finishResponse();
1360
- }
1361
- }, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
1362
- return;
1363
- }
1364
- this.clearAllTimers();
1365
- this.responseBuffer = '';
1366
- this.isWaitingForResponse = false;
1367
- this.responseSettleIgnoreUntil = 0;
1368
- this.submitRetryUsed = false;
1369
- this.submitRetryPromptSnippet = '';
1370
- this.finishRetryCount = 0;
1371
- this.currentTurnScope = null;
1372
- this.activeModal = null;
1373
- this.setStatus('idle', 'response_finished');
1374
- this.onStatusChange?.();
1375
938
  }
1376
939
 
1377
- private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[]): boolean {
1378
- const allowImmediateScriptIdleCommit = this.provider.allowInputDuringGeneration === true;
1379
- if (!allowImmediateScriptIdleCommit) return false;
1380
- if (
1381
- !session
1382
- || session.status !== 'idle'
1383
- || !this.isWaitingForResponse
1384
- || !this.currentTurnScope
1385
- || this.activeModal
1386
- || session.modal
1387
- ) {
1388
- return false;
1389
- }
1390
-
1391
- const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && m.content.trim());
1392
- if (!visibleAssistant) return false;
1393
-
1394
- this.clearAllTimers();
1395
- this.responseBuffer = '';
1396
- this.isWaitingForResponse = false;
1397
- this.responseSettleIgnoreUntil = 0;
1398
- this.submitRetryUsed = false;
1399
- this.submitRetryPromptSnippet = '';
1400
- this.finishRetryCount = 0;
1401
- this.currentTurnScope = null;
1402
- this.activeModal = null;
1403
- this.setStatus('idle', 'script_idle_commit');
1404
- this.onStatusChange?.();
1405
- this.recordTrace('script_idle_commit', {
1406
- messageCount: parsedMessages.length,
1407
- lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
940
+ runParseApproval(tail: string): { message: string; buttons: string[] } | null {
941
+ const screenText = this.terminalScreen.getText();
942
+ const buffer = screenText || this.accumulatedBuffer;
943
+ return this.runner.parseApproval({
944
+ buffer,
945
+ screenText,
946
+ rawBuffer: this.accumulatedRawBuffer,
947
+ tail,
948
+ screen: buildCliScreenSnapshot(screenText),
949
+ bufferScreen: buildCliScreenSnapshot(buffer),
950
+ tailScreen: buildCliScreenSnapshot(tail),
1408
951
  });
1409
- return true;
1410
- }
1411
-
1412
- private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
1413
- const parsed = this.parseCurrentTranscript(
1414
- [],
1415
- this.responseBuffer,
1416
- this.currentTurnScope,
1417
- );
1418
- if (parsed && Array.isArray(parsed.messages)) {
1419
- const parsedMessages = normalizeCliParsedMessages(parsed.messages, {
1420
- scope: null,
1421
- lastOutputAt: this.lastOutputAt,
1422
- });
1423
- const lastAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
1424
- if (this.currentTurnScope) {
1425
- LOG.info(
1426
- 'CLI',
1427
- `[${this.cliType}] commitCurrentTranscript parserMessages=${parsedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
1428
- );
1429
- }
1430
- this.recordTrace('commit_transcript', {
1431
- parsedStatus: parsed.status || null,
1432
- messageCount: parsedMessages.length,
1433
- lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
1434
- messages: summarizeCliTraceMessages(parsedMessages),
1435
- ...buildCliTraceParseSnapshot({
1436
- accumulatedBuffer: this.accumulatedBuffer,
1437
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1438
- responseBuffer: this.responseBuffer,
1439
- partialResponse: this.responseBuffer,
1440
- scope: this.currentTurnScope,
1441
- }),
1442
- });
1443
- if (!lastAssistant && this.currentTurnScope) {
1444
- LOG.warn(
1445
- 'CLI',
1446
- `[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1447
- );
1448
- }
1449
- const hasAssistant = !!lastAssistant;
1450
- return {
1451
- hasAssistant,
1452
- assistantContent: lastAssistant?.content || '',
1453
- };
1454
- }
1455
- if (this.currentTurnScope) {
1456
- LOG.info(
1457
- 'CLI',
1458
- `[${this.cliType}] commitCurrentTranscript parsed.messages=none responseBufferLen=${this.responseBuffer.length} accumulatedBufferLen=${this.accumulatedBuffer.length} parsedStatus=${parsed?.status || '-'} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1459
- );
1460
- }
1461
- return {
1462
- hasAssistant: false,
1463
- assistantContent: '',
1464
- };
1465
- }
1466
-
1467
-
1468
- // ─── Script Execution ──────────────────────────
1469
-
1470
- private invokeCliScript<T>(script: Function, input: any): T {
1471
- const hasStateFactory = typeof this.cliScripts?.createState === 'function';
1472
- const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
1473
- return expectsStateArgument
1474
- ? script(this.scriptState, input)
1475
- : script(input);
1476
- }
1477
-
1478
- private runParseSession(): ParsedSession | null {
1479
- if (typeof this.cliScripts?.parseSession !== 'function') {
1480
- this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
1481
- return null;
1482
- }
1483
- try {
1484
- const screenText = this.terminalScreen.getText();
1485
- const parseScreenText = this.getParseScreenText(screenText);
1486
- const tail = this.recentOutputBuffer.slice(-500);
1487
- const input = buildCliParseInput({
1488
- accumulatedBuffer: this.accumulatedBuffer,
1489
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1490
- recentOutputBuffer: this.recentOutputBuffer,
1491
- terminalScreenText: parseScreenText,
1492
- baseMessages: [],
1493
- partialResponse: this.responseBuffer,
1494
- isWaitingForResponse: this.isWaitingForResponse,
1495
- scope: this.currentTurnScope,
1496
- runtimeSettings: this.runtimeSettings,
1497
- });
1498
- const session = this.invokeCliScript<ParsedSession | null>(
1499
- this.cliScripts.parseSession,
1500
- { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
1501
- );
1502
- this.parseErrorMessage = null;
1503
- return session && typeof session === 'object' ? session : null;
1504
- } catch (e: any) {
1505
- const message = e?.message || String(e);
1506
- this.parseErrorMessage = message;
1507
- LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
1508
- return null;
1509
- }
1510
952
  }
1511
953
 
1512
- private runDetectStatus(text: string): string | null {
1513
- if (!this.cliScripts?.detectStatus) return null;
1514
- try {
1515
- const screenText = this.terminalScreen.getText();
1516
- const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
1517
- tail: text.slice(-500),
1518
- screenText,
1519
- rawBuffer: this.accumulatedRawBuffer,
1520
- isWaitingForResponse: this.isWaitingForResponse,
1521
- screen: buildCliScreenSnapshot(screenText),
1522
- tailScreen: buildCliScreenSnapshot(text.slice(-500)),
1523
- });
1524
- return status;
1525
- } catch (e: any) {
1526
- LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
1527
- return null;
1528
- }
1529
- }
1530
-
1531
- private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
1532
- if (!this.cliScripts?.parseApproval) return null;
1533
- try {
1534
- const screenText = this.terminalScreen.getText();
1535
- const buffer = screenText || this.accumulatedBuffer;
1536
- return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
1537
- buffer,
1538
- screenText,
1539
- rawBuffer: this.accumulatedRawBuffer,
1540
- tail,
1541
- screen: buildCliScreenSnapshot(screenText),
1542
- bufferScreen: buildCliScreenSnapshot(buffer),
1543
- tailScreen: buildCliScreenSnapshot(tail),
1544
- });
1545
- } catch (e: any) {
1546
- LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
1547
- return null;
954
+ private applyParsedSessionMetadata(parsed: any): void {
955
+ const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
956
+ ? parsed.providerSessionId.trim()
957
+ : '';
958
+ if (providerSessionId && providerSessionId !== this.providerSessionId) {
959
+ this.providerSessionId = providerSessionId;
960
+ this.updateRuntimeMeta({ providerSessionId });
1548
961
  }
1549
962
  }
1550
963
 
1551
- private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
1552
- return !!(startupModal || this.activeModal);
1553
- }
1554
-
1555
964
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1556
965
  if (this.parseErrorMessage) return 'error';
1557
- if (this.hasActionableApproval(startupModal)) return 'waiting_approval';
1558
- if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
1559
- return this.currentStatus;
966
+ if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
967
+ if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
968
+ return this.engine.currentStatus;
1560
969
  }
1561
970
 
1562
971
  // ─── Public API (CliAdapter) ───────────────────
@@ -1564,17 +973,154 @@ export class ProviderCliAdapter implements CliAdapter {
1564
973
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1565
974
  const allowParse = options.allowParse !== false;
1566
975
  const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
976
+ const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
977
+ ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
978
+ : null;
979
+ // (D4) Poll-driven static-idle confirm. A hosted CLI session whose boot
980
+ // banner drove the FSM to 'generating' can then sit at a STATIC ready
981
+ // prompt emitting NO further PTY output — every output-driven busy→idle
982
+ // re-eval is starved and the startup-settle loop has hard-stopped past
983
+ // spawnAt+10s, so currentStatus stays frozen at generating and the
984
+ // dashboard disables Send. This read-path poll is the only place that can
985
+ // release the wedge. Gate PRECISELY (reuse resolveStartupState's proven
986
+ // predicates so a real generating turn is NEVER mis-flipped):
987
+ // (a) no recent output for >= statusActivityHold (the 2000ms stable
988
+ // window resolveStartupState uses),
989
+ // (b) runDetectStatus(current screen) === 'idle' (same detector),
990
+ // (c) no active/parsed modal — an approval/choice screen is never
991
+ // flipped to idle.
992
+ // A real generating turn keeps producing output (fresh
993
+ // lastNonEmptyOutputAt) and/or shows 'esc to cancel' (detects busy) → it
994
+ // fails (a) or (b). confirmPollStaticIdle adds the final structural guard
995
+ // (currentStatus==='generating' AND no currentTurnScope/activeModal), so
996
+ // this only releases the boot-banner wedge and the post-turn static-idle
997
+ // case. Direct-spawn sessions already settle idle in the startup window
998
+ // → this is a no-op for them.
999
+ if (
1000
+ allowParse
1001
+ && this.engine.currentStatus === 'generating'
1002
+ && !this.engine.currentTurnScope
1003
+ && !this.engine.activeModal
1004
+ ) {
1005
+ const now = Date.now();
1006
+ const quietForMs = this.lastNonEmptyOutputAt
1007
+ ? (now - this.lastNonEmptyOutputAt)
1008
+ : Number.MAX_SAFE_INTEGER;
1009
+ let eligible = false;
1010
+ if (quietForMs >= this.getStatusActivityHoldMs()) {
1011
+ const screenText = this.terminalScreen.getText();
1012
+ const pollDetect = this.runDetectStatus(screenText || this.recentOutputBuffer);
1013
+ const pollModal = this.runParseApproval(screenText)
1014
+ || this.runParseApproval(this.recentOutputBuffer);
1015
+ // (FALSEIDLE Path-C) Final-assistant / pending-response discriminator.
1016
+ // Paths A and B refuse to finalize a turn whose partial-response buffer
1017
+ // is still non-empty (getCompletedFinalizationBlock 'partial_response_pending'
1018
+ // / completionFinalAssistantEvidence turnClosed at cli-provider-instance.ts).
1019
+ // Path C (this poll) previously OMITTED it, so a genuinely-live but
1020
+ // momentarily-silent turn — silent thinking, a backgrounded/long tool child,
1021
+ // the gap between two assistant bubbles — whose currentTurnScope anchor was
1022
+ // lost still satisfied the weaker gate and flipped to idle prematurely.
1023
+ // Require an EMPTY partial buffer here too. getPartialResponse() returns the
1024
+ // accumulated assistant stream while isWaitingForResponse (which
1025
+ // applyGenerating leaves set on the boot-banner wedge too), so this does NOT
1026
+ // reintroduce the D4b wedge: the attach/boot-banner seeds only the static
1027
+ // ready screen — no assistant turn ever streamed — so its partial buffer is
1028
+ // empty and the gate still releases it. A mid-turn quiet gap holds a
1029
+ // non-empty buffer and is deferred.
1030
+ const partial = this.getPartialResponse();
1031
+ const partialPending = typeof partial === 'string' && partial.trim().length > 0;
1032
+ eligible = pollDetect === 'idle' && !pollModal && !partialPending;
1033
+ }
1034
+ if (eligible) {
1035
+ // (FALSEIDLE Path-C) Debounce for autonomous mesh sessions. A worker /
1036
+ // coordinator has no human to correct a premature idle, and a single
1037
+ // runDetectStatus point-sample can land in a live turn's momentary silence.
1038
+ // Require STATIC_IDLE_POLL_CONFIRM_COUNT consecutive eligible polls before
1039
+ // confirming, so the FSM must observe a sustained static-idle screen — a
1040
+ // turn that resumes (fresh output, or a re-armed turn scope) resets the
1041
+ // streak. The status poll runs on the 30s-idle / 5s-generating heartbeat and
1042
+ // this getStatus gate is re-hit each dashboard status tick, so 2 confirms is
1043
+ // ~one extra tick of hysteresis — enough to reject a one-sample silence gap
1044
+ // without materially delaying a genuine boot-wedge release. Foreground /
1045
+ // attended sessions keep the single-poll confirm (a human is watching Send).
1046
+ const requiredStreak = this.isAutonomousMeshSession()
1047
+ ? ProviderCliAdapter.STATIC_IDLE_POLL_CONFIRM_COUNT
1048
+ : 1;
1049
+ this.staticIdlePollStreak += 1;
1050
+ if (this.staticIdlePollStreak >= requiredStreak) {
1051
+ this.engine.confirmPollStaticIdle('poll_static_idle');
1052
+ this.staticIdlePollStreak = 0;
1053
+ }
1054
+ } else {
1055
+ this.staticIdlePollStreak = 0;
1056
+ }
1057
+ } else {
1058
+ this.staticIdlePollStreak = 0;
1059
+ }
1567
1060
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1568
- let effectiveModal = startupModal || this.activeModal;
1061
+ let effectiveModal = startupModal || this.engine.activeModal;
1062
+ // (fix) When we have no captured modal yet, take one more live attempt
1063
+ // with the current screen text — the engine's settle pass can miss
1064
+ // the modal when it happens to fire exactly between writes, and
1065
+ // without a modal here the dashboard could never show the buttons.
1066
+ // We deliberately do NOT overwrite an existing engine.activeModal so
1067
+ // a stable matched modal wins. This runs even outside the startup
1068
+ // gate because Claude's approval frames can appear long after launch.
1069
+ if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
1070
+ const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
1071
+ if (liveDetect === 'waiting_approval') {
1072
+ const liveModal = this.runParseApproval(this.terminalScreen.getText())
1073
+ || this.runParseApproval(this.recentOutputBuffer);
1074
+ // (fix) Only surface modals that have at least one non-empty
1075
+ // button. Half-rendered approval frames produced
1076
+ // { message, buttons: [] } briefly — auto-approve would then
1077
+ // pick buttonIndex=-1 and resolveModal would write the
1078
+ // provider's index-0 key into the prompt. Skip those frames
1079
+ // and let a later evaluate find the complete modal.
1080
+ const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
1081
+ && liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
1082
+ if (liveModal && buttonsOk) {
1083
+ effectiveModal = liveModal;
1084
+ if (!this.engine.activeModal) this.engine.activeModal = liveModal;
1085
+ }
1086
+ }
1087
+ }
1088
+ // Only surface waiting_approval when we ALSO have a concrete modal
1089
+ // (message + buttons). detectStatus alone can fire while parseApproval
1090
+ // is still null — the engine logs "detectStatus=waiting_approval but
1091
+ // parseApproval returned null; ignoring". Without this guard getStatus
1092
+ // was shipping a bare waiting_approval with activeModal=null and the
1093
+ // user perceived the flow as broken.
1094
+ if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
1095
+ effectiveStatus = 'waiting_approval';
1096
+ } else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
1097
+ effectiveStatus = 'idle';
1098
+ }
1569
1099
  if (allowParse && !startupModal && !effectiveModal) {
1570
1100
  const parsed = this.getFreshParsedStatusCache();
1571
1101
  const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1572
1102
  && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1573
1103
  ? parsed.activeModal
1574
1104
  : null;
1105
+ const hasFinalAssistant = (p: any) => {
1106
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
1107
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
1108
+ };
1575
1109
  if (parsed?.status === 'waiting_approval' && parsedModal) {
1576
1110
  effectiveStatus = 'waiting_approval';
1577
1111
  effectiveModal = parsedModal;
1112
+ } else if (
1113
+ effectiveStatus === 'idle'
1114
+ && parsed?.status === 'generating'
1115
+ && !hasFinalAssistant(parsed)
1116
+ ) {
1117
+ effectiveStatus = 'generating';
1118
+ } else if (
1119
+ effectiveStatus === 'generating'
1120
+ && parsed?.status === 'idle'
1121
+ && hasFinalAssistant(parsed)
1122
+ ) {
1123
+ effectiveStatus = 'idle';
1578
1124
  }
1579
1125
  }
1580
1126
  const bufferState = this.getBufferState();
@@ -1583,8 +1129,21 @@ export class ProviderCliAdapter implements CliAdapter {
1583
1129
  messages: [],
1584
1130
  workingDir: this.workingDir,
1585
1131
  activeModal: effectiveModal,
1586
- errorMessage: this.parseErrorMessage || undefined,
1587
- errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
1132
+ approvalEntrySeq: this.engine.approvalEntrySeq,
1133
+ lastResolvedEntrySeq: this.engine.lastResolvedEntrySeq,
1134
+ pendingOutboundCount: this.pendingOutboundQueue.length,
1135
+ pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
1136
+ id: message.id,
1137
+ role: message.role,
1138
+ content: message.content,
1139
+ queuedAt: message.queuedAt,
1140
+ source: message.source,
1141
+ })),
1142
+ errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
1143
+ errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
1144
+ providerSessionId: this.providerSessionId || undefined,
1145
+ lastOutputAt: this.lastOutputAt,
1146
+ lastScreenChangeAt: this.lastScreenChangeAt,
1588
1147
  ...(bufferState ? { bufferState } : {}),
1589
1148
  };
1590
1149
  }
@@ -1600,15 +1159,16 @@ export class ProviderCliAdapter implements CliAdapter {
1600
1159
  const cached = this.parsedStatusCache;
1601
1160
  const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1602
1161
  if (
1603
- cached
1162
+ !this.providerOwnsTranscript()
1163
+ && cached
1604
1164
  && cached.responseBuffer === this.responseBuffer
1605
- && cached.currentTurnScope === this.currentTurnScope
1165
+ && cached.currentTurnScope === this.engine.currentTurnScope
1606
1166
  && cached.recentOutputBuffer === this.recentOutputBuffer
1607
1167
  && cached.accumulatedBuffer === this.accumulatedBuffer
1608
1168
  && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
1609
1169
  && cached.screenText === parseScreenText
1610
- && cached.currentStatus === this.currentStatus
1611
- && cached.activeModal === this.activeModal
1170
+ && cached.currentStatus === this.engine.currentStatus
1171
+ && cached.activeModal === this.engine.activeModal
1612
1172
  && cached.cliName === this.cliName
1613
1173
  ) {
1614
1174
  return cached.result;
@@ -1623,14 +1183,20 @@ export class ProviderCliAdapter implements CliAdapter {
1623
1183
  const bufferState = this.getBufferState();
1624
1184
  const result = {
1625
1185
  id: (parsed as any).id || 'cli_session',
1626
- status: parsed.status || this.currentStatus,
1186
+ status: parsed.status || this.engine.currentStatus,
1627
1187
  title: (parsed as any).title || this.cliName,
1628
1188
  messages: normalizeCliParsedMessages(parsed.messages, {
1629
1189
  scope: null,
1630
1190
  lastOutputAt: this.lastOutputAt,
1631
1191
  }),
1632
1192
  activeModal,
1633
- providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
1193
+ providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
1194
+ errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
1195
+ ? (parsed as any).errorMessage.trim()
1196
+ : undefined,
1197
+ errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
1198
+ ? (parsed as any).errorReason.trim()
1199
+ : undefined,
1634
1200
  ...(bufferState ? { bufferState } : {}),
1635
1201
  ...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
1636
1202
  ? { transcriptAuthority: (parsed as any).transcriptAuthority }
@@ -1642,13 +1208,13 @@ export class ProviderCliAdapter implements CliAdapter {
1642
1208
 
1643
1209
  this.parsedStatusCache = {
1644
1210
  responseBuffer: this.responseBuffer,
1645
- currentTurnScope: this.currentTurnScope,
1211
+ currentTurnScope: this.engine.currentTurnScope,
1646
1212
  recentOutputBuffer: this.recentOutputBuffer,
1647
1213
  accumulatedBuffer: this.accumulatedBuffer,
1648
1214
  accumulatedRawBufferKey,
1649
1215
  screenText: parseScreenText,
1650
- currentStatus: this.currentStatus,
1651
- activeModal: this.activeModal,
1216
+ currentStatus: this.engine.currentStatus,
1217
+ activeModal: this.engine.activeModal,
1652
1218
  cliName: this.cliName,
1653
1219
  result,
1654
1220
  };
@@ -1656,22 +1222,22 @@ export class ProviderCliAdapter implements CliAdapter {
1656
1222
  }
1657
1223
 
1658
1224
  async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
1659
- const fn = this.cliScripts?.[scriptName];
1660
- if (typeof fn !== 'function') {
1661
- throw new Error(`CLI script '${scriptName}' not available`);
1662
- }
1663
1225
  const input = buildCliParseInput({
1664
1226
  accumulatedBuffer: this.accumulatedBuffer,
1665
1227
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1666
1228
  recentOutputBuffer: this.recentOutputBuffer,
1667
1229
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1230
+ workingDir: this.workingDir,
1231
+ providerSessionId: this.providerSessionId || undefined,
1232
+ historySessionId: this.providerSessionId || undefined,
1668
1233
  baseMessages: [],
1669
1234
  partialResponse: this.responseBuffer,
1670
- isWaitingForResponse: this.isWaitingForResponse,
1671
- scope: this.currentTurnScope,
1235
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1236
+ scope: this.engine.currentTurnScope,
1672
1237
  runtimeSettings: this.runtimeSettings,
1238
+ spawnAt: this.spawnAt,
1673
1239
  });
1674
- return await Promise.resolve(this.invokeCliScript(fn, {
1240
+ return await Promise.resolve(this.runner.invokeByName(scriptName, {
1675
1241
  ...input,
1676
1242
  args: args && typeof args === 'object' ? { ...args } : {},
1677
1243
  }));
@@ -1683,7 +1249,7 @@ export class ProviderCliAdapter implements CliAdapter {
1683
1249
 
1684
1250
  /** Whether this adapter has CLI scripts loaded */
1685
1251
  hasCliScripts(): boolean {
1686
- return typeof this.cliScripts?.detectStatus === 'function';
1252
+ return this.runner.hasDetectStatus();
1687
1253
  }
1688
1254
 
1689
1255
  /**
@@ -1692,24 +1258,29 @@ export class ProviderCliAdapter implements CliAdapter {
1692
1258
  */
1693
1259
  async resolveAction(data: any): Promise<void> {
1694
1260
  let promptText = '';
1695
- if (this.cliScripts && typeof this.cliScripts.resolveAction === 'function') {
1696
- try {
1697
- promptText = this.cliScripts.resolveAction(data);
1698
- } catch (e: any) {
1699
- LOG.warn('CLI', `[${this.cliType}] resolveAction error: ${e.message}`);
1700
- }
1261
+ try {
1262
+ promptText = this.runner.invokeByName('resolveAction', data);
1263
+ } catch {
1264
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
1265
+ return;
1701
1266
  }
1702
1267
  if (!promptText) {
1703
- LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not supply a prompt`);
1268
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
1704
1269
  return;
1705
1270
  }
1706
1271
  await this.sendMessage(promptText);
1707
1272
  }
1708
1273
 
1274
+ async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
1275
+ // Legacy TUI providers do not currently expose an interactive prompt
1276
+ // protocol. Spec-backed claude-cli implements the first real wiring.
1277
+ }
1278
+
1709
1279
  private isSubmitStuck(normalizedPromptSnippet: string): boolean {
1710
- if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
1711
- if (this.hasActionableApproval()) return false;
1712
- if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return false;
1280
+ if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
1281
+ if (this.engine.hasActionableApproval()) return false;
1282
+ // If there's already meaningful response content beyond the echoed prompt, not stuck
1283
+ if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
1713
1284
  const screenText = this.terminalScreen.getText();
1714
1285
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
1715
1286
  const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
@@ -1718,25 +1289,97 @@ export class ProviderCliAdapter implements CliAdapter {
1718
1289
  return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
1719
1290
  }
1720
1291
 
1721
- private async writeToPty(data: string): Promise<void> {
1292
+ private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
1293
+ const raw = String(this.responseBuffer || '').trim();
1294
+ if (!raw) return false;
1295
+ const normalizedPrompt = compactPromptText(promptSnippet);
1296
+ if (!normalizedPrompt) return true;
1297
+ const normalizedBuffer = compactPromptText(raw);
1298
+ if (!normalizedBuffer) return false;
1299
+ if (normalizedBuffer === normalizedPrompt) return false;
1300
+ if (normalizedBuffer.startsWith(normalizedPrompt)) {
1301
+ const remainder = normalizedBuffer
1302
+ .slice(normalizedPrompt.length)
1303
+ .replace(/[─═\-]+/g, '')
1304
+ .replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
1305
+ .replace(/accepteditson\([^)]*\)/gi, '')
1306
+ .replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
1307
+ .replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
1308
+ .replace(/esctointerrupt/gi, '')
1309
+ .replace(/❯/g, '')
1310
+ .replace(/^[\s\-–—:;,.!/?]+/, '')
1311
+ .trim();
1312
+ return remainder.length > 0;
1313
+ }
1314
+ return true;
1315
+ }
1316
+
1317
+ /** Serialize `data` onto the per-session PTY write chain. Returns a promise
1318
+ * that resolves (or rejects) with THIS write's own outcome, so callers keep
1319
+ * their existing "await my write completion" semantics. The underlying chain
1320
+ * link swallows its own error before releasing the next write, so a failed
1321
+ * write surfaces to its caller but never wedges subsequent writes behind a
1322
+ * permanently-rejected tail (FIFO order preserved). */
1323
+ private writeToPty(data: string): Promise<void> {
1324
+ const run = this.ptyWriteChain.then(() => this.doWriteToPty(data));
1325
+ // Keep the tail resolved so the next writeToPty() always chains onto a
1326
+ // settled promise (never a rejected one). The caller still sees run's
1327
+ // rejection; only the shared chain is insulated from it.
1328
+ this.ptyWriteChain = run.catch(() => {});
1329
+ return run;
1330
+ }
1331
+
1332
+ private async doWriteToPty(data: string): Promise<void> {
1722
1333
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1334
+ // win32 ConPTY paced write: a single unbounded write beyond ~1KB overflows
1335
+ // the console input pipe and drops LEADING bytes (the "long message gets
1336
+ // truncated, head lost / tail kept" failure). Split a large payload into
1337
+ // bounded, surrogate-safe chunks written with a short gap so the console
1338
+ // reader keeps up. Small payloads (the common case — short prompts, lone
1339
+ // submit keys) still go out in a single write.
1340
+ //
1341
+ // The submit key, when present, is the TAIL of `data` (callers pass
1342
+ // `body + sendKey` for the atomic-submit paths). Because we chunk the
1343
+ // combined string, the submit key always rides in the SAME final write as
1344
+ // the body's tail — the win32 invariant that ConPTY recognizes it as a
1345
+ // submit — and is never emitted before the whole body has been written
1346
+ // (no partial-body submit). Body-only writes (wait_for_echo strategy) have
1347
+ // their submit key sent separately by the caller afterwards, unchanged.
1348
+ if (process.platform === 'win32' && shouldChunkWin32Write(data.length)) {
1349
+ await this.writeWin32Chunked(data);
1350
+ return;
1351
+ }
1723
1352
  await this.ptyProcess.write(data);
1724
1353
  }
1725
1354
 
1355
+ /** Write `data` to the PTY in bounded, surrogate-safe chunks with a short
1356
+ * inter-chunk gap (win32 paced write). Awaits each chunk's write and the gap
1357
+ * so the returned promise resolves only after the FINAL chunk (carrying any
1358
+ * trailing submit key) has been written. */
1359
+ private async writeWin32Chunked(data: string): Promise<void> {
1360
+ const chunks = chunkPreservingSurrogates(data, WIN32_PTY_WRITE_CHUNK_CHARS);
1361
+ for (let i = 0; i < chunks.length; i += 1) {
1362
+ if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1363
+ await this.ptyProcess.write(chunks[i]);
1364
+ if (i + 1 < chunks.length) {
1365
+ await new Promise<void>(resolve => setTimeout(resolve, WIN32_PTY_WRITE_CHUNK_GAP_MS));
1366
+ }
1367
+ }
1368
+ }
1369
+
1726
1370
  private resetPendingSendState(reason: string): void {
1727
- this.isWaitingForResponse = false;
1728
1371
  this.responseBuffer = '';
1729
- this.currentTurnScope = null;
1730
- this.submitPendingUntil = 0;
1731
- this.clearIdleFinishCandidate(reason);
1732
1372
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1733
- if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1734
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1373
+ this.engine.resetActiveTurnState();
1374
+ this.engine.clearIdleFinishCandidate(reason);
1735
1375
  }
1736
1376
 
1737
1377
  private commitSendUserTurn(state: SendMessageState): void {
1738
1378
  if (state.didCommitUserTurn) return;
1739
1379
  state.didCommitUserTurn = true;
1380
+ // The first turn has now been written atomically (win32-safe); later turns
1381
+ // can use the normal echo-gated path now that the input layer is proven live.
1382
+ this.firstTurnSent = true;
1740
1383
  }
1741
1384
 
1742
1385
  private armResponseTimeout(): void {
@@ -1748,42 +1391,14 @@ export class ProviderCliAdapter implements CliAdapter {
1748
1391
  }
1749
1392
  this.responseTimeout = setTimeout(() => {
1750
1393
  this.responseTimeout = null;
1751
- if (!this.isWaitingForResponse) return;
1752
-
1753
- const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
1754
- this.recordTrace('response_timeout_check', {
1755
- timeoutMs,
1756
- detectedStatus: detectedStatusBeforeEval,
1757
- currentStatus: this.currentStatus,
1758
- isWaitingForResponse: this.isWaitingForResponse,
1759
- hasActionableApproval: this.hasActionableApproval(),
1760
- ...buildCliTraceParseSnapshot({
1761
- accumulatedBuffer: this.accumulatedBuffer,
1762
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1763
- responseBuffer: this.responseBuffer,
1764
- partialResponse: this.responseBuffer,
1765
- scope: this.currentTurnScope,
1766
- }),
1767
- });
1394
+ if (!this.engine.isWaitingForResponse) return;
1768
1395
 
1769
- // maxResponse is a watchdog/checkpoint, not a completion signal. The old
1770
- // behavior called finishResponse() unconditionally at the default 300s,
1771
- // which fabricated idle transitions and downstream generating_completed
1772
- // notifications while long-running CLIs were still generating. Re-run the
1396
+ // maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
1773
1397
  // normal settled parser instead and keep the turn open unless the provider
1774
1398
  // actually reports an idle, commit-ready state.
1775
- this.settledBuffer = this.recentOutputBuffer;
1776
- this.evaluateSettled();
1777
-
1778
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1779
- const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
1780
- this.recordTrace('response_timeout_kept_open', {
1781
- timeoutMs,
1782
- detectedStatusBeforeEval,
1783
- detectedStatusAfterEval,
1784
- currentStatus: this.currentStatus,
1785
- isWaitingForResponse: this.isWaitingForResponse,
1786
- });
1399
+ this.engine.evaluateSettled(this.getSnapshot());
1400
+
1401
+ if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
1787
1402
  this.armResponseTimeout();
1788
1403
  }
1789
1404
  }, timeoutMs);
@@ -1795,37 +1410,42 @@ export class ProviderCliAdapter implements CliAdapter {
1795
1410
  });
1796
1411
  }
1797
1412
 
1413
+ // WIN32-READY-HOLD: choose the retry write for a stuck prompt. When the FIRST turn
1414
+ // is stuck on win32 — the premature-ready swallow window — the prompt text itself
1415
+ // may have been partially eaten by a not-yet-live ConPTY input layer, so re-sending
1416
+ // a bare CR keeps hitting nothing. Re-type the whole `text + sendKey` atomically
1417
+ // once so the input layer (now live) receives a self-contained, submit-coupled
1418
+ // write. All other cases keep the cheap bare-CR retry (the prompt is fully echoed
1419
+ // and only the Enter is missing).
1420
+ private writeStuckRetry(state: SendMessageState, mode: string): void {
1421
+ const retypeFirstTurn = process.platform === 'win32' && state.isFirstTurn;
1422
+ if (retypeFirstTurn) {
1423
+ LOG.info('CLI', `[${this.cliType}] ${mode}: re-typing full prompt atomically (win32 first-turn swallow recovery)`);
1424
+ void this.writeToPty(state.text + this.sendKey).catch((error) => {
1425
+ LOG.warn('CLI', `[${this.cliType}] ${mode} re-type write failed: ${error?.message || error}`);
1426
+ });
1427
+ return;
1428
+ }
1429
+ this.writeSubmitKeyForRetry(mode);
1430
+ }
1431
+
1798
1432
  private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
1799
1433
  this.submitRetryTimer = null;
1800
1434
  if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1801
- const screenText = this.terminalScreen.getText();
1802
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1435
+ this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1803
1436
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
1804
- this.recordTrace('submit_write', {
1805
- mode: 'submit_retry',
1806
- attempt,
1807
- sendKey: this.sendKey,
1808
- screenText: summarizeCliTraceText(screenText, 500),
1809
- });
1810
- this.writeSubmitKeyForRetry('submit_retry');
1811
- if (attempt >= 3) { this.submitRetryUsed = true; return; }
1437
+ this.writeStuckRetry(state, 'submit_retry');
1438
+ if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
1812
1439
  this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
1813
1440
  }
1814
1441
 
1815
1442
  private retryImmediateSubmitIfStuck(state: SendMessageState): void {
1816
1443
  this.submitRetryTimer = null;
1817
1444
  if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1818
- const screenText = this.terminalScreen.getText();
1819
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1445
+ this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1820
1446
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
1821
- this.recordTrace('submit_write', {
1822
- mode: 'immediate_retry',
1823
- attempt: 1,
1824
- sendKey: this.sendKey,
1825
- screenText: summarizeCliTraceText(screenText, 500),
1826
- });
1827
- this.writeSubmitKeyForRetry('immediate_retry');
1828
- this.submitRetryUsed = true;
1447
+ this.writeStuckRetry(state, 'immediate_retry');
1448
+ this.engine.submitRetryUsed = true;
1829
1449
  }
1830
1450
 
1831
1451
  private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
@@ -1833,13 +1453,7 @@ export class ProviderCliAdapter implements CliAdapter {
1833
1453
  completion.resolveOnce();
1834
1454
  return;
1835
1455
  }
1836
- this.submitPendingUntil = 0;
1837
- const screenText = this.terminalScreen.getText();
1838
- this.recordTrace('submit_write', {
1839
- mode: 'submit_key',
1840
- sendKey: this.sendKey,
1841
- screenText: summarizeCliTraceText(screenText, 500),
1842
- });
1456
+ this.engine.submitPendingUntil = 0;
1843
1457
  void this.writeToPty(this.sendKey).then(() => {
1844
1458
  this.commitSendUserTurn(state);
1845
1459
  this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
@@ -1849,13 +1463,7 @@ export class ProviderCliAdapter implements CliAdapter {
1849
1463
  }
1850
1464
 
1851
1465
  private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
1852
- this.submitPendingUntil = 0;
1853
- this.recordTrace('submit_write', {
1854
- mode: 'immediate',
1855
- text: summarizeCliTraceText(state.text, 500),
1856
- sendKey: this.sendKey,
1857
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
1858
- });
1466
+ this.engine.submitPendingUntil = 0;
1859
1467
  void this.writeToPty(state.text + this.sendKey).then(() => {
1860
1468
  this.commitSendUserTurn(state);
1861
1469
  this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
@@ -1899,7 +1507,8 @@ export class ProviderCliAdapter implements CliAdapter {
1899
1507
  requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
1900
1508
  screenText: summarizeCliTraceText(screenText, 1000),
1901
1509
  };
1902
- this.recordTrace('submit_echo_missing', diagnostic);
1510
+ LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
1511
+
1903
1512
  if (this.requirePromptEchoBeforeSubmit) {
1904
1513
  // At this point the prompt text write already completed. Rejecting without
1905
1514
  // a submit key can leave the delegated CLI with an unsent prompt sitting at
@@ -1924,12 +1533,153 @@ export class ProviderCliAdapter implements CliAdapter {
1924
1533
  ), 50);
1925
1534
  }
1926
1535
 
1927
- async sendMessage(text: string): Promise<void> {
1536
+ async sendMessage(text: string, options: { force?: boolean; meshTaskId?: string } = {}): Promise<void> {
1537
+ if (options.force === true) {
1538
+ await this.forceSendMessage(text, options.meshTaskId);
1539
+ return;
1540
+ }
1541
+ await this.sendMessageNow(text, true, options.meshTaskId);
1542
+ }
1543
+
1544
+ async forceSendMessage(text: string, meshTaskId?: string): Promise<void> {
1545
+ if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1546
+ const content = String(text || '');
1547
+ if (!content.trim()) return;
1548
+ // ARCH-REFACTOR R1: a force-send (mesh coordinator dispatch / reconcile
1549
+ // redelivery) bypasses the normal turnScope pipeline (raw PTY write), so there is
1550
+ // no turnScope to carry the taskId. Bind it directly on the engine so the
1551
+ // resulting turn's completion is still attributed to the right task.
1552
+ if (typeof meshTaskId === 'string' && meshTaskId.trim()) {
1553
+ this.engine.currentTurnTaskId = meshTaskId;
1554
+ }
1555
+ // Modal-park guard (defense-in-depth — the primary guard is at the
1556
+ // cli-provider-instance force-forward chokepoint). A force-write writes raw
1557
+ // keystrokes into the PTY, bypassing the busy send-guard. If the session is
1558
+ // parked on a tool-consent modal, the modal's key handler eats those bytes
1559
+ // and silently resolves an approval the user never made. Hold the write and
1560
+ // let the mesh reconcile loop redeliver once the modal is resolved. We only
1561
+ // hold for an actionable approval modal; plain generating is still force-written
1562
+ // (that is the deadlock the force path exists to break).
1563
+ if (this.engine.currentStatus === 'waiting_approval' || this.engine.hasActionableApproval()) {
1564
+ LOG.info('CLI', `[${this.cliType}] force-send held — session parked on approval modal (status=${this.engine.currentStatus})`);
1565
+ return;
1566
+ }
1567
+ LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
1568
+ // Settle, then submit atomically. The previous split-write fix
1569
+ // (write text → settle → write a separate '\r') regressed win32:
1570
+ // ConPTY does not treat a lone CR arriving in its own PTY chunk after a
1571
+ // delay as a submit key, so the Enter was swallowed and the prompt sat
1572
+ // typed-but-unsent. Instead, honor a fixed settle gap so the TUI input
1573
+ // handler is in its input-accepting state, then write `content + sendKey`
1574
+ // as ONE PTY write — the same atomic submit the normal `immediate`
1575
+ // strategy uses. Keeping the Enter in the same write unit as the text is
1576
+ // the invariant that makes win32 ConPTY recognize the submit, and it
1577
+ // also makes the original inject↔submit race impossible (the Enter can
1578
+ // never be separated from the text it submits). The settle still guards
1579
+ // the original concern — that a force-dispatch injected into a freshly
1580
+ // idle session lands before the TUI is ready to accept input.
1581
+ await this.waitForForceSubmitSettle();
1582
+ await this.writeToPty(content + this.sendKey);
1583
+ this.onStatusChange?.();
1584
+ }
1585
+
1586
+ private async waitForForceSubmitSettle(): Promise<void> {
1587
+ // A fixed minimum gap so the input handler is ready to accept the paste.
1588
+ // We deliberately do NOT echo-gate here: the submit key is written in the
1589
+ // same atomic PTY write as the text (forceSendMessage), so there is no
1590
+ // separate Enter that could race the echo, and on win32 the echo gate
1591
+ // never reliably matched anyway.
1592
+ await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
1593
+ }
1594
+
1595
+ private enqueuePendingOutboundMessage(text: string, reason: string, meshTaskId?: string): void {
1596
+ const content = String(text || '');
1597
+ const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
1598
+ if (duplicate) {
1599
+ return;
1600
+ }
1601
+ const queuedAt = Date.now();
1602
+ const message: PendingOutboundMessage = {
1603
+ id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
1604
+ role: 'user',
1605
+ content,
1606
+ queuedAt,
1607
+ source: 'sendMessage',
1608
+ ...(typeof meshTaskId === 'string' && meshTaskId.trim() ? { meshTaskId } : {}),
1609
+ };
1610
+ this.pendingOutboundQueue.push(message);
1611
+ LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
1612
+ this.onStatusChange?.();
1613
+ }
1614
+
1615
+ private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
1616
+ if (this.provider.allowInputDuringGeneration === true) return null;
1617
+ if (this.engine.hasActionableApproval()) return null;
1618
+ const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1619
+ ? String(parsedStatusBeforeSend.status)
1620
+ : '';
1621
+ const hasFinalAssistant = (p: any) => {
1622
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
1623
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
1624
+ };
1625
+ if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
1626
+ if (this.engine.currentStatus === 'generating') return 'current_status_generating';
1627
+ if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating') {
1628
+ const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
1629
+ const parsedHasActionableModal = Boolean(
1630
+ parsedModal
1631
+ && Array.isArray(parsedModal.buttons)
1632
+ && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1633
+ );
1634
+ const terminalLooksIdle = this.engine.currentStatus === 'idle'
1635
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1636
+ && !this.engine.isWaitingForResponse
1637
+ && !this.engine.currentTurnScope
1638
+ && !this.engine.hasActionableApproval()
1639
+ && !parsedHasActionableModal;
1640
+ return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
1641
+ }
1642
+ if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
1643
+ return null;
1644
+ }
1645
+
1646
+ private schedulePendingOutboundFlush(delayMs = 0): void {
1647
+ if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
1648
+ this.pendingOutboundFlushTimer = setTimeout(() => {
1649
+ this.pendingOutboundFlushTimer = null;
1650
+ void this.flushPendingOutboundQueue();
1651
+ }, Math.max(0, delayMs));
1652
+ }
1653
+
1654
+ private async flushPendingOutboundQueue(): Promise<void> {
1655
+ if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
1656
+ if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
1657
+ this.pendingOutboundFlushInFlight = true;
1658
+ try {
1659
+ while (this.pendingOutboundQueue.length > 0) {
1660
+ if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
1661
+ const next = this.pendingOutboundQueue[0];
1662
+ try {
1663
+ await this.sendMessageNow(next.content, false, next.meshTaskId);
1664
+ this.pendingOutboundQueue.shift();
1665
+ this.onStatusChange?.();
1666
+ } catch (error: any) {
1667
+ LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
1668
+ this.schedulePendingOutboundFlush(1000);
1669
+ break;
1670
+ }
1671
+ }
1672
+ } finally {
1673
+ this.pendingOutboundFlushInFlight = false;
1674
+ }
1675
+ }
1676
+
1677
+ private async sendMessageNow(text: string, allowQueue: boolean, meshTaskId?: string): Promise<void> {
1928
1678
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1929
1679
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
1930
1680
  const allowInterventionPrompt = allowInputDuringGeneration
1931
- && this.isWaitingForResponse
1932
- && !this.hasActionableApproval();
1681
+ && this.engine.isWaitingForResponse
1682
+ && !this.engine.hasActionableApproval();
1933
1683
  if (this.startupParseGate) {
1934
1684
  const deadline = Date.now() + 10000;
1935
1685
  while (this.startupParseGate && Date.now() < deadline) {
@@ -1937,76 +1687,106 @@ export class ProviderCliAdapter implements CliAdapter {
1937
1687
  await new Promise(resolve => setTimeout(resolve, 50));
1938
1688
  }
1939
1689
  }
1690
+ const parsedStatusBeforeSend = !allowInputDuringGeneration
1691
+ ? (() => {
1692
+ try {
1693
+ return this.getScriptParsedStatus?.() || null;
1694
+ } catch {
1695
+ return null;
1696
+ }
1697
+ })()
1698
+ : null;
1699
+ const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
1700
+ if (allowQueue && queueReason) {
1701
+ this.enqueuePendingOutboundMessage(text, queueReason, meshTaskId);
1702
+ return;
1703
+ }
1940
1704
  if (!allowInterventionPrompt) {
1941
1705
  await this.waitForInteractivePrompt();
1942
1706
  }
1943
1707
  if (!this.ready) {
1944
1708
  this.resolveStartupState('send_precheck');
1945
- if (this.runDetectStatus(this.recentOutputBuffer) === 'idle' && this.currentStatus === 'idle') {
1709
+ if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
1946
1710
  this.ready = true;
1947
1711
  this.startupParseGate = false;
1712
+ this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
1948
1713
  LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
1949
1714
  }
1950
1715
  }
1951
- if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1952
- const parsedStatusBeforeSend = !allowInputDuringGeneration
1953
- ? (() => {
1954
- try {
1955
- return this.getScriptParsedStatus?.() || null;
1956
- } catch {
1957
- return null;
1958
- }
1959
- })()
1960
- : null;
1716
+ if (!this.ready) {
1717
+ // Readiness barrier (queue-until-ready). A task dispatched the instant a
1718
+ // freshly-spawned session is launched can arrive BEFORE the PTY prints its
1719
+ // interactive prompt (this.ready flips ~2-6s later). Previously this threw
1720
+ // "not ready" and the delegated-task delivery promise requeued the task,
1721
+ // which on win32 raced the auto-launch cooldown and could strand the worker
1722
+ // idle with no work (the "first big message lost" failure). Instead, when the
1723
+ // caller allows queueing, BUFFER the message in the pending-outbound queue and
1724
+ // return — the startup-settle path flips this.ready and flushes the queue once
1725
+ // the prompt is actually up (see resolveStartupState → flushPendingOutboundQueue),
1726
+ // so the message is delivered late rather than dropped. A non-queueable caller
1727
+ // (e.g. an internal flush) still throws so it isn't silently swallowed.
1728
+ if (allowQueue) {
1729
+ this.enqueuePendingOutboundMessage(text, 'not_ready_pending_prompt', meshTaskId);
1730
+ return;
1731
+ }
1732
+ throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
1733
+ }
1961
1734
  const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1962
1735
  ? String(parsedStatusBeforeSend.status)
1963
1736
  : '';
1964
- if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
1737
+ if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating')) {
1965
1738
  const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
1966
1739
  const parsedHasActionableModal = Boolean(
1967
1740
  parsedModal
1968
1741
  && Array.isArray(parsedModal.buttons)
1969
1742
  && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1970
1743
  );
1971
- const terminalLooksIdle = this.currentStatus === 'idle'
1744
+ const terminalLooksIdle = this.engine.currentStatus === 'idle'
1972
1745
  && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1973
- && !this.isWaitingForResponse
1974
- && !this.currentTurnScope
1975
- && !this.hasActionableApproval()
1746
+ && !this.engine.isWaitingForResponse
1747
+ && !this.engine.currentTurnScope
1748
+ && !this.engine.hasActionableApproval()
1976
1749
  && !parsedHasActionableModal;
1977
1750
  if (!terminalLooksIdle) {
1751
+ if (allowQueue) {
1752
+ this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`, meshTaskId);
1753
+ return;
1754
+ }
1978
1755
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1979
1756
  }
1980
1757
  }
1981
- if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1982
- if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
1758
+ if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
1759
+ const snap = this.getSnapshot();
1760
+ if (
1761
+ !this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
1762
+ && !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
1763
+ ) {
1764
+ if (allowQueue) {
1765
+ this.enqueuePendingOutboundMessage(text, 'waiting_for_response', meshTaskId);
1766
+ return;
1767
+ }
1983
1768
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1984
1769
  }
1985
1770
  }
1986
- this.isWaitingForResponse = true;
1987
1771
  this.responseBuffer = '';
1988
- this.finishRetryCount = 0;
1989
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1990
- this.clearIdleFinishCandidate('send_message');
1991
- this.currentTurnScope = {
1772
+ const turnScope: TurnParseScope = {
1992
1773
  prompt: text,
1993
1774
  startedAt: Date.now(),
1994
1775
  bufferStart: this.accumulatedBuffer.length,
1995
1776
  rawBufferStart: this.accumulatedRawBuffer.length,
1777
+ // ARCH-REFACTOR R1: bind this turn to its mesh task. engine.onTurnStarted
1778
+ // copies this into currentTurnTaskId so the turn's completion event carries
1779
+ // the right id even if a later task overwrites the session scalar meanwhile.
1780
+ ...(typeof meshTaskId === 'string' && meshTaskId.trim() ? { taskId: meshTaskId } : {}),
1996
1781
  };
1997
- this.recordTrace('send_message', {
1998
- text: summarizeCliTraceText(text, 500),
1999
- estimatedLines: estimatePromptDisplayLines(text),
2000
- turnScope: this.currentTurnScope,
2001
- });
2002
- LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
2003
- this.submitRetryUsed = false;
2004
- this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
2005
- const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
1782
+ LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
2006
1783
  if (this.submitRetryTimer) {
2007
1784
  clearTimeout(this.submitRetryTimer);
2008
1785
  this.submitRetryTimer = null;
2009
1786
  }
1787
+ this.engine.onTurnStarted(turnScope);
1788
+ this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
1789
+ const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
2010
1790
  const estimatedLines = estimatePromptDisplayLines(text);
2011
1791
  const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
2012
1792
  const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
@@ -2018,13 +1798,10 @@ export class ProviderCliAdapter implements CliAdapter {
2018
1798
  maxEchoWaitMs,
2019
1799
  retryDelayMs,
2020
1800
  didCommitUserTurn: false,
1801
+ // Capture BEFORE the send commits — commitSendUserTurn flips firstTurnSent.
1802
+ isFirstTurn: !this.firstTurnSent,
2021
1803
  };
2022
- if (this.settleTimer) {
2023
- clearTimeout(this.settleTimer);
2024
- this.settleTimer = null;
2025
- }
2026
- this.responseEpoch += 1;
2027
- this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
1804
+ this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
2028
1805
  await new Promise<void>((resolve, reject) => {
2029
1806
  let resolved = false;
2030
1807
  const completion: SendMessageCompletion = {
@@ -2041,30 +1818,34 @@ export class ProviderCliAdapter implements CliAdapter {
2041
1818
  },
2042
1819
  };
2043
1820
 
2044
- if (this.submitStrategy === 'immediate') {
1821
+ // WIN32-READY-HOLD: the very first turn after startup is the one exposed to
1822
+ // the premature-ready swallow (ready released before win32 ConPTY input is
1823
+ // live). Force it through the atomic content+sendKey single write so the
1824
+ // submit CR can never be separated from the text it submits — the same path
1825
+ // the `immediate` strategy already uses. Restricted to win32 + the first
1826
+ // turn so Mac/linux echo-gated behavior and all later turns are unchanged.
1827
+ const useAtomicFirstTurn = this.submitStrategy === 'immediate'
1828
+ || (process.platform === 'win32' && sendState.isFirstTurn);
1829
+ if (useAtomicFirstTurn) {
2045
1830
  this.submitImmediatePrompt(sendState, completion);
2046
1831
  return;
2047
1832
  }
2048
1833
 
2049
1834
  if (submitDelayMs > 0) {
2050
- this.submitPendingUntil = Date.now() + submitDelayMs;
1835
+ this.engine.submitPendingUntil = Date.now() + submitDelayMs;
2051
1836
  }
2052
- this.recordTrace('submit_write', {
2053
- mode: 'type_then_submit',
2054
- text: summarizeCliTraceText(text, 500),
2055
- sendKey: this.sendKey,
2056
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2057
- });
2058
1837
  const submitStartedAt = Date.now();
2059
1838
  void this.writeToPty(text).then(
2060
1839
  () => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
2061
1840
  completion.rejectOnce,
2062
1841
  );
2063
1842
  });
1843
+ // Schedule settle after successful send
1844
+ this.engine.scheduleSettle();
2064
1845
  }
2065
1846
 
2066
1847
  getPartialResponse(): string {
2067
- if (!this.isWaitingForResponse) return '';
1848
+ if (!this.engine.isWaitingForResponse) return '';
2068
1849
  return this.responseBuffer;
2069
1850
  }
2070
1851
 
@@ -2077,10 +1858,10 @@ export class ProviderCliAdapter implements CliAdapter {
2077
1858
  cliType: this.cliType,
2078
1859
  cliName: this.cliName,
2079
1860
  workingDir: this.workingDir,
2080
- currentStatus: this.currentStatus,
1861
+ currentStatus: this.engine.currentStatus,
2081
1862
  ready: this.ready,
2082
- isWaitingForResponse: this.isWaitingForResponse,
2083
- activeModal: this.activeModal,
1863
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1864
+ activeModal: this.engine.activeModal,
2084
1865
  parseErrorMessage: this.parseErrorMessage,
2085
1866
  messageCounts: {
2086
1867
  parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
@@ -2106,10 +1887,10 @@ export class ProviderCliAdapter implements CliAdapter {
2106
1887
  lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
2107
1888
  },
2108
1889
  parser: {
2109
- scriptNames: listCliScriptNames(this.cliScripts),
2110
- traceSessionId: this.traceSessionId,
2111
- traceSeq: this.traceSeq,
2112
- currentTurnScope: this.currentTurnScope,
1890
+ scriptNames: this.runner.getScriptNames(),
1891
+ traceSessionId: this.engine.getTraceSessionId(),
1892
+ traceSeq: this.engine.getTraceEntries().length,
1893
+ currentTurnScope: this.engine.currentTurnScope,
2113
1894
  parsedStatusCache: parsedResult
2114
1895
  ? {
2115
1896
  id: parsedResult.id,
@@ -2122,26 +1903,25 @@ export class ProviderCliAdapter implements CliAdapter {
2122
1903
  activeModal: parsedResult.activeModal,
2123
1904
  }
2124
1905
  : null,
2125
- pendingScriptStatus: this.pendingScriptStatus,
2126
- pendingScriptStatusSince: this.pendingScriptStatusSince,
1906
+ pendingScriptStatus: this.engine.pendingScriptStatus,
1907
+ pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
2127
1908
  },
2128
1909
  runtimeMetadata: this.getRuntimeMetadata(),
2129
- statusHistory: this.statusHistory.slice(-80),
2130
- traceEntries: this.traceEntries.slice(-120),
1910
+ statusHistory: this.engine.getStatusHistory().slice(-80),
1911
+ traceEntries: this.engine.getTraceEntries().slice(-120),
2131
1912
  timing: {
2132
1913
  spawnAt: this.spawnAt,
2133
1914
  startupFirstOutputAt: this.startupFirstOutputAt,
2134
- submitPendingUntil: this.submitPendingUntil,
2135
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2136
- responseEpoch: this.responseEpoch,
1915
+ submitPendingUntil: this.engine.submitPendingUntil,
1916
+ responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
1917
+ responseEpoch: this.engine.responseEpoch,
2137
1918
  resizeSuppressUntil: this.resizeSuppressUntil,
2138
- lastApprovalResolvedAt: this.lastApprovalResolvedAt,
1919
+ lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
2139
1920
  },
2140
1921
  finish: {
2141
- idleFinishCandidate: this.idleFinishCandidate,
2142
- finishRetryCount: this.finishRetryCount,
2143
- submitRetryUsed: this.submitRetryUsed,
2144
- submitRetryPromptSnippet: this.submitRetryPromptSnippet,
1922
+ finishRetryCount: this.engine.finishRetryCount,
1923
+ submitRetryUsed: this.engine.submitRetryUsed,
1924
+ submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
2145
1925
  },
2146
1926
  };
2147
1927
  }
@@ -2151,7 +1931,48 @@ export class ProviderCliAdapter implements CliAdapter {
2151
1931
  return this.ptyProcess.getMetadata();
2152
1932
  }
2153
1933
 
1934
+ /**
1935
+ * Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
1936
+ * (pure — same inputs the live PTY was spawned with) so the dashboard sees the
1937
+ * resolved binary, full arg vector, and cwd without us having to persist it.
1938
+ * extraEnv values are intentionally dropped (keys only) so secrets passed at
1939
+ * launch time never reach the dashboard.
1940
+ */
1941
+ getLaunchInfo(): CliLaunchInfo {
1942
+ let command: string | undefined;
1943
+ let args: string[] = [...this.extraArgs];
1944
+ try {
1945
+ const plan = resolveCliSpawnPlan({
1946
+ provider: this.provider,
1947
+ runtimeSettings: this.runtimeSettings,
1948
+ workingDir: this.workingDir,
1949
+ extraArgs: this.extraArgs,
1950
+ extraEnv: this.extraEnv,
1951
+ });
1952
+ command = plan.binaryPath;
1953
+ args = plan.allArgs;
1954
+ } catch {
1955
+ // Spawn plan resolution can throw before the binary is resolvable
1956
+ // (e.g. CLI not installed). Fall back to the raw extra args so the
1957
+ // panel still shows what it can rather than failing the whole call.
1958
+ }
1959
+ return {
1960
+ command,
1961
+ args,
1962
+ extraArgs: [...this.extraArgs],
1963
+ cwd: this.workingDir,
1964
+ extraEnvKeys: Object.keys(this.extraEnv || {}),
1965
+ providerSessionId: this.providerSessionId || undefined,
1966
+ };
1967
+ }
1968
+
2154
1969
  updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
1970
+ const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
1971
+ ? meta.providerSessionId.trim()
1972
+ : '';
1973
+ if (nextProviderSessionId) {
1974
+ this.providerSessionId = nextProviderSessionId;
1975
+ }
2155
1976
  if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
2156
1977
  this.ptyProcess.updateMeta(meta, replace);
2157
1978
  }
@@ -2172,7 +1993,7 @@ export class ProviderCliAdapter implements CliAdapter {
2172
1993
  this.timeouts.shutdownGrace,
2173
1994
  typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
2174
1995
  );
2175
- const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
1996
+ const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
2176
1997
 
2177
1998
  try {
2178
1999
  if (wasProcessing) {
@@ -2210,7 +2031,7 @@ export class ProviderCliAdapter implements CliAdapter {
2210
2031
  return new Promise((resolve) => {
2211
2032
  const startedAt = Date.now();
2212
2033
  const timer = setInterval(() => {
2213
- if (!this.ptyProcess || this.currentStatus === 'stopped') {
2034
+ if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
2214
2035
  clearInterval(timer);
2215
2036
  resolve(true);
2216
2037
  return;
@@ -2224,18 +2045,20 @@ export class ProviderCliAdapter implements CliAdapter {
2224
2045
  }
2225
2046
 
2226
2047
  shutdown(): void {
2227
- this.clearIdleFinishCandidate('shutdown');
2048
+ this.engine.clearIdleFinishCandidate('shutdown');
2228
2049
  this.clearAllTimers();
2229
2050
  this.pendingOutputParseChunks = [];
2230
2051
  this.pendingTerminalQueryTail = '';
2231
2052
  this.ptyOutputChunks = [];
2232
- this.finishRetryCount = 0;
2053
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2054
+ this.pendingOutboundQueue = [];
2055
+ this.pendingOutboundFlushInFlight = false;
2233
2056
  if (this.ptyProcess) {
2234
2057
  this.ptyProcess.write('\x03');
2235
2058
  setTimeout(() => {
2236
2059
  try { this.ptyProcess?.kill(); } catch { }
2237
2060
  this.ptyProcess = null;
2238
- this.setStatus('stopped', 'stop_cmd');
2061
+ this.engine.setStatus('stopped', 'stop_cmd');
2239
2062
  this.ready = false;
2240
2063
  this.startupParseGate = false;
2241
2064
  this.spawnAt = 0;
@@ -2245,12 +2068,14 @@ export class ProviderCliAdapter implements CliAdapter {
2245
2068
  }
2246
2069
 
2247
2070
  detach(): void {
2248
- this.clearIdleFinishCandidate('detach');
2071
+ this.engine.clearIdleFinishCandidate('detach');
2249
2072
  this.clearAllTimers();
2250
2073
  this.pendingOutputParseChunks = [];
2251
2074
  this.pendingTerminalQueryTail = '';
2252
2075
  this.ptyOutputChunks = [];
2253
- this.finishRetryCount = 0;
2076
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2077
+ this.pendingOutboundQueue = [];
2078
+ this.pendingOutboundFlushInFlight = false;
2254
2079
  if (this.ptyProcess) {
2255
2080
  try {
2256
2081
  if (typeof this.ptyProcess.detach === 'function') {
@@ -2268,82 +2093,138 @@ export class ProviderCliAdapter implements CliAdapter {
2268
2093
  }
2269
2094
 
2270
2095
  clearHistory(): void {
2271
- this.clearIdleFinishCandidate('clear_history');
2096
+ this.engine.clearIdleFinishCandidate('clear_history');
2272
2097
  this.accumulatedBuffer = '';
2273
2098
  this.accumulatedRawBuffer = '';
2274
- this.currentTurnScope = null;
2275
- this.submitRetryUsed = false;
2276
- this.submitRetryPromptSnippet = '';
2099
+ this.engine.currentTurnScope = null;
2100
+ this.engine.submitRetryUsed = false;
2101
+ this.engine.submitRetryPromptSnippet = '';
2277
2102
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
2278
2103
  this.pendingOutputParseChunks = [];
2279
2104
  this.pendingTerminalQueryTail = '';
2280
2105
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2281
2106
  this.ptyOutputChunks = [];
2282
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2283
- this.finishRetryCount = 0;
2107
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2108
+ this.pendingOutboundQueue = [];
2109
+ this.pendingOutboundFlushInFlight = false;
2284
2110
  this.resetTerminalScreen();
2285
2111
  this.ptyProcess?.clearBuffer?.();
2286
2112
  this.onStatusChange?.();
2287
2113
  }
2288
2114
 
2289
- isProcessing(): boolean { return this.isWaitingForResponse; }
2115
+ isProcessing(): boolean { return this.engine.isWaitingForResponse; }
2290
2116
  isReady(): boolean { return this.ready; }
2291
2117
 
2292
- async writeRaw(data: string): Promise<void> {
2293
- this.recordTrace('write_raw', {
2294
- keys: JSON.stringify(data),
2295
- length: data.length,
2296
- });
2297
- await this.writeToPty(data);
2118
+ // ─── State machine property accessors (delegate to engine) ──────────────
2119
+ // These expose engine state for external callers (tests, debug tools, etc.)
2120
+
2121
+ get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
2122
+ set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
2123
+
2124
+ get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
2125
+ set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
2126
+
2127
+ get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
2128
+ set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
2129
+
2130
+ get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
2131
+ set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
2132
+ // ARCH-REFACTOR R1: the mesh taskId bound to the most recently started turn,
2133
+ // surviving past turn settle until the next turn starts. The provider instance
2134
+ // reads this when stamping completion events so they carry the completing turn's
2135
+ // task rather than the racy last-write-wins session scalar.
2136
+ get currentTurnTaskId(): string | null { return this.engine.currentTurnTaskId; }
2137
+ // R4d: wall-clock when the most recently started turn began (persists past settle).
2138
+ // The provider instance's startup-grace idle-stayed synthesis anchors its window on
2139
+ // this so a delayed-dispatch first turn whose duration overruns the now-anchored
2140
+ // window is still attributed to the startup collapse.
2141
+ get currentTurnStartedAt(): number { return this.engine.currentTurnStartedAt; }
2142
+
2143
+ get responseEpoch(): number { return this.engine.responseEpoch; }
2144
+ set responseEpoch(v: number) { this.engine.responseEpoch = v; }
2145
+
2146
+ get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
2147
+ set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
2148
+
2149
+ get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
2150
+ set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
2151
+
2152
+ get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
2153
+ set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
2154
+
2155
+ get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
2156
+ set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
2157
+
2158
+ get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
2159
+ set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
2160
+
2161
+ get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
2162
+ get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
2163
+
2164
+ get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
2165
+ get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
2166
+
2167
+ get finishRetryCount(): number { return this.engine.finishRetryCount; }
2168
+ set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
2169
+
2170
+ get traceSessionId(): string { return this.engine.getTraceSessionId(); }
2171
+ get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
2172
+ get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
2173
+ get traceSeq(): number { return this.engine.getTraceEntries().length; }
2174
+
2175
+ /** Expose engine's evaluateSettled for test access */
2176
+ evaluateSettled(): void {
2177
+ LOG.debug(
2178
+ 'CLI',
2179
+ `[${this.cliType}] settled diagnostics delegated to state engine`);
2180
+ this.engine.evaluateSettled(this.getSnapshot());
2181
+ }
2182
+ /** Expose engine's scheduleSettle for test access */
2183
+ scheduleSettle(): void { this.engine.scheduleSettle(); }
2184
+ /** Expose engine's clearIdleFinishCandidate for test access */
2185
+ clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
2186
+ /** Expose engine's finishResponse for test access */
2187
+ finishResponse(): void { this.engine.finishResponse(); }
2188
+ /** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
2189
+ getSnapshot(): CliBufferSnapshot {
2190
+ const screenText = this.terminalScreen.getText() || '';
2191
+ return {
2192
+ accumulatedBuffer: this.accumulatedBuffer,
2193
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
2194
+ recentOutputBuffer: this.recentOutputBuffer,
2195
+ responseBuffer: this.responseBuffer,
2196
+ screenText,
2197
+ parseScreenText: this.getParseScreenText(screenText),
2198
+ workingDir: this.workingDir,
2199
+ providerSessionId: this.providerSessionId,
2200
+ runtimeSettings: this.runtimeSettings,
2201
+ isWaitingForResponse: this.engine.isWaitingForResponse,
2202
+ currentTurnScope: this.engine.currentTurnScope,
2203
+ lastOutputAt: this.lastOutputAt,
2204
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2205
+ lastScreenChangeAt: this.lastScreenChangeAt,
2206
+ lastScreenSnapshot: this.lastScreenSnapshot,
2207
+ };
2208
+ }
2209
+ isAlive(): boolean { return this.ptyProcess !== null; }
2210
+ flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
2211
+
2212
+ async writeRaw(data: string | Buffer): Promise<void> {
2213
+ const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
2214
+ await this.writeToPty(str);
2298
2215
  }
2299
2216
 
2300
2217
  resolveModal(buttonIndex: number): void {
2301
- let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
2302
- if (!modal && typeof this.cliScripts?.parseSession === 'function') {
2303
- try {
2304
- const parsed = this.getScriptParsedStatus();
2305
- const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
2306
- && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
2307
- ? parsed.activeModal
2308
- : null;
2309
- if (parsed?.status === 'waiting_approval' && parsedModal) {
2310
- modal = parsedModal;
2311
- this.activeModal = parsedModal;
2312
- if (this.currentStatus !== 'waiting_approval') {
2313
- this.setStatus('waiting_approval', 'resolve_modal_parse');
2314
- this.onStatusChange?.();
2315
- }
2316
- }
2317
- } catch {
2318
- // Ignore parse failures here; resolveModal falls back to current state.
2319
- }
2320
- }
2321
- if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
2322
- this.clearIdleFinishCandidate('resolve_modal');
2323
- this.recordTrace('resolve_modal', {
2324
- buttonIndex,
2325
- activeModal: modal,
2326
- });
2327
- this.activeModal = null;
2328
- this.lastApprovalResolvedAt = Date.now();
2329
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2330
- if (this.approvalExitTimeout) {
2331
- clearTimeout(this.approvalExitTimeout);
2332
- this.approvalExitTimeout = null;
2333
- }
2334
- this.setStatus('generating', 'approval_resolved');
2335
- this.onStatusChange?.();
2336
- if (buttonIndex in this.approvalKeys) {
2337
- this.ptyProcess.write(this.approvalKeys[buttonIndex]);
2338
- } else {
2339
- const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
2340
- const clampedIndex = buttonCount > 0
2341
- ? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
2342
- : Math.max(0, buttonIndex);
2343
- const DOWN = '\x1B[B';
2344
- const keys = DOWN.repeat(clampedIndex) + '\r';
2345
- this.ptyProcess.write(keys);
2346
- }
2218
+ this.engine.resolveModal(buttonIndex);
2219
+ }
2220
+
2221
+ getApprovalKeyForIndex(buttonIndex: number): string | undefined {
2222
+ return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
2223
+ }
2224
+
2225
+ /** Returns true if an approval was resolved within the adapter's cooldown window. */
2226
+ isApprovalRecentlyResolved(): boolean {
2227
+ return this.engine.isApprovalRecentlyResolved();
2347
2228
  }
2348
2229
 
2349
2230
  resize(cols: number, rows: number): void {
@@ -2357,7 +2238,7 @@ export class ProviderCliAdapter implements CliAdapter {
2357
2238
  }
2358
2239
 
2359
2240
  private getParsedDebugState(): Record<string, any> | null {
2360
- if (this.startupParseGate || typeof this.cliScripts?.parseSession !== 'function') return null;
2241
+ if (this.startupParseGate || !this.runner.hasParseSession()) return null;
2361
2242
  try {
2362
2243
  const parsed = this.getScriptParsedStatus();
2363
2244
  return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
@@ -2369,18 +2250,43 @@ export class ProviderCliAdapter implements CliAdapter {
2369
2250
  getDebugState(): Record<string, any> {
2370
2251
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2371
2252
  const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2372
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
2373
- const effectiveReady = this.ready || !!startupModal;
2253
+ const startupDetectedStatus = this.startupParseGate && !startupModal
2254
+ ? this.runDetectStatus(this.recentOutputBuffer || screenText)
2255
+ : null;
2256
+ const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
2374
2257
  const parsedDebugState = this.getParsedDebugState();
2375
2258
  const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
2259
+ const hasFinalAssistant = (p: any) => {
2260
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
2261
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
2262
+ };
2263
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
2264
+ if (parsedDebugState?.status === 'error') {
2265
+ effectiveStatus = 'error';
2266
+ }
2267
+ // (fix) Mirror the getStatus contract: never surface waiting_approval
2268
+ // without a concrete modal. detectStatus alone can fire while
2269
+ // parseApproval is still null, and a bare debugState waiting_approval
2270
+ // confused dashboards reading the debug payload.
2271
+ const debugEffectiveModal = startupModal || this.engine.activeModal;
2272
+ if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
2273
+ effectiveStatus = 'waiting_approval';
2274
+ }
2275
+ if (
2276
+ effectiveStatus === 'idle'
2277
+ && parsedDebugState?.status === 'generating'
2278
+ && !hasFinalAssistant(parsedDebugState)
2279
+ ) {
2280
+ effectiveStatus = 'generating';
2281
+ }
2376
2282
  return {
2377
2283
  type: this.cliType,
2378
2284
  name: this.cliName,
2379
2285
  providerResolution: this.providerResolutionMeta,
2380
2286
  status: effectiveStatus,
2381
2287
  projectedStatus: effectiveStatus,
2382
- rawStatus: this.currentStatus,
2383
- lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
2288
+ rawStatus: this.engine.currentStatus,
2289
+ lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
2384
2290
  ready: effectiveReady,
2385
2291
  startupParseGate: this.startupParseGate,
2386
2292
  spawnAt: this.spawnAt,
@@ -2394,38 +2300,47 @@ export class ProviderCliAdapter implements CliAdapter {
2394
2300
  providerSessionId: parsedDebugState.providerSessionId,
2395
2301
  transcriptAuthority: parsedDebugState.transcriptAuthority,
2396
2302
  coverage: parsedDebugState.coverage,
2303
+ errorMessage: parsedDebugState.errorMessage,
2304
+ errorReason: parsedDebugState.errorReason,
2397
2305
  activeModal: parsedDebugState.activeModal,
2398
2306
  messageCount: parsedMessages.length,
2399
2307
  } : null,
2400
2308
  screenText: screenText.slice(-4000),
2401
- currentTurnScope: this.currentTurnScope,
2309
+ currentTurnScope: this.engine.currentTurnScope,
2402
2310
  startupBuffer: this.startupBuffer.slice(-4000),
2403
2311
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
2404
- settledBuffer: this.settledBuffer.slice(-500),
2405
2312
  accumulatedBufferLength: this.accumulatedBuffer.length,
2406
2313
  accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
2407
2314
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
2408
2315
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
2409
2316
  responseBuffer: this.responseBuffer.slice(-1000),
2317
+ pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
2318
+ id: message.id,
2319
+ role: message.role,
2320
+ content: message.content,
2321
+ queuedAt: message.queuedAt,
2322
+ source: message.source,
2323
+ })),
2324
+ pendingOutboundCount: this.pendingOutboundQueue.length,
2410
2325
  lastOutputAt: this.lastOutputAt,
2411
2326
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2412
2327
  lastScreenChangeAt: this.lastScreenChangeAt,
2413
2328
  lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
2414
- isWaitingForResponse: this.isWaitingForResponse,
2415
- activeModal: startupModal || this.activeModal,
2416
- lastApprovalResolvedAt: this.lastApprovalResolvedAt,
2329
+ isWaitingForResponse: this.engine.isWaitingForResponse,
2330
+ activeModal: startupModal || this.engine.activeModal,
2331
+ lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
2417
2332
  sendDelayMs: this.sendDelayMs,
2418
2333
  sendKey: this.sendKey,
2419
2334
  submitStrategy: this.submitStrategy,
2420
2335
  requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
2421
- submitPendingUntil: this.submitPendingUntil,
2422
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2336
+ submitPendingUntil: this.engine.submitPendingUntil,
2337
+ responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
2423
2338
  resizeSuppressUntil: this.resizeSuppressUntil,
2424
2339
  hasCliScripts: this.hasCliScripts(),
2425
- scriptNames: listCliScriptNames(this.cliScripts),
2426
- traceSessionId: this.traceSessionId,
2427
- traceEntryCount: this.traceEntries.length,
2428
- statusHistory: this.statusHistory.slice(-30),
2340
+ scriptNames: this.runner.getScriptNames(),
2341
+ traceSessionId: this.engine.getTraceSessionId(),
2342
+ traceEntryCount: this.engine.getTraceEntries().length,
2343
+ statusHistory: this.engine.getStatusHistory().slice(-30),
2429
2344
  timeouts: this.timeouts,
2430
2345
  pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
2431
2346
  pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
@@ -2435,20 +2350,21 @@ export class ProviderCliAdapter implements CliAdapter {
2435
2350
 
2436
2351
  getTraceState(limit = 120): Record<string, any> {
2437
2352
  const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
2353
+ const traceEntries = this.engine.getTraceEntries();
2438
2354
  return {
2439
- sessionId: this.traceSessionId,
2355
+ sessionId: this.engine.getTraceSessionId(),
2440
2356
  providerResolution: this.providerResolutionMeta,
2441
- entryCount: this.traceEntries.length,
2442
- entries: this.traceEntries.slice(-cappedLimit),
2357
+ entryCount: traceEntries.length,
2358
+ entries: traceEntries.slice(-cappedLimit),
2443
2359
  screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
2444
2360
  recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
2445
2361
  responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
2446
2362
  status: this.projectEffectiveStatus(),
2447
2363
  projectedStatus: this.projectEffectiveStatus(),
2448
- rawStatus: this.currentStatus,
2449
- lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
2450
- activeModal: this.activeModal,
2451
- currentTurnScope: this.currentTurnScope,
2364
+ rawStatus: this.engine.currentStatus,
2365
+ lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
2366
+ activeModal: this.engine.activeModal,
2367
+ currentTurnScope: this.engine.currentTurnScope,
2452
2368
  messages: [],
2453
2369
  };
2454
2370
  }