@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.491

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 +137 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +23 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +71 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +64 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +48 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +152 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router-aggregate-status.d.ts +24 -0
  56. package/dist/commands/router-mesh-session-owner.d.ts +59 -0
  57. package/dist/commands/router-refine.d.ts +151 -0
  58. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  59. package/dist/commands/router.d.ts +203 -15
  60. package/dist/commands/upgrade-helper.d.ts +41 -1
  61. package/dist/config/chat-history.d.ts +11 -0
  62. package/dist/config/config.d.ts +19 -0
  63. package/dist/config/mesh-config.d.ts +66 -0
  64. package/dist/config/mesh-json-config.d.ts +199 -0
  65. package/dist/config/registry-resolver.d.ts +54 -0
  66. package/dist/config/repo-settings.d.ts +77 -0
  67. package/dist/config/state-store.d.ts +30 -0
  68. package/dist/daemon/dev-server.d.ts +0 -2
  69. package/dist/detection/cli-detector.d.ts +17 -0
  70. package/dist/detection/ide-detector.d.ts +13 -0
  71. package/dist/detection/win32-ide-version.d.ts +37 -0
  72. package/dist/git/change-impact-config.d.ts +159 -0
  73. package/dist/git/git-commands.d.ts +26 -2
  74. package/dist/git/git-diff.d.ts +6 -0
  75. package/dist/git/git-executor.d.ts +11 -0
  76. package/dist/git/git-status.d.ts +80 -0
  77. package/dist/git/git-types.d.ts +2 -50
  78. package/dist/git/git-worktree.d.ts +71 -1
  79. package/dist/git/index.d.ts +3 -1
  80. package/dist/index.d.ts +70 -17
  81. package/dist/index.js +53556 -19074
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +53380 -19063
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/installer.d.ts +1 -4
  86. package/dist/ipc/local-ipc-server.d.ts +91 -0
  87. package/dist/launch.d.ts +1 -1
  88. package/dist/logging/async-batch-writer.d.ts +10 -0
  89. package/dist/logging/debug-config.d.ts +16 -0
  90. package/dist/logging/log-redactor.d.ts +24 -0
  91. package/dist/logging/log-tail-reader.d.ts +81 -0
  92. package/dist/logging/logger.d.ts +1 -1
  93. package/dist/mesh/contracts.d.ts +256 -0
  94. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  95. package/dist/mesh/coordinator-registry.d.ts +59 -0
  96. package/dist/mesh/mesh-active-work.d.ts +203 -0
  97. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  98. package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
  99. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  100. package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
  101. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  102. package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
  103. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  104. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  105. package/dist/mesh/mesh-events-pending.d.ts +258 -0
  106. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  107. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  108. package/dist/mesh/mesh-events.d.ts +6 -49
  109. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  110. package/dist/mesh/mesh-host-ownership.d.ts +21 -1
  111. package/dist/mesh/mesh-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 +162 -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 +84 -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 +457 -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 +567 -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 +34 -0
  134. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  135. package/dist/mesh/mesh-work-queue.d.ts +392 -1
  136. package/dist/mesh/preview-freshness.d.ts +28 -0
  137. package/dist/mesh/refine-config.d.ts +97 -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 +426 -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 +196 -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 +30 -0
  161. package/dist/providers/provider-instance.d.ts +31 -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 +616 -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 +110 -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 +27 -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 +20 -6
  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 +1048 -1143
  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 +2788 -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 +724 -27
  252. package/src/commands/handler.ts +865 -2
  253. package/src/commands/high-family/index.ts +28 -0
  254. package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
  255. package/src/commands/high-family/mesh-events.ts +102 -0
  256. package/src/commands/high-family/mesh-status.ts +857 -0
  257. package/src/commands/high-family/types.ts +80 -0
  258. package/src/commands/low-family/coordinator-prompt.ts +125 -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 +270 -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 +1309 -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 +1794 -0
  283. package/src/commands/router-worktree-cleanup.ts +870 -0
  284. package/src/commands/router.ts +567 -4149
  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 +300 -24
  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 +822 -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 +190 -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 +510 -0
  323. package/src/mesh/coordinator-prompt.ts +627 -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 +2030 -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 +1548 -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 +41 -3
  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 +897 -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 +2498 -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 +1696 -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 +1792 -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 +2292 -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 +215 -0
  363. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  364. package/src/mesh/mesh-work-queue.ts +1489 -145
  365. package/src/mesh/preview-freshness.ts +163 -0
  366. package/src/mesh/refine-config.ts +129 -12
  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 +2771 -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 +207 -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 +93 -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 +538 -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 +861 -14
  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 +148 -55
  460. package/src/status/builders.ts +69 -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 +100 -27
  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 +26 -6
  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
@@ -0,0 +1,1282 @@
1
+ /**
2
+ * CliStateEngine — CLI provider status state machine
3
+ *
4
+ * Owns all status-transition logic, timer management, and script-driven
5
+ * evaluation. Reads buffer state from the transport and writes to PTY via
6
+ * the transport interface — adapter stays as pure I/O layer.
7
+ */
8
+
9
+ import { LOG } from '../logging/logger.js';
10
+ import {
11
+ buildCliParseInput,
12
+ normalizeCliParsedMessages,
13
+ type TurnParseScope,
14
+ } from './provider-cli-parse.js';
15
+ import {
16
+ buildCliScreenSnapshot,
17
+ compactPromptText,
18
+ normalizePromptText,
19
+ promptLikelyVisible,
20
+ type CliChatMessage,
21
+ type CliProviderModule,
22
+ type CliSessionStatus,
23
+ type CliTraceEntry,
24
+ type ParsedSession,
25
+ } from './provider-cli-shared.js';
26
+ import type { CliScriptRunner } from './cli-script-runner.js';
27
+
28
+ // ─── Types ─────────────────────────────────────────────────────────────────
29
+
30
+ export interface CliBufferSnapshot {
31
+ accumulatedBuffer: string;
32
+ accumulatedRawBuffer: string;
33
+ recentOutputBuffer: string;
34
+ responseBuffer: string;
35
+ screenText: string;
36
+ parseScreenText: string;
37
+ workingDir: string;
38
+ providerSessionId: string | null;
39
+ runtimeSettings: Record<string, any>;
40
+ isWaitingForResponse: boolean;
41
+ currentTurnScope: TurnParseScope | null;
42
+ lastOutputAt: number;
43
+ lastNonEmptyOutputAt: number;
44
+ lastScreenChangeAt: number;
45
+ lastScreenSnapshot: string;
46
+ }
47
+
48
+ /** What the engine needs from the transport layer */
49
+ export interface CliTransportAccess {
50
+ getSnapshot(): CliBufferSnapshot;
51
+ writeRaw(data: string | Buffer): void;
52
+ getApprovalKeyForIndex(buttonIndex: number): string | undefined;
53
+ flushOutboundQueue(): void;
54
+ isAlive(): boolean;
55
+ /** Optional: override script dispatch (used by tests to mock detection) */
56
+ runDetectStatus?(text: string): string | null;
57
+ /** Optional: override script dispatch (used by tests to mock approval) */
58
+ runParseApproval?(tail: string): { message: string; buttons: string[] } | null;
59
+ /** Optional: override full session parse (used by tests to mock parsing) */
60
+ runParseSession?(): ParsedSession | null;
61
+ /** Optional: provider type override — used when tests patch the adapter's cliType */
62
+ cliType?: string;
63
+ }
64
+
65
+ export interface CliStateEngineCallbacks {
66
+ onStatusChange(): void;
67
+ onApplyParsedSession(session: ParsedSession): void;
68
+ onTurnCompleted(): void;
69
+ }
70
+
71
+ interface IdleFinishCandidate {
72
+ armedAt: number;
73
+ lastOutputAt: number;
74
+ lastScreenChangeAt: number;
75
+ responseEpoch: number;
76
+ assistantLength: number;
77
+ }
78
+
79
+ interface SettledEvalContext {
80
+ now: number;
81
+ modal: { message: string; buttons: string[] } | null;
82
+ status: string;
83
+ parsedMessages: CliChatMessage[];
84
+ lastParsedAssistant: CliChatMessage | undefined;
85
+ parsedStatus: string | null;
86
+ prevStatus: string;
87
+ }
88
+
89
+ // ─── Constants ─────────────────────────────────────────────────────────────
90
+
91
+ const SCRIPT_STATUS_DEBOUNCE_MS = 3000;
92
+ const MAX_FINISH_RETRIES = 2;
93
+ const FINISH_RETRY_DELAY_MS = 300;
94
+ const MAX_TRACE_ENTRIES = 250;
95
+ const APPROVAL_EXIT_TIMEOUT_MS = 60_000;
96
+ const IDLE_CONFIRMATION_GRACE_MS = 2_000;
97
+
98
+ // ─── Engine ────────────────────────────────────────────────────────────────
99
+
100
+ export class CliStateEngine {
101
+ // ── Status ───────────────────────────────────────
102
+ currentStatus: CliSessionStatus['status'] = 'starting';
103
+ isWaitingForResponse = false;
104
+ currentTurnScope: TurnParseScope | null = null;
105
+ // ARCH-REFACTOR R1 (per-turn task identity): the mesh taskId bound to the most
106
+ // recently STARTED turn. Unlike currentTurnScope (nulled the moment the turn
107
+ // settles, before the completion event is even built), this persists past
108
+ // completion and is only overwritten when the NEXT turn starts. That window is
109
+ // exactly what the completion path needs: when a turn settles to idle, this still
110
+ // holds THAT turn's taskId (the next task's turn cannot have started yet — it is
111
+ // queued in pendingOutbound and only flushed asynchronously after idle), so the
112
+ // completion event carries the correct id instead of the racy session scalar.
113
+ currentTurnTaskId: string | null = null;
114
+ // GENERATING-BOUNDARY (R4d): wall-clock when the most recently STARTED turn began
115
+ // (set by onTurnStarted, persists past completion until the next turn starts).
116
+ // The startup-grace idle-stayed synthesis anchors its window on when the FIRST turn
117
+ // STARTED — not on when it finished — so a turn dispatched a few seconds after the
118
+ // grace collapse and then running for a non-trivial duration is still attributed to
119
+ // the startup collapse even though its COMPLETION lands past a now-anchored window.
120
+ currentTurnStartedAt = 0;
121
+ activeModal: { message: string; buttons: string[] } | null = null;
122
+
123
+ // ── Approval ─────────────────────────────────────
124
+ lastApprovalResolvedAt = 0;
125
+ lastResolvedModalMessage = '';
126
+ /**
127
+ * Monotonic counter bumped every time the FSM *enters* waiting_approval
128
+ * with a freshly captured modal (see `applyWaitingApproval`). It is the
129
+ * single discriminator between "the same approval re-observed across TUI
130
+ * paint flaps" and "a genuinely new, distinct approval".
131
+ *
132
+ * The message-equality cooldown below (`lastResolvedModalMessage`) cannot
133
+ * tell these apart on its own: claude-cli routinely presents consecutive
134
+ * approvals whose modal message text is identical (e.g. two back-to-back
135
+ * Bash-command prompts). When that second approval arrived inside
136
+ * `approvalCooldown`, the message-equality guard silently swallowed the
137
+ * key write and the approval stuck forever — fatal under auto-approval.
138
+ *
139
+ * `approvalEntrySeq` increments on every fresh entry; `lastResolvedEntrySeq`
140
+ * records which entry the cooldown belongs to. We only short-circuit the
141
+ * write when we are still resolving *that same* entry — a new entry (new
142
+ * seq) is always a real, distinct approval and must be written.
143
+ */
144
+ approvalEntrySeq = 0;
145
+ // Records which approval entry the last resolveModal() handled. Set unconditionally on
146
+ // every resolveModal (auto-approve fire, dashboard/mesh_approve, dev-cli-debug), so
147
+ // `lastResolvedEntrySeq >= approvalEntrySeq` is positive, ADHDev-side proof that the
148
+ // current/latest approval entry was actually resolved. Exposed via getStatus so the
149
+ // completion finalization gate (FALSEIDLE-a) can require resolution evidence before
150
+ // confirming a waiting_approval→idle transition. Public (read-only by convention).
151
+ lastResolvedEntrySeq = -1;
152
+ /**
153
+ * When the engine previously held a modal but the latest parse failed
154
+ * to extract one, we record the timestamp here and only drop the modal
155
+ * after the configured `approvalCooldown` to avoid flapping between
156
+ * waiting_approval and generating on every Claude TUI redraw — that
157
+ * flapping is what fed auto-approve a fresh modal signature on each
158
+ * paint and made the engine type "1" repeatedly into the prompt.
159
+ */
160
+ modalLostAt = 0;
161
+ private approvalExitTimeout: NodeJS.Timeout | null = null;
162
+
163
+ // ── Response tracking ────────────────────────────
164
+ responseEpoch = 0;
165
+ submitPendingUntil = 0;
166
+ responseSettleIgnoreUntil = 0;
167
+ submitRetryUsed = false;
168
+ submitRetryPromptSnippet = '';
169
+ finishRetryCount = 0;
170
+ providerErrorMessage: string | null = null;
171
+ providerErrorReason: string | null = null;
172
+
173
+ // ── Timers ───────────────────────────────────────
174
+ private settleTimer: NodeJS.Timeout | null = null;
175
+ private idleTimeout: NodeJS.Timeout | null = null;
176
+ private finishRetryTimer: NodeJS.Timeout | null = null;
177
+ private providerErrorRetryTimer: NodeJS.Timeout | null = null;
178
+ private providerErrorRetryKey = '';
179
+
180
+ // ── Debounce ─────────────────────────────────────
181
+ pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
182
+ pendingScriptStatusSince = 0;
183
+ private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
184
+
185
+ // ── Idle candidate ───────────────────────────────
186
+ private idleFinishCandidate: IdleFinishCandidate | null = null;
187
+
188
+ // ── Idle confirmation grace ──────────────────────
189
+ /**
190
+ * `finishResponse` produces the `generating → idle` transition that
191
+ * coordinators interpret as "task complete". Some providers (antigravity-
192
+ * cli observed in the wild) briefly paint a screen that looks like an
193
+ * idle prompt between tool result frames while still actively running,
194
+ * which fired `response_finished` and broke completion semantics.
195
+ * We defer the actual idle transition by IDLE_CONFIRMATION_GRACE_MS and
196
+ * cancel it if the scripted detection re-detects generating during that
197
+ * window — a true completion stays idle for many seconds, so a 2-second
198
+ * grace is sufficient to filter the paint blip.
199
+ */
200
+ private pendingIdleFinishTimer: NodeJS.Timeout | null = null;
201
+ private pendingIdleFinishAt = 0;
202
+
203
+ // ── Status history (debug) ───────────────────────
204
+ private statusHistory: { status: string; at: number; trigger?: string }[] = [];
205
+ private traceEntries: CliTraceEntry[] = [];
206
+ private traceSeq = 0;
207
+ private traceSessionId = '';
208
+
209
+ constructor(
210
+ private readonly provider: CliProviderModule,
211
+ private readonly runner: CliScriptRunner,
212
+ private readonly transport: CliTransportAccess,
213
+ private readonly callbacks: CliStateEngineCallbacks,
214
+ private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>,
215
+ ) {}
216
+
217
+ // ─── Public API ────────────────────────────────────────────────────────
218
+
219
+ setStatus(status: CliSessionStatus['status'], trigger?: string): void {
220
+ const prev = this.currentStatus;
221
+ if (prev === status) return;
222
+ this.currentStatus = status;
223
+ this.statusHistory.push({ status, at: Date.now(), trigger });
224
+ if (this.statusHistory.length > 50) this.statusHistory.shift();
225
+ this.recordTrace('status', { previousStatus: prev, trigger: trigger || null });
226
+ LOG.info('CLI', `[${this.provider.type}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
227
+ }
228
+
229
+ scheduleSettle(): void {
230
+ if (this.settleTimer) clearTimeout(this.settleTimer);
231
+ const epoch = this.responseEpoch;
232
+ const delay = Math.max(
233
+ this.timeouts.outputSettle,
234
+ this.submitPendingUntil > Date.now()
235
+ ? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
236
+ : 0,
237
+ );
238
+ this.settleTimer = setTimeout(() => {
239
+ this.settleTimer = null;
240
+ if (epoch !== this.responseEpoch) return;
241
+ this.evaluateSettled(this.transport.getSnapshot());
242
+ }, delay);
243
+ }
244
+
245
+ /** Called from sendMessage in transport once a turn scope is established. */
246
+ onTurnStarted(turnScope: TurnParseScope): void {
247
+ this.isWaitingForResponse = true;
248
+ this.finishRetryCount = 0;
249
+ this.clearIdleFinishCandidate('send_message');
250
+ this.currentTurnScope = turnScope;
251
+ // ARCH-REFACTOR R1: bind this turn's mesh taskId. A task-less (ad-hoc dashboard)
252
+ // turn carries no taskId → null here, which correctly clears any prior task's id
253
+ // so an ad-hoc turn's completion is never stamped with a stale taskId.
254
+ this.currentTurnTaskId = typeof turnScope.taskId === 'string' && turnScope.taskId.trim()
255
+ ? turnScope.taskId
256
+ : null;
257
+ // R4d: stamp the turn-start moment so the startup-grace idle-stayed synthesis can
258
+ // anchor its window on dispatch time rather than completion time.
259
+ this.currentTurnStartedAt = Date.now();
260
+ this.responseEpoch += 1;
261
+ }
262
+
263
+ /** Called when PTY exits */
264
+ onPtyExit(): void {
265
+ this.clearAllTimers();
266
+ this.setStatus('stopped', 'pty_exit');
267
+ }
268
+
269
+ /** Called when adapter starts up successfully */
270
+ onSpawnReady(): void {
271
+ this.setStatus('starting', 'pty_ready');
272
+ this.traceEntries = [];
273
+ this.traceSeq = 0;
274
+ this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
275
+ this.recordTrace('session_start', { providerType: this.provider.type });
276
+ }
277
+
278
+ resolveModal(buttonIndex: number): void {
279
+ const snap = this.transport.getSnapshot();
280
+ const parseApproval = typeof this.transport.runParseApproval === 'function'
281
+ ? (s: CliBufferSnapshot) => this.transport.runParseApproval!(s.recentOutputBuffer.slice(-500))
282
+ : (s: CliBufferSnapshot) => this.runParseApproval(s);
283
+ let modal = this.activeModal ?? parseApproval(snap);
284
+
285
+ if (!modal && this.runner.hasParseSession()) {
286
+ try {
287
+ const parsed = this.runParseSession(snap) as any;
288
+ const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
289
+ && parsed.activeModal.buttons.some((b: any) => typeof b === 'string' && b.trim())
290
+ ? parsed.activeModal : null;
291
+ if (parsed?.status === 'waiting_approval' && parsedModal) {
292
+ modal = parsedModal;
293
+ // No modal was held (`this.activeModal` was null above), so a
294
+ // freshly parsed approval here is a new entry by definition.
295
+ this.activeModal = parsedModal;
296
+ this.approvalEntrySeq++;
297
+ if (this.currentStatus !== 'waiting_approval') {
298
+ this.setStatus('waiting_approval', 'resolve_modal_parse');
299
+ this.callbacks.onStatusChange();
300
+ }
301
+ }
302
+ } catch { /* ignore parse failures */ }
303
+ }
304
+
305
+ if (!this.transport.isAlive()) return;
306
+ // (fix) Hard fail-safe: never write an approval key without a concrete
307
+ // modal that has buttons. The previous gate `currentStatus !== 'WA' &&
308
+ // !modal` let resolveModal proceed whenever status was still pinned to
309
+ // waiting_approval, even if parseApproval had returned null. Combined
310
+ // with auto-approve and Claude's modal flapping (status==WA but modal
311
+ // briefly null between paints) this typed "1" into the prompt over and
312
+ // over. Require a real modal with at least one button.
313
+ const buttonsValid = Array.isArray(modal?.buttons)
314
+ && modal.buttons.some((b: any) => typeof b === 'string' && b.trim());
315
+ if (!modal || !buttonsValid) return;
316
+
317
+ const currentModalMessage = typeof modal?.message === 'string' ? modal.message.trim() : '';
318
+ const inCooldown = !!this.lastApprovalResolvedAt
319
+ && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
320
+ // (fix) Suppress the duplicate key-write ONLY when this is the *same*
321
+ // approval entry re-observed across TUI paint flaps — i.e. the FSM has
322
+ // not entered waiting_approval afresh since the last resolve
323
+ // (approvalEntrySeq unchanged). A new entry (bumped seq) is always a
324
+ // distinct approval and must be written, even within the cooldown
325
+ // window and even when its message text matches the previous one.
326
+ //
327
+ // Previously this gated on message equality alone, so consecutive
328
+ // claude-cli approvals that share message text (very common) had the
329
+ // second write swallowed — the approval stuck unresolved despite
330
+ // auto-approval being on.
331
+ const sameEntryReResolve = this.approvalEntrySeq === this.lastResolvedEntrySeq;
332
+ if (inCooldown && sameEntryReResolve && currentModalMessage === this.lastResolvedModalMessage) return;
333
+
334
+ this.clearIdleFinishCandidate('resolve_modal');
335
+ this.recordTrace('resolve_modal', { buttonIndex, activeModal: modal, approvalEntrySeq: this.approvalEntrySeq });
336
+ this.activeModal = null;
337
+ this.lastApprovalResolvedAt = Date.now();
338
+ this.lastResolvedModalMessage = currentModalMessage;
339
+ this.lastResolvedEntrySeq = this.approvalEntrySeq;
340
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
341
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
342
+ this.setStatus('generating', 'approval_resolved');
343
+ this.callbacks.onStatusChange();
344
+
345
+ const approvalKey = this.transport.getApprovalKeyForIndex(buttonIndex);
346
+ if (approvalKey !== undefined) {
347
+ this.transport.writeRaw(approvalKey);
348
+ } else {
349
+ const DOWN = '\x1B[B';
350
+ const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
351
+ const clamped = buttonCount > 0
352
+ ? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
353
+ : Math.max(0, buttonIndex);
354
+ this.transport.writeRaw(DOWN.repeat(clamped) + '\r');
355
+ }
356
+ }
357
+
358
+ isApprovalRecentlyResolved(): boolean {
359
+ return !!(this.lastApprovalResolvedAt
360
+ && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown);
361
+ }
362
+
363
+ /**
364
+ * Called from sendMessage before starting a new turn.
365
+ * Clears stale idle response state when the terminal looks idle and no modal is active.
366
+ */
367
+ clearStaleIdleResponseGuard(reason: string, snap: CliBufferSnapshot): boolean {
368
+ const blockingModal = this.activeModal
369
+ ?? (typeof this.transport.runParseApproval === 'function'
370
+ ? this.transport.runParseApproval(snap.recentOutputBuffer.slice(-500))
371
+ : this.runParseApproval(snap));
372
+ const isIdle = (typeof this.transport.runDetectStatus === 'function'
373
+ ? this.transport.runDetectStatus(snap.recentOutputBuffer)
374
+ : this.runDetectStatus(snap)) === 'idle';
375
+ if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
376
+ return false;
377
+ }
378
+ this.clearAllTimers();
379
+ this.clearIdleFinishCandidate(reason);
380
+ this.isWaitingForResponse = false;
381
+ this.responseSettleIgnoreUntil = 0;
382
+ this.submitRetryUsed = false;
383
+ this.submitRetryPromptSnippet = '';
384
+ this.finishRetryCount = 0;
385
+ this.currentTurnScope = null;
386
+ this.activeModal = null;
387
+ this.recordTrace('stale_idle_response_cleared', { reason });
388
+ this.callbacks.onTurnCompleted();
389
+ return true;
390
+ }
391
+
392
+ /**
393
+ * Called from sendMessage before starting a new turn.
394
+ * Clears stale idle response state when the parsed session confirms idle with a final assistant message.
395
+ */
396
+ clearParsedIdleResponseGuard(reason: string, parsedStatus: any, snap: CliBufferSnapshot): boolean {
397
+ const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
398
+ const parsedModal = parsedStatus?.activeModal ?? parsedStatus?.modal ?? null;
399
+ const blockingModal = this.activeModal
400
+ ?? (typeof this.transport.runParseApproval === 'function'
401
+ ? this.transport.runParseApproval(snap.recentOutputBuffer.slice(-500))
402
+ : this.runParseApproval(snap));
403
+ if (
404
+ !this.isWaitingForResponse
405
+ || parsedRawStatus !== 'idle'
406
+ || !!parsedModal
407
+ || !!blockingModal
408
+ || !this.parsedStatusHasFinalAssistantMessage(parsedStatus)
409
+ ) {
410
+ return false;
411
+ }
412
+ this.clearAllTimers();
413
+ this.clearIdleFinishCandidate(reason);
414
+ this.isWaitingForResponse = false;
415
+ this.responseSettleIgnoreUntil = 0;
416
+ this.submitRetryUsed = false;
417
+ this.submitRetryPromptSnippet = '';
418
+ this.finishRetryCount = 0;
419
+ this.currentTurnScope = null;
420
+ this.activeModal = null;
421
+ this.setStatus('idle', reason);
422
+ this.recordTrace('parsed_idle_response_cleared', {
423
+ reason,
424
+ parsedStatus: parsedRawStatus,
425
+ parsedMessageCount: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : 0,
426
+ });
427
+ this.callbacks.onTurnCompleted();
428
+ return true;
429
+ }
430
+
431
+ clearAllTimers(): void {
432
+ if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
433
+ if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
434
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
435
+ if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
436
+ if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
437
+ if (this.providerErrorRetryTimer) { clearTimeout(this.providerErrorRetryTimer); this.providerErrorRetryTimer = null; }
438
+ if (this.pendingIdleFinishTimer) { clearTimeout(this.pendingIdleFinishTimer); this.pendingIdleFinishTimer = null; this.pendingIdleFinishAt = 0; }
439
+ this.providerErrorRetryKey = '';
440
+ }
441
+
442
+ resetActiveTurnState(): void {
443
+ this.clearAllTimers();
444
+ this.isWaitingForResponse = false;
445
+ this.responseSettleIgnoreUntil = 0;
446
+ this.submitRetryUsed = false;
447
+ this.submitRetryPromptSnippet = '';
448
+ this.finishRetryCount = 0;
449
+ this.currentTurnScope = null;
450
+ this.activeModal = null;
451
+ this.pendingScriptStatus = null;
452
+ this.pendingScriptStatusSince = 0;
453
+ }
454
+
455
+ clearIdleFinishCandidate(reason: string): void {
456
+ if (!this.idleFinishCandidate) return;
457
+ this.recordTrace('idle_candidate_reset', { reason, candidate: this.idleFinishCandidate });
458
+ this.idleFinishCandidate = null;
459
+ }
460
+
461
+ /**
462
+ * Poll-driven static-idle confirm (D4). A hosted CLI session (e.g. a fresh
463
+ * antigravity coordinator) whose boot banner drove the FSM into 'generating'
464
+ * can then sit at a STATIC ready prompt emitting no further PTY output. Every
465
+ * output-driven busy→idle re-eval (handleOutput/resolveStartupState/settle)
466
+ * is starved because there is no new output, and the startup-settle loop has
467
+ * hard-stopped past spawnAt+10s — so currentStatus stays frozen at generating
468
+ * and the dashboard disables Send. This is the ONE path that can release that
469
+ * wedge from the read-only status poll.
470
+ *
471
+ * Safety: this must NEVER flip a real generating turn to idle. The gate is
472
+ * done by the caller (getStatus) reusing resolveStartupState's proven
473
+ * predicates: no recent PTY output for a grace window, runDetectStatus of the
474
+ * current screen === 'idle', and no active/parsed modal. Here we add the
475
+ * final structural guard: there must be NO active turn scope. A live user
476
+ * turn always carries a currentTurnScope (set in onTurnStarted), so this only
477
+ * releases the boot-banner wedge and the post-turn static-idle case, both of
478
+ * which have already had their scope nulled. Returns true when it transitioned.
479
+ */
480
+ confirmPollStaticIdle(reason: string): boolean {
481
+ if (this.currentStatus !== 'generating') return false;
482
+ if (this.currentTurnScope || this.activeModal) return false;
483
+ this.clearAllTimers();
484
+ this.clearIdleFinishCandidate(reason);
485
+ this.isWaitingForResponse = false;
486
+ this.responseSettleIgnoreUntil = 0;
487
+ this.submitRetryUsed = false;
488
+ this.submitRetryPromptSnippet = '';
489
+ this.finishRetryCount = 0;
490
+ this.currentTurnScope = null;
491
+ this.activeModal = null;
492
+ this.setStatus('idle', reason);
493
+ this.recordTrace('poll_static_idle_confirmed', { reason });
494
+ return true;
495
+ }
496
+
497
+ hasActionableApproval(startupModal?: { message: string; buttons: string[] } | null): boolean {
498
+ return !!(startupModal ?? this.activeModal);
499
+ }
500
+
501
+ getTraceEntries(): CliTraceEntry[] { return this.traceEntries; }
502
+ getStatusHistory(): { status: string; at: number; trigger?: string }[] { return this.statusHistory; }
503
+ getTraceSessionId(): string { return this.traceSessionId; }
504
+
505
+ /** Record a trace entry from the transport layer (e.g. output events in debug mode). */
506
+ recordExternalTrace(type: string, payload: Record<string, any> = {}): void {
507
+ this.recordTrace(type, payload);
508
+ }
509
+
510
+ // ─── Script dispatch (builds inputs from snapshot) ──────────────────────
511
+
512
+ runDetectStatus(snap: CliBufferSnapshot): string | null {
513
+ const tail = snap.recentOutputBuffer.slice(-500);
514
+ return this.runner.detectStatus({
515
+ tail,
516
+ screenText: snap.screenText,
517
+ rawBuffer: snap.accumulatedRawBuffer,
518
+ isWaitingForResponse: snap.isWaitingForResponse,
519
+ screen: buildCliScreenSnapshot(snap.screenText),
520
+ tailScreen: buildCliScreenSnapshot(tail),
521
+ });
522
+ }
523
+
524
+ runParseApproval(snap: CliBufferSnapshot): { message: string; buttons: string[] } | null {
525
+ const tail = snap.recentOutputBuffer.slice(-500);
526
+ const buffer = snap.screenText || snap.accumulatedBuffer;
527
+ return this.runner.parseApproval({
528
+ buffer,
529
+ screenText: snap.screenText,
530
+ rawBuffer: snap.accumulatedRawBuffer,
531
+ tail,
532
+ screen: buildCliScreenSnapshot(snap.screenText),
533
+ bufferScreen: buildCliScreenSnapshot(buffer),
534
+ tailScreen: buildCliScreenSnapshot(tail),
535
+ });
536
+ }
537
+
538
+ runParseSession(snap: CliBufferSnapshot): ParsedSession | null {
539
+ // Allow transport to override session parsing (enables test mocking)
540
+ if (typeof this.transport.runParseSession === 'function') {
541
+ const session = this.transport.runParseSession();
542
+ if (session && typeof session === 'object') {
543
+ this.callbacks.onApplyParsedSession(session);
544
+ }
545
+ return session;
546
+ }
547
+ const tail = snap.recentOutputBuffer.slice(-500);
548
+ const input = buildCliParseInput({
549
+ accumulatedBuffer: snap.accumulatedBuffer,
550
+ accumulatedRawBuffer: snap.accumulatedRawBuffer,
551
+ recentOutputBuffer: snap.recentOutputBuffer,
552
+ terminalScreenText: snap.parseScreenText,
553
+ workingDir: snap.workingDir,
554
+ providerSessionId: snap.providerSessionId || undefined,
555
+ historySessionId: snap.providerSessionId || undefined,
556
+ baseMessages: [],
557
+ partialResponse: snap.responseBuffer,
558
+ isWaitingForResponse: snap.isWaitingForResponse,
559
+ scope: snap.currentTurnScope,
560
+ runtimeSettings: snap.runtimeSettings,
561
+ });
562
+ const session = this.runner.parseSession({
563
+ ...input,
564
+ tail,
565
+ tailScreen: buildCliScreenSnapshot(tail),
566
+ });
567
+ if (session && typeof session === 'object') {
568
+ this.callbacks.onApplyParsedSession(session);
569
+ }
570
+ return session;
571
+ }
572
+
573
+ // ─── Core evaluation loop ───────────────────────────────────────────────
574
+
575
+ evaluateSettled(snap: CliBufferSnapshot): void {
576
+ const now = Date.now();
577
+ if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
578
+ const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
579
+ if (this.settleTimer) clearTimeout(this.settleTimer);
580
+ this.settleTimer = setTimeout(() => {
581
+ this.settleTimer = null;
582
+ this.evaluateSettled(this.transport.getSnapshot());
583
+ }, delayTime);
584
+ return;
585
+ }
586
+
587
+ if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.runner.parseErrorMessage) {
588
+ const tail = snap.recentOutputBuffer;
589
+ const modal = this.runParseApproval(snap);
590
+ const lightweightStatus = this.runner.hasDetectStatus() ? this.runDetectStatus(snap) : null;
591
+ if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') return;
592
+ }
593
+
594
+ const session = this.runParseSession(snap);
595
+ if (!session) return;
596
+
597
+ const { status, messages } = session;
598
+ const modal = (session as any).activeModal ?? session.modal ?? null;
599
+ const parsedStatus = (session as any).parsedStatus ?? null;
600
+ const parsedMessages = normalizeCliParsedMessages(messages, {
601
+ scope: null,
602
+ lastOutputAt: snap.lastOutputAt,
603
+ });
604
+
605
+ if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages, snap)) return;
606
+
607
+ const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
608
+
609
+ if (
610
+ this.currentTurnScope
611
+ && !lastParsedAssistant
612
+ && !this.submitRetryUsed
613
+ && this.transport.isAlive()
614
+ && !this.hasActionableApproval()
615
+ && promptLikelyVisible(snap.screenText, normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || ''))
616
+ && !this.hasMeaningfulResponseBuffer(snap, normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || ''))
617
+ ) {
618
+ this.submitRetryUsed = true;
619
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
620
+ LOG.info('CLI', `[${this.provider.type}] Retrying submit key from settled parser (no assistant yet)`);
621
+ this.transport.writeRaw('\r');
622
+ if (this.settleTimer) clearTimeout(this.settleTimer);
623
+ this.settleTimer = setTimeout(() => {
624
+ this.settleTimer = null;
625
+ this.evaluateSettled(this.transport.getSnapshot());
626
+ }, this.timeouts.outputSettle + 150);
627
+ return;
628
+ }
629
+
630
+ if (!status) return;
631
+
632
+ const prevStatus = this.currentStatus;
633
+ const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
634
+
635
+ if (!this.applyPendingScriptStatusDebounce(ctx)) return;
636
+
637
+ const recentInteractiveActivity = this.hasRecentInteractiveActivity(snap, now);
638
+ LOG.debug(
639
+ 'CLI',
640
+ `[${this.provider.type}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify((lastParsedAssistant?.content || '').slice(0, 120)).slice(0, 160)} responseBuffer=${JSON.stringify((snap.responseBuffer || '').slice(0, 160)).slice(0, 220)} recentActivity=${recentInteractiveActivity}`
641
+ );
642
+
643
+ // recent_activity_hold protects an in-flight user turn from a false
644
+ // idle blip. It must NOT fire during startup — when the adapter has
645
+ // no currentTurnScope, there is no user turn to protect; the recent
646
+ // activity is just the CLI painting its welcome screen. Firing here
647
+ // produced the startup status flip the user reported
648
+ // (generating → idle → generating → idle within the first few seconds
649
+ // of claude-cli launch).
650
+ //
651
+ // When isWaitingForResponse=true and a currentTurnScope is alive, a
652
+ // PTY quiet period (>statusActivityHold ms without output) does NOT
653
+ // mean the response is done — claude-cli regularly pauses 2s+ between
654
+ // chunks during parsing/generation. recentInteractiveActivity going
655
+ // false during such a pause was the root cause of false-idle
656
+ // regressions. We hold generating whenever an active response turn is
657
+ // in flight (unconditionally, not just when there was recent output).
658
+ // The real completion gate is applyIdle's idleFinishCandidate +
659
+ // idleFinish timeout, which requires stable quiet AND a parsed
660
+ // assistant message.
661
+ //
662
+ // Fast-path exception: only release the hold if the parser shows a
663
+ // *current-turn* final standard assistant after the last user message,
664
+ // and it is not still streaming. Using !!lastParsedAssistant was too
665
+ // broad — it matched previous-turn assistant messages and caused
666
+ // false-idle between the first assistant text chunk and the first
667
+ // tool call (the agent outputs text, then immediately begins tool use;
668
+ // the gap between them hit this fast path and committed idle).
669
+ const hasFinalCurrentTurnAssistant = (() => {
670
+ if (parsedStatus !== 'idle') return false;
671
+ const msgs: any[] = Array.isArray(parsedMessages) ? parsedMessages : [];
672
+ let lastUserIdx = -1;
673
+ for (let i = msgs.length - 1; i >= 0; i--) {
674
+ if (msgs[i]?.role === 'user') { lastUserIdx = i; break; }
675
+ }
676
+ // No user message visible: fall back to any non-streaming standard assistant.
677
+ const searchSlice = lastUserIdx >= 0 ? msgs.slice(lastUserIdx + 1) : msgs;
678
+ return searchSlice.some((m: any) => {
679
+ if (!m || m.role !== 'assistant') return false;
680
+ if (typeof m.content !== 'string' || !m.content.trim()) return false;
681
+ const kind = typeof m.kind === 'string' && m.kind.trim() ? m.kind.trim() : 'standard';
682
+ return kind === 'standard' && m.meta?.streaming !== true;
683
+ });
684
+ })();
685
+ const shouldHoldGenerating = status === 'idle'
686
+ && this.isWaitingForResponse
687
+ && !!this.currentTurnScope
688
+ && !modal
689
+ && !hasFinalCurrentTurnAssistant;
690
+
691
+ if (shouldHoldGenerating) { this.applyHoldGenerating(ctx); return; }
692
+ if (status === 'error') {
693
+ if (this.maybeScheduleProviderErrorRetry(ctx, session, snap)) return;
694
+ this.applyError(ctx, session);
695
+ return;
696
+ }
697
+ if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
698
+ if (status === 'generating') { this.applyGenerating(ctx); return; }
699
+ if (status === 'idle') { this.applyIdle(ctx, snap, now); }
700
+ }
701
+
702
+ // ─── State transitions ──────────────────────────────────────────────────
703
+
704
+ private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
705
+ const { now, status, prevStatus } = ctx;
706
+ const shouldDebounce = prevStatus === 'idle' && !this.isWaitingForResponse
707
+ && !this.currentTurnScope && (status === 'generating' || status === 'waiting_approval');
708
+
709
+ if (!shouldDebounce) {
710
+ this.pendingScriptStatus = null;
711
+ this.pendingScriptStatusSince = 0;
712
+ if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
713
+ return true;
714
+ }
715
+
716
+ const armPending = (delayMs: number) => {
717
+ if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
718
+ this.pendingScriptStatusTimer = setTimeout(() => {
719
+ this.pendingScriptStatusTimer = null;
720
+ this.evaluateSettled(this.transport.getSnapshot());
721
+ }, delayMs);
722
+ };
723
+
724
+ if (this.pendingScriptStatus !== status) {
725
+ this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
726
+ this.pendingScriptStatusSince = now;
727
+ armPending(SCRIPT_STATUS_DEBOUNCE_MS);
728
+ return false;
729
+ }
730
+ const elapsed = now - this.pendingScriptStatusSince;
731
+ if (elapsed < SCRIPT_STATUS_DEBOUNCE_MS) { armPending(SCRIPT_STATUS_DEBOUNCE_MS - elapsed); return false; }
732
+ return true;
733
+ }
734
+
735
+ private applyHoldGenerating(ctx: SettledEvalContext): void {
736
+ this.clearIdleFinishCandidate('hold_generating_recent_activity');
737
+ this.setStatus('generating', 'recent_activity_hold');
738
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
739
+ this.idleTimeout = setTimeout(() => {
740
+ if (this.isWaitingForResponse && !this.hasActionableApproval()) {
741
+ if (this.shouldDeferIdleTimeoutFinish()) return;
742
+ this.finishResponse();
743
+ }
744
+ }, this.timeouts.generatingIdle);
745
+ this.callbacks.onStatusChange();
746
+ }
747
+
748
+ private applyWaitingApproval(ctx: SettledEvalContext): void {
749
+ const { modal } = ctx;
750
+ this.clearIdleFinishCandidate('waiting_approval');
751
+ const inCooldown = this.lastApprovalResolvedAt
752
+ && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
753
+ if (inCooldown && !modal) {
754
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
755
+ this.activeModal = null;
756
+ const reason = inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable';
757
+ if (this.isWaitingForResponse) {
758
+ this.setStatus('idle', reason);
759
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
760
+ this.idleTimeout = setTimeout(() => {
761
+ if (this.isWaitingForResponse && !this.hasActionableApproval()) {
762
+ if (this.shouldDeferIdleTimeoutFinish()) return;
763
+ this.finishResponse();
764
+ }
765
+ }, this.timeouts.generatingIdle);
766
+ } else {
767
+ this.setStatus('idle', reason);
768
+ }
769
+ this.callbacks.onStatusChange();
770
+ return;
771
+ }
772
+ // (fix) A *real* modal with valid buttons surfacing during the cooldown
773
+ // window is a genuinely new approval, not a trailing repaint of the one
774
+ // we just resolved — resolveModal cleared activeModal and flipped status
775
+ // to generating, so detectStatus only re-reports waiting_approval with a
776
+ // concrete modal when the CLI actually presents the next prompt.
777
+ // Previously this case fell through BOTH the `inCooldown && !modal`
778
+ // branch above and the `!inCooldown` branch below, so the FSM silently
779
+ // ignored the second approval — leaving it stuck unresolved under
780
+ // auto-approval. Capture it like any fresh entry (the modal-message
781
+ // cooldown still de-dupes the same approval inside resolveModal, now
782
+ // keyed on approvalEntrySeq). The `!modal` flap case stays gated by
783
+ // cooldown above; only an actionable new modal breaks through here.
784
+ if (!inCooldown || modal) {
785
+ if (!modal) {
786
+ LOG.warn('CLI', `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
787
+ // (fix) If we previously surfaced waiting_approval but the
788
+ // modal extraction is now failing, eventually drop the modal
789
+ // and fall back to generating so the dashboard doesn't show
790
+ // a "waiting" badge with no buttons. BUT defer the transition
791
+ // for a hysteresis window — Claude TUI re-renders parts of
792
+ // the approval frame multiple times per second and the
793
+ // intermediate parses occasionally lose buttons for a single
794
+ // pass. Flapping the engine status WA → generating → WA →
795
+ // generating between paints fed auto-approve a fresh modal
796
+ // signature each time, which typed the approval key
797
+ // ("1") into the prompt repeatedly. Wait for the modal to
798
+ // stay gone for `approvalCooldown` before clearing.
799
+ if (this.currentStatus === 'waiting_approval' && this.activeModal) {
800
+ const lostAt = this.modalLostAt || Date.now();
801
+ if (!this.modalLostAt) this.modalLostAt = lostAt;
802
+ if (Date.now() - lostAt >= this.timeouts.approvalCooldown) {
803
+ this.activeModal = null;
804
+ this.modalLostAt = 0;
805
+ this.setStatus('generating', 'approval_lost_modal');
806
+ this.callbacks.onStatusChange();
807
+ }
808
+ }
809
+ return;
810
+ }
811
+ this.modalLostAt = 0;
812
+ this.isWaitingForResponse = true;
813
+ this.setStatus('waiting_approval', 'script_detect');
814
+ // (fix) Don't overwrite an already-captured modal with a fresh
815
+ // re-parse on every evaluate — Claude TUI redraws option labels
816
+ // partially between paints (e.g. "Yes, and don't ask again for ..."
817
+ // is wider than the row and ships with a different trailing
818
+ // string each paint), which made the dashboard flap the modal
819
+ // signature continuously. Keep the first stable modal whose
820
+ // button count matches the latest parse; only swap when the
821
+ // shape clearly changed (different number of buttons → different
822
+ // approval).
823
+ const prev = this.activeModal;
824
+ const prevBtnCount = Array.isArray(prev?.buttons) ? prev!.buttons.length : 0;
825
+ const nextBtnCount = Array.isArray(modal.buttons) ? modal.buttons.length : 0;
826
+ if (!prev || prevBtnCount !== nextBtnCount) {
827
+ this.activeModal = modal;
828
+ // A fresh modal was captured (none held, or the button shape
829
+ // changed). This is a distinct approval entry — bump the seq so
830
+ // resolveModal's message-equality cooldown does not mistake it
831
+ // for a flap-repaint of the previously resolved approval.
832
+ this.approvalEntrySeq++;
833
+ this.callbacks.onStatusChange();
834
+ }
835
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
836
+ this.armApprovalExitTimeout();
837
+ }
838
+ }
839
+
840
+ private applyGenerating(ctx: SettledEvalContext): void {
841
+ const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
842
+ const noActiveTurn = !this.currentTurnScope;
843
+ // If the turn is already complete (isWaitingForResponse=false, no scope,
844
+ // no modal), PTY generating blips are just the CLI repainting its prompt.
845
+ // Check BEFORE cancelling the pending idle-finish timer — we don't want
846
+ // to cancel a scheduled idle transition just because the CLI blinked.
847
+ if (!this.isWaitingForResponse && noActiveTurn && !modal) return;
848
+ this.clearIdleFinishCandidate('generating');
849
+ // Cancel any pending grace-window idle transition. We have fresh
850
+ // evidence the provider is still generating; the previous
851
+ // finishResponse() was a paint blip, not a real completion.
852
+ this.cancelPendingIdleFinish('generating_signal_returned');
853
+ const snap = this.transport.getSnapshot();
854
+ const effectiveScreenText = snap.screenText || snap.accumulatedBuffer;
855
+ const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
856
+ const parsedShowsLiveProgress = parsedStatus === 'generating' && !!lastParsedAssistant;
857
+ if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveProgress) return;
858
+ if (prevStatus === 'waiting_approval') {
859
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
860
+ this.activeModal = null;
861
+ this.lastApprovalResolvedAt = Date.now();
862
+ }
863
+ if (!this.isWaitingForResponse) { this.isWaitingForResponse = true; }
864
+ this.setStatus('generating', 'script_detect');
865
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
866
+ this.idleTimeout = setTimeout(() => {
867
+ if (this.isWaitingForResponse) {
868
+ if (this.shouldDeferIdleTimeoutFinish()) return;
869
+ this.finishResponse();
870
+ }
871
+ }, this.timeouts.generatingIdle);
872
+ this.callbacks.onStatusChange();
873
+ }
874
+
875
+ private applyError(ctx: SettledEvalContext, session: ParsedSession): void {
876
+ this.clearIdleFinishCandidate('provider_error');
877
+ this.clearAllTimers();
878
+ this.isWaitingForResponse = false;
879
+ this.responseSettleIgnoreUntil = 0;
880
+ this.submitRetryUsed = false;
881
+ this.submitRetryPromptSnippet = '';
882
+ this.finishRetryCount = 0;
883
+ this.currentTurnScope = null;
884
+ this.activeModal = null;
885
+ this.providerErrorMessage = typeof session.errorMessage === 'string' && session.errorMessage.trim()
886
+ ? session.errorMessage.trim() : 'Provider reported an error';
887
+ this.providerErrorReason = typeof session.errorReason === 'string' && session.errorReason.trim()
888
+ ? session.errorReason.trim() : 'provider_error';
889
+ this.setStatus('error', this.providerErrorReason);
890
+ this.callbacks.onStatusChange();
891
+ }
892
+
893
+ private maybeScheduleProviderErrorRetry(ctx: SettledEvalContext, session: ParsedSession, snap: CliBufferSnapshot): boolean {
894
+ const retryPrompt = typeof (session as any).retryPrompt === 'string' ? String((session as any).retryPrompt).trim() : '';
895
+ const retryDelayMs = typeof (session as any).retryDelayMs === 'number' ? Number((session as any).retryDelayMs) : NaN;
896
+ if (!retryPrompt || !Number.isFinite(retryDelayMs) || retryDelayMs < 0 || !this.transport.isAlive()) return false;
897
+
898
+ const retryAttempt = typeof (session as any).retryAttempt === 'number' ? Number((session as any).retryAttempt) : 0;
899
+ const errorReason = typeof session.errorReason === 'string' && session.errorReason.trim() ? session.errorReason.trim() : 'provider_error';
900
+ const retryKey = `${errorReason}:${retryAttempt}:${retryPrompt}`;
901
+ if (this.providerErrorRetryTimer && this.providerErrorRetryKey === retryKey) return true;
902
+
903
+ if (this.providerErrorRetryTimer) clearTimeout(this.providerErrorRetryTimer);
904
+ this.providerErrorRetryKey = retryKey;
905
+ this.clearIdleFinishCandidate('provider_error_retry');
906
+ if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
907
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
908
+ this.providerErrorMessage = typeof session.errorMessage === 'string' && session.errorMessage.trim()
909
+ ? session.errorMessage.trim() : 'Provider reported an error';
910
+ this.providerErrorReason = errorReason;
911
+ this.activeModal = null;
912
+ this.responseSettleIgnoreUntil = Date.now() + retryDelayMs + this.timeouts.outputSettle + 400;
913
+ this.setStatus('generating', 'provider_error_retry_scheduled');
914
+ this.callbacks.onStatusChange();
915
+
916
+ this.providerErrorRetryTimer = setTimeout(() => {
917
+ this.providerErrorRetryTimer = null;
918
+ this.providerErrorRetryKey = '';
919
+ if (!this.transport.isAlive()) return;
920
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
921
+ this.submitRetryUsed = false;
922
+ this.transport.writeRaw(`${retryPrompt}\r`);
923
+ if (this.settleTimer) clearTimeout(this.settleTimer);
924
+ this.settleTimer = setTimeout(() => {
925
+ this.settleTimer = null;
926
+ this.evaluateSettled(this.transport.getSnapshot());
927
+ }, this.timeouts.outputSettle + 150);
928
+ }, retryDelayMs);
929
+ return true;
930
+ }
931
+
932
+ private applyIdle(ctx: SettledEvalContext, snap: CliBufferSnapshot, now: number): void {
933
+ const { modal, lastParsedAssistant, prevStatus } = ctx;
934
+ if (prevStatus === 'waiting_approval') {
935
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
936
+ this.activeModal = null;
937
+ this.lastApprovalResolvedAt = Date.now();
938
+ this.setStatus('idle', 'approval_prompt_gone_script_idle');
939
+ }
940
+ if (!this.isWaitingForResponse) {
941
+ if (prevStatus !== 'idle') {
942
+ this.clearIdleFinishCandidate('idle_without_response');
943
+ this.setStatus('idle', 'script_detect');
944
+ this.callbacks.onStatusChange();
945
+ }
946
+ return;
947
+ }
948
+ const quietForMs = snap.lastNonEmptyOutputAt ? (now - snap.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
949
+ const screenStableMs = snap.lastScreenChangeAt ? (now - snap.lastScreenChangeAt) : 0;
950
+ const hasAssistantTurn = !!lastParsedAssistant;
951
+ const assistantLength = (lastParsedAssistant as any)?.content?.length || 0;
952
+ const idleFinishConfirmMs = this.timeouts.idleFinishConfirm;
953
+ const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
954
+ const idleReady = !modal && hasAssistantTurn && quietForMs >= idleQuietThresholdMs && screenStableMs >= idleFinishConfirmMs;
955
+ const candidate = this.idleFinishCandidate;
956
+ const candidateQuiet = !!candidate && candidate.responseEpoch === this.responseEpoch
957
+ && candidate.lastOutputAt === snap.lastOutputAt
958
+ && candidate.lastScreenChangeAt === snap.lastScreenChangeAt
959
+ && assistantLength >= candidate.assistantLength
960
+ && (now - candidate.armedAt) >= idleFinishConfirmMs;
961
+
962
+ if (idleReady && candidateQuiet) {
963
+ this.clearIdleFinishCandidate('finish_response');
964
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
965
+ this.finishResponse();
966
+ return;
967
+ }
968
+
969
+ if (idleReady) {
970
+ if (!candidate) { this.armIdleFinishCandidate(snap, assistantLength); return; }
971
+ } else {
972
+ this.clearIdleFinishCandidate('idle_not_ready');
973
+ }
974
+
975
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
976
+ this.idleTimeout = setTimeout(() => {
977
+ if (this.isWaitingForResponse && !this.hasActionableApproval()) {
978
+ if (this.shouldDeferIdleTimeoutFinish()) return;
979
+ const parsed = this.runParseSession(this.transport.getSnapshot());
980
+ if (this.shouldDeferFinishForTranscript(parsed)) {
981
+ this.rescheduleTranscriptFinishCheck('transcript_idle_timeout_not_final');
982
+ return;
983
+ }
984
+ this.clearIdleFinishCandidate('idle_timeout_finish');
985
+ this.finishResponse();
986
+ }
987
+ }, this.timeouts.idleFinish);
988
+ }
989
+
990
+ finishResponse(): void {
991
+ if (this.submitPendingUntil > Date.now()) return;
992
+ if (this.responseSettleIgnoreUntil > Date.now()) return;
993
+ const snap = this.transport.getSnapshot();
994
+ const parsedBeforeFinish = this.runParseSession(snap);
995
+ if (this.shouldDeferFinishForTranscript(parsedBeforeFinish)) {
996
+ this.rescheduleTranscriptFinishCheck('transcript_finish_not_final');
997
+ return;
998
+ }
999
+ this.clearIdleFinishCandidate('finish_response_enter');
1000
+ const commitResult = this.commitCurrentTranscript(snap);
1001
+ if (this.shouldRetryFinishResponse(snap, commitResult)) {
1002
+ this.finishRetryCount += 1;
1003
+ if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
1004
+ this.finishRetryTimer = setTimeout(() => {
1005
+ this.finishRetryTimer = null;
1006
+ if (this.isWaitingForResponse && !this.hasActionableApproval()) this.finishResponse();
1007
+ }, FINISH_RETRY_DELAY_MS);
1008
+ return;
1009
+ }
1010
+ this.resetActiveTurnState();
1011
+ this.callbacks.onTurnCompleted();
1012
+ // Defer the actual `generating → idle` transition by a short grace
1013
+ // window. If applyGenerating fires again before the grace expires —
1014
+ // antigravity's tool-result paint blips do this — cancelPendingIdle
1015
+ // Finish() drops the pending transition and we stay generating.
1016
+ this.scheduleIdleFinish('response_finished');
1017
+ this.transport.flushOutboundQueue();
1018
+ }
1019
+
1020
+ private scheduleIdleFinish(reason: string): void {
1021
+ // If we are already deferring, replace the schedule so the most
1022
+ // recent finishResponse "wins". Reasons accumulate via the trigger.
1023
+ if (this.pendingIdleFinishTimer) clearTimeout(this.pendingIdleFinishTimer);
1024
+ this.pendingIdleFinishAt = Date.now() + IDLE_CONFIRMATION_GRACE_MS;
1025
+ this.pendingIdleFinishTimer = setTimeout(() => {
1026
+ this.pendingIdleFinishTimer = null;
1027
+ this.pendingIdleFinishAt = 0;
1028
+ // If a new user turn started during the grace window, we owe
1029
+ // generating semantics to that turn — do not retroactively idle.
1030
+ if (this.isWaitingForResponse) return;
1031
+ // The timer firing without a cancelPendingIdleFinish call means
1032
+ // no fresh applyGenerating happened during the grace window;
1033
+ // the previous fake-blip is over. Commit the idle.
1034
+ //
1035
+ // Note: the previous "currentStatus === 'generating' → return"
1036
+ // guard caused stuck-generating sessions — finishResponse leaves
1037
+ // currentStatus='generating' on purpose (so the dashboard keeps
1038
+ // the spinner during the 2s grace), and then the timer fired
1039
+ // without cancellation but refused to commit because the very
1040
+ // status we are about to transition out of was still set.
1041
+ // Re-checking currentStatus there meant idle was unreachable.
1042
+ this.setStatus('idle', reason);
1043
+ this.callbacks.onStatusChange();
1044
+ }, IDLE_CONFIRMATION_GRACE_MS);
1045
+ }
1046
+
1047
+ private cancelPendingIdleFinish(reason: string): void {
1048
+ if (!this.pendingIdleFinishTimer) return;
1049
+ clearTimeout(this.pendingIdleFinishTimer);
1050
+ this.pendingIdleFinishTimer = null;
1051
+ this.pendingIdleFinishAt = 0;
1052
+ this.recordTrace('idle_finish_cancelled', { trigger: reason });
1053
+ }
1054
+
1055
+ // ─── Helpers ────────────────────────────────────────────────────────────
1056
+
1057
+ private armApprovalExitTimeout(): void {
1058
+ if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
1059
+ this.approvalExitTimeout = setTimeout(() => {
1060
+ if (!this.hasActionableApproval()) return;
1061
+ const snap = this.transport.getSnapshot();
1062
+ const modal = typeof this.transport.runParseApproval === 'function'
1063
+ ? this.transport.runParseApproval(snap.recentOutputBuffer.slice(-500))
1064
+ : this.runParseApproval(snap);
1065
+ const detectStatus = typeof this.transport.runDetectStatus === 'function'
1066
+ ? this.transport.runDetectStatus(snap.recentOutputBuffer)
1067
+ : this.runDetectStatus(snap);
1068
+ const stillWaiting = detectStatus === 'waiting_approval' || !!modal;
1069
+ if (stillWaiting) {
1070
+ if (!modal) {
1071
+ LOG.warn('CLI', `[${this.provider.type}] approval timeout: no actionable modal; keeping fail-closed`);
1072
+ this.activeModal = null;
1073
+ this.callbacks.onStatusChange();
1074
+ this.armApprovalExitTimeout();
1075
+ return;
1076
+ }
1077
+ this.activeModal = modal;
1078
+ this.callbacks.onStatusChange();
1079
+ this.armApprovalExitTimeout();
1080
+ return;
1081
+ }
1082
+ LOG.warn('CLI', `[${this.provider.type}] Approval timeout — auto-clearing`);
1083
+ this.activeModal = null;
1084
+ this.lastApprovalResolvedAt = Date.now();
1085
+ this.setStatus('idle', 'approval_timeout');
1086
+ this.callbacks.onStatusChange();
1087
+ }, APPROVAL_EXIT_TIMEOUT_MS);
1088
+ }
1089
+
1090
+ private armIdleFinishCandidate(snap: CliBufferSnapshot, assistantLength: number): void {
1091
+ const now = Date.now();
1092
+ this.idleFinishCandidate = {
1093
+ armedAt: now,
1094
+ lastOutputAt: snap.lastOutputAt,
1095
+ lastScreenChangeAt: snap.lastScreenChangeAt,
1096
+ responseEpoch: this.responseEpoch,
1097
+ assistantLength,
1098
+ };
1099
+ if (this.settleTimer) clearTimeout(this.settleTimer);
1100
+ this.settleTimer = setTimeout(() => {
1101
+ this.settleTimer = null;
1102
+ this.evaluateSettled(this.transport.getSnapshot());
1103
+ }, this.timeouts.idleFinishConfirm);
1104
+ }
1105
+
1106
+ private shouldDeferIdleTimeoutFinish(): boolean {
1107
+ if (!this.isWaitingForResponse || this.hasActionableApproval()) return false;
1108
+ const snap = this.transport.getSnapshot();
1109
+ const detectFn = typeof this.transport.runDetectStatus === 'function'
1110
+ ? () => this.transport.runDetectStatus!(snap.recentOutputBuffer)
1111
+ : () => this.runDetectStatus(snap);
1112
+ const latestStatus = detectFn() || this.currentStatus;
1113
+ if (latestStatus === 'generating') {
1114
+ this.evaluateSettled(snap);
1115
+ return true;
1116
+ }
1117
+ return false;
1118
+ }
1119
+
1120
+ private hasRecentInteractiveActivity(snap: CliBufferSnapshot, now: number): boolean {
1121
+ const quietForMs = snap.lastNonEmptyOutputAt ? (now - snap.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1122
+ const screenStableMs = snap.lastScreenChangeAt ? (now - snap.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
1123
+ return quietForMs < this.timeouts.statusActivityHold || screenStableMs < this.timeouts.statusActivityHold;
1124
+ }
1125
+
1126
+ private hasMeaningfulResponseBuffer(snap: CliBufferSnapshot, normalizedPromptSnippet: string): boolean {
1127
+ const raw = String(snap.responseBuffer || '').trim();
1128
+ if (!raw) return false;
1129
+ const normalizedPrompt = compactPromptText(normalizedPromptSnippet);
1130
+ if (!normalizedPrompt) return true;
1131
+ const normalizedBuffer = compactPromptText(raw);
1132
+ if (!normalizedBuffer) return false;
1133
+ if (normalizedBuffer === normalizedPrompt) return false;
1134
+ if (normalizedBuffer.startsWith(normalizedPrompt)) {
1135
+ const remainder = normalizedBuffer.slice(normalizedPrompt.length)
1136
+ .replace(/[─═\-]+/g, '')
1137
+ .replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
1138
+ .replace(/esctointerrupt/gi, '')
1139
+ .replace(/❯/g, '')
1140
+ .replace(/^[\s\-–—:;,.!/?]+/, '')
1141
+ .trim();
1142
+ return remainder.length > 0;
1143
+ }
1144
+ return true;
1145
+ }
1146
+
1147
+ private shouldDeferFinishForTranscript(parsed: any): boolean {
1148
+ // Honor only the explicit manifest opt-in. We used to also hard-code
1149
+ // codex-cli here, but that left codex sessions wedged in `generating`
1150
+ // whenever the PTY parser or native transcript missed the final
1151
+ // assistant line — a much more common failure mode than the original
1152
+ // background-tool race this was meant to guard against. If a provider
1153
+ // really needs the gate, the manifest can set
1154
+ // `requiresFinalAssistantBeforeIdle: true`.
1155
+ const requiresFinalAssistant = !!this.provider.requiresFinalAssistantBeforeIdle;
1156
+ if (!requiresFinalAssistant) return false;
1157
+ if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
1158
+ const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
1159
+ if (parsedStatus !== 'idle') return true;
1160
+ if (parsed?.activeModal || parsed?.modal) return true;
1161
+ // For providers that own their history externally (e.g. SpecCliAdapter
1162
+ // reading native JSONL), parsed.messages only reflects PTY-visible
1163
+ // content which may include previous-turn assistant messages. Gate on
1164
+ // whether the *current turn* has a final assistant: find the last user
1165
+ // in parsed.messages, and check there is an assistant after it.
1166
+ // If no user found at all (e.g. PTY hid it), defer — the native-history
1167
+ // completion gate in cli-provider-instance will confirm via JSONL read.
1168
+ // For providers that own their history externally (e.g. SpecCliAdapter
1169
+ // reading native JSONL), parsed.messages may include previous-turn
1170
+ // assistant messages. Only an assistant that appears *after* the last
1171
+ // user message counts as evidence the current turn finished.
1172
+ const messages: any[] = Array.isArray(parsed?.messages) ? parsed.messages : [];
1173
+ let lastUserIdx = -1;
1174
+ for (let i = messages.length - 1; i >= 0; i--) {
1175
+ if (messages[i]?.role === 'user') { lastUserIdx = i; break; }
1176
+ }
1177
+ if (lastUserIdx < 0) {
1178
+ // No user message visible in PTY parser. Two sub-cases:
1179
+ // (a) messages is non-empty — some content was parsed but no user
1180
+ // line was found. Use the any-assistant fallback.
1181
+ // (b) messages is empty — the provider owns history externally
1182
+ // (e.g. SpecCliAdapter always returns []) and PTY gives us
1183
+ // nothing to judge. Do NOT defer here: deferring with empty
1184
+ // messages causes an infinite reschedule loop because
1185
+ // parsedStatusHasFinalStandardAssistantMessage also returns
1186
+ // false on empty input. Let cli-provider-instance's native-
1187
+ // history gate handle this case.
1188
+ if (messages.length === 0) return false;
1189
+ return !this.parsedStatusHasFinalStandardAssistantMessage(parsed);
1190
+ }
1191
+ // Check for a final standard assistant after the last user
1192
+ const hasCurrentTurnAssistant = messages.slice(lastUserIdx + 1).some((m: any) => {
1193
+ if (!m || m.role !== 'assistant') return false;
1194
+ if (typeof m.content !== 'string' || !m.content.trim()) return false;
1195
+ const kind = typeof m.kind === 'string' && m.kind.trim() ? m.kind.trim() : 'standard';
1196
+ return kind === 'standard' && m.meta?.streaming !== true;
1197
+ });
1198
+ return !hasCurrentTurnAssistant;
1199
+ }
1200
+
1201
+ private rescheduleTranscriptFinishCheck(reason: string): void {
1202
+ this.clearIdleFinishCandidate(reason);
1203
+ this.setStatus('generating', reason);
1204
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1205
+ this.idleTimeout = setTimeout(() => {
1206
+ if (!this.isWaitingForResponse || this.hasActionableApproval()) return;
1207
+ this.evaluateSettled(this.transport.getSnapshot());
1208
+ }, this.timeouts.idleFinishConfirm);
1209
+ this.recordTrace('transcript_finish_deferred', { reason });
1210
+ }
1211
+
1212
+ private shouldRetryFinishResponse(snap: CliBufferSnapshot, commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
1213
+ if (!this.currentTurnScope) return false;
1214
+ if (this.hasActionableApproval()) return false;
1215
+ if (this.finishRetryCount >= MAX_FINISH_RETRIES) return false;
1216
+ if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
1217
+ if (this.runDetectStatus(snap) !== 'idle') return false;
1218
+ const now = Date.now();
1219
+ const quietForMs = snap.lastNonEmptyOutputAt ? (now - snap.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1220
+ const screenStableMs = snap.lastScreenChangeAt ? (now - snap.lastScreenChangeAt) : 0;
1221
+ return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
1222
+ }
1223
+
1224
+ private commitCurrentTranscript(snap: CliBufferSnapshot): { hasAssistant: boolean; assistantContent: string } {
1225
+ const parsed = this.runParseSession(snap);
1226
+ if (parsed && Array.isArray(parsed.messages)) {
1227
+ const parsedMessages = normalizeCliParsedMessages(parsed.messages, { scope: null, lastOutputAt: snap.lastOutputAt });
1228
+ const lastAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
1229
+ if (lastAssistant) return { hasAssistant: true, assistantContent: lastAssistant.content || '' };
1230
+ }
1231
+ return { hasAssistant: false, assistantContent: '' };
1232
+ }
1233
+
1234
+ private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[], snap: CliBufferSnapshot): boolean {
1235
+ if (!this.provider.allowInputDuringGeneration) return false;
1236
+ if (!session || session.status !== 'idle' || !this.isWaitingForResponse || !this.currentTurnScope || this.activeModal || session.modal) return false;
1237
+ const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && String(m.content || '').trim());
1238
+ if (!visibleAssistant) return false;
1239
+ this.resetActiveTurnState();
1240
+ this.callbacks.onTurnCompleted();
1241
+ this.setStatus('idle', 'script_idle_commit');
1242
+ this.callbacks.onStatusChange();
1243
+ this.transport.flushOutboundQueue();
1244
+ return true;
1245
+ }
1246
+
1247
+ private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
1248
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
1249
+ const last = [...messages].reverse().find((m: any) => {
1250
+ if (!m || m.role !== 'assistant') return false;
1251
+ return typeof m.content === 'string' && m.content.trim().length > 0;
1252
+ });
1253
+ return !!last;
1254
+ }
1255
+
1256
+ private parsedStatusHasFinalStandardAssistantMessage(parsed: any): boolean {
1257
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
1258
+ const last = [...messages].reverse().find((m: any) => {
1259
+ if (!m || m.role !== 'assistant') return false;
1260
+ if (typeof m.content !== 'string' || !m.content.trim()) return false;
1261
+ const kind = typeof m.kind === 'string' && m.kind.trim() ? m.kind.trim() : 'standard';
1262
+ return kind === 'standard' && m.meta?.streaming !== true;
1263
+ });
1264
+ return !!last;
1265
+ }
1266
+
1267
+ private recordTrace(type: string, payload: Record<string, any> = {}): void {
1268
+ const entry: CliTraceEntry = {
1269
+ id: ++this.traceSeq,
1270
+ at: Date.now(),
1271
+ type,
1272
+ status: this.currentStatus,
1273
+ isWaitingForResponse: this.isWaitingForResponse,
1274
+ activeModal: this.activeModal ? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] } : null,
1275
+ payload,
1276
+ };
1277
+ this.traceEntries.push(entry);
1278
+ if (this.traceEntries.length > MAX_TRACE_ENTRIES) {
1279
+ this.traceEntries.splice(0, this.traceEntries.length - MAX_TRACE_ENTRIES);
1280
+ }
1281
+ }
1282
+ }