@adhdev/daemon-core 0.9.82-rc.48 → 0.9.82-rc.481

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