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