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