@adhdev/daemon-core 0.9.82-rc.39 → 0.9.82-rc.390

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