commandmate 0.44.1 → 0.44.3

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 (604) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +169 -169
  3. package/.next/app-path-routes-manifest.json +35 -35
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/react-loadable-manifest.json +0 -6
  7. package/.next/required-server-files.json +1 -1
  8. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/api/agents/update/route.js +1 -1
  10. package/.next/server/app/api/agents/update/route_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/agents/versions/route.js +1 -1
  12. package/.next/server/app/api/agents/versions/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/app/release-notes/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/app/update/route.js +2 -2
  15. package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/auth/login/route.js +1 -1
  18. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/capabilities/route.js +1 -1
  22. package/.next/server/app/api/capabilities/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/daily-summary/route.js +1 -1
  24. package/.next/server/app/api/daily-summary/route.js.nft.json +1 -1
  25. package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/daily-summary/status/route.js +1 -1
  27. package/.next/server/app/api/daily-summary/status/route.js.nft.json +1 -1
  28. package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/external-apps/[id]/route.js +1 -1
  31. package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/external-apps/route.js +1 -1
  33. package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/hooks/agent-event/route.js +2 -2
  37. package/.next/server/app/api/hooks/agent-event/route.js.nft.json +1 -1
  38. package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/hooks/claude-done/route.js +2 -2
  40. package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
  41. package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/hooks/permission-request/route.js +1 -1
  43. package/.next/server/app/api/hooks/permission-request/route.js.nft.json +1 -1
  44. package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/push/escalation/route.js +1 -1
  48. package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/push/subscriptions/route.js +2 -2
  50. package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/push/vapid/route.js +1 -1
  52. package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/relays/[relayId]/cancel/route.js +1 -1
  54. package/.next/server/app/api/relays/[relayId]/cancel/route.js.nft.json +1 -1
  55. package/.next/server/app/api/relays/[relayId]/cancel/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/relays/route.js +1 -1
  57. package/.next/server/app/api/relays/route.js.nft.json +1 -1
  58. package/.next/server/app/api/relays/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/remote/pair/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/repositories/[id]/route.js +1 -1
  61. package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  63. package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/repositories/clone/route.js +1 -1
  66. package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
  67. package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/repositories/restore/route.js +1 -1
  70. package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -1
  71. package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/repositories/route.js +1 -1
  73. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  74. package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/repositories/scan/route.js +1 -1
  76. package/.next/server/app/api/repositories/scan/route.js.nft.json +1 -1
  77. package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/api/repositories/sync/route.js +1 -1
  79. package/.next/server/app/api/repositories/sync/route.js.nft.json +1 -1
  80. package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
  82. package/.next/server/app/api/settings/default-agents/route.js +1 -1
  83. package/.next/server/app/api/settings/default-agents/route.js.nft.json +1 -1
  84. package/.next/server/app/api/settings/default-agents/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/settings/default-surface-mode/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/sidebar/group-order/route.js +1 -1
  87. package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
  88. package/.next/server/app/api/skills/[id]/route.js +1 -1
  89. package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
  90. package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/skills/route.js +1 -1
  94. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/templates/[id]/route.js +2 -2
  99. package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
  100. package/.next/server/app/api/templates/route.js +2 -2
  101. package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  105. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  106. package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/worktrees/[id]/capture/route.js +1 -1
  108. package/.next/server/app/api/worktrees/[id]/capture/route.js.nft.json +1 -1
  109. package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/api/worktrees/[id]/clear-composer/route.js +1 -1
  111. package/.next/server/app/api/worktrees/[id]/clear-composer/route.js.nft.json +1 -1
  112. package/.next/server/app/api/worktrees/[id]/clear-composer/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/worktrees/[id]/cli-reference/route_client-reference-manifest.js +1 -1
  114. package/.next/server/app/api/worktrees/[id]/cli-tool/route.js +1 -1
  115. package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
  116. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route.js +3 -3
  117. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route.js.nft.json +1 -1
  118. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
  119. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  120. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  121. package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/api/worktrees/[id]/direct-input/route.js +1 -1
  123. package/.next/server/app/api/worktrees/[id]/direct-input/route.js.nft.json +1 -1
  124. package/.next/server/app/api/worktrees/[id]/direct-input/route_client-reference-manifest.js +1 -1
  125. package/.next/server/app/api/worktrees/[id]/env/route_client-reference-manifest.js +1 -1
  126. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
  127. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +2 -2
  128. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -1
  129. package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
  130. package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
  131. package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
  132. package/.next/server/app/api/worktrees/[id]/git/branch/create/route.js +1 -1
  133. package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
  134. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route.js +1 -1
  135. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
  136. package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
  137. package/.next/server/app/api/worktrees/[id]/git/checkout/route.js +1 -1
  138. package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
  139. package/.next/server/app/api/worktrees/[id]/git/commit/route.js +1 -1
  140. package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
  141. package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
  142. package/.next/server/app/api/worktrees/[id]/git/fetch/route.js +1 -1
  143. package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
  144. package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
  145. package/.next/server/app/api/worktrees/[id]/git/pull/route.js +1 -1
  146. package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
  147. package/.next/server/app/api/worktrees/[id]/git/push/route.js +1 -1
  148. package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
  149. package/.next/server/app/api/worktrees/[id]/git/reset/route.js +1 -1
  150. package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
  151. package/.next/server/app/api/worktrees/[id]/git/revert/route.js +1 -1
  152. package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
  153. package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
  154. package/.next/server/app/api/worktrees/[id]/git/stage/route.js +1 -1
  155. package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
  156. package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
  157. package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
  158. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route.js +1 -1
  159. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
  160. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route.js +1 -1
  161. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
  162. package/.next/server/app/api/worktrees/[id]/git/stash/push/route.js +1 -1
  163. package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
  164. package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
  165. package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
  166. package/.next/server/app/api/worktrees/[id]/git/unstage/route.js +1 -1
  167. package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
  168. package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
  169. package/.next/server/app/api/worktrees/[id]/instances/notes/route.js +1 -1
  170. package/.next/server/app/api/worktrees/[id]/instances/notes/route_client-reference-manifest.js +1 -1
  171. package/.next/server/app/api/worktrees/[id]/instances/opencode/route.js +1 -1
  172. package/.next/server/app/api/worktrees/[id]/instances/opencode/route.js.nft.json +1 -1
  173. package/.next/server/app/api/worktrees/[id]/instances/opencode/route_client-reference-manifest.js +1 -1
  174. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  175. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  176. package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
  177. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  178. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  179. package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
  180. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
  181. package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
  182. package/.next/server/app/api/worktrees/[id]/marp-render/route.js +1 -1
  183. package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
  184. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js +1 -1
  185. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
  186. package/.next/server/app/api/worktrees/[id]/memos/route.js +1 -1
  187. package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
  188. package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
  189. package/.next/server/app/api/worktrees/[id]/opencode/diff/route.js +1 -1
  190. package/.next/server/app/api/worktrees/[id]/opencode/diff/route_client-reference-manifest.js +1 -1
  191. package/.next/server/app/api/worktrees/[id]/opencode/session/route.js +1 -2
  192. package/.next/server/app/api/worktrees/[id]/opencode/session/route.js.nft.json +1 -1
  193. package/.next/server/app/api/worktrees/[id]/opencode/session/route_client-reference-manifest.js +1 -1
  194. package/.next/server/app/api/worktrees/[id]/opencode/share/route.js +1 -2
  195. package/.next/server/app/api/worktrees/[id]/opencode/share/route.js.nft.json +1 -1
  196. package/.next/server/app/api/worktrees/[id]/opencode/share/route_client-reference-manifest.js +1 -1
  197. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  198. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  199. package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
  200. package/.next/server/app/api/worktrees/[id]/resolve-target/route_client-reference-manifest.js +1 -1
  201. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  202. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  203. package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
  204. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  205. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  206. package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
  207. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +2 -2
  208. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -1
  209. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
  210. package/.next/server/app/api/worktrees/[id]/schedules/active/route.js +2 -2
  211. package/.next/server/app/api/worktrees/[id]/schedules/active/route.js.nft.json +1 -1
  212. package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
  213. package/.next/server/app/api/worktrees/[id]/schedules/route.js +3 -3
  214. package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -1
  215. package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
  216. package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
  217. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  218. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  219. package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
  220. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route.js +3 -3
  221. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route.js.nft.json +1 -1
  222. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
  223. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
  224. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route.js +1 -1
  225. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
  226. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
  227. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
  228. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
  229. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route.js +1 -1
  230. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
  231. package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
  232. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  233. package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
  234. package/.next/server/app/api/worktrees/[id]/special-keys/route.js +1 -1
  235. package/.next/server/app/api/worktrees/[id]/special-keys/route.js.nft.json +1 -1
  236. package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
  237. package/.next/server/app/api/worktrees/[id]/tasks/route.js +1 -1
  238. package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
  239. package/.next/server/app/api/worktrees/[id]/terminal/route.js +2 -2
  240. package/.next/server/app/api/worktrees/[id]/terminal/route.js.nft.json +1 -1
  241. package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
  242. package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
  243. package/.next/server/app/api/worktrees/[id]/timers/route.js +1 -1
  244. package/.next/server/app/api/worktrees/[id]/timers/route.js.nft.json +1 -1
  245. package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
  246. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route.js +1 -1
  247. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  248. package/.next/server/app/api/worktrees/[id]/todos/route.js +1 -1
  249. package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
  250. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
  251. package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
  252. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
  253. package/.next/server/app/api/worktrees/[id]/verify/config/route.js +1 -1
  254. package/.next/server/app/api/worktrees/[id]/verify/config/route_client-reference-manifest.js +1 -1
  255. package/.next/server/app/api/worktrees/[id]/verify/route.js +1 -1
  256. package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
  257. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route.js +1 -1
  258. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route_client-reference-manifest.js +1 -1
  259. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
  260. package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
  261. package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
  262. package/.next/server/app/api/worktrees/apply-default-agents/route_client-reference-manifest.js +1 -1
  263. package/.next/server/app/api/worktrees/route.js +1 -1
  264. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  265. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  266. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  267. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  268. package/.next/server/app/more/page_client-reference-manifest.js +1 -1
  269. package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  270. package/.next/server/app/page_client-reference-manifest.js +1 -1
  271. package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
  272. package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
  273. package/.next/server/app/review/page_client-reference-manifest.js +1 -1
  274. package/.next/server/app/sessions/page.js +1 -1
  275. package/.next/server/app/sessions/page.js.nft.json +1 -1
  276. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  277. package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
  278. package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
  279. package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  280. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  281. package/.next/server/app/worktrees/[id]/page.js +11 -11
  282. package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
  283. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  284. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  285. package/.next/server/app-paths-manifest.json +35 -35
  286. package/.next/server/chunks/1191.js +2 -2
  287. package/.next/server/chunks/1210.js +1 -1
  288. package/.next/server/chunks/1559.js +2 -0
  289. package/.next/server/chunks/1789.js +1 -0
  290. package/.next/server/chunks/2119.js +1 -1
  291. package/.next/server/chunks/2404.js +1 -1
  292. package/.next/server/chunks/2445.js +1 -1
  293. package/.next/server/chunks/2460.js +1 -0
  294. package/.next/server/chunks/2710.js +1 -1
  295. package/.next/server/chunks/2908.js +1 -0
  296. package/.next/server/chunks/2980.js +1 -1
  297. package/.next/server/chunks/3241.js +1 -1
  298. package/.next/server/chunks/3480.js +1 -1
  299. package/.next/server/chunks/3718.js +1 -1
  300. package/.next/server/chunks/4207.js +2 -2
  301. package/.next/server/chunks/450.js +1 -1
  302. package/.next/server/chunks/4672.js +5 -0
  303. package/.next/server/chunks/{9353.js → 4718.js} +2 -2
  304. package/.next/server/chunks/4837.js +14 -0
  305. package/.next/server/chunks/5048.js +1 -1
  306. package/.next/server/chunks/5209.js +1 -0
  307. package/.next/server/chunks/5595.js +1 -0
  308. package/.next/server/chunks/5625.js +25 -0
  309. package/.next/server/chunks/5845.js +1 -0
  310. package/.next/server/chunks/611.js +1 -1
  311. package/.next/server/chunks/6130.js +1 -1
  312. package/.next/server/chunks/6401.js +1 -1
  313. package/.next/server/chunks/6781.js +1 -1
  314. package/.next/server/chunks/6793.js +3 -3
  315. package/.next/server/chunks/7491.js +2 -2
  316. package/.next/server/chunks/7905.js +1 -1
  317. package/.next/server/chunks/850.js +1 -0
  318. package/.next/server/chunks/8543.js +1 -1
  319. package/.next/server/chunks/8728.js +3 -3
  320. package/.next/server/chunks/8791.js +1 -1
  321. package/.next/server/chunks/8800.js +1 -1
  322. package/.next/server/chunks/8918.js +7 -7
  323. package/.next/server/chunks/9130.js +1 -5
  324. package/.next/server/chunks/9390.js +2 -3
  325. package/.next/server/chunks/9584.js +1 -1
  326. package/.next/server/chunks/9627.js +6 -6
  327. package/.next/server/chunks/9731.js +3 -3
  328. package/.next/server/middleware-manifest.json +5 -5
  329. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  330. package/.next/server/pages/500.html +1 -1
  331. package/.next/server/server-reference-manifest.json +1 -1
  332. package/.next/static/chunks/{1642-fb2e176aaadbd137.js → 1642-767aed8e7c61ef7f.js} +1 -1
  333. package/.next/static/chunks/{2407-f1f258d4a1d92620.js → 2407-06d35bb1aa02de80.js} +1 -1
  334. package/.next/static/chunks/{2619-04bc32f026a0d946.js → 2619-a305ad4810f1393a.js} +1 -1
  335. package/.next/static/chunks/4727-5d695d71e3fdf97b.js +1 -0
  336. package/.next/static/chunks/{8165-3853772f6ae1299b.js → 8165-3525fb42f1b4976e.js} +1 -1
  337. package/.next/static/chunks/app/{layout-da66300cc20244ed.js → layout-0d1750b833f5cc3e.js} +1 -1
  338. package/.next/static/chunks/app/sessions/page-2af68c63211c751f.js +1 -0
  339. package/.next/static/chunks/app/worktrees/[id]/page-e36898c3a80e7302.js +1 -0
  340. package/.next/static/chunks/app/worktrees/[id]/terminal/{page-373ea37b16d930c7.js → page-c601f3b3ece6557e.js} +1 -1
  341. package/.next/static/css/057f64a65726c439.css +1 -0
  342. package/.next/trace +13 -13
  343. package/dist/cli/commands/ask.d.ts.map +1 -1
  344. package/dist/cli/commands/ask.js +120 -119
  345. package/dist/cli/commands/attach.d.ts +94 -0
  346. package/dist/cli/commands/attach.d.ts.map +1 -1
  347. package/dist/cli/commands/attach.js +145 -7
  348. package/dist/cli/commands/auto-yes.js +1 -1
  349. package/dist/cli/commands/capture.d.ts.map +1 -1
  350. package/dist/cli/commands/capture.js +10 -13
  351. package/dist/cli/commands/command-target.d.ts +23 -0
  352. package/dist/cli/commands/command-target.d.ts.map +1 -0
  353. package/dist/cli/commands/command-target.js +25 -0
  354. package/dist/cli/commands/instances.d.ts.map +1 -1
  355. package/dist/cli/commands/instances.js +41 -2
  356. package/dist/cli/commands/message-dispatch.d.ts +36 -0
  357. package/dist/cli/commands/message-dispatch.d.ts.map +1 -0
  358. package/dist/cli/commands/message-dispatch.js +63 -0
  359. package/dist/cli/commands/reply.d.ts.map +1 -1
  360. package/dist/cli/commands/reply.js +2 -5
  361. package/dist/cli/commands/respond.d.ts.map +1 -1
  362. package/dist/cli/commands/respond.js +3 -6
  363. package/dist/cli/commands/send.d.ts.map +1 -1
  364. package/dist/cli/commands/send.js +149 -146
  365. package/dist/cli/commands/wait.d.ts.map +1 -1
  366. package/dist/cli/commands/wait.js +608 -416
  367. package/dist/cli/types/api-responses.d.ts +174 -27
  368. package/dist/cli/types/api-responses.d.ts.map +1 -1
  369. package/dist/cli/types/api-responses.js +4 -2
  370. package/dist/cli/utils/api-client.d.ts +25 -5
  371. package/dist/cli/utils/api-client.d.ts.map +1 -1
  372. package/dist/cli/utils/api-client.js +87 -55
  373. package/dist/cli/utils/server-url.d.ts +1 -0
  374. package/dist/cli/utils/server-url.d.ts.map +1 -1
  375. package/dist/cli/utils/server-url.js +7 -0
  376. package/dist/config/uat-isolation.d.ts +140 -0
  377. package/dist/config/uat-isolation.d.ts.map +1 -0
  378. package/dist/config/uat-isolation.js +246 -0
  379. package/dist/lib/global-state.d.ts +12 -0
  380. package/dist/lib/global-state.d.ts.map +1 -0
  381. package/dist/lib/global-state.js +16 -0
  382. package/dist/lib/polling/auto-yes-suppression-reason.d.ts +33 -0
  383. package/dist/lib/polling/auto-yes-suppression-reason.d.ts.map +1 -0
  384. package/dist/lib/polling/auto-yes-suppression-reason.js +2 -0
  385. package/dist/lib/session/prompt-view.d.ts +228 -0
  386. package/dist/lib/session/prompt-view.d.ts.map +1 -0
  387. package/dist/lib/session/prompt-view.js +290 -0
  388. package/dist/lib/session/status-evidence-type.d.ts +16 -0
  389. package/dist/lib/session/status-evidence-type.d.ts.map +1 -0
  390. package/dist/lib/session/status-evidence-type.js +2 -0
  391. package/dist/lib/tmux/legacy-session-alias.d.ts.map +1 -1
  392. package/dist/lib/tmux/legacy-session-alias.js +2 -1
  393. package/dist/server/locales/en/worktree.json +3 -1
  394. package/dist/server/locales/ja/worktree.json +3 -1
  395. package/dist/server/server.js +5 -3
  396. package/dist/server/src/cli/types/api-responses.js +4 -2
  397. package/dist/server/src/config/cli-tool-timing-config.js +1 -6
  398. package/dist/server/src/config/uat-isolation.js +246 -0
  399. package/dist/server/src/lib/assistant-response-saver.js +62 -3
  400. package/dist/server/src/lib/auto-yes-lifecycle.js +139 -0
  401. package/dist/server/src/lib/auto-yes-poller.js +465 -160
  402. package/dist/server/src/lib/auto-yes-state.js +2 -2
  403. package/dist/server/src/lib/cli-tools/antigravity.js +39 -80
  404. package/dist/server/src/lib/cli-tools/base.js +151 -0
  405. package/dist/server/src/lib/cli-tools/codex.js +44 -80
  406. package/dist/server/src/lib/cli-tools/command-code.js +24 -56
  407. package/dist/server/src/lib/cli-tools/copilot.js +25 -67
  408. package/dist/server/src/lib/cli-tools/direct-input.js +8 -0
  409. package/dist/server/src/lib/cli-tools/gemini.js +23 -73
  410. package/dist/server/src/lib/cli-tools/opencode-pane-width.js +53 -0
  411. package/dist/server/src/lib/cli-tools/opencode-v2.js +9 -7
  412. package/dist/server/src/lib/cli-tools/opencode.js +21 -85
  413. package/dist/server/src/lib/cli-tools/submit-verified-sender.js +127 -4
  414. package/dist/server/src/lib/cli-tools/vibe-local.js +21 -66
  415. package/dist/server/src/lib/detection/cli-patterns.js +374 -3047
  416. package/dist/server/src/lib/detection/idle-evidence-observation.js +2 -2
  417. package/dist/server/src/lib/detection/prompt-detect-multiple-choice.js +7 -32
  418. package/dist/server/src/lib/detection/prompt-detector.js +2 -35
  419. package/dist/server/src/lib/detection/selection-shape-patterns.js +69 -0
  420. package/dist/server/src/lib/detection/selection-shape.js +32 -520
  421. package/dist/server/src/lib/detection/shared/pasted-text.js +41 -0
  422. package/dist/server/src/lib/detection/shared/shell-pane-command.js +16 -0
  423. package/dist/server/src/lib/detection/shared/strip-box-drawing.js +25 -0
  424. package/dist/server/src/lib/detection/status-detector.js +1 -1
  425. package/dist/server/src/lib/detection/tools/antigravity/detect.js +12 -60
  426. package/dist/server/src/lib/detection/tools/antigravity/dialog.js +2 -15
  427. package/dist/server/src/lib/detection/tools/antigravity/live-region.js +39 -0
  428. package/dist/server/src/lib/detection/tools/antigravity/patterns.js +286 -0
  429. package/dist/server/src/lib/detection/tools/claude/detect.js +11 -17
  430. package/dist/server/src/lib/detection/tools/claude/live-region.js +26 -0
  431. package/dist/server/src/lib/detection/tools/claude/patterns.js +264 -4
  432. package/dist/server/src/lib/detection/tools/claude/prompt.js +2 -10
  433. package/dist/server/src/lib/detection/tools/codex/cli-patterns.js +1 -1
  434. package/dist/server/src/lib/detection/tools/codex/detect.js +71 -130
  435. package/dist/server/src/lib/detection/tools/codex/live-region.js +104 -0
  436. package/dist/server/src/lib/detection/tools/codex/patterns.js +855 -0
  437. package/dist/server/src/lib/detection/tools/codex/prompt.js +2 -10
  438. package/dist/server/src/lib/detection/tools/command-code/detect.js +19 -50
  439. package/dist/server/src/lib/detection/tools/command-code/dialog.js +2 -18
  440. package/dist/server/src/lib/detection/tools/command-code/live-region.js +36 -0
  441. package/dist/server/src/lib/detection/tools/command-code/patterns.js +234 -0
  442. package/dist/server/src/lib/detection/tools/command-code/selection-shape.js +461 -0
  443. package/dist/server/src/lib/detection/tools/context-above.js +20 -0
  444. package/dist/server/src/lib/detection/tools/copilot/detect.js +13 -35
  445. package/dist/server/src/lib/detection/tools/copilot/live-region.js +52 -0
  446. package/dist/server/src/lib/detection/tools/copilot/patterns.js +640 -0
  447. package/dist/server/src/lib/detection/tools/copilot/prompt.js +2 -10
  448. package/dist/server/src/lib/detection/tools/frame.js +36 -2
  449. package/dist/server/src/lib/detection/tools/gemini/patterns.js +29 -0
  450. package/dist/server/src/lib/detection/tools/live-region-specs.js +31 -0
  451. package/dist/server/src/lib/detection/tools/live-region.js +204 -0
  452. package/dist/server/src/lib/detection/tools/opencode/detect.js +11 -49
  453. package/dist/server/src/lib/detection/tools/opencode/live-region.js +24 -0
  454. package/dist/server/src/lib/detection/tools/opencode/patterns.js +508 -0
  455. package/dist/server/src/lib/detection/tools/opencode-gutter-live-region.js +72 -0
  456. package/dist/server/src/lib/detection/tools/opencode-v2/detect.js +10 -42
  457. package/dist/server/src/lib/detection/tools/opencode-v2/live-region.js +24 -0
  458. package/dist/server/src/lib/detection/tools/opencode-v2/patterns.js +63 -0
  459. package/dist/server/src/lib/detection/tools/run-detection.js +31 -50
  460. package/dist/server/src/lib/detection/tools/verdicts.js +59 -0
  461. package/dist/server/src/lib/detection/tools/vibe-local/patterns.js +20 -0
  462. package/dist/server/src/lib/detection/truncate-raw-content.js +38 -0
  463. package/dist/server/src/lib/detection/turn-abandoned.js +66 -0
  464. package/dist/server/src/lib/detection/unclassified-frame-tracker.js +2 -2
  465. package/dist/server/src/lib/global-state.js +16 -0
  466. package/dist/server/src/lib/hooks/agent-session-telemetry.js +4 -6
  467. package/dist/server/src/lib/hooks/permission-decision-state.js +2 -2
  468. package/dist/server/src/lib/hooks/sources/antigravity/history.js +11 -76
  469. package/dist/server/src/lib/hooks/sources/antigravity/hooks-config.js +63 -1
  470. package/dist/server/src/lib/hooks/sources/antigravity/source.js +16 -0
  471. package/dist/server/src/lib/hooks/sources/antigravity/transcript.js +3 -12
  472. package/dist/server/src/lib/hooks/sources/claude/history.js +17 -156
  473. package/dist/server/src/lib/hooks/sources/claude/queued-notice.js +53 -0
  474. package/dist/server/src/lib/hooks/sources/claude/source.js +33 -3
  475. package/dist/server/src/lib/hooks/sources/claude/transcript.js +3 -13
  476. package/dist/server/src/lib/hooks/sources/codex/history.js +11 -74
  477. package/dist/server/src/lib/hooks/sources/codex/hooks-config.js +147 -11
  478. package/dist/server/src/lib/hooks/sources/codex/relay-install.js +64 -21
  479. package/dist/server/src/lib/hooks/sources/codex/transcript.js +5 -15
  480. package/dist/server/src/lib/hooks/sources/command-code/history.js +20 -121
  481. package/dist/server/src/lib/hooks/sources/command-code/transcript.js +3 -13
  482. package/dist/server/src/lib/hooks/sources/copilot/hook-settings.js +161 -31
  483. package/dist/server/src/lib/hooks/sources/define-source.js +40 -55
  484. package/dist/server/src/lib/hooks/sources/event-mapper.js +2 -1
  485. package/dist/server/src/lib/hooks/sources/opencode/diff.js +3 -4
  486. package/dist/server/src/lib/hooks/sources/opencode/history.js +2 -1
  487. package/dist/server/src/lib/hooks/sources/opencode/launch-settings.js +2 -1
  488. package/dist/server/src/lib/hooks/sources/opencode/payloads.js +2 -1
  489. package/dist/server/src/lib/hooks/sources/opencode/ports.js +2 -1
  490. package/dist/server/src/lib/hooks/sources/opencode/subscription.js +4 -3
  491. package/dist/server/src/lib/hooks/sources/opencode/transcript.js +3 -16
  492. package/dist/server/src/lib/hooks/sources/opencode-v2/history.js +2 -1
  493. package/dist/server/src/lib/hooks/sources/opencode-v2/mappers.js +5 -4
  494. package/dist/server/src/lib/hooks/sources/opencode-v2/ports.js +2 -1
  495. package/dist/server/src/lib/hooks/sources/opencode-v2/subscription.js +3 -7
  496. package/dist/server/src/lib/hooks/sources/opencode-v2/usage.js +2 -1
  497. package/dist/server/src/lib/hooks/sources/pending-decisions.js +2 -1
  498. package/dist/server/src/lib/hooks/sources/registry.js +3 -2
  499. package/dist/server/src/lib/hooks/sources/transcript-history.js +262 -0
  500. package/dist/server/src/lib/hooks/sources/turn-body.js +31 -0
  501. package/dist/server/src/lib/hooks/tool-input-normalization-state.js +2 -2
  502. package/dist/server/src/lib/polling/antigravity-permission-receipts.js +2 -2
  503. package/dist/server/src/lib/polling/auto-yes-dialog-gate.js +16 -4
  504. package/dist/server/src/lib/polling/auto-yes-enter-fallback-state.js +142 -0
  505. package/dist/server/src/lib/polling/auto-yes-enter-fallback.js +187 -0
  506. package/dist/server/src/lib/polling/auto-yes-resolver.js +9 -3
  507. package/dist/server/src/lib/polling/auto-yes-suppression-reason.js +2 -0
  508. package/dist/server/src/lib/polling/auto-yes-suppression-state.js +2 -2
  509. package/dist/server/src/lib/polling/pending-scraped-response.js +190 -0
  510. package/dist/server/src/lib/polling/prompt-dedup-state.js +2 -2
  511. package/dist/server/src/lib/polling/prompt-dedup.js +2 -2
  512. package/dist/server/src/lib/polling/response-checker-dialog-gate.js +95 -0
  513. package/dist/server/src/lib/polling/response-checker-extraction-result.js +50 -0
  514. package/dist/server/src/lib/polling/response-checker-extraction-steps.js +670 -0
  515. package/dist/server/src/lib/polling/response-checker.js +666 -1162
  516. package/dist/server/src/lib/polling/response-dedup.js +3 -4
  517. package/dist/server/src/lib/polling/response-poller-constants.js +17 -0
  518. package/dist/server/src/lib/polling/response-poller-core.js +54 -20
  519. package/dist/server/src/lib/polling/startup-screen.js +142 -0
  520. package/dist/server/src/lib/polling/structured-history-gate.js +3 -2
  521. package/dist/server/src/lib/prompt-answer-semantic.js +178 -0
  522. package/dist/server/src/lib/prompt-answer-sender.js +6 -5
  523. package/dist/server/src/lib/push/failure-episode-state.js +2 -2
  524. package/dist/server/src/lib/push/notification-dedup.js +3 -4
  525. package/dist/server/src/lib/push/prompt-card-state.js +2 -2
  526. package/dist/server/src/lib/push/waiting-push-notifier.js +3 -4
  527. package/dist/server/src/lib/realtime/publisher-registry.js +3 -2
  528. package/dist/server/src/lib/realtime/terminal-broadcast.js +38 -2
  529. package/dist/server/src/lib/realtime/terminal-session-ownership.js +45 -0
  530. package/dist/server/src/lib/relay/relay-delivery.js +6 -2
  531. package/dist/server/src/lib/relay/relay-readiness.js +38 -1
  532. package/dist/server/src/lib/relay/relay-triggers.js +9 -0
  533. package/dist/server/src/lib/resource-cleanup.js +6 -4
  534. package/dist/server/src/lib/response-cleaner.js +24 -1
  535. package/dist/server/src/lib/response-extractor.js +16 -0
  536. package/dist/server/src/lib/schedule-manager.js +8 -10
  537. package/dist/server/src/lib/session/agent-event-ask-user-question.js +168 -0
  538. package/dist/server/src/lib/session/agent-event-awaiting-instruction.js +104 -0
  539. package/dist/server/src/lib/session/agent-event-dedup.js +539 -0
  540. package/dist/server/src/lib/session/agent-event-model.js +562 -0
  541. package/dist/server/src/lib/session/agent-event-record.js +10 -0
  542. package/dist/server/src/lib/session/agent-event-state.js +106 -1854
  543. package/dist/server/src/lib/session/agent-event-structured-state.js +109 -0
  544. package/dist/server/src/lib/session/agent-event-turn.js +811 -0
  545. package/dist/server/src/lib/session/agent-session-lifecycle.js +11 -2
  546. package/dist/server/src/lib/session/chat-turn-progress.js +322 -0
  547. package/dist/server/src/lib/session/claude-executor.js +45 -5
  548. package/dist/server/src/lib/session/claude-session.js +21 -65
  549. package/dist/server/src/lib/session/cli-session.js +17 -0
  550. package/dist/server/src/lib/session/current-output-builder.js +454 -1103
  551. package/dist/server/src/lib/session/current-output-history-writers.js +255 -0
  552. package/dist/server/src/lib/session/current-output-types.js +9 -0
  553. package/dist/server/src/lib/session/detection-divergence.js +55 -0
  554. package/dist/server/src/lib/session/global-session-constants.js +2 -13
  555. package/dist/server/src/lib/session/hook-turn-hold.js +102 -0
  556. package/dist/server/src/lib/session/layer-disagreement.js +2 -2
  557. package/dist/server/src/lib/session/opencode-session-store.js +2 -1
  558. package/dist/server/src/lib/session/prompt-view.js +290 -0
  559. package/dist/server/src/lib/session/prompt-waiting-composition.js +35 -0
  560. package/dist/server/src/lib/session/provisional-turn.js +3 -1
  561. package/dist/server/src/lib/session/send-user-message.js +1 -1
  562. package/dist/server/src/lib/session/session-starting-state.js +4 -6
  563. package/dist/server/src/lib/session/status-evidence-type.js +2 -0
  564. package/dist/server/src/lib/session/status-evidence.js +2 -2
  565. package/dist/server/src/lib/session/structured-status-merge.js +335 -0
  566. package/dist/server/src/lib/session/waiting-episode-state.js +3 -4
  567. package/dist/server/src/lib/session/waiting-kind.js +3 -0
  568. package/dist/server/src/lib/session-cleanup.js +6 -8
  569. package/dist/server/src/lib/skills/install-plan.js +2 -1
  570. package/dist/server/src/lib/skills/snapshot-store.js +2 -2
  571. package/dist/server/src/lib/skills/uninstall-plan.js +2 -2
  572. package/dist/server/src/lib/timer-manager.js +6 -8
  573. package/dist/server/src/lib/tmux/geometry-delegation.js +3 -4
  574. package/dist/server/src/lib/tmux/legacy-session-alias.js +2 -1
  575. package/dist/server/src/lib/tmux/session-hooks.js +2 -1
  576. package/dist/server/src/lib/tmux/session-status-options.js +3 -3
  577. package/dist/server/src/lib/tmux/tmux-capture-cache.js +3 -2
  578. package/dist/server/src/lib/tmux/tmux.js +33 -59
  579. package/dist/server/src/lib/tui-accumulator.js +2 -2
  580. package/dist/server/src/lib/version-checker.js +6 -6
  581. package/dist/server/src/lib/ws-server.js +47 -11
  582. package/docs/en/user-guide/webapp-guide.md +27 -3
  583. package/package.json +1 -1
  584. package/release-notes/0.44.2.json +33 -0
  585. package/release-notes/0.44.3.json +82 -0
  586. package/scripts/hooks/cmate-agent-event.sh +18 -0
  587. package/.next/server/chunks/1068.js +0 -1
  588. package/.next/server/chunks/434.js +0 -1
  589. package/.next/server/chunks/5775.js +0 -1
  590. package/.next/server/chunks/6474.js +0 -14
  591. package/.next/server/chunks/650.js +0 -1
  592. package/.next/server/chunks/6661.js +0 -1
  593. package/.next/server/chunks/8583.js +0 -1
  594. package/.next/server/chunks/964.js +0 -1
  595. package/.next/server/chunks/967.js +0 -1
  596. package/.next/server/chunks/9882.js +0 -25
  597. package/.next/static/chunks/591-308072f285c34787.js +0 -1
  598. package/.next/static/chunks/app/sessions/page-049e06335187aa91.js +0 -1
  599. package/.next/static/chunks/app/worktrees/[id]/page-040c5f0f585e6812.js +0 -1
  600. package/.next/static/css/38070d879efa1371.css +0 -1
  601. package/dist/server/src/lib/cli-tools/index.js +0 -41
  602. package/dist/server/src/lib/polling/global-session-poller.js +0 -140
  603. /package/.next/static/{57lOiz8zSEHdw97XIm0af → tWwHQzmvc-rv9KKUtDWfQ}/_buildManifest.js +0 -0
  604. /package/.next/static/{57lOiz8zSEHdw97XIm0af → tWwHQzmvc-rv9KKUtDWfQ}/_ssgManifest.js +0 -0
@@ -1,16 +1,10 @@
1
1
  "use strict";
2
2
  /** Response checking and extraction logic for CLI tool polling (Issue #575 split from response-poller.ts). */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.PENDING_SCRAPE_HOLD_MS = void 0;
5
- exports.incompleteResult = incompleteResult;
6
- exports.buildPromptExtractionResult = buildPromptExtractionResult;
4
+ exports.isNumberedDialogVouched = exports.buildPromptExtractionResult = exports.incompleteResult = exports.flushPendingScrapedResponse = exports.hasPendingScrapedResponse = exports.resetPendingScrapedResponses = exports.PENDING_SCRAPE_HOLD_MS = void 0;
7
5
  exports.detectPromptOnCleanFrame = detectPromptOnCleanFrame;
8
- exports.isNumberedDialogVouched = isNumberedDialogVouched;
9
6
  exports.detectPromptWithOptions = detectPromptWithOptions;
10
7
  exports.extractResponse = extractResponse;
11
- exports.resetPendingScrapedResponses = resetPendingScrapedResponses;
12
- exports.hasPendingScrapedResponse = hasPendingScrapedResponse;
13
- exports.flushPendingScrapedResponse = flushPendingScrapedResponse;
14
8
  exports.checkForResponse = checkForResponse;
15
9
  const cli_session_1 = require("../../lib/session/cli-session");
16
10
  const db_instance_1 = require("../../lib/db/db-instance");
@@ -18,8 +12,8 @@ const db_1 = require("../../lib/db");
18
12
  const ws_server_1 = require("../../lib/ws-server");
19
13
  const prompt_detector_1 = require("../../lib/detection/prompt-detector");
20
14
  const dialog_1 = require("../../lib/detection/tools/antigravity/dialog");
21
- const detect_1 = require("../../lib/detection/tools/antigravity/detect");
22
15
  const frame_1 = require("../../lib/detection/tools/frame");
16
+ const live_region_1 = require("../../lib/detection/tools/live-region");
23
17
  const dialog_2 = require("../../lib/detection/tools/command-code/dialog");
24
18
  const conversation_logger_1 = require("../../lib/conversation-logger");
25
19
  const types_1 = require("../../lib/cli-tools/types");
@@ -35,15 +29,18 @@ const response_cleaner_1 = require("../response-cleaner");
35
29
  const copilot_constants_1 = require("../../config/copilot-constants");
36
30
  const tui_accumulator_1 = require("../tui-accumulator");
37
31
  const prompt_dedup_1 = require("./prompt-dedup");
38
- const antigravity_permission_receipts_1 = require("./antigravity-permission-receipts");
39
- // Issue #2457: the same rollout table, `hasDialogRules` cross-check and
40
- // `detectDialog` seam Auto-Yes reads, reached through the presence helper rather
41
- // than through `evaluateAutoYesDialogGate` — see `isNumberedDialogVouched`.
42
- const auto_yes_dialog_gate_1 = require("./auto-yes-dialog-gate");
43
32
  const prompt_dedup_state_1 = require("./prompt-dedup-state");
44
33
  const response_dedup_1 = require("./response-dedup");
45
34
  const structured_history_gate_1 = require("./structured-history-gate");
46
- const stop_history_capture_1 = require("../../lib/hooks/stop-history-capture");
35
+ const pending_scraped_response_1 = require("./pending-scraped-response");
36
+ // Issue #3213: the held scrape (#2436) lives in `./pending-scraped-response`.
37
+ // Re-exported under the names it had here, so no import site — and no suite
38
+ // that replaces this module by path — has to change.
39
+ var pending_scraped_response_2 = require("./pending-scraped-response");
40
+ Object.defineProperty(exports, "PENDING_SCRAPE_HOLD_MS", { enumerable: true, get: function () { return pending_scraped_response_2.PENDING_SCRAPE_HOLD_MS; } });
41
+ Object.defineProperty(exports, "resetPendingScrapedResponses", { enumerable: true, get: function () { return pending_scraped_response_2.resetPendingScrapedResponses; } });
42
+ Object.defineProperty(exports, "hasPendingScrapedResponse", { enumerable: true, get: function () { return pending_scraped_response_2.hasPendingScrapedResponse; } });
43
+ Object.defineProperty(exports, "flushPendingScrapedResponse", { enumerable: true, get: function () { return pending_scraped_response_2.flushPendingScrapedResponse; } });
47
44
  const relay_triggers_1 = require("../../lib/relay/relay-triggers");
48
45
  // Issue #2317 Phase D: while a human holds the pane's geometry, the frame is
49
46
  // their terminal (44 rows), not the 1000-row canvas every rule below was
@@ -68,16 +65,6 @@ const upstream_faults_1 = require("../../lib/detection/upstream-faults");
68
65
  const waiting_push_notifier_1 = require("../../lib/push/waiting-push-notifier");
69
66
  const waiting_episode_state_1 = require("../../lib/session/waiting-episode-state");
70
67
  const task_transition_service_1 = require("../../lib/tasks/task-transition-service");
71
- /**
72
- * How many rows from the bottom of a copilot capture may hold its status bar.
73
- *
74
- * `readCopilotStatusBar` stops at the first non-blank row from the end, and the
75
- * capture handed to it here has already had its trailing blanks trimmed, so one
76
- * row is enough in practice. The slack exists so the reader keeps working if that
77
- * trim ever changes, without mapping a thousand rows through `stripAnsi` on every
78
- * poll tick. (Issue #1897)
79
- */
80
- const COPILOT_STATUS_BAR_SCAN_ROWS = 8;
81
68
  /**
82
69
  * How many rows from the bottom of a capture the upstream-fault check reads
83
70
  * (Issue #2000).
@@ -156,46 +143,22 @@ function broadcastPromptSweptToAnswered(worktreeId) {
156
143
  // the notification path owns. Idempotent by replacement, and it starts no timer
157
144
  // and touches no database until a wait actually opens.
158
145
  (0, waiting_push_notifier_1.startWaitingPushNotifier)();
159
- /**
160
- * Creates an incomplete extraction result with empty response.
161
- * Centralizes the repeated pattern of returning an in-progress/incomplete state.
162
- *
163
- * @param lineCount - Current line count for state tracking
164
- * @returns ExtractionResult with empty response and isComplete: false
165
- */
166
- function incompleteResult(lineCount) {
167
- return { response: '', isComplete: false, lineCount };
168
- }
169
- /**
170
- * Build a complete ExtractionResult for a detected prompt.
171
- *
172
- * Shared between Claude early prompt detection (section 3-4, site 1) and
173
- * fallback prompt detection (section 3-4, site 2) in extractResponse().
174
- * Applies resolveExtractionStartIndex() to limit extraction to lastCapturedLine
175
- * onwards, then strips ANSI codes for safe DB storage (Stage 4 MF-001).
176
- *
177
- * @param lines - The trimmed tmux buffer lines array
178
- * @param lastCapturedLine - Number of lines previously captured
179
- * @param totalLines - Total line count in the buffer
180
- * @param bufferReset - External buffer reset flag
181
- * @param cliToolId - CLI tool identifier
182
- * @param findRecentUserPromptIndex - Callback to locate the most recent user prompt
183
- * @param promptDetection - Prompt detection result to carry on the extraction result
184
- * @param captureWindowSaturated - True when the capture was clipped by the capture window (#1670)
185
- * @returns ExtractionResult with isComplete: true and ANSI-stripped response
186
- */
187
- function buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection, captureWindowSaturated = false) {
188
- const startIndex = (0, response_extractor_1.resolveExtractionStartIndex)(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, captureWindowSaturated);
189
- const extractedLines = lines.slice(startIndex);
190
- return {
191
- response: (0, cli_patterns_1.stripAnsi)(extractedLines.join('\n')),
192
- isComplete: true,
193
- lineCount: totalLines,
194
- promptDetection,
195
- bufferReset,
196
- captureWindowSaturated,
197
- };
198
- }
146
+ // Issue #3374: the extraction result, the two dialog gates and the steps split
147
+ // out of `extractResponse` live in their own modules. The exported names are
148
+ // re-exported under the names they had here, so no import site — and no suite
149
+ // that replaces this module by path — has to change. `detectPromptOnCleanFrame`,
150
+ // `detectPromptWithOptions` and `extractResponse` stay here:
151
+ // `tests/unit/guards/prompt-detector-single-entry-2368.test.ts` pins this file as
152
+ // the one that calls `detectPrompt`, and only this file may import
153
+ // `@/lib/tmux/tmux-capture-cache` (.eslintrc.json `no-restricted-imports`).
154
+ const response_checker_extraction_result_1 = require("./response-checker-extraction-result");
155
+ var response_checker_extraction_result_2 = require("./response-checker-extraction-result");
156
+ Object.defineProperty(exports, "incompleteResult", { enumerable: true, get: function () { return response_checker_extraction_result_2.incompleteResult; } });
157
+ Object.defineProperty(exports, "buildPromptExtractionResult", { enumerable: true, get: function () { return response_checker_extraction_result_2.buildPromptExtractionResult; } });
158
+ const response_checker_dialog_gate_1 = require("./response-checker-dialog-gate");
159
+ var response_checker_dialog_gate_2 = require("./response-checker-dialog-gate");
160
+ Object.defineProperty(exports, "isNumberedDialogVouched", { enumerable: true, get: function () { return response_checker_dialog_gate_2.isNumberedDialogVouched; } });
161
+ const response_checker_extraction_steps_1 = require("./response-checker-extraction-steps");
199
162
  /**
200
163
  * The one reading of a frame that every consumer of it shares, for a capture
201
164
  * that has ALREADY been through `stripBoxDrawing(stripAnsi(…))`.
@@ -234,9 +197,14 @@ function buildPromptExtractionResult(lines, lastCapturedLine, totalLines, buffer
234
197
  * then handed back as no prompt unless agy asked CommandMate about a tool call
235
198
  * for that instance a moment ago. Omitted, the reading is not gated — which is
236
199
  * what every caller that only DISPLAYS the frame wants.
200
+ * @param frame - the SAME tick's `normalizeFrame(raw, cliToolId)`, when the
201
+ * caller built one (Issue #3183). The Auto-Yes poller builds it once and hands
202
+ * the same object to the dialog gate, so the quotation veto below and the gate
203
+ * read one live region. Omitted, it is built here from `rawFrame` (or, failing
204
+ * that, `cleanOutput`).
237
205
  * @returns PromptDetectionResult with isPrompt, promptData, and cleanContent
238
206
  */
239
- function detectPromptOnCleanFrame(cleanOutput, cliToolId, precomputedLines, rawFrame, receiptScope) {
207
+ function detectPromptOnCleanFrame(cleanOutput, cliToolId, precomputedLines, rawFrame, receiptScope, frame) {
240
208
  // Issue #2364: agy's `↑/↓ Navigate` dialogs are read by agy's own reader
241
209
  // before the generic pass, on the same spelling `tools/antigravity/detect.ts`
242
210
  // reads them on. The generic multiple-choice parser takes one row per option
@@ -255,7 +223,7 @@ function detectPromptOnCleanFrame(cleanOutput, cliToolId, precomputedLines, rawF
255
223
  if (cliToolId === 'antigravity') {
256
224
  const dialog = (0, dialog_1.detectAntigravityNumberedDialogPrompt)(cleanOutput);
257
225
  if (dialog !== null) {
258
- return isWithheldForWantOfReceipt(receiptScope)
226
+ return (0, response_checker_dialog_gate_1.isWithheldForWantOfReceipt)(receiptScope)
259
227
  ? { isPrompt: false, cleanContent: cleanOutput.trim() }
260
228
  : dialog;
261
229
  }
@@ -287,114 +255,35 @@ function detectPromptOnCleanFrame(cleanOutput, cliToolId, precomputedLines, rawF
287
255
  }
288
256
  const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
289
257
  const result = (0, prompt_detector_1.detectPrompt)(cleanOutput, precomputedLines ? { ...promptOptions, precomputedLines } : promptOptions);
290
- // Issue #2851: agy's reader above declines a dialog quoted in a reply (#2845),
291
- // and this generic pass then read the quotation itself — `Do you want to
292
- // proceed?` and its four options, above a live `>` composer — as an answerable
293
- // `multiple_choice`. agy's Auto-Yes gate row is `legacy`, so nothing after this
294
- // point judges the frame again and the poller answered a dialog nobody had
295
- // opened. The status side declines the same candidate with the same predicate
296
- // (`isStalePrompt`, #2845): agy draws its numbered screens in place of the
297
- // composer or below it, never above it, so a composer under the list's last
298
- // row makes the list a quotation.
258
+ // Issue #3183 (agy-only before, #2851): a numbered list on a frame whose
259
+ // input box is the bottom of the pane is a quotation — a reply quoting a
260
+ // dialog, or one left in the scrollback — and is not a prompt to answer. The
261
+ // status side declines the same candidate with the same rule on the same
262
+ // region (`run-detection.ts`), so `waiting` and an automatic answer cannot
263
+ // disagree about one frame. Every tool, not only agy: #2851 reached Auto-Yes
264
+ // because agy's `>` is not one of the glyphs the generic parser stops at, and
265
+ // opencode's gutter is not either.
299
266
  //
300
- // `rawFrame` when the caller has it, and `cleanOutput` otherwise. The status
301
- // side normalises the capture as captured and the predicate then strips box
302
- // drawing exactly once; handing it `cleanOutput` — already stripped once, and
303
- // `stripBoxDrawing` is not idempotent — would strip a second time and could
304
- // read different rows.
305
- if (cliToolId === 'antigravity' &&
306
- result.isPrompt &&
307
- (0, detect_1.isAntigravityQuotedNumberedList)((0, frame_1.normalizeFrame)(rawFrame ?? cleanOutput), result)) {
308
- return { isPrompt: false, cleanContent: cleanOutput.trim() };
267
+ // `rawFrame` when there is no prebuilt frame: the input-box markers are rule
268
+ // rows that the cleaned spelling has blanked. `stripBoxDrawing` is not
269
+ // idempotent, so the cleaned string is never cleaned again here.
270
+ if (result.isPrompt) {
271
+ const liveRegion = frame !== undefined
272
+ ? (0, frame_1.liveRegionOf)(frame, cliToolId)
273
+ : (0, frame_1.normalizeFrame)(rawFrame ?? cleanOutput, cliToolId).liveRegion;
274
+ if ((0, live_region_1.isQuotedNumberedPrompt)(liveRegion, result)) {
275
+ return { isPrompt: false, cleanContent: cleanOutput.trim() };
276
+ }
309
277
  }
310
278
  // Issue #2849: the same withholding on this exit. agy's reader answering `null`
311
279
  // does not mean the frame is no dialog — the generic pass above reads shapes
312
280
  // agy's reader declines, and #2851 showed it can read a quotation — so a gate
313
281
  // that covered only the reader's exit would leave this one open.
314
- if (cliToolId === 'antigravity' && result.isPrompt && isWithheldForWantOfReceipt(receiptScope)) {
282
+ if (cliToolId === 'antigravity' && result.isPrompt && (0, response_checker_dialog_gate_1.isWithheldForWantOfReceipt)(receiptScope)) {
315
283
  return { isPrompt: false, cleanContent: cleanOutput.trim() };
316
284
  }
317
285
  return result;
318
286
  }
319
- /**
320
- * Should an agy frame that reads as a prompt be treated as no prompt because
321
- * agy asked CommandMate about no tool call lately? (Issue #2849)
322
- *
323
- * agy asks (`PreToolUse`) before it draws ANY approval dialog and waits for the
324
- * reply, so a dialog that is really open follows a question a moment before it,
325
- * and one that is only quoted in a reply follows nothing. The frame carries no
326
- * tool name, so the check is time-only.
327
- *
328
- * Not gated (false) when there is no scope: a caller that only shows the frame —
329
- * the status API, the response poller's stored `prompt` row, the notification —
330
- * must keep showing a real dialog on a machine whose hook is not installed or
331
- * did not reach us. Only the caller that ANSWERS the frame opts in, because for
332
- * it a missed answer costs a human a keystroke and a wrong one is sent as an
333
- * utterance nobody asked for.
334
- *
335
- * `debug`, not `info`: the poller re-reads a static pane every 2s, so a
336
- * quotation left on screen would print this on every tick.
337
- */
338
- function isWithheldForWantOfReceipt(scope) {
339
- if (scope === undefined)
340
- return false;
341
- if ((0, antigravity_permission_receipts_1.hasRecentAntigravityPermissionReceipt)(scope.worktreeId, 'antigravity', scope.instanceId))
342
- return false;
343
- logger.debug('antigravity-dialog-no-recent-receipt', {
344
- worktreeId: scope.worktreeId,
345
- instanceId: scope.instanceId ?? 'antigravity',
346
- });
347
- return true;
348
- }
349
- /**
350
- * Is the generic parser's numbered-list candidate a dialog the tool vouches for?
351
- * (Issue #2457)
352
- *
353
- * ## The candidate this removes
354
- *
355
- * `detectPrompt` classifies a frame as `multiple_choice` from the ROWS alone,
356
- * and for Claude `buildDetectPromptOptions` returns
357
- * `requireDefaultIndicator: false` — so a reply that happens to answer in
358
- * Markdown ("1. … 2. … 3. …") is a candidate with no `❯` anywhere near it.
359
- * Every one of those was stored as a `prompt` message, and the chat surface drew
360
- * a tool-approval chip over an ordinary answer.
361
- *
362
- * ## Why the whole frame, and only the frame
363
- *
364
- * `detectDialog` decides by POSITION and CHROME — where the option block sits
365
- * relative to the transcript tail, what (if anything) is drawn under it. None of
366
- * that survives being handed the extracted response, the question string or a
367
- * tail window, so the caller passes the same capture the candidate came off, as
368
- * captured: `evaluateDialogPresence` documents why the box drawing must still be
369
- * on it.
370
- *
371
- * ## What a `false` means downstream
372
- *
373
- * Not "there is no output here" — only "this is not a prompt". The caller falls
374
- * through to the ordinary completion/partial reading, so a real reply is still
375
- * saved by the normal path and a half-written one still reports
376
- * `isComplete: false`.
377
- *
378
- * @param cliToolId - CLI tool the frame came from
379
- * @param promptDetection - What the generic parser (or a tool reader) answered
380
- * @param frame - The whole capture this tick made, as captured
381
- * @returns True when the candidate may be treated as a live prompt
382
- */
383
- function isNumberedDialogVouched(cliToolId, promptDetection, frame) {
384
- const presence = (0, auto_yes_dialog_gate_1.evaluateDialogPresence)(cliToolId, promptDetection.promptData?.type, frame);
385
- if (presence.present)
386
- return true;
387
- // `debug`, not `info`: the poller re-reads a static idle pane every 2s, so a
388
- // reply carrying a numbered list would print this on every tick for as long as
389
- // it stays on screen. The suppression is not silent where it matters — nothing
390
- // is stored, so History simply shows the reply the ordinary path saves.
391
- logger.debug('prompt-candidate-not-vouched', {
392
- cliToolId,
393
- promptType: promptDetection.promptData?.type,
394
- gateMode: presence.mode,
395
- });
396
- return false;
397
- }
398
287
  /**
399
288
  * Internal helper: detect prompt with CLI-tool-specific options.
400
289
  *
@@ -412,9 +301,6 @@ function detectPromptWithOptions(output, cliToolId) {
412
301
  // was DERIVED from.
413
302
  return detectPromptOnCleanFrame((0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(output)), cliToolId, undefined, output);
414
303
  }
415
- // ============================================================================
416
- // extractResponse (internal)
417
- // ============================================================================
418
304
  /**
419
305
  * Extract CLI tool response from tmux output
420
306
  * Detects when a CLI tool has completed a response by looking for tool-specific patterns
@@ -425,7 +311,8 @@ function detectPromptWithOptions(output, cliToolId) {
425
311
  * @param captureWindowLines - Size of the capture window `output` was produced with.
426
312
  * Used only to decide whether the capture came back clipped (Issue #1670);
427
313
  * defaults to the window `checkForResponse()` uses.
428
- * @returns Extracted response or null if incomplete
314
+ * @returns Null when there is no new output; otherwise the extraction result,
315
+ * with `isComplete: false` when the response is not finished yet
429
316
  */
430
317
  function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines = tmux_capture_cache_1.CACHE_MAX_CAPTURE_LINES) {
431
318
  // Trim trailing empty lines from the output before processing
@@ -471,32 +358,7 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
471
358
  // silently delete another tool's boundary, and a boundary that stops existing
472
359
  // does not fail loudly: it puts terminal furniture back into History.
473
360
  const openCodeCleanLines = cliToolId === 'opencode' ? lines.map(cli_patterns_1.stripAnsi) : null;
474
- // Issue #2250: Command Code is the fourth. Its landmark is its own — the
475
- // `❯ Ask your question...` composer fenced by two full-pane rules, with the
476
- // permission-mode row underneath — and it is load-bearing rather than tidy:
477
- // that placeholder is drawn with the same `❯ <text>` shape as a transcript
478
- // echo, so without the boundary `findRecentUserPromptIndex` anchors the turn
479
- // on the FOOTER and every reply extracts as empty (#1289's defect, verbatim).
480
- //
481
- // Issue #2400: codex is the fifth, and the one that had been missing. It pins
482
- // the same two rows — `› Ask Codex to do anything` and the `model · cwd`
483
- // status bar — and without a boundary the saturated-window anchor (#1670)
484
- // walked into them: the newest `›` in the pane was the COMPOSER, so extraction
485
- // started on the status bar and every reply on a saturated pane was saved as
486
- // that one row. `findCodexChromeStart` reads the composer by its SGR
487
- // attributes (#2310) rather than by its placeholder wording, which is what the
488
- // previous guard did and why it stopped working at codex 0.15x.
489
- const chromeStart = cliToolId === 'claude'
490
- ? (0, cli_patterns_1.findClaudeChromeStart)(lines)
491
- : cliToolId === 'copilot'
492
- ? (0, cli_patterns_1.findCopilotChromeStart)(lines)
493
- : cliToolId === 'command-code'
494
- ? (0, cli_patterns_1.findCommandCodeChromeStart)(lines)
495
- : cliToolId === 'codex'
496
- ? (0, cli_patterns_1.findCodexChromeStart)(lines)
497
- : openCodeCleanLines
498
- ? (0, cli_patterns_1.findOpenCodeChromeStart)(openCodeCleanLines)
499
- : -1;
361
+ const chromeStart = (0, response_checker_extraction_steps_1.findChromeStart)(cliToolId, lines, openCodeCleanLines);
500
362
  const contentEnd = chromeStart >= 0 ? chromeStart : totalLines;
501
363
  const BUFFER_RESET_TOLERANCE = 25;
502
364
  const bufferShrank = totalLines > 0 && lastCapturedLine > BUFFER_RESET_TOLERANCE && (totalLines + BUFFER_RESET_TOLERANCE) < lastCapturedLine;
@@ -515,79 +377,7 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
515
377
  : linesToCheck.join('\n');
516
378
  // Get tool-specific patterns from shared module
517
379
  const { promptPattern, separatorPattern, thinkingPattern, skipPatterns } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
518
- const findRecentUserPromptIndex = (windowSize = 60) => {
519
- let userPromptPattern;
520
- if (cliToolId === 'codex') {
521
- // Issue #2400: codex's three uses of `›` are separated by their SGR
522
- // attributes, not by their text (#2310). This branch used to exclude the
523
- // composer with a negative lookahead over its placeholder strings
524
- // (`Implement`, `Find and fix`, `Type`, `Summarize`) — codex 0.1x wording,
525
- // none of which 0.15x draws. `Ask Codex to do anything` passed the guard,
526
- // became the newest "echo", and on a saturated pane (#1670) — the only
527
- // path where this anchor decides where extraction STARTS — every reply was
528
- // saved as the single status-bar row below it.
529
- //
530
- // Two independent things now keep the composer out, and the reader needs
531
- // both because neither covers the other's frames: `contentEnd` cuts the
532
- // composer off structurally when `findCodexChromeStart` located it, and
533
- // when it did not, `findCodexUserEchoIndex` steps over the bottom-most
534
- // `›` row instead. The second is what still answers on an ANSI-stripped
535
- // capture, where none of #2310's attributes survive to be read.
536
- return (0, cli_patterns_1.findCodexUserEchoIndex)(lines, contentEnd, windowSize, chromeStart >= 0);
537
- }
538
- else if (openCodeCleanLines) {
539
- // Issue #1911: anchor on the newest ECHOED USER PROMPT, not on the
540
- // second-to-last `▣ Build` row. The old anchor belonged to the PREVIOUS
541
- // turn, so the echoed prompt of the current one was always extracted as
542
- // part of the reply — and on the first turn of a session, where there is
543
- // no second marker, it fell through to line 0 and the whole pane (banner
544
- // included) became the answer. `windowSize` is ignored: the alternate
545
- // screen has no scrollback, so the whole pane IS the window, and every
546
- // caller already passes `totalLines` or more for this tool.
547
- return (0, response_extractor_1.resolveOpenCodeTurnRegion)(openCodeCleanLines).echoEnd;
548
- }
549
- else if (cliToolId === 'copilot') {
550
- // Issue #1897: copilot 1.0.80 draws the transcript one column in, so the
551
- // bare `^[>❯]` form below never matched the echoed prompt -- every copilot
552
- // extraction fell back to line 0, i.e. to the launch banner. The composer,
553
- // which IS at column 0, lives below `contentEnd` and so cannot be picked up
554
- // as an echo here.
555
- //
556
- // The scan then walks past the echo's own wrapped rows and returns the LAST
557
- // of them, so that callers' `+ 1` lands on the reply rather than on the
558
- // second half of the operator's question.
559
- //
560
- // Same defect as #1911's opencode branch above and the same shape of fix,
561
- // but NOT the same code: opencode's echo is a `┃ <text>` gutter row and
562
- // copilot's is ` ❯ <text>` at the pane's one-column indent, so the anchor
563
- // and the continuation rule are both tool-specific measurements.
564
- for (let i = contentEnd - 1; i >= Math.max(0, contentEnd - windowSize); i--) {
565
- if (!cli_patterns_1.COPILOT_USER_ECHO_PATTERN.test((0, cli_patterns_1.stripAnsi)(lines[i])))
566
- continue;
567
- let echoEnd = i;
568
- while (echoEnd + 1 < contentEnd &&
569
- cli_patterns_1.COPILOT_TRANSCRIPT_CONTINUATION_PATTERN.test((0, cli_patterns_1.stripAnsi)(lines[echoEnd + 1]))) {
570
- echoEnd++;
571
- }
572
- return echoEnd;
573
- }
574
- return -1;
575
- }
576
- else {
577
- userPromptPattern = /^[>❯]\s+\S/;
578
- }
579
- // Issue #1289: for Claude the search stops above the footer. The text the
580
- // user just typed sits in the footer's input box and matches the same "❯ …"
581
- // shape as the transcript echo; anchoring on it would treat the footer as
582
- // the newest turn and extract the status bar as its reply.
583
- for (let i = contentEnd - 1; i >= Math.max(0, contentEnd - windowSize); i--) {
584
- const cleanLine = (0, cli_patterns_1.stripAnsi)(lines[i]);
585
- if (userPromptPattern.test(cleanLine)) {
586
- return i;
587
- }
588
- }
589
- return -1;
590
- };
380
+ const findRecentUserPromptIndex = (windowSize = 60) => (0, response_checker_extraction_steps_1.findRecentUserPromptIndexInFrame)(cliToolId, lines, openCodeCleanLines, chromeStart, contentEnd, windowSize);
591
381
  // Early check for interactive prompts (before extraction logic)
592
382
  //
593
383
  // Issue #2250: `command-code` must be here. Its permission dialog draws its
@@ -607,270 +397,21 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
607
397
  // completion/partial reading to answer, which is what a reply containing a
608
398
  // numbered list needs. The gate is handed `fullOutput`, the capture itself,
609
399
  // not the string `detectPromptWithOptions` cleaned for the parser.
610
- if (promptDetection.isPrompt && isNumberedDialogVouched(cliToolId, promptDetection, fullOutput)) {
611
- return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection, captureWindowSaturated);
400
+ if (promptDetection.isPrompt && (0, response_checker_dialog_gate_1.isNumberedDialogVouched)(cliToolId, promptDetection, fullOutput)) {
401
+ return (0, response_checker_extraction_result_1.buildPromptExtractionResult)(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection, captureWindowSaturated);
612
402
  }
613
403
  }
614
404
  // Strip ANSI codes before pattern matching
615
405
  const cleanOutputToCheck = (0, cli_patterns_1.stripAnsi)(outputToCheck);
616
- const hasPrompt = promptPattern.test(cleanOutputToCheck);
617
- const hasSeparator = separatorPattern.test(cleanOutputToCheck);
618
- // Issue #1671: Codex's activity markers are past-tense transcript records that
619
- // never leave the scrollback, so testing them against this fixed tail window
620
- // reports "still thinking" for a finished turn whenever its final message was
621
- // short enough to keep the last "• Ran <cmd>" row inside the window. Codex gets
622
- // a liveness check that keys off the status line it repaints above the composer
623
- // instead; every other tool keeps the tail-window match.
624
- const isThinking = cliToolId === 'codex'
625
- ? (0, cli_patterns_1.isCodexTurnActive)(lines, checkLineCount)
626
- : thinkingPattern.test(cleanOutputToCheck);
627
- // Issue #1897: copilot's `hasPrompt` is worthless as a completion signal and
628
- // its `isThinking` is worthless as a liveness one. The `❯` composer is drawn
629
- // between its two rules throughout a turn (measured on every frame of #1885's
630
- // running fixtures), and `COPILOT_THINKING_PATTERN` matches nothing copilot
631
- // 1.0.80 draws (0 of 44 live generating frames). So `hasPrompt && !isThinking`
632
- // was true on the very first poll of a running turn -- the extractor declared
633
- // the turn finished, saved the status bar as the reply, and `checkForResponse`
634
- // stopped polling, which is why the real answer never reached History.
635
- //
636
- // 1.0.80 paints the turn's state on the bottom row of the pane and nowhere
637
- // else, so that ROW -- never a tail window, which copilot's own reply text can
638
- // forge (`status-vocabulary-in-response.txt`) -- is the evidence. `idle` is a
639
- // positive observation that the turn is over (design policy §4 D1 decision 1
640
- // item 2); `working` and `null` (a dialog box has taken the bar away) both mean
641
- // "not finished", and the dialog case is already served by the prompt path
642
- // above.
643
- const copilotStatusBar = cliToolId === 'copilot'
644
- ? (0, cli_patterns_1.readCopilotStatusBar)(lines.slice(Math.max(0, totalLines - COPILOT_STATUS_BAR_SCAN_ROWS)).map(cli_patterns_1.stripAnsi))
645
- : null;
646
- // Prompt-based completion logic
647
- const isPromptBasedComplete = cliToolId === 'copilot'
648
- ? copilotStatusBar === 'idle'
649
- : (cliToolId === 'codex' || cliToolId === 'gemini' || cliToolId === 'vibe-local' || cliToolId === 'antigravity') && hasPrompt && !isThinking;
650
- const isClaudeComplete = cliToolId === 'claude' && hasPrompt && hasSeparator && !isThinking;
651
- // Issue #2250: claude's shape, because Command Code's layout is claude's — the
652
- // composer sits between two full-pane rules and is drawn only when the agent
653
- // will accept input. Deliberately NOT keyed on `✻ Worked for`: that row is the
654
- // live turn's, not the transcript's (it is present in `turn-version.txt` and
655
- // gone from `dialog-create-file.txt`, the same pane one prompt later) and
656
- // `WorkedDurationNote` omits it entirely for a turn under 1000 ms.
657
- const isCommandCodeComplete = cliToolId === 'command-code' && hasPrompt && hasSeparator && !isThinking;
658
- const isOpenCodeDone = cliToolId === 'opencode' && (0, response_extractor_1.isOpenCodeComplete)(cleanOutputToCheck);
659
- if (isPromptBasedComplete || isClaudeComplete || isCommandCodeComplete || isOpenCodeDone) {
660
- const responseLines = [];
661
- const startIndex = (0, response_extractor_1.resolveExtractionStartIndex)(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, captureWindowSaturated);
662
- // `contentEnd` bounds the content only; `endIndex` keeps reporting the full
663
- // buffer so lineCount bookkeeping in session_states is unchanged (#1289).
664
- //
665
- // Issue #2400: codex is the exception, and it is the pre-existing behaviour
666
- // rather than a new rule. Before this Issue the loop below stopped on the
667
- // composer's `›` and wrote that row's index into `endIndex`; now the composer
668
- // is outside `contentEnd`, so the break can no longer fire on it and
669
- // `endIndex` would silently advance ~3 rows further. Those rows matter for
670
- // codex specifically: it renders INLINE, and it repaints the composer band in
671
- // place — the next turn's transcript is printed over exactly the rows the
672
- // composer occupied in this capture. A cursor parked past them would skip
673
- // real content on the following poll. So the cursor stops where the content
674
- // stops, which is what it did before.
675
- let endIndex = cliToolId === 'codex' ? contentEnd : totalLines;
676
- for (let i = startIndex; i < contentEnd; i++) {
677
- const line = lines[i];
678
- const cleanLine = (0, cli_patterns_1.stripAnsi)(line);
679
- if (cliToolId === 'codex' && /^›\s+/.test(cleanLine)) {
680
- endIndex = i;
681
- break;
682
- }
683
- if (cliToolId === 'gemini' && /^(%|\$|.*@.*[%$#])\s*$/.test(cleanLine)) {
684
- endIndex = i;
685
- break;
686
- }
687
- // Antigravity (agy): the bare ">" input box line marks the end of the
688
- // response (the status bar and shortcuts footer follow below it). (Issue #988)
689
- if (cliToolId === 'antigravity' && /^>\s*$/.test(cleanLine)) {
690
- endIndex = i;
691
- break;
692
- }
693
- // Issue #1911: both rows this stops on (`Ask anything...` in the composer,
694
- // `tab agents ctrl+p commands` under its border) live in the chrome, which
695
- // `contentEnd` now excludes structurally. Kept only as the fallback for a
696
- // frame whose chrome could not be located, because there it is still the
697
- // one boundary available — and #1883 measured that a REPLY can contain
698
- // `Ask anything...`, so cutting the turn on it is a last resort, not the
699
- // primary rule.
700
- if (cliToolId === 'opencode' && chromeStart < 0) {
701
- if (cli_patterns_1.OPENCODE_PROMPT_PATTERN.test(cleanLine) || cli_patterns_1.OPENCODE_PROMPT_AFTER_RESPONSE.test(cleanLine)) {
702
- endIndex = i;
703
- break;
704
- }
705
- }
706
- const shouldSkip = skipPatterns.some(pattern => pattern.test(cleanLine));
707
- if (shouldSkip) {
708
- continue;
709
- }
710
- responseLines.push(line);
711
- }
712
- const response = responseLines.join('\n').trim();
713
- // DR-004: Check only the tail of the response for thinking indicators.
714
- //
715
- // Issue #1897: not for copilot. This is the same tail-window match the #1671
716
- // codex fix removed from the liveness test, and on copilot it is both
717
- // redundant and harmful: the status bar above has already made a positive
718
- // `idle` observation about THIS frame, while the window here sees transcript
719
- // that never scrolls away. `COPILOT_THINKING_PATTERN`'s braille alternative
720
- // matches any spinner glyph a reply happens to quote, and the turn would then
721
- // be reported unfinished for the rest of the session.
722
- const responseTailLines = response.split('\n').slice(-thinking_constants_1.THINKING_TAIL_LINE_COUNT).join('\n');
723
- if (cliToolId !== 'copilot' && thinkingPattern.test(responseTailLines)) {
724
- return incompleteResult(totalLines);
725
- }
726
- // CRITICAL FIX: Detect and skip Claude Code startup banner/screen
727
- if (cliToolId === 'claude') {
728
- const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
729
- // Issue #2247: `│` is what Claude Code draws markdown TABLES with -- the
730
- // live frame in `tests/fixtures/claude-live-2247/turn-table.txt` is a
731
- // two-row table and nothing else -- so it identified a reply, not a banner.
732
- // The banner's own frame glyphs are the rounded corners and the block
733
- // shading; those stay.
734
- const hasBannerArt = /[╭╮╰╯]/.test(cleanResponse) || /░{3,}/.test(cleanResponse) || /▓{3,}/.test(cleanResponse);
735
- // Issue #2247: the bare `v\d+\.\d+` alternative matched any version string a
736
- // reply happens to mention. The frame that lost a turn on 2026-09-02 was
737
- // "GitHub Release v0.30.0 を公開しました" (148 chars, well under the 2000
738
- // below). What the banner actually prints is the tool's own name and
739
- // version on one row -- `Claude Code v2.1.258` -- so that is what is
740
- // matched now, plus the `claude/` form older banners used.
741
- const hasVersionInfo = /Claude Code v\d+\.\d+|claude\//.test(cleanResponse);
742
- const hasStartupTips = /Tip:|for shortcuts|\?\s*for help/.test(cleanResponse);
743
- const hasProjectInit = /^\s*\/Users\/.*$/m.test(cleanResponse) && cleanResponse.split('\n').length < 30;
744
- // Issue #2247: the anchors above are only evidence of a banner on a pane
745
- // that has not had a single turn yet -- the same shape as the #1897 copilot
746
- // fix below. Claude echoes every prompt into the transcript as `❯ <text>`,
747
- // and the startup screen has none, so an echo anywhere in the transcript
748
- // rules the banner out no matter what the reply quotes.
749
- //
750
- // The search is `findRecentUserPromptIndex`, deliberately: it is the same
751
- // `/^[>❯]\s+\S/` this file already anchors extraction on, and it stops at
752
- // `contentEnd`. That bound is load-bearing rather than incidental -- the
753
- // footer's composer draws a DIM ghost suggestion (`❯ Try "write a test for
754
- // <filepath>"`, see `boot-banner.txt`) whose stripped bytes are identical
755
- // to a real echo (#1879), so a scan over the whole pane would read the
756
- // startup screen as "already had a turn" and put the banner back in
757
- // History.
758
- const hasTurnEcho = findRecentUserPromptIndex(totalLines) >= 0;
759
- const userPromptMatch = cleanResponse.match(/^[>❯]\s+(\S.*)$/m);
760
- if (userPromptMatch) {
761
- const userPromptIndex = cleanResponse.indexOf(userPromptMatch[0]);
762
- const contentAfterPrompt = cleanResponse.substring(userPromptIndex + userPromptMatch[0].length).trim();
763
- const contentLines = contentAfterPrompt.split('\n').filter(line => {
764
- const trimmed = line.trim();
765
- return trimmed &&
766
- !skipPatterns.some(p => p.test(trimmed)) &&
767
- !/^─+$/.test(trimmed);
768
- });
769
- if (contentLines.length === 0) {
770
- return incompleteResult(totalLines);
771
- }
772
- }
773
- else if (!hasTurnEcho &&
774
- (hasBannerArt || hasVersionInfo || hasStartupTips || hasProjectInit) &&
775
- response.length < 2000) {
776
- // Issue #2247: this branch used to swallow the turn in silence -- the
777
- // poller kept ticking every 2s and `response-poller` logged nothing at
778
- // all, so the only way to tell a lost turn from an idle session was to
779
- // re-run `extractResponse` on a saved pane by hand. It is reached only
780
- // before the first echo lands, so it cannot become a per-tick flood.
781
- logger.info('Claude startup banner suppressed, response not saved', {
782
- responseLength: response.length,
783
- hasBannerArt,
784
- hasVersionInfo,
785
- hasStartupTips,
786
- hasProjectInit,
787
- });
788
- return incompleteResult(totalLines);
789
- }
790
- }
791
- // Issue #1897: copilot's launch screen is a complete, idle frame -- composer
792
- // drawn, key hints on the status bar -- so every check above accepts it and
793
- // History used to open with the banner ("Current Sessions Issues Pull
794
- // requests Gists / No copilot-instructions.md found… / Tip: /app") saved as
795
- // the agent's first reply, before the operator had said anything.
796
- //
797
- // What actually distinguishes it is that no turn has happened: copilot echoes
798
- // every prompt into the transcript as ` ❯ <text>`, and the launch screen has
799
- // none. The banner anchors are only consulted once that echo is missing, so a
800
- // reply that quotes any of this wording is unaffected.
801
- if (cliToolId === 'copilot') {
802
- const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
803
- const hasUserEcho = cleanResponse
804
- .split('\n')
805
- .some(line => cli_patterns_1.COPILOT_USER_ECHO_PATTERN.test(line));
806
- if (!hasUserEcho && cli_patterns_1.COPILOT_BOOT_BANNER_ANCHORS.some(anchor => anchor.test(cleanResponse))) {
807
- return incompleteResult(totalLines);
808
- }
809
- }
810
- // Issue #2250: Command Code's launch screen is a complete, idle frame --
811
- // block-art logo, three `#` banner rows, composer drawn between its two
812
- // rules -- so every check above accepts it, and History would open with
813
- // `# Command Code v1.40.1 / # models: … / # <cwd>` saved as the agent's
814
- // first reply before the operator has said anything.
815
- //
816
- // The rule is ONE condition and it is a positive one: Command Code echoes
817
- // every prompt into the transcript as `❯ <text>`, and the launch screen has
818
- // none. That is the shape #1897 and #2247 both converged on; the anchor
819
- // heuristics claude carries above (`hasBannerArt` / `hasVersionInfo` /
820
- // `hasStartupTips`) are deliberately NOT reproduced here, because they are
821
- // what #2247 had to take back -- a bare version string in a reply is a
822
- // normal reply, and Command Code prints its own version on every launch.
823
- //
824
- // `findRecentUserPromptIndex` is the same `/^[>❯]\s+\S/` scan the extraction
825
- // anchors on, and it stops at `contentEnd`, so the composer's own dim
826
- // `❯ Ask your question...` placeholder cannot be mistaken for an echo
827
- // (#1879's trap).
828
- if (cliToolId === 'command-code' && findRecentUserPromptIndex(totalLines) < 0) {
829
- logger.info('Command Code launch screen suppressed, response not saved', {
830
- responseLength: response.length,
831
- });
832
- return incompleteResult(totalLines);
833
- }
834
- // Gemini-specific check
835
- if (cliToolId === 'gemini') {
836
- const bannerCharCount = (response.match(/[░███]/g) || []).length;
837
- const totalChars = response.length;
838
- if (bannerCharCount > totalChars * 0.3) {
839
- return incompleteResult(totalLines);
840
- }
841
- if (response_poller_core_1.GEMINI_LOADING_INDICATORS.some(indicator => response.includes(indicator))) {
842
- return incompleteResult(totalLines);
843
- }
844
- if (!response.includes('\u2726') && response.length < 10) {
845
- return incompleteResult(totalLines);
846
- }
847
- }
848
- // OpenCode banner defense
849
- if (cliToolId === 'opencode') {
850
- const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
851
- if (cleanResponse.length < 50 || !cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanOutputToCheck)) {
852
- const contentLines = cleanResponse.split('\n').filter(line => {
853
- const trimmed = line.trim();
854
- return trimmed && !cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(p => p.test(trimmed));
855
- });
856
- if (contentLines.length === 0) {
857
- return incompleteResult(totalLines);
858
- }
859
- }
860
- }
861
- return {
862
- response,
863
- isComplete: true,
864
- lineCount: endIndex,
865
- bufferReset,
866
- captureWindowSaturated,
867
- // Issue #1911: opencode only. `echoEnd < 0` means the turn is longer than
868
- // the alternate-screen pane and its head has already scrolled away, so
869
- // `response` starts mid-answer. Nothing else in this frame can recover it.
870
- turnHeadTruncated: openCodeCleanLines
871
- ? (0, response_extractor_1.resolveOpenCodeTurnRegion)(openCodeCleanLines).headTruncated
872
- : undefined,
873
- };
406
+ // Issue #3213: what the steps split out of this function read off the frame.
407
+ const ctx = {
408
+ cliToolId, lines, totalLines, openCodeCleanLines, chromeStart, contentEnd,
409
+ lastCapturedLine, bufferReset, captureWindowSaturated, checkLineCount, cleanOutputToCheck,
410
+ promptPattern, separatorPattern, thinkingPattern, skipPatterns,
411
+ findRecentUserPromptIndex,
412
+ };
413
+ if ((0, response_checker_extraction_steps_1.isTurnComplete)(ctx)) {
414
+ return (0, response_checker_extraction_steps_1.extractCompletedResponse)(ctx);
874
415
  }
875
416
  // Check if this is an interactive prompt
876
417
  if (cliToolId !== 'opencode') {
@@ -879,234 +420,623 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
879
420
  // reaches when the completion rules said "unfinished", so a candidate
880
421
  // accepted here would end the turn on it just the same.
881
422
  const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
882
- if (promptDetection.isPrompt && isNumberedDialogVouched(cliToolId, promptDetection, fullOutput)) {
883
- return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection, captureWindowSaturated);
423
+ if (promptDetection.isPrompt && (0, response_checker_dialog_gate_1.isNumberedDialogVouched)(cliToolId, promptDetection, fullOutput)) {
424
+ return (0, response_checker_extraction_result_1.buildPromptExtractionResult)(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection, captureWindowSaturated);
884
425
  }
885
426
  }
886
- // Partial response in progress
887
- const responseLines = [];
888
- const endIndex = totalLines;
889
- // Issue #1670: a saturated window makes lastCapturedLine meaningless here too —
890
- // starting the partial slice at it would stream an arbitrary tail fragment
891
- // instead of the turn so far. Re-anchor on the echoed user prompt.
892
- const partialBufferReset = bufferReset || captureWindowSaturated || lastCapturedLine >= endIndex - 5;
893
- const recentPromptIndex = partialBufferReset ? findRecentUserPromptIndex(80) : -1;
894
- const startIndex = partialBufferReset
895
- ? (recentPromptIndex >= 0 ? recentPromptIndex + 1 : Math.max(0, endIndex - 80))
896
- : Math.max(0, lastCapturedLine);
897
- // Partial (still-streaming) content is bounded by the footer too (#1289).
898
- for (let i = startIndex; i < Math.min(endIndex, contentEnd); i++) {
899
- const line = lines[i];
900
- const cleanLine = (0, cli_patterns_1.stripAnsi)(line);
901
- const shouldSkip = skipPatterns.some(pattern => pattern.test(cleanLine));
902
- if (shouldSkip) {
903
- continue;
904
- }
905
- responseLines.push(line);
906
- }
907
- const partialResponse = responseLines.join('\n').trim();
908
- if (partialResponse) {
909
- return {
910
- response: partialResponse,
911
- isComplete: false,
912
- lineCount: endIndex,
913
- };
914
- }
915
- // Response not yet complete
916
- return incompleteResult(totalLines);
427
+ return (0, response_checker_extraction_steps_1.extractPartialResponse)(ctx);
917
428
  }
918
429
  // ============================================================================
919
430
  // checkForResponse (exported for response-poller-core.ts)
920
431
  // ============================================================================
921
- // ============================================================================
922
- // The held scrape (Issue #2436)
923
- // ============================================================================
924
432
  /**
925
- * How long a scraped reply is held while its transcript finishes closing.
433
+ * Record what the structured writers said about this turn, for the ticks that
434
+ * will not get to ask (Issue #2399).
926
435
  *
927
- * Derived from `STOP_TRANSCRIPT_DEFERRED_DELAYS_MS`, not spelled again: those
928
- * are the instants the Stop receiver re-reads the transcript at after it has
929
- * answered the agent (#2398), so their SUM is the moment after which nobody is
930
- * still trying. Holding past it would be holding for a row that has no producer
931
- * left; stopping short of it would race the producer that is still running.
436
+ * One line either way, but named because the two calls are a pair and the
437
+ * failure mode of writing only one of them is silent: mark without settle and
438
+ * the reader is re-asked forever after a turn it already recorded; settle
439
+ * without mark and the fix does not exist.
932
440
  *
933
- * The measured gap this covers is under a second — codex 2026-09-08 appended
934
- * `task_complete` ~700 ms after the frame went quiet — so the budget is roughly
935
- * ten times the case it exists for, spent only on turns that ask for it.
441
+ * @param pollerKey - Poller key ("worktreeId:instanceId")
442
+ * @param recorded - Whether a structured writer owns this turn
936
443
  */
937
- exports.PENDING_SCRAPE_HOLD_MS = stop_history_capture_1.STOP_TRANSCRIPT_DEFERRED_DELAYS_MS.reduce((total, delay) => total + delay, 0);
444
+ function markOrSettleStructuredHistoryRecheck(pollerKey, recorded) {
445
+ if (recorded) {
446
+ (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
447
+ }
448
+ else {
449
+ (0, response_dedup_1.markStructuredHistoryRecheckPending)(pollerKey);
450
+ }
451
+ }
938
452
  /**
939
- * Held scrapes, by poller key.
453
+ * Read the frame this tick captured: is there a finished turn on it that the
454
+ * cursor has not already counted?
455
+ *
456
+ * The part of {@link checkForResponse} between the capture and the prompt
457
+ * check, split out as it was (Issue #3213). It starts on the line after the
458
+ * capture's `await` and holds no `await` of its own, so it moved as a
459
+ * synchronous function and the call adds no yield. The part above it — the
460
+ * session check through the capture — has four, and stays where it was: a
461
+ * function holding them would have to be awaited, and that yield would fall
462
+ * between the capture and everything this tick does with it.
940
463
  *
941
- * **Deliberately not in `./response-dedup`.** That module's two caches are
942
- * cleared by `stopPollingByKey` — `clearResponseHashCache` is called from
943
- * inside it — so a held reply parked beside them would be dropped by the very
944
- * event that has to write it (Issue #2436, requirement B). It lives here, next
945
- * to the code that fills it and the code that writes it out, and
946
- * `flushPendingScrapedResponse` is what the poller calls before it clears
947
- * anything.
464
+ * Not a pure read. It feeds the fault notifier and the Layer-2 accumulator on
465
+ * every tick, and on an unfinished frame it may mark pending prompts answered.
948
466
  *
949
- * On `globalThis` for the reason every shared map in this subsystem is (#1736):
950
- * under `next dev` the poller's bundle and each route's bundle would otherwise
951
- * hold a private copy, and a held reply only one bundle can see is a lost one.
467
+ * Every `return false` is the tick's own result; the caller returns it as it is.
468
+ *
469
+ * @param ctx - What this tick is keyed by
470
+ * @param output - The capture this tick made
471
+ * @param lastCapturedLine - Its cursor, 0 when there is none
472
+ * @returns What the tick goes on with, or `false` when the tick ends here
952
473
  */
953
- const pendingScrapedResponses = (globalThis.__pendingScrapedResponses ??= new Map());
954
- /** Forget every held scrape. Test seam. */
955
- function resetPendingScrapedResponses() {
956
- pendingScrapedResponses.clear();
957
- }
958
- /** Whether a scrape is being held for this poller key. Test seam / diagnostics. */
959
- function hasPendingScrapedResponse(pollerKey) {
960
- return pendingScrapedResponses.has(pollerKey);
474
+ function extractCompletedTurn(ctx, output, lastCapturedLine) {
475
+ const { db, worktreeId, cliToolId, instanceId, resolvedInstanceId, pollerKey } = ctx;
476
+ // Issue #2000: the frame is in hand, so this is the cheapest place to ask
477
+ // whether the model API has stalled the session. Level in, edge out — see
478
+ // the helper.
479
+ observeUpstreamFaultForPush(worktreeId, cliToolId, instanceId, output);
480
+ // Layer 2: Accumulate TUI content for full-screen TUI tools, so a turn that
481
+ // outgrows the alternate-screen pane keeps the head that has scrolled away.
482
+ if (cliToolId === 'opencode' || cliToolId === 'copilot') {
483
+ // Issue #1911: opencode is accumulated from the CURRENT TURN'S REGION
484
+ // rather than the whole frame. Feeding the raw pane seeded the accumulator
485
+ // with the previous turn's transcript, the echoed prompt and the bottom
486
+ // chrome on the very first poll, so the accumulated content could never be
487
+ // used as a response source without re-introducing defect 1.
488
+ const accumulatorSource = cliToolId === 'opencode' ? (0, response_extractor_1.sliceOpenCodeTurn)(output) : output;
489
+ (0, tui_accumulator_1.accumulateTuiContent)(pollerKey, accumulatorSource, cliToolId);
490
+ }
491
+ // Extract response
492
+ const result = extractResponse(output, lastCapturedLine, cliToolId, tmux_capture_cache_1.CACHE_MAX_CAPTURE_LINES);
493
+ if (!result || !result.isComplete) {
494
+ // DR-004 windowing: Only check tail lines
495
+ const { thinkingPattern } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
496
+ const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
497
+ const tailLines = cleanOutput.split('\n').slice(-thinking_constants_1.THINKING_TAIL_LINE_COUNT).join('\n');
498
+ if (thinkingPattern.test(tailLines)) {
499
+ // Intended twin of the sweep in recordCompletedResponse: this one runs when
500
+ // working is visible on an unfinished frame, that one when a response is
501
+ // recorded (Issue #31).
502
+ const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId, resolvedInstanceId, broadcastPromptSweptToAnswered(worktreeId));
503
+ if (answeredCount > 0) {
504
+ logger.info('marked-answeredcount-pending');
505
+ }
506
+ }
507
+ return false;
508
+ }
509
+ const isFullScreenTui = cliToolId === 'opencode' || cliToolId === 'copilot';
510
+ // Issue #1268: line-count bookkeeping is only meaningful for tools that keep
511
+ // scrollback. Alternate-screen tools (claude since v2, opencode, copilot)
512
+ // always capture exactly pane_height lines, so lastCapturedLine saturates at
513
+ // the pane height on the first save and every later check would see
514
+ // `lineCount <= lastCapturedLine` and drop the response forever — leaving
515
+ // History stuck on "Waiting for response..." while the terminal shows the
516
+ // reply. Those tools dedup on response content instead (see below).
517
+ //
518
+ // Issue #1670: the scrollback tools reach the SAME dead end from the other
519
+ // side. Their buffer does grow — but only until it outgrows the capture
520
+ // window, after which the capture is clipped, the count is pinned at the
521
+ // window size, and the cursor can never be overtaken again. #1268 fixed
522
+ // saturation at pane height; this is saturation at the capture window, and it
523
+ // disables the cursor for exactly as long as the clipping lasts (a session
524
+ // restart or a cleared pane un-saturates it and the cursor comes back).
525
+ const lineCountIsCursor = !(0, types_1.usesAlternateScreen)(cliToolId) && !result.captureWindowSaturated;
526
+ // Duplicate prevention
527
+ if (lineCountIsCursor && !result.bufferReset && result.lineCount === lastCapturedLine) {
528
+ return false;
529
+ }
530
+ if (lineCountIsCursor && !result.bufferReset && result.lineCount <= lastCapturedLine) {
531
+ // Every poll of a finished, unchanged screen lands here, so it is debug.
532
+ logger.debug('already-saved-up-to-last-captured-line', {
533
+ worktreeId,
534
+ cliToolId,
535
+ instanceId: resolvedInstanceId,
536
+ lineCount: result.lineCount,
537
+ lastCapturedLine,
538
+ });
539
+ return false;
540
+ }
541
+ return { result, isFullScreenTui, lineCountIsCursor };
961
542
  }
962
543
  /**
963
- * Hold this tick's scraped reply instead of writing it (Issue #2436).
544
+ * Save a live prompt as a prompt message, and raise what follows from it: the
545
+ * task event, the push notification and the waiting episode.
964
546
  *
965
- * Called when the transcript reader has said `not_yet_closed`: the agent's own
966
- * Markdown for this turn is coming, and writing the pane's copy now is what put
967
- * 234,323 characters of prompt echo, intermediate output and footer into
968
- * History beside the real answer.
547
+ * The body of the `if (promptIsLive)` branch of {@link checkForResponse}, split
548
+ * out as it was (Issue #3213). Both returns are the tick's own result — `false`
549
+ * for a prompt the dedup has already recorded, `true` once the row is written.
969
550
  *
970
- * A second hold for the same key REPLACES the content and keeps the original
971
- * deadline. Replaces, because a frame that moved on is a better copy of the
972
- * same turn; keeps, because a pane that redraws every tick would otherwise push
973
- * its own deadline forward forever and the hold would stop being bounded.
551
+ * @param ctx - What this tick is keyed by
552
+ * @param promptDetection - The detection that passed the #2457 gate
553
+ * @param result - The complete extraction this tick made
554
+ * @param isFullScreenTui - True for opencode and copilot, which keep polling after a prompt
555
+ * @returns What `checkForResponse` returns for this tick
974
556
  */
975
- function holdScrapedResponse(pollerKey, pending) {
976
- const existing = pendingScrapedResponses.get(pollerKey);
977
- if (existing) {
978
- existing.content = pending.content;
979
- return;
557
+ function savePromptMessage(ctx, promptDetection, result, isFullScreenTui) {
558
+ const { db, worktree, worktreeId, cliToolId, instanceId, resolvedInstanceId, pollerKey } = ctx;
559
+ // Issue #565: Content hash-based duplicate prompt prevention
560
+ const promptContent = promptDetection.rawContent || promptDetection.cleanContent;
561
+ const normalizedForDedup = (0, prompt_dedup_1.normalizePromptForDedup)(promptContent, cliToolId);
562
+ if ((0, prompt_dedup_1.isDuplicatePrompt)(pollerKey, normalizedForDedup)) {
563
+ // Issue #1695: the log line below is invisible to `commandmate capture
564
+ // --json`, so a suppressed prompt and a prompt the detection layer never
565
+ // classified (#1676) look identical from the CLI — both say "nothing was
566
+ // recorded". Count the skip so the payload can tell them apart.
567
+ (0, prompt_dedup_state_1.recordPromptDedupSkip)(worktreeId, cliToolId, instanceId);
568
+ logger.info('duplicate-prompt-skipped', { worktreeId, cliToolId });
569
+ return false;
570
+ }
571
+ // Issue #571: Clean TUI decorations from Copilot prompt content before saving
572
+ let promptSaveContent = promptContent;
573
+ if (cliToolId === 'copilot') {
574
+ promptSaveContent = (0, response_cleaner_1.cleanCopilotResponse)(promptContent);
575
+ promptSaveContent = (0, response_cleaner_1.truncateMessage)(promptSaveContent, copilot_constants_1.COPILOT_MAX_MESSAGE_LENGTH, copilot_constants_1.COPILOT_TRUNCATION_MARKER);
576
+ }
577
+ // This is a prompt - save as prompt message
578
+ (0, db_1.clearInProgressMessageId)(db, worktreeId, cliToolId, resolvedInstanceId);
579
+ const message = (0, db_1.createMessage)(db, {
580
+ worktreeId,
581
+ role: 'assistant',
582
+ content: promptSaveContent,
583
+ messageType: 'prompt',
584
+ promptData: promptDetection.promptData,
585
+ timestamp: new Date(),
586
+ cliToolId,
587
+ instanceId: resolvedInstanceId,
588
+ });
589
+ (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
590
+ (0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
591
+ // Issue #1548: the agent is blocked on input. Raised after the dedup and
592
+ // save above, so the task log counts prompts the system actually recorded
593
+ // rather than every poll that saw the same one still on screen. No-ops
594
+ // when this instance is not running a contract.
595
+ (0, task_transition_service_1.applyEventToActiveTask)(db, worktreeId, cliToolId, resolvedInstanceId, 'prompt_detected', {
596
+ promptType: promptDetection.promptData?.type,
597
+ });
598
+ // Web Push fan-out (Issue #1125): agent is now waiting for a prompt reply.
599
+ // Fire-and-forget — push is advisory and must never block/break the poller.
600
+ //
601
+ // Issue #1790: the wait is now named by #1786's episode rather than by the
602
+ // prompt text. The two lines below are ordered, not incidental:
603
+ //
604
+ // 1. the notification is raised first, while it still has the prompt's
605
+ // own question to quote — it records the episode in the dedup, so
606
+ // whichever path reports the wait second says nothing;
607
+ // 2. `observeWaitingEdge` then opens that same episode, which is what
608
+ // lets the edge listener (and #1788's WebSocket frame) agree with this
609
+ // call about *which* wait this is instead of raising a second one.
610
+ //
611
+ // Both use one timestamp so the episode the notification claims and the
612
+ // episode the store opens are the same number.
613
+ const promptObservedAt = Date.now();
614
+ const promptWaitingSince = (0, waiting_episode_state_1.getWaitingEpisode)(worktreeId, cliToolId, instanceId)?.since ?? promptObservedAt;
615
+ // Issue #1999: Auto-Yes is a declaration that this session's prompts are
616
+ // answered without a human, so notifying for one is telling the reader the
617
+ // opposite of the truth. Only the notification is gated — the episode
618
+ // below still opens, so the WebSocket frame, the status API and the #1790
619
+ // reminder all see the wait exactly as they did before. The gate runs
620
+ // before the call rather than inside it because `shouldSendWaitingPush`
621
+ // records the episode the moment it decides to send.
622
+ if (!(0, prompt_push_gate_1.isPromptPushSuppressed)({
623
+ worktreeId,
624
+ cliToolId,
625
+ instanceId,
626
+ waitingSince: promptWaitingSince,
627
+ })) {
628
+ void (0, push_1.notifyPushSubscribers)({
629
+ worktreeId,
630
+ worktreeName: worktree.name,
631
+ kind: 'prompt',
632
+ agentName: resolvedInstanceId,
633
+ instanceId: resolvedInstanceId,
634
+ waitingKind: 'prompt',
635
+ waitingSince: promptWaitingSince,
636
+ excerpt: promptDetection.promptData?.question ?? promptSaveContent,
637
+ }).catch(() => { });
980
638
  }
981
- pendingScrapedResponses.set(pollerKey, pending);
639
+ (0, waiting_episode_state_1.observeWaitingEdge)({
640
+ worktreeId,
641
+ cliToolId,
642
+ instanceId,
643
+ waiting: true,
644
+ kind: 'prompt',
645
+ now: promptObservedAt,
646
+ });
647
+ if (!isFullScreenTui) {
648
+ (0, response_poller_core_1.stopPolling)(worktreeId, cliToolId, instanceId);
649
+ }
650
+ return true;
982
651
  }
983
652
  /**
984
- * Drop a held scrape without writing it (Issue #2436).
653
+ * Clean a complete response the way its tool needs before it is saved.
985
654
  *
986
- * The one thing that justifies dropping it: the transcript reader has since
987
- * written the turn as the agent's own Markdown, so the pane's copy is the
988
- * duplicate this Issue exists to stop.
655
+ * The "Clean up responses" step of {@link checkForResponse}, split out as it
656
+ * was (Issue #3213). Not a pure function: for copilot and opencode it reads the
657
+ * Layer-2 accumulator and clears it, so it is called once per tick, at the
658
+ * position the step had.
659
+ *
660
+ * @param cliToolId - CLI tool identifier
661
+ * @param result - The complete extraction this tick made
662
+ * @param pollerKey - Poller key ("worktreeId:instanceId")
663
+ * @returns The cleaned response
989
664
  */
990
- function discardPendingScrapedResponse(pollerKey) {
991
- pendingScrapedResponses.delete(pollerKey);
665
+ function cleanCompletedResponse(cliToolId, result, pollerKey) {
666
+ // Clean up responses
667
+ let cleanedResponse = result.response;
668
+ if (cliToolId === 'gemini') {
669
+ cleanedResponse = (0, response_cleaner_1.cleanGeminiResponse)(result.response);
670
+ }
671
+ else if (cliToolId === 'claude') {
672
+ cleanedResponse = (0, response_cleaner_1.cleanClaudeResponse)(result.response);
673
+ }
674
+ else if (cliToolId === 'copilot') {
675
+ const accumulatedContent = (0, tui_accumulator_1.getAccumulatedContent)(pollerKey);
676
+ const sourceContent = accumulatedContent || result.response;
677
+ cleanedResponse = (0, response_cleaner_1.cleanCopilotResponse)(sourceContent);
678
+ cleanedResponse = (0, response_cleaner_1.truncateMessage)(cleanedResponse, copilot_constants_1.COPILOT_MAX_MESSAGE_LENGTH, copilot_constants_1.COPILOT_TRUNCATION_MARKER);
679
+ (0, tui_accumulator_1.clearTuiAccumulator)(pollerKey);
680
+ }
681
+ else if (cliToolId === 'opencode') {
682
+ // Issue #1911 defect 3: opencode wrote to the Layer-2 accumulator but never
683
+ // read it, so any turn longer than the pane was saved without its head.
684
+ //
685
+ // Read it only when the head is ACTUALLY gone, which is what
686
+ // `turnHeadTruncated` measures — deliberately NOT copilot's unconditional
687
+ // `accumulated || response`. The accumulator appends whatever the overlap
688
+ // check cannot match against the previous poll, and opencode rewrites rows
689
+ // in place while it works (`+ Thought: … · 12ms` becomes `· 579ms`, a
690
+ // pending patch row becomes the applied edit). Every such rewrite breaks
691
+ // the overlap and re-appends the lines above it, so preferring the
692
+ // accumulator for the common short answer would duplicate content that
693
+ // `result.response` already holds exactly. When the echo is off screen the
694
+ // frame is missing content outright, and a possible duplicate beats a
695
+ // guaranteed truncation.
696
+ const accumulatedContent = (0, tui_accumulator_1.getAccumulatedContent)(pollerKey);
697
+ const sourceContent = result.turnHeadTruncated && accumulatedContent
698
+ ? accumulatedContent
699
+ : result.response;
700
+ cleanedResponse = (0, response_cleaner_1.cleanOpenCodeResponse)(sourceContent);
701
+ (0, tui_accumulator_1.clearTuiAccumulator)(pollerKey);
702
+ }
703
+ return cleanedResponse;
992
704
  }
993
705
  /**
994
- * Write a held scrape now, whatever the clock says (Issue #2436).
706
+ * Skip a response the content dedup has already seen, and re-ask the transcript
707
+ * reader on the ticks that are owed an answer (Issue #2399).
995
708
  *
996
- * Exported because `response-poller-core` calls it from `stopPollingByKey`,
997
- * which is every way a polling cycle ends: an explicit stop, the session going
998
- * away, `MAX_POLLING_DURATION`, and the restart that opens the NEXT turn. A
999
- * held reply must not be able to outlive the cycle that holds it — the caches
1000
- * that key it are cleared in that same function, and a reply nobody writes is
1001
- * strictly worse than the duplicate row this Issue is trading against.
709
+ * The body of the `isDuplicateResponse` branch of {@link checkForResponse},
710
+ * split out as it was (Issue #3213). Both returns are the tick's own result,
711
+ * and the caller returns it without doing anything else — so the one `await`
712
+ * this split adds comes after every write of the tick.
1002
713
  *
1003
- * **Deliberately bypasses `isDuplicateResponse`.** The hash for this content was
1004
- * registered by the tick that decided to hold it (the dedup guard's check is
1005
- * also its write), so a re-check here would answer "duplicate" for the reply
1006
- * that has never been saved. That is requirement A of the Issue in one line.
714
+ * @param ctx - What this tick is keyed by
715
+ * @param result - The complete extraction this tick made
716
+ * @param claudeMetadata - Claude's parsed metadata, undefined for every other tool
717
+ * @returns What `checkForResponse` returns for this tick
718
+ */
719
+ async function recheckDuplicateResponse(ctx, result, claudeMetadata) {
720
+ const { db, worktree, worktreeId, cliToolId, instanceId, resolvedInstanceId, pollerKey } = ctx;
721
+ // Issue #1695: this branch used to drop the response silently — the
722
+ // prompt-side guard above has logged its skip since #565, this one
723
+ // logged nothing at all, so a reply that never reached History left no
724
+ // trace anywhere. Same action name shape as its sibling so both skips
725
+ // are found by one grep.
726
+ logger.info('duplicate-response-skipped', { worktreeId, cliToolId, instanceId: resolvedInstanceId });
727
+ (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
728
+ // Issue #2399: the skip above is about the SCREEN, and until this Issue
729
+ // it also ended the tick for the TRANSCRIPT READER 100 lines below —
730
+ // which is the one consumer for whom "the frame has not changed" is not
731
+ // evidence of anything. A pull-mode agent closes its turn in its own
732
+ // file AFTER the pane has gone quiet, so the reader's single ask (on the
733
+ // poll that saved the scrape) is systematically too early, and every
734
+ // later poll returned here. Measured on codex 2026-09-07: one
735
+ // `codex-transcript-turn-open`, `task_complete` appended 1.8 s later,
736
+ // and then `duplicate-response-skipped` every 2 s until
737
+ // `MAX_POLLING_DURATION` ran out. The Markdown row was never written and
738
+ // the only thing left in History was the scrape — for a saturated pane,
739
+ // a single footer line.
740
+ //
741
+ // So the reader is re-asked from inside the skip, throttled by
742
+ // `claimStructuredHistoryRecheck` (once on the first duplicate tick,
743
+ // then every third — see `./response-dedup`). Deliberately the reader
744
+ // and nothing else: the scrape stays suppressed, the cursor has already
745
+ // been advanced above, and none of the bookkeeping the guard skips has a
746
+ // second producer to be asked about.
747
+ //
748
+ // Order over the alternative in the Issue (hoist the reader above the
749
+ // guard): the reader is a WRITE, and hoisting it would run that write on
750
+ // every one of the 900 ticks of a 30-minute cycle instead of on the ones
751
+ // that are owed an answer — the same argument the #2317 Phase D comment
752
+ // below makes for not letting the delegation test short-circuit it.
753
+ //
754
+ // What this does NOT do is retract the scraped row the earlier tick
755
+ // saved. Three reasons, and the first is decisive: nothing here can
756
+ // identify that row. The hash this guard matched is per pollerKey, not
757
+ // per turn — it survives the `resume` of a chain paused on a prompt —
758
+ // so the row it stands for may belong to an earlier turn entirely, and
759
+ // a scraped row carries no turn key to join on. Second, `archived` in
760
+ // this schema is the tombstone of an operator clearing History (#168),
761
+ // written by `archiveMessages` for a whole worktree; reusing it for
762
+ // "superseded" would make a clear and a handover indistinguishable in
763
+ // the table. Third, the scrape is not always junk — when a turn is
764
+ // interrupted the pane holds text the transcript's closed turn does not
765
+ // — and a duplicated row is visible and recoverable where a deleted one
766
+ // is neither. Two rows for one turn is the failure this trades for, and
767
+ // #2401 has already stopped the junk one being picked as a relay's
768
+ // answer.
769
+ //
770
+ // Issue #2436 narrowed what that trade costs, without changing the
771
+ // decision above. The row this skip cannot retract is now only ever one
772
+ // the poller had no reason to hold: a turn whose reader said
773
+ // `not_yet_closed` is held at the save path below rather than written,
774
+ // so on the ordinary codex turn there is no earlier row here to regret.
775
+ // What remains is the case the three reasons above are actually about —
776
+ // a scrape written when the reader could tell us nothing, and a
777
+ // transcript that closed later anyway — and for that the row stays,
778
+ // folded rather than deleted on the chat surface (`ChatMessageBubble`).
779
+ if ((0, response_dedup_1.claimStructuredHistoryRecheck)(pollerKey)) {
780
+ const recaptured = await (0, structured_history_gate_1.captureStructuredHistoryTurn)(worktreeId, cliToolId, instanceId, {
781
+ worktreePath: worktree.path,
782
+ transcriptPathHint: claudeMetadata?.logFilePath ?? null,
783
+ });
784
+ if (recaptured) {
785
+ (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
786
+ // Issue #2436: the turn is now the agent's own Markdown, so a
787
+ // scrape held for it is exactly the second row this Issue exists
788
+ // to stop. Dropped, not written — the only case where dropping a
789
+ // held reply loses nothing.
790
+ (0, pending_scraped_response_1.discardPendingScrapedResponse)(pollerKey);
791
+ logger.info('structured-history-recheck-captured', {
792
+ worktreeId,
793
+ cliToolId,
794
+ instanceId: resolvedInstanceId,
795
+ });
796
+ // The turn IS now in History, as the agent's own Markdown, so this
797
+ // tick recorded something and says so. Inert for the poller either
798
+ // way: `runPollTick` only reads this value after a stop the tick
799
+ // raised itself, and this branch raises none.
800
+ return true;
801
+ }
802
+ }
803
+ return false;
804
+ }
805
+ /**
806
+ * Hold the scraped reply, write it as a message, or drop it.
1007
807
  *
1008
- * Synchronous through the row: `better-sqlite3` is, and this runs on shutdown
1009
- * paths where an awaited continuation may never be reached. The Markdown
1010
- * conversation log is fired afterwards and not waited for, because it is a
1011
- * secondary record and the row is the one History reads.
808
+ * The three-way branch on the save path of {@link checkForResponse}, split out
809
+ * as it was (Issue #3213). Only the branch moved. What decides it — the
810
+ * structured-history gate and the conversation log, both awaited — stays in
811
+ * the caller, {@link recordCompletedResponse}, together with the writes that
812
+ * follow the call (the waiting edge, the push, the cursor): an `await` on a
813
+ * function holding the first without the second would put a yield between the
814
+ * writes here and those, and `onRelayTurnCompleted` starts work that could then
815
+ * run in between.
1012
816
  *
1013
- * @param pollerKey - Poller key ("worktreeId:instanceId")
1014
- * @param reason - What ended the hold; logged, for the operator reading back
1015
- * @returns Whether a held reply was written
817
+ * @param ctx - What this tick is keyed by
818
+ * @param cleanedResponse - The cleaned response
819
+ * @param claudeMetadata - Claude's parsed metadata, undefined for every other tool
820
+ * @param decision - Whether the scrape is held, written or suppressed
1016
821
  */
1017
- function flushPendingScrapedResponse(pollerKey, reason) {
1018
- const pending = pendingScrapedResponses.get(pollerKey);
1019
- if (!pending)
1020
- return false;
1021
- pendingScrapedResponses.delete(pollerKey);
1022
- try {
1023
- const db = (0, db_instance_1.getDbInstance)();
822
+ function recordOrHoldScrapedResponse(ctx, cleanedResponse, claudeMetadata, decision) {
823
+ const { db, worktree, worktreeId, cliToolId, resolvedInstanceId, pollerKey } = ctx;
824
+ const { structuredHistoryLive, suppressScrapedHistory, holdScrapedHistory } = decision;
825
+ // Issue #2041: the one write the structured path replaces. The scraped text
826
+ // is dropped, not saved-and-deduped, because the two renderings of one turn
827
+ // are not byte-comparable — the pane's copy is hard-wrapped at the pane
828
+ // width and gutter-prefixed, so no content check could ever recognise them
829
+ // as the same reply.
830
+ if (holdScrapedHistory) {
831
+ (0, pending_scraped_response_1.holdScrapedResponse)(pollerKey, {
832
+ worktreeId,
833
+ cliToolId,
834
+ instanceId: resolvedInstanceId,
835
+ content: cleanedResponse,
836
+ // The instant the turn was JUDGED finished, not the instant the row is
837
+ // written. History sorts on this, and a row dated at the end of the
838
+ // hold would sort under the NEXT turn's prompt.
839
+ timestamp: new Date(),
840
+ summary: claudeMetadata?.summary,
841
+ logFileName: claudeMetadata?.logFileName,
842
+ requestId: claudeMetadata?.requestId,
843
+ worktreePath: worktree.path,
844
+ transcriptPathHint: claudeMetadata?.logFilePath ?? null,
845
+ expiresAt: Date.now() + pending_scraped_response_1.PENDING_SCRAPE_HOLD_MS,
846
+ });
847
+ logger.info('structured-history-scrape-held', {
848
+ worktreeId,
849
+ cliToolId,
850
+ instanceId: resolvedInstanceId,
851
+ scrapedLength: cleanedResponse.length,
852
+ holdMs: pending_scraped_response_1.PENDING_SCRAPE_HOLD_MS,
853
+ });
854
+ // The completion edge is announced HERE and not at the flush, because the
855
+ // turn finished now. A relay waiting on this session gets #2401's grace
856
+ // window to find a turn-keyed row, which is exactly the row the hold is
857
+ // waiting for; delaying the announcement by the hold would delay every
858
+ // delivery by it too.
859
+ (0, relay_triggers_1.onRelayTurnCompleted)(worktreeId, cliToolId, resolvedInstanceId, false);
860
+ }
861
+ else if (!suppressScrapedHistory) {
862
+ // Create new CLI tool message in database
1024
863
  const message = (0, db_1.createMessage)(db, {
1025
- worktreeId: pending.worktreeId,
864
+ worktreeId,
1026
865
  role: 'assistant',
1027
- content: pending.content,
866
+ content: cleanedResponse,
1028
867
  messageType: 'normal',
1029
- timestamp: pending.timestamp,
1030
- cliToolId: pending.cliToolId,
1031
- instanceId: pending.instanceId,
1032
- summary: pending.summary,
1033
- logFileName: pending.logFileName,
1034
- requestId: pending.requestId,
868
+ timestamp: new Date(),
869
+ cliToolId,
870
+ instanceId: resolvedInstanceId,
871
+ summary: claudeMetadata?.summary,
872
+ logFileName: claudeMetadata?.logFileName,
873
+ requestId: claudeMetadata?.requestId,
1035
874
  });
1036
- (0, ws_server_1.broadcastMessage)('message', { worktreeId: pending.worktreeId, message });
1037
- logger.info('pending-scrape-flushed', {
1038
- worktreeId: pending.worktreeId,
1039
- cliToolId: pending.cliToolId,
1040
- instanceId: pending.instanceId,
1041
- reason,
1042
- scrapedLength: pending.content.length,
1043
- heldForMs: Date.now() - pending.timestamp.getTime(),
1044
- });
1045
- void (0, conversation_logger_1.recordClaudeConversation)(db, pending.worktreeId, pending.content, pending.cliToolId).catch(() => { });
1046
- return true;
875
+ // Broadcast message to WebSocket clients
876
+ (0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
877
+ // Issue #2377: the scrape path's completion edge. `settled: false` is the
878
+ // whole difference from the gate's own announcement: this row was read off
879
+ // a SCREEN whose completion was judged by string analysis, so a relay
880
+ // waiting on this session re-reads after a few seconds of quiet before it
881
+ // delivers — the Issue's 「完了検知 + 数秒の静穏」 for the three tools that
882
+ // keep no transcript. Announced here rather than after the `if` because
883
+ // the suppressed branch means the gate already announced it, settled.
884
+ (0, relay_triggers_1.onRelayTurnCompleted)(worktreeId, cliToolId, resolvedInstanceId, false);
1047
885
  }
1048
- catch (error) {
1049
- logger.warn('pending-scrape-flush-failed', {
1050
- worktreeId: pending.worktreeId,
1051
- cliToolId: pending.cliToolId,
1052
- instanceId: pending.instanceId,
1053
- reason,
1054
- error: error instanceof Error ? error.message : String(error),
886
+ else {
887
+ // Issue #2436: a hold from an earlier tick of this same turn is now moot
888
+ // — the row it was waiting for exists.
889
+ if (structuredHistoryLive)
890
+ (0, pending_scraped_response_1.discardPendingScrapedResponse)(pollerKey);
891
+ logger.info('structured-history-scrape-suppressed', {
892
+ worktreeId,
893
+ cliToolId,
894
+ instanceId: resolvedInstanceId,
895
+ scrapedLength: cleanedResponse.length,
896
+ // Which of the two reasons suppressed it. Without this the Phase D case
897
+ // and the ordinary #2041/#2121 handover are one indistinguishable log
898
+ // line, and "History is missing a turn" has two very different causes.
899
+ reason: structuredHistoryLive ? 'structured-history' : 'geometry-delegated',
1055
900
  });
1056
- return false;
1057
901
  }
1058
902
  }
1059
903
  /**
1060
- * End a hold whose deadline has passed, one way or the other (Issue #2436).
904
+ * Record a finished reply and close the tick: the structured-history gate, the
905
+ * conversation log, the prompt sweep, the scrape, the waiting edge, the push
906
+ * and the cursor.
1061
907
  *
1062
- * Run at the top of every tick, because the tick that has to notice an expiry
1063
- * is very often one that returns early — a static frame yields no new lines for
1064
- * a scrollback tool and is a duplicate for an alternate-screen one, and neither
1065
- * of those paths reaches the save block at the bottom of `checkForResponse`.
908
+ * The rest of {@link checkForResponse} from the structured-history gate to its
909
+ * last return, split out as it was (Issue #3213). One function, tail included,
910
+ * on purpose: the gate and the conversation log are awaited, and everything
911
+ * after them, from the prompt sweep to `stopPolling`, runs without a yield. Cut
912
+ * anywhere short of the last return and the caller's `await` would put one
913
+ * inside that run. Taken whole, the one `await` the split adds comes after
914
+ * every write of the tick, as in {@link recheckDuplicateResponse}. Both returns
915
+ * are the tick's own result.
1066
916
  *
1067
- * The reader is asked ONE more time before the row is written. By the deadline
1068
- * the throttled recheck (#2399) has usually already captured the turn and
1069
- * dropped the hold, but the two are not in step — the recheck is every third
1070
- * duplicate tick and this is a wall clock — and a last read costs one tail
1071
- * parse against writing a pane dump that is about to be superseded.
917
+ * @param ctx - What this tick is keyed by
918
+ * @param turn - The finished turn this tick read off the frame
919
+ * @param claudeMetadata - Claude's parsed metadata, undefined for every other tool
920
+ * @param cleanedResponse - The cleaned response
921
+ * @param delegation - What the geometry-delegation probe answered for this tick
922
+ * @returns What `checkForResponse` returns for this tick
1072
923
  */
1073
- async function settleExpiredPendingScrapedResponse(pollerKey) {
1074
- const pending = pendingScrapedResponses.get(pollerKey);
1075
- if (!pending || Date.now() < pending.expiresAt)
1076
- return;
1077
- const captured = await (0, structured_history_gate_1.captureStructuredHistoryTurn)(pending.worktreeId, pending.cliToolId, pending.instanceId, { worktreePath: pending.worktreePath, transcriptPathHint: pending.transcriptPathHint });
1078
- if (captured) {
1079
- discardPendingScrapedResponse(pollerKey);
1080
- (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
1081
- logger.info('pending-scrape-superseded', {
1082
- worktreeId: pending.worktreeId,
1083
- cliToolId: pending.cliToolId,
1084
- instanceId: pending.instanceId,
1085
- heldForMs: Date.now() - pending.timestamp.getTime(),
1086
- });
1087
- return;
924
+ async function recordCompletedResponse(ctx, turn, claudeMetadata, cleanedResponse, delegation) {
925
+ const { db, worktree, worktreeId, cliToolId, instanceId, resolvedInstanceId, pollerKey } = ctx;
926
+ const { result, isFullScreenTui, lineCountIsCursor } = turn;
927
+ // Issue #2041: opencode's own server is publishing this reply as Markdown
928
+ // over the SSE stream `lib/hooks/sources/opencode/history` is writing from,
929
+ // so the scrape below would be a second copy of the same turn — the agent's
930
+ // text once as it wrote it and once as its TUI drew it, 200 columns wide.
931
+ //
932
+ // Read here rather than at the top of the function on purpose: everything
933
+ // above this line is bookkeeping the event stream has no second producer for
934
+ // (the prompt row, Auto-Yes, the waiting episode, the push fan-out), and the
935
+ // liveness answer is only allowed to suppress the two calls that RECORD THE
936
+ // REPLY. See `./structured-history-gate` for the whole argument.
937
+ //
938
+ // Issue #2121 adds the second shape. Claude has no stream to be live on; it
939
+ // has a transcript file, and this is the moment to read it — the turn is
940
+ // finished (everything above this line established that) and the row is
941
+ // about to be written. `captureStructuredHistoryTurn` writes the agent's own
942
+ // Markdown and answers true, or answers false and leaves the scrape below to
943
+ // be the only record. `||` and not `&&`: the two are different tools'
944
+ // answers to the same question, and each one is false for the other's tool.
945
+ //
946
+ // Issue #2436 adds the third answer. `captureReport.outcome` distinguishes
947
+ // "the agent has not closed this turn yet" from "there is nothing to read
948
+ // here", which the boolean could not: both arrived as `false`, and `false`
949
+ // meant "save the pane's copy". See {@link StructuredHistoryCaptureOutcome}.
950
+ const captureReport = {};
951
+ const structuredHistoryLive = (0, structured_history_gate_1.isStructuredHistoryWriterLive)(worktreeId, cliToolId, instanceId) ||
952
+ (await (0, structured_history_gate_1.captureStructuredHistoryTurn)(worktreeId, cliToolId, instanceId, {
953
+ worktreePath: worktree.path,
954
+ transcriptPathHint: claudeMetadata?.logFilePath ?? null,
955
+ }, captureReport));
956
+ // Issue #2399: remember which way that went, because the next tick may not
957
+ // get here. A `false` is the reader saying "not yet, or not mine", and the
958
+ // dedup guard above turns every following poll of the same static frame into
959
+ // a return — so unless the fact is written down now, the ask never happens
960
+ // again. A `true` settles it: the turn is recorded and there is nothing left
961
+ // to re-ask about.
962
+ markOrSettleStructuredHistoryRecheck(pollerKey, structuredHistoryLive);
963
+ // Issue #2317 Phase D: the scrape is dropped while the geometry is
964
+ // delegated, and the transcript capture above is what makes that safe.
965
+ //
966
+ // ORDER IS LOAD-BEARING. The delegation test is folded in AFTER the two
967
+ // calls above rather than short-circuiting them: `captureStructuredHistoryTurn`
968
+ // is a WRITE, not a query — it is the moment claude's own transcript becomes
969
+ // a History row — so an `||` that put the delegation first would suppress
970
+ // the scrape and the real reply together, and the turn would vanish. Read as
971
+ // it stands: record the turn from the agent's own file, then drop the pane's
972
+ // copy of it, which at 44 rows is a fraction of the answer hard-wrapped at
973
+ // the reader's terminal width.
974
+ //
975
+ // When the transcript capture answers false (an unreadable or absent file)
976
+ // the turn goes UNRECORDED for as long as the delegation lasts. That is the
977
+ // deliberate trade: a missing row is recoverable, a truncated reply saved as
978
+ // the agent's answer is not.
979
+ const suppressScrapedHistory = structuredHistoryLive || delegation.delegated;
980
+ // Issue #2436: not suppressed — HELD. The reader has read the transcript,
981
+ // found the newest turn still open and said so, which means the agent's own
982
+ // Markdown for this turn is on its way. Writing the pane's copy now is what
983
+ // put a 234,323-character dump of prompt echo, intermediate output and
984
+ // footer into History beside the real answer, and #2399 explicitly accepted
985
+ // that trade because the boolean it had could not tell "not yet" from
986
+ // "never". It can now.
987
+ //
988
+ // Everything else this tick does still happens: the cursor advances, the
989
+ // prompts are marked answered, the waiting episode closes, the push goes
990
+ // out. Only the two writes that RECORD THE REPLY wait.
991
+ const holdScrapedHistory = !suppressScrapedHistory && captureReport.outcome === 'not_yet_closed';
992
+ // Create Markdown log file for the conversation pair
993
+ if (cleanedResponse && !suppressScrapedHistory && !holdScrapedHistory) {
994
+ await (0, conversation_logger_1.recordClaudeConversation)(db, worktreeId, cleanedResponse, cliToolId);
1088
995
  }
1089
- flushPendingScrapedResponse(pollerKey, 'hold-expired');
1090
- }
1091
- /**
1092
- * Record what the structured writers said about this turn, for the ticks that
1093
- * will not get to ask (Issue #2399).
1094
- *
1095
- * One line either way, but named because the two calls are a pair and the
1096
- * failure mode of writing only one of them is silent: mark without settle and
1097
- * the reader is re-asked forever after a turn it already recorded; settle
1098
- * without mark and the fix does not exist.
1099
- *
1100
- * @param pollerKey - Poller key ("worktreeId:instanceId")
1101
- * @param recorded - Whether a structured writer owns this turn
1102
- */
1103
- function markOrSettleStructuredHistoryRecheck(pollerKey, recorded) {
1104
- if (recorded) {
1105
- (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
996
+ // Mark any pending prompts as answered. Intended twin of the sweep in
997
+ // extractCompletedTurn (working visible on an unfinished frame); this one runs
998
+ // when a response is recorded (Issue #31).
999
+ const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId, resolvedInstanceId, broadcastPromptSweptToAnswered(worktreeId));
1000
+ if (answeredCount > 0) {
1001
+ logger.info('marked-answeredcount-pending');
1106
1002
  }
1107
- else {
1108
- (0, response_dedup_1.markStructuredHistoryRecheckPending)(pollerKey);
1003
+ // Race condition prevention: re-check session state before saving.
1004
+ // Issue #1268: skipped for alternate-screen tools for the same reason as the
1005
+ // dedup gates above — their line count never grows past the pane height.
1006
+ const currentSessionState = (0, db_1.getSessionState)(db, worktreeId, resolvedInstanceId);
1007
+ if (lineCountIsCursor && currentSessionState && result.lineCount <= currentSessionState.lastCapturedLine) {
1008
+ logger.info('race-condition-detected-skipping-save-re');
1009
+ return false;
1010
+ }
1011
+ recordOrHoldScrapedResponse(ctx, cleanedResponse, claudeMetadata, {
1012
+ structuredHistoryLive,
1013
+ suppressScrapedHistory,
1014
+ holdScrapedHistory,
1015
+ });
1016
+ // Issue #1790: the agent has just produced a reply, so whatever it was
1017
+ // waiting for is over. Closing the episode here is what makes a *second*
1018
+ // prompt in the same session notify again: without it a wait opened by the
1019
+ // prompt branch above could stay open until a browser next probes the status
1020
+ // API, and every later prompt would be folded into that stale episode and
1021
+ // silently deduped. Nothing to close is a no-op, and no notification is
1022
+ // raised for a closing edge.
1023
+ (0, waiting_episode_state_1.observeWaitingEdge)({ worktreeId, cliToolId, instanceId, waiting: false });
1024
+ // Web Push fan-out (Issue #1125): session completed (running → idle).
1025
+ // Fire-and-forget — push is advisory and must never block/break the poller.
1026
+ void (0, push_1.notifyPushSubscribers)({
1027
+ worktreeId,
1028
+ worktreeName: worktree.name,
1029
+ kind: 'completion',
1030
+ agentName: resolvedInstanceId,
1031
+ excerpt: cleanedResponse,
1032
+ }).catch(() => { });
1033
+ // Update session state
1034
+ (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
1035
+ // For full-screen TUIs, stop polling after saving the response.
1036
+ if (isFullScreenTui) {
1037
+ (0, response_poller_core_1.stopPolling)(worktreeId, cliToolId, instanceId);
1109
1038
  }
1039
+ return true;
1110
1040
  }
1111
1041
  /**
1112
1042
  * Check for CLI tool response once
@@ -1129,7 +1059,7 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1129
1059
  // Get worktree to verify it exists
1130
1060
  const worktree = (0, db_1.getWorktreeById)(db, worktreeId);
1131
1061
  if (!worktree) {
1132
- logger.error('worktree-worktreeid-not');
1062
+ logger.error('worktree-not-found');
1133
1063
  (0, response_poller_core_1.stopPolling)(worktreeId, cliToolId, instanceId);
1134
1064
  return false;
1135
1065
  }
@@ -1139,18 +1069,30 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1139
1069
  logger.info('session-not-running');
1140
1070
  // `stopPolling` is what confirms a held scrape here; see
1141
1071
  // `flushPendingScrapedResponse` and `stopPollingByKey`. Requirement B of
1142
- // Issue #2436: this return is ~300 lines above the save path, so a hold
1143
- // released only down there would never be released at all.
1072
+ // Issue #2436: this return comes long before the save path (~300 lines
1073
+ // below when measured; a function of its own today,
1074
+ // `recordCompletedResponse`), so a hold released only down there would
1075
+ // never be released at all.
1144
1076
  (0, response_poller_core_1.stopPolling)(worktreeId, cliToolId, instanceId);
1145
1077
  return false;
1146
1078
  }
1147
1079
  const pollerKey = (0, response_poller_core_1.getPollerKey)(worktreeId, cliToolId, instanceId);
1080
+ // Issue #3213: what the steps split out of this function are keyed by.
1081
+ const ctx = {
1082
+ db,
1083
+ worktree,
1084
+ worktreeId,
1085
+ cliToolId,
1086
+ instanceId,
1087
+ resolvedInstanceId,
1088
+ pollerKey,
1089
+ };
1148
1090
  // Issue #2436: a scrape held by an earlier tick, whose transcript has now
1149
1091
  // had its whole budget to close. Ahead of everything below because most
1150
1092
  // ticks of a finished turn never reach the save path — a static frame is a
1151
1093
  // duplicate for an alternate-screen tool and yields no new lines for a
1152
1094
  // scrollback one, and both of those return early.
1153
- await settleExpiredPendingScrapedResponse(pollerKey);
1095
+ await (0, pending_scraped_response_1.settleExpiredPendingScrapedResponse)(pollerKey);
1154
1096
  // Issue #2317 Phase D: is a human reading this pane at their own terminal
1155
1097
  // size right now, and did they just stop?
1156
1098
  //
@@ -1188,61 +1130,11 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1188
1130
  // taken from the same constant extractResponse() measures saturation against
1189
1131
  // (Issue #1670) — a literal here would silently decouple the two.
1190
1132
  const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, tmux_capture_cache_1.CACHE_MAX_CAPTURE_LINES, instanceId);
1191
- // Issue #2000: the frame is in hand, so this is the cheapest place to ask
1192
- // whether the model API has stalled the session. Level in, edge out — see
1193
- // the helper.
1194
- observeUpstreamFaultForPush(worktreeId, cliToolId, instanceId, output);
1195
- // Layer 2: Accumulate TUI content for full-screen TUI tools, so a turn that
1196
- // outgrows the alternate-screen pane keeps the head that has scrolled away.
1197
- if (cliToolId === 'opencode' || cliToolId === 'copilot') {
1198
- // Issue #1911: opencode is accumulated from the CURRENT TURN'S REGION
1199
- // rather than the whole frame. Feeding the raw pane seeded the accumulator
1200
- // with the previous turn's transcript, the echoed prompt and the bottom
1201
- // chrome on the very first poll, so the accumulated content could never be
1202
- // used as a response source without re-introducing defect 1.
1203
- const accumulatorSource = cliToolId === 'opencode' ? (0, response_extractor_1.sliceOpenCodeTurn)(output) : output;
1204
- (0, tui_accumulator_1.accumulateTuiContent)(pollerKey, accumulatorSource, cliToolId);
1205
- }
1206
- // Extract response
1207
- const result = extractResponse(output, lastCapturedLine, cliToolId, tmux_capture_cache_1.CACHE_MAX_CAPTURE_LINES);
1208
- if (!result || !result.isComplete) {
1209
- // DR-004 windowing: Only check tail lines
1210
- const { thinkingPattern } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
1211
- const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
1212
- const tailLines = cleanOutput.split('\n').slice(-thinking_constants_1.THINKING_TAIL_LINE_COUNT).join('\n');
1213
- if (thinkingPattern.test(tailLines)) {
1214
- const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId, resolvedInstanceId, broadcastPromptSweptToAnswered(worktreeId));
1215
- if (answeredCount > 0) {
1216
- logger.info('marked-answeredcount-pending');
1217
- }
1218
- }
1219
- return false;
1220
- }
1221
- const isFullScreenTui = cliToolId === 'opencode' || cliToolId === 'copilot';
1222
- // Issue #1268: line-count bookkeeping is only meaningful for tools that keep
1223
- // scrollback. Alternate-screen tools (claude since v2, opencode, copilot)
1224
- // always capture exactly pane_height lines, so lastCapturedLine saturates at
1225
- // the pane height on the first save and every later check would see
1226
- // `lineCount <= lastCapturedLine` and drop the response forever — leaving
1227
- // History stuck on "Waiting for response..." while the terminal shows the
1228
- // reply. Those tools dedup on response content instead (see below).
1229
- //
1230
- // Issue #1670: the scrollback tools reach the SAME dead end from the other
1231
- // side. Their buffer does grow — but only until it outgrows the capture
1232
- // window, after which the capture is clipped, the count is pinned at the
1233
- // window size, and the cursor can never be overtaken again. #1268 fixed
1234
- // saturation at pane height; this is saturation at the capture window, and it
1235
- // disables the cursor for exactly as long as the clipping lasts (a session
1236
- // restart or a cleared pane un-saturates it and the cursor comes back).
1237
- const lineCountIsCursor = !(0, types_1.usesAlternateScreen)(cliToolId) && !result.captureWindowSaturated;
1238
- // Duplicate prevention
1239
- if (lineCountIsCursor && !result.bufferReset && result.lineCount === lastCapturedLine && !sessionState?.inProgressMessageId) {
1240
- return false;
1241
- }
1242
- if (lineCountIsCursor && !result.bufferReset && result.lineCount <= lastCapturedLine) {
1243
- logger.info('already-saved-up-to-line-lastcapturedlin');
1133
+ const turn = extractCompletedTurn(ctx, output, lastCapturedLine);
1134
+ if (!turn) {
1244
1135
  return false;
1245
1136
  }
1137
+ const { result, isFullScreenTui, lineCountIsCursor } = turn;
1246
1138
  // Response is complete! Check if it's a prompt.
1247
1139
  const promptDetection = result.promptDetection ?? detectPromptWithOptions(result.response, cliToolId);
1248
1140
  // Issue #2457: the gate is applied HERE as well as in `extractResponse`, and
@@ -1260,100 +1152,9 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1260
1152
  //
1261
1153
  // The frame is the capture this tick made — never `result.response`, which
1262
1154
  // has lost the position `detectDialog` judges by.
1263
- const promptIsLive = promptDetection.isPrompt && isNumberedDialogVouched(cliToolId, promptDetection, output);
1155
+ const promptIsLive = promptDetection.isPrompt && (0, response_checker_dialog_gate_1.isNumberedDialogVouched)(cliToolId, promptDetection, output);
1264
1156
  if (promptIsLive) {
1265
- // Issue #565: Content hash-based duplicate prompt prevention
1266
- const promptContent = promptDetection.rawContent || promptDetection.cleanContent;
1267
- const normalizedForDedup = (0, prompt_dedup_1.normalizePromptForDedup)(promptContent, cliToolId);
1268
- if ((0, prompt_dedup_1.isDuplicatePrompt)(pollerKey, normalizedForDedup)) {
1269
- // Issue #1695: the log line below is invisible to `commandmate capture
1270
- // --json`, so a suppressed prompt and a prompt the detection layer never
1271
- // classified (#1676) look identical from the CLI — both say "nothing was
1272
- // recorded". Count the skip so the payload can tell them apart.
1273
- (0, prompt_dedup_state_1.recordPromptDedupSkip)(worktreeId, cliToolId, instanceId);
1274
- logger.info('duplicate-prompt-skipped', { worktreeId, cliToolId });
1275
- return false;
1276
- }
1277
- // Issue #571: Clean TUI decorations from Copilot prompt content before saving
1278
- let promptSaveContent = promptContent;
1279
- if (cliToolId === 'copilot') {
1280
- promptSaveContent = (0, response_cleaner_1.cleanCopilotResponse)(promptContent);
1281
- promptSaveContent = (0, response_cleaner_1.truncateMessage)(promptSaveContent, copilot_constants_1.COPILOT_MAX_MESSAGE_LENGTH, copilot_constants_1.COPILOT_TRUNCATION_MARKER);
1282
- }
1283
- // This is a prompt - save as prompt message
1284
- (0, db_1.clearInProgressMessageId)(db, worktreeId, cliToolId, resolvedInstanceId);
1285
- const message = (0, db_1.createMessage)(db, {
1286
- worktreeId,
1287
- role: 'assistant',
1288
- content: promptSaveContent,
1289
- messageType: 'prompt',
1290
- promptData: promptDetection.promptData,
1291
- timestamp: new Date(),
1292
- cliToolId,
1293
- instanceId: resolvedInstanceId,
1294
- });
1295
- (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
1296
- (0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
1297
- // Issue #1548: the agent is blocked on input. Raised after the dedup and
1298
- // save above, so the task log counts prompts the system actually recorded
1299
- // rather than every poll that saw the same one still on screen. No-ops
1300
- // when this instance is not running a contract.
1301
- (0, task_transition_service_1.applyEventToActiveTask)(db, worktreeId, cliToolId, resolvedInstanceId, 'prompt_detected', {
1302
- promptType: promptDetection.promptData?.type,
1303
- });
1304
- // Web Push fan-out (Issue #1125): agent is now waiting for a prompt reply.
1305
- // Fire-and-forget — push is advisory and must never block/break the poller.
1306
- //
1307
- // Issue #1790: the wait is now named by #1786's episode rather than by the
1308
- // prompt text. The two lines below are ordered, not incidental:
1309
- //
1310
- // 1. the notification is raised first, while it still has the prompt's
1311
- // own question to quote — it records the episode in the dedup, so
1312
- // whichever path reports the wait second says nothing;
1313
- // 2. `observeWaitingEdge` then opens that same episode, which is what
1314
- // lets the edge listener (and #1788's WebSocket frame) agree with this
1315
- // call about *which* wait this is instead of raising a second one.
1316
- //
1317
- // Both use one timestamp so the episode the notification claims and the
1318
- // episode the store opens are the same number.
1319
- const promptObservedAt = Date.now();
1320
- const promptWaitingSince = (0, waiting_episode_state_1.getWaitingEpisode)(worktreeId, cliToolId, instanceId)?.since ?? promptObservedAt;
1321
- // Issue #1999: Auto-Yes is a declaration that this session's prompts are
1322
- // answered without a human, so notifying for one is telling the reader the
1323
- // opposite of the truth. Only the notification is gated — the episode
1324
- // below still opens, so the WebSocket frame, the status API and the #1790
1325
- // reminder all see the wait exactly as they did before. The gate runs
1326
- // before the call rather than inside it because `shouldSendWaitingPush`
1327
- // records the episode the moment it decides to send.
1328
- if (!(0, prompt_push_gate_1.isPromptPushSuppressed)({
1329
- worktreeId,
1330
- cliToolId,
1331
- instanceId,
1332
- waitingSince: promptWaitingSince,
1333
- })) {
1334
- void (0, push_1.notifyPushSubscribers)({
1335
- worktreeId,
1336
- worktreeName: worktree.name,
1337
- kind: 'prompt',
1338
- agentName: resolvedInstanceId,
1339
- instanceId: resolvedInstanceId,
1340
- waitingKind: 'prompt',
1341
- waitingSince: promptWaitingSince,
1342
- excerpt: promptDetection.promptData?.question ?? promptSaveContent,
1343
- }).catch(() => { });
1344
- }
1345
- (0, waiting_episode_state_1.observeWaitingEdge)({
1346
- worktreeId,
1347
- cliToolId,
1348
- instanceId,
1349
- waiting: true,
1350
- kind: 'prompt',
1351
- now: promptObservedAt,
1352
- });
1353
- if (!isFullScreenTui) {
1354
- (0, response_poller_core_1.stopPolling)(worktreeId, cliToolId, instanceId);
1355
- }
1356
- return true;
1157
+ return savePromptMessage(ctx, promptDetection, result, isFullScreenTui);
1357
1158
  }
1358
1159
  // Validate response content is not empty
1359
1160
  if (!result.response || result.response.trim() === '') {
@@ -1364,43 +1165,7 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1364
1165
  const claudeMetadata = cliToolId === 'claude'
1365
1166
  ? (0, claude_output_1.parseClaudeOutput)(result.response)
1366
1167
  : undefined;
1367
- // Clean up responses
1368
- let cleanedResponse = result.response;
1369
- if (cliToolId === 'gemini') {
1370
- cleanedResponse = (0, response_cleaner_1.cleanGeminiResponse)(result.response);
1371
- }
1372
- else if (cliToolId === 'claude') {
1373
- cleanedResponse = (0, response_cleaner_1.cleanClaudeResponse)(result.response);
1374
- }
1375
- else if (cliToolId === 'copilot') {
1376
- const accumulatedContent = (0, tui_accumulator_1.getAccumulatedContent)(pollerKey);
1377
- const sourceContent = accumulatedContent || result.response;
1378
- cleanedResponse = (0, response_cleaner_1.cleanCopilotResponse)(sourceContent);
1379
- cleanedResponse = (0, response_cleaner_1.truncateMessage)(cleanedResponse, copilot_constants_1.COPILOT_MAX_MESSAGE_LENGTH, copilot_constants_1.COPILOT_TRUNCATION_MARKER);
1380
- (0, tui_accumulator_1.clearTuiAccumulator)(pollerKey);
1381
- }
1382
- else if (cliToolId === 'opencode') {
1383
- // Issue #1911 defect 3: opencode wrote to the Layer-2 accumulator but never
1384
- // read it, so any turn longer than the pane was saved without its head.
1385
- //
1386
- // Read it only when the head is ACTUALLY gone, which is what
1387
- // `turnHeadTruncated` measures — deliberately NOT copilot's unconditional
1388
- // `accumulated || response`. The accumulator appends whatever the overlap
1389
- // check cannot match against the previous poll, and opencode rewrites rows
1390
- // in place while it works (`+ Thought: … · 12ms` becomes `· 579ms`, a
1391
- // pending patch row becomes the applied edit). Every such rewrite breaks
1392
- // the overlap and re-appends the lines above it, so preferring the
1393
- // accumulator for the common short answer would duplicate content that
1394
- // `result.response` already holds exactly. When the echo is off screen the
1395
- // frame is missing content outright, and a possible duplicate beats a
1396
- // guaranteed truncation.
1397
- const accumulatedContent = (0, tui_accumulator_1.getAccumulatedContent)(pollerKey);
1398
- const sourceContent = result.turnHeadTruncated && accumulatedContent
1399
- ? accumulatedContent
1400
- : result.response;
1401
- cleanedResponse = (0, response_cleaner_1.cleanOpenCodeResponse)(sourceContent);
1402
- (0, tui_accumulator_1.clearTuiAccumulator)(pollerKey);
1403
- }
1168
+ const cleanedResponse = cleanCompletedResponse(cliToolId, result, pollerKey);
1404
1169
  // If cleaned response is empty or just "[No content]", skip saving
1405
1170
  if (!cleanedResponse || cleanedResponse.trim() === '' || cleanedResponse === '[No content]') {
1406
1171
  (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
@@ -1419,274 +1184,13 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1419
1184
  // the poller would append the same finished reply every 2 s.
1420
1185
  if (!lineCountIsCursor) {
1421
1186
  if ((0, response_dedup_1.isDuplicateResponse)(pollerKey, cleanedResponse)) {
1422
- // Issue #1695: this branch used to drop the response silently — the
1423
- // prompt-side guard above has logged its skip since #565, this one
1424
- // logged nothing at all, so a reply that never reached History left no
1425
- // trace anywhere. Same action name shape as its sibling so both skips
1426
- // are found by one grep.
1427
- logger.info('duplicate-response-skipped', { worktreeId, cliToolId, instanceId: resolvedInstanceId });
1428
- (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
1429
- // Issue #2399: the skip above is about the SCREEN, and until this Issue
1430
- // it also ended the tick for the TRANSCRIPT READER 100 lines below —
1431
- // which is the one consumer for whom "the frame has not changed" is not
1432
- // evidence of anything. A pull-mode agent closes its turn in its own
1433
- // file AFTER the pane has gone quiet, so the reader's single ask (on the
1434
- // poll that saved the scrape) is systematically too early, and every
1435
- // later poll returned here. Measured on codex 2026-09-07: one
1436
- // `codex-transcript-turn-open`, `task_complete` appended 1.8 s later,
1437
- // and then `duplicate-response-skipped` every 2 s until
1438
- // `MAX_POLLING_DURATION` ran out. The Markdown row was never written and
1439
- // the only thing left in History was the scrape — for a saturated pane,
1440
- // a single footer line.
1441
- //
1442
- // So the reader is re-asked from inside the skip, throttled by
1443
- // `claimStructuredHistoryRecheck` (once on the first duplicate tick,
1444
- // then every third — see `./response-dedup`). Deliberately the reader
1445
- // and nothing else: the scrape stays suppressed, the cursor has already
1446
- // been advanced above, and none of the bookkeeping the guard skips has a
1447
- // second producer to be asked about.
1448
- //
1449
- // Order over the alternative in the Issue (hoist the reader above the
1450
- // guard): the reader is a WRITE, and hoisting it would run that write on
1451
- // every one of the 900 ticks of a 30-minute cycle instead of on the ones
1452
- // that are owed an answer — the same argument the #2317 Phase D comment
1453
- // below makes for not letting the delegation test short-circuit it.
1454
- //
1455
- // What this does NOT do is retract the scraped row the earlier tick
1456
- // saved. Three reasons, and the first is decisive: nothing here can
1457
- // identify that row. The hash this guard matched is per pollerKey, not
1458
- // per turn — it survives the `resume` of a chain paused on a prompt —
1459
- // so the row it stands for may belong to an earlier turn entirely, and
1460
- // a scraped row carries no turn key to join on. Second, `archived` in
1461
- // this schema is the tombstone of an operator clearing History (#168),
1462
- // written by `archiveMessages` for a whole worktree; reusing it for
1463
- // "superseded" would make a clear and a handover indistinguishable in
1464
- // the table. Third, the scrape is not always junk — when a turn is
1465
- // interrupted the pane holds text the transcript's closed turn does not
1466
- // — and a duplicated row is visible and recoverable where a deleted one
1467
- // is neither. Two rows for one turn is the failure this trades for, and
1468
- // #2401 has already stopped the junk one being picked as a relay's
1469
- // answer.
1470
- //
1471
- // Issue #2436 narrowed what that trade costs, without changing the
1472
- // decision above. The row this skip cannot retract is now only ever one
1473
- // the poller had no reason to hold: a turn whose reader said
1474
- // `not_yet_closed` is held at the save path below rather than written,
1475
- // so on the ordinary codex turn there is no earlier row here to regret.
1476
- // What remains is the case the three reasons above are actually about —
1477
- // a scrape written when the reader could tell us nothing, and a
1478
- // transcript that closed later anyway — and for that the row stays,
1479
- // folded rather than deleted on the chat surface (`ChatMessageBubble`).
1480
- if ((0, response_dedup_1.claimStructuredHistoryRecheck)(pollerKey)) {
1481
- const recaptured = await (0, structured_history_gate_1.captureStructuredHistoryTurn)(worktreeId, cliToolId, instanceId, {
1482
- worktreePath: worktree.path,
1483
- transcriptPathHint: claudeMetadata?.logFilePath ?? null,
1484
- });
1485
- if (recaptured) {
1486
- (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
1487
- // Issue #2436: the turn is now the agent's own Markdown, so a
1488
- // scrape held for it is exactly the second row this Issue exists
1489
- // to stop. Dropped, not written — the only case where dropping a
1490
- // held reply loses nothing.
1491
- discardPendingScrapedResponse(pollerKey);
1492
- logger.info('structured-history-recheck-captured', {
1493
- worktreeId,
1494
- cliToolId,
1495
- instanceId: resolvedInstanceId,
1496
- });
1497
- // The turn IS now in History, as the agent's own Markdown, so this
1498
- // tick recorded something and says so. Inert for the poller either
1499
- // way: `runPollTick` only reads this value after a stop the tick
1500
- // raised itself, and this branch raises none.
1501
- return true;
1502
- }
1503
- }
1504
- return false;
1187
+ // `await`, so that a rejection still lands in the catch below.
1188
+ return await recheckDuplicateResponse(ctx, result, claudeMetadata);
1505
1189
  }
1506
1190
  }
1507
- // Issue #2041: opencode's own server is publishing this reply as Markdown
1508
- // over the SSE stream `lib/hooks/sources/opencode/history` is writing from,
1509
- // so the scrape below would be a second copy of the same turn — the agent's
1510
- // text once as it wrote it and once as its TUI drew it, 200 columns wide.
1511
- //
1512
- // Read here rather than at the top of the function on purpose: everything
1513
- // above this line is bookkeeping the event stream has no second producer for
1514
- // (the prompt row, Auto-Yes, the waiting episode, the push fan-out), and the
1515
- // liveness answer is only allowed to suppress the two calls that RECORD THE
1516
- // REPLY. See `./structured-history-gate` for the whole argument.
1517
- //
1518
- // Issue #2121 adds the second shape. Claude has no stream to be live on; it
1519
- // has a transcript file, and this is the moment to read it — the turn is
1520
- // finished (everything above this line established that) and the row is
1521
- // about to be written. `captureStructuredHistoryTurn` writes the agent's own
1522
- // Markdown and answers true, or answers false and leaves the scrape below to
1523
- // be the only record. `||` and not `&&`: the two are different tools'
1524
- // answers to the same question, and each one is false for the other's tool.
1525
- //
1526
- // Issue #2436 adds the third answer. `captureReport.outcome` distinguishes
1527
- // "the agent has not closed this turn yet" from "there is nothing to read
1528
- // here", which the boolean could not: both arrived as `false`, and `false`
1529
- // meant "save the pane's copy". See {@link StructuredHistoryCaptureOutcome}.
1530
- const captureReport = {};
1531
- const structuredHistoryLive = (0, structured_history_gate_1.isStructuredHistoryWriterLive)(worktreeId, cliToolId, instanceId) ||
1532
- (await (0, structured_history_gate_1.captureStructuredHistoryTurn)(worktreeId, cliToolId, instanceId, {
1533
- worktreePath: worktree.path,
1534
- transcriptPathHint: claudeMetadata?.logFilePath ?? null,
1535
- }, captureReport));
1536
- // Issue #2399: remember which way that went, because the next tick may not
1537
- // get here. A `false` is the reader saying "not yet, or not mine", and the
1538
- // dedup guard above turns every following poll of the same static frame into
1539
- // a return — so unless the fact is written down now, the ask never happens
1540
- // again. A `true` settles it: the turn is recorded and there is nothing left
1541
- // to re-ask about.
1542
- markOrSettleStructuredHistoryRecheck(pollerKey, structuredHistoryLive);
1543
- // Issue #2317 Phase D: the scrape is dropped while the geometry is
1544
- // delegated, and the transcript capture above is what makes that safe.
1545
- //
1546
- // ORDER IS LOAD-BEARING. The delegation test is folded in AFTER the two
1547
- // calls above rather than short-circuiting them: `captureStructuredHistoryTurn`
1548
- // is a WRITE, not a query — it is the moment claude's own transcript becomes
1549
- // a History row — so an `||` that put the delegation first would suppress
1550
- // the scrape and the real reply together, and the turn would vanish. Read as
1551
- // it stands: record the turn from the agent's own file, then drop the pane's
1552
- // copy of it, which at 44 rows is a fraction of the answer hard-wrapped at
1553
- // the reader's terminal width.
1554
- //
1555
- // When the transcript capture answers false (an unreadable or absent file)
1556
- // the turn goes UNRECORDED for as long as the delegation lasts. That is the
1557
- // deliberate trade: a missing row is recoverable, a truncated reply saved as
1558
- // the agent's answer is not.
1559
- const suppressScrapedHistory = structuredHistoryLive || delegation.delegated;
1560
- // Issue #2436: not suppressed — HELD. The reader has read the transcript,
1561
- // found the newest turn still open and said so, which means the agent's own
1562
- // Markdown for this turn is on its way. Writing the pane's copy now is what
1563
- // put a 234,323-character dump of prompt echo, intermediate output and
1564
- // footer into History beside the real answer, and #2399 explicitly accepted
1565
- // that trade because the boolean it had could not tell "not yet" from
1566
- // "never". It can now.
1567
- //
1568
- // Everything else this tick does still happens: the cursor advances, the
1569
- // prompts are marked answered, the waiting episode closes, the push goes
1570
- // out. Only the two writes that RECORD THE REPLY wait.
1571
- const holdScrapedHistory = !suppressScrapedHistory && captureReport.outcome === 'not_yet_closed';
1572
- // Create Markdown log file for the conversation pair
1573
- if (cleanedResponse && !suppressScrapedHistory && !holdScrapedHistory) {
1574
- await (0, conversation_logger_1.recordClaudeConversation)(db, worktreeId, cleanedResponse, cliToolId);
1575
- }
1576
- // Mark any pending prompts as answered
1577
- const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId, resolvedInstanceId, broadcastPromptSweptToAnswered(worktreeId));
1578
- if (answeredCount > 0) {
1579
- logger.info('marked-answeredcount-pending');
1580
- }
1581
- // Race condition prevention: re-check session state before saving.
1582
- // Issue #1268: skipped for alternate-screen tools for the same reason as the
1583
- // dedup gates above — their line count never grows past the pane height.
1584
- const currentSessionState = (0, db_1.getSessionState)(db, worktreeId, resolvedInstanceId);
1585
- if (lineCountIsCursor && currentSessionState && result.lineCount <= currentSessionState.lastCapturedLine) {
1586
- logger.info('race-condition-detected-skipping-save-re');
1587
- return false;
1588
- }
1589
- // Issue #2041: the one write the structured path replaces. The scraped text
1590
- // is dropped, not saved-and-deduped, because the two renderings of one turn
1591
- // are not byte-comparable — the pane's copy is hard-wrapped at the pane
1592
- // width and gutter-prefixed, so no content check could ever recognise them
1593
- // as the same reply.
1594
- if (holdScrapedHistory) {
1595
- holdScrapedResponse(pollerKey, {
1596
- worktreeId,
1597
- cliToolId,
1598
- instanceId: resolvedInstanceId,
1599
- content: cleanedResponse,
1600
- // The instant the turn was JUDGED finished, not the instant the row is
1601
- // written. History sorts on this, and a row dated at the end of the
1602
- // hold would sort under the NEXT turn's prompt.
1603
- timestamp: new Date(),
1604
- summary: claudeMetadata?.summary,
1605
- logFileName: claudeMetadata?.logFileName,
1606
- requestId: claudeMetadata?.requestId,
1607
- worktreePath: worktree.path,
1608
- transcriptPathHint: claudeMetadata?.logFilePath ?? null,
1609
- expiresAt: Date.now() + exports.PENDING_SCRAPE_HOLD_MS,
1610
- });
1611
- logger.info('structured-history-scrape-held', {
1612
- worktreeId,
1613
- cliToolId,
1614
- instanceId: resolvedInstanceId,
1615
- scrapedLength: cleanedResponse.length,
1616
- holdMs: exports.PENDING_SCRAPE_HOLD_MS,
1617
- });
1618
- // The completion edge is announced HERE and not at the flush, because the
1619
- // turn finished now. A relay waiting on this session gets #2401's grace
1620
- // window to find a turn-keyed row, which is exactly the row the hold is
1621
- // waiting for; delaying the announcement by the hold would delay every
1622
- // delivery by it too.
1623
- (0, relay_triggers_1.onRelayTurnCompleted)(worktreeId, cliToolId, resolvedInstanceId, false);
1624
- }
1625
- else if (!suppressScrapedHistory) {
1626
- // Create new CLI tool message in database
1627
- const message = (0, db_1.createMessage)(db, {
1628
- worktreeId,
1629
- role: 'assistant',
1630
- content: cleanedResponse,
1631
- messageType: 'normal',
1632
- timestamp: new Date(),
1633
- cliToolId,
1634
- instanceId: resolvedInstanceId,
1635
- summary: claudeMetadata?.summary,
1636
- logFileName: claudeMetadata?.logFileName,
1637
- requestId: claudeMetadata?.requestId,
1638
- });
1639
- // Broadcast message to WebSocket clients
1640
- (0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
1641
- // Issue #2377: the scrape path's completion edge. `settled: false` is the
1642
- // whole difference from the gate's own announcement: this row was read off
1643
- // a SCREEN whose completion was judged by string analysis, so a relay
1644
- // waiting on this session re-reads after a few seconds of quiet before it
1645
- // delivers — the Issue's 「完了検知 + 数秒の静穏」 for the three tools that
1646
- // keep no transcript. Announced here rather than after the `if` because
1647
- // the suppressed branch means the gate already announced it, settled.
1648
- (0, relay_triggers_1.onRelayTurnCompleted)(worktreeId, cliToolId, resolvedInstanceId, false);
1649
- }
1650
- else {
1651
- // Issue #2436: a hold from an earlier tick of this same turn is now moot
1652
- // — the row it was waiting for exists.
1653
- if (structuredHistoryLive)
1654
- discardPendingScrapedResponse(pollerKey);
1655
- logger.info('structured-history-scrape-suppressed', {
1656
- worktreeId,
1657
- cliToolId,
1658
- instanceId: resolvedInstanceId,
1659
- scrapedLength: cleanedResponse.length,
1660
- // Which of the two reasons suppressed it. Without this the Phase D case
1661
- // and the ordinary #2041/#2121 handover are one indistinguishable log
1662
- // line, and "History is missing a turn" has two very different causes.
1663
- reason: structuredHistoryLive ? 'structured-history' : 'geometry-delegated',
1664
- });
1665
- }
1666
- // Issue #1790: the agent has just produced a reply, so whatever it was
1667
- // waiting for is over. Closing the episode here is what makes a *second*
1668
- // prompt in the same session notify again: without it a wait opened by the
1669
- // prompt branch above could stay open until a browser next probes the status
1670
- // API, and every later prompt would be folded into that stale episode and
1671
- // silently deduped. Nothing to close is a no-op, and no notification is
1672
- // raised for a closing edge.
1673
- (0, waiting_episode_state_1.observeWaitingEdge)({ worktreeId, cliToolId, instanceId, waiting: false });
1674
- // Web Push fan-out (Issue #1125): session completed (running → idle).
1675
- // Fire-and-forget — push is advisory and must never block/break the poller.
1676
- void (0, push_1.notifyPushSubscribers)({
1677
- worktreeId,
1678
- worktreeName: worktree.name,
1679
- kind: 'completion',
1680
- agentName: resolvedInstanceId,
1681
- excerpt: cleanedResponse,
1682
- }).catch(() => { });
1683
- // Update session state
1684
- (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
1685
- // For full-screen TUIs, stop polling after saving the response.
1686
- if (isFullScreenTui) {
1687
- (0, response_poller_core_1.stopPolling)(worktreeId, cliToolId, instanceId);
1688
- }
1689
- return true;
1191
+ // The rest of the tick, to its last return. `await` for the same reason as
1192
+ // above.
1193
+ return await recordCompletedResponse(ctx, turn, claudeMetadata, cleanedResponse, delegation);
1690
1194
  }
1691
1195
  catch (error) {
1692
1196
  logger.error('response:check-failed', { error: error instanceof Error ? error.message : String(error) });