@adhdev/daemon-core 0.9.82-rc.4 → 0.9.82-rc.400

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 (418) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +11 -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 +67 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +123 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -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 +14 -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 +7 -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 +15 -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 +60 -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 +2 -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 +116 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router.d.ts +267 -2
  56. package/dist/commands/upgrade-helper.d.ts +41 -1
  57. package/dist/config/chat-history.d.ts +9 -0
  58. package/dist/config/config.d.ts +5 -0
  59. package/dist/config/mesh-config.d.ts +77 -1
  60. package/dist/config/mesh-json-config.d.ts +299 -0
  61. package/dist/daemon/dev-server.d.ts +0 -2
  62. package/dist/detection/ide-detector.d.ts +13 -0
  63. package/dist/detection/win32-ide-version.d.ts +37 -0
  64. package/dist/git/change-impact-config.d.ts +159 -0
  65. package/dist/git/git-commands.d.ts +12 -1
  66. package/dist/git/git-diff.d.ts +6 -0
  67. package/dist/git/git-executor.d.ts +11 -0
  68. package/dist/git/git-status.d.ts +28 -0
  69. package/dist/git/git-types.d.ts +5 -43
  70. package/dist/git/git-worktree.d.ts +22 -0
  71. package/dist/git/index.d.ts +2 -0
  72. package/dist/index.d.ts +57 -13
  73. package/dist/index.js +48373 -18578
  74. package/dist/index.js.map +1 -1
  75. package/dist/index.mjs +47576 -17923
  76. package/dist/index.mjs.map +1 -1
  77. package/dist/installer.d.ts +1 -4
  78. package/dist/ipc/local-ipc-server.d.ts +91 -0
  79. package/dist/launch.d.ts +1 -1
  80. package/dist/logging/async-batch-writer.d.ts +10 -0
  81. package/dist/logging/log-redactor.d.ts +24 -0
  82. package/dist/logging/log-tail-reader.d.ts +81 -0
  83. package/dist/logging/logger.d.ts +1 -1
  84. package/dist/mesh/contracts.d.ts +164 -0
  85. package/dist/mesh/coordinator-prompt.d.ts +89 -0
  86. package/dist/mesh/coordinator-registry.d.ts +59 -0
  87. package/dist/mesh/mesh-active-work.d.ts +174 -0
  88. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  89. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  90. package/dist/mesh/mesh-event-classify.d.ts +5 -0
  91. package/dist/mesh/mesh-event-forwarding.d.ts +19 -0
  92. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  93. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  94. package/dist/mesh/mesh-events-pending.d.ts +55 -0
  95. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  96. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  97. package/dist/mesh/mesh-events.d.ts +6 -37
  98. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  99. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  100. package/dist/mesh/mesh-init.d.ts +86 -0
  101. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  102. package/dist/mesh/mesh-ledger.d.ts +77 -1
  103. package/dist/mesh/mesh-missions.d.ts +164 -0
  104. package/dist/mesh/mesh-node-identity.d.ts +289 -0
  105. package/dist/mesh/mesh-queue-assignment.d.ts +95 -0
  106. package/dist/mesh/mesh-reconcile-loop.d.ts +65 -0
  107. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  108. package/dist/mesh/mesh-refine-gates.d.ts +428 -0
  109. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  110. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  111. package/dist/mesh/mesh-routing.d.ts +70 -0
  112. package/dist/mesh/mesh-runtime-store.d.ts +447 -0
  113. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  114. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  115. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  116. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  117. package/dist/mesh/mesh-work-queue.d.ts +250 -6
  118. package/dist/mesh/preview-freshness.d.ts +18 -0
  119. package/dist/mesh/refine-config.d.ts +216 -0
  120. package/dist/mesh/worktree-bootstrap-config.d.ts +145 -0
  121. package/dist/providers/acp-provider-instance.d.ts +5 -0
  122. package/dist/providers/approval-utils.d.ts +20 -0
  123. package/dist/providers/chat-message-normalization.d.ts +13 -0
  124. package/dist/providers/cli-provider-instance.d.ts +157 -3
  125. package/dist/providers/contracts.d.ts +139 -6
  126. package/dist/providers/external-sources.d.ts +71 -0
  127. package/dist/providers/manual-attendance.d.ts +63 -0
  128. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  129. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  130. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  131. package/dist/providers/native-history/constants.d.ts +12 -0
  132. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  133. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  134. package/dist/providers/native-history/index.d.ts +13 -0
  135. package/dist/providers/provider-instance-manager.d.ts +14 -0
  136. package/dist/providers/provider-instance.d.ts +23 -1
  137. package/dist/providers/provider-loader.d.ts +26 -4
  138. package/dist/providers/provider-trust.d.ts +31 -0
  139. package/dist/providers/read-chat-contract.d.ts +29 -0
  140. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  141. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  142. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  143. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  144. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  145. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  146. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  147. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  148. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  149. package/dist/providers/sdk/v1/index.d.ts +30 -0
  150. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  151. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  152. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  153. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  154. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  155. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  156. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  157. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  158. package/dist/providers/spec/adapter.d.ts +91 -0
  159. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  160. package/dist/providers/spec/evaluator.d.ts +45 -0
  161. package/dist/providers/spec/fsm-driver.d.ts +407 -0
  162. package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
  163. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  164. package/dist/providers/spec/fsm-types.d.ts +174 -0
  165. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  166. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  167. package/dist/providers/spec/route.d.ts +4 -0
  168. package/dist/providers/spec/types.d.ts +222 -0
  169. package/dist/providers/status-monitor.d.ts +7 -7
  170. package/dist/providers/transcript-v2.d.ts +176 -0
  171. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  172. package/dist/providers/working-dir.d.ts +17 -0
  173. package/dist/repo-mesh-types.d.ts +536 -1
  174. package/dist/runtime-defaults.d.ts +2 -0
  175. package/dist/sessions/registry.d.ts +6 -0
  176. package/dist/shared-types-extra.d.ts +2 -4
  177. package/dist/shared-types.d.ts +59 -2
  178. package/dist/status/normalize.d.ts +1 -1
  179. package/dist/status/normalize.js +1 -0
  180. package/dist/status/normalize.js.map +1 -1
  181. package/dist/status/normalize.mjs +1 -0
  182. package/dist/status/normalize.mjs.map +1 -1
  183. package/dist/status/reporter.d.ts +2 -0
  184. package/dist/status/snapshot.d.ts +26 -0
  185. package/dist/system/hash.d.ts +8 -0
  186. package/dist/system/load-better-sqlite3.d.ts +21 -0
  187. package/dist/types.d.ts +5 -0
  188. package/package.json +7 -3
  189. package/src/agent-stream/poller.ts +2 -3
  190. package/src/agent-stream/provider-adapter.ts +1 -1
  191. package/src/boot/daemon-lifecycle.ts +66 -12
  192. package/src/boot/process-hardening.ts +89 -0
  193. package/src/build-info.ts +73 -0
  194. package/src/chat/source-machine.ts +534 -0
  195. package/src/chat/source-resolver.ts +0 -0
  196. package/src/chat/subscription-updates.ts +20 -1
  197. package/src/cli-adapter-types.d.ts +1 -0
  198. package/src/cli-adapter-types.ts +68 -2
  199. package/src/cli-adapters/cli-script-runner.ts +421 -0
  200. package/src/cli-adapters/cli-state-engine.ts +1221 -0
  201. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  202. package/src/cli-adapters/provider-cli-adapter.ts +888 -1144
  203. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  204. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  205. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  206. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  207. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  208. package/src/cli-adapters/pty-transport.ts +2 -1
  209. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  210. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  211. package/src/cli-adapters/resolve-executable.ts +90 -0
  212. package/src/cli-adapters/session-host-transport.ts +2 -1
  213. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  214. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  215. package/src/cli-adapters/terminal-screen.ts +16 -81
  216. package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
  217. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  218. package/src/commands/chat-commands-read.ts +2327 -0
  219. package/src/commands/chat-commands-scope.ts +54 -0
  220. package/src/commands/chat-commands-shared.ts +114 -0
  221. package/src/commands/chat-commands-write.ts +880 -0
  222. package/src/commands/chat-commands.ts +19 -1841
  223. package/src/commands/cli-manager.ts +513 -26
  224. package/src/commands/handler.ts +841 -2
  225. package/src/commands/high-family/index.ts +28 -0
  226. package/src/commands/high-family/mesh-coordinator-launch.ts +673 -0
  227. package/src/commands/high-family/mesh-events.ts +80 -0
  228. package/src/commands/high-family/mesh-status.ts +678 -0
  229. package/src/commands/high-family/types.ts +76 -0
  230. package/src/commands/low-family/coordinator-prompt.ts +72 -0
  231. package/src/commands/low-family/daemon-lifecycle.ts +107 -0
  232. package/src/commands/low-family/diagnostics.ts +57 -0
  233. package/src/commands/low-family/index.ts +37 -0
  234. package/src/commands/low-family/mesh-ledger.ts +62 -0
  235. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  236. package/src/commands/low-family/notification.ts +116 -0
  237. package/src/commands/low-family/refine-config.ts +106 -0
  238. package/src/commands/low-family/session-host.ts +274 -0
  239. package/src/commands/low-family/spec-providerdev.ts +217 -0
  240. package/src/commands/low-family/status-meta.ts +112 -0
  241. package/src/commands/low-family/types.ts +39 -0
  242. package/src/commands/med-family/cli-agent.ts +238 -0
  243. package/src/commands/med-family/fast-forward.ts +230 -0
  244. package/src/commands/med-family/ide.ts +163 -0
  245. package/src/commands/med-family/index.ts +37 -0
  246. package/src/commands/med-family/mesh-crud.ts +865 -0
  247. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  248. package/src/commands/med-family/mesh-queue.ts +131 -0
  249. package/src/commands/med-family/mesh-restart.ts +92 -0
  250. package/src/commands/med-family/types.ts +120 -0
  251. package/src/commands/mesh-coordinator.ts +334 -124
  252. package/src/commands/router.ts +2636 -2310
  253. package/src/commands/stream-commands.ts +8 -0
  254. package/src/commands/upgrade-helper.ts +310 -45
  255. package/src/config/chat-history.ts +483 -24
  256. package/src/config/config.ts +12 -0
  257. package/src/config/mesh-config.ts +353 -22
  258. package/src/config/mesh-json-config.ts +617 -0
  259. package/src/config/recent-activity.ts +8 -2
  260. package/src/daemon/dev-auto-implement.ts +3 -2
  261. package/src/daemon/dev-cli-debug.ts +10 -1
  262. package/src/daemon/dev-server.ts +0 -541
  263. package/src/detection/cli-detector.ts +28 -9
  264. package/src/detection/ide-detector.ts +55 -16
  265. package/src/detection/win32-ide-version.ts +106 -0
  266. package/src/git/change-impact-config.ts +354 -0
  267. package/src/git/git-commands.ts +56 -15
  268. package/src/git/git-diff.ts +81 -11
  269. package/src/git/git-executor.ts +12 -0
  270. package/src/git/git-status.ts +670 -48
  271. package/src/git/git-summary.ts +3 -0
  272. package/src/git/git-types.ts +17 -54
  273. package/src/git/git-worktree.ts +76 -1
  274. package/src/git/index.ts +16 -0
  275. package/src/index.ts +179 -12
  276. package/src/installer.d.ts +1 -1
  277. package/src/installer.ts +8 -6
  278. package/src/ipc/local-ipc-server.ts +278 -0
  279. package/src/launch.d.ts +1 -1
  280. package/src/launch.ts +37 -28
  281. package/src/logging/async-batch-writer.ts +55 -0
  282. package/src/logging/command-log.ts +7 -5
  283. package/src/logging/log-redactor.ts +100 -0
  284. package/src/logging/log-tail-reader.ts +341 -0
  285. package/src/logging/logger.ts +14 -7
  286. package/src/mesh/contracts.ts +338 -0
  287. package/src/mesh/coordinator-prompt.ts +381 -33
  288. package/src/mesh/coordinator-registry.ts +121 -0
  289. package/src/mesh/mesh-active-work.ts +645 -0
  290. package/src/mesh/mesh-coordinator-config.ts +97 -0
  291. package/src/mesh/mesh-delivery-policy.ts +315 -0
  292. package/src/mesh/mesh-event-classify.ts +51 -0
  293. package/src/mesh/mesh-event-forwarding.ts +1762 -0
  294. package/src/mesh/mesh-event-trace.ts +67 -0
  295. package/src/mesh/mesh-events-coordinator.ts +31 -0
  296. package/src/mesh/mesh-events-pending.ts +590 -0
  297. package/src/mesh/mesh-events-stale.ts +389 -0
  298. package/src/mesh/mesh-events-utils.ts +443 -0
  299. package/src/mesh/mesh-events.ts +32 -897
  300. package/src/mesh/mesh-fast-forward.ts +839 -0
  301. package/src/mesh/mesh-host-ownership.ts +73 -0
  302. package/src/mesh/mesh-init.ts +260 -0
  303. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  304. package/src/mesh/mesh-ledger.ts +596 -102
  305. package/src/mesh/mesh-missions.ts +322 -0
  306. package/src/mesh/mesh-node-identity.ts +1887 -0
  307. package/src/mesh/mesh-queue-assignment.ts +1726 -0
  308. package/src/mesh/mesh-reconcile-loop.ts +1550 -0
  309. package/src/mesh/mesh-refine-batch.ts +205 -0
  310. package/src/mesh/mesh-refine-gates.ts +1660 -0
  311. package/src/mesh/mesh-refine-status.ts +231 -0
  312. package/src/mesh/mesh-review-inbox.ts +307 -0
  313. package/src/mesh/mesh-routing.ts +291 -0
  314. package/src/mesh/mesh-runtime-store.ts +1817 -0
  315. package/src/mesh/mesh-scheduling-runtime.ts +201 -0
  316. package/src/mesh/mesh-task-stats.ts +161 -0
  317. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  318. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  319. package/src/mesh/mesh-work-queue.ts +1178 -162
  320. package/src/mesh/preview-freshness.ts +118 -0
  321. package/src/mesh/refine-config.ts +423 -0
  322. package/src/mesh/worktree-bootstrap-config.ts +369 -0
  323. package/src/providers/acp-provider-instance.ts +43 -10
  324. package/src/providers/approval-utils.d.ts +5 -0
  325. package/src/providers/approval-utils.ts +57 -5
  326. package/src/providers/chat-message-normalization.ts +48 -6
  327. package/src/providers/cli-provider-instance.ts +1580 -103
  328. package/src/providers/contracts.d.ts +55 -0
  329. package/src/providers/contracts.ts +150 -6
  330. package/src/providers/extension-provider-instance.ts +12 -7
  331. package/src/providers/external-sources.ts +218 -0
  332. package/src/providers/ide-provider-instance.ts +35 -12
  333. package/src/providers/manual-attendance.ts +85 -0
  334. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  335. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  336. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  337. package/src/providers/native-history/constants.ts +19 -0
  338. package/src/providers/native-history/dispatcher.ts +339 -0
  339. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  340. package/src/providers/native-history/index.ts +30 -0
  341. package/src/providers/provider-instance-manager.ts +30 -0
  342. package/src/providers/provider-instance.ts +19 -1
  343. package/src/providers/provider-loader.ts +668 -50
  344. package/src/providers/provider-schema.ts +87 -14
  345. package/src/providers/provider-trust.ts +114 -0
  346. package/src/providers/read-chat-contract.ts +76 -16
  347. package/src/providers/sdk/README.md +49 -0
  348. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  349. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  350. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  351. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  352. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  353. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  354. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  355. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  356. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  357. package/src/providers/sdk/v1/index.ts +152 -0
  358. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  359. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  360. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  361. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  362. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  363. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  364. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  365. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  366. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  367. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  368. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  369. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  370. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  371. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  372. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  373. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  374. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  375. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  376. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  377. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  378. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  379. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  380. package/src/providers/sdk/v1/validators/index.ts +19 -0
  381. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  382. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  383. package/src/providers/spec/adapter.ts +235 -0
  384. package/src/providers/spec/cli-adapter.ts +1056 -0
  385. package/src/providers/spec/evaluator.ts +407 -0
  386. package/src/providers/spec/fsm-driver.ts +1304 -0
  387. package/src/providers/spec/fsm-evaluator.ts +272 -0
  388. package/src/providers/spec/fsm-loader.ts +120 -0
  389. package/src/providers/spec/fsm-types.ts +273 -0
  390. package/src/providers/spec/native-history-executor.ts +1128 -0
  391. package/src/providers/spec/pre-launch-trust.ts +104 -0
  392. package/src/providers/spec/route.ts +51 -0
  393. package/src/providers/spec/types.ts +262 -0
  394. package/src/providers/status-monitor.d.ts +7 -7
  395. package/src/providers/status-monitor.ts +37 -22
  396. package/src/providers/transcript-v2.ts +567 -0
  397. package/src/providers/types/interactive-prompt.ts +536 -0
  398. package/src/providers/version-archive.ts +64 -24
  399. package/src/providers/working-dir.ts +23 -0
  400. package/src/repo-mesh-types.ts +755 -2
  401. package/src/runtime-defaults.ts +39 -0
  402. package/src/sessions/registry.ts +6 -0
  403. package/src/shared-types-extra.ts +2 -4
  404. package/src/shared-types.d.ts +8 -0
  405. package/src/shared-types.ts +64 -1
  406. package/src/status/builders.ts +26 -6
  407. package/src/status/normalize.ts +2 -0
  408. package/src/status/reporter.ts +19 -1
  409. package/src/status/snapshot.ts +95 -26
  410. package/src/system/hash.ts +23 -0
  411. package/src/system/host-memory.ts +29 -12
  412. package/src/system/load-better-sqlite3.ts +68 -0
  413. package/src/types.ts +5 -0
  414. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  415. package/dist/mesh/mesh-sync.d.ts +0 -53
  416. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  417. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  418. 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,14 @@ 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
+ }
121
+
113
122
  export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
114
123
  if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
115
124
  if (maxChars <= 0) return '';
@@ -119,27 +128,63 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
119
128
  return current.slice(-keepFromCurrent) + chunk;
120
129
  }
121
130
 
131
+ // Force-send (mesh coordinator dispatch / reconcile redelivery) writes raw
132
+ // keystrokes straight into the PTY, bypassing the normal echo-wait submit
133
+ // pipeline.
134
+ //
135
+ // History of this path's failure modes:
136
+ // 1. Originally it wrote `text + sendKey` in a single PTY write with zero
137
+ // settle time. Injected into an idle TUI input box that was still entering
138
+ // its input-accepting state, the trailing submit key could be swallowed —
139
+ // prompt typed but never submitted ("text injected, Enter not pressed").
140
+ // 2. The first fix split inject from submit: write(text) → echo-gated settle →
141
+ // write(sendKey) as a *separate* PTY write. That broke win32: ConPTY/TUI
142
+ // does not recognize a lone '\r' that arrives in its own PTY chunk
143
+ // 150ms+ after the text as a submit key, and the win32 echo gate never
144
+ // matched (ConPTY echo/parsing differences), so the cap was burned and the
145
+ // same detached lone CR was emitted — adding latency while the Enter still
146
+ // got swallowed.
147
+ //
148
+ // Current behavior: keep a fixed settle gap so the input handler is in its
149
+ // input-accepting state, then write `content + sendKey` as a SINGLE atomic PTY
150
+ // write — the same verified mechanism the normal `immediate` submit strategy
151
+ // uses (submitImmediatePrompt). With the Enter in the same write unit as the
152
+ // text, win32 ConPTY always sees it as a submit and the race that the split was
153
+ // trying to solve cannot happen (the Enter can never be separated from the
154
+ // text). FORCE_SUBMIT_SETTLE_MS is that minimum pre-submit gap.
155
+ const FORCE_SUBMIT_SETTLE_MS = 150;
156
+
122
157
  // ─── Adapter ────────────────────────────────────────
123
158
 
124
159
  export class ProviderCliAdapter implements CliAdapter {
125
- readonly cliType: string;
160
+ cliType: string;
126
161
  readonly cliName: string;
127
162
  public workingDir: string;
128
163
 
129
164
  private provider: CliProviderModule;
130
165
  private ptyProcess: PtyRuntimeTransport | null = null;
131
166
  private transportFactory: PtyTransportFactory;
132
- private currentStatus: CliSessionStatus['status'] = 'starting';
133
167
  private onStatusChange: (() => void) | null = null;
134
168
 
169
+ // ─── State machine engine ─────────────────────────
170
+ readonly engine: CliStateEngine;
171
+
135
172
  private responseBuffer = '';
136
173
  private recentOutputBuffer = '';
137
- private isWaitingForResponse = false;
138
- private activeModal: { message: string; buttons: string[] } | null = null;
139
- private parseErrorMessage: string | null = null;
174
+ private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
175
+ private providerSessionId: string | null = null;
140
176
  private responseTimeout: NodeJS.Timeout | null = null;
141
- private idleTimeout: NodeJS.Timeout | null = null;
142
177
  private ready = false;
178
+ // WIN32-READY-HOLD: the ready barrier can release on screen/spec-FSM grace
179
+ // before win32 ConPTY's input layer is live. The first split write (text, then a
180
+ // separate trailing CR via waitForEchoAndSubmit) then has its submit CR swallowed,
181
+ // and every CR-only retry re-sends a bare CR the input layer keeps dropping — the
182
+ // first message is typed-but-never-submitted and lost. Routing only the FIRST turn
183
+ // through the atomic content+sendKey single write (submitImmediatePrompt) keeps the
184
+ // Enter in the same PTY write unit as the text, the invariant win32 ConPTY needs to
185
+ // recognize a submit, so the swallow is bypassed. Subsequent turns (input layer now
186
+ // proven live) keep the normal echo-gated path. Flips true on first committed turn.
187
+ private firstTurnSent = false;
143
188
  private startupBuffer = '';
144
189
  private startupParseGate = false;
145
190
  private startupSettleTimer: NodeJS.Timeout | null = null;
@@ -164,39 +209,48 @@ export class ProviderCliAdapter implements CliAdapter {
164
209
  private serverConn: any = null;
165
210
  private logBuffer: { message: string; level: string }[] = [];
166
211
 
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;
212
+ private pendingOutboundQueue: PendingOutboundMessage[] = [];
213
+ private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
214
+ private pendingOutboundFlushInFlight = false;
215
+ // Submit retry timer — PTY-level, not state machine
183
216
  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;
189
217
 
190
218
  // Resize redraw suppression
191
219
  private resizeSuppressUntil: number = 0;
192
220
 
193
- // Debug: status transition history
194
- private statusHistory: { status: string; at: number; trigger?: string }[] = [];
221
+ // (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
222
+ // ChatSourceMachine holds the lock by state, not by a mutable field on
223
+ // the adapter. Removed entirely; no callers remain after the readChat
224
+ // ladder was replaced.
225
+
226
+ // ─── Script runner (parsing isolated here, adapter stays as transport) ───
227
+ private readonly runner: CliScriptRunner;
228
+ /** @deprecated use runner.cliScripts for direct script access */
229
+ get cliScripts(): CliScripts { return this.runner.cliScripts; }
230
+
231
+ /**
232
+ * Recent script invocations (oldest → newest). Exposed via the
233
+ * `get_chat_debug_bundle` daemon command so anyone debugging a
234
+ * stuck-status regression can read what each detectStatus /
235
+ * parseSession call actually saw and returned, instead of having
236
+ * to instrument the daemon.
237
+ */
238
+ getScriptInvocationTrace() {
239
+ return this.runner.getInvocationTrace();
240
+ }
195
241
 
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;
242
+ /**
243
+ * Returns the full raw PTY byte stream captured since adapter start.
244
+ * Used by the `record_provider_pty` IPC command to produce fixtures.
245
+ * Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
246
+ */
247
+ getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
248
+ return {
249
+ text: this.accumulatedRawBuffer,
250
+ droppedChars: this.accumulatedRawBufferDroppedChars,
251
+ };
252
+ }
253
+ set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
200
254
  private runtimeSettings: Record<string, any> = {};
201
255
  /** Full accumulated rendered PTY transcript for parser/readback use */
202
256
  private accumulatedBuffer: string = '';
@@ -205,7 +259,7 @@ export class ProviderCliAdapter implements CliAdapter {
205
259
  /** Full accumulated raw PTY output (with ANSI) */
206
260
  private accumulatedRawBuffer: string = '';
207
261
  /** Current visible terminal screen snapshot */
208
- private terminalScreen = new TerminalScreen(24, 80);
262
+ private terminalScreen = new TerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
209
263
  private static readonly MAX_RESPONSE_BUFFER = 8000;
210
264
  private static readonly MAX_RECENT_OUTPUT_BUFFER = 1000;
211
265
  private responseBufferDroppedChars = 0;
@@ -216,10 +270,6 @@ export class ProviderCliAdapter implements CliAdapter {
216
270
  * Hermes turn (tool calls + reasoning + final bubble) without the
217
271
  * rolling window pushing the turn's ╭─ opening line out of view. */
218
272
  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
273
  private parsedStatusCache: {
224
274
  responseBuffer: string;
225
275
  currentTurnScope: TurnParseScope | null;
@@ -233,11 +283,8 @@ export class ProviderCliAdapter implements CliAdapter {
233
283
  result: any;
234
284
  } | null = null;
235
285
  private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
236
- private static readonly MAX_TRACE_ENTRIES = 250;
237
286
 
238
287
  private readonly providerResolutionMeta: ProviderResolutionMeta;
239
- private static readonly FINISH_RETRY_DELAY_MS = 300;
240
- private static readonly MAX_FINISH_RETRIES = 2;
241
288
 
242
289
  private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
243
290
  const build = (droppedChars: number, maxChars: number) => droppedChars > 0
@@ -313,13 +360,13 @@ export class ProviderCliAdapter implements CliAdapter {
313
360
  if (
314
361
  cached
315
362
  && cached.responseBuffer === this.responseBuffer
316
- && cached.currentTurnScope === this.currentTurnScope
363
+ && cached.currentTurnScope === this.engine.currentTurnScope
317
364
  && cached.recentOutputBuffer === this.recentOutputBuffer
318
365
  && cached.accumulatedBuffer === this.accumulatedBuffer
319
366
  && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
320
367
  && cached.screenText === this.lastScreenText
321
- && cached.currentStatus === this.currentStatus
322
- && cached.activeModal === this.activeModal
368
+ && cached.currentStatus === this.engine.currentStatus
369
+ && cached.activeModal === this.engine.activeModal
323
370
  && cached.cliName === this.cliName
324
371
  ) {
325
372
  return cached.result;
@@ -343,86 +390,6 @@ export class ProviderCliAdapter implements CliAdapter {
343
390
  return this.timeouts.statusActivityHold;
344
391
  }
345
392
 
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
- });
424
- }
425
-
426
393
  // Resolved timeouts
427
394
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
428
395
 
@@ -432,7 +399,6 @@ export class ProviderCliAdapter implements CliAdapter {
432
399
  private readonly sendKey: string;
433
400
  private readonly submitStrategy: 'wait_for_echo' | 'immediate';
434
401
  private readonly requirePromptEchoBeforeSubmit: boolean;
435
- private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
436
402
 
437
403
  constructor(
438
404
  provider: CliProviderModule,
@@ -441,6 +407,7 @@ export class ProviderCliAdapter implements CliAdapter {
441
407
  private extraEnv: Record<string, string> = {},
442
408
  transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
443
409
  ) {
410
+ this.runner = new CliScriptRunner(provider.type);
444
411
  this.provider = provider;
445
412
  this.transportFactory = transportFactory;
446
413
  this.cliType = provider.type;
@@ -458,10 +425,33 @@ export class ProviderCliAdapter implements CliAdapter {
458
425
  this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
459
426
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
460
427
 
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);
428
+ // State machine engineowns all status transitions
429
+ this.engine = new CliStateEngine(
430
+ provider,
431
+ this.runner,
432
+ this as unknown as CliTransportAccess,
433
+ {
434
+ onStatusChange: () => { this.onStatusChange?.(); },
435
+ onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
436
+ onTurnCompleted: () => { this.responseBuffer = ''; },
437
+ } satisfies CliStateEngineCallbacks,
438
+ resolvedConfig.timeouts,
439
+ );
440
+
441
+ // Scripts delegated to CliScriptRunner — adapter stays as transport.
442
+ // Pass the manifest tui block so the runner can build the
443
+ // declarativeDetectStatus / declarativeParseApproval SDK functions
444
+ // that v1 extended-tier overrides depend on (without these, scripts
445
+ // like codex-cli's detect_status v1 fail-closed with return 'idle'
446
+ // and the session sticks in `generating` forever).
447
+ this.runner.setScripts(provider.scripts || {}, provider.tui);
448
+ // Per-invocation wall-clock budget for provider scripts. Manifests may
449
+ // raise this for genuinely slow parsers, but the default (50ms) is the
450
+ // settle-loop safety net — any script that exceeds it gets flagged in
451
+ // the invocation trace and surfaced via the debug bundle so we can
452
+ // identify the offender instead of guessing why the settle loop hangs.
453
+ this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
454
+ const scriptNames = this.runner.getScriptNames();
465
455
  if (scriptNames.length > 0) {
466
456
  LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
467
457
  LOG.info(
@@ -487,14 +477,9 @@ export class ProviderCliAdapter implements CliAdapter {
487
477
 
488
478
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
489
479
  setCliScripts(scripts: CliScripts): void {
490
- this.cliScripts = scripts;
480
+ this.runner.setScripts(scripts);
491
481
  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(', ')}]`);
482
+ LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
498
483
  }
499
484
 
500
485
  /** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
@@ -548,15 +533,6 @@ export class ProviderCliAdapter implements CliAdapter {
548
533
  });
549
534
 
550
535
  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
536
 
561
537
  try {
562
538
  this.ptyProcess = this.transportFactory.spawn(
@@ -620,13 +596,12 @@ export class ProviderCliAdapter implements CliAdapter {
620
596
  this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
621
597
  LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
622
598
  this.flushPendingOutputParse();
623
- this.recordTrace('exit', { exitCode });
624
599
  this.ptyProcess = null;
625
- this.setStatus('stopped', 'pty_exit');
600
+ this.engine.onPtyExit();
626
601
  this.ready = false;
627
602
  this.startupParseGate = false;
628
603
  this.spawnAt = 0;
629
- this.scriptState = null;
604
+ this.runner.resetSessionState();
630
605
  this.onStatusChange?.();
631
606
  });
632
607
 
@@ -635,17 +610,14 @@ export class ProviderCliAdapter implements CliAdapter {
635
610
  this.startupBuffer = '';
636
611
  this.startupFirstOutputAt = 0;
637
612
  if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
638
- this.resetTerminalScreen(24, 80);
613
+ this.resetTerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
639
614
  this.pendingTerminalQueryTail = '';
640
- this.currentTurnScope = null;
641
- this.finishRetryCount = 0;
642
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
643
615
  this.ready = false;
616
+ // Each fresh spawn re-enters the premature-ready swallow window — the next
617
+ // turn is again a "first turn" and must use the win32-safe atomic send path.
618
+ this.firstTurnSent = false;
644
619
  await this.ptyProcess.ready;
645
- this.recordTrace('ready', {
646
- runtimeMeta: this.getRuntimeMetadata(),
647
- });
648
- this.setStatus('starting', 'pty_ready');
620
+ this.engine.onSpawnReady();
649
621
  this.scheduleStartupSettleCheck();
650
622
  this.onStatusChange?.();
651
623
  }
@@ -671,11 +643,11 @@ export class ProviderCliAdapter implements CliAdapter {
671
643
  if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
672
644
  this.startupFirstOutputAt = now;
673
645
  }
674
- if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
675
- this.clearIdleFinishCandidate('new_output');
646
+ if (rawData.length > 0 || cleanData.length > 0) {
647
+ this.engine.clearIdleFinishCandidate('new_output');
676
648
  }
677
649
  if (getDebugRuntimeConfig().collectDebugTrace) {
678
- this.recordTrace('output', {
650
+ this.engine.recordExternalTrace('output', {
679
651
  rawLength: rawData.length,
680
652
  cleanLength: cleanData.length,
681
653
  rawPreview: summarizeCliTraceText(rawData, 300),
@@ -687,7 +659,7 @@ export class ProviderCliAdapter implements CliAdapter {
687
659
  this.scheduleStartupSettleCheck();
688
660
  }
689
661
 
690
- if (this.isWaitingForResponse && cleanData) {
662
+ if (this.engine.isWaitingForResponse && cleanData) {
691
663
  const previousResponseLen = this.responseBuffer.length;
692
664
  this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
693
665
  this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
@@ -726,19 +698,19 @@ export class ProviderCliAdapter implements CliAdapter {
726
698
  // Keep turn-scope offsets aligned with the truncated buffer so scoped
727
699
  // parses don't lose the beginning of a long turn (e.g. the Hermes
728
700
  // ╭─ opening line) when the rolling window sheds bytes.
729
- if (this.currentTurnScope) {
701
+ if (this.engine.currentTurnScope) {
730
702
  if (droppedClean > 0) {
731
- this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
703
+ this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
732
704
  }
733
705
  if (droppedRaw > 0) {
734
- this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
706
+ this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
735
707
  }
736
708
  }
737
709
 
738
710
  this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
739
711
 
740
712
  // ─── Script-based status detection
741
- this.scheduleSettle();
713
+ this.engine.scheduleSettle();
742
714
  }
743
715
 
744
716
  private resolveStartupState(
@@ -761,6 +733,11 @@ export class ProviderCliAdapter implements CliAdapter {
761
733
  if (stableMs < 2000) return;
762
734
 
763
735
  const startupModal = this.runParseApproval(this.recentOutputBuffer);
736
+ const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
737
+ if (!startupModal && startupStatus !== 'idle') {
738
+ this.scheduleStartupSettleCheck();
739
+ return;
740
+ }
764
741
  this.startupParseGate = false;
765
742
  if (this.startupSettleTimer) {
766
743
  clearTimeout(this.startupSettleTimer);
@@ -768,20 +745,33 @@ export class ProviderCliAdapter implements CliAdapter {
768
745
  }
769
746
  this.ready = true;
770
747
  if (startupModal) {
771
- this.activeModal = startupModal;
772
- this.setStatus('waiting_approval', `startup_ready:${trigger}`);
748
+ this.engine.activeModal = startupModal;
749
+ this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
773
750
  } else {
774
- if (this.currentStatus === 'waiting_approval' || this.activeModal) {
775
- this.lastApprovalResolvedAt = Date.now();
751
+ if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
752
+ this.engine.lastApprovalResolvedAt = Date.now();
776
753
  }
777
- this.activeModal = null;
778
- this.setStatus('idle', `startup_ready:${trigger}`);
754
+ this.engine.activeModal = null;
755
+ // Clear the in-flight turn flag at the same time we declare
756
+ // startup-idle. Otherwise the next settled evaluation sees
757
+ // isWaitingForResponse=true + recent CLI welcome-screen paints
758
+ // and flips us right back to generating via the hold path
759
+ // (the "startup → generating → idle → generating → idle"
760
+ // flicker the user observed on claude-cli launch).
761
+ this.engine.isWaitingForResponse = false;
762
+ this.engine.currentTurnScope = null;
763
+ this.engine.setStatus('idle', `startup_ready:${trigger}`);
779
764
  }
780
765
  LOG.info(
781
766
  'CLI',
782
767
  `[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
783
768
  );
784
769
  this.onStatusChange?.();
770
+ // Readiness barrier flush: a message queued because the session was not yet
771
+ // ready (sendMessageNow's not_ready_pending_prompt path) has no turn-completion
772
+ // event to trigger its flush. Now that the interactive prompt is up and we are
773
+ // idle, drain it. No-op when the queue is empty or we settled to a modal.
774
+ if (!startupModal) this.schedulePendingOutboundFlush();
785
775
  }
786
776
 
787
777
  private scheduleStartupSettleCheck(): void {
@@ -801,88 +791,6 @@ export class ProviderCliAdapter implements CliAdapter {
801
791
  }, delayMs);
802
792
  }
803
793
 
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
794
  private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
887
795
  const startedAt = Date.now();
888
796
  let loggedWait = false;
@@ -892,7 +800,7 @@ export class ProviderCliAdapter implements CliAdapter {
892
800
  const screenText = this.terminalScreen.getText() || '';
893
801
  const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
894
802
  const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
895
- const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
803
+ const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
896
804
  const interactiveReady = status === 'idle'
897
805
  && stableMs >= 700
898
806
  && recentlyOutput >= 350;
@@ -926,637 +834,84 @@ export class ProviderCliAdapter implements CliAdapter {
926
834
 
927
835
  private clearAllTimers(): void {
928
836
  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
837
  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
838
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
936
839
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
840
+ this.engine.clearAllTimers();
937
841
  }
938
842
 
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;
999
-
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
- });
843
+ // ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
1020
844
 
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
- }),
845
+ runParseSession(): ParsedSession | null {
846
+ const screenText = this.terminalScreen.getText();
847
+ const parseScreenText = this.getParseScreenText(screenText);
848
+ const tail = this.recentOutputBuffer.slice(-500);
849
+ const input = buildCliParseInput({
850
+ accumulatedBuffer: this.accumulatedBuffer,
851
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
852
+ recentOutputBuffer: this.recentOutputBuffer,
853
+ terminalScreenText: parseScreenText,
854
+ workingDir: this.workingDir,
855
+ providerSessionId: this.providerSessionId || undefined,
856
+ historySessionId: this.providerSessionId || undefined,
857
+ baseMessages: [],
858
+ partialResponse: this.responseBuffer,
859
+ isWaitingForResponse: this.engine.isWaitingForResponse,
860
+ scope: this.engine.currentTurnScope,
861
+ runtimeSettings: this.runtimeSettings,
862
+ spawnAt: this.spawnAt,
1043
863
  });
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
- }),
864
+ const session = this.runner.parseSession({
865
+ ...input,
866
+ tail,
867
+ tailScreen: buildCliScreenSnapshot(tail),
1168
868
  });
1169
- this.onStatusChange?.();
1170
- }
1171
-
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
- }
869
+ if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
870
+ return session;
1206
871
  }
1207
872
 
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
- }),
873
+ runDetectStatus(text: string): string | null {
874
+ const screenText = this.terminalScreen.getText();
875
+ const tail = text.slice(-500);
876
+ return this.runner.detectStatus({
877
+ tail,
878
+ screenText,
879
+ rawBuffer: this.accumulatedRawBuffer,
880
+ isWaitingForResponse: this.engine.isWaitingForResponse,
881
+ screen: buildCliScreenSnapshot(screenText),
882
+ tailScreen: buildCliScreenSnapshot(tail),
1339
883
  });
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
884
  }
1376
885
 
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),
886
+ runParseApproval(tail: string): { message: string; buttons: string[] } | null {
887
+ const screenText = this.terminalScreen.getText();
888
+ const buffer = screenText || this.accumulatedBuffer;
889
+ return this.runner.parseApproval({
890
+ buffer,
891
+ screenText,
892
+ rawBuffer: this.accumulatedRawBuffer,
893
+ tail,
894
+ screen: buildCliScreenSnapshot(screenText),
895
+ bufferScreen: buildCliScreenSnapshot(buffer),
896
+ tailScreen: buildCliScreenSnapshot(tail),
1408
897
  });
1409
- return true;
1410
898
  }
1411
899
 
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
- }
1511
-
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;
900
+ private applyParsedSessionMetadata(parsed: any): void {
901
+ const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
902
+ ? parsed.providerSessionId.trim()
903
+ : '';
904
+ if (providerSessionId && providerSessionId !== this.providerSessionId) {
905
+ this.providerSessionId = providerSessionId;
906
+ this.updateRuntimeMeta({ providerSessionId });
1548
907
  }
1549
908
  }
1550
909
 
1551
- private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
1552
- return !!(startupModal || this.activeModal);
1553
- }
1554
-
1555
910
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1556
911
  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;
912
+ if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
913
+ if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
914
+ return this.engine.currentStatus;
1560
915
  }
1561
916
 
1562
917
  // ─── Public API (CliAdapter) ───────────────────
@@ -1564,17 +919,73 @@ export class ProviderCliAdapter implements CliAdapter {
1564
919
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1565
920
  const allowParse = options.allowParse !== false;
1566
921
  const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
922
+ const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
923
+ ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
924
+ : null;
1567
925
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1568
- let effectiveModal = startupModal || this.activeModal;
926
+ let effectiveModal = startupModal || this.engine.activeModal;
927
+ // (fix) When we have no captured modal yet, take one more live attempt
928
+ // with the current screen text — the engine's settle pass can miss
929
+ // the modal when it happens to fire exactly between writes, and
930
+ // without a modal here the dashboard could never show the buttons.
931
+ // We deliberately do NOT overwrite an existing engine.activeModal so
932
+ // a stable matched modal wins. This runs even outside the startup
933
+ // gate because Claude's approval frames can appear long after launch.
934
+ if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
935
+ const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
936
+ if (liveDetect === 'waiting_approval') {
937
+ const liveModal = this.runParseApproval(this.terminalScreen.getText())
938
+ || this.runParseApproval(this.recentOutputBuffer);
939
+ // (fix) Only surface modals that have at least one non-empty
940
+ // button. Half-rendered approval frames produced
941
+ // { message, buttons: [] } briefly — auto-approve would then
942
+ // pick buttonIndex=-1 and resolveModal would write the
943
+ // provider's index-0 key into the prompt. Skip those frames
944
+ // and let a later evaluate find the complete modal.
945
+ const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
946
+ && liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
947
+ if (liveModal && buttonsOk) {
948
+ effectiveModal = liveModal;
949
+ if (!this.engine.activeModal) this.engine.activeModal = liveModal;
950
+ }
951
+ }
952
+ }
953
+ // Only surface waiting_approval when we ALSO have a concrete modal
954
+ // (message + buttons). detectStatus alone can fire while parseApproval
955
+ // is still null — the engine logs "detectStatus=waiting_approval but
956
+ // parseApproval returned null; ignoring". Without this guard getStatus
957
+ // was shipping a bare waiting_approval with activeModal=null and the
958
+ // user perceived the flow as broken.
959
+ if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
960
+ effectiveStatus = 'waiting_approval';
961
+ } else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
962
+ effectiveStatus = 'idle';
963
+ }
1569
964
  if (allowParse && !startupModal && !effectiveModal) {
1570
965
  const parsed = this.getFreshParsedStatusCache();
1571
966
  const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1572
967
  && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1573
968
  ? parsed.activeModal
1574
969
  : null;
970
+ const hasFinalAssistant = (p: any) => {
971
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
972
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
973
+ };
1575
974
  if (parsed?.status === 'waiting_approval' && parsedModal) {
1576
975
  effectiveStatus = 'waiting_approval';
1577
976
  effectiveModal = parsedModal;
977
+ } else if (
978
+ effectiveStatus === 'idle'
979
+ && parsed?.status === 'generating'
980
+ && !hasFinalAssistant(parsed)
981
+ ) {
982
+ effectiveStatus = 'generating';
983
+ } else if (
984
+ effectiveStatus === 'generating'
985
+ && parsed?.status === 'idle'
986
+ && hasFinalAssistant(parsed)
987
+ ) {
988
+ effectiveStatus = 'idle';
1578
989
  }
1579
990
  }
1580
991
  const bufferState = this.getBufferState();
@@ -1583,8 +994,21 @@ export class ProviderCliAdapter implements CliAdapter {
1583
994
  messages: [],
1584
995
  workingDir: this.workingDir,
1585
996
  activeModal: effectiveModal,
1586
- errorMessage: this.parseErrorMessage || undefined,
1587
- errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
997
+ approvalEntrySeq: this.engine.approvalEntrySeq,
998
+ lastResolvedEntrySeq: this.engine.lastResolvedEntrySeq,
999
+ pendingOutboundCount: this.pendingOutboundQueue.length,
1000
+ pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
1001
+ id: message.id,
1002
+ role: message.role,
1003
+ content: message.content,
1004
+ queuedAt: message.queuedAt,
1005
+ source: message.source,
1006
+ })),
1007
+ errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
1008
+ errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
1009
+ providerSessionId: this.providerSessionId || undefined,
1010
+ lastOutputAt: this.lastOutputAt,
1011
+ lastScreenChangeAt: this.lastScreenChangeAt,
1588
1012
  ...(bufferState ? { bufferState } : {}),
1589
1013
  };
1590
1014
  }
@@ -1600,15 +1024,16 @@ export class ProviderCliAdapter implements CliAdapter {
1600
1024
  const cached = this.parsedStatusCache;
1601
1025
  const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1602
1026
  if (
1603
- cached
1027
+ !this.providerOwnsTranscript()
1028
+ && cached
1604
1029
  && cached.responseBuffer === this.responseBuffer
1605
- && cached.currentTurnScope === this.currentTurnScope
1030
+ && cached.currentTurnScope === this.engine.currentTurnScope
1606
1031
  && cached.recentOutputBuffer === this.recentOutputBuffer
1607
1032
  && cached.accumulatedBuffer === this.accumulatedBuffer
1608
1033
  && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
1609
1034
  && cached.screenText === parseScreenText
1610
- && cached.currentStatus === this.currentStatus
1611
- && cached.activeModal === this.activeModal
1035
+ && cached.currentStatus === this.engine.currentStatus
1036
+ && cached.activeModal === this.engine.activeModal
1612
1037
  && cached.cliName === this.cliName
1613
1038
  ) {
1614
1039
  return cached.result;
@@ -1623,14 +1048,20 @@ export class ProviderCliAdapter implements CliAdapter {
1623
1048
  const bufferState = this.getBufferState();
1624
1049
  const result = {
1625
1050
  id: (parsed as any).id || 'cli_session',
1626
- status: parsed.status || this.currentStatus,
1051
+ status: parsed.status || this.engine.currentStatus,
1627
1052
  title: (parsed as any).title || this.cliName,
1628
1053
  messages: normalizeCliParsedMessages(parsed.messages, {
1629
1054
  scope: null,
1630
1055
  lastOutputAt: this.lastOutputAt,
1631
1056
  }),
1632
1057
  activeModal,
1633
- providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
1058
+ providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
1059
+ errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
1060
+ ? (parsed as any).errorMessage.trim()
1061
+ : undefined,
1062
+ errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
1063
+ ? (parsed as any).errorReason.trim()
1064
+ : undefined,
1634
1065
  ...(bufferState ? { bufferState } : {}),
1635
1066
  ...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
1636
1067
  ? { transcriptAuthority: (parsed as any).transcriptAuthority }
@@ -1642,13 +1073,13 @@ export class ProviderCliAdapter implements CliAdapter {
1642
1073
 
1643
1074
  this.parsedStatusCache = {
1644
1075
  responseBuffer: this.responseBuffer,
1645
- currentTurnScope: this.currentTurnScope,
1076
+ currentTurnScope: this.engine.currentTurnScope,
1646
1077
  recentOutputBuffer: this.recentOutputBuffer,
1647
1078
  accumulatedBuffer: this.accumulatedBuffer,
1648
1079
  accumulatedRawBufferKey,
1649
1080
  screenText: parseScreenText,
1650
- currentStatus: this.currentStatus,
1651
- activeModal: this.activeModal,
1081
+ currentStatus: this.engine.currentStatus,
1082
+ activeModal: this.engine.activeModal,
1652
1083
  cliName: this.cliName,
1653
1084
  result,
1654
1085
  };
@@ -1656,22 +1087,22 @@ export class ProviderCliAdapter implements CliAdapter {
1656
1087
  }
1657
1088
 
1658
1089
  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
1090
  const input = buildCliParseInput({
1664
1091
  accumulatedBuffer: this.accumulatedBuffer,
1665
1092
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1666
1093
  recentOutputBuffer: this.recentOutputBuffer,
1667
1094
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1095
+ workingDir: this.workingDir,
1096
+ providerSessionId: this.providerSessionId || undefined,
1097
+ historySessionId: this.providerSessionId || undefined,
1668
1098
  baseMessages: [],
1669
1099
  partialResponse: this.responseBuffer,
1670
- isWaitingForResponse: this.isWaitingForResponse,
1671
- scope: this.currentTurnScope,
1100
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1101
+ scope: this.engine.currentTurnScope,
1672
1102
  runtimeSettings: this.runtimeSettings,
1103
+ spawnAt: this.spawnAt,
1673
1104
  });
1674
- return await Promise.resolve(this.invokeCliScript(fn, {
1105
+ return await Promise.resolve(this.runner.invokeByName(scriptName, {
1675
1106
  ...input,
1676
1107
  args: args && typeof args === 'object' ? { ...args } : {},
1677
1108
  }));
@@ -1683,7 +1114,7 @@ export class ProviderCliAdapter implements CliAdapter {
1683
1114
 
1684
1115
  /** Whether this adapter has CLI scripts loaded */
1685
1116
  hasCliScripts(): boolean {
1686
- return typeof this.cliScripts?.detectStatus === 'function';
1117
+ return this.runner.hasDetectStatus();
1687
1118
  }
1688
1119
 
1689
1120
  /**
@@ -1692,24 +1123,29 @@ export class ProviderCliAdapter implements CliAdapter {
1692
1123
  */
1693
1124
  async resolveAction(data: any): Promise<void> {
1694
1125
  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
- }
1126
+ try {
1127
+ promptText = this.runner.invokeByName('resolveAction', data);
1128
+ } catch {
1129
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
1130
+ return;
1701
1131
  }
1702
1132
  if (!promptText) {
1703
- LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not supply a prompt`);
1133
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
1704
1134
  return;
1705
1135
  }
1706
1136
  await this.sendMessage(promptText);
1707
1137
  }
1708
1138
 
1139
+ async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
1140
+ // Legacy TUI providers do not currently expose an interactive prompt
1141
+ // protocol. Spec-backed claude-cli implements the first real wiring.
1142
+ }
1143
+
1709
1144
  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;
1145
+ if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
1146
+ if (this.engine.hasActionableApproval()) return false;
1147
+ // If there's already meaningful response content beyond the echoed prompt, not stuck
1148
+ if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
1713
1149
  const screenText = this.terminalScreen.getText();
1714
1150
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
1715
1151
  const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
@@ -1718,25 +1154,82 @@ export class ProviderCliAdapter implements CliAdapter {
1718
1154
  return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
1719
1155
  }
1720
1156
 
1157
+ private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
1158
+ const raw = String(this.responseBuffer || '').trim();
1159
+ if (!raw) return false;
1160
+ const normalizedPrompt = compactPromptText(promptSnippet);
1161
+ if (!normalizedPrompt) return true;
1162
+ const normalizedBuffer = compactPromptText(raw);
1163
+ if (!normalizedBuffer) return false;
1164
+ if (normalizedBuffer === normalizedPrompt) return false;
1165
+ if (normalizedBuffer.startsWith(normalizedPrompt)) {
1166
+ const remainder = normalizedBuffer
1167
+ .slice(normalizedPrompt.length)
1168
+ .replace(/[─═\-]+/g, '')
1169
+ .replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
1170
+ .replace(/accepteditson\([^)]*\)/gi, '')
1171
+ .replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
1172
+ .replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
1173
+ .replace(/esctointerrupt/gi, '')
1174
+ .replace(/❯/g, '')
1175
+ .replace(/^[\s\-–—:;,.!/?]+/, '')
1176
+ .trim();
1177
+ return remainder.length > 0;
1178
+ }
1179
+ return true;
1180
+ }
1181
+
1721
1182
  private async writeToPty(data: string): Promise<void> {
1722
1183
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1184
+ // win32 ConPTY paced write: a single unbounded write beyond ~1KB overflows
1185
+ // the console input pipe and drops LEADING bytes (the "long message gets
1186
+ // truncated, head lost / tail kept" failure). Split a large payload into
1187
+ // bounded, surrogate-safe chunks written with a short gap so the console
1188
+ // reader keeps up. Small payloads (the common case — short prompts, lone
1189
+ // submit keys) still go out in a single write.
1190
+ //
1191
+ // The submit key, when present, is the TAIL of `data` (callers pass
1192
+ // `body + sendKey` for the atomic-submit paths). Because we chunk the
1193
+ // combined string, the submit key always rides in the SAME final write as
1194
+ // the body's tail — the win32 invariant that ConPTY recognizes it as a
1195
+ // submit — and is never emitted before the whole body has been written
1196
+ // (no partial-body submit). Body-only writes (wait_for_echo strategy) have
1197
+ // their submit key sent separately by the caller afterwards, unchanged.
1198
+ if (process.platform === 'win32' && shouldChunkWin32Write(data.length)) {
1199
+ await this.writeWin32Chunked(data);
1200
+ return;
1201
+ }
1723
1202
  await this.ptyProcess.write(data);
1724
1203
  }
1725
1204
 
1205
+ /** Write `data` to the PTY in bounded, surrogate-safe chunks with a short
1206
+ * inter-chunk gap (win32 paced write). Awaits each chunk's write and the gap
1207
+ * so the returned promise resolves only after the FINAL chunk (carrying any
1208
+ * trailing submit key) has been written. */
1209
+ private async writeWin32Chunked(data: string): Promise<void> {
1210
+ const chunks = chunkPreservingSurrogates(data, WIN32_PTY_WRITE_CHUNK_CHARS);
1211
+ for (let i = 0; i < chunks.length; i += 1) {
1212
+ if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1213
+ await this.ptyProcess.write(chunks[i]);
1214
+ if (i + 1 < chunks.length) {
1215
+ await new Promise<void>(resolve => setTimeout(resolve, WIN32_PTY_WRITE_CHUNK_GAP_MS));
1216
+ }
1217
+ }
1218
+ }
1219
+
1726
1220
  private resetPendingSendState(reason: string): void {
1727
- this.isWaitingForResponse = false;
1728
1221
  this.responseBuffer = '';
1729
- this.currentTurnScope = null;
1730
- this.submitPendingUntil = 0;
1731
- this.clearIdleFinishCandidate(reason);
1732
1222
  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; }
1223
+ this.engine.resetActiveTurnState();
1224
+ this.engine.clearIdleFinishCandidate(reason);
1735
1225
  }
1736
1226
 
1737
1227
  private commitSendUserTurn(state: SendMessageState): void {
1738
1228
  if (state.didCommitUserTurn) return;
1739
1229
  state.didCommitUserTurn = true;
1230
+ // The first turn has now been written atomically (win32-safe); later turns
1231
+ // can use the normal echo-gated path now that the input layer is proven live.
1232
+ this.firstTurnSent = true;
1740
1233
  }
1741
1234
 
1742
1235
  private armResponseTimeout(): void {
@@ -1748,42 +1241,14 @@ export class ProviderCliAdapter implements CliAdapter {
1748
1241
  }
1749
1242
  this.responseTimeout = setTimeout(() => {
1750
1243
  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
- });
1244
+ if (!this.engine.isWaitingForResponse) return;
1768
1245
 
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
1246
+ // maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
1773
1247
  // normal settled parser instead and keep the turn open unless the provider
1774
1248
  // 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
- });
1249
+ this.engine.evaluateSettled(this.getSnapshot());
1250
+
1251
+ if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
1787
1252
  this.armResponseTimeout();
1788
1253
  }
1789
1254
  }, timeoutMs);
@@ -1795,37 +1260,42 @@ export class ProviderCliAdapter implements CliAdapter {
1795
1260
  });
1796
1261
  }
1797
1262
 
1263
+ // WIN32-READY-HOLD: choose the retry write for a stuck prompt. When the FIRST turn
1264
+ // is stuck on win32 — the premature-ready swallow window — the prompt text itself
1265
+ // may have been partially eaten by a not-yet-live ConPTY input layer, so re-sending
1266
+ // a bare CR keeps hitting nothing. Re-type the whole `text + sendKey` atomically
1267
+ // once so the input layer (now live) receives a self-contained, submit-coupled
1268
+ // write. All other cases keep the cheap bare-CR retry (the prompt is fully echoed
1269
+ // and only the Enter is missing).
1270
+ private writeStuckRetry(state: SendMessageState, mode: string): void {
1271
+ const retypeFirstTurn = process.platform === 'win32' && state.isFirstTurn;
1272
+ if (retypeFirstTurn) {
1273
+ LOG.info('CLI', `[${this.cliType}] ${mode}: re-typing full prompt atomically (win32 first-turn swallow recovery)`);
1274
+ void this.writeToPty(state.text + this.sendKey).catch((error) => {
1275
+ LOG.warn('CLI', `[${this.cliType}] ${mode} re-type write failed: ${error?.message || error}`);
1276
+ });
1277
+ return;
1278
+ }
1279
+ this.writeSubmitKeyForRetry(mode);
1280
+ }
1281
+
1798
1282
  private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
1799
1283
  this.submitRetryTimer = null;
1800
1284
  if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1801
- const screenText = this.terminalScreen.getText();
1802
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1285
+ this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1803
1286
  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; }
1287
+ this.writeStuckRetry(state, 'submit_retry');
1288
+ if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
1812
1289
  this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
1813
1290
  }
1814
1291
 
1815
1292
  private retryImmediateSubmitIfStuck(state: SendMessageState): void {
1816
1293
  this.submitRetryTimer = null;
1817
1294
  if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1818
- const screenText = this.terminalScreen.getText();
1819
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1295
+ this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1820
1296
  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;
1297
+ this.writeStuckRetry(state, 'immediate_retry');
1298
+ this.engine.submitRetryUsed = true;
1829
1299
  }
1830
1300
 
1831
1301
  private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
@@ -1833,13 +1303,7 @@ export class ProviderCliAdapter implements CliAdapter {
1833
1303
  completion.resolveOnce();
1834
1304
  return;
1835
1305
  }
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
- });
1306
+ this.engine.submitPendingUntil = 0;
1843
1307
  void this.writeToPty(this.sendKey).then(() => {
1844
1308
  this.commitSendUserTurn(state);
1845
1309
  this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
@@ -1849,13 +1313,7 @@ export class ProviderCliAdapter implements CliAdapter {
1849
1313
  }
1850
1314
 
1851
1315
  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
- });
1316
+ this.engine.submitPendingUntil = 0;
1859
1317
  void this.writeToPty(state.text + this.sendKey).then(() => {
1860
1318
  this.commitSendUserTurn(state);
1861
1319
  this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
@@ -1899,7 +1357,8 @@ export class ProviderCliAdapter implements CliAdapter {
1899
1357
  requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
1900
1358
  screenText: summarizeCliTraceText(screenText, 1000),
1901
1359
  };
1902
- this.recordTrace('submit_echo_missing', diagnostic);
1360
+ LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
1361
+
1903
1362
  if (this.requirePromptEchoBeforeSubmit) {
1904
1363
  // At this point the prompt text write already completed. Rejecting without
1905
1364
  // a submit key can leave the delegated CLI with an unsent prompt sitting at
@@ -1924,12 +1383,145 @@ export class ProviderCliAdapter implements CliAdapter {
1924
1383
  ), 50);
1925
1384
  }
1926
1385
 
1927
- async sendMessage(text: string): Promise<void> {
1386
+ async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
1387
+ if (options.force === true) {
1388
+ await this.forceSendMessage(text);
1389
+ return;
1390
+ }
1391
+ await this.sendMessageNow(text, true);
1392
+ }
1393
+
1394
+ async forceSendMessage(text: string): Promise<void> {
1395
+ if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1396
+ const content = String(text || '');
1397
+ if (!content.trim()) return;
1398
+ // Modal-park guard (defense-in-depth — the primary guard is at the
1399
+ // cli-provider-instance force-forward chokepoint). A force-write writes raw
1400
+ // keystrokes into the PTY, bypassing the busy send-guard. If the session is
1401
+ // parked on a tool-consent modal, the modal's key handler eats those bytes
1402
+ // and silently resolves an approval the user never made. Hold the write and
1403
+ // let the mesh reconcile loop redeliver once the modal is resolved. We only
1404
+ // hold for an actionable approval modal; plain generating is still force-written
1405
+ // (that is the deadlock the force path exists to break).
1406
+ if (this.engine.currentStatus === 'waiting_approval' || this.engine.hasActionableApproval()) {
1407
+ LOG.info('CLI', `[${this.cliType}] force-send held — session parked on approval modal (status=${this.engine.currentStatus})`);
1408
+ return;
1409
+ }
1410
+ LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
1411
+ // Settle, then submit atomically. The previous split-write fix
1412
+ // (write text → settle → write a separate '\r') regressed win32:
1413
+ // ConPTY does not treat a lone CR arriving in its own PTY chunk after a
1414
+ // delay as a submit key, so the Enter was swallowed and the prompt sat
1415
+ // typed-but-unsent. Instead, honor a fixed settle gap so the TUI input
1416
+ // handler is in its input-accepting state, then write `content + sendKey`
1417
+ // as ONE PTY write — the same atomic submit the normal `immediate`
1418
+ // strategy uses. Keeping the Enter in the same write unit as the text is
1419
+ // the invariant that makes win32 ConPTY recognize the submit, and it
1420
+ // also makes the original inject↔submit race impossible (the Enter can
1421
+ // never be separated from the text it submits). The settle still guards
1422
+ // the original concern — that a force-dispatch injected into a freshly
1423
+ // idle session lands before the TUI is ready to accept input.
1424
+ await this.waitForForceSubmitSettle();
1425
+ await this.writeToPty(content + this.sendKey);
1426
+ this.onStatusChange?.();
1427
+ }
1428
+
1429
+ private async waitForForceSubmitSettle(): Promise<void> {
1430
+ // A fixed minimum gap so the input handler is ready to accept the paste.
1431
+ // We deliberately do NOT echo-gate here: the submit key is written in the
1432
+ // same atomic PTY write as the text (forceSendMessage), so there is no
1433
+ // separate Enter that could race the echo, and on win32 the echo gate
1434
+ // never reliably matched anyway.
1435
+ await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
1436
+ }
1437
+
1438
+ private enqueuePendingOutboundMessage(text: string, reason: string): void {
1439
+ const content = String(text || '');
1440
+ const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
1441
+ if (duplicate) {
1442
+ return;
1443
+ }
1444
+ const queuedAt = Date.now();
1445
+ const message: PendingOutboundMessage = {
1446
+ id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
1447
+ role: 'user',
1448
+ content,
1449
+ queuedAt,
1450
+ source: 'sendMessage',
1451
+ };
1452
+ this.pendingOutboundQueue.push(message);
1453
+ LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
1454
+ this.onStatusChange?.();
1455
+ }
1456
+
1457
+ private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
1458
+ if (this.provider.allowInputDuringGeneration === true) return null;
1459
+ if (this.engine.hasActionableApproval()) return null;
1460
+ const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1461
+ ? String(parsedStatusBeforeSend.status)
1462
+ : '';
1463
+ const hasFinalAssistant = (p: any) => {
1464
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
1465
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
1466
+ };
1467
+ if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
1468
+ if (this.engine.currentStatus === 'generating') return 'current_status_generating';
1469
+ if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating') {
1470
+ const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
1471
+ const parsedHasActionableModal = Boolean(
1472
+ parsedModal
1473
+ && Array.isArray(parsedModal.buttons)
1474
+ && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1475
+ );
1476
+ const terminalLooksIdle = this.engine.currentStatus === 'idle'
1477
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1478
+ && !this.engine.isWaitingForResponse
1479
+ && !this.engine.currentTurnScope
1480
+ && !this.engine.hasActionableApproval()
1481
+ && !parsedHasActionableModal;
1482
+ return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
1483
+ }
1484
+ if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
1485
+ return null;
1486
+ }
1487
+
1488
+ private schedulePendingOutboundFlush(delayMs = 0): void {
1489
+ if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
1490
+ this.pendingOutboundFlushTimer = setTimeout(() => {
1491
+ this.pendingOutboundFlushTimer = null;
1492
+ void this.flushPendingOutboundQueue();
1493
+ }, Math.max(0, delayMs));
1494
+ }
1495
+
1496
+ private async flushPendingOutboundQueue(): Promise<void> {
1497
+ if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
1498
+ if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
1499
+ this.pendingOutboundFlushInFlight = true;
1500
+ try {
1501
+ while (this.pendingOutboundQueue.length > 0) {
1502
+ if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
1503
+ const next = this.pendingOutboundQueue[0];
1504
+ try {
1505
+ await this.sendMessageNow(next.content, false);
1506
+ this.pendingOutboundQueue.shift();
1507
+ this.onStatusChange?.();
1508
+ } catch (error: any) {
1509
+ LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
1510
+ this.schedulePendingOutboundFlush(1000);
1511
+ break;
1512
+ }
1513
+ }
1514
+ } finally {
1515
+ this.pendingOutboundFlushInFlight = false;
1516
+ }
1517
+ }
1518
+
1519
+ private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
1928
1520
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1929
1521
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
1930
1522
  const allowInterventionPrompt = allowInputDuringGeneration
1931
- && this.isWaitingForResponse
1932
- && !this.hasActionableApproval();
1523
+ && this.engine.isWaitingForResponse
1524
+ && !this.engine.hasActionableApproval();
1933
1525
  if (this.startupParseGate) {
1934
1526
  const deadline = Date.now() + 10000;
1935
1527
  while (this.startupParseGate && Date.now() < deadline) {
@@ -1937,76 +1529,102 @@ export class ProviderCliAdapter implements CliAdapter {
1937
1529
  await new Promise(resolve => setTimeout(resolve, 50));
1938
1530
  }
1939
1531
  }
1532
+ const parsedStatusBeforeSend = !allowInputDuringGeneration
1533
+ ? (() => {
1534
+ try {
1535
+ return this.getScriptParsedStatus?.() || null;
1536
+ } catch {
1537
+ return null;
1538
+ }
1539
+ })()
1540
+ : null;
1541
+ const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
1542
+ if (allowQueue && queueReason) {
1543
+ this.enqueuePendingOutboundMessage(text, queueReason);
1544
+ return;
1545
+ }
1940
1546
  if (!allowInterventionPrompt) {
1941
1547
  await this.waitForInteractivePrompt();
1942
1548
  }
1943
1549
  if (!this.ready) {
1944
1550
  this.resolveStartupState('send_precheck');
1945
- if (this.runDetectStatus(this.recentOutputBuffer) === 'idle' && this.currentStatus === 'idle') {
1551
+ if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
1946
1552
  this.ready = true;
1947
1553
  this.startupParseGate = false;
1554
+ this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
1948
1555
  LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
1949
1556
  }
1950
1557
  }
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;
1558
+ if (!this.ready) {
1559
+ // Readiness barrier (queue-until-ready). A task dispatched the instant a
1560
+ // freshly-spawned session is launched can arrive BEFORE the PTY prints its
1561
+ // interactive prompt (this.ready flips ~2-6s later). Previously this threw
1562
+ // "not ready" and the delegated-task delivery promise requeued the task,
1563
+ // which on win32 raced the auto-launch cooldown and could strand the worker
1564
+ // idle with no work (the "first big message lost" failure). Instead, when the
1565
+ // caller allows queueing, BUFFER the message in the pending-outbound queue and
1566
+ // return — the startup-settle path flips this.ready and flushes the queue once
1567
+ // the prompt is actually up (see resolveStartupState → flushPendingOutboundQueue),
1568
+ // so the message is delivered late rather than dropped. A non-queueable caller
1569
+ // (e.g. an internal flush) still throws so it isn't silently swallowed.
1570
+ if (allowQueue) {
1571
+ this.enqueuePendingOutboundMessage(text, 'not_ready_pending_prompt');
1572
+ return;
1573
+ }
1574
+ throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
1575
+ }
1961
1576
  const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1962
1577
  ? String(parsedStatusBeforeSend.status)
1963
1578
  : '';
1964
- if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
1579
+ if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'no_progress' || parsedSessionStatus === 'long_generating')) {
1965
1580
  const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
1966
1581
  const parsedHasActionableModal = Boolean(
1967
1582
  parsedModal
1968
1583
  && Array.isArray(parsedModal.buttons)
1969
1584
  && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1970
1585
  );
1971
- const terminalLooksIdle = this.currentStatus === 'idle'
1586
+ const terminalLooksIdle = this.engine.currentStatus === 'idle'
1972
1587
  && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1973
- && !this.isWaitingForResponse
1974
- && !this.currentTurnScope
1975
- && !this.hasActionableApproval()
1588
+ && !this.engine.isWaitingForResponse
1589
+ && !this.engine.currentTurnScope
1590
+ && !this.engine.hasActionableApproval()
1976
1591
  && !parsedHasActionableModal;
1977
1592
  if (!terminalLooksIdle) {
1593
+ if (allowQueue) {
1594
+ this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
1595
+ return;
1596
+ }
1978
1597
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1979
1598
  }
1980
1599
  }
1981
- if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1982
- if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
1600
+ if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
1601
+ const snap = this.getSnapshot();
1602
+ if (
1603
+ !this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
1604
+ && !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
1605
+ ) {
1606
+ if (allowQueue) {
1607
+ this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
1608
+ return;
1609
+ }
1983
1610
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1984
1611
  }
1985
1612
  }
1986
- this.isWaitingForResponse = true;
1987
1613
  this.responseBuffer = '';
1988
- this.finishRetryCount = 0;
1989
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1990
- this.clearIdleFinishCandidate('send_message');
1991
- this.currentTurnScope = {
1614
+ const turnScope: TurnParseScope = {
1992
1615
  prompt: text,
1993
1616
  startedAt: Date.now(),
1994
1617
  bufferStart: this.accumulatedBuffer.length,
1995
1618
  rawBufferStart: this.accumulatedRawBuffer.length,
1996
1619
  };
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);
1620
+ LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
2006
1621
  if (this.submitRetryTimer) {
2007
1622
  clearTimeout(this.submitRetryTimer);
2008
1623
  this.submitRetryTimer = null;
2009
1624
  }
1625
+ this.engine.onTurnStarted(turnScope);
1626
+ this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
1627
+ const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
2010
1628
  const estimatedLines = estimatePromptDisplayLines(text);
2011
1629
  const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
2012
1630
  const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
@@ -2018,13 +1636,10 @@ export class ProviderCliAdapter implements CliAdapter {
2018
1636
  maxEchoWaitMs,
2019
1637
  retryDelayMs,
2020
1638
  didCommitUserTurn: false,
1639
+ // Capture BEFORE the send commits — commitSendUserTurn flips firstTurnSent.
1640
+ isFirstTurn: !this.firstTurnSent,
2021
1641
  };
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;
1642
+ this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
2028
1643
  await new Promise<void>((resolve, reject) => {
2029
1644
  let resolved = false;
2030
1645
  const completion: SendMessageCompletion = {
@@ -2041,30 +1656,34 @@ export class ProviderCliAdapter implements CliAdapter {
2041
1656
  },
2042
1657
  };
2043
1658
 
2044
- if (this.submitStrategy === 'immediate') {
1659
+ // WIN32-READY-HOLD: the very first turn after startup is the one exposed to
1660
+ // the premature-ready swallow (ready released before win32 ConPTY input is
1661
+ // live). Force it through the atomic content+sendKey single write so the
1662
+ // submit CR can never be separated from the text it submits — the same path
1663
+ // the `immediate` strategy already uses. Restricted to win32 + the first
1664
+ // turn so Mac/linux echo-gated behavior and all later turns are unchanged.
1665
+ const useAtomicFirstTurn = this.submitStrategy === 'immediate'
1666
+ || (process.platform === 'win32' && sendState.isFirstTurn);
1667
+ if (useAtomicFirstTurn) {
2045
1668
  this.submitImmediatePrompt(sendState, completion);
2046
1669
  return;
2047
1670
  }
2048
1671
 
2049
1672
  if (submitDelayMs > 0) {
2050
- this.submitPendingUntil = Date.now() + submitDelayMs;
1673
+ this.engine.submitPendingUntil = Date.now() + submitDelayMs;
2051
1674
  }
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
1675
  const submitStartedAt = Date.now();
2059
1676
  void this.writeToPty(text).then(
2060
1677
  () => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
2061
1678
  completion.rejectOnce,
2062
1679
  );
2063
1680
  });
1681
+ // Schedule settle after successful send
1682
+ this.engine.scheduleSettle();
2064
1683
  }
2065
1684
 
2066
1685
  getPartialResponse(): string {
2067
- if (!this.isWaitingForResponse) return '';
1686
+ if (!this.engine.isWaitingForResponse) return '';
2068
1687
  return this.responseBuffer;
2069
1688
  }
2070
1689
 
@@ -2077,10 +1696,10 @@ export class ProviderCliAdapter implements CliAdapter {
2077
1696
  cliType: this.cliType,
2078
1697
  cliName: this.cliName,
2079
1698
  workingDir: this.workingDir,
2080
- currentStatus: this.currentStatus,
1699
+ currentStatus: this.engine.currentStatus,
2081
1700
  ready: this.ready,
2082
- isWaitingForResponse: this.isWaitingForResponse,
2083
- activeModal: this.activeModal,
1701
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1702
+ activeModal: this.engine.activeModal,
2084
1703
  parseErrorMessage: this.parseErrorMessage,
2085
1704
  messageCounts: {
2086
1705
  parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
@@ -2106,10 +1725,10 @@ export class ProviderCliAdapter implements CliAdapter {
2106
1725
  lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
2107
1726
  },
2108
1727
  parser: {
2109
- scriptNames: listCliScriptNames(this.cliScripts),
2110
- traceSessionId: this.traceSessionId,
2111
- traceSeq: this.traceSeq,
2112
- currentTurnScope: this.currentTurnScope,
1728
+ scriptNames: this.runner.getScriptNames(),
1729
+ traceSessionId: this.engine.getTraceSessionId(),
1730
+ traceSeq: this.engine.getTraceEntries().length,
1731
+ currentTurnScope: this.engine.currentTurnScope,
2113
1732
  parsedStatusCache: parsedResult
2114
1733
  ? {
2115
1734
  id: parsedResult.id,
@@ -2122,26 +1741,25 @@ export class ProviderCliAdapter implements CliAdapter {
2122
1741
  activeModal: parsedResult.activeModal,
2123
1742
  }
2124
1743
  : null,
2125
- pendingScriptStatus: this.pendingScriptStatus,
2126
- pendingScriptStatusSince: this.pendingScriptStatusSince,
1744
+ pendingScriptStatus: this.engine.pendingScriptStatus,
1745
+ pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
2127
1746
  },
2128
1747
  runtimeMetadata: this.getRuntimeMetadata(),
2129
- statusHistory: this.statusHistory.slice(-80),
2130
- traceEntries: this.traceEntries.slice(-120),
1748
+ statusHistory: this.engine.getStatusHistory().slice(-80),
1749
+ traceEntries: this.engine.getTraceEntries().slice(-120),
2131
1750
  timing: {
2132
1751
  spawnAt: this.spawnAt,
2133
1752
  startupFirstOutputAt: this.startupFirstOutputAt,
2134
- submitPendingUntil: this.submitPendingUntil,
2135
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2136
- responseEpoch: this.responseEpoch,
1753
+ submitPendingUntil: this.engine.submitPendingUntil,
1754
+ responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
1755
+ responseEpoch: this.engine.responseEpoch,
2137
1756
  resizeSuppressUntil: this.resizeSuppressUntil,
2138
- lastApprovalResolvedAt: this.lastApprovalResolvedAt,
1757
+ lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
2139
1758
  },
2140
1759
  finish: {
2141
- idleFinishCandidate: this.idleFinishCandidate,
2142
- finishRetryCount: this.finishRetryCount,
2143
- submitRetryUsed: this.submitRetryUsed,
2144
- submitRetryPromptSnippet: this.submitRetryPromptSnippet,
1760
+ finishRetryCount: this.engine.finishRetryCount,
1761
+ submitRetryUsed: this.engine.submitRetryUsed,
1762
+ submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
2145
1763
  },
2146
1764
  };
2147
1765
  }
@@ -2151,7 +1769,48 @@ export class ProviderCliAdapter implements CliAdapter {
2151
1769
  return this.ptyProcess.getMetadata();
2152
1770
  }
2153
1771
 
1772
+ /**
1773
+ * Launch metadata for the dashboard Session info panel. Re-derives the spawn plan
1774
+ * (pure — same inputs the live PTY was spawned with) so the dashboard sees the
1775
+ * resolved binary, full arg vector, and cwd without us having to persist it.
1776
+ * extraEnv values are intentionally dropped (keys only) so secrets passed at
1777
+ * launch time never reach the dashboard.
1778
+ */
1779
+ getLaunchInfo(): CliLaunchInfo {
1780
+ let command: string | undefined;
1781
+ let args: string[] = [...this.extraArgs];
1782
+ try {
1783
+ const plan = resolveCliSpawnPlan({
1784
+ provider: this.provider,
1785
+ runtimeSettings: this.runtimeSettings,
1786
+ workingDir: this.workingDir,
1787
+ extraArgs: this.extraArgs,
1788
+ extraEnv: this.extraEnv,
1789
+ });
1790
+ command = plan.binaryPath;
1791
+ args = plan.allArgs;
1792
+ } catch {
1793
+ // Spawn plan resolution can throw before the binary is resolvable
1794
+ // (e.g. CLI not installed). Fall back to the raw extra args so the
1795
+ // panel still shows what it can rather than failing the whole call.
1796
+ }
1797
+ return {
1798
+ command,
1799
+ args,
1800
+ extraArgs: [...this.extraArgs],
1801
+ cwd: this.workingDir,
1802
+ extraEnvKeys: Object.keys(this.extraEnv || {}),
1803
+ providerSessionId: this.providerSessionId || undefined,
1804
+ };
1805
+ }
1806
+
2154
1807
  updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
1808
+ const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
1809
+ ? meta.providerSessionId.trim()
1810
+ : '';
1811
+ if (nextProviderSessionId) {
1812
+ this.providerSessionId = nextProviderSessionId;
1813
+ }
2155
1814
  if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
2156
1815
  this.ptyProcess.updateMeta(meta, replace);
2157
1816
  }
@@ -2172,7 +1831,7 @@ export class ProviderCliAdapter implements CliAdapter {
2172
1831
  this.timeouts.shutdownGrace,
2173
1832
  typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
2174
1833
  );
2175
- const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
1834
+ const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
2176
1835
 
2177
1836
  try {
2178
1837
  if (wasProcessing) {
@@ -2210,7 +1869,7 @@ export class ProviderCliAdapter implements CliAdapter {
2210
1869
  return new Promise((resolve) => {
2211
1870
  const startedAt = Date.now();
2212
1871
  const timer = setInterval(() => {
2213
- if (!this.ptyProcess || this.currentStatus === 'stopped') {
1872
+ if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
2214
1873
  clearInterval(timer);
2215
1874
  resolve(true);
2216
1875
  return;
@@ -2224,18 +1883,20 @@ export class ProviderCliAdapter implements CliAdapter {
2224
1883
  }
2225
1884
 
2226
1885
  shutdown(): void {
2227
- this.clearIdleFinishCandidate('shutdown');
1886
+ this.engine.clearIdleFinishCandidate('shutdown');
2228
1887
  this.clearAllTimers();
2229
1888
  this.pendingOutputParseChunks = [];
2230
1889
  this.pendingTerminalQueryTail = '';
2231
1890
  this.ptyOutputChunks = [];
2232
- this.finishRetryCount = 0;
1891
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
1892
+ this.pendingOutboundQueue = [];
1893
+ this.pendingOutboundFlushInFlight = false;
2233
1894
  if (this.ptyProcess) {
2234
1895
  this.ptyProcess.write('\x03');
2235
1896
  setTimeout(() => {
2236
1897
  try { this.ptyProcess?.kill(); } catch { }
2237
1898
  this.ptyProcess = null;
2238
- this.setStatus('stopped', 'stop_cmd');
1899
+ this.engine.setStatus('stopped', 'stop_cmd');
2239
1900
  this.ready = false;
2240
1901
  this.startupParseGate = false;
2241
1902
  this.spawnAt = 0;
@@ -2245,12 +1906,14 @@ export class ProviderCliAdapter implements CliAdapter {
2245
1906
  }
2246
1907
 
2247
1908
  detach(): void {
2248
- this.clearIdleFinishCandidate('detach');
1909
+ this.engine.clearIdleFinishCandidate('detach');
2249
1910
  this.clearAllTimers();
2250
1911
  this.pendingOutputParseChunks = [];
2251
1912
  this.pendingTerminalQueryTail = '';
2252
1913
  this.ptyOutputChunks = [];
2253
- this.finishRetryCount = 0;
1914
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
1915
+ this.pendingOutboundQueue = [];
1916
+ this.pendingOutboundFlushInFlight = false;
2254
1917
  if (this.ptyProcess) {
2255
1918
  try {
2256
1919
  if (typeof this.ptyProcess.detach === 'function') {
@@ -2268,82 +1931,128 @@ export class ProviderCliAdapter implements CliAdapter {
2268
1931
  }
2269
1932
 
2270
1933
  clearHistory(): void {
2271
- this.clearIdleFinishCandidate('clear_history');
1934
+ this.engine.clearIdleFinishCandidate('clear_history');
2272
1935
  this.accumulatedBuffer = '';
2273
1936
  this.accumulatedRawBuffer = '';
2274
- this.currentTurnScope = null;
2275
- this.submitRetryUsed = false;
2276
- this.submitRetryPromptSnippet = '';
1937
+ this.engine.currentTurnScope = null;
1938
+ this.engine.submitRetryUsed = false;
1939
+ this.engine.submitRetryPromptSnippet = '';
2277
1940
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
2278
1941
  this.pendingOutputParseChunks = [];
2279
1942
  this.pendingTerminalQueryTail = '';
2280
1943
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2281
1944
  this.ptyOutputChunks = [];
2282
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2283
- this.finishRetryCount = 0;
1945
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
1946
+ this.pendingOutboundQueue = [];
1947
+ this.pendingOutboundFlushInFlight = false;
2284
1948
  this.resetTerminalScreen();
2285
1949
  this.ptyProcess?.clearBuffer?.();
2286
1950
  this.onStatusChange?.();
2287
1951
  }
2288
1952
 
2289
- isProcessing(): boolean { return this.isWaitingForResponse; }
1953
+ isProcessing(): boolean { return this.engine.isWaitingForResponse; }
2290
1954
  isReady(): boolean { return this.ready; }
2291
1955
 
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);
1956
+ // ─── State machine property accessors (delegate to engine) ──────────────
1957
+ // These expose engine state for external callers (tests, debug tools, etc.)
1958
+
1959
+ get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
1960
+ set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
1961
+
1962
+ get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
1963
+ set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
1964
+
1965
+ get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
1966
+ set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
1967
+
1968
+ get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
1969
+ set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
1970
+
1971
+ get responseEpoch(): number { return this.engine.responseEpoch; }
1972
+ set responseEpoch(v: number) { this.engine.responseEpoch = v; }
1973
+
1974
+ get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
1975
+ set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
1976
+
1977
+ get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
1978
+ set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
1979
+
1980
+ get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
1981
+ set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
1982
+
1983
+ get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
1984
+ set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
1985
+
1986
+ get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
1987
+ set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
1988
+
1989
+ get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
1990
+ get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
1991
+
1992
+ get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
1993
+ get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
1994
+
1995
+ get finishRetryCount(): number { return this.engine.finishRetryCount; }
1996
+ set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
1997
+
1998
+ get traceSessionId(): string { return this.engine.getTraceSessionId(); }
1999
+ get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
2000
+ get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
2001
+ get traceSeq(): number { return this.engine.getTraceEntries().length; }
2002
+
2003
+ /** Expose engine's evaluateSettled for test access */
2004
+ evaluateSettled(): void {
2005
+ LOG.debug(
2006
+ 'CLI',
2007
+ `[${this.cliType}] settled diagnostics delegated to state engine`);
2008
+ this.engine.evaluateSettled(this.getSnapshot());
2009
+ }
2010
+ /** Expose engine's scheduleSettle for test access */
2011
+ scheduleSettle(): void { this.engine.scheduleSettle(); }
2012
+ /** Expose engine's clearIdleFinishCandidate for test access */
2013
+ clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
2014
+ /** Expose engine's finishResponse for test access */
2015
+ finishResponse(): void { this.engine.finishResponse(); }
2016
+ /** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
2017
+ getSnapshot(): CliBufferSnapshot {
2018
+ const screenText = this.terminalScreen.getText() || '';
2019
+ return {
2020
+ accumulatedBuffer: this.accumulatedBuffer,
2021
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
2022
+ recentOutputBuffer: this.recentOutputBuffer,
2023
+ responseBuffer: this.responseBuffer,
2024
+ screenText,
2025
+ parseScreenText: this.getParseScreenText(screenText),
2026
+ workingDir: this.workingDir,
2027
+ providerSessionId: this.providerSessionId,
2028
+ runtimeSettings: this.runtimeSettings,
2029
+ isWaitingForResponse: this.engine.isWaitingForResponse,
2030
+ currentTurnScope: this.engine.currentTurnScope,
2031
+ lastOutputAt: this.lastOutputAt,
2032
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2033
+ lastScreenChangeAt: this.lastScreenChangeAt,
2034
+ lastScreenSnapshot: this.lastScreenSnapshot,
2035
+ };
2036
+ }
2037
+ isAlive(): boolean { return this.ptyProcess !== null; }
2038
+ flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
2039
+
2040
+ async writeRaw(data: string | Buffer): Promise<void> {
2041
+ const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
2042
+ await this.writeToPty(str);
2298
2043
  }
2299
2044
 
2300
2045
  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
- }
2046
+ this.engine.resolveModal(buttonIndex);
2047
+ }
2048
+
2049
+ getApprovalKeyForIndex(buttonIndex: number): string | undefined {
2050
+ return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
2051
+ }
2052
+
2053
+ /** Returns true if an approval was resolved within the adapter's cooldown window. */
2054
+ isApprovalRecentlyResolved(): boolean {
2055
+ return this.engine.isApprovalRecentlyResolved();
2347
2056
  }
2348
2057
 
2349
2058
  resize(cols: number, rows: number): void {
@@ -2357,7 +2066,7 @@ export class ProviderCliAdapter implements CliAdapter {
2357
2066
  }
2358
2067
 
2359
2068
  private getParsedDebugState(): Record<string, any> | null {
2360
- if (this.startupParseGate || typeof this.cliScripts?.parseSession !== 'function') return null;
2069
+ if (this.startupParseGate || !this.runner.hasParseSession()) return null;
2361
2070
  try {
2362
2071
  const parsed = this.getScriptParsedStatus();
2363
2072
  return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
@@ -2369,18 +2078,43 @@ export class ProviderCliAdapter implements CliAdapter {
2369
2078
  getDebugState(): Record<string, any> {
2370
2079
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2371
2080
  const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2372
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
2373
- const effectiveReady = this.ready || !!startupModal;
2081
+ const startupDetectedStatus = this.startupParseGate && !startupModal
2082
+ ? this.runDetectStatus(this.recentOutputBuffer || screenText)
2083
+ : null;
2084
+ const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
2374
2085
  const parsedDebugState = this.getParsedDebugState();
2375
2086
  const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
2087
+ const hasFinalAssistant = (p: any) => {
2088
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
2089
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
2090
+ };
2091
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
2092
+ if (parsedDebugState?.status === 'error') {
2093
+ effectiveStatus = 'error';
2094
+ }
2095
+ // (fix) Mirror the getStatus contract: never surface waiting_approval
2096
+ // without a concrete modal. detectStatus alone can fire while
2097
+ // parseApproval is still null, and a bare debugState waiting_approval
2098
+ // confused dashboards reading the debug payload.
2099
+ const debugEffectiveModal = startupModal || this.engine.activeModal;
2100
+ if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
2101
+ effectiveStatus = 'waiting_approval';
2102
+ }
2103
+ if (
2104
+ effectiveStatus === 'idle'
2105
+ && parsedDebugState?.status === 'generating'
2106
+ && !hasFinalAssistant(parsedDebugState)
2107
+ ) {
2108
+ effectiveStatus = 'generating';
2109
+ }
2376
2110
  return {
2377
2111
  type: this.cliType,
2378
2112
  name: this.cliName,
2379
2113
  providerResolution: this.providerResolutionMeta,
2380
2114
  status: effectiveStatus,
2381
2115
  projectedStatus: effectiveStatus,
2382
- rawStatus: this.currentStatus,
2383
- lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
2116
+ rawStatus: this.engine.currentStatus,
2117
+ lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
2384
2118
  ready: effectiveReady,
2385
2119
  startupParseGate: this.startupParseGate,
2386
2120
  spawnAt: this.spawnAt,
@@ -2394,38 +2128,47 @@ export class ProviderCliAdapter implements CliAdapter {
2394
2128
  providerSessionId: parsedDebugState.providerSessionId,
2395
2129
  transcriptAuthority: parsedDebugState.transcriptAuthority,
2396
2130
  coverage: parsedDebugState.coverage,
2131
+ errorMessage: parsedDebugState.errorMessage,
2132
+ errorReason: parsedDebugState.errorReason,
2397
2133
  activeModal: parsedDebugState.activeModal,
2398
2134
  messageCount: parsedMessages.length,
2399
2135
  } : null,
2400
2136
  screenText: screenText.slice(-4000),
2401
- currentTurnScope: this.currentTurnScope,
2137
+ currentTurnScope: this.engine.currentTurnScope,
2402
2138
  startupBuffer: this.startupBuffer.slice(-4000),
2403
2139
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
2404
- settledBuffer: this.settledBuffer.slice(-500),
2405
2140
  accumulatedBufferLength: this.accumulatedBuffer.length,
2406
2141
  accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
2407
2142
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
2408
2143
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
2409
2144
  responseBuffer: this.responseBuffer.slice(-1000),
2145
+ pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
2146
+ id: message.id,
2147
+ role: message.role,
2148
+ content: message.content,
2149
+ queuedAt: message.queuedAt,
2150
+ source: message.source,
2151
+ })),
2152
+ pendingOutboundCount: this.pendingOutboundQueue.length,
2410
2153
  lastOutputAt: this.lastOutputAt,
2411
2154
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2412
2155
  lastScreenChangeAt: this.lastScreenChangeAt,
2413
2156
  lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
2414
- isWaitingForResponse: this.isWaitingForResponse,
2415
- activeModal: startupModal || this.activeModal,
2416
- lastApprovalResolvedAt: this.lastApprovalResolvedAt,
2157
+ isWaitingForResponse: this.engine.isWaitingForResponse,
2158
+ activeModal: startupModal || this.engine.activeModal,
2159
+ lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
2417
2160
  sendDelayMs: this.sendDelayMs,
2418
2161
  sendKey: this.sendKey,
2419
2162
  submitStrategy: this.submitStrategy,
2420
2163
  requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
2421
- submitPendingUntil: this.submitPendingUntil,
2422
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2164
+ submitPendingUntil: this.engine.submitPendingUntil,
2165
+ responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
2423
2166
  resizeSuppressUntil: this.resizeSuppressUntil,
2424
2167
  hasCliScripts: this.hasCliScripts(),
2425
- scriptNames: listCliScriptNames(this.cliScripts),
2426
- traceSessionId: this.traceSessionId,
2427
- traceEntryCount: this.traceEntries.length,
2428
- statusHistory: this.statusHistory.slice(-30),
2168
+ scriptNames: this.runner.getScriptNames(),
2169
+ traceSessionId: this.engine.getTraceSessionId(),
2170
+ traceEntryCount: this.engine.getTraceEntries().length,
2171
+ statusHistory: this.engine.getStatusHistory().slice(-30),
2429
2172
  timeouts: this.timeouts,
2430
2173
  pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
2431
2174
  pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
@@ -2435,20 +2178,21 @@ export class ProviderCliAdapter implements CliAdapter {
2435
2178
 
2436
2179
  getTraceState(limit = 120): Record<string, any> {
2437
2180
  const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
2181
+ const traceEntries = this.engine.getTraceEntries();
2438
2182
  return {
2439
- sessionId: this.traceSessionId,
2183
+ sessionId: this.engine.getTraceSessionId(),
2440
2184
  providerResolution: this.providerResolutionMeta,
2441
- entryCount: this.traceEntries.length,
2442
- entries: this.traceEntries.slice(-cappedLimit),
2185
+ entryCount: traceEntries.length,
2186
+ entries: traceEntries.slice(-cappedLimit),
2443
2187
  screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
2444
2188
  recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
2445
2189
  responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
2446
2190
  status: this.projectEffectiveStatus(),
2447
2191
  projectedStatus: this.projectEffectiveStatus(),
2448
- rawStatus: this.currentStatus,
2449
- lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
2450
- activeModal: this.activeModal,
2451
- currentTurnScope: this.currentTurnScope,
2192
+ rawStatus: this.engine.currentStatus,
2193
+ lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
2194
+ activeModal: this.engine.activeModal,
2195
+ currentTurnScope: this.engine.currentTurnScope,
2452
2196
  messages: [],
2453
2197
  };
2454
2198
  }