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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +159 -159
- package/.next/app-path-routes-manifest.json +28 -28
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +12 -12
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/versions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/capabilities/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/agent-event/route.js +1 -1
- package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/claude-done/route.js +2 -2
- package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/permission-request/route.js +1 -1
- package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/relays/[relayId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/relays/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/remote/pair/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/settings/default-agents/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/settings/default-surface-mode/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/slash-commands/route.js.nft.json +1 -1
- package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/clear-composer/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cli-reference/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/env/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/instances/opencode/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/opencode/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/opencode/session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/opencode/share/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/resolve-target/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/config/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/apply-default-agents/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
- package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/more/page_client-reference-manifest.js +1 -1
- package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
- package/.next/server/app/review/page_client-reference-manifest.js +1 -1
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +11 -11
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +28 -28
- package/.next/server/chunks/1210.js +1 -1
- package/.next/server/chunks/5043.js +1 -1
- package/.next/server/chunks/5164.js +1 -1
- package/.next/server/chunks/6491.js +2 -2
- package/.next/server/chunks/6838.js +1 -1
- package/.next/server/chunks/7875.js +2 -2
- package/.next/server/chunks/8918.js +4 -4
- package/.next/server/chunks/9130.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{4718-0053d42fb635e700.js → 4718-d97f52cd932dc1ee.js} +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-18a7d9374bd8c218.js +1 -0
- package/.next/static/css/{96d94a4ba771efc3.css → fb455b4e308adbaa.css} +1 -1
- package/.next/trace +13 -13
- package/README.md +6 -2
- package/dist/cli/commands/instances.d.ts.map +1 -1
- package/dist/cli/commands/instances.js +44 -15
- package/dist/cli/commands/peers.d.ts.map +1 -1
- package/dist/cli/commands/peers.js +12 -0
- package/dist/cli/commands/whoami.d.ts +67 -0
- package/dist/cli/commands/whoami.d.ts.map +1 -1
- package/dist/cli/commands/whoami.js +106 -0
- package/dist/cli/utils/api-client.d.ts +30 -1
- package/dist/cli/utils/api-client.d.ts.map +1 -1
- package/dist/cli/utils/api-client.js +28 -5
- package/dist/server/src/lib/detection/cli-patterns.js +206 -2
- package/dist/server/src/lib/hooks/agent-event-service.js +27 -5
- package/dist/server/src/lib/hooks/sources/codex/history.js +63 -3
- package/dist/server/src/lib/hooks/sources/codex/transcript.js +140 -6
- package/dist/server/src/lib/hooks/sources/launch-command.js +112 -15
- package/dist/server/src/lib/hooks/sources/turn-body.js +3 -1
- package/dist/server/src/lib/hooks/stop-history-capture.js +252 -50
- package/dist/server/src/lib/polling/response-checker.js +129 -5
- package/dist/server/src/lib/polling/response-dedup.js +136 -0
- package/dist/server/src/lib/relay/relay-delivery.js +160 -6
- package/package.json +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-06e1ff5ce2d126fd.js +0 -1
- /package/.next/static/{UIUPchtWvcEuQgcbGJv4g → eVkZd6vpcQwyJIlbymCUg}/_buildManifest.js +0 -0
- /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
|
-
:
|
|
313
|
-
? (0, cli_patterns_1.
|
|
314
|
-
:
|
|
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
|
-
|
|
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
|
-
*
|
|
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),
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
|
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