commandmate 0.32.0 → 0.33.0

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 (212) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +159 -159
  3. package/.next/app-path-routes-manifest.json +28 -28
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/prerender-manifest.json +12 -12
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/api/agents/update/route_client-reference-manifest.js +1 -1
  9. package/.next/server/app/api/agents/versions/route_client-reference-manifest.js +1 -1
  10. package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
  12. package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/auth/status/route_client-reference-manifest.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_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/hooks/agent-event/route.js +1 -1
  31. package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/hooks/claude-done/route.js +2 -2
  33. package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/hooks/permission-request/route.js +1 -1
  35. package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/relays/[relayId]/cancel/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/relays/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/remote/pair/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/settings/default-agents/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/settings/default-surface-mode/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
  58. package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
  61. package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/slash-commands/route.js.nft.json +1 -1
  64. package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  67. package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
  70. package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
  73. package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/worktrees/[id]/clear-composer/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/worktrees/[id]/cli-reference/route_client-reference-manifest.js +1 -1
  76. package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/worktrees/[id]/env/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
  82. package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
  84. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
  87. package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
  88. package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
  90. package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
  94. package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
  100. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
  105. package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
  106. package/.next/server/app/api/worktrees/[id]/instances/opencode/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
  109. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
  111. package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
  112. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
  114. package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
  115. package/.next/server/app/api/worktrees/[id]/opencode/diff/route_client-reference-manifest.js +1 -1
  116. package/.next/server/app/api/worktrees/[id]/opencode/session/route_client-reference-manifest.js +1 -1
  117. package/.next/server/app/api/worktrees/[id]/opencode/share/route_client-reference-manifest.js +1 -1
  118. package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
  119. package/.next/server/app/api/worktrees/[id]/resolve-target/route_client-reference-manifest.js +1 -1
  120. package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
  121. package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
  123. package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
  124. package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
  125. package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
  126. package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
  127. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
  128. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
  129. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
  130. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
  131. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
  132. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
  133. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
  134. package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
  135. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js.nft.json +1 -1
  136. package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
  137. package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
  138. package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
  139. package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
  140. package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
  141. package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
  142. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  143. package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
  144. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
  145. package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
  146. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
  147. package/.next/server/app/api/worktrees/[id]/verify/config/route_client-reference-manifest.js +1 -1
  148. package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
  149. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route_client-reference-manifest.js +1 -1
  150. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
  151. package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
  152. package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
  153. package/.next/server/app/api/worktrees/apply-default-agents/route_client-reference-manifest.js +1 -1
  154. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  155. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  156. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  157. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  158. package/.next/server/app/more/page_client-reference-manifest.js +1 -1
  159. package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  160. package/.next/server/app/page_client-reference-manifest.js +1 -1
  161. package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
  162. package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
  163. package/.next/server/app/review/page_client-reference-manifest.js +1 -1
  164. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  165. package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
  166. package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
  167. package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  168. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  169. package/.next/server/app/worktrees/[id]/page.js +11 -11
  170. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  171. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  172. package/.next/server/app-paths-manifest.json +28 -28
  173. package/.next/server/chunks/1210.js +1 -1
  174. package/.next/server/chunks/5043.js +1 -1
  175. package/.next/server/chunks/5164.js +1 -1
  176. package/.next/server/chunks/6491.js +2 -2
  177. package/.next/server/chunks/6838.js +1 -1
  178. package/.next/server/chunks/7875.js +2 -2
  179. package/.next/server/chunks/8918.js +4 -4
  180. package/.next/server/chunks/9130.js +1 -1
  181. package/.next/server/middleware-manifest.json +5 -5
  182. package/.next/server/pages/500.html +1 -1
  183. package/.next/server/server-reference-manifest.json +1 -1
  184. package/.next/static/chunks/{4718-0053d42fb635e700.js → 4718-d97f52cd932dc1ee.js} +1 -1
  185. package/.next/static/chunks/app/worktrees/[id]/page-18a7d9374bd8c218.js +1 -0
  186. package/.next/static/css/{96d94a4ba771efc3.css → fb455b4e308adbaa.css} +1 -1
  187. package/.next/trace +13 -13
  188. package/README.md +6 -2
  189. package/dist/cli/commands/instances.d.ts.map +1 -1
  190. package/dist/cli/commands/instances.js +44 -15
  191. package/dist/cli/commands/peers.d.ts.map +1 -1
  192. package/dist/cli/commands/peers.js +12 -0
  193. package/dist/cli/commands/whoami.d.ts +67 -0
  194. package/dist/cli/commands/whoami.d.ts.map +1 -1
  195. package/dist/cli/commands/whoami.js +106 -0
  196. package/dist/cli/utils/api-client.d.ts +30 -1
  197. package/dist/cli/utils/api-client.d.ts.map +1 -1
  198. package/dist/cli/utils/api-client.js +28 -5
  199. package/dist/server/src/lib/detection/cli-patterns.js +206 -2
  200. package/dist/server/src/lib/hooks/agent-event-service.js +27 -5
  201. package/dist/server/src/lib/hooks/sources/codex/history.js +63 -3
  202. package/dist/server/src/lib/hooks/sources/codex/transcript.js +140 -6
  203. package/dist/server/src/lib/hooks/sources/launch-command.js +112 -15
  204. package/dist/server/src/lib/hooks/sources/turn-body.js +3 -1
  205. package/dist/server/src/lib/hooks/stop-history-capture.js +252 -50
  206. package/dist/server/src/lib/polling/response-checker.js +129 -5
  207. package/dist/server/src/lib/polling/response-dedup.js +136 -0
  208. package/dist/server/src/lib/relay/relay-delivery.js +160 -6
  209. package/package.json +1 -1
  210. package/.next/static/chunks/app/worktrees/[id]/page-06e1ff5ce2d126fd.js +0 -1
  211. /package/.next/static/{UIUPchtWvcEuQgcbGJv4g → eVkZd6vpcQwyJIlbymCUg}/_buildManifest.js +0 -0
  212. /package/.next/static/{UIUPchtWvcEuQgcbGJv4g → eVkZd6vpcQwyJIlbymCUg}/_ssgManifest.js +0 -0
@@ -303,15 +303,26 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
303
303
  // that placeholder is drawn with the same `❯ <text>` shape as a transcript
304
304
  // echo, so without the boundary `findRecentUserPromptIndex` anchors the turn
305
305
  // on the FOOTER and every reply extracts as empty (#1289's defect, verbatim).
306
+ //
307
+ // Issue #2400: codex is the fifth, and the one that had been missing. It pins
308
+ // the same two rows — `› Ask Codex to do anything` and the `model · cwd`
309
+ // status bar — and without a boundary the saturated-window anchor (#1670)
310
+ // walked into them: the newest `›` in the pane was the COMPOSER, so extraction
311
+ // started on the status bar and every reply on a saturated pane was saved as
312
+ // that one row. `findCodexChromeStart` reads the composer by its SGR
313
+ // attributes (#2310) rather than by its placeholder wording, which is what the
314
+ // previous guard did and why it stopped working at codex 0.15x.
306
315
  const chromeStart = cliToolId === 'claude'
307
316
  ? (0, cli_patterns_1.findClaudeChromeStart)(lines)
308
317
  : cliToolId === 'copilot'
309
318
  ? (0, cli_patterns_1.findCopilotChromeStart)(lines)
310
319
  : cliToolId === 'command-code'
311
320
  ? (0, cli_patterns_1.findCommandCodeChromeStart)(lines)
312
- : openCodeCleanLines
313
- ? (0, cli_patterns_1.findOpenCodeChromeStart)(openCodeCleanLines)
314
- : -1;
321
+ : cliToolId === 'codex'
322
+ ? (0, cli_patterns_1.findCodexChromeStart)(lines)
323
+ : openCodeCleanLines
324
+ ? (0, cli_patterns_1.findOpenCodeChromeStart)(openCodeCleanLines)
325
+ : -1;
315
326
  const contentEnd = chromeStart >= 0 ? chromeStart : totalLines;
316
327
  const BUFFER_RESET_TOLERANCE = 25;
317
328
  const bufferShrank = totalLines > 0 && lastCapturedLine > BUFFER_RESET_TOLERANCE && (totalLines + BUFFER_RESET_TOLERANCE) < lastCapturedLine;
@@ -333,7 +344,22 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
333
344
  const findRecentUserPromptIndex = (windowSize = 60) => {
334
345
  let userPromptPattern;
335
346
  if (cliToolId === 'codex') {
336
- userPromptPattern = /^›\s+(?!Implement|Find and fix|Type|Summarize)/;
347
+ // Issue #2400: codex's three uses of `›` are separated by their SGR
348
+ // attributes, not by their text (#2310). This branch used to exclude the
349
+ // composer with a negative lookahead over its placeholder strings
350
+ // (`Implement`, `Find and fix`, `Type`, `Summarize`) — codex 0.1x wording,
351
+ // none of which 0.15x draws. `Ask Codex to do anything` passed the guard,
352
+ // became the newest "echo", and on a saturated pane (#1670) — the only
353
+ // path where this anchor decides where extraction STARTS — every reply was
354
+ // saved as the single status-bar row below it.
355
+ //
356
+ // Two independent things now keep the composer out, and the reader needs
357
+ // both because neither covers the other's frames: `contentEnd` cuts the
358
+ // composer off structurally when `findCodexChromeStart` located it, and
359
+ // when it did not, `findCodexUserEchoIndex` steps over the bottom-most
360
+ // `›` row instead. The second is what still answers on an ANSI-stripped
361
+ // capture, where none of #2310's attributes survive to be read.
362
+ return (0, cli_patterns_1.findCodexUserEchoIndex)(lines, contentEnd, windowSize, chromeStart >= 0);
337
363
  }
338
364
  else if (openCodeCleanLines) {
339
365
  // Issue #1911: anchor on the newest ECHOED USER PROMPT, not on the
@@ -453,9 +479,20 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
453
479
  if (isPromptBasedComplete || isClaudeComplete || isCommandCodeComplete || isOpenCodeDone) {
454
480
  const responseLines = [];
455
481
  const startIndex = (0, response_extractor_1.resolveExtractionStartIndex)(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, captureWindowSaturated);
456
- let endIndex = totalLines;
457
482
  // `contentEnd` bounds the content only; `endIndex` keeps reporting the full
458
483
  // buffer so lineCount bookkeeping in session_states is unchanged (#1289).
484
+ //
485
+ // Issue #2400: codex is the exception, and it is the pre-existing behaviour
486
+ // rather than a new rule. Before this Issue the loop below stopped on the
487
+ // composer's `›` and wrote that row's index into `endIndex`; now the composer
488
+ // is outside `contentEnd`, so the break can no longer fire on it and
489
+ // `endIndex` would silently advance ~3 rows further. Those rows matter for
490
+ // codex specifically: it renders INLINE, and it repaints the composer band in
491
+ // place — the next turn's transcript is printed over exactly the rows the
492
+ // composer occupied in this capture. A cursor parked past them would skip
493
+ // real content on the following poll. So the cursor stops where the content
494
+ // stops, which is what it did before.
495
+ let endIndex = cliToolId === 'codex' ? contentEnd : totalLines;
459
496
  for (let i = startIndex; i < contentEnd; i++) {
460
497
  const line = lines[i];
461
498
  const cleanLine = (0, cli_patterns_1.stripAnsi)(line);
@@ -698,6 +735,26 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
698
735
  // ============================================================================
699
736
  // checkForResponse (exported for response-poller-core.ts)
700
737
  // ============================================================================
738
+ /**
739
+ * Record what the structured writers said about this turn, for the ticks that
740
+ * will not get to ask (Issue #2399).
741
+ *
742
+ * One line either way, but named because the two calls are a pair and the
743
+ * failure mode of writing only one of them is silent: mark without settle and
744
+ * the reader is re-asked forever after a turn it already recorded; settle
745
+ * without mark and the fix does not exist.
746
+ *
747
+ * @param pollerKey - Poller key ("worktreeId:instanceId")
748
+ * @param recorded - Whether a structured writer owns this turn
749
+ */
750
+ function markOrSettleStructuredHistoryRecheck(pollerKey, recorded) {
751
+ if (recorded) {
752
+ (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
753
+ }
754
+ else {
755
+ (0, response_dedup_1.markStructuredHistoryRecheckPending)(pollerKey);
756
+ }
757
+ }
701
758
  /**
702
759
  * Check for CLI tool response once
703
760
  *
@@ -994,6 +1051,66 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
994
1051
  // are found by one grep.
995
1052
  logger.info('duplicate-response-skipped', { worktreeId, cliToolId, instanceId: resolvedInstanceId });
996
1053
  (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
1054
+ // Issue #2399: the skip above is about the SCREEN, and until this Issue
1055
+ // it also ended the tick for the TRANSCRIPT READER 100 lines below —
1056
+ // which is the one consumer for whom "the frame has not changed" is not
1057
+ // evidence of anything. A pull-mode agent closes its turn in its own
1058
+ // file AFTER the pane has gone quiet, so the reader's single ask (on the
1059
+ // poll that saved the scrape) is systematically too early, and every
1060
+ // later poll returned here. Measured on codex 2026-09-07: one
1061
+ // `codex-transcript-turn-open`, `task_complete` appended 1.8 s later,
1062
+ // and then `duplicate-response-skipped` every 2 s until
1063
+ // `MAX_POLLING_DURATION` ran out. The Markdown row was never written and
1064
+ // the only thing left in History was the scrape — for a saturated pane,
1065
+ // a single footer line.
1066
+ //
1067
+ // So the reader is re-asked from inside the skip, throttled by
1068
+ // `claimStructuredHistoryRecheck` (once on the first duplicate tick,
1069
+ // then every third — see `./response-dedup`). Deliberately the reader
1070
+ // and nothing else: the scrape stays suppressed, the cursor has already
1071
+ // been advanced above, and none of the bookkeeping the guard skips has a
1072
+ // second producer to be asked about.
1073
+ //
1074
+ // Order over the alternative in the Issue (hoist the reader above the
1075
+ // guard): the reader is a WRITE, and hoisting it would run that write on
1076
+ // every one of the 900 ticks of a 30-minute cycle instead of on the ones
1077
+ // that are owed an answer — the same argument the #2317 Phase D comment
1078
+ // below makes for not letting the delegation test short-circuit it.
1079
+ //
1080
+ // What this does NOT do is retract the scraped row the earlier tick
1081
+ // saved. Three reasons, and the first is decisive: nothing here can
1082
+ // identify that row. The hash this guard matched is per pollerKey, not
1083
+ // per turn — it survives the `resume` of a chain paused on a prompt —
1084
+ // so the row it stands for may belong to an earlier turn entirely, and
1085
+ // a scraped row carries no turn key to join on. Second, `archived` in
1086
+ // this schema is the tombstone of an operator clearing History (#168),
1087
+ // written by `archiveMessages` for a whole worktree; reusing it for
1088
+ // "superseded" would make a clear and a handover indistinguishable in
1089
+ // the table. Third, the scrape is not always junk — when a turn is
1090
+ // interrupted the pane holds text the transcript's closed turn does not
1091
+ // — and a duplicated row is visible and recoverable where a deleted one
1092
+ // is neither. Two rows for one turn is the failure this trades for, and
1093
+ // #2401 has already stopped the junk one being picked as a relay's
1094
+ // answer.
1095
+ if ((0, response_dedup_1.claimStructuredHistoryRecheck)(pollerKey)) {
1096
+ const recaptured = await (0, structured_history_gate_1.captureStructuredHistoryTurn)(worktreeId, cliToolId, instanceId, {
1097
+ worktreePath: worktree.path,
1098
+ transcriptPathHint: claudeMetadata?.logFilePath ?? null,
1099
+ });
1100
+ if (recaptured) {
1101
+ (0, response_dedup_1.settleStructuredHistoryRecheck)(pollerKey);
1102
+ logger.info('structured-history-recheck-captured', {
1103
+ worktreeId,
1104
+ cliToolId,
1105
+ instanceId: resolvedInstanceId,
1106
+ });
1107
+ // The turn IS now in History, as the agent's own Markdown, so this
1108
+ // tick recorded something and says so. Inert for the poller either
1109
+ // way: `runPollTick` only reads this value after a stop the tick
1110
+ // raised itself, and this branch raises none.
1111
+ return true;
1112
+ }
1113
+ }
997
1114
  return false;
998
1115
  }
999
1116
  }
@@ -1020,6 +1137,13 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
1020
1137
  worktreePath: worktree.path,
1021
1138
  transcriptPathHint: claudeMetadata?.logFilePath ?? null,
1022
1139
  }));
1140
+ // Issue #2399: remember which way that went, because the next tick may not
1141
+ // get here. A `false` is the reader saying "not yet, or not mine", and the
1142
+ // dedup guard above turns every following poll of the same static frame into
1143
+ // a return — so unless the fact is written down now, the ask never happens
1144
+ // again. A `true` settles it: the turn is recorded and there is nothing left
1145
+ // to re-ask about.
1146
+ markOrSettleStructuredHistoryRecheck((0, response_poller_core_1.getPollerKey)(worktreeId, cliToolId, instanceId), structuredHistoryLive);
1023
1147
  // Issue #2317 Phase D: the scrape is dropped while the geometry is
1024
1148
  // delegated, and the transcript capture above is what makes that safe.
1025
1149
  //
@@ -17,14 +17,69 @@
17
17
  * during THIS turn?" and deliberately does NOT persist across turns: an
18
18
  * identical response in a later turn is a real response and must be saved.
19
19
  * Suppressing it would reproduce the very bug this module fixes.
20
+ *
21
+ * ## The second cache: what the dedup skip owes the transcript reader (#2399)
22
+ *
23
+ * The guard above answers a question about the SCREEN, and `response-checker`
24
+ * used to read its answer as a question about the TURN — a duplicate frame ended
25
+ * the tick before `captureStructuredHistoryTurn` was reached, so for a pull-mode
26
+ * tool (claude, codex, antigravity, command-code) the transcript reader was
27
+ * asked exactly once per turn: on the poll that saved the scrape. For a reader
28
+ * the premise is backwards. "The screen stopped changing" is precisely the
29
+ * moment BEFORE the agent's own file closes the turn, so the one ask lands too
30
+ * early, answers false, and the 900 ticks that follow — 30 minutes of
31
+ * `MAX_POLLING_DURATION` at 2 s each — all return at the guard. Measured on
32
+ * codex 2026-09-07: `codex-transcript-turn-open` once, then
33
+ * `duplicate-response-skipped` until the poller's budget ran out, with the
34
+ * rollout's `task_complete` appended 1.8 s after the single ask.
35
+ *
36
+ * So the dedup verdict now carries a caveat, and it lives here rather than in
37
+ * `response-checker` for one reason: its lifetime is the response hash's
38
+ * lifetime exactly. `stopPolling`, the `resume` that keeps a paused chain's
39
+ * hash, and the worktree rename all have to treat the two the same, and the
40
+ * only way to guarantee that without touching `response-poller-core` (which
41
+ * owns those three calls) is for one function to clear both.
20
42
  */
21
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.STRUCTURED_HISTORY_RECHECK_TICK_INTERVAL = void 0;
22
45
  exports.isDuplicateResponse = isDuplicateResponse;
46
+ exports.markStructuredHistoryRecheckPending = markStructuredHistoryRecheckPending;
47
+ exports.settleStructuredHistoryRecheck = settleStructuredHistoryRecheck;
48
+ exports.claimStructuredHistoryRecheck = claimStructuredHistoryRecheck;
23
49
  exports.clearResponseHashCache = clearResponseHashCache;
24
50
  exports.renameResponseHashCacheKey = renameResponseHashCacheKey;
25
51
  const crypto_1 = require("crypto");
26
52
  const responseHashCache = globalThis.__responseHashCache ??
27
53
  (globalThis.__responseHashCache = new Map());
54
+ /**
55
+ * In-memory cache: pollerKey -> ticks still to wait before the transcript reader
56
+ * is asked again (Issue #2399). Absence means nothing is owed.
57
+ *
58
+ * On `globalThis` for the same reason `responseHashCache` is, and populated and
59
+ * cleared at exactly the same moments — see the module comment.
60
+ */
61
+ const structuredHistoryRecheckCache = globalThis.__structuredHistoryRecheckCache ??
62
+ (globalThis.__structuredHistoryRecheckCache = new Map());
63
+ /**
64
+ * How many duplicate ticks separate two asks of the transcript reader (#2399).
65
+ *
66
+ * The reader's work is a tail read of the agent's transcript — 4 MiB for codex
67
+ * — parsed and rendered, so asking on all 900 ticks of a 30-minute cycle is not
68
+ * free even though the poller has nothing else to do with them. Three is chosen
69
+ * against the two numbers that bracket it:
70
+ *
71
+ * - the FIRST duplicate tick always asks (the countdown starts at zero), so in
72
+ * the ordinary case — the turn closed between the scrape and the next poll —
73
+ * the Markdown row lands ~2 s later and the throttle never applies at all;
74
+ * - a turn that closes late is then re-asked every ~6 s, which is inside the
75
+ * window where a missing reply is still an operator's "it hasn't shown up
76
+ * yet" rather than a lost turn.
77
+ *
78
+ * The cost this bounds is only ever paid while a turn is BOTH finished on screen
79
+ * and still open in the transcript; the moment either the reader writes the row
80
+ * or the cycle ends, the entry is gone.
81
+ */
82
+ exports.STRUCTURED_HISTORY_RECHECK_TICK_INTERVAL = 3;
28
83
  /**
29
84
  * Check whether the given response content was already saved during the current
30
85
  * polling cycle for the same pollerKey. If it is new, updates the cache.
@@ -41,15 +96,88 @@ function isDuplicateResponse(pollerKey, content) {
41
96
  responseHashCache.set(pollerKey, hash);
42
97
  return false;
43
98
  }
99
+ /**
100
+ * Note that the transcript reader has unfinished business for this pollerKey
101
+ * (Issue #2399).
102
+ *
103
+ * Called by `response-checker` on every poll that gets PAST the guard above and
104
+ * finds neither structured writer willing to own the turn — i.e. the push
105
+ * subscription is not live and the pull reader answered false. That false is the
106
+ * one the poller used to treat as final, and this records that it was not.
107
+ *
108
+ * Idempotent, and deliberately does not restart the countdown: a turn that has
109
+ * been waiting five ticks for its transcript to close must not be pushed back to
110
+ * the end of the queue by a sixth poll that learned nothing new.
111
+ *
112
+ * The tool is not consulted. `captureStructuredHistoryTurn` answers "does this
113
+ * tool keep a transcript at all?" in a map lookup and returns false for gemini,
114
+ * copilot and opencode without touching a file, so an entry for one of them
115
+ * costs a lookup every third tick and duplicating the gate's own dispatch here
116
+ * would cost a second place for it to go stale.
117
+ *
118
+ * @param pollerKey - Poller key ("worktreeId:instanceId")
119
+ */
120
+ function markStructuredHistoryRecheckPending(pollerKey) {
121
+ if (!structuredHistoryRecheckCache.has(pollerKey)) {
122
+ structuredHistoryRecheckCache.set(pollerKey, 0);
123
+ }
124
+ }
125
+ /**
126
+ * Forget the outstanding recheck for this pollerKey (Issue #2399).
127
+ *
128
+ * Called when the question has an answer: the reader wrote the row, or a push
129
+ * subscription turned out to own the turn after all. Nothing is owed and the
130
+ * next duplicate tick returns at the guard exactly as it did before this Issue.
131
+ *
132
+ * @param pollerKey - Poller key ("worktreeId:instanceId")
133
+ */
134
+ function settleStructuredHistoryRecheck(pollerKey) {
135
+ structuredHistoryRecheckCache.delete(pollerKey);
136
+ }
137
+ /**
138
+ * Take this tick's turn at asking the transcript reader again (Issue #2399).
139
+ *
140
+ * A claim rather than a query, because the throttle has to be spent exactly once
141
+ * per tick and the caller is the only thing that knows a tick happened: this
142
+ * both answers "ask now?" and advances the countdown, so two calls in one tick
143
+ * would ask twice as often rather than twice.
144
+ *
145
+ * False when nothing is owed for this key ({@link markStructuredHistoryRecheckPending}
146
+ * was never called, or {@link settleStructuredHistoryRecheck} has since run) and
147
+ * false on the ticks the throttle swallows. True on the first duplicate tick
148
+ * after a pending mark, and every
149
+ * {@link STRUCTURED_HISTORY_RECHECK_TICK_INTERVAL} ticks after that.
150
+ *
151
+ * @param pollerKey - Poller key ("worktreeId:instanceId")
152
+ * @returns true when the caller should ask the reader on this tick
153
+ */
154
+ function claimStructuredHistoryRecheck(pollerKey) {
155
+ const ticksToWait = structuredHistoryRecheckCache.get(pollerKey);
156
+ if (ticksToWait === undefined)
157
+ return false;
158
+ if (ticksToWait > 0) {
159
+ structuredHistoryRecheckCache.set(pollerKey, ticksToWait - 1);
160
+ return false;
161
+ }
162
+ structuredHistoryRecheckCache.set(pollerKey, exports.STRUCTURED_HISTORY_RECHECK_TICK_INTERVAL - 1);
163
+ return true;
164
+ }
44
165
  /**
45
166
  * Clear the response hash cache for a specific pollerKey.
46
167
  * Called during session cleanup / stopPolling, so each new user turn starts
47
168
  * with a clean slate.
48
169
  *
170
+ * Issue #2399: the outstanding recheck goes with it. The caveat is a note about
171
+ * one particular cached hash — "the screen behind it was static before the
172
+ * transcript closed" — so it can outlive neither the hash nor the cycle, and
173
+ * carrying it into a new turn would spend reads asking about a turn that is
174
+ * over.
175
+ *
49
176
  * @param pollerKey - Poller key ("worktreeId:instanceId")
50
177
  */
51
178
  function clearResponseHashCache(pollerKey) {
52
179
  responseHashCache.delete(pollerKey);
180
+ structuredHistoryRecheckCache.delete(pollerKey);
53
181
  }
54
182
  /**
55
183
  * Move the cached hash from one pollerKey to another.
@@ -71,4 +199,12 @@ function renameResponseHashCacheKey(oldKey, newKey) {
71
199
  responseHashCache.delete(oldKey);
72
200
  if (hash !== undefined)
73
201
  responseHashCache.set(newKey, hash);
202
+ // Issue #2399: the recheck caveat moves with the hash it is about. Leaving it
203
+ // behind would strand a turn whose transcript is still open — the new key
204
+ // starts owing nothing, so every later tick returns at the dedup guard and the
205
+ // Markdown row is never written.
206
+ const ticksToWait = structuredHistoryRecheckCache.get(oldKey);
207
+ structuredHistoryRecheckCache.delete(oldKey);
208
+ if (ticksToWait !== undefined)
209
+ structuredHistoryRecheckCache.set(newKey, ticksToWait);
74
210
  }
@@ -12,7 +12,10 @@
12
12
  * agent has CLOSED, which is a stronger statement than any amount of quiet,
13
13
  * so the capture is delivered as soon as it lands. Both triggers of that
14
14
  * capture — the Stop hook and the poller — reach the same function, which is
15
- * why this module needs one hook and not two.
15
+ * why this module needs one hook and not two. For these five the transcript
16
+ * is also the ONLY thing a reply may be read from: the poller's own scrape
17
+ * of the same turn arrives first and carries junk (Issue #2401), so an
18
+ * unmarked row is stepped over and the marked one waited for.
16
19
  * - **Tools without one** (copilot, gemini, vibe-local). What a finished turn
17
20
  * leaves behind is the poller's copy of the SCREEN, judged by a string
18
21
  * analysis of a frame that may still be being drawn. The Issue asks for
@@ -67,6 +70,86 @@ const logger = (0, logger_1.createLogger)('relay-delivery');
67
70
  const REPLY_LOOKBACK_MESSAGES = 40;
68
71
  /** How many times the scrape path re-reads before it gives up on stability. */
69
72
  const SCRAPE_STABILITY_ATTEMPTS = 3;
73
+ /**
74
+ * The `request_id` shape a transcript reader mints for a finished turn.
75
+ *
76
+ * `claude-turn:<uuid>`, `codex-turn:<turn_id>`, `antigravity-turn:<id>`,
77
+ * `command-code-turn:<id>` and opencode's `oc-turn:<message id>`. Matched as a
78
+ * SHAPE rather than as a list of five literals for the reason `ask`'s copy of
79
+ * this line gives (Issue #2386): the sixth reader lands INSIDE the filter
80
+ * instead of silently outside it. A `:` cannot appear in any of the ids
81
+ * themselves, so this cannot match a row that merely contains the text.
82
+ *
83
+ * Copied rather than imported, deliberately. `src/cli/commands/ask.ts` has the
84
+ * same regexp, and importing it here would invert the dependency (lib reaching
85
+ * into the CLI bundle); `AGENT_MARKDOWN_REQUEST_ID_PREFIXES` in
86
+ * `src/types/agent-transcript.ts` is the LIST form this shape is chosen not to
87
+ * be. One regexp in each layer is cheaper than either edge.
88
+ */
89
+ const TURN_REQUEST_ID_PATTERN = /-turn:/;
90
+ /**
91
+ * Request-id namespaces CommandMate writes ABOUT a session, not for it.
92
+ *
93
+ * `chat_messages` has no `system` role, so "reply from X" (Issue #2377) and
94
+ * "the model changed" (Issue #2357) are both stored as assistant rows and told
95
+ * apart by their request id. Neither is a word the worker said, and handing the
96
+ * relay's own notice back to the session it came from is the smallest possible
97
+ * loop. `ask` steps over exactly these two for the same reason.
98
+ */
99
+ const SYSTEM_ROW_REQUEST_ID_PREFIXES = [
100
+ chat_db_1.RELAY_SYSTEM_REQUEST_ID_PREFIX,
101
+ chat_db_1.MODEL_CHANGE_REQUEST_ID_PREFIX,
102
+ ];
103
+ /**
104
+ * Whether this tool's reply is read out of a transcript rather than off a screen.
105
+ *
106
+ * For the five that answer `true` the ledger is authoritative: a row with no
107
+ * turn marker was written by the SCRAPER, and for these tools a scraped row is
108
+ * never the answer — it is a footer line, a half-drawn frame or the ANSI dump
109
+ * the send path flushes (Issues #2398 / #2400). The relay may therefore hold
110
+ * out for a marked row. For copilot, gemini and vibe-local the scraper's row is
111
+ * the only record there will ever be, and demanding a marker would deliver
112
+ * nothing, ever.
113
+ *
114
+ * A record and not a `Set`, so a ninth entry in `CLI_TOOL_IDS` fails `tsc` here
115
+ * instead of quietly defaulting to "screen-scraped" — the idiom
116
+ * `cli-tools/install-hints` uses for the same reason.
117
+ *
118
+ * Deliberately NOT `isPullTranscriptHistory`: that asks who does the READING,
119
+ * so opencode (whose own server pushes) answers `false` even though its rows
120
+ * carry `oc-turn:` like the other four. It also lives in `lib/polling`, whose
121
+ * module graph reaches this file's triggers — importing it would close a cycle
122
+ * the relay was built to avoid.
123
+ */
124
+ const TRANSCRIPT_READER_TOOLS = {
125
+ claude: true,
126
+ codex: true,
127
+ antigravity: true,
128
+ 'command-code': true,
129
+ opencode: true,
130
+ copilot: false,
131
+ gemini: false,
132
+ 'vibe-local': false,
133
+ };
134
+ /**
135
+ * How long the scrape path holds out for the transcript row.
136
+ *
137
+ * Issue #2386 measured the gap on codex 0.153.4 at 5.2 s: the scraper writes
138
+ * the pane into the ledger in the millisecond before the send, and the rollout
139
+ * reader writes the real answer about five seconds after the completion was
140
+ * already announced. Three times the measured gap, the same window `ask` holds,
141
+ * and spent only when the ledger has nothing marked yet — a worker whose
142
+ * transcript already landed stashes on the first read.
143
+ *
144
+ * When it does elapse nothing is delivered. That is the safe direction and not
145
+ * a lost answer: the relay is a STANDING instruction, so the transcript
146
+ * reader's own `settled` announcement (or the next completion) still delivers,
147
+ * and if neither ever comes the requester is told by the expiry notice rather
148
+ * than by a footer line dressed up as the reply.
149
+ */
150
+ const RELAY_TURN_ROW_GRACE_MS = 15_000;
151
+ /** How often the grace above re-reads the ledger. */
152
+ const RELAY_TURN_ROW_POLL_MS = 1_000;
70
153
  const inFlight = (globalThis.__relayDeliveryInFlight ??= new Set());
71
154
  /** Forget every in-flight delivery. Test seam. */
72
155
  function resetRelayDeliveryState() {
@@ -77,14 +160,37 @@ function senderLabel(db, relay) {
77
160
  const worker = (0, relay_session_ref_1.resolveRelaySession)(db, relay.to);
78
161
  return { alias: worker.alias, worktreeId: worker.worktreeId };
79
162
  }
163
+ /** Whether a row's `request_id` says a transcript reader wrote it. */
164
+ function isTurnRow(requestId) {
165
+ return typeof requestId === 'string' && TURN_REQUEST_ID_PATTERN.test(requestId);
166
+ }
167
+ /** Whether a row is CommandMate's own furniture rather than the worker's words. */
168
+ function isSystemRow(requestId) {
169
+ return typeof requestId === 'string'
170
+ && SYSTEM_ROW_REQUEST_ID_PREFIXES.some((prefix) => requestId.startsWith(prefix));
171
+ }
172
+ /** Whether this worker's answer is only ever a transcript row. */
173
+ function requiresTurnRow(cliToolId) {
174
+ return TRANSCRIPT_READER_TOOLS[cliToolId] === true;
175
+ }
80
176
  /**
81
177
  * The newest thing B actually said, at or after `since`.
82
178
  *
83
- * Three kinds of row are stepped over, and each exclusion is load-bearing:
179
+ * Four kinds of row are stepped over, and each exclusion is load-bearing:
84
180
  * prompt rows (a question is not an answer, and the confirmation notice is a
85
- * separate delivery), relay SYSTEM rows (this session's own transcript
86
- * furniture — delivering "Reply from X" back to X is the smallest possible
87
- * loop), and empty bodies.
181
+ * separate delivery), CommandMate's own system rows (delivering "Reply from X"
182
+ * back to X is the smallest possible loop), empty bodies — and, for a tool that
183
+ * keeps a transcript, every row the SCRAPER wrote.
184
+ *
185
+ * That last one is Issue #2401. Until it was added the filter looked only at
186
+ * shape, never at provenance, so the newest row won whatever had written it:
187
+ * a codex footer line, a partial scrape, or the raw ANSI the send path flushes
188
+ * lands in the ledger with `request_id IS NULL` and, being newest, WAS the
189
+ * reply — forwarded to the requesting agent as the worker's own answer, which
190
+ * it then summarises. #2386 fixed the identical hole in `ask` by demanding the
191
+ * turn marker; the relay looked exempt only because its 15-second pump usually
192
+ * gave the transcript time to land, and a race the timing usually wins is still
193
+ * a race.
88
194
  */
89
195
  function findWorkerReply(db, worker, since) {
90
196
  const messages = (0, chat_db_1.getMessages)(db, worker.worktreeId, {
@@ -93,13 +199,16 @@ function findWorkerReply(db, worker, since) {
93
199
  instanceId: worker.instanceId,
94
200
  matchResolvedInstance: true,
95
201
  });
202
+ const requireTurnRow = requiresTurnRow(worker.cliToolId);
96
203
  // `getMessages` answers newest-first, so the first candidate IS the newest.
97
204
  for (const message of messages) {
98
205
  if (message.role !== 'assistant')
99
206
  continue;
100
207
  if (message.messageType === 'prompt')
101
208
  continue;
102
- if (message.requestId?.startsWith(chat_db_1.RELAY_SYSTEM_REQUEST_ID_PREFIX))
209
+ if (isSystemRow(message.requestId))
210
+ continue;
211
+ if (requireTurnRow && !isTurnRow(message.requestId))
103
212
  continue;
104
213
  if (message.content.trim() === '')
105
214
  continue;
@@ -153,6 +262,42 @@ function sleep(ms) {
153
262
  setTimeout(resolve, ms);
154
263
  });
155
264
  }
265
+ /**
266
+ * Hold out for the transcript row this scrape got ahead of, then stash it.
267
+ *
268
+ * Called for a tool whose answers come out of a transcript, on the completion
269
+ * edge the POLLER raised — which is the edge that fires early. #2386 measured
270
+ * the reader landing about five seconds behind it, so re-reading beats both
271
+ * giving up on the first look and delivering what is there instead.
272
+ *
273
+ * The re-read is {@link stashReplyForOpenRelays} and not
274
+ * {@link findWorkerReply}, because "is there an answer yet" has to be asked per
275
+ * relay against that relay's own `createdAt`: a marked row older than the relay
276
+ * answered somebody else's question and must not end the wait. The stash itself
277
+ * is guarded on `pending_kind IS NULL`, so asking repeatedly costs reads and
278
+ * never a second payload.
279
+ *
280
+ * Returns having delivered nothing when the grace elapses. See
281
+ * {@link RELAY_TURN_ROW_GRACE_MS} for why that is the safe direction.
282
+ */
283
+ async function deliverWhenTranscriptCatchesUp(db, worker) {
284
+ const deadline = Date.now() + RELAY_TURN_ROW_GRACE_MS;
285
+ for (;;) {
286
+ if (stashReplyForOpenRelays(db, worker, Date.now()) > 0) {
287
+ void pumpRelayDeliveries();
288
+ return;
289
+ }
290
+ if (Date.now() >= deadline)
291
+ break;
292
+ await sleep(RELAY_TURN_ROW_POLL_MS);
293
+ }
294
+ logger.info('relay-scrape-row-not-transcript-backed', {
295
+ worktreeId: worker.worktreeId,
296
+ cliToolId: worker.cliToolId,
297
+ instanceId: worker.instanceId,
298
+ graceMs: RELAY_TURN_ROW_GRACE_MS,
299
+ });
300
+ }
156
301
  /**
157
302
  * B finished a turn. Deliver it, or arrange to.
158
303
  *
@@ -174,6 +319,15 @@ async function notifyRelayTurnCompleted(worker, options) {
174
319
  void pumpRelayDeliveries();
175
320
  return;
176
321
  }
322
+ // The scrape path, and it forks on who is allowed to write this worker's
323
+ // answers. A tool with a transcript has an authoritative record, so the
324
+ // scraped row this announcement is about is not a candidate at all — the
325
+ // relay waits for the marked one instead of watching an unmarked one hold
326
+ // still (Issue #2401).
327
+ if (requiresTurnRow(worker.cliToolId)) {
328
+ await deliverWhenTranscriptCatchesUp(db, worker);
329
+ return;
330
+ }
177
331
  // The scrape path. The row that is newest now may be a frame mid-draw, so
178
332
  // the delivery waits until the newest row stops changing — bounded, because
179
333
  // an agent that keeps writing is an agent whose next completion will trigger
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "A local control plane for agent CLIs — orchestration and visibility for Claude Code, Codex, Gemini CLI, and more across Git worktrees",
5
5
  "keywords": [
6
6
  "claude-code",