mixdog 1.2.2 → 1.2.4
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/README.md +18 -0
- package/package.json +2 -1
- package/scripts/gate-local.mjs +6 -4
- package/scripts/lib/gate-scheduler.mjs +29 -11
- package/scripts/probe-provider-catalog.mjs +31 -0
- package/scripts/provider-pingpong-live.mjs +599 -0
- package/scripts/session-sweep.mjs +5 -25
- package/src/agents/advisor/AGENT.md +1 -1
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/maintainer/AGENT.md +1 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -4
- package/src/agents/security/AGENT.md +1 -2
- package/src/agents/writer/AGENT.md +2 -3
- package/src/defaults/shipped-history.json +11 -1
- package/src/defaults/skills/image/SKILL.md +21 -23
- package/src/defaults/skills/setup/SKILL.md +6 -1
- package/src/defaults/skills/setup/references/actions.md +44 -22
- package/src/defaults/skills/setup/references/surfaces.md +10 -8
- package/src/headless-exec.mjs +15 -8
- package/src/lib/keychain-cjs.cjs +20 -15
- package/src/output-styles/common.md +11 -10
- package/src/rules/agent/AGENT.md +6 -4
- package/src/rules/lead/LEAD.md +7 -9
- package/src/rules/shared/45-destructive.md +4 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +28 -7
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -11
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +92 -61
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +10 -1
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +7 -0
- package/src/runtime/agent/orchestrator/config.mjs +5 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +26 -7
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +112 -7
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +101 -23
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-key-fetch.mjs +36 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +48 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +24 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +9 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +26 -44
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +31 -15
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-blocks.mjs +20 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-delta.mjs +15 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/message-events.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +12 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-outcome.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-state.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +41 -12
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +131 -9
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +29 -10
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +125 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +12 -62
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +117 -41
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +117 -72
- package/src/runtime/agent/orchestrator/providers/compat-request-policy.mjs +78 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +39 -5
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-native-context.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-exec.mjs +144 -34
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +28 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-request.mjs +8 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +42 -11
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-messages.mjs +109 -6
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -2
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +73 -7
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +75 -6
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +34 -7
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +32 -6
- package/src/runtime/agent/orchestrator/providers/deepseek.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +29 -5
- package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +36 -13
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +101 -11
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +41 -5
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-rest-stream/watchdogs.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +19 -14
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +371 -41
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +97 -49
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +81 -10
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +35 -8
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +60 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +253 -42
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-server-tool-usage.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-system-blocks.mjs +27 -6
- package/src/runtime/agent/orchestrator/providers/lib/claude-bridge-schema.mjs +392 -0
- package/src/runtime/agent/orchestrator/providers/lib/inclusive-usage.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/lib/origin-bound-fetch.mjs +16 -0
- package/src/runtime/agent/orchestrator/providers/lib/request-compression.mjs +86 -0
- package/src/runtime/agent/orchestrator/providers/lib/responses-terminal-fields.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/lib/wire-pairing.mjs +50 -22
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +133 -21
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +13 -15
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +1 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +92 -16
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -13
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +133 -62
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +35 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +39 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/tool-call-acc.mjs +2 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +84 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +47 -25
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-reasoning-field.mjs +155 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +40 -24
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +63 -25
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-head-parity.json +2901 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-parity.fixtures.mjs +183 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +87 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +51 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +103 -26
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +23 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +11 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +15 -23
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +64 -52
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +23 -10
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +28 -5
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-items.mjs +18 -7
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/text-relay.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +80 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +72 -24
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +323 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +50 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +7 -19
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/refusals.mjs +60 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/tool-calls.mjs +51 -36
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/terminal-frames.mjs +25 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -9
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +181 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +78 -70
- package/src/runtime/agent/orchestrator/providers/openrouter.mjs +74 -0
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +150 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/transport-scope.d.mts +6 -0
- package/src/runtime/agent/orchestrator/providers/transport-scope.mjs +28 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +164 -0
- package/src/runtime/agent/orchestrator/providers/wire-route.mjs +522 -0
- package/src/runtime/agent/orchestrator/providers/xai-tool-search-head-parity.json +1131 -0
- package/src/runtime/agent/orchestrator/providers/xai-tool-search.fixtures.mjs +195 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +104 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +8 -6
- package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +24 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +30 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +3 -2
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +8 -3
- package/src/runtime/agent/orchestrator/runtime-core/provider-request-snapshot.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/session-title.mjs +21 -1
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +16 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge-contract.mjs +6 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +526 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +10 -7
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +178 -71
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +102 -12
- package/src/runtime/agent/orchestrator/runtime-core/tool-exposure.mjs +96 -0
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -8
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -2
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/cache/tool-result-reuse.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +5 -11
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +6 -22
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +16 -93
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/compact/summary-anchor.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +148 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/context-tool-schema.mjs +17 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +8 -4
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/historical-session-closure.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +2 -5
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +89 -1
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +56 -0
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -10
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -9
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +24 -10
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +135 -0
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +31 -4
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +67 -20
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +10 -10
- package/src/runtime/agent/orchestrator/session/loop/tool-call-resolution.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +30 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +29 -5
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +147 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-support.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +23 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +34 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +54 -13
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-failure.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +57 -25
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +57 -13
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/failed-turn-rewind.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +6 -120
- package/src/runtime/agent/orchestrator/session/manager/pending-persist-queue.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +231 -16
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +42 -49
- package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +26 -4
- package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +36 -35
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +29 -13
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-recovery.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +4 -10
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/send-instrumentation.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +96 -0
- package/src/runtime/agent/orchestrator/session/side-question.mjs +197 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +22 -15
- package/src/runtime/agent/orchestrator/session/store/lifecycle-read.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/store/live-cache-eviction.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/store/liveness-veto.mjs +46 -37
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +44 -37
- package/src/runtime/agent/orchestrator/session/store/session-observers.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +59 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +27 -21
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +6 -18
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +33 -106
- package/src/runtime/agent/orchestrator/session/store/write-admission.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-pool.mjs +5 -8
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +136 -196
- package/src/runtime/agent/orchestrator/session/store-summary-archived-agent.mjs +34 -15
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +53 -115
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/store-summary-locations.mjs +3 -29
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +17 -10
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +9 -20
- package/src/runtime/agent/orchestrator/session/store.mjs +22 -19
- package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +12 -12
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +38 -14
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +15 -4
- package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-schema-rejection.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +72 -15
- package/src/runtime/agent/orchestrator/session/turn-failure.mjs +45 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +50 -0
- package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +218 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +160 -39
- package/src/runtime/agent/orchestrator/tools/builtin/git-partial-stage.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +48 -44
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +247 -49
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +101 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/lib/process-table-fixture.mjs +50 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +303 -63
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +108 -18
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +77 -11
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +81 -5
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +73 -39
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -4
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +88 -12
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +2 -0
- package/src/runtime/attachments/store.d.mts +9 -0
- package/src/runtime/attachments/store.mjs +80 -9
- package/src/runtime/browser-bridge/browser-action-contract.d.mts +0 -1
- package/src/runtime/browser-bridge/browser-action-contract.mjs +0 -2
- package/src/runtime/computer-bridge/action-schema.mjs +219 -196
- package/src/runtime/computer-bridge/linux-readiness.d.mts +0 -2
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -16
- package/src/runtime/effort-judge/judge-client.mjs +21 -4
- package/src/runtime/effort-judge/judge-worker.mjs +4 -1
- package/src/runtime/github/tool.mjs +208 -17
- package/src/runtime/media/adapters/codex-image.mjs +140 -21
- package/src/runtime/media/catalog.mjs +4 -4
- package/src/runtime/media/chatgpt-usage.mjs +55 -0
- package/src/runtime/media/defaults.mjs +37 -0
- package/src/runtime/media/jobs.mjs +12 -2
- package/src/runtime/media/lanes.mjs +12 -0
- package/src/runtime/media/tool-defs.mjs +15 -4
- package/src/runtime/media/tool.mjs +41 -12
- package/src/runtime/media/upstream-error.mjs +72 -6
- package/src/runtime/memory/index.mjs +1 -0
- package/src/runtime/memory/lib/http-router/ingest-routes.mjs +22 -1
- package/src/runtime/memory/lib/ingest/transcript-offsets.mjs +2 -0
- package/src/runtime/memory/lib/ingest/transcript-rows.mjs +1 -0
- package/src/runtime/memory/lib/pg/adapter.mjs +98 -1
- package/src/runtime/memory/lib/pg/process.mjs +36 -33
- package/src/runtime/memory/lib/pg/run-child.mjs +49 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +112 -4
- package/src/runtime/memory/lib/runtime-fetcher.mjs +12 -13
- package/src/runtime/memory/lib/transcript-ingest.mjs +14 -0
- package/src/runtime/office/README.md +1 -1
- package/src/runtime/office/authoring/docx-html-runner.mjs +1 -1
- package/src/runtime/office/authoring/html-browser.mjs +18 -8
- package/src/runtime/office/authoring/html-document-author.mjs +2 -0
- package/src/runtime/office/authoring/pdf-html-runner.mjs +1 -1
- package/src/runtime/office/authoring/pptx-contact-sheet.mjs +2 -0
- package/src/runtime/office/authoring/pptx-html-measure.mjs +3 -1
- package/src/runtime/office/authoring/pptx-html-runner.mjs +2 -0
- package/src/runtime/office/authoring/xlsx-html-runner.mjs +3 -1
- package/src/runtime/office/core/office-actions-read.mjs +3 -0
- package/src/runtime/office/core/office-render-preview.mjs +2 -0
- package/src/runtime/office/core/office-session-dispatch.mjs +7 -0
- package/src/runtime/office/core/office-sessionless-actions.mjs +8 -0
- package/src/runtime/office/core/office-sessions.mjs +5 -0
- package/src/runtime/office/core/office-transactions.mjs +3 -0
- package/src/runtime/office/core/office-write-ledger.mjs +94 -0
- package/src/runtime/office/design/library/templates/mixdog-executive.mixdog-edit.pptx +0 -0
- package/src/runtime/office/index.mjs +29 -0
- package/src/runtime/office/pdf/pdf-analysis.mjs +0 -68
- package/src/runtime/office/pdf/pdf-edit-document.mjs +2 -0
- package/src/runtime/office/pdf/pdf-render.mjs +23 -7
- package/src/runtime/office/pdf/pdf-security.mjs +2 -0
- package/src/runtime/office/portable/portable-opc.mjs +4 -1
- package/src/runtime/office/quality/visual-diff.mjs +2 -0
- package/src/runtime/session-search/session-search-ingest.mjs +1 -1
- package/src/runtime/shared/agent-reap-state.mjs +13 -0
- package/src/runtime/shared/atomic-file.d.mts +1 -1
- package/src/runtime/shared/background-tasks.mjs +6 -1
- package/src/runtime/shared/bounded-file-read.mjs +13 -0
- package/src/runtime/shared/bounded-line-diff.mjs +191 -0
- package/src/runtime/shared/browser-launch.mjs +15 -0
- package/src/runtime/shared/buffered-appender.mjs +18 -0
- package/src/runtime/shared/config.mjs +78 -18
- package/src/runtime/shared/err-text.mjs +48 -4
- package/src/runtime/shared/file-lock.mjs +6 -5
- package/src/runtime/shared/file-permissions.d.mts +6 -1
- package/src/runtime/shared/file-permissions.mjs +54 -6
- package/src/runtime/shared/llm/anthropic-betas.mjs +1 -2
- package/src/runtime/shared/llm/anthropic-effort.mjs +6 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +1 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +5 -2
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +3 -0
- package/src/runtime/shared/loopback-listener.mjs +9 -0
- package/src/runtime/shared/model-route-chain.d.mts +8 -0
- package/src/runtime/shared/model-route-chain.mjs +48 -0
- package/src/runtime/shared/model-route-fallback.mjs +22 -0
- package/src/runtime/shared/pid-liveness.mjs +12 -0
- package/src/runtime/shared/provider-api-key.mjs +8 -0
- package/src/runtime/shared/provider-timeout-phase.mjs +17 -0
- package/src/runtime/shared/quiet-progress.d.mts +35 -0
- package/src/runtime/shared/quiet-progress.mjs +101 -0
- package/src/runtime/shared/quota-resume-store.mjs +32 -1
- package/src/runtime/shared/read-row-numbers.mjs +44 -0
- package/src/runtime/shared/remote-intent.mjs +2 -42
- package/src/runtime/shared/review-diff.mjs +52 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +4 -0
- package/src/runtime/shared/session-sidebar-host.mjs +38 -0
- package/src/runtime/shared/slow-span.mjs +121 -0
- package/src/runtime/shared/statusline/statusline-segments.mjs +52 -6
- package/src/runtime/shared/tool-card-model/card-header.mjs +7 -1
- package/src/runtime/shared/tool-card-model/generic-detail.mjs +39 -5
- package/src/runtime/shared/tool-card-model.mjs +5 -2
- package/src/runtime/shared/tool-output-budget.mjs +2 -0
- package/src/runtime/shared/tool-output-limit.mjs +4 -0
- package/src/runtime/shared/tool-primitives.mjs +13 -0
- package/src/runtime/shared/tool-result-summary.mjs +22 -1
- package/src/runtime/shared/tool-status.mjs +3 -2
- package/src/runtime/shared/tool-surface.mjs +20 -6
- package/src/runtime/shared/tool-work-units.mjs +54 -11
- package/src/runtime/shared/transcript-metadata.mjs +49 -1
- package/src/runtime/shared/transcript-turn-keys.mjs +33 -0
- package/src/runtime/shared/transcript-writer/purge.mjs +74 -0
- package/src/runtime/shared/transcript-writer/transcript-file.mjs +10 -1
- package/src/runtime/shared/transcript-writer.mjs +24 -6
- package/src/runtime/shared/turn-changes.mjs +269 -100
- package/src/runtime/shared/turn-review-reply.mjs +8 -39
- package/src/runtime/shared/turn-snapshot-store.mjs +19 -108
- package/src/runtime/shared/turn-snapshot.mjs +865 -683
- package/src/runtime/shared/turn-worktree-snapshot.mjs +183 -255
- package/src/runtime/shared/user-data-guard.mjs +262 -49
- package/src/runtime/shared/worktrees/activity-gate.mjs +175 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +179 -0
- package/src/runtime/shared/worktrees/carry.mjs +533 -0
- package/src/runtime/shared/worktrees/contract.mjs +60 -0
- package/src/runtime/shared/worktrees/dependencies.mjs +456 -0
- package/src/runtime/shared/worktrees/file-recovery.mjs +84 -0
- package/src/runtime/shared/worktrees/git-head.mjs +214 -0
- package/src/runtime/shared/worktrees/git.mjs +8 -3
- package/src/runtime/shared/worktrees/include.mjs +125 -7
- package/src/runtime/shared/worktrees/lock.mjs +1 -1
- package/src/runtime/shared/worktrees/maintenance.mjs +20 -2
- package/src/runtime/shared/worktrees/manager.mjs +525 -31
- package/src/runtime/shared/worktrees/merge.mjs +31 -21
- package/src/runtime/shared/worktrees/owner.mjs +2 -1
- package/src/runtime/shared/worktrees/paths.d.mts +6 -0
- package/src/runtime/shared/worktrees/preparation-report.mjs +79 -0
- package/src/runtime/shared/worktrees/setup-commands.mjs +232 -0
- package/src/runtime/shared/worktrees/workspace-visibility.mjs +17 -0
- package/src/runtime/terminal-bridge/client.mjs +57 -20
- package/src/runtime/terminal-bridge/tool-defs.mjs +13 -6
- package/src/runtime/tidy/apply.mjs +47 -2
- package/src/runtime/tidy/run-engines.mjs +11 -5
- package/src/session-runtime/agent-tool-routing.mjs +19 -13
- package/src/session-runtime/boot/apis.mjs +20 -6
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +4 -0
- package/src/session-runtime/boot/infrastructure.mjs +6 -1
- package/src/session-runtime/boot/session.mjs +3 -3
- package/src/session-runtime/boot/tools.mjs +7 -1
- package/src/session-runtime/conversation-import/service.mjs +1 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +12 -5
- package/src/session-runtime/goal-facade-api.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +91 -0
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +15 -5
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +137 -26
- package/src/session-runtime/internal-tool-executor.mjs +8 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +187 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +71 -27
- package/src/session-runtime/lifecycle/session-catalog.mjs +25 -18
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +6 -0
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +36 -48
- package/src/session-runtime/lifecycle/session-switch.mjs +1 -1
- package/src/session-runtime/lifecycle/shared.mjs +5 -6
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -5
- package/src/session-runtime/lifecycle/teardown.mjs +70 -14
- package/src/session-runtime/lifecycle-api.mjs +1 -0
- package/src/session-runtime/mcp-glue/connect.mjs +17 -5
- package/src/session-runtime/media-api.mjs +8 -0
- package/src/session-runtime/model-route/route-ownership.mjs +60 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -2
- package/src/session-runtime/model-route/set-route.mjs +116 -36
- package/src/session-runtime/model-route/web-search-route.mjs +7 -2
- package/src/session-runtime/model-route-api.mjs +4 -2
- package/src/session-runtime/native-web-search/routes.mjs +37 -24
- package/src/session-runtime/native-web-search-usage.mjs +51 -0
- package/src/session-runtime/native-web-search.mjs +19 -9
- package/src/session-runtime/new-session-config.mjs +4 -2
- package/src/session-runtime/provider-auth/login.mjs +4 -0
- package/src/session-runtime/provider-auth/setup.mjs +4 -0
- package/src/session-runtime/provider-models/catalog-load.mjs +9 -0
- package/src/session-runtime/provider-models/web-search-catalog.mjs +23 -3
- package/src/session-runtime/provider-models.mjs +1 -0
- package/src/session-runtime/provider-usage.mjs +4 -0
- package/src/session-runtime/resource-scope-decorate.mjs +10 -3
- package/src/session-runtime/route-resolution.mjs +3 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +6 -0
- package/src/session-runtime/runtime-review-api.mjs +8 -2
- package/src/session-runtime/services/agent-task-status.mjs +2 -0
- package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +0 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +6 -7
- package/src/session-runtime/services/agent-tool/execute-send.mjs +2 -2
- package/src/session-runtime/services/agent-tool/execute-spawn.mjs +36 -3
- package/src/session-runtime/services/agent-tool/execute.mjs +9 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +30 -7
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +81 -20
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +7 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +6 -3
- package/src/session-runtime/services/agent-tool/lead-worker-index/forgotten-leads.mjs +29 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index/index-file.mjs +1 -1
- package/src/session-runtime/services/agent-tool/lead-worker-index/lead-rows.mjs +3 -4
- package/src/session-runtime/services/agent-tool/lead-worker-index/reap-timers.mjs +9 -3
- package/src/session-runtime/services/agent-tool/lead-worker-index/row-settlement.mjs +7 -18
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +82 -13
- package/src/session-runtime/services/agent-tool/model-select.mjs +82 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -6
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -5
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -4
- package/src/session-runtime/services/agent-tool/spawn-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +1 -3
- package/src/session-runtime/services/agent-tool/tag-registry/tag-refresh.mjs +1 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +18 -10
- package/src/session-runtime/services/agent-tool/terminal-reaper/scan-admission.mjs +8 -41
- package/src/session-runtime/services/agent-tool/terminal-reaper/stale-rows.mjs +3 -3
- package/src/session-runtime/services/agent-tool/terminal-reaper.mjs +6 -23
- package/src/session-runtime/services/agent-tool/terminal-trace.mjs +0 -1
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +32 -20
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +10 -1
- package/src/session-runtime/services/agent-tool/worker-index.mjs +74 -15
- package/src/session-runtime/services/agent-tool.mjs +7 -0
- package/src/session-runtime/services/agent-watchdog-registry.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +19 -3
- package/src/session-runtime/services/memory-runtime-proxy/calls.mjs +4 -0
- package/src/session-runtime/services/memory-runtime-proxy.mjs +1 -0
- package/src/session-runtime/services/provider-admin.mjs +158 -15
- package/src/session-runtime/session-create/session-options.mjs +7 -5
- package/src/session-runtime/session-create/session-start-hook.mjs +2 -1
- package/src/session-runtime/session-transcript-purge.mjs +32 -0
- package/src/session-runtime/session-transcript.mjs +4 -5
- package/src/session-runtime/session-turn-api.mjs +17 -14
- package/src/session-runtime/settings-builtin-tools-api.mjs +18 -6
- package/src/session-runtime/setup-tool/executor.mjs +57 -9
- package/src/session-runtime/setup-tool/extended-actions.mjs +86 -6
- package/src/session-runtime/setup-tool/settings-contract.mjs +18 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +25 -4
- package/src/session-runtime/skills-api.mjs +3 -5
- package/src/session-runtime/tool-policy-refresh.mjs +83 -32
- package/src/session-runtime/tool-surface.mjs +5 -1
- package/src/session-runtime/turn/agent-ops.mjs +49 -2
- package/src/session-runtime/turn/compaction-gate.mjs +25 -0
- package/src/session-runtime/turn/session-ops.mjs +7 -5
- package/src/session-runtime/turn/side-question-ops.mjs +19 -0
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +7 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +12 -3
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +18 -2
- package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
- package/src/session-runtime/turn/turn-run.mjs +26 -45
- package/src/session-runtime/usage-stats-api.mjs +46 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +16 -1
- package/src/session-runtime/workspace/admission-gate.mjs +44 -15
- package/src/session-runtime/workspace/project-branches.mjs +35 -41
- package/src/session-runtime/workspace/transition-steps.mjs +215 -116
- package/src/session-runtime/workspace/transitions.mjs +428 -73
- package/src/standalone/channel-transport.mjs +3 -4
- package/src/standalone/daemon-agent-control.mjs +9 -1
- package/src/standalone/daemon-desktop-runtime.mjs +32 -1
- package/src/standalone/daemon-local-session-bridge.mjs +14 -1
- package/src/standalone/daemon-owner.mjs +17 -4
- package/src/standalone/daemon-pg-stop.mjs +49 -0
- package/src/standalone/daemon-session-control.mjs +88 -0
- package/src/standalone/daemon-stop-budget.d.mts +2 -0
- package/src/standalone/daemon-stop-budget.mjs +18 -0
- package/src/standalone/daemon-stored-session-views.mjs +25 -3
- package/src/standalone/daemon-telemetry.mjs +19 -0
- package/src/standalone/daemon.mjs +63 -1
- package/src/standalone/desktop-service-registry.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +80 -30
- package/src/standalone/session-client.mjs +42 -0
- package/src/standalone/session-protocol.mjs +14 -0
- package/src/standalone/session-proxy/projection.mjs +41 -4
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +15 -1
- package/src/standalone/session-runtime-inline-host.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +91 -49
- package/src/standalone/session-service/agent-tree/agent-child.mjs +19 -1
- package/src/standalone/session-service/agent-tree/agent-registry.mjs +25 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +9 -1
- package/src/standalone/session-service/agent-tree.mjs +28 -2
- package/src/standalone/session-service/entries/session-loads.mjs +16 -5
- package/src/standalone/session-service/entries.mjs +1 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +39 -0
- package/src/standalone/session-service/quota-resume.mjs +28 -13
- package/src/standalone/session-service/session-calls/session-action.mjs +18 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +17 -6
- package/src/standalone/session-service/session-calls/session-views.mjs +6 -0
- package/src/standalone/session-service-api.mjs +6 -1
- package/src/standalone/session-state-patch.d.mts +2 -1
- package/src/standalone/session-state-patch.mjs +41 -1
- package/src/standalone/session-tool-output.mjs +82 -0
- package/src/tui/App.jsx +25 -0
- package/src/tui/app/app-view/floating-panel.jsx +26 -1
- package/src/tui/app/app-view.jsx +2 -1
- package/src/tui/app/btw-controller.mjs +118 -0
- package/src/tui/app/create-app-pickers.mjs +2 -0
- package/src/tui/app/provider-setup/provider-items.mjs +8 -0
- package/src/tui/app/provider-setup-picker.mjs +15 -3
- package/src/tui/app/shell-layout/row-budgets.mjs +11 -1
- package/src/tui/app/shell-layout/surfaces.mjs +3 -1
- package/src/tui/app/slash-commands.mjs +19 -3
- package/src/tui/app/slash-dispatch/session-commands.mjs +68 -6
- package/src/tui/app/use-global-key-input/approval-shortcut.mjs +12 -0
- package/src/tui/app/use-global-key-input.mjs +5 -12
- package/src/tui/app/use-prompt-handlers.mjs +11 -0
- package/src/tui/components/BtwPanel.jsx +53 -0
- package/src/tui/components/QuestionPanel.jsx +275 -0
- package/src/tui/components/Spinner.jsx +4 -0
- package/src/tui/components/SpinnerByline.jsx +21 -2
- package/src/tui/components/question-panel-model.mjs +156 -0
- package/src/tui/components/status-line/local-line.mjs +10 -2
- package/src/tui/dist/index.mjs +2897 -1722
- package/src/tui/session/active-tool-summary.mjs +1 -1
- package/src/tui/session/context-state.mjs +4 -5
- package/src/tui/session/goal-continuation/continuation-queue.mjs +2 -3
- package/src/tui/session/goal-continuation/deadline-delivery.mjs +4 -2
- package/src/tui/session/goal-continuation/session-boundary.mjs +21 -0
- package/src/tui/session/quota-resume.mjs +140 -61
- package/src/tui/session/session-api/commands.mjs +14 -10
- package/src/tui/session/session-api/controls.mjs +14 -2
- package/src/tui/session/session-api/integrations.mjs +9 -2
- package/src/tui/session/session-api/lifecycle/dispose.mjs +3 -9
- package/src/tui/session/session-api/lifecycle/resume.mjs +2 -7
- package/src/tui/session/session-api/lifecycle.mjs +14 -11
- package/src/tui/session/session-api/media.mjs +2 -0
- package/src/tui/session/session-api/routes.mjs +81 -57
- package/src/tui/session/session-api/settings.mjs +9 -3
- package/src/tui/session/session-api/worktree.mjs +3 -2
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/session-flow/auto-clear.mjs +1 -3
- package/src/tui/session/session-flow/drain.mjs +6 -4
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -0
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -1
- package/src/tui/session/session-flow/queue.mjs +1 -1
- package/src/tui/session/tool-approval.mjs +20 -8
- package/src/tui/session/transcript-spill.mjs +14 -7
- package/src/tui/session/transcript-store/streaming-tail.mjs +1 -1
- package/src/tui/session/tui-steering-persist.mjs +3 -5
- package/src/tui/session/turn-quiet.mjs +53 -0
- package/src/tui/session/turn-stream.mjs +179 -32
- package/src/tui/session/turn.mjs +50 -36
- package/src/tui/session/worktree-setup-notice.mjs +38 -0
- package/src/tui/spinner-meta.mjs +10 -0
- package/src/tui/spinner-verbs.mjs +3 -0
- package/src/ui/statusline.mjs +13 -3
- package/src/workflows/default/WORKFLOW.md +3 -1
- package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/manager/foreign-pending-messages.mjs +0 -346
- package/src/runtime/agent/orchestrator/session/manager/session-owner-liveness.mjs +0 -117
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +0 -246
- package/src/session-runtime/services/agent-tool/terminal-reaper/reap-timers.mjs +0 -142
- package/src/session-runtime/turn/share-ops.mjs +0 -77
- package/src/tui/session/cross-surface-share.mjs +0 -406
- package/src/tui/session/live-share/delta.mjs +0 -153
- package/src/tui/session/live-share/mirror.mjs +0 -106
- package/src/tui/session/live-share/owner.mjs +0 -254
- package/src/tui/session/live-share/retry.mjs +0 -54
- package/src/tui/session/live-share/viewer/frame-router.mjs +0 -32
- package/src/tui/session/live-share/viewer/pipe-client.mjs +0 -108
- package/src/tui/session/live-share/viewer/submit-acks.mjs +0 -65
- package/src/tui/session/live-share/viewer/sync-waiters.mjs +0 -53
- package/src/tui/session/live-share/viewer.mjs +0 -99
- package/src/tui/session/live-share/wire.mjs +0 -44
- package/src/tui/session/live-share.mjs +0 -154
- package/src/tui/session/shared-dir-watch.mjs +0 -69
|
@@ -1,27 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
2
|
import {
|
|
5
3
|
_resetTurnWorktreeSnapshotsForTest,
|
|
4
|
+
canonicalPath,
|
|
6
5
|
createTurnWorktreeSnapshot,
|
|
7
|
-
diffTurnWorktreeHead,
|
|
8
|
-
freezeTurnWorktreeChanges,
|
|
9
6
|
noteTurnWorktreeChange,
|
|
10
7
|
refreshTurnWorktreeSnapshot,
|
|
11
8
|
recordTurnWorktreeFileChange,
|
|
9
|
+
resumeTurnWorktreeFilePatch,
|
|
12
10
|
resumeTurnWorktreeSnapshot,
|
|
13
|
-
revertTurnWorktreeFile,
|
|
14
|
-
revertTurnWorktreePaths,
|
|
15
|
-
revertTurnWorktreeSnapshot,
|
|
16
11
|
} from './turn-worktree-snapshot.mjs';
|
|
17
12
|
import {
|
|
18
13
|
clearSessionChangeHistory as clearStoredChangeHistory,
|
|
19
14
|
loadSessionSnapshotRecords,
|
|
20
15
|
loadTurnChangeRecord,
|
|
21
16
|
loadTurnChangeRecords,
|
|
22
|
-
loadTurnReviewSummary,
|
|
23
17
|
loadTurnSnapshotRecord,
|
|
24
|
-
saveTurnReviewSummary,
|
|
25
18
|
saveTurnSnapshotRecord,
|
|
26
19
|
sessionChangeHistoryCleared,
|
|
27
20
|
updateTurnChangeRecord,
|
|
@@ -29,11 +22,15 @@ import {
|
|
|
29
22
|
} from './turn-snapshot-store.mjs';
|
|
30
23
|
import {
|
|
31
24
|
applyTurnChangeFiles,
|
|
25
|
+
changeDisplayPatch,
|
|
32
26
|
changePathKey,
|
|
33
27
|
findChangeFile,
|
|
28
|
+
isBinaryChangeContent,
|
|
34
29
|
measureLegacyChangeFile,
|
|
35
30
|
namedChangePatch,
|
|
31
|
+
repositoryObjectFormat,
|
|
36
32
|
sessionChangeReview,
|
|
33
|
+
splitPatchSections,
|
|
37
34
|
storedChangeFile,
|
|
38
35
|
trackedChangeFile,
|
|
39
36
|
turnChangesReply,
|
|
@@ -47,21 +44,37 @@ import {
|
|
|
47
44
|
primeReviewOwner,
|
|
48
45
|
recordReviewOwner,
|
|
49
46
|
} from './session-baseline-store.mjs';
|
|
50
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
boundReviewPatch,
|
|
49
|
+
createReviewPatchBudget,
|
|
50
|
+
filePatchReply,
|
|
51
|
+
patchOmittedFile,
|
|
52
|
+
REVIEW_FILE_PATCH_SHOW_MAX_BYTES,
|
|
53
|
+
reviewDiffTooLarge,
|
|
54
|
+
reviewSectionTruncated,
|
|
55
|
+
} from './review-diff.mjs';
|
|
51
56
|
import { PATCH_EDIT_MAX_BYTES } from './bounded-file-read.mjs';
|
|
52
57
|
import { cleanString as clean } from './clean.mjs';
|
|
58
|
+
import { createLineDiffBudget, lineDiffHunks, unifiedHunksBody } from './bounded-line-diff.mjs';
|
|
53
59
|
|
|
54
60
|
// Turn-scoped review registry.
|
|
55
61
|
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// the
|
|
59
|
-
//
|
|
60
|
-
//
|
|
62
|
+
// Every file tool write folds its exact before/after bytes into the session's
|
|
63
|
+
// tracker as ordered own segments per file: a write that starts from the
|
|
64
|
+
// bytes the file's last segment ended with extends that segment, any other
|
|
65
|
+
// opens a new one, so whatever changed the file in between (a foreign edit)
|
|
66
|
+
// is in no segment. The turn review, its change record and every Undo/
|
|
67
|
+
// Reapply/latest-turn revert work from those segments only, reverse-applied
|
|
68
|
+
// newest first against the current disk with conflict detection: a foreign
|
|
69
|
+
// edit outside the turn's hunks survives an Undo, and one overlapping them
|
|
70
|
+
// fails it instead of being overwritten. Each file counts the summed line
|
|
71
|
+
// deltas of its own writes.
|
|
72
|
+
//
|
|
73
|
+
// A Git worktree also gets a shadow-index snapshot at turn start: the durable
|
|
74
|
+
// baseline of the session-wide review.
|
|
61
75
|
//
|
|
62
|
-
// Worker
|
|
63
|
-
//
|
|
64
|
-
// to a worktree snapshot's authoritative totals.
|
|
76
|
+
// Worker trackers remain separate until the worker completes: then its
|
|
77
|
+
// segments, owned paths and counts join the owning Lead turn's.
|
|
65
78
|
|
|
66
79
|
const DISABLED = /^(0|false|off)$/i.test(String(process.env.MIXDOG_TURN_SNAPSHOT || ''));
|
|
67
80
|
const TURN_CACHE_MAX = 32;
|
|
@@ -74,6 +87,9 @@ const MAX_AGENT_REVIEW_BYTES_PER_TURN = 4 * 1024 * 1024;
|
|
|
74
87
|
const _turnsBySession = new Map();
|
|
75
88
|
const _diffTrackersBySession = new Map();
|
|
76
89
|
let _agentTurnSeq = 0;
|
|
90
|
+
// Orders every tracked write across trackers, so a worker's segments merge into
|
|
91
|
+
// its Lead's in the order the writes happened.
|
|
92
|
+
let _writeSeq = 0;
|
|
77
93
|
// `${sessionId}\0${checkpointId}` → seals in flight. A sealing turn's change
|
|
78
94
|
// record is `pending` and its session refuses Undo/Reapply until it lands.
|
|
79
95
|
const _pendingFreezes = new Map();
|
|
@@ -143,28 +159,58 @@ function ownedPathsUnder(root, owned) {
|
|
|
143
159
|
return out;
|
|
144
160
|
}
|
|
145
161
|
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
/** Fold one write's line counts into `stats` (owned entry key → summed
|
|
163
|
+
* counts). A rename moves the source's counts to its destination; a change
|
|
164
|
+
* that reports no counts (an Undo/Reapply fold) adds nothing. */
|
|
165
|
+
function foldOwnStats(stats, raw, source, destination) {
|
|
166
|
+
const measured = Number.isFinite(raw?.additions) || Number.isFinite(raw?.deletions);
|
|
167
|
+
const prior = stats.get(source.key);
|
|
168
|
+
if (destination) stats.delete(source.key);
|
|
169
|
+
const target = destination || source;
|
|
170
|
+
const existing = destination ? stats.get(destination.key) : null;
|
|
171
|
+
if (!prior && !existing && !measured) return;
|
|
172
|
+
stats.set(target.key, {
|
|
173
|
+
path: target.path,
|
|
174
|
+
display: target.display,
|
|
175
|
+
additions: (prior?.additions || 0) + (existing?.additions || 0) + (Number(raw?.additions) || 0),
|
|
176
|
+
deletions: (prior?.deletions || 0) + (existing?.deletions || 0) + (Number(raw?.deletions) || 0),
|
|
177
|
+
});
|
|
157
178
|
}
|
|
158
179
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
function mergeOwnStats(target, source) {
|
|
181
|
+
for (const [key, entry] of source) {
|
|
182
|
+
const prior = target.get(key);
|
|
183
|
+
target.set(
|
|
184
|
+
key,
|
|
185
|
+
prior
|
|
186
|
+
? { ...prior, additions: prior.additions + entry.additions, deletions: prior.deletions + entry.deletions }
|
|
187
|
+
: { ...entry }
|
|
188
|
+
);
|
|
166
189
|
}
|
|
167
|
-
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Lookup of the tracker's own-write counts by a reviewed file path: relative
|
|
193
|
+
* to `root`, or (`display`) the tools' display path a tracked patch shows. */
|
|
194
|
+
function ownCountIndex(tracker, root, { display = false } = {}) {
|
|
195
|
+
const keyOf = (path) => pathKey(display ? path : resolve(root, path));
|
|
196
|
+
const index = new Map();
|
|
197
|
+
for (const entry of tracker?.ownStats?.values() || []) index.set(keyOf(display ? entry.display : entry.path), entry);
|
|
198
|
+
return (path) => (path ? index.get(keyOf(path)) : undefined);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Files counted by the summed per-call deltas of their own writes (a
|
|
202
|
+
* rename's old path included). A file no own write reported counts for
|
|
203
|
+
* keeps the counts it has. */
|
|
204
|
+
function withOwnCounts(files, find) {
|
|
205
|
+
return files.map((file) => {
|
|
206
|
+
const hits = [find(file.path), file.oldPath ? find(file.oldPath) : undefined].filter(Boolean);
|
|
207
|
+
if (hits.length === 0) return file;
|
|
208
|
+
return {
|
|
209
|
+
...file,
|
|
210
|
+
additions: hits.reduce((sum, hit) => sum + hit.additions, 0),
|
|
211
|
+
deletions: hits.reduce((sum, hit) => sum + hit.deletions, 0),
|
|
212
|
+
};
|
|
213
|
+
});
|
|
168
214
|
}
|
|
169
215
|
|
|
170
216
|
// A change whose bytes were never captured (binary/oversized) carries
|
|
@@ -219,23 +265,30 @@ function createDiffTracker(meta = {}) {
|
|
|
219
265
|
// The seal (refresh, record, freeze) in flight, if any.
|
|
220
266
|
freezing: null,
|
|
221
267
|
revision: 0,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
268
|
+
// Current path key → { segments }: the file's own segments in write
|
|
269
|
+
// order, each `{ before, after, firstSeq, lastSeq }` whose sides are
|
|
270
|
+
// `{ path, displayPath, content }` (content null = absent).
|
|
271
|
+
chains: new Map(),
|
|
272
|
+
// Current path key → { chain, owned, stats } of a chain an Undo restored,
|
|
273
|
+
// kept so a Reapply puts it back.
|
|
274
|
+
undoneChains: new Map(),
|
|
275
|
+
// The review rendered for one revision (trackerView).
|
|
276
|
+
view: null,
|
|
227
277
|
worktreeSnapshot: null,
|
|
228
278
|
// Session-owned relative paths. These outlive the before/after buffers on
|
|
229
279
|
// purpose: they are what scopes a revert once the exact content is gone.
|
|
230
280
|
ownedPaths: new Set(),
|
|
281
|
+
// Owned entry key → { path, display, additions, deletions }: the summed
|
|
282
|
+
// line deltas of every own write of the file. Outlives the buffers too.
|
|
283
|
+
ownStats: new Map(),
|
|
231
284
|
};
|
|
232
285
|
}
|
|
233
286
|
|
|
234
287
|
function releaseDiffTrackerContent(tracker) {
|
|
235
288
|
if (!tracker) return;
|
|
236
|
-
tracker.
|
|
237
|
-
tracker.
|
|
238
|
-
tracker.
|
|
289
|
+
tracker.chains.clear();
|
|
290
|
+
tracker.undoneChains.clear();
|
|
291
|
+
tracker.view = null;
|
|
239
292
|
}
|
|
240
293
|
|
|
241
294
|
function resetDiffTracker(sessionId, meta = {}) {
|
|
@@ -248,253 +301,271 @@ function resetDiffTracker(sessionId, meta = {}) {
|
|
|
248
301
|
|
|
249
302
|
function trackedContentBytes(tracker) {
|
|
250
303
|
let total = 0;
|
|
251
|
-
for (const
|
|
252
|
-
|
|
304
|
+
for (const chain of tracker.chains.values()) {
|
|
305
|
+
for (const { before, after } of chain.segments) {
|
|
306
|
+
total += contentByteLength(before.content) + contentByteLength(after.content);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
253
309
|
return total;
|
|
254
310
|
}
|
|
255
311
|
|
|
256
|
-
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
312
|
+
/** A segment that leaves its file where and as it found it. */
|
|
313
|
+
function noopSegment({ before, after }) {
|
|
314
|
+
return pathKey(before.path) === pathKey(after.path) && sameContent(before.content, after.content);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Join one own segment to the chain of the file it starts at. It extends the
|
|
318
|
+
* chain's last segment when it starts from that segment's exact after bytes;
|
|
319
|
+
* otherwise something else changed the file in between, and it opens a new
|
|
320
|
+
* segment so that change stays out of both. A move onto a path another chain
|
|
321
|
+
* holds lands as a delete of its source and a write of its destination. */
|
|
322
|
+
function appendSegment(tracker, segment) {
|
|
323
|
+
const startKey = pathKey(segment.before.path);
|
|
324
|
+
const endKey = pathKey(segment.after.path);
|
|
325
|
+
if (startKey !== endKey && tracker.chains.has(endKey)) {
|
|
326
|
+
appendSegment(tracker, { ...segment, after: { ...segment.before, content: null } });
|
|
327
|
+
appendSegment(tracker, { ...segment, before: { ...segment.after, content: null } });
|
|
328
|
+
return;
|
|
263
329
|
}
|
|
330
|
+
const chain = tracker.chains.get(startKey) || { segments: [] };
|
|
331
|
+
tracker.chains.delete(startKey);
|
|
332
|
+
const last = chain.segments.at(-1);
|
|
333
|
+
if (last && sameContent(last.after.content, segment.before.content)) {
|
|
334
|
+
last.after = segment.after;
|
|
335
|
+
last.lastSeq = segment.lastSeq;
|
|
336
|
+
} else {
|
|
337
|
+
chain.segments.push({ ...segment });
|
|
338
|
+
}
|
|
339
|
+
tracker.chains.set(endKey, chain);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const isBinarySide = (content) => isOmittedContent(content) || isBinaryChangeContent(content);
|
|
343
|
+
|
|
344
|
+
/** What a chain records, relative to `root`: its segments that change
|
|
345
|
+
* something — of a binary file only the last, since bytes from before a
|
|
346
|
+
* foreign rewrite cannot be restored around it — as steps with the
|
|
347
|
+
* root-relative paths of their sides ('' outside the root). `start`/`end`
|
|
348
|
+
* are the sides the change begins and ends at (a created file has no old
|
|
349
|
+
* side, a deleted one no new side). Null when the chain changes nothing. */
|
|
350
|
+
function chainEdit(chain, root) {
|
|
351
|
+
let segments = chain.segments.filter((segment) => !noopSegment(segment));
|
|
352
|
+
if (segments.length === 0) return null;
|
|
353
|
+
const binary = segments.some(({ before, after }) => [before.content, after.content].some(isBinarySide));
|
|
354
|
+
if (binary) segments = segments.slice(-1);
|
|
355
|
+
const steps = segments.map(({ before, after }) => ({
|
|
356
|
+
before,
|
|
357
|
+
after,
|
|
358
|
+
oldPath: trackedRelativePath(root, before),
|
|
359
|
+
newPath: trackedRelativePath(root, after),
|
|
360
|
+
}));
|
|
361
|
+
const first = steps[0];
|
|
362
|
+
const last = steps.at(-1);
|
|
363
|
+
const start = first.before.content === null ? first.after : first.before;
|
|
364
|
+
const end = last.after.content === null ? last.before : last.after;
|
|
365
|
+
let status = 'M';
|
|
366
|
+
if (first.before.content === null && last.after.content !== null) status = 'A';
|
|
367
|
+
else if (last.after.content === null && first.before.content !== null) status = 'D';
|
|
368
|
+
else if (pathKey(start.path) !== pathKey(end.path)) status = 'R';
|
|
369
|
+
return {
|
|
370
|
+
steps,
|
|
371
|
+
start,
|
|
372
|
+
end,
|
|
373
|
+
status,
|
|
374
|
+
binary,
|
|
375
|
+
inside: steps.every((step) => step.oldPath && step.newPath),
|
|
376
|
+
omitted: steps.some(({ before, after }) => [before.content, after.content].some(isOmittedContent)),
|
|
377
|
+
};
|
|
264
378
|
}
|
|
265
379
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
380
|
+
/** A side's name in the review: relative to `root` inside it, else the
|
|
381
|
+
* tool's display path. */
|
|
382
|
+
const viewPath = (root, entry) => trackedRelativePath(root, entry) || displayPath(entry.displayPath || entry.path);
|
|
383
|
+
|
|
384
|
+
/** One step's display section, named as the review shows it, with its line
|
|
385
|
+
* counts: hunks, a `Binary files … differ` line, or the not-captured note. */
|
|
386
|
+
function renderStep({ before, after }, root, budget) {
|
|
387
|
+
const oldGit = `a/${viewPath(root, before)}`;
|
|
388
|
+
const newGit = `b/${viewPath(root, after)}`;
|
|
389
|
+
const oldHeader = before.content === null ? '/dev/null' : oldGit;
|
|
390
|
+
const newHeader = after.content === null ? '/dev/null' : newGit;
|
|
273
391
|
let head = `diff --git ${oldGit} ${newGit}\n`;
|
|
274
|
-
if (before
|
|
275
|
-
else if (
|
|
276
|
-
const
|
|
392
|
+
if (before.content === null) head += 'new file mode 100644\n';
|
|
393
|
+
else if (after.content === null) head += 'deleted file mode 100644\n';
|
|
394
|
+
const binaryLine = `Binary files ${oldHeader} and ${newHeader} differ\n`;
|
|
395
|
+
const omitted = [before.content, after.content].find(isOmittedContent);
|
|
277
396
|
if (omitted) {
|
|
278
397
|
let change = 'changed';
|
|
279
|
-
if (after
|
|
280
|
-
else if (before
|
|
281
|
-
|
|
398
|
+
if (after.content === null) change = 'deleted';
|
|
399
|
+
else if (before.content === null) change = 'added';
|
|
400
|
+
const note = `binary/large file ${change} (${omitted.size} bytes; content not captured)\n`;
|
|
401
|
+
return { text: `${head}${note}${binaryLine}`, additions: 0, deletions: 0 };
|
|
282
402
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (oldText === null || newText === null) {
|
|
286
|
-
return `${head}Binary files ${oldHeader} and ${newHeader} differ\n`;
|
|
403
|
+
if ([before.content, after.content].some(isBinaryChangeContent)) {
|
|
404
|
+
return { text: `${head}${binaryLine}`, additions: 0, deletions: 0 };
|
|
287
405
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
406
|
+
const text = (content) => (content === null ? '' : content.toString('utf8'));
|
|
407
|
+
const { hunks } = lineDiffHunks(text(before.content), text(after.content), budget, [before.content, after.content]);
|
|
408
|
+
let additions = 0;
|
|
409
|
+
let deletions = 0;
|
|
410
|
+
for (const line of hunks.flatMap((hunk) => hunk.lines)) {
|
|
411
|
+
if (line.startsWith('+')) additions += 1;
|
|
412
|
+
else if (line.startsWith('-')) deletions += 1;
|
|
295
413
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
414
|
+
return { text: `${head}${unifiedHunksBody(oldHeader, newHeader, hunks)}`, additions, deletions };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The tracker's own changes as the review shows them: a row per file with
|
|
418
|
+
* its own counts, and the display patch of its segments, named relative to
|
|
419
|
+
* the tracker's patch root (a worker without one: the tools' display
|
|
420
|
+
* paths). Beside a Git root a file outside it is left out, as from the
|
|
421
|
+
* record's undoable files. Cached per revision; `budget` bounds a render. */
|
|
422
|
+
function trackerView(tracker, budget = createLineDiffBudget()) {
|
|
423
|
+
const { root, kind } = trackerPatchRoot(tracker);
|
|
424
|
+
if (tracker.view?.revision === tracker.revision && tracker.view.root === root) return tracker.view;
|
|
425
|
+
const rows = [];
|
|
304
426
|
if (!tracker.valid) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const renamePairs = new Map();
|
|
309
|
-
const pairedDestinations = new Set();
|
|
310
|
-
for (const [destinationKey, originKey] of tracker.originByCurrentPath) {
|
|
311
|
-
if (
|
|
312
|
-
destinationKey === originKey ||
|
|
313
|
-
tracker.currentByPath.has(originKey) ||
|
|
314
|
-
!tracker.currentByPath.has(destinationKey) ||
|
|
315
|
-
!tracker.baselineByPath.has(originKey) ||
|
|
316
|
-
tracker.baselineByPath.has(destinationKey)
|
|
317
|
-
) {
|
|
318
|
-
continue;
|
|
427
|
+
// The content is gone: which files changed is known, their diff is not.
|
|
428
|
+
for (const path of ownedPathsUnder(root, tracker.ownedPaths)) {
|
|
429
|
+
rows.push({ path, status: 'M', additions: 0, deletions: 0, text: '' });
|
|
319
430
|
}
|
|
320
|
-
renamePairs.set(originKey, destinationKey);
|
|
321
|
-
pairedDestinations.add(destinationKey);
|
|
322
431
|
}
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
let unifiedDiff = '';
|
|
342
|
-
for (const [before, after] of pairs) {
|
|
343
|
-
const beforeContent = before?.content ?? null;
|
|
344
|
-
const afterContent = after?.content ?? null;
|
|
345
|
-
const samePath = before?.displayPath === after?.displayPath;
|
|
346
|
-
if (samePath && sameContent(beforeContent, afterContent)) continue;
|
|
347
|
-
const rendered = renderTrackedPair(
|
|
348
|
-
before || { displayPath: after?.displayPath, content: null },
|
|
349
|
-
after || { displayPath: before?.displayPath, content: null }
|
|
350
|
-
);
|
|
351
|
-
unifiedDiff += rendered;
|
|
352
|
-
}
|
|
353
|
-
const bounded = boundReviewPatch(unifiedDiff, MAX_PATCH_BYTES);
|
|
354
|
-
tracker.unifiedDiff = bounded.patch;
|
|
355
|
-
tracker.patchTruncated = bounded.truncated;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function trackedPairs(tracker) {
|
|
359
|
-
const pairs = [];
|
|
360
|
-
const handledOrigins = new Set();
|
|
361
|
-
for (const [destinationKey, after] of tracker.currentByPath) {
|
|
362
|
-
const originKey = tracker.originByCurrentPath.get(destinationKey) || destinationKey;
|
|
363
|
-
handledOrigins.add(originKey);
|
|
364
|
-
pairs.push({
|
|
365
|
-
originKey,
|
|
366
|
-
destinationKey,
|
|
367
|
-
before: tracker.baselineByPath.get(originKey) || null,
|
|
368
|
-
after,
|
|
432
|
+
for (const chain of tracker.chains.values()) {
|
|
433
|
+
const edit = chainEdit(chain, root);
|
|
434
|
+
if (!edit || (kind === 'git' && !edit.inside)) continue;
|
|
435
|
+
const rendered = edit.steps.map((step) => renderStep(step, root, budget));
|
|
436
|
+
const path = viewPath(root, edit.end);
|
|
437
|
+
const oldPath = viewPath(root, edit.start);
|
|
438
|
+
const omitted = edit.steps.flatMap(({ before, after }) => [before.content, after.content]).find(isOmittedContent);
|
|
439
|
+
rows.push({
|
|
440
|
+
path,
|
|
441
|
+
...(oldPath !== path ? { oldPath } : {}),
|
|
442
|
+
status: edit.status,
|
|
443
|
+
additions: rendered.reduce((sum, step) => sum + step.additions, 0),
|
|
444
|
+
deletions: rendered.reduce((sum, step) => sum + step.deletions, 0),
|
|
445
|
+
...(edit.binary ? { binary: true } : {}),
|
|
446
|
+
// Its bytes were never captured: listed, never diffed or undone.
|
|
447
|
+
...(omitted ? { omitted: true, size: omitted.size } : {}),
|
|
448
|
+
text: rendered.map((step) => step.text).join(''),
|
|
369
449
|
});
|
|
370
450
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return target;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function pairMatchesFile(pair, worktree, file) {
|
|
389
|
-
const requested = safeTrackedTarget(worktree, file);
|
|
390
|
-
return [pair.before, pair.after].some((entry) => {
|
|
391
|
-
if (!entry) return false;
|
|
392
|
-
const values = [entry.path, entry.displayPath].filter(Boolean);
|
|
393
|
-
return values.some((value) => safeTrackedTarget(worktree, value) === requested);
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function currentTrackedContent(path) {
|
|
398
|
-
try {
|
|
399
|
-
const stat = await lstat(path);
|
|
400
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
401
|
-
throw new Error('turn review revert only supports regular files');
|
|
402
|
-
}
|
|
403
|
-
return await readFile(path);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (error?.code === 'ENOENT') return null;
|
|
406
|
-
throw error;
|
|
407
|
-
}
|
|
451
|
+
rows.sort((left, right) => left.path.localeCompare(right.path));
|
|
452
|
+
const bounded = boundReviewPatch(rows.map((row) => row.text).join(''), MAX_PATCH_BYTES);
|
|
453
|
+
const counted = root ? ownCountIndex(tracker, root) : ownCountIndex(tracker, '', { display: true });
|
|
454
|
+
tracker.view = {
|
|
455
|
+
revision: tracker.revision,
|
|
456
|
+
root,
|
|
457
|
+
files: withOwnCounts(
|
|
458
|
+
rows.map(({ text, ...row }) => row),
|
|
459
|
+
counted
|
|
460
|
+
),
|
|
461
|
+
patch: bounded.patch,
|
|
462
|
+
truncated: bounded.truncated,
|
|
463
|
+
};
|
|
464
|
+
return tracker.view;
|
|
408
465
|
}
|
|
409
466
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
);
|
|
422
|
-
const afterPath = safeTrackedTarget(
|
|
423
|
-
worktree,
|
|
424
|
-
pair.after?.path || pair.after?.displayPath || pair.before?.path || pair.before?.displayPath
|
|
467
|
+
/** True when `value` (absolute, or relative to `root`) names a path the chain
|
|
468
|
+
* touched. */
|
|
469
|
+
function chainMatchesPath(chain, root, value) {
|
|
470
|
+
const target = clean(value);
|
|
471
|
+
if (!target) return false;
|
|
472
|
+
const key = changePathKey(relative(root, isAbsolute(target) ? resolve(target) : resolve(root, target)));
|
|
473
|
+
return chain.segments.some((segment) =>
|
|
474
|
+
[segment.before, segment.after].some((entry) => {
|
|
475
|
+
const path = trackedRelativePath(root, entry);
|
|
476
|
+
return Boolean(path) && changePathKey(path) === key;
|
|
477
|
+
})
|
|
425
478
|
);
|
|
426
|
-
const expectedAfter = pair.after?.content ?? null;
|
|
427
|
-
const actualAfter = await currentTrackedContent(afterPath);
|
|
428
|
-
if (!sameContent(actualAfter, expectedAfter)) {
|
|
429
|
-
throw new Error('turn review revert refused because the file changed after this session edit');
|
|
430
|
-
}
|
|
431
|
-
if (beforePath !== afterPath && (await currentTrackedContent(beforePath)) !== null) {
|
|
432
|
-
throw new Error('turn review revert refused because the original path changed after this session edit');
|
|
433
|
-
}
|
|
434
|
-
return { beforePath, afterPath };
|
|
435
479
|
}
|
|
436
480
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
481
|
+
/** The owned entries (and their counts) of every path a chain touched. */
|
|
482
|
+
function chainOwnership(tracker, chain) {
|
|
483
|
+
const owned = [
|
|
484
|
+
...new Set(
|
|
485
|
+
chain.segments
|
|
486
|
+
.flatMap((segment) => [segment.before, segment.after])
|
|
487
|
+
.map((entry) => ownedPathEntry(entry.path, displayPath(entry.displayPath || entry.path)))
|
|
488
|
+
),
|
|
489
|
+
].filter((entry) => tracker.ownedPaths.has(entry));
|
|
490
|
+
const stats = owned
|
|
491
|
+
.map((entry) => [pathKey(entry), tracker.ownStats.get(pathKey(entry))])
|
|
492
|
+
.filter(([, entry]) => entry);
|
|
493
|
+
return { owned, stats };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** An Undo reverse-applied these chains: the turn no longer changes them, so
|
|
497
|
+
* they leave its review. Each is kept for a Reapply. */
|
|
498
|
+
function undoTrackedChains(tracker, chains) {
|
|
499
|
+
for (const [key, chain] of tracker.chains) {
|
|
500
|
+
if (!chains.includes(chain)) continue;
|
|
501
|
+
const ownership = chainOwnership(tracker, chain);
|
|
502
|
+
tracker.undoneChains.set(key, { chain, ...ownership });
|
|
503
|
+
for (const entry of ownership.owned) tracker.ownedPaths.delete(entry);
|
|
504
|
+
for (const [statKey] of ownership.stats) tracker.ownStats.delete(statKey);
|
|
505
|
+
tracker.chains.delete(key);
|
|
444
506
|
}
|
|
507
|
+
tracker.revision += 1;
|
|
445
508
|
}
|
|
446
509
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
tracker.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
// Validate every current file before the first write so an external edit
|
|
460
|
-
// cannot leave a turn-wide revert half-applied.
|
|
461
|
-
const validated = [];
|
|
462
|
-
for (const pair of pairs) validated.push(await validateTrackedPair(pair, worktree));
|
|
463
|
-
for (let index = 0; index < pairs.length; index += 1) {
|
|
464
|
-
await restoreTrackedPair(pairs[index], validated[index]);
|
|
465
|
-
commitTrackedPairRestore(tracker, pairs[index]);
|
|
510
|
+
/** A Reapply forward-applied the undone chains matching `files` again. */
|
|
511
|
+
function redoTrackedChains(tracker, root, files) {
|
|
512
|
+
for (const [key, undone] of tracker.undoneChains) {
|
|
513
|
+
const matches = files.some((file) =>
|
|
514
|
+
[file.path, file.oldPath].some((path) => chainMatchesPath(undone.chain, root, path))
|
|
515
|
+
);
|
|
516
|
+
if (!matches) continue;
|
|
517
|
+
tracker.undoneChains.delete(key);
|
|
518
|
+
tracker.chains.set(key, undone.chain);
|
|
519
|
+
for (const entry of undone.owned) tracker.ownedPaths.add(entry);
|
|
520
|
+
for (const [statKey, entry] of undone.stats) tracker.ownStats.set(statKey, entry);
|
|
466
521
|
}
|
|
467
522
|
tracker.revision += 1;
|
|
468
|
-
refreshUnifiedDiff(tracker);
|
|
469
|
-
if (!tracker.unifiedDiff) releaseDiffTrackerContent(tracker);
|
|
470
|
-
// The absolute paths now back at their turn-start bytes.
|
|
471
|
-
return [...new Set(validated.flatMap(({ beforePath, afterPath }) => [beforePath, afterPath]))];
|
|
472
523
|
}
|
|
473
524
|
|
|
474
|
-
/** A
|
|
475
|
-
|
|
476
|
-
|
|
525
|
+
/** A new write supersedes whatever an Undo set aside for the paths it
|
|
526
|
+
* touches (`keys`). */
|
|
527
|
+
function dropUndoneChains(tracker, keys) {
|
|
528
|
+
for (const [key, undone] of tracker.undoneChains) {
|
|
529
|
+
const touched = undone.chain.segments.some(
|
|
530
|
+
(segment) => keys.has(pathKey(segment.before.path)) || keys.has(pathKey(segment.after.path))
|
|
531
|
+
);
|
|
532
|
+
if (touched) tracker.undoneChains.delete(key);
|
|
533
|
+
}
|
|
477
534
|
}
|
|
478
535
|
|
|
479
536
|
/**
|
|
480
537
|
* Fold exact committed before/after file states into a session's current turn.
|
|
481
538
|
* A change is `{ path, displayPath, before, after }`; a rename additionally
|
|
482
|
-
* carries `{ newPath, newDisplayPath }`.
|
|
539
|
+
* carries `{ newPath, newDisplayPath }`. `budget` is the line-diff budget of
|
|
540
|
+
* the tool call the changes come from. Returns the turn's review patch.
|
|
483
541
|
*/
|
|
484
|
-
export function recordTurnDiffChanges(sessionId, changes = []) {
|
|
542
|
+
export function recordTurnDiffChanges(sessionId, changes = [], budget = createLineDiffBudget()) {
|
|
485
543
|
if (DISABLED) return '';
|
|
486
544
|
const id = clean(sessionId);
|
|
487
545
|
if (!id) return '';
|
|
488
546
|
const tracker = _diffTrackersBySession.get(id) || resetDiffTracker(id);
|
|
489
|
-
const list = Array.isArray(changes) ? changes : [];
|
|
490
547
|
const ownerTracker = tracker?.ownerSessionId ? _diffTrackersBySession.get(tracker.ownerSessionId) : tracker;
|
|
491
548
|
const snapshot = ownerTracker?.worktreeSnapshot;
|
|
549
|
+
const raws = Array.isArray(changes) ? changes : [];
|
|
550
|
+
// Git reports the turn's root canonically (macOS /var -> /private/var):
|
|
551
|
+
// each written path is spelled the same way, or it never relates to it.
|
|
552
|
+
const list = snapshot?.root ? raws.map(canonicalChangePaths) : raws;
|
|
492
553
|
// Independent of this tracker's validity: the session review needs every
|
|
493
554
|
// write's first-seen state even once the turn's buffers are gone.
|
|
494
555
|
captureReviewBaselines(id, tracker, ownerTracker, list, snapshot);
|
|
495
|
-
if (!tracker
|
|
496
|
-
foldTrackedChanges(tracker, list, snapshot);
|
|
497
|
-
return tracker.
|
|
556
|
+
if (!tracker) return '';
|
|
557
|
+
if (tracker.valid && !tracker.sealed) foldTrackedChanges(tracker, list, snapshot);
|
|
558
|
+
return trackerView(tracker, budget).patch;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function canonicalChangePaths(raw) {
|
|
562
|
+
const path = clean(raw?.path);
|
|
563
|
+
const newPath = clean(raw?.newPath);
|
|
564
|
+
return {
|
|
565
|
+
...raw,
|
|
566
|
+
...(isAbsolute(path) ? { path: canonicalPath(path) } : {}),
|
|
567
|
+
...(isAbsolute(newPath) ? { newPath: canonicalPath(newPath) } : {}),
|
|
568
|
+
};
|
|
498
569
|
}
|
|
499
570
|
|
|
500
571
|
const insidePath = (root, path) => {
|
|
@@ -528,9 +599,9 @@ function captureReviewBaselines(sessionId, tracker, ownerTracker, list, snapshot
|
|
|
528
599
|
captureSessionBaselines(sessionId, changes, { owner: tracker?.ownerSessionId || '' });
|
|
529
600
|
}
|
|
530
601
|
|
|
531
|
-
/** Fold exact before/after states into `tracker
|
|
532
|
-
* Lead turn's Git baseline, when
|
|
533
|
-
* path's first-before bytes. */
|
|
602
|
+
/** Fold exact before/after states into `tracker` as own segments (see
|
|
603
|
+
* appendSegment). `snapshot` — the owning Lead turn's Git baseline, when
|
|
604
|
+
* tools are writing — learns each written path's first-before bytes. */
|
|
534
605
|
function foldTrackedChanges(tracker, list, snapshot) {
|
|
535
606
|
// The content ceiling is enforced BEFORE anything is stored: an over-budget
|
|
536
607
|
// batch invalidates the tracker up front instead of retaining its buffers.
|
|
@@ -559,63 +630,68 @@ function foldTrackedChanges(tracker, list, snapshot) {
|
|
|
559
630
|
// attribution a shared worktree cannot otherwise reconstruct. It keeps the
|
|
560
631
|
// ABSOLUTE file: the display path is relative to the session cwd, which
|
|
561
632
|
// is not necessarily the repository root the scoped diff runs from.
|
|
562
|
-
|
|
633
|
+
const sourceEntry = ownedPathEntry(sourcePath, sourceDisplay);
|
|
634
|
+
tracker.ownedPaths.add(sourceEntry);
|
|
635
|
+
let destination = null;
|
|
563
636
|
if (destinationPath) {
|
|
564
|
-
|
|
637
|
+
const display = displayPath(raw?.newDisplayPath || destinationPath);
|
|
638
|
+
const entry = ownedPathEntry(destinationPath, display);
|
|
639
|
+
tracker.ownedPaths.add(entry);
|
|
640
|
+
destination = { key: pathKey(entry), path: entry, display };
|
|
565
641
|
}
|
|
642
|
+
foldOwnStats(
|
|
643
|
+
tracker.ownStats,
|
|
644
|
+
raw,
|
|
645
|
+
{ key: pathKey(sourceEntry), path: sourceEntry, display: sourceDisplay },
|
|
646
|
+
destination
|
|
647
|
+
);
|
|
566
648
|
if (!storeContent) continue;
|
|
567
649
|
const after = contentBuffer(raw?.after);
|
|
568
|
-
if (!destinationPath)
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (!tracker.currentByPath.delete(sourceKey) && !tracker.baselineByPath.has(sourceKey)) {
|
|
580
|
-
tracker.baselineByPath.set(sourceKey, {
|
|
581
|
-
path: sourcePath,
|
|
582
|
-
displayPath: sourceDisplay,
|
|
583
|
-
content: before,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
tracker.originByCurrentPath.delete(sourceKey);
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
if (before === null && after === null) continue;
|
|
590
|
-
recordFirstBaseline(tracker, sourceKey, { path: sourcePath, displayPath: sourceDisplay, content: before });
|
|
591
|
-
tracker.currentByPath.set(sourceKey, {
|
|
592
|
-
path: sourcePath,
|
|
593
|
-
displayPath: sourceDisplay,
|
|
594
|
-
content: after,
|
|
595
|
-
});
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const destinationKey = pathKey(destinationPath);
|
|
600
|
-
const destinationDisplay = displayPath(raw?.newDisplayPath || destinationPath);
|
|
601
|
-
recordFirstBaseline(tracker, sourceKey, { path: sourcePath, displayPath: sourceDisplay, content: before });
|
|
602
|
-
const originKey = tracker.originByCurrentPath.get(sourceKey) || sourceKey;
|
|
603
|
-
tracker.currentByPath.delete(sourceKey);
|
|
604
|
-
tracker.originByCurrentPath.delete(sourceKey);
|
|
605
|
-
tracker.currentByPath.set(destinationKey, {
|
|
606
|
-
path: destinationPath,
|
|
607
|
-
displayPath: destinationDisplay,
|
|
608
|
-
content: after,
|
|
650
|
+
if (!destinationPath && before === null && after === null) continue;
|
|
651
|
+
dropUndoneChains(tracker, new Set([sourceKey, ...(destination ? [pathKey(destinationPath)] : [])]));
|
|
652
|
+
_writeSeq += 1;
|
|
653
|
+
const seq = _writeSeq;
|
|
654
|
+
appendSegment(tracker, {
|
|
655
|
+
before: { path: sourcePath, displayPath: sourceDisplay, content: before },
|
|
656
|
+
after: destination
|
|
657
|
+
? { path: destinationPath, displayPath: destination.display, content: after }
|
|
658
|
+
: { path: sourcePath, displayPath: sourceDisplay, content: after },
|
|
659
|
+
firstSeq: seq,
|
|
660
|
+
lastSeq: seq,
|
|
609
661
|
});
|
|
610
|
-
tracker.originByCurrentPath.delete(destinationKey);
|
|
611
|
-
if (destinationKey !== originKey) tracker.originByCurrentPath.set(destinationKey, originKey);
|
|
612
662
|
}
|
|
613
663
|
tracker.revision += 1;
|
|
614
664
|
if (trackedContentBytes(tracker) > MAX_TRACKED_CONTENT_BYTES) {
|
|
615
665
|
tracker.valid = false;
|
|
616
666
|
releaseDiffTrackerContent(tracker);
|
|
617
667
|
}
|
|
618
|
-
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Join a completed worker's own segments to its Lead's tracker: both sets
|
|
671
|
+
* replay in write order, so a worker write between two Lead writes of one
|
|
672
|
+
* file continues their segment instead of reading as a foreign gap. A
|
|
673
|
+
* worker whose content was dropped drops the Lead's exact content too. */
|
|
674
|
+
function mergeTrackedContent(target, source) {
|
|
675
|
+
// The worker's owned paths and counts joined as well: any view is stale.
|
|
676
|
+
target.revision += 1;
|
|
677
|
+
if (!target.valid) return;
|
|
678
|
+
if (!source.valid) {
|
|
679
|
+
target.valid = false;
|
|
680
|
+
releaseDiffTrackerContent(target);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const segmentsOf = (tracker) => [...tracker.chains.values()].flatMap((chain) => chain.segments);
|
|
684
|
+
const written = new Set(
|
|
685
|
+
segmentsOf(source).flatMap((segment) => [pathKey(segment.before.path), pathKey(segment.after.path)])
|
|
686
|
+
);
|
|
687
|
+
const segments = [...segmentsOf(target), ...segmentsOf(source)].sort((left, right) => left.firstSeq - right.firstSeq);
|
|
688
|
+
target.chains = new Map();
|
|
689
|
+
for (const segment of segments) appendSegment(target, segment);
|
|
690
|
+
dropUndoneChains(target, written);
|
|
691
|
+
if (trackedContentBytes(target) > MAX_TRACKED_CONTENT_BYTES) {
|
|
692
|
+
target.valid = false;
|
|
693
|
+
releaseDiffTrackerContent(target);
|
|
694
|
+
}
|
|
619
695
|
}
|
|
620
696
|
|
|
621
697
|
function trimAgentReviews(turn) {
|
|
@@ -680,13 +756,14 @@ export async function beginTurnSnapshot(worktree, sessionId, options = {}) {
|
|
|
680
756
|
// Turn start and Undo/Reapply share the per-session lock: a start waits for
|
|
681
757
|
// an in-flight mutation, and a mutation queued after this start re-checks
|
|
682
758
|
// under the lock and finds the turn running.
|
|
683
|
-
//
|
|
684
|
-
// this same step
|
|
685
|
-
// the
|
|
759
|
+
// Late-worker re-seals still queued for the previous turn run first, in
|
|
760
|
+
// this same step — including one a worker queued while an earlier one ran —
|
|
761
|
+
// so the workers' edits stay in that turn's record and nothing the new turn
|
|
762
|
+
// writes can reach it. (A queued re-seal implies a chain.)
|
|
686
763
|
const { token, tracker } = _changeMutations.has(ownerSessionId)
|
|
687
764
|
? await serializeChangeMutation(ownerSessionId, async () => {
|
|
688
765
|
const previous = _diffTrackersBySession.get(ownerSessionId);
|
|
689
|
-
|
|
766
|
+
while (previous?.resealQueued) await runQueuedReseal(ownerSessionId, previous).catch(() => {});
|
|
690
767
|
return openTurnSnapshot(worktree, ownerSessionId, options);
|
|
691
768
|
})
|
|
692
769
|
: openTurnSnapshot(worktree, ownerSessionId, options);
|
|
@@ -780,24 +857,29 @@ export function beginAgentTurnReview(ownerSessionId, childSessionId, meta = {})
|
|
|
780
857
|
};
|
|
781
858
|
}
|
|
782
859
|
|
|
783
|
-
/** Freeze successful worker apply_patch diffs into their owning Lead turn.
|
|
860
|
+
/** Freeze successful worker apply_patch diffs into their owning Lead turn.
|
|
861
|
+
* Everything a seal reads — the agent entry, the worker's owned paths and a
|
|
862
|
+
* late re-seal — is published before this returns, so a seal started right
|
|
863
|
+
* after it already includes the worker. */
|
|
784
864
|
export function completeAgentTurnReview(handle, patches = []) {
|
|
785
865
|
if (!handle || handle.completed === true) return false;
|
|
786
866
|
handle.completed = true;
|
|
787
|
-
const
|
|
867
|
+
const ownerSessionId = clean(handle.ownerSessionId);
|
|
788
868
|
const key = clean(handle.sessionId) || handle.id;
|
|
869
|
+
const turn = _turnsBySession.get(ownerSessionId);
|
|
789
870
|
const tracker = _diffTrackersBySession.get(key);
|
|
790
871
|
const matchingTracker =
|
|
791
|
-
tracker && tracker.ownerSessionId ===
|
|
872
|
+
tracker && tracker.ownerSessionId === ownerSessionId && tracker.ownerGeneration === handle.generation;
|
|
792
873
|
// A worker that runs its own session turn replaces the bound tracker with
|
|
793
874
|
// its turn's own one (no owner). beginAgentTurnReview reset any older
|
|
794
875
|
// tracker, so an unowned one here holds this execution's edits.
|
|
795
876
|
const source = matchingTracker || (tracker && !tracker.ownerSessionId) ? tracker : null;
|
|
877
|
+
const ownerTracker = _diffTrackersBySession.get(ownerSessionId);
|
|
796
878
|
try {
|
|
797
879
|
if (!turn || turn.generation !== handle.generation) return false;
|
|
798
880
|
const prior = turn.agents.get(key);
|
|
799
881
|
const tracked = Boolean(source) && source.revision > 0;
|
|
800
|
-
const patch = tracked ? source.
|
|
882
|
+
const patch = tracked ? trackerView(source).patch : mergePatches(patches);
|
|
801
883
|
if (!patch) {
|
|
802
884
|
if (tracked && prior) {
|
|
803
885
|
turn.agents.delete(key);
|
|
@@ -812,15 +894,17 @@ export function completeAgentTurnReview(handle, patches = []) {
|
|
|
812
894
|
tag: handle.tag || prior?.tag || null,
|
|
813
895
|
patch: tracked ? patch : mergePatches([prior?.patch, patch]),
|
|
814
896
|
});
|
|
815
|
-
const ownerTracker = _diffTrackersBySession.get(clean(handle.ownerSessionId));
|
|
816
897
|
if (source && ownerTracker && ownerTracker !== source) {
|
|
817
898
|
for (const path of source.ownedPaths) ownerTracker.ownedPaths.add(path);
|
|
899
|
+
mergeOwnStats(ownerTracker.ownStats, source.ownStats);
|
|
900
|
+
mergeTrackedContent(ownerTracker, source);
|
|
818
901
|
}
|
|
819
902
|
trimAgentReviews(turn);
|
|
820
|
-
// A worker that finishes after its Lead turn sealed
|
|
821
|
-
// change record (its edits are part of the turn's net
|
|
903
|
+
// A worker that finishes after its Lead turn sealed (or while it seals)
|
|
904
|
+
// re-seals that turn's change record (its edits are part of the turn's net
|
|
905
|
+
// worktree diff).
|
|
822
906
|
if (ownerTracker && ownerTracker !== tracker && ownerTracker.ownerGeneration === handle.generation) {
|
|
823
|
-
resealTurn(
|
|
907
|
+
resealTurn(ownerSessionId, ownerTracker);
|
|
824
908
|
}
|
|
825
909
|
return true;
|
|
826
910
|
} finally {
|
|
@@ -839,7 +923,7 @@ export function completeAgentTurnReview(handle, patches = []) {
|
|
|
839
923
|
async function persistTurnSnapshotRecord(ownerSessionId, tracker) {
|
|
840
924
|
const snapshot = tracker?.worktreeSnapshot;
|
|
841
925
|
if (!ownerSessionId || tracker.discarded || !snapshot?.baselineTree || !snapshot.root) return;
|
|
842
|
-
const owned = [...
|
|
926
|
+
const owned = [...ownedPathsUnder(snapshot.root, tracker.ownedPaths)];
|
|
843
927
|
if (owned.length === 0) return;
|
|
844
928
|
try {
|
|
845
929
|
await saveTurnSnapshotRecord(ownerSessionId, {
|
|
@@ -868,10 +952,11 @@ function attributedAgents(ownerSessionId, tracker) {
|
|
|
868
952
|
}));
|
|
869
953
|
}
|
|
870
954
|
|
|
871
|
-
/** Path of a tracked entry relative to `root`, or '' outside it
|
|
955
|
+
/** Path of a tracked entry relative to `root`, or '' outside it (or without
|
|
956
|
+
* a root). */
|
|
872
957
|
function trackedRelativePath(root, entry) {
|
|
873
958
|
const value = clean(entry?.path) || clean(entry?.displayPath);
|
|
874
|
-
if (!value) return '';
|
|
959
|
+
if (!root || !value) return '';
|
|
875
960
|
const rel = relative(root, isAbsolute(value) ? resolve(value) : resolve(root, value));
|
|
876
961
|
if (!rel || isAbsolute(rel) || rel.split(/[\\/]+/).includes('..')) return '';
|
|
877
962
|
return rel.replace(/\\/g, '/');
|
|
@@ -879,42 +964,79 @@ function trackedRelativePath(root, entry) {
|
|
|
879
964
|
|
|
880
965
|
/** A record file for an edit the worktree root cannot replay: outside it, or
|
|
881
966
|
* too large/binary to capture. Display-only (`viewPatch`), never undone. */
|
|
882
|
-
function untrackableRecordFile(
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
967
|
+
function untrackableRecordFile(edit, root, budget) {
|
|
968
|
+
const path = viewPath(root, edit.end);
|
|
969
|
+
const oldPath = viewPath(root, edit.start);
|
|
970
|
+
const named = edit.omitted
|
|
971
|
+
? []
|
|
972
|
+
: edit.steps.map(({ before, after }) =>
|
|
973
|
+
namedChangePatch({ before: before.content, after: after.content, oldName: path, newName: path, budget })
|
|
974
|
+
);
|
|
975
|
+
const viewPatch = named.map((step) => step.patch).join('');
|
|
890
976
|
return {
|
|
891
|
-
path
|
|
892
|
-
...(
|
|
893
|
-
status,
|
|
894
|
-
additions: named
|
|
895
|
-
deletions: named
|
|
896
|
-
...(
|
|
897
|
-
...(omitted ? { binary: true } : {}),
|
|
977
|
+
path,
|
|
978
|
+
...(oldPath !== path ? { oldPath } : {}),
|
|
979
|
+
status: edit.status,
|
|
980
|
+
additions: named.reduce((sum, step) => sum + step.additions, 0),
|
|
981
|
+
deletions: named.reduce((sum, step) => sum + step.deletions, 0),
|
|
982
|
+
...(viewPatch ? { viewPatch } : {}),
|
|
983
|
+
...(edit.omitted ? { binary: true } : {}),
|
|
898
984
|
revertable: false,
|
|
899
|
-
reason: omitted ? 'binary/large file content was not captured' : 'file is outside the recorded root',
|
|
985
|
+
reason: edit.omitted ? 'binary/large file content was not captured' : 'file is outside the recorded root',
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** The root a tracker's own patches are relative to, and a getter for the
|
|
990
|
+
* object format `git apply` hashes their binary content with there (one Git
|
|
991
|
+
* call, made only when asked). */
|
|
992
|
+
function trackerPatchRoot(tracker, fallback = '') {
|
|
993
|
+
const git = Boolean(clean(tracker?.worktreeSnapshot?.root));
|
|
994
|
+
const root = git ? tracker.worktreeSnapshot.root : clean(tracker?.worktreeRoot) || clean(fallback);
|
|
995
|
+
let format = null;
|
|
996
|
+
const objectFormat = () => {
|
|
997
|
+
format ||= git ? repositoryObjectFormat(root) : Promise.resolve('sha1');
|
|
998
|
+
return format;
|
|
999
|
+
};
|
|
1000
|
+
return { root: root ? resolve(root) : '', kind: git ? 'git' : 'tracked', objectFormat };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** One chain's edit (chainEdit) as a record file: one replayable patch
|
|
1004
|
+
* section per own segment, oldest first, or a view-only entry when it lies
|
|
1005
|
+
* outside the root or its bytes were never captured. `objectFormat` hashes a
|
|
1006
|
+
* binary patch, `budget` bounds the line diff. */
|
|
1007
|
+
function editChangeFile(edit, root, objectFormat, budget) {
|
|
1008
|
+
if (edit.omitted || !edit.inside) return untrackableRecordFile(edit, root, budget);
|
|
1009
|
+
const sections = edit.steps.map(({ before, after, oldPath, newPath }) =>
|
|
1010
|
+
trackedChangeFile({ oldPath, newPath, before: before.content, after: after.content, objectFormat, budget })
|
|
1011
|
+
);
|
|
1012
|
+
if (sections.length === 1) return sections[0];
|
|
1013
|
+
const path = trackedRelativePath(root, edit.end);
|
|
1014
|
+
const oldPath = trackedRelativePath(root, edit.start);
|
|
1015
|
+
const base = {
|
|
1016
|
+
path,
|
|
1017
|
+
...(oldPath !== path ? { oldPath } : {}),
|
|
1018
|
+
status: edit.status,
|
|
1019
|
+
additions: sections.reduce((sum, section) => sum + section.additions, 0),
|
|
1020
|
+
deletions: sections.reduce((sum, section) => sum + section.deletions, 0),
|
|
900
1021
|
};
|
|
1022
|
+
const blocked = sections.find((section) => section.revertable !== true);
|
|
1023
|
+
if (blocked) return { ...base, revertable: false, reason: blocked.reason };
|
|
1024
|
+
return { ...base, revertable: true, patch: Buffer.concat(sections.map((section) => section.patch)) };
|
|
901
1025
|
}
|
|
902
1026
|
|
|
903
|
-
/** The
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
const oldPath = trackedRelativePath(root, pair.before || pair.after);
|
|
909
|
-
const newPath = trackedRelativePath(root, pair.after || pair.before);
|
|
910
|
-
if (oldPath === newPath && sameContent(before, after)) return null;
|
|
911
|
-
return { before, after, oldPath, newPath, omitted: [before, after].some(isOmittedContent) };
|
|
1027
|
+
/** The object format the binary patches of `edits` hash with: resolved (one
|
|
1028
|
+
* Git call) only when one of them replays binary content. */
|
|
1029
|
+
async function editsObjectFormat(edits, patchRoot) {
|
|
1030
|
+
const binary = edits.some((edit) => edit.binary && edit.inside && !edit.omitted);
|
|
1031
|
+
return binary ? await patchRoot.objectFormat() : 'sha1';
|
|
912
1032
|
}
|
|
913
1033
|
|
|
914
|
-
/** Record files of
|
|
915
|
-
|
|
1034
|
+
/** Record files of the tracker's own changes. `outside` are those beyond the
|
|
1035
|
+
* root (view-only beside a Git turn's files). */
|
|
1036
|
+
async function ownRecordFiles(tracker, patchRoot) {
|
|
1037
|
+
const { root } = patchRoot;
|
|
916
1038
|
if (!tracker.valid) {
|
|
917
|
-
|
|
1039
|
+
const files = [...ownedPathsUnder(root, tracker.ownedPaths)].map((path) => ({
|
|
918
1040
|
path,
|
|
919
1041
|
status: 'M',
|
|
920
1042
|
additions: 0,
|
|
@@ -922,63 +1044,41 @@ function trackedRecordFiles(tracker, root) {
|
|
|
922
1044
|
revertable: false,
|
|
923
1045
|
reason: 'file content exceeded the undo tracking limit',
|
|
924
1046
|
}));
|
|
1047
|
+
return { files, outside: [] };
|
|
925
1048
|
}
|
|
1049
|
+
const edits = [...tracker.chains.values()].map((chain) => chainEdit(chain, root)).filter(Boolean);
|
|
1050
|
+
const objectFormat = await editsObjectFormat(edits, patchRoot);
|
|
1051
|
+
// One line-diff budget for the whole seal pass.
|
|
1052
|
+
const budget = createLineDiffBudget();
|
|
926
1053
|
const files = [];
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (edit.omitted || !edit.oldPath || !edit.newPath) {
|
|
931
|
-
files.push(untrackableRecordFile(pair, edit));
|
|
932
|
-
continue;
|
|
933
|
-
}
|
|
934
|
-
files.push(
|
|
935
|
-
trackedChangeFile({ oldPath: edit.oldPath, newPath: edit.newPath, before: edit.before, after: edit.after })
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
return files;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
/** Edits of a Git turn that lie outside its worktree root: the baseline diff
|
|
942
|
-
* never sees them, so they are recorded view-only beside the Git files. */
|
|
943
|
-
function outsideRecordFiles(tracker, root) {
|
|
944
|
-
if (!tracker.valid) return [];
|
|
945
|
-
const files = [];
|
|
946
|
-
for (const pair of trackedPairs(tracker)) {
|
|
947
|
-
const edit = recordedPair(pair, root);
|
|
948
|
-
if (edit && (!edit.oldPath || !edit.newPath)) {
|
|
949
|
-
files.push(untrackableRecordFile(pair, edit));
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return files;
|
|
1054
|
+
const outside = [];
|
|
1055
|
+
for (const edit of edits) (edit.inside ? files : outside).push(editChangeFile(edit, root, objectFormat, budget));
|
|
1056
|
+
return { files, outside };
|
|
953
1057
|
}
|
|
954
1058
|
|
|
955
|
-
/** Freeze the turn's
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
1059
|
+
/** Freeze the turn's own changes into its durable change record: per file,
|
|
1060
|
+
* the session's own segments (its completed workers' included) as one
|
|
1061
|
+
* replayable patch, never the worktree's net diff, counted by the summed
|
|
1062
|
+
* deltas of those writes (`counts: 'own'`; older records without the marker
|
|
1063
|
+
* carry net worktree patches and counts). */
|
|
959
1064
|
async function freezeTurnChanges(ownerSessionId, tracker, agents) {
|
|
960
1065
|
const checkpointId = clean(tracker.checkpointId);
|
|
961
1066
|
if (!checkpointId) return null;
|
|
962
|
-
|
|
963
|
-
if (
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const result = await freezeTurnWorktreeChanges(tracker.worktreeSnapshot, scope);
|
|
968
|
-
if (result) {
|
|
969
|
-
frozen = { kind: 'git', ...result, outside: outsideRecordFiles(tracker, tracker.worktreeSnapshot.root) };
|
|
970
|
-
}
|
|
971
|
-
} else if (tracker.worktreeRoot) {
|
|
972
|
-
frozen = { kind: 'tracked', root: tracker.worktreeRoot, files: trackedRecordFiles(tracker, tracker.worktreeRoot) };
|
|
973
|
-
}
|
|
974
|
-
if (!frozen) return null;
|
|
1067
|
+
const patchRoot = trackerPatchRoot(tracker);
|
|
1068
|
+
if (!patchRoot.root) return null;
|
|
1069
|
+
const { files, outside } = await ownRecordFiles(tracker, patchRoot);
|
|
1070
|
+
const git = patchRoot.kind === 'git';
|
|
1071
|
+
const counted = ownCountIndex(tracker, patchRoot.root);
|
|
975
1072
|
const record = await upsertTurnChangeRecord(ownerSessionId, {
|
|
976
1073
|
checkpointId,
|
|
977
|
-
kind:
|
|
978
|
-
root:
|
|
1074
|
+
kind: patchRoot.kind,
|
|
1075
|
+
root: patchRoot.root,
|
|
979
1076
|
agents,
|
|
980
|
-
|
|
981
|
-
|
|
1077
|
+
counts: 'own',
|
|
1078
|
+
// A non-Git turn has no view-only side list: an edit it cannot replay
|
|
1079
|
+
// blocks its Undo like before.
|
|
1080
|
+
files: withOwnCounts(git ? files : [...files, ...outside], counted).map(storedChangeFile),
|
|
1081
|
+
outside: git ? withOwnCounts(outside, counted).map(storedChangeFile) : [],
|
|
982
1082
|
});
|
|
983
1083
|
// A fresh freeze measured the disk itself; nothing about it is unknown.
|
|
984
1084
|
_unknownChangeStates.delete(pendingFreezeKey(ownerSessionId, checkpointId));
|
|
@@ -992,13 +1092,14 @@ const pendingFreezeKey = (ownerSessionId, checkpointId) => `${ownerSessionId}\0$
|
|
|
992
1092
|
* baseline and freeze the change record. Single-flight per tracker; a seal
|
|
993
1093
|
* requested while another runs waits for it and then seals again only if a
|
|
994
1094
|
* completion wake reopened the turn meanwhile. `reseal` (a late worker) only
|
|
995
|
-
* re-freezes a turn that is still sealed
|
|
1095
|
+
* re-freezes a turn that is still sealed, after any seal in flight: that one
|
|
1096
|
+
* captured its agents before the worker was attributed.
|
|
996
1097
|
*/
|
|
997
1098
|
function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
998
1099
|
// Its history was deleted (clearSessionChangeHistory): never written again.
|
|
999
1100
|
if (tracker.discarded) return Promise.resolve();
|
|
1000
1101
|
if (tracker.freezing) {
|
|
1001
|
-
if (reseal) return tracker.freezing;
|
|
1102
|
+
if (reseal) return tracker.freezing.then(() => sealTurn(ownerSessionId, tracker, { reseal: true }));
|
|
1002
1103
|
return tracker.freezing.then(() => (tracker.sealed ? undefined : sealTurn(ownerSessionId, tracker)));
|
|
1003
1104
|
}
|
|
1004
1105
|
const epoch = tracker.sealEpoch;
|
|
@@ -1014,6 +1115,8 @@ function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
|
1014
1115
|
}
|
|
1015
1116
|
}
|
|
1016
1117
|
if (tracker.discarded || tracker.sealEpoch !== epoch || (reseal && !tracker.sealed)) return;
|
|
1118
|
+
// A re-seal never rewrites a turn once the next one opened.
|
|
1119
|
+
if (reseal && turnSuperseded(ownerSessionId, tracker)) return;
|
|
1017
1120
|
tracker.sealed = true;
|
|
1018
1121
|
await persistTurnSnapshotRecord(ownerSessionId, tracker);
|
|
1019
1122
|
try {
|
|
@@ -1021,11 +1124,8 @@ function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
|
1021
1124
|
} catch {
|
|
1022
1125
|
/* the change record adds Undo history; it never fails a turn */
|
|
1023
1126
|
}
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1026
|
-
// this session's bounded before/after content so Review can still undo its
|
|
1027
|
-
// own files after the turn completes.
|
|
1028
|
-
if (worktreeSnapshotUsable(tracker)) releaseDiffTrackerContent(tracker);
|
|
1127
|
+
// The bounded own before/after content stays until the next turn: a late
|
|
1128
|
+
// worker's re-seal and an Undo/Reapply of this turn fold into it.
|
|
1029
1129
|
})().finally(() => {
|
|
1030
1130
|
if (tracker.freezing === run) tracker.freezing = null;
|
|
1031
1131
|
const count = (_pendingFreezes.get(key) || 1) - 1;
|
|
@@ -1037,7 +1137,9 @@ function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
|
1037
1137
|
}
|
|
1038
1138
|
|
|
1039
1139
|
function resealTurn(ownerSessionId, tracker) {
|
|
1040
|
-
|
|
1140
|
+
// A turn still running seals with every published agent; one sealed or
|
|
1141
|
+
// sealing queues its re-seal (sealTurn runs it after a seal in flight).
|
|
1142
|
+
if (!(tracker.sealed || tracker.freezing) || !clean(tracker.checkpointId) || tracker.resealQueued) return;
|
|
1041
1143
|
// On the session lock: an in-flight Undo/Reapply lands before the re-freeze
|
|
1042
1144
|
// reads the worktree. A turn start queued ahead of it runs it before opening
|
|
1043
1145
|
// (beginTurnSnapshot), so one queued re-seal covers every late worker.
|
|
@@ -1045,10 +1147,17 @@ function resealTurn(ownerSessionId, tracker) {
|
|
|
1045
1147
|
void serializeChangeMutation(ownerSessionId, () => runQueuedReseal(ownerSessionId, tracker)).catch(() => {});
|
|
1046
1148
|
}
|
|
1047
1149
|
|
|
1150
|
+
/** True once a newer turn of the session opened over `tracker`'s. */
|
|
1151
|
+
function turnSuperseded(ownerSessionId, tracker) {
|
|
1152
|
+
const turn = _turnsBySession.get(ownerSessionId);
|
|
1153
|
+
return Boolean(turn) && turn.generation !== tracker.ownerGeneration;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1048
1156
|
/** Run `tracker`'s queued re-seal once, whichever lock step reaches it first. */
|
|
1049
1157
|
function runQueuedReseal(ownerSessionId, tracker) {
|
|
1050
1158
|
if (!tracker.resealQueued) return undefined;
|
|
1051
1159
|
tracker.resealQueued = false;
|
|
1160
|
+
if (turnSuperseded(ownerSessionId, tracker)) return undefined;
|
|
1052
1161
|
return sealTurn(ownerSessionId, tracker, { reseal: true });
|
|
1053
1162
|
}
|
|
1054
1163
|
|
|
@@ -1065,92 +1174,128 @@ export async function completeTurnSnapshot(sessionId, expected = null) {
|
|
|
1065
1174
|
return true;
|
|
1066
1175
|
}
|
|
1067
1176
|
|
|
1068
|
-
/**
|
|
1069
|
-
*
|
|
1070
|
-
* worktree cannot attribute concurrent mutations to one session, and a
|
|
1071
|
-
* non-Git worktree never had a baseline to begin with. In both cases the
|
|
1072
|
-
* exact apply_patch buffers are the only safe revert source.
|
|
1073
|
-
*/
|
|
1177
|
+
/** Is this turn's review labelled with its uncontended Git baseline? (The
|
|
1178
|
+
* review itself is always the tracker's own segments.) */
|
|
1074
1179
|
function worktreeSnapshotUsable(tracker) {
|
|
1075
1180
|
return Boolean(tracker && !tracker.worktreeContended && tracker.worktreeSnapshot);
|
|
1076
1181
|
}
|
|
1077
1182
|
|
|
1078
|
-
/**
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
const relativeTarget =
|
|
1085
|
-
normalizedRoot && normalizedTarget.startsWith(`${normalizedRoot}/`)
|
|
1086
|
-
? normalizedTarget.slice(normalizedRoot.length + 1)
|
|
1087
|
-
: normalizedTarget;
|
|
1088
|
-
const normalizedOwned = pathKey(owned);
|
|
1089
|
-
return normalizedOwned === relativeTarget || normalizedOwned === normalizedTarget;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
/** Re-open the recorded baseline for a session whose tracker is gone. Returns
|
|
1093
|
-
* null when the record is missing or its tree object has been collected. */
|
|
1094
|
-
async function resumeRecordedSnapshot(ownerSessionId, { omitPatch = false } = {}) {
|
|
1095
|
-
if (!ownerSessionId) return null;
|
|
1096
|
-
let record = null;
|
|
1097
|
-
try {
|
|
1098
|
-
record = await loadTurnSnapshotRecord(ownerSessionId);
|
|
1099
|
-
} catch {
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1183
|
+
/** The latest turn's review once its tracker is gone (the next turn replaced
|
|
1184
|
+
* it, the cache evicted it, the runtime restarted): its change record's
|
|
1185
|
+
* files still applied, with their own counts and recorded patches — never a
|
|
1186
|
+
* worktree diff, so a foreign edit inside an own file stays out. */
|
|
1187
|
+
async function recordedTurnReview(ownerSessionId, { omitPatch }) {
|
|
1188
|
+
const record = await latestTurnRecord(ownerSessionId);
|
|
1102
1189
|
if (!record) return null;
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
void saveTurnReviewSummary(ownerSessionId, resumed.record, resumed.snapshot);
|
|
1190
|
+
const files = (record.files || []).filter((file) => fileState(file) === 'applied');
|
|
1191
|
+
const patches = files.map(changeDisplayPatch);
|
|
1192
|
+
const rows = files.map((file, index) => {
|
|
1193
|
+
const row = {
|
|
1194
|
+
path: file.path,
|
|
1195
|
+
...(file.oldPath ? { oldPath: file.oldPath } : {}),
|
|
1196
|
+
status: file.status,
|
|
1197
|
+
additions: Number(file.additions) || 0,
|
|
1198
|
+
deletions: Number(file.deletions) || 0,
|
|
1199
|
+
...(file.binary ? { binary: true } : {}),
|
|
1200
|
+
};
|
|
1201
|
+
// The record dropped its patch for size: still listed too large.
|
|
1202
|
+
return recordPatchOmitted(file, patches[index]) ? patchOmittedFile(row) : row;
|
|
1203
|
+
});
|
|
1204
|
+
const shaped = omitPatch ? { files: rows, patch: '' } : perFileReview(rows, patches.join(''));
|
|
1119
1205
|
return {
|
|
1120
1206
|
supported: true,
|
|
1121
|
-
files:
|
|
1122
|
-
patch:
|
|
1123
|
-
...(
|
|
1207
|
+
files: shaped.files,
|
|
1208
|
+
patch: shaped.patch,
|
|
1209
|
+
...(omitPatch ? { patchOmitted: true } : {}),
|
|
1210
|
+
...(shaped.diffTooLarge ? { diffTooLarge: true } : {}),
|
|
1124
1211
|
snapshotKind: 'scoped',
|
|
1125
1212
|
revertMode: 'scoped',
|
|
1126
|
-
checkpointId: clean(
|
|
1127
|
-
patchTruncated:
|
|
1213
|
+
checkpointId: clean(record.checkpointId),
|
|
1214
|
+
patchTruncated: false,
|
|
1128
1215
|
authoritative: true,
|
|
1129
1216
|
agents: publicAgentReviews(ownerSessionId),
|
|
1130
1217
|
reason: 'recorded',
|
|
1131
1218
|
};
|
|
1132
1219
|
}
|
|
1133
1220
|
|
|
1134
|
-
/**
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1221
|
+
/** A record file kept without its patch for size. */
|
|
1222
|
+
const recordPatchOmitted = (file, patch) => file?.patchOmitted === true && !patch;
|
|
1223
|
+
|
|
1224
|
+
/** A review reply's files and patch under the per-file caps: a file whose
|
|
1225
|
+
* sections were cut for display, or pass the per-file cap or the total left,
|
|
1226
|
+
* is listed with its counts but without its patch (read on its own when it
|
|
1227
|
+
* is opened). `diffTooLarge` once nothing fit. */
|
|
1228
|
+
function perFileReview(files, patch) {
|
|
1229
|
+
const sections = new Map();
|
|
1230
|
+
for (const section of splitPatchSections(Buffer.from(patch || '', 'utf8'))) {
|
|
1231
|
+
const key = pathKey(section.newPath ?? section.oldPath ?? '');
|
|
1232
|
+
const entry = sections.get(key) || { parts: [], bytes: 0, cut: false };
|
|
1233
|
+
entry.parts.push(section.patch);
|
|
1234
|
+
entry.bytes += section.patch.length;
|
|
1235
|
+
entry.cut ||= reviewSectionTruncated(section.patch);
|
|
1236
|
+
sections.set(key, entry);
|
|
1237
|
+
}
|
|
1238
|
+
const budget = createReviewPatchBudget();
|
|
1239
|
+
const omitted = new Set();
|
|
1240
|
+
const kept = [];
|
|
1241
|
+
for (const [key, entry] of sections) {
|
|
1242
|
+
if (!entry.cut && budget.admit(entry.bytes)) kept.push(...entry.parts);
|
|
1243
|
+
else omitted.add(key);
|
|
1244
|
+
}
|
|
1245
|
+
const shaped = files.map((file) => (omitted.has(pathKey(file.path)) ? patchOmittedFile(file) : file));
|
|
1246
|
+
const text = Buffer.concat(kept).toString('utf8');
|
|
1247
|
+
return { files: shaped, patch: text, diffTooLarge: reviewDiffTooLarge(shaped, text) };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** The latest turn's change record, its unrecordable file states applied. */
|
|
1251
|
+
async function latestTurnRecord(ownerSessionId) {
|
|
1252
|
+
const checkpointId = await latestRecordedCheckpoint(ownerSessionId);
|
|
1253
|
+
const stored = checkpointId ? await loadTurnChangeRecord(ownerSessionId, checkpointId).catch(() => null) : null;
|
|
1254
|
+
return withUnknownChangeStates(ownerSessionId, stored);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** One file's whole display patch in the tracker's view, rendered on its own
|
|
1258
|
+
* instead of within the reply's caps; '' when the view lists no such file. */
|
|
1259
|
+
function trackerFilePatch(tracker, path) {
|
|
1260
|
+
const { root, kind } = trackerPatchRoot(tracker);
|
|
1261
|
+
const key = pathKey(path);
|
|
1262
|
+
const budget = createLineDiffBudget();
|
|
1263
|
+
for (const chain of tracker.chains.values()) {
|
|
1264
|
+
const edit = chainEdit(chain, root);
|
|
1265
|
+
if (!edit || (kind === 'git' && !edit.inside) || pathKey(viewPath(root, edit.end)) !== key) continue;
|
|
1266
|
+
return edit.steps.map((step) => renderStep(step, root, budget).text).join('');
|
|
1267
|
+
}
|
|
1268
|
+
return '';
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/** One file of the latest turn's review with its whole patch, up to the
|
|
1272
|
+
* file-patch read's cap: the file a reply listed `patchOmitted`. */
|
|
1273
|
+
export async function getTurnReviewFilePatch(_worktree, sessionId, file) {
|
|
1274
|
+
const path = clean(file).replace(/\\/g, '/');
|
|
1275
|
+
if (DISABLED) return { supported: false, path, patch: '' };
|
|
1276
|
+
const ownerSessionId = clean(sessionId);
|
|
1277
|
+
if (!ownerSessionId || !path) return { supported: true, path, patch: '' };
|
|
1278
|
+
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1279
|
+
if (tracker) return filePatchReply(path, trackerFilePatch(tracker, path));
|
|
1280
|
+
// Only a file the recorded review lists, by its own path.
|
|
1281
|
+
const record = await latestTurnRecord(ownerSessionId);
|
|
1282
|
+
const match = record ? findChangeFile(record, path) : null;
|
|
1283
|
+
if (
|
|
1284
|
+
!match ||
|
|
1285
|
+
fileState(match) !== 'applied' ||
|
|
1286
|
+
changePathKey(match.path) !== changePathKey(relativeRecordPath(record, path))
|
|
1287
|
+
) {
|
|
1288
|
+
return filePatchReply(path, '');
|
|
1289
|
+
}
|
|
1290
|
+
const patch = changeDisplayPatch(match);
|
|
1291
|
+
if (recordPatchOmitted(match, patch)) return { ...filePatchReply(path, ''), ...patchOmittedFile({}) };
|
|
1292
|
+
return filePatchReply(path, patch);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** `value` as findChangeFile reads it: relative to the record's root. */
|
|
1296
|
+
function relativeRecordPath(record, value) {
|
|
1297
|
+
const target = clean(value).replace(/\\/g, '/');
|
|
1298
|
+
return isAbsolute(target) && record?.root ? relative(record.root, resolve(target)).replace(/\\/g, '/') : target;
|
|
1154
1299
|
}
|
|
1155
1300
|
|
|
1156
1301
|
function assertCheckpointMatches(expectedCheckpointId, actualCheckpointId) {
|
|
@@ -1163,69 +1308,38 @@ function assertCheckpointMatches(expectedCheckpointId, actualCheckpointId) {
|
|
|
1163
1308
|
}
|
|
1164
1309
|
}
|
|
1165
1310
|
|
|
1166
|
-
/**
|
|
1167
|
-
|
|
1168
|
-
const resumed = await resumeRecordedSnapshot(ownerSessionId);
|
|
1169
|
-
if (!resumed) return null;
|
|
1170
|
-
assertCheckpointMatches(expectedCheckpointId, resumed.record.checkpointId);
|
|
1171
|
-
const owned = resumed.record.toolFiles;
|
|
1172
|
-
const targets = file ? owned.filter((path) => recordedPathMatchesFile(path, resumed.record.root, file)) : owned;
|
|
1173
|
-
if (targets.length === 0) {
|
|
1174
|
-
throw new Error('turn review revert cannot attribute this file to the session');
|
|
1175
|
-
}
|
|
1176
|
-
const paths = await revertTurnWorktreePaths(resumed.snapshot, targets);
|
|
1177
|
-
return { checkpointId: clean(resumed.record.checkpointId), paths };
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
/** Return the authoritative Lead net diff plus attributed child reviews. */
|
|
1311
|
+
/** Return the Lead turn's own-file diff plus attributed child reviews, and
|
|
1312
|
+
* `historyTurns`: the retained turn records clearSessionChangeHistory deletes. */
|
|
1181
1313
|
export async function getTurnReviewDiff(_worktree, sessionId, options = {}) {
|
|
1182
|
-
if (DISABLED) return { supported: false, files: [], patch: '', agents: [] };
|
|
1314
|
+
if (DISABLED) return { supported: false, files: [], patch: '', agents: [], historyTurns: 0 };
|
|
1183
1315
|
const ownerSessionId = clean(sessionId);
|
|
1316
|
+
const review = await readTurnReview(ownerSessionId, options);
|
|
1317
|
+
const records = await loadTurnChangeRecords(ownerSessionId).catch(() => []);
|
|
1318
|
+
return { ...review, historyTurns: records.length };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async function readTurnReview(ownerSessionId, options) {
|
|
1184
1322
|
const turn = _turnsBySession.get(ownerSessionId);
|
|
1185
1323
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
// Only the review bar's interval timer sends `poll: 'background'`; an
|
|
1189
|
-
// open review (asking for its patch, no `summary`) keeps a short gap.
|
|
1190
|
-
// Every other read (opening the review, a manual or tool-boundary
|
|
1191
|
-
// refresh, a client that sends no `poll`) captures the worktree now.
|
|
1192
|
-
const refreshOptions = options.poll === 'background' ? { visible: options.summary !== true } : { force: true };
|
|
1193
|
-
try {
|
|
1194
|
-
await refreshTurnWorktreeSnapshot(isolatedWorktreeSnapshot, refreshOptions);
|
|
1195
|
-
} catch {
|
|
1196
|
-
// Best-effort refresh: the review reads the last captured state.
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
const snapshot = isolatedWorktreeSnapshot;
|
|
1200
|
-
// Without a usable baseline the review falls back to this session's own
|
|
1201
|
-
// mutations — including a worktree that has no Git repository at all, which
|
|
1202
|
-
// used to leave the bar with no revert mode whatsoever.
|
|
1203
|
-
const trackedRevertAvailable = !snapshot && tracker?.valid === true && trackedPairs(tracker).length > 0;
|
|
1204
|
-
if (!snapshot && !trackedRevertAvailable) {
|
|
1205
|
-
// Both in-memory sources are gone: the next turn replaced the tracker, the
|
|
1206
|
-
// turn cache evicted it, or the runtime restarted. The recorded baseline is
|
|
1207
|
-
// still on disk and still exact. Rebuilt per read and then discarded, so a
|
|
1208
|
-
// summary read skips its patch text instead of dropping it afterwards.
|
|
1209
|
-
// A collapsed read that asked for no refresh (entering the session) takes
|
|
1210
|
-
// the last computed summary instead of queueing a rescan.
|
|
1211
|
-
if (options.refresh === false && options.summary === true) {
|
|
1212
|
-
const cached = await cachedReviewFromRecord(ownerSessionId);
|
|
1213
|
-
if (cached) return cached;
|
|
1214
|
-
}
|
|
1215
|
-
const recorded = await scopedReviewFromRecord(ownerSessionId, { omitPatch: options.summary === true });
|
|
1324
|
+
if (!tracker) {
|
|
1325
|
+
const recorded = await recordedTurnReview(ownerSessionId, { omitPatch: options.summary === true });
|
|
1216
1326
|
if (recorded) return recorded;
|
|
1217
1327
|
}
|
|
1328
|
+
const view = tracker ? trackerView(tracker) : null;
|
|
1329
|
+
const worktree = worktreeSnapshotUsable(tracker);
|
|
1218
1330
|
let revertMode = '';
|
|
1219
|
-
if (
|
|
1220
|
-
else if (
|
|
1331
|
+
if (worktree) revertMode = 'worktree';
|
|
1332
|
+
else if (tracker?.valid && view.files.length) revertMode = 'tracked';
|
|
1333
|
+
const shaped = perFileReview(view?.files || [], view?.patch || '');
|
|
1221
1334
|
return {
|
|
1222
1335
|
supported: true,
|
|
1223
|
-
files:
|
|
1224
|
-
patch:
|
|
1225
|
-
|
|
1336
|
+
files: shaped.files,
|
|
1337
|
+
patch: shaped.patch,
|
|
1338
|
+
...(shaped.diffTooLarge ? { diffTooLarge: true } : {}),
|
|
1339
|
+
snapshotKind: worktree ? 'worktree' : 'tool',
|
|
1226
1340
|
revertMode,
|
|
1227
1341
|
checkpointId: clean(turn?.checkpointId || tracker?.checkpointId),
|
|
1228
|
-
patchTruncated:
|
|
1342
|
+
patchTruncated: false,
|
|
1229
1343
|
authoritative: Boolean(turn && tracker),
|
|
1230
1344
|
agents: publicAgentReviews(ownerSessionId),
|
|
1231
1345
|
...(turn ? { generation: turn.generation } : { reason: 'no-turn' }),
|
|
@@ -1259,16 +1373,83 @@ export async function getSessionReviewDiff(worktree, sessionId) {
|
|
|
1259
1373
|
// session recorded before baselines existed) come from them.
|
|
1260
1374
|
loadTurnChangeRecords(ownerSessionId).catch(() => []),
|
|
1261
1375
|
]);
|
|
1262
|
-
|
|
1263
|
-
// `historyTurns`: the retained turn records clearSessionChangeHistory deletes.
|
|
1264
|
-
return { ...merged, historyTurns: records.length };
|
|
1376
|
+
return await sessionChangeReview(review, { baselines, records });
|
|
1265
1377
|
}
|
|
1266
1378
|
|
|
1267
|
-
|
|
1379
|
+
/** What the session review diffs inside a Git root: the stored scope (or
|
|
1380
|
+
* live tracker) of this worktree, the paths the session owns there, and its
|
|
1381
|
+
* earliest baseline tree with the recorded first states that complete it. */
|
|
1382
|
+
async function sessionReviewScope(worktree, ownerSessionId) {
|
|
1268
1383
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1269
1384
|
const scopes = await loadSessionSnapshotRecords(ownerSessionId).catch(() => []);
|
|
1270
1385
|
const stored = scopes.find((scope) => scopeMatchesWorktree(scope, worktree, tracker));
|
|
1271
1386
|
const root = stored?.root || tracker?.worktreeSnapshot?.root || clean(worktree);
|
|
1387
|
+
const ownedPaths = new Set(stored?.toolFiles || []);
|
|
1388
|
+
for (const path of ownedPathsUnder(root, tracker?.ownedPaths)) ownedPaths.add(path);
|
|
1389
|
+
const baselineTree = stored?.baselineTree || tracker?.worktreeSnapshot?.baselineTree;
|
|
1390
|
+
const baselineFiles = new Map((stored?.baselineFiles || []).map((entry) => [pathKey(entry.path), entry]));
|
|
1391
|
+
const storedPaths = new Set((stored?.toolFiles || []).map(pathKey));
|
|
1392
|
+
for (const entry of tracker?.worktreeSnapshot?.baselineFiles.values() || []) {
|
|
1393
|
+
if (!storedPaths.has(pathKey(entry.path))) baselineFiles.set(pathKey(entry.path), entry);
|
|
1394
|
+
}
|
|
1395
|
+
return { tracker, stored, root, ownedPaths, baselineTree, baselineFiles: [...baselineFiles.values()] };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** `value` (absolute, or relative to `root`) as a path relative to `root`;
|
|
1399
|
+
* null outside it. */
|
|
1400
|
+
function relativeInside(root, value) {
|
|
1401
|
+
const target = clean(value);
|
|
1402
|
+
if (!root || !target) return null;
|
|
1403
|
+
const rel = relative(resolve(root), isAbsolute(target) ? resolve(target) : resolve(root, target));
|
|
1404
|
+
if (!rel || isAbsolute(rel) || rel.split(/[\\/]+/).includes('..')) return null;
|
|
1405
|
+
return rel.replace(/\\/g, '/');
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** One file of the session review with its whole patch, up to the file-patch
|
|
1409
|
+
* read's cap: the file a reply listed `patchOmitted`. `file` is the listed
|
|
1410
|
+
* file's `id` or path, `oldPath` a rename's source. Inside the Git root it is
|
|
1411
|
+
* the session's baseline tree against the worktree now for that path alone;
|
|
1412
|
+
* elsewhere its first-seen state measured against the disk now. */
|
|
1413
|
+
export async function getSessionReviewFilePatch(worktree, sessionId, file, oldPath = '') {
|
|
1414
|
+
const path = clean(file).replace(/\\/g, '/');
|
|
1415
|
+
if (DISABLED) return { supported: false, path, patch: '' };
|
|
1416
|
+
const ownerSessionId = clean(sessionId);
|
|
1417
|
+
if (!ownerSessionId || !path) return { supported: true, path, patch: '' };
|
|
1418
|
+
const scope = await sessionReviewScope(worktree, ownerSessionId);
|
|
1419
|
+
const rel = scope.baselineTree ? relativeInside(scope.root, path) : null;
|
|
1420
|
+
if (rel !== null) {
|
|
1421
|
+
// Each target is authorized on its own: an unowned file is never read,
|
|
1422
|
+
// an unowned rename source never joins its diff.
|
|
1423
|
+
const owned = new Set([...scope.ownedPaths].map(pathKey));
|
|
1424
|
+
if (!owned.has(pathKey(rel))) return { supported: true, path, patch: '' };
|
|
1425
|
+
const source = relativeInside(scope.root, oldPath) || '';
|
|
1426
|
+
const oldRel = source && owned.has(pathKey(source)) ? source : '';
|
|
1427
|
+
const result = await resumeTurnWorktreeFilePatch(scope.root, scope.baselineTree, {
|
|
1428
|
+
paths: [...scope.ownedPaths],
|
|
1429
|
+
baselineFiles: scope.baselineFiles,
|
|
1430
|
+
path: rel,
|
|
1431
|
+
oldPath: oldRel,
|
|
1432
|
+
maxBytes: REVIEW_FILE_PATCH_SHOW_MAX_BYTES,
|
|
1433
|
+
}).catch(() => null);
|
|
1434
|
+
if (result) return { supported: true, path, ...result };
|
|
1435
|
+
}
|
|
1436
|
+
const [base, baselines, records] = await Promise.all([
|
|
1437
|
+
// Beside a Git baseline a file outside its root is measured from its
|
|
1438
|
+
// first state; the Git side's own files need no diff for that.
|
|
1439
|
+
rel === null && scope.baselineTree && scope.root
|
|
1440
|
+
? { supported: true, root: scope.root, files: [], patch: '', source: 'git' }
|
|
1441
|
+
: baselineSessionReviewDiff(worktree, ownerSessionId),
|
|
1442
|
+
loadSessionBaselines(ownerSessionId).catch(() => null),
|
|
1443
|
+
loadTurnChangeRecords(ownerSessionId).catch(() => []),
|
|
1444
|
+
]);
|
|
1445
|
+
return await sessionChangeReview(base, { baselines, records, file: path });
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
1449
|
+
const { tracker, stored, root, ownedPaths, baselineTree, baselineFiles } = await sessionReviewScope(
|
|
1450
|
+
worktree,
|
|
1451
|
+
ownerSessionId
|
|
1452
|
+
);
|
|
1272
1453
|
// A running turn is still mutating the worktree: shell, office and script
|
|
1273
1454
|
// edits only reach the checkpoint diff once it is refreshed, exactly as the
|
|
1274
1455
|
// turn review bar does.
|
|
@@ -1279,8 +1460,6 @@ async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
|
1279
1460
|
// Best-effort refresh: the checkpoint diff uses the last captured state.
|
|
1280
1461
|
}
|
|
1281
1462
|
}
|
|
1282
|
-
const ownedPaths = new Set(stored?.toolFiles || []);
|
|
1283
|
-
for (const path of sessionAttributedPaths(root, tracker)) ownedPaths.add(path);
|
|
1284
1463
|
if (ownedPaths.size === 0) {
|
|
1285
1464
|
// Authoritatively empty only when the session left ownership metadata
|
|
1286
1465
|
// behind (a stored scope, a live tracker, or a deleted history); with
|
|
@@ -1296,21 +1475,12 @@ async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
|
1296
1475
|
source: known ? 'empty' : 'absent',
|
|
1297
1476
|
};
|
|
1298
1477
|
}
|
|
1299
|
-
const baselineTree = stored?.baselineTree || tracker?.worktreeSnapshot?.baselineTree;
|
|
1300
1478
|
if (root && baselineTree) {
|
|
1301
|
-
const baselineFiles = new Map((stored?.baselineFiles || []).map((entry) => [pathKey(entry.path), entry]));
|
|
1302
|
-
const storedPaths = new Set((stored?.toolFiles || []).map(pathKey));
|
|
1303
|
-
for (const entry of tracker?.worktreeSnapshot?.baselineFiles.values() || []) {
|
|
1304
|
-
if (!storedPaths.has(pathKey(entry.path))) baselineFiles.set(pathKey(entry.path), entry);
|
|
1305
|
-
}
|
|
1306
1479
|
const snapshot = await resumeTurnWorktreeSnapshot(root, baselineTree, {
|
|
1307
1480
|
paths: [...ownedPaths],
|
|
1308
|
-
baselineFiles
|
|
1481
|
+
baselineFiles,
|
|
1309
1482
|
}).catch(() => null);
|
|
1310
1483
|
if (snapshot) {
|
|
1311
|
-
// The island counts only what is still uncommitted: the owned files
|
|
1312
|
-
// that differ from HEAD now, measured against HEAD.
|
|
1313
|
-
const uncommittedFiles = await diffTurnWorktreeHead(snapshot, [...ownedPaths]).catch(() => null);
|
|
1314
1484
|
return {
|
|
1315
1485
|
supported: true,
|
|
1316
1486
|
root,
|
|
@@ -1320,7 +1490,6 @@ async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
|
1320
1490
|
snapshotKind: 'session',
|
|
1321
1491
|
authoritative: true,
|
|
1322
1492
|
source: 'git',
|
|
1323
|
-
...(uncommittedFiles ? { uncommittedFiles } : {}),
|
|
1324
1493
|
};
|
|
1325
1494
|
}
|
|
1326
1495
|
}
|
|
@@ -1333,8 +1502,7 @@ async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
|
1333
1502
|
supported: true,
|
|
1334
1503
|
root,
|
|
1335
1504
|
files: [],
|
|
1336
|
-
patch:
|
|
1337
|
-
patchTruncated: tracker.patchTruncated === true,
|
|
1505
|
+
patch: '',
|
|
1338
1506
|
snapshotKind: 'session-tool',
|
|
1339
1507
|
authoritative: true,
|
|
1340
1508
|
source: 'tool',
|
|
@@ -1354,52 +1522,108 @@ async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
|
1354
1522
|
};
|
|
1355
1523
|
}
|
|
1356
1524
|
|
|
1357
|
-
|
|
1358
|
-
function unavailableTrackedRevertReason(tracker) {
|
|
1359
|
-
return tracker.worktreeContended
|
|
1360
|
-
? 'turn review revert is unavailable while sessions share a worktree'
|
|
1361
|
-
: 'turn review tracked file snapshot is unavailable';
|
|
1362
|
-
}
|
|
1525
|
+
const UNATTRIBUTED_FILE = 'turn review revert cannot attribute this file to the session';
|
|
1363
1526
|
|
|
1364
|
-
/**
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
|
|
1527
|
+
/** Reverse-apply the latest turn's own changes — every file its own tools
|
|
1528
|
+
* wrote (`file` null) or the own file the user picked. A sealed turn undoes
|
|
1529
|
+
* its change record (state included); a running one its tracker's own
|
|
1530
|
+
* segments. Resolves to the turn's checkpoint, the absolute paths touched and
|
|
1531
|
+
* whether the record already carries the new state. */
|
|
1532
|
+
async function revertLatestTurnUnlocked(worktree, ownerSessionId, file, expectedCheckpointId) {
|
|
1369
1533
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1370
1534
|
if (tracker) assertCheckpointMatches(expectedCheckpointId, tracker.checkpointId);
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const
|
|
1375
|
-
if (
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1535
|
+
if (!tracker || tracker.sealed) {
|
|
1536
|
+
const checkpointId = clean(tracker?.checkpointId) || (await latestRecordedCheckpoint(ownerSessionId));
|
|
1537
|
+
if (!tracker) assertCheckpointMatches(expectedCheckpointId, checkpointId);
|
|
1538
|
+
const record = checkpointId ? await loadTurnChangeRecord(ownerSessionId, checkpointId) : null;
|
|
1539
|
+
if (record) return await revertRecordedTurn(ownerSessionId, withUnknownChangeStates(ownerSessionId, record), file);
|
|
1540
|
+
if (!tracker) throw new Error('turn worktree snapshot is unavailable');
|
|
1541
|
+
}
|
|
1542
|
+
const paths = await revertTrackedTurn(tracker, worktree, file);
|
|
1543
|
+
return { checkpointId: clean(tracker.checkpointId), paths, recorded: false };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** The latest turn on record: the stored Git review's, else the newest change record's. */
|
|
1547
|
+
async function latestRecordedCheckpoint(ownerSessionId) {
|
|
1548
|
+
const snapshot = await loadTurnSnapshotRecord(ownerSessionId).catch(() => null);
|
|
1549
|
+
if (clean(snapshot?.checkpointId)) return clean(snapshot.checkpointId);
|
|
1550
|
+
const records = await loadTurnChangeRecords(ownerSessionId).catch(() => []);
|
|
1551
|
+
return clean(records.at(-1)?.checkpointId);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const recordFilePaths = (root, files) =>
|
|
1555
|
+
files
|
|
1556
|
+
.flatMap((entry) => [entry.path, entry.oldPath])
|
|
1557
|
+
.filter(Boolean)
|
|
1558
|
+
.map((path) => resolve(root, path));
|
|
1559
|
+
|
|
1560
|
+
/** Undo a sealed turn's applied record files (all, or the one named). */
|
|
1561
|
+
async function revertRecordedTurn(ownerSessionId, record, file) {
|
|
1562
|
+
let targets;
|
|
1563
|
+
if (file) {
|
|
1564
|
+
const match = findChangeFile(record, file);
|
|
1565
|
+
if (!match) throw new Error(UNATTRIBUTED_FILE);
|
|
1566
|
+
if (fileState(match) !== 'applied') throw new Error(`${match.path} is already undone`);
|
|
1567
|
+
targets = [match];
|
|
1568
|
+
} else {
|
|
1569
|
+
targets = (record.files || []).filter((entry) => fileState(entry) === 'applied');
|
|
1570
|
+
}
|
|
1571
|
+
if (targets.length > 0) await mutateRecordFiles(ownerSessionId, record, targets, true);
|
|
1572
|
+
return { checkpointId: clean(record.checkpointId), paths: recordFilePaths(record.root, targets), recorded: true };
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/** Undo a running turn's own segments (every file, or the one matching
|
|
1576
|
+
* `file`), newest first. */
|
|
1577
|
+
async function revertTrackedTurn(tracker, worktree, file) {
|
|
1578
|
+
if (!tracker.valid) throw new Error('turn review tracked file snapshot is unavailable');
|
|
1579
|
+
const patchRoot = trackerPatchRoot(tracker, resolve(clean(worktree) || '.'));
|
|
1580
|
+
const git = patchRoot.kind === 'git';
|
|
1581
|
+
let targets = [...tracker.chains.values()]
|
|
1582
|
+
.map((chain) => ({ chain, edit: chainEdit(chain, patchRoot.root) }))
|
|
1583
|
+
// Beside a Git root, an edit outside it is view-only, never undone.
|
|
1584
|
+
.filter(({ edit }) => edit && (!git || edit.inside));
|
|
1585
|
+
if (file) {
|
|
1586
|
+
const target = targets.find(({ chain }) => chainMatchesPath(chain, patchRoot.root, file));
|
|
1587
|
+
if (!target) throw new Error(UNATTRIBUTED_FILE);
|
|
1588
|
+
targets = [target];
|
|
1387
1589
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1590
|
+
if (targets.length === 0) return [];
|
|
1591
|
+
for (const { edit } of targets) {
|
|
1592
|
+
const omitted = edit.steps.flatMap(({ before, after }) => [before.content, after.content]).find(isOmittedContent);
|
|
1593
|
+
if (!omitted) continue;
|
|
1594
|
+
throw new Error(
|
|
1595
|
+
`turn review revert is unavailable for ${edit.start.displayPath}: binary/large file content was not captured (${omitted.size} bytes)`
|
|
1393
1596
|
);
|
|
1394
1597
|
}
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1598
|
+
const edits = targets.map(({ edit }) => edit);
|
|
1599
|
+
const objectFormat = await editsObjectFormat(edits, patchRoot);
|
|
1600
|
+
const budget = createLineDiffBudget();
|
|
1601
|
+
const files = edits.map((edit) => storedChangeFile(editChangeFile(edit, patchRoot.root, objectFormat, budget)));
|
|
1602
|
+
const blocked = files.find((entry) => entry.revertable !== true);
|
|
1603
|
+
if (blocked) throw new Error(`turn review revert is unavailable for ${blocked.path}: ${blocked.reason}`);
|
|
1604
|
+
await applyTurnChangeFiles({ root: patchRoot.root, kind: patchRoot.kind, counts: 'own' }, files, {
|
|
1605
|
+
reverse: true,
|
|
1606
|
+
});
|
|
1607
|
+
undoTrackedChains(
|
|
1608
|
+
tracker,
|
|
1609
|
+
targets.map(({ chain }) => chain)
|
|
1610
|
+
);
|
|
1611
|
+
const paths = recordFilePaths(patchRoot.root, files);
|
|
1612
|
+
noteTurnWorktreeChange(paths);
|
|
1613
|
+
if (tracker.worktreeSnapshot) {
|
|
1614
|
+
try {
|
|
1615
|
+
await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot, { force: true });
|
|
1616
|
+
} catch {
|
|
1617
|
+
// Best-effort refresh: the next review read captures afresh.
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return paths;
|
|
1398
1621
|
}
|
|
1399
1622
|
|
|
1400
|
-
/** Record a composer
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1623
|
+
/** Record a running turn's composer Undo in the record an earlier seal of the
|
|
1624
|
+
* same turn left (a completion wake reopened it): a record file whose every
|
|
1625
|
+
* side (a rename's old path too) was undone is `reverted`; every other file
|
|
1626
|
+
* keeps its state. */
|
|
1403
1627
|
async function recordComposerRevert(ownerSessionId, { checkpointId, paths }) {
|
|
1404
1628
|
if (!ownerSessionId || !checkpointId) return;
|
|
1405
1629
|
const record = await loadTurnChangeRecord(ownerSessionId, checkpointId);
|
|
@@ -1425,26 +1649,28 @@ async function recordComposerRevert(ownerSessionId, { checkpointId, paths }) {
|
|
|
1425
1649
|
/** Composer Undo of the latest turn, on the per-session change lock shared
|
|
1426
1650
|
* with historical Undo/Reapply, recorded in the turn's change record so the
|
|
1427
1651
|
* historical view offers Reapply rather than a second Undo. */
|
|
1428
|
-
function revertLatestTurn(worktree, sessionId, file,
|
|
1652
|
+
function revertLatestTurn(worktree, sessionId, file, expectedCheckpointId) {
|
|
1429
1653
|
const ownerSessionId = clean(sessionId);
|
|
1430
1654
|
return serializeChangeMutation(ownerSessionId, async () => {
|
|
1431
|
-
// A seal still freezing would record the pre-restore
|
|
1655
|
+
// A seal still freezing would record the pre-restore state after this.
|
|
1432
1656
|
const freezing = _diffTrackersBySession.get(ownerSessionId)?.freezing;
|
|
1433
1657
|
if (freezing) await freezing.catch(() => {});
|
|
1434
|
-
const restored = await revertLatestTurnUnlocked(worktree, ownerSessionId, file,
|
|
1435
|
-
await recordComposerRevert(ownerSessionId, restored);
|
|
1658
|
+
const restored = await revertLatestTurnUnlocked(worktree, ownerSessionId, file, expectedCheckpointId);
|
|
1659
|
+
if (!restored.recorded) await recordComposerRevert(ownerSessionId, restored);
|
|
1436
1660
|
return await getTurnReviewDiff(worktree, ownerSessionId);
|
|
1437
1661
|
});
|
|
1438
1662
|
}
|
|
1439
1663
|
|
|
1440
|
-
/**
|
|
1664
|
+
/** Undo this turn's own changes to one reviewed file (never a restore to HEAD
|
|
1665
|
+
* or to the turn-start bytes: foreign edits outside its hunks stay). */
|
|
1441
1666
|
export function revertTurnReviewFile(worktree, sessionId, file, expectedCheckpointId = '') {
|
|
1442
|
-
return
|
|
1667
|
+
if (!clean(file)) return Promise.reject(new TypeError('turn review file path is required'));
|
|
1668
|
+
return revertLatestTurn(worktree, sessionId, file, expectedCheckpointId);
|
|
1443
1669
|
}
|
|
1444
1670
|
|
|
1445
|
-
/**
|
|
1671
|
+
/** Undo this turn's own changes to every file its tools wrote. */
|
|
1446
1672
|
export function revertTurnReview(worktree, sessionId, expectedCheckpointId = '') {
|
|
1447
|
-
return revertLatestTurn(worktree, sessionId, null,
|
|
1673
|
+
return revertLatestTurn(worktree, sessionId, null, expectedCheckpointId);
|
|
1448
1674
|
}
|
|
1449
1675
|
|
|
1450
1676
|
// ── Historical turn changes (per-turn Undo/Reapply) ─────────────────────────
|
|
@@ -1483,9 +1709,10 @@ export async function getTurnChanges(sessionId, options = {}) {
|
|
|
1483
1709
|
// An old record's outside-root file kept no counts: measure it so the card
|
|
1484
1710
|
// agrees with the session review and the island.
|
|
1485
1711
|
if (record?.files?.length) {
|
|
1712
|
+
const budget = createLineDiffBudget();
|
|
1486
1713
|
record = {
|
|
1487
1714
|
...record,
|
|
1488
|
-
files: await Promise.all(record.files.map((file) => measureLegacyChangeFile(file, record.root))),
|
|
1715
|
+
files: await Promise.all(record.files.map((file) => measureLegacyChangeFile(file, record.root, budget))),
|
|
1489
1716
|
};
|
|
1490
1717
|
}
|
|
1491
1718
|
let status = 'missing';
|
|
@@ -1553,88 +1780,68 @@ function withUnknownChangeStates(ownerSessionId, record) {
|
|
|
1553
1780
|
return keys ? { ...record, files: markUnknownStates(record.files, keys) } : record;
|
|
1554
1781
|
}
|
|
1555
1782
|
|
|
1556
|
-
/**
|
|
1557
|
-
* Undo/Reapply
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1560
|
-
|
|
1783
|
+
/** Bring the latest-turn review (composer bar, session Diff) in line with an
|
|
1784
|
+
* Undo/Reapply that just landed: signal the moved paths, fold the record's
|
|
1785
|
+
* own chains out of (or back into) the turn's tracker when it is that turn,
|
|
1786
|
+
* re-capture the Git snapshot (sealed ones too) and persist its
|
|
1787
|
+
* attribution. Best-effort: the mutation itself already landed. */
|
|
1788
|
+
async function refreshLatestTurnReview(ownerSessionId, record, targets, reverse) {
|
|
1789
|
+
noteTurnWorktreeChange(recordFilePaths(record.root, targets));
|
|
1561
1790
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1562
|
-
if (!tracker
|
|
1563
|
-
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (inside(move.source) && inside(move.destination)) moves.push(move);
|
|
1791
|
+
if (!tracker) return;
|
|
1792
|
+
if (tracker.valid && clean(tracker.checkpointId) === clean(record.checkpointId)) {
|
|
1793
|
+
const root = resolve(record.root);
|
|
1794
|
+
if (reverse) {
|
|
1795
|
+
const matches = (chain) =>
|
|
1796
|
+
targets.some((file) => [file.path, file.oldPath].some((path) => chainMatchesPath(chain, root, path)));
|
|
1797
|
+
undoTrackedChains(tracker, [...tracker.chains.values()].filter(matches));
|
|
1798
|
+
} else {
|
|
1799
|
+
redoTrackedChains(tracker, root, targets);
|
|
1800
|
+
}
|
|
1573
1801
|
}
|
|
1574
|
-
if (
|
|
1802
|
+
if (!tracker.worktreeSnapshot) return;
|
|
1575
1803
|
try {
|
|
1576
|
-
|
|
1804
|
+
await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot, { force: true });
|
|
1577
1805
|
} catch {
|
|
1578
|
-
|
|
1806
|
+
// Best-effort refresh: persist the last captured state.
|
|
1579
1807
|
}
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
/** One moved file as a tracked change: its pre-mutation bytes and what the
|
|
1584
|
-
* mutation left at its destination. */
|
|
1585
|
-
async function movedFileChange(root, move) {
|
|
1586
|
-
const change = {
|
|
1587
|
-
path: move.source,
|
|
1588
|
-
displayPath: trackedRelativePath(root, { path: move.source }),
|
|
1589
|
-
before: move.before,
|
|
1590
|
-
after: await currentTrackedContent(move.destination),
|
|
1591
|
-
};
|
|
1592
|
-
if (move.destination === move.source) return change;
|
|
1593
|
-
return {
|
|
1594
|
-
...change,
|
|
1595
|
-
newPath: move.destination,
|
|
1596
|
-
newDisplayPath: trackedRelativePath(root, { path: move.destination }),
|
|
1597
|
-
};
|
|
1808
|
+
if (tracker.sealed) await persistTurnSnapshotRecord(ownerSessionId, tracker);
|
|
1598
1809
|
}
|
|
1599
1810
|
|
|
1600
|
-
/**
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
tracker.valid = false;
|
|
1612
|
-
releaseDiffTrackerContent(tracker);
|
|
1613
|
-
refreshUnifiedDiff(tracker);
|
|
1811
|
+
/** Reverse-apply (`reverse`) or forward-apply the record files `targets`
|
|
1812
|
+
* against the current disk, all-or-nothing: `git apply` checks every hunk
|
|
1813
|
+
* (every binary file's exact bytes) before it writes, so an overlapping
|
|
1814
|
+
* foreign edit fails the whole call and nothing is overwritten. Then their
|
|
1815
|
+
* state is recorded. Resolves to the updated record. */
|
|
1816
|
+
async function mutateRecordFiles(ownerSessionId, record, targets, reverse) {
|
|
1817
|
+
const verb = reverse ? 'undone' : 'reapplied';
|
|
1818
|
+
const checkpointId = clean(record.checkpointId);
|
|
1819
|
+
const blocked = targets.find((entry) => entry.revertable !== true);
|
|
1820
|
+
if (blocked) {
|
|
1821
|
+
throw new Error(`${blocked.path} cannot be ${verb}: ${blocked.reason || 'no replayable patch was recorded'}`);
|
|
1614
1822
|
}
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
* too) and persist its attribution. Best-effort: the mutation itself
|
|
1621
|
-
* already landed. */
|
|
1622
|
-
async function refreshLatestTurnReview(ownerSessionId, root, targets, fold) {
|
|
1623
|
-
noteTurnWorktreeChange(
|
|
1624
|
-
targets
|
|
1625
|
-
.flatMap((entry) => [entry.path, entry.oldPath])
|
|
1626
|
-
.filter(Boolean)
|
|
1627
|
-
.map((path) => resolve(root, path))
|
|
1628
|
-
);
|
|
1629
|
-
if (fold?.tracker.valid) await foldReviewMutation(fold);
|
|
1630
|
-
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1631
|
-
if (!tracker?.worktreeSnapshot) return;
|
|
1823
|
+
await applyTurnChangeFiles(record, targets, { reverse });
|
|
1824
|
+
// State lands only after git applied every target; a state that cannot
|
|
1825
|
+
// land takes the worktree change back with it.
|
|
1826
|
+
const keys = new Set(targets.map((entry) => changePathKey(entry.path)));
|
|
1827
|
+
let updated;
|
|
1632
1828
|
try {
|
|
1633
|
-
await
|
|
1634
|
-
} catch {
|
|
1635
|
-
|
|
1829
|
+
updated = await writeTurnChangeState(ownerSessionId, checkpointId, keys, reverse ? 'reverted' : 'applied');
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
try {
|
|
1832
|
+
await applyTurnChangeFiles(record, targets, { reverse: !reverse });
|
|
1833
|
+
} catch (rollbackError) {
|
|
1834
|
+
await markTurnChangeStateUnknown(ownerSessionId, checkpointId, keys);
|
|
1835
|
+
await refreshLatestTurnReview(ownerSessionId, record, targets, reverse);
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`turn changes were ${verb} but could not be recorded (${error.message}), and restoring the worktree failed (${rollbackError.message}); Undo/Reapply of those files is disabled`,
|
|
1838
|
+
{ cause: error }
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
throw error;
|
|
1636
1842
|
}
|
|
1637
|
-
|
|
1843
|
+
await refreshLatestTurnReview(ownerSessionId, record, targets, reverse);
|
|
1844
|
+
return updated;
|
|
1638
1845
|
}
|
|
1639
1846
|
|
|
1640
1847
|
/** Undo (`reverse`) or Reapply one file or the whole turn, all-or-nothing. */
|
|
@@ -1663,32 +1870,7 @@ async function mutateTurnChanges(sessionId, checkpointIdValue, fileValue, revisi
|
|
|
1663
1870
|
targets = (record.files || []).filter((entry) => fileState(entry) === from);
|
|
1664
1871
|
if (targets.length === 0) throw new Error(`this turn has no changes to be ${verb}`);
|
|
1665
1872
|
}
|
|
1666
|
-
const
|
|
1667
|
-
if (blocked) {
|
|
1668
|
-
throw new Error(`${blocked.path} cannot be ${verb}: ${blocked.reason || 'no replayable patch was recorded'}`);
|
|
1669
|
-
}
|
|
1670
|
-
const fold = await trackedReviewFold(ownerSessionId, record.root, targets, reverse);
|
|
1671
|
-
const root = await applyTurnChangeFiles(record, targets, { reverse });
|
|
1672
|
-
// State lands only after git applied every target; a state that cannot
|
|
1673
|
-
// land takes the worktree change back with it.
|
|
1674
|
-
const keys = new Set(targets.map((entry) => changePathKey(entry.path)));
|
|
1675
|
-
let updated;
|
|
1676
|
-
try {
|
|
1677
|
-
updated = await writeTurnChangeState(ownerSessionId, checkpointId, keys, reverse ? 'reverted' : 'applied');
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
try {
|
|
1680
|
-
await applyTurnChangeFiles(record, targets, { reverse: !reverse });
|
|
1681
|
-
} catch (rollbackError) {
|
|
1682
|
-
await markTurnChangeStateUnknown(ownerSessionId, checkpointId, keys);
|
|
1683
|
-
await refreshLatestTurnReview(ownerSessionId, root, targets, fold);
|
|
1684
|
-
throw new Error(
|
|
1685
|
-
`turn changes were ${verb} but could not be recorded (${error.message}), and restoring the worktree failed (${rollbackError.message}); Undo/Reapply of those files is disabled`,
|
|
1686
|
-
{ cause: error }
|
|
1687
|
-
);
|
|
1688
|
-
}
|
|
1689
|
-
throw error;
|
|
1690
|
-
}
|
|
1691
|
-
await refreshLatestTurnReview(ownerSessionId, root, targets, fold);
|
|
1873
|
+
const updated = await mutateRecordFiles(ownerSessionId, record, targets, reverse);
|
|
1692
1874
|
return turnChangesReply({
|
|
1693
1875
|
status: updated ? 'ready' : 'missing',
|
|
1694
1876
|
checkpointId,
|