commandmate 0.30.0 → 0.30.1
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 +28 -28
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/versions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/conversation/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/messages/route.js +1 -1
- package/.next/server/app/api/assistant/messages/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/session/route.js +1 -1
- package/.next/server/app/api/assistant/session/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/start/route.js +2 -2
- package/.next/server/app/api/assistant/start/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/terminal/route.js +2 -2
- package/.next/server/app/api/assistant/terminal/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/capabilities/route.js +1 -1
- package/.next/server/app/api/capabilities/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/agent-event/route.js +1 -1
- package/.next/server/app/api/hooks/agent-event/route.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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/remote/pair/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route.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.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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.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.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.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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.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]/env/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/instances/opencode/route.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.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.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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/opencode/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/opencode/session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/opencode/share/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.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.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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.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.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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.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_client-reference-manifest.js +1 -1
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/config/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/apply-default-agents/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
- package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/more/page_client-reference-manifest.js +1 -1
- package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
- package/.next/server/app/review/page_client-reference-manifest.js +1 -1
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +12 -12
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +28 -28
- package/.next/server/chunks/1058.js +1 -1
- package/.next/server/chunks/1886.js +2 -2
- package/.next/server/chunks/2119.js +1 -1
- package/.next/server/chunks/2445.js +1 -1
- package/.next/server/chunks/3099.js +5 -0
- package/.next/server/chunks/5017.js +1 -1
- package/.next/server/chunks/5043.js +1 -0
- package/.next/server/chunks/552.js +25 -0
- package/.next/server/chunks/6130.js +1 -1
- package/.next/server/chunks/6894.js +1 -1
- package/.next/server/chunks/9696.js +2 -2
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{6381-28d0e95620aa018e.js → 6381-dbf04a99647f6018.js} +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-aac399392ec8c273.js +1 -0
- package/.next/static/css/23f9c40ecc458c77.css +1 -0
- package/.next/trace +13 -13
- package/dist/server/src/lib/history/user-turn-recorder.js +9 -4
- package/dist/server/src/lib/hooks/agent-event-service.js +40 -6
- package/dist/server/src/lib/hooks/sources/antigravity/history.js +101 -13
- package/dist/server/src/lib/hooks/sources/claude/history.js +161 -18
- package/dist/server/src/lib/hooks/sources/codex/history.js +100 -13
- package/dist/server/src/lib/hooks/stop-history-capture.js +161 -0
- package/dist/server/src/lib/polling/response-checker.js +43 -3
- package/dist/server/src/lib/polling/structured-history-gate.js +131 -6
- package/dist/server/src/lib/session/current-output-builder.js +118 -16
- package/package.json +1 -1
- package/.next/server/chunks/2827.js +0 -29
- package/.next/server/chunks/6781.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-d48b95ed554bcd13.js +0 -1
- package/.next/static/css/61e10d4b1b11b193.css +0 -1
- /package/.next/static/{7K0XXFu_ty4wRSkl4X21E → pcpB4K6KreMzu2qbhvuRY}/_buildManifest.js +0 -0
- /package/.next/static/{7K0XXFu_ty4wRSkl4X21E → pcpB4K6KreMzu2qbhvuRY}/_ssgManifest.js +0 -0
|
@@ -94,6 +94,7 @@ exports.acceptCodexRolloutPath = acceptCodexRolloutPath;
|
|
|
94
94
|
exports.isCodexRolloutFileFor = isCodexRolloutFileFor;
|
|
95
95
|
exports.findCodexRolloutPath = findCodexRolloutPath;
|
|
96
96
|
exports.captureCodexTranscriptTurn = captureCodexTranscriptTurn;
|
|
97
|
+
exports.resolveCodexTranscriptPath = resolveCodexTranscriptPath;
|
|
97
98
|
const promises_1 = require("fs/promises");
|
|
98
99
|
const os_1 = require("os");
|
|
99
100
|
const path_1 = require("path");
|
|
@@ -296,15 +297,21 @@ async function scanForRollout(dir, sessionId, depth) {
|
|
|
296
297
|
return null;
|
|
297
298
|
}
|
|
298
299
|
/**
|
|
299
|
-
* Read
|
|
300
|
+
* Read this instance's unwritten turns out of its rollout and write them.
|
|
300
301
|
*
|
|
301
|
-
* **
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
302
|
+
* **Every turn in the window that is not already a row**, oldest first, which is
|
|
303
|
+
* the shape Issue #2246 gave all three pull readers and the reasoning for it is
|
|
304
|
+
* written out once on `../claude/history`'s
|
|
305
|
+
* {@link captureClaudeTranscriptTurn}. The short version: this reader only runs
|
|
306
|
+
* when the poller judges a turn finished, so a missed judgement used to lose a
|
|
307
|
+
* turn permanently — by the next judgement "the newest turn" had moved on.
|
|
306
308
|
*
|
|
307
|
-
*
|
|
309
|
+
* #2121's one-row-per-turn argument is preserved by the **anchor**: the newest
|
|
310
|
+
* turn in the window this reader has already written is where the backfill
|
|
311
|
+
* starts, and a window with no anchor falls back to the newest turn alone.
|
|
312
|
+
*
|
|
313
|
+
* The return value is the poller's instruction, and it is about the **newest**
|
|
314
|
+
* turn whatever else was written on the way. **True** means History holds
|
|
308
315
|
* this turn as the agent's own Markdown and the scrape must be dropped. **False**
|
|
309
316
|
* means it does not, for any reason at all — no session pointer, no file, an
|
|
310
317
|
* unreadable file, a turn codex has not closed yet — and the scrape must be
|
|
@@ -350,8 +357,7 @@ async function captureCodexTranscriptTurn(target, capture = {}) {
|
|
|
350
357
|
return false;
|
|
351
358
|
const parsed = (0, transcript_1.parseCodexRollout)(text);
|
|
352
359
|
const built = (0, transcript_1.buildCodexTurns)(parsed.records, sessionId);
|
|
353
|
-
|
|
354
|
-
if (!turn) {
|
|
360
|
+
if (built.turns.length === 0) {
|
|
355
361
|
logger.info('codex-transcript-no-turn', {
|
|
356
362
|
worktreeId: target.worktreeId,
|
|
357
363
|
instanceId,
|
|
@@ -375,8 +381,33 @@ async function captureCodexTranscriptTurn(target, capture = {}) {
|
|
|
375
381
|
duplicateStreamRecords: built.duplicateStreamRecords,
|
|
376
382
|
});
|
|
377
383
|
}
|
|
378
|
-
const
|
|
379
|
-
|
|
384
|
+
const pending = await selectUnwrittenCodexTurns(target, built.turns);
|
|
385
|
+
if (pending.turns.length === 0) {
|
|
386
|
+
logger.debug('codex-transcript-turns-already-saved', {
|
|
387
|
+
worktreeId: target.worktreeId,
|
|
388
|
+
instanceId,
|
|
389
|
+
turnsInWindow: built.turns.length,
|
|
390
|
+
});
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (pending.turns.length > 1) {
|
|
394
|
+
logger.info('codex-transcript-backfilling-turns', {
|
|
395
|
+
worktreeId: target.worktreeId,
|
|
396
|
+
instanceId,
|
|
397
|
+
path,
|
|
398
|
+
pendingTurns: pending.turns.length,
|
|
399
|
+
turnsInWindow: built.turns.length,
|
|
400
|
+
anchored: pending.anchored,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
let captured = false;
|
|
404
|
+
for (let index = 0; index < pending.turns.length; index += 1) {
|
|
405
|
+
const turn = pending.turns[index];
|
|
406
|
+
const previousStartedAt = index === 0 ? pending.previousStartedAt : pending.turns[index - 1].startedAt;
|
|
407
|
+
const lastUserRow = await recordCodexUserTurns(target, turn, previousStartedAt);
|
|
408
|
+
captured = await writeCodexTurn(target, turn, (0, transcript_1.renderCodexTurn)(turn), resolveAssistantTimestampMs(turn, lastUserRow), path);
|
|
409
|
+
}
|
|
410
|
+
return captured;
|
|
380
411
|
}
|
|
381
412
|
catch (error) {
|
|
382
413
|
logger.error('codex-transcript-capture-failed', {
|
|
@@ -387,6 +418,58 @@ async function captureCodexTranscriptTurn(target, capture = {}) {
|
|
|
387
418
|
return false;
|
|
388
419
|
}
|
|
389
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* The rollout file this instance would be read from, or null (Issue #2246).
|
|
423
|
+
*
|
|
424
|
+
* The same two steps {@link captureCodexTranscriptTurn} opens with, asked
|
|
425
|
+
* without reading or writing anything; see `../claude/history`'s
|
|
426
|
+
* {@link resolveClaudeTranscriptPath} for what the Stop receiver does with it.
|
|
427
|
+
*
|
|
428
|
+
* Never throws.
|
|
429
|
+
*/
|
|
430
|
+
async function resolveCodexTranscriptPath(target, capture = {}) {
|
|
431
|
+
try {
|
|
432
|
+
const sessionId = await resolveCodexSessionId(target);
|
|
433
|
+
if (!sessionId)
|
|
434
|
+
return null;
|
|
435
|
+
return await locateCodexRollout(capture.codexHome ?? resolveCodexHome(), sessionId);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The turns {@link captureCodexTranscriptTurn} still has to write (Issue #2246).
|
|
443
|
+
*
|
|
444
|
+
* The same rule as `../claude/history`'s, on codex's own key: search backwards
|
|
445
|
+
* from the newest turn for one that is already a row, and take everything after
|
|
446
|
+
* it. Record order and never a timestamp, and bounded by the window — a turn
|
|
447
|
+
* whose records fell outside {@link CODEX_TRANSCRIPT_TAIL_BYTES} is not in `turns`.
|
|
448
|
+
*
|
|
449
|
+
* @param turns - Every turn in the window, oldest first
|
|
450
|
+
*/
|
|
451
|
+
async function selectUnwrittenCodexTurns(target, turns) {
|
|
452
|
+
const [{ getDbInstance }, { findMessageByRequestId }] = await Promise.all([
|
|
453
|
+
Promise.resolve().then(() => __importStar(require('../../../../lib/db/db-instance'))),
|
|
454
|
+
Promise.resolve().then(() => __importStar(require('../../../../lib/db'))),
|
|
455
|
+
]);
|
|
456
|
+
const db = getDbInstance();
|
|
457
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
458
|
+
const requestId = (0, agent_transcript_1.codexTurnRequestId)(turns[index].turnId);
|
|
459
|
+
if (!findMessageByRequestId(db, target.worktreeId, requestId))
|
|
460
|
+
continue;
|
|
461
|
+
return {
|
|
462
|
+
turns: turns.slice(index + 1),
|
|
463
|
+
previousStartedAt: turns[index].startedAt,
|
|
464
|
+
anchored: true,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
turns: turns.slice(-1),
|
|
469
|
+
previousStartedAt: turns.length > 1 ? turns[turns.length - 2].startedAt : 0,
|
|
470
|
+
anchored: false,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
390
473
|
/**
|
|
391
474
|
* The rollout file for this session, or null.
|
|
392
475
|
*
|
|
@@ -435,11 +518,15 @@ async function isReadableFile(path) {
|
|
|
435
518
|
*
|
|
436
519
|
* @returns The last row written or found, for the timestamp
|
|
437
520
|
*/
|
|
438
|
-
async function recordCodexUserTurns(target, turn) {
|
|
521
|
+
async function recordCodexUserTurns(target, turn, previousStartedAt = 0) {
|
|
439
522
|
const instanceId = target.instanceId ?? target.cliToolId;
|
|
440
523
|
let last = { outcome: 'skipped', messageId: null, timestampMs: null };
|
|
524
|
+
// Issue #2246: a turn read late is a turn whose `/send` row may be older than
|
|
525
|
+
// #2196's symmetric window reaches. The previous turn's start is the tightest
|
|
526
|
+
// honest bound, and it only ever widens the search.
|
|
527
|
+
const adoption = previousStartedAt > 0 ? { adoptionFromMs: previousStartedAt } : {};
|
|
441
528
|
for (const prompt of turn.prompts) {
|
|
442
|
-
const recorded = await (0, user_turn_recorder_1.recordUserTurn)(target, (0, agent_transcript_1.codexPromptRequestId)(prompt.itemId), prompt.text, prompt.timestampMs);
|
|
529
|
+
const recorded = await (0, user_turn_recorder_1.recordUserTurn)(target, (0, agent_transcript_1.codexPromptRequestId)(prompt.itemId), prompt.text, prompt.timestampMs, adoption);
|
|
443
530
|
if (recorded.outcome === 'failed') {
|
|
444
531
|
logger.warn('codex-transcript-user-turn-failed', {
|
|
445
532
|
worktreeId: target.worktreeId,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading the transcript when the agent says it stopped (Issue #2246).
|
|
4
|
+
*
|
|
5
|
+
* ## Why the poller was not enough
|
|
6
|
+
*
|
|
7
|
+
* `lib/polling/structured-history-gate` is asked to record a turn at exactly one
|
|
8
|
+
* moment: the poll on which the scraper decided the turn had finished and was
|
|
9
|
+
* about to save its own copy. That is the right moment — doing the handover at
|
|
10
|
+
* the point of the write is what makes it impossible for both writers to run —
|
|
11
|
+
* but it is the *only* moment, and the decision behind it is a string analysis
|
|
12
|
+
* of a terminal frame.
|
|
13
|
+
*
|
|
14
|
+
* When that analysis misses one completion the cost is not a delay. By the time
|
|
15
|
+
* the next completion is judged, the newest turn is the next one; before #2246
|
|
16
|
+
* the reader wrote only that, so the missed turn belonged to nobody and stayed
|
|
17
|
+
* missing. One misjudged frame, one turn gone (measured 2026-09-02: a reply the
|
|
18
|
+
* transcript held in full and History never showed).
|
|
19
|
+
*
|
|
20
|
+
* The agent itself knows the boundary exactly, and has been telling us: the
|
|
21
|
+
* `stop` hook. This module is that second trigger.
|
|
22
|
+
*
|
|
23
|
+
* ## What it does not do
|
|
24
|
+
*
|
|
25
|
+
* **It does not replace the poller's call.** A session with no hooks — or with
|
|
26
|
+
* hooks the operator never wired up — sends no `stop`, and for it the poller is
|
|
27
|
+
* still the only trigger there is. Two triggers for one turn cost nothing
|
|
28
|
+
* because the readers are idempotent and the gate serialises them per instance;
|
|
29
|
+
* one trigger that is sometimes absent costs a reply.
|
|
30
|
+
*
|
|
31
|
+
* **It does not decide anything about the turn.** The whole judgement stays in
|
|
32
|
+
* the gate and the readers, so the two entry points cannot drift.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: a hook post is fire-and-forget from a CLI's stop handler, and a
|
|
35
|
+
* failure here must not become the agent's problem.
|
|
36
|
+
*
|
|
37
|
+
* @module lib/hooks/stop-history-capture
|
|
38
|
+
*/
|
|
39
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
40
|
+
if (k2 === undefined) k2 = k;
|
|
41
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
42
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
43
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
44
|
+
}
|
|
45
|
+
Object.defineProperty(o, k2, desc);
|
|
46
|
+
}) : (function(o, m, k, k2) {
|
|
47
|
+
if (k2 === undefined) k2 = k;
|
|
48
|
+
o[k2] = m[k];
|
|
49
|
+
}));
|
|
50
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
51
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
52
|
+
}) : function(o, v) {
|
|
53
|
+
o["default"] = v;
|
|
54
|
+
});
|
|
55
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
56
|
+
var ownKeys = function(o) {
|
|
57
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
58
|
+
var ar = [];
|
|
59
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
60
|
+
return ar;
|
|
61
|
+
};
|
|
62
|
+
return ownKeys(o);
|
|
63
|
+
};
|
|
64
|
+
return function (mod) {
|
|
65
|
+
if (mod && mod.__esModule) return mod;
|
|
66
|
+
var result = {};
|
|
67
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
68
|
+
__setModuleDefault(result, mod);
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
})();
|
|
72
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
73
|
+
exports.STOP_TRANSCRIPT_RETRY_DELAY_MS = void 0;
|
|
74
|
+
exports.captureTranscriptTurnOnStop = captureTranscriptTurnOnStop;
|
|
75
|
+
const logger_1 = require("../../lib/logger");
|
|
76
|
+
const logger = (0, logger_1.createLogger)('lib/hooks/stop-history-capture');
|
|
77
|
+
/**
|
|
78
|
+
* How long to wait before asking a second time.
|
|
79
|
+
*
|
|
80
|
+
* The `stop` hook fires when the agent considers the turn over, and the last
|
|
81
|
+
* assistant record is appended to the transcript around — not necessarily
|
|
82
|
+
* before — that instant. A reader that arrives in the gap sees the prompt
|
|
83
|
+
* record and no body, answers false (correctly: an empty row would be a blank
|
|
84
|
+
* reply forever) and, without a retry, hands a turn the agent has definitely
|
|
85
|
+
* finished back to the scraper.
|
|
86
|
+
*
|
|
87
|
+
* Half a second is chosen against what the gap actually is — a file append that
|
|
88
|
+
* has already been issued — rather than against a timeout. One retry and not a
|
|
89
|
+
* loop, because the poller's own trigger is still behind this: if the file is
|
|
90
|
+
* still empty after the retry, the next poll is the right place to notice, and
|
|
91
|
+
* a hook handler that waits on a file is a hook handler that can hang a turn.
|
|
92
|
+
*/
|
|
93
|
+
exports.STOP_TRANSCRIPT_RETRY_DELAY_MS = 500;
|
|
94
|
+
function sleep(ms) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
setTimeout(resolve, ms);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Ask the pull-mode reader to record this instance's turn, now.
|
|
101
|
+
*
|
|
102
|
+
* The tool question is asked first and without doing any work
|
|
103
|
+
* (`isPullTranscriptHistory`), so a `stop` from gemini, copilot or opencode
|
|
104
|
+
* costs one map lookup and returns — in particular it never waits out the
|
|
105
|
+
* retry delay. opencode has a second writer too, but a *push* one: its
|
|
106
|
+
* subscription has already received the reply and there is nothing to pull.
|
|
107
|
+
*
|
|
108
|
+
* The import is dynamic so that the poller's module graph — which reaches every
|
|
109
|
+
* transcript reader and the whole opencode client — does not become a static
|
|
110
|
+
* dependency of the hook receiver.
|
|
111
|
+
*
|
|
112
|
+
* @param worktree - The worktree the event resolved to
|
|
113
|
+
* @param cliToolId - The tool that sent the event
|
|
114
|
+
* @param instanceId - The already-resolved instance id
|
|
115
|
+
* @returns Whether History now holds this instance's newest turn as Markdown
|
|
116
|
+
*/
|
|
117
|
+
async function captureTranscriptTurnOnStop(worktree, cliToolId, instanceId, options = {}) {
|
|
118
|
+
try {
|
|
119
|
+
const { captureStructuredHistoryTurn, hasStructuredHistoryTranscript, isPullTranscriptHistory } = await Promise.resolve().then(() => __importStar(require('../../lib/polling/structured-history-gate')));
|
|
120
|
+
if (!isPullTranscriptHistory(cliToolId))
|
|
121
|
+
return false;
|
|
122
|
+
// No `transcriptPathHint`: that value is a line the *pane* printed, and the
|
|
123
|
+
// hook receiver has not read the pane. A tool whose transcript can only be
|
|
124
|
+
// found that way has no session pointer either, and therefore sends no stop
|
|
125
|
+
// event this path could act on.
|
|
126
|
+
const capture = { worktreePath: worktree.path, transcriptPathHint: null };
|
|
127
|
+
if (await captureStructuredHistoryTurn(worktree.id, cliToolId, instanceId, capture)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const retryDelayMs = options.retryDelayMs ?? exports.STOP_TRANSCRIPT_RETRY_DELAY_MS;
|
|
131
|
+
if (retryDelayMs <= 0)
|
|
132
|
+
return false;
|
|
133
|
+
// The retry is for one failure and one only: the turn's last assistant
|
|
134
|
+
// record had not been appended yet. Every other reason the capture answers
|
|
135
|
+
// false — no session pointer, no transcript, the operator never wired hooks
|
|
136
|
+
// for this tool — is a reason waiting cannot fix, and this handler is on the
|
|
137
|
+
// agent's own stop path, so half a second spent for nothing is half a second
|
|
138
|
+
// added to every turn it is spent on.
|
|
139
|
+
if (!(await hasStructuredHistoryTranscript(worktree.id, cliToolId, instanceId, capture))) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
await sleep(retryDelayMs);
|
|
143
|
+
const captured = await captureStructuredHistoryTurn(worktree.id, cliToolId, instanceId, capture);
|
|
144
|
+
logger.debug('stop-history-capture-retried', {
|
|
145
|
+
worktreeId: worktree.id,
|
|
146
|
+
cliToolId,
|
|
147
|
+
instanceId,
|
|
148
|
+
captured,
|
|
149
|
+
});
|
|
150
|
+
return captured;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.warn('stop-history-capture-failed', {
|
|
154
|
+
worktreeId: worktree.id,
|
|
155
|
+
cliToolId,
|
|
156
|
+
instanceId,
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
});
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -434,10 +434,36 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
|
|
|
434
434
|
// CRITICAL FIX: Detect and skip Claude Code startup banner/screen
|
|
435
435
|
if (cliToolId === 'claude') {
|
|
436
436
|
const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
// Issue #2247: `│` is what Claude Code draws markdown TABLES with -- the
|
|
438
|
+
// live frame in `tests/fixtures/claude-live-2247/turn-table.txt` is a
|
|
439
|
+
// two-row table and nothing else -- so it identified a reply, not a banner.
|
|
440
|
+
// The banner's own frame glyphs are the rounded corners and the block
|
|
441
|
+
// shading; those stay.
|
|
442
|
+
const hasBannerArt = /[╭╮╰╯]/.test(cleanResponse) || /░{3,}/.test(cleanResponse) || /▓{3,}/.test(cleanResponse);
|
|
443
|
+
// Issue #2247: the bare `v\d+\.\d+` alternative matched any version string a
|
|
444
|
+
// reply happens to mention. The frame that lost a turn on 2026-09-02 was
|
|
445
|
+
// "GitHub Release v0.30.0 を公開しました" (148 chars, well under the 2000
|
|
446
|
+
// below). What the banner actually prints is the tool's own name and
|
|
447
|
+
// version on one row -- `Claude Code v2.1.258` -- so that is what is
|
|
448
|
+
// matched now, plus the `claude/` form older banners used.
|
|
449
|
+
const hasVersionInfo = /Claude Code v\d+\.\d+|claude\//.test(cleanResponse);
|
|
439
450
|
const hasStartupTips = /Tip:|for shortcuts|\?\s*for help/.test(cleanResponse);
|
|
440
451
|
const hasProjectInit = /^\s*\/Users\/.*$/m.test(cleanResponse) && cleanResponse.split('\n').length < 30;
|
|
452
|
+
// Issue #2247: the anchors above are only evidence of a banner on a pane
|
|
453
|
+
// that has not had a single turn yet -- the same shape as the #1897 copilot
|
|
454
|
+
// fix below. Claude echoes every prompt into the transcript as `❯ <text>`,
|
|
455
|
+
// and the startup screen has none, so an echo anywhere in the transcript
|
|
456
|
+
// rules the banner out no matter what the reply quotes.
|
|
457
|
+
//
|
|
458
|
+
// The search is `findRecentUserPromptIndex`, deliberately: it is the same
|
|
459
|
+
// `/^[>❯]\s+\S/` this file already anchors extraction on, and it stops at
|
|
460
|
+
// `contentEnd`. That bound is load-bearing rather than incidental -- the
|
|
461
|
+
// footer's composer draws a DIM ghost suggestion (`❯ Try "write a test for
|
|
462
|
+
// <filepath>"`, see `boot-banner.txt`) whose stripped bytes are identical
|
|
463
|
+
// to a real echo (#1879), so a scan over the whole pane would read the
|
|
464
|
+
// startup screen as "already had a turn" and put the banner back in
|
|
465
|
+
// History.
|
|
466
|
+
const hasTurnEcho = findRecentUserPromptIndex(totalLines) >= 0;
|
|
441
467
|
const userPromptMatch = cleanResponse.match(/^[>❯]\s+(\S.*)$/m);
|
|
442
468
|
if (userPromptMatch) {
|
|
443
469
|
const userPromptIndex = cleanResponse.indexOf(userPromptMatch[0]);
|
|
@@ -452,7 +478,21 @@ function extractResponse(output, lastCapturedLine, cliToolId, captureWindowLines
|
|
|
452
478
|
return incompleteResult(totalLines);
|
|
453
479
|
}
|
|
454
480
|
}
|
|
455
|
-
else if (
|
|
481
|
+
else if (!hasTurnEcho &&
|
|
482
|
+
(hasBannerArt || hasVersionInfo || hasStartupTips || hasProjectInit) &&
|
|
483
|
+
response.length < 2000) {
|
|
484
|
+
// Issue #2247: this branch used to swallow the turn in silence -- the
|
|
485
|
+
// poller kept ticking every 2s and `response-poller` logged nothing at
|
|
486
|
+
// all, so the only way to tell a lost turn from an idle session was to
|
|
487
|
+
// re-run `extractResponse` on a saved pane by hand. It is reached only
|
|
488
|
+
// before the first echo lands, so it cannot become a per-tick flood.
|
|
489
|
+
logger.info('Claude startup banner suppressed, response not saved', {
|
|
490
|
+
responseLength: response.length,
|
|
491
|
+
hasBannerArt,
|
|
492
|
+
hasVersionInfo,
|
|
493
|
+
hasStartupTips,
|
|
494
|
+
hasProjectInit,
|
|
495
|
+
});
|
|
456
496
|
return incompleteResult(totalLines);
|
|
457
497
|
}
|
|
458
498
|
}
|
|
@@ -64,8 +64,11 @@
|
|
|
64
64
|
* @module lib/polling/structured-history-gate
|
|
65
65
|
*/
|
|
66
66
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.resetStructuredHistoryCaptureQueue = resetStructuredHistoryCaptureQueue;
|
|
68
|
+
exports.isPullTranscriptHistory = isPullTranscriptHistory;
|
|
67
69
|
exports.isStructuredHistoryWriterLive = isStructuredHistoryWriterLive;
|
|
68
70
|
exports.captureStructuredHistoryTurn = captureStructuredHistoryTurn;
|
|
71
|
+
exports.hasStructuredHistoryTranscript = hasStructuredHistoryTranscript;
|
|
69
72
|
const logger_1 = require("../../lib/logger");
|
|
70
73
|
const subscription_1 = require("../../lib/hooks/sources/opencode/subscription");
|
|
71
74
|
const history_1 = require("../../lib/hooks/sources/claude/history");
|
|
@@ -76,6 +79,68 @@ const tool_id_2 = require("../../lib/hooks/sources/codex/tool-id");
|
|
|
76
79
|
const tool_id_3 = require("../../lib/hooks/sources/antigravity/tool-id");
|
|
77
80
|
const registry_1 = require("../../lib/hooks/sources/registry");
|
|
78
81
|
const logger = (0, logger_1.createLogger)('lib/polling/structured-history-gate');
|
|
82
|
+
/**
|
|
83
|
+
* One in-flight capture per instance (Issue #2246).
|
|
84
|
+
*
|
|
85
|
+
* Until this Issue there was one caller — the poller's save path, whose ticks do
|
|
86
|
+
* not overlap — and the Stop hook receiver is now a second one, arriving at
|
|
87
|
+
* exactly the moment the poller is most likely to be inside the same turn. Two
|
|
88
|
+
* things follow, and it is worth separating them because #2246's own text runs
|
|
89
|
+
* them together.
|
|
90
|
+
*
|
|
91
|
+
* **The duplicate-row race it describes is not open today, and this is what
|
|
92
|
+
* keeps it shut.** The readers are idempotent through `findMessageByRequestId`,
|
|
93
|
+
* which is a check-then-write over a *non-unique* index — but the check and the
|
|
94
|
+
* write are adjacent and synchronous (`better-sqlite3` is), and the only `await`
|
|
95
|
+
* in that stretch is the dynamic import *before* the check. So there is no point
|
|
96
|
+
* for a second caller to interleave at, and two concurrent captures of one turn
|
|
97
|
+
* already end in one row. That is a property of two adjacent statements, which
|
|
98
|
+
* is exactly the kind of property a later refactor removes without noticing: an
|
|
99
|
+
* `await` inserted between them re-opens the window silently, and the row it
|
|
100
|
+
* costs is a duplicated reply nobody sees until an operator reports it.
|
|
101
|
+
*
|
|
102
|
+
* **What it saves today is the work.** Both triggers otherwise read, parse and
|
|
103
|
+
* render the same 4 MiB tail at the same time, and the second one throws all of
|
|
104
|
+
* it away.
|
|
105
|
+
*
|
|
106
|
+
* A queue rather than a shared result: the second caller runs the read again
|
|
107
|
+
* after the first finishes, because it may be asking about a newer turn and
|
|
108
|
+
* because the answer it needs is about the file as it is *now*. Re-running is
|
|
109
|
+
* cheap and, after the first call, finds the row and answers true.
|
|
110
|
+
*
|
|
111
|
+
* On `globalThis` for the reason every shared map in this subsystem is (#1736):
|
|
112
|
+
* under `next dev` the poller's bundle and the hook receiver's bundle would each
|
|
113
|
+
* get a private copy of a module-scoped map, and a lock only one of two bundles
|
|
114
|
+
* can see is not a lock.
|
|
115
|
+
*/
|
|
116
|
+
const captureQueue = (globalThis.__structuredHistoryCaptureQueue ??= new Map());
|
|
117
|
+
/** Forget every instance's in-flight capture. Test seam. */
|
|
118
|
+
function resetStructuredHistoryCaptureQueue() {
|
|
119
|
+
captureQueue.clear();
|
|
120
|
+
}
|
|
121
|
+
/** The queue key; the same triple `buildCompositeKey` spells everywhere else. */
|
|
122
|
+
function captureKeyOf(worktreeId, cliToolId, instanceId) {
|
|
123
|
+
return `${worktreeId}\u0000${cliToolId}\u0000${instanceId}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Run `work` after whatever is already running for this instance.
|
|
127
|
+
*
|
|
128
|
+
* The stored promise is a settled-only shadow of the real one, so a caller that
|
|
129
|
+
* throws cannot reject the next caller's chain, and the entry is removed only by
|
|
130
|
+
* whoever put it there — a later call that has already replaced it must not have
|
|
131
|
+
* its own lock deleted underneath it.
|
|
132
|
+
*/
|
|
133
|
+
function serializePerInstance(key, work) {
|
|
134
|
+
const previous = captureQueue.get(key) ?? Promise.resolve();
|
|
135
|
+
const result = previous.then(work, work);
|
|
136
|
+
const settled = result.then(() => undefined, () => undefined);
|
|
137
|
+
captureQueue.set(key, settled);
|
|
138
|
+
void settled.then(() => {
|
|
139
|
+
if (captureQueue.get(key) === settled)
|
|
140
|
+
captureQueue.delete(key);
|
|
141
|
+
});
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
79
144
|
/**
|
|
80
145
|
* Which reader answers for which tool.
|
|
81
146
|
*
|
|
@@ -91,9 +156,18 @@ const logger = (0, logger_1.createLogger)('lib/polling/structured-history-gate')
|
|
|
91
156
|
* {@link captureStructuredHistoryTurn} logs it and falls back to the scraper.
|
|
92
157
|
*/
|
|
93
158
|
const PULL_TRANSCRIPT_READERS = {
|
|
94
|
-
[tool_id_1.CLAUDE_CLI_TOOL_ID]:
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
[tool_id_1.CLAUDE_CLI_TOOL_ID]: {
|
|
160
|
+
capture: (target, capture) => (0, history_1.captureClaudeTranscriptTurn)(target, capture),
|
|
161
|
+
locate: (target, capture) => (0, history_1.resolveClaudeTranscriptPath)(target, capture),
|
|
162
|
+
},
|
|
163
|
+
[tool_id_2.CODEX_CLI_TOOL_ID]: {
|
|
164
|
+
capture: (target, capture) => (0, history_2.captureCodexTranscriptTurn)(target, capture),
|
|
165
|
+
locate: (target, capture) => (0, history_2.resolveCodexTranscriptPath)(target, capture),
|
|
166
|
+
},
|
|
167
|
+
[tool_id_3.ANTIGRAVITY_CLI_TOOL_ID]: {
|
|
168
|
+
capture: (target, capture) => (0, history_3.captureAntigravityTranscriptTurn)(target, capture),
|
|
169
|
+
locate: (target, capture) => (0, history_3.resolveAntigravityTranscriptPath)(target, capture),
|
|
170
|
+
},
|
|
97
171
|
};
|
|
98
172
|
/**
|
|
99
173
|
* What this tool's source says about who records its replies.
|
|
@@ -114,6 +188,17 @@ function transcriptHistoryModeOf(cliToolId) {
|
|
|
114
188
|
return null;
|
|
115
189
|
}
|
|
116
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Whether this tool keeps a transcript a reader can be asked to pull from.
|
|
193
|
+
*
|
|
194
|
+
* Exported for the Stop receiver (Issue #2246), which has to decide whether the
|
|
195
|
+
* *retry* below is worth waiting for before it answers the agent's hook. It is
|
|
196
|
+
* the same question {@link captureStructuredHistoryTurn} opens with, asked
|
|
197
|
+
* without doing the read — never a tool id comparison, here or there.
|
|
198
|
+
*/
|
|
199
|
+
function isPullTranscriptHistory(cliToolId) {
|
|
200
|
+
return transcriptHistoryModeOf(cliToolId) === 'pull';
|
|
201
|
+
}
|
|
117
202
|
/**
|
|
118
203
|
* Whether the agent's own server is recording this instance's replies.
|
|
119
204
|
*
|
|
@@ -180,7 +265,7 @@ function isStructuredHistoryWriterLive(worktreeId, cliToolId, instanceId) {
|
|
|
180
265
|
* @param capture - Where to look; see {@link StructuredHistoryCapture}
|
|
181
266
|
*/
|
|
182
267
|
async function captureStructuredHistoryTurn(worktreeId, cliToolId, instanceId, capture) {
|
|
183
|
-
if (
|
|
268
|
+
if (!isPullTranscriptHistory(cliToolId))
|
|
184
269
|
return false;
|
|
185
270
|
const reader = PULL_TRANSCRIPT_READERS[cliToolId];
|
|
186
271
|
if (!reader) {
|
|
@@ -191,11 +276,51 @@ async function captureStructuredHistoryTurn(worktreeId, cliToolId, instanceId, c
|
|
|
191
276
|
});
|
|
192
277
|
return false;
|
|
193
278
|
}
|
|
279
|
+
const resolvedInstanceId = instanceId ?? cliToolId;
|
|
280
|
+
return serializePerInstance(captureKeyOf(worktreeId, cliToolId, resolvedInstanceId), async () => {
|
|
281
|
+
try {
|
|
282
|
+
return await reader.capture({ worktreeId, cliToolId, instanceId: resolvedInstanceId }, capture);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
logger.warn('structured-history-capture-unavailable', {
|
|
286
|
+
worktreeId,
|
|
287
|
+
cliToolId,
|
|
288
|
+
instanceId: resolvedInstanceId,
|
|
289
|
+
error: error instanceof Error ? error.message : String(error),
|
|
290
|
+
});
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Whether a pull reader can name a transcript for this instance (Issue #2246).
|
|
297
|
+
*
|
|
298
|
+
* Not "is there a turn to write" and not "did anything get written" — only
|
|
299
|
+
* whether the file the reader would open exists right now. The one caller is
|
|
300
|
+
* `lib/hooks/stop-history-capture`, which asks it after a capture answered
|
|
301
|
+
* false, to decide whether waiting half a second and asking again could help.
|
|
302
|
+
* An instance with no transcript will not have one in half a second, and the
|
|
303
|
+
* delay would be paid on every `stop` of every session without hooks.
|
|
304
|
+
*
|
|
305
|
+
* False for every tool that does not declare `transcriptHistory: 'pull'`, and
|
|
306
|
+
* never throws — an unanswerable question is one whose answer is "no file",
|
|
307
|
+
* which costs the retry and nothing else.
|
|
308
|
+
*
|
|
309
|
+
* @param worktreeId - The worktree
|
|
310
|
+
* @param cliToolId - The tool driving the pane
|
|
311
|
+
* @param instanceId - The agent instance; defaults to the primary
|
|
312
|
+
* @param capture - Where to look; see {@link StructuredHistoryCapture}
|
|
313
|
+
*/
|
|
314
|
+
async function hasStructuredHistoryTranscript(worktreeId, cliToolId, instanceId, capture) {
|
|
315
|
+
const reader = isPullTranscriptHistory(cliToolId) ? PULL_TRANSCRIPT_READERS[cliToolId] : undefined;
|
|
316
|
+
if (!reader)
|
|
317
|
+
return false;
|
|
194
318
|
try {
|
|
195
|
-
|
|
319
|
+
const path = await reader.locate({ worktreeId, cliToolId, instanceId: instanceId ?? cliToolId }, capture);
|
|
320
|
+
return path !== null;
|
|
196
321
|
}
|
|
197
322
|
catch (error) {
|
|
198
|
-
logger.warn('structured-history-
|
|
323
|
+
logger.warn('structured-history-locate-unavailable', {
|
|
199
324
|
worktreeId,
|
|
200
325
|
cliToolId,
|
|
201
326
|
instanceId: instanceId ?? cliToolId,
|