mixdog 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +2 -1
- package/scripts/gate-local.mjs +19 -3
- package/scripts/model-catalog-audit.mjs +1 -15
- package/scripts/provider-pingpong-live.mjs +509 -0
- package/src/agents/advisor/AGENT.md +1 -1
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +3 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +6 -0
- 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 +7 -4
- package/src/output-styles/common.md +11 -10
- package/src/repl/slash-commands.mjs +16 -1
- package/src/rules/agent/AGENT.md +5 -5
- package/src/rules/lead/LEAD.md +5 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
- 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/client-config.mjs +36 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +48 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +48 -37
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +10 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +102 -45
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
- 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 +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +40 -7
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +88 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +30 -10
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +106 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +10 -61
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +94 -15
- 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 +75 -10
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +103 -23
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-messages.mjs +43 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +32 -6
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +27 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +64 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +70 -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 +80 -18
- package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +46 -8
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +273 -0
- package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +41 -5
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +369 -112
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +140 -35
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +70 -24
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +80 -29
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +69 -19
- 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 +246 -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 +388 -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/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 +48 -22
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +119 -18
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +13 -15
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -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 +33 -1
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +123 -61
- 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 +27 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +37 -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 +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +47 -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 +151 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +64 -23
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +79 -33
- 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 +172 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +144 -15
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +109 -26
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +27 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +34 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +23 -4
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
- 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 +43 -15
- 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 +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/text-relay.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
- package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +80 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +37 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +76 -33
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +323 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +44 -16
- package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +7 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
- 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 -35
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
- 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-terminal.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +41 -22
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +170 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +75 -70
- package/src/runtime/agent/orchestrator/providers/openrouter.mjs +75 -0
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +720 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +123 -219
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
- package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
- 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 +157 -0
- package/src/runtime/agent/orchestrator/providers/wire-route.mjs +512 -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 +183 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +100 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
- 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 +14 -3
- 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 +176 -71
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +62 -10
- package/src/runtime/agent/orchestrator/runtime-core/tool-exposure.mjs +96 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +14 -93
- package/src/runtime/agent/orchestrator/session/compact/summary-anchor.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +120 -0
- 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/eager-dispatch/admission.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +91 -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 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +21 -10
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +132 -0
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +67 -20
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +104 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-call-resolution.mjs +95 -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 +15 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +150 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +34 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +57 -21
- 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 +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +208 -14
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +16 -4
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +25 -3
- package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -2
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +168 -29
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +96 -0
- package/src/runtime/agent/orchestrator/session/side-question.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +17 -3
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +58 -2
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +41 -28
- package/src/runtime/agent/orchestrator/session/store/session-observers.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/store/write-admission.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
- package/src/runtime/agent/orchestrator/session/store-summary-archived-agent.mjs +34 -15
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/store.mjs +19 -3
- package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +1 -1
- 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 +77 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +14 -10
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +49 -34
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +23 -1
- 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/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/computer-bridge/action-schema.mjs +44 -21
- package/src/runtime/github/tool.mjs +92 -2
- 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/pg/adapter.mjs +98 -1
- package/src/runtime/memory/lib/pg/process.mjs +65 -50
- package/src/runtime/memory/lib/pg/run-child.mjs +49 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +124 -16
- 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 +2 -2
- package/src/runtime/office/authoring/html-document-author.mjs +2 -0
- package/src/runtime/office/authoring/pptx-contact-sheet.mjs +2 -0
- package/src/runtime/office/authoring/pptx-html-measure.mjs +2 -0
- package/src/runtime/office/authoring/pptx-html-runner.mjs +2 -0
- package/src/runtime/office/authoring/xlsx-html-runner.mjs +2 -0
- 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 +98 -0
- package/src/runtime/office/design/library/templates/mixdog-executive.mixdog-edit.pptx +0 -0
- package/src/runtime/office/index.mjs +30 -0
- 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/office/shared/values.mjs +0 -5
- package/src/runtime/shared/background-tasks.mjs +92 -14
- package/src/runtime/shared/bounded-file-read.mjs +13 -0
- package/src/runtime/shared/bounded-line-diff.mjs +182 -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 +31 -0
- package/src/runtime/shared/error-presentation.mjs +159 -19
- 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 +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +129 -134
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
- package/src/runtime/shared/llm/model-catalog.mjs +53 -40
- package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
- 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/llm/usage-rollup.mjs +0 -1
- package/src/runtime/shared/loopback-listener.mjs +9 -0
- package/src/runtime/shared/model-display.mjs +13 -0
- package/src/runtime/shared/provider-api-key.mjs +2 -0
- package/src/runtime/shared/quota-resume-store.mjs +11 -0
- package/src/runtime/shared/review-diff.mjs +52 -0
- package/src/runtime/shared/session-baseline-store.mjs +960 -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 +114 -0
- package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
- package/src/runtime/shared/statusline/statusline-segments.mjs +28 -4
- package/src/runtime/shared/task-notification-envelope.mjs +2 -2
- package/src/runtime/shared/tool-card-model/card-header.mjs +6 -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-limit.mjs +4 -0
- package/src/runtime/shared/tool-primitives.mjs +13 -0
- package/src/runtime/shared/tool-result-summary.mjs +8 -1
- package/src/runtime/shared/tool-surface.mjs +18 -3
- package/src/runtime/shared/tool-work-units.mjs +40 -4
- package/src/runtime/shared/transcript-writer/purge.mjs +74 -0
- package/src/runtime/shared/transcript-writer.mjs +10 -2
- package/src/runtime/shared/turn-changes.mjs +670 -274
- package/src/runtime/shared/turn-review-reply.mjs +8 -39
- package/src/runtime/shared/turn-snapshot-store.mjs +94 -2
- package/src/runtime/shared/turn-snapshot.mjs +986 -690
- package/src/runtime/shared/turn-worktree-snapshot.mjs +279 -262
- package/src/runtime/shared/user-data-guard.mjs +262 -49
- package/src/runtime/shared/worktrees/carry.mjs +531 -0
- package/src/runtime/shared/worktrees/contract.mjs +34 -24
- package/src/runtime/shared/worktrees/file-recovery.mjs +68 -0
- package/src/runtime/shared/worktrees/git-head.mjs +205 -0
- package/src/runtime/shared/worktrees/git.mjs +8 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +1 -1
- package/src/runtime/shared/worktrees/manager.mjs +156 -8
- package/src/runtime/shared/worktrees/merge.mjs +31 -21
- package/src/runtime/shared/worktrees/paths.d.mts +6 -0
- package/src/runtime/shared/worktrees/test-support.mjs +19 -4
- package/src/runtime/terminal-bridge/client.mjs +44 -11
- 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 +19 -5
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +12 -1
- package/src/session-runtime/boot/session.mjs +0 -1
- package/src/session-runtime/boot/tools.mjs +4 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +12 -5
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +65 -0
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +11 -4
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +115 -18
- package/src/session-runtime/internal-tool-executor.mjs +6 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +188 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +71 -27
- package/src/session-runtime/lifecycle/session-catalog.mjs +14 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +6 -0
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +29 -6
- package/src/session-runtime/lifecycle/teardown.mjs +73 -8
- package/src/session-runtime/lifecycle-api.mjs +1 -0
- package/src/session-runtime/mcp-glue/connect.mjs +46 -14
- package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
- package/src/session-runtime/mcp-glue.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +174 -0
- package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
- package/src/session-runtime/model-route/set-route.mjs +85 -17
- package/src/session-runtime/model-route/web-search-route.mjs +1 -1
- package/src/session-runtime/native-web-search/routes.mjs +22 -3
- package/src/session-runtime/native-web-search-usage.mjs +51 -0
- package/src/session-runtime/native-web-search.mjs +10 -1
- 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-models/model-meta.mjs +12 -3
- package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
- package/src/session-runtime/quick-web-search-models.mjs +13 -38
- package/src/session-runtime/resource-extension-sync.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +98 -2
- package/src/session-runtime/resource-scope-decorate.mjs +10 -3
- package/src/session-runtime/runtime-review-api.mjs +21 -2
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +81 -20
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
- 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.mjs +73 -7
- package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +15 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -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 +46 -7
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
- package/src/session-runtime/services/agent-tool.mjs +3 -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 +81 -15
- package/src/session-runtime/session-transcript-purge.mjs +32 -0
- package/src/session-runtime/session-turn-api.mjs +14 -5
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -0
- package/src/session-runtime/settings-compaction-api.mjs +1 -0
- package/src/session-runtime/setup-tool/executor.mjs +36 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +86 -6
- package/src/session-runtime/setup-tool/settings-contract.mjs +13 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +13 -4
- package/src/session-runtime/skills-api.mjs +3 -5
- package/src/session-runtime/tool-surface.mjs +5 -1
- 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 +22 -0
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +5 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +8 -3
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +10 -0
- package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
- package/src/session-runtime/turn/turn-run.mjs +25 -9
- package/src/session-runtime/usage-stats-api.mjs +43 -11
- package/src/session-runtime/workspace/project-branches.mjs +7 -5
- package/src/session-runtime/workspace/transition-steps.mjs +129 -31
- package/src/session-runtime/workspace/transitions.mjs +190 -15
- package/src/standalone/daemon-agent-control.mjs +9 -1
- package/src/standalone/daemon-drain-policy.mjs +8 -0
- 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 +51 -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 +2 -0
- package/src/standalone/daemon-telemetry.mjs +74 -0
- package/src/standalone/daemon.mjs +64 -8
- package/src/standalone/desktop-service-registry.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +70 -19
- 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 +1 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
- package/src/standalone/session-service/agent-tree.mjs +12 -0
- package/src/standalone/session-service/quota-resume.mjs +8 -0
- package/src/standalone/session-service/retention/busy.mjs +15 -1
- package/src/standalone/session-service/session-calls/session-action.mjs +18 -3
- package/src/standalone/session-service-api.mjs +11 -1
- package/src/standalone/session-service.mjs +1 -0
- package/src/standalone/session-state-patch.d.mts +2 -1
- package/src/standalone/session-state-patch.mjs +41 -1
- 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 +1 -0
- package/src/tui/app/btw-controller.mjs +118 -0
- package/src/tui/app/create-app-pickers.mjs +6 -0
- package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
- package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
- package/src/tui/app/extension-pickers.mjs +2 -1
- package/src/tui/app/model-options.mjs +11 -16
- 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 +20 -5
- package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
- package/src/tui/app/slash-dispatch/session-commands.mjs +19 -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/StatusLine.jsx +2 -0
- package/src/tui/components/question-panel-model.mjs +156 -0
- package/src/tui/components/status-line/local-line.mjs +5 -2
- package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
- package/src/tui/dist/index.mjs +3221 -1500
- package/src/tui/session/context-state.mjs +4 -2
- package/src/tui/session/quota-resume.mjs +46 -14
- package/src/tui/session/session-api/commands.mjs +43 -24
- package/src/tui/session/session-api/controls.mjs +2 -2
- package/src/tui/session/session-api/extensions.mjs +22 -0
- package/src/tui/session/session-api/integrations.mjs +16 -2
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +7 -2
- package/src/tui/session/session-api/routes.mjs +71 -7
- package/src/tui/session/session-api/settings.mjs +11 -14
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/session-flow/drain.mjs +7 -1
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
- package/src/tui/session/tool-approval.mjs +10 -7
- package/src/tui/session/transcript-spill.mjs +14 -7
- package/src/tui/session/turn-stream.mjs +174 -31
- package/src/tui/session/turn.mjs +46 -16
- package/src/tui/statusline-ansi-bridge.mjs +1 -1
- package/src/ui/statusline-agents.mjs +15 -4
- package/src/ui/statusline.mjs +7 -3
- package/src/ui/streaming-markdown-heal.mjs +34 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
- package/src/workflows/default/WORKFLOW.md +3 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
- package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
|
@@ -5,17 +5,28 @@
|
|
|
5
5
|
// turn-snapshot-store.mjs. Every file carries its own complete patch, separate
|
|
6
6
|
// from the bounded display patch the review bar renders, so a historical turn
|
|
7
7
|
// stays undoable after later turns, cache eviction or a runtime restart.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
8
9
|
import { lstat, open, realpath, stat } from 'node:fs/promises';
|
|
9
|
-
import { isAbsolute, relative, resolve } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
11
|
+
import { deflateSync } from 'node:zlib';
|
|
11
12
|
import { cleanString as clean } from './clean.mjs';
|
|
12
13
|
import { runGitOffThread } from './git-runner.mjs';
|
|
14
|
+
import { createLineDiffBudget, lineDiffHunks } from './bounded-line-diff.mjs';
|
|
15
|
+
import {
|
|
16
|
+
createReviewPatchBudget,
|
|
17
|
+
filePatchReply,
|
|
18
|
+
PATCH_TOO_LARGE,
|
|
19
|
+
patchOmittedFile,
|
|
20
|
+
REVIEW_FILE_PATCH_MAX_BYTES,
|
|
21
|
+
REVIEW_FILE_PATCH_SHOW_MAX_BYTES,
|
|
22
|
+
REVIEW_PATCH_MAX_BYTES,
|
|
23
|
+
reviewDiffTooLarge,
|
|
24
|
+
} from './review-diff.mjs';
|
|
13
25
|
|
|
14
26
|
/** Largest single-file patch a record keeps replayable. */
|
|
15
27
|
export const TURN_CHANGE_FILE_PATCH_MAX_BYTES = 8 * 1024 * 1024;
|
|
16
28
|
const APPLY_TIMEOUT_MS = 60_000;
|
|
17
29
|
const APPLY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
|
|
18
|
-
const TRACKED_DIFF_TIMEOUT_MS = 2_000;
|
|
19
30
|
const STATUSES = new Set(['A', 'M', 'D', 'R']);
|
|
20
31
|
// Characters a patch header cannot carry unquoted; such a path is recorded but
|
|
21
32
|
// never replayed.
|
|
@@ -156,13 +167,15 @@ export function splitPatchSections(buffer) {
|
|
|
156
167
|
});
|
|
157
168
|
}
|
|
158
169
|
|
|
159
|
-
// ── Exact tracker (
|
|
170
|
+
// ── Exact tracker patches (the session's own tool bytes) ────────────────────
|
|
160
171
|
|
|
161
172
|
const utf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
162
|
-
|
|
173
|
+
/** Text of one change side (bytes or a string): '' when absent, null for
|
|
174
|
+
* binary content (not UTF-8, or holding a NUL byte). */
|
|
175
|
+
export function decodeText(content) {
|
|
163
176
|
if (content == null) return '';
|
|
164
177
|
try {
|
|
165
|
-
const text = utf8Decoder.decode(content);
|
|
178
|
+
const text = typeof content === 'string' ? content : utf8Decoder.decode(content);
|
|
166
179
|
// A NUL byte marks binary content: it gets no text diff.
|
|
167
180
|
return text.includes('\0') ? null : text;
|
|
168
181
|
} catch {
|
|
@@ -170,13 +183,79 @@ function decodeText(content) {
|
|
|
170
183
|
}
|
|
171
184
|
}
|
|
172
185
|
|
|
186
|
+
/** True for bytes that get a binary patch instead of a text diff (not UTF-8,
|
|
187
|
+
* or holding a NUL byte). */
|
|
188
|
+
export function isBinaryChangeContent(content) {
|
|
189
|
+
return content != null && decodeText(content) === null;
|
|
190
|
+
}
|
|
191
|
+
|
|
173
192
|
const headerTab = (path) => (path.includes(' ') ? '\t' : '');
|
|
174
193
|
|
|
194
|
+
/** The object format (`sha1`/`sha256`) `git apply` hashes blobs with in `root`:
|
|
195
|
+
* the repository's own, or sha1 outside any repository. */
|
|
196
|
+
export async function repositoryObjectFormat(root) {
|
|
197
|
+
const result = await runGitOffThread(['rev-parse', '--show-object-format'], {
|
|
198
|
+
cwd: root,
|
|
199
|
+
timeoutMs: APPLY_TIMEOUT_MS,
|
|
200
|
+
maxBytes: APPLY_OUTPUT_MAX_BYTES,
|
|
201
|
+
}).catch(() => null);
|
|
202
|
+
return result?.code === 0 && clean(result.stdout) === 'sha256' ? 'sha256' : 'sha1';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const BASE85 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~';
|
|
206
|
+
|
|
207
|
+
/** One line of a git binary hunk: a length letter, then 4-byte groups as base85. */
|
|
208
|
+
function base85Line(chunk) {
|
|
209
|
+
let line = String.fromCharCode(chunk.length <= 26 ? 64 + chunk.length : 70 + chunk.length);
|
|
210
|
+
for (let offset = 0; offset < chunk.length; offset += 4) {
|
|
211
|
+
let value = 0;
|
|
212
|
+
for (let index = 0; index < 4; index += 1) value = value * 256 + (chunk[offset + index] ?? 0);
|
|
213
|
+
const group = new Array(5);
|
|
214
|
+
for (let index = 4; index >= 0; index -= 1) {
|
|
215
|
+
group[index] = BASE85[value % 85];
|
|
216
|
+
value = Math.floor(value / 85);
|
|
217
|
+
}
|
|
218
|
+
line += group.join('');
|
|
219
|
+
}
|
|
220
|
+
return line;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function binaryLiteral(bytes) {
|
|
224
|
+
const deflated = deflateSync(bytes);
|
|
225
|
+
let text = `literal ${bytes.length}\n`;
|
|
226
|
+
for (let offset = 0; offset < deflated.length; offset += 52) {
|
|
227
|
+
text += `${base85Line(deflated.subarray(offset, offset + 52))}\n`;
|
|
228
|
+
}
|
|
229
|
+
return `${text}\n`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The `index` line and `GIT binary patch` body of a byte change. Full blob
|
|
233
|
+
* ids make `git apply` verify the file still holds the recorded bytes on
|
|
234
|
+
* either side before it writes, so a binary file changed since is refused. */
|
|
235
|
+
function binaryPatchBody(before, after, status, objectFormat) {
|
|
236
|
+
const empty = Buffer.alloc(0);
|
|
237
|
+
const id = (bytes) => createHash(objectFormat).update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
|
|
238
|
+
const zero = '0'.repeat(objectFormat === 'sha256' ? 64 : 40);
|
|
239
|
+
const oldId = before === null ? zero : id(before);
|
|
240
|
+
const newId = after === null ? zero : id(after);
|
|
241
|
+
return `index ${oldId}..${newId}${status === 'M' ? ' 100644' : ''}\nGIT binary patch\n${binaryLiteral(after ?? empty)}${binaryLiteral(before ?? empty)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
175
244
|
/**
|
|
176
245
|
* One git-format patch from exact before/after bytes (null = absent). Paths
|
|
177
|
-
* are root-relative.
|
|
246
|
+
* are root-relative. Text diffs as hunks within `budget` (see
|
|
247
|
+
* bounded-line-diff.mjs); binary content (isBinaryChangeContent) as a
|
|
248
|
+
* full-index binary patch hashed in `objectFormat`. Returns the record file
|
|
249
|
+
* without its state.
|
|
178
250
|
*/
|
|
179
|
-
export function trackedChangeFile({
|
|
251
|
+
export function trackedChangeFile({
|
|
252
|
+
oldPath,
|
|
253
|
+
newPath,
|
|
254
|
+
before,
|
|
255
|
+
after,
|
|
256
|
+
objectFormat = 'sha1',
|
|
257
|
+
budget = createLineDiffBudget(),
|
|
258
|
+
}) {
|
|
180
259
|
let status = 'M';
|
|
181
260
|
if (before === null) status = 'A';
|
|
182
261
|
else if (after === null) status = 'D';
|
|
@@ -189,22 +268,16 @@ export function trackedChangeFile({ oldPath, newPath, before, after }) {
|
|
|
189
268
|
const oldText = decodeText(before);
|
|
190
269
|
const newText = decodeText(after);
|
|
191
270
|
if (oldText === null || newText === null) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (oldText !== newText) {
|
|
201
|
-
const result = structuredPatch(`a/${oldPath}`, `b/${newPath}`, oldText, newText, '', '', {
|
|
202
|
-
context: 3,
|
|
203
|
-
timeout: TRACKED_DIFF_TIMEOUT_MS,
|
|
204
|
-
});
|
|
205
|
-
if (!result) return { ...base, revertable: false, reason: 'the diff took too long to compute' };
|
|
206
|
-
hunks = result.hunks;
|
|
271
|
+
let patch = `diff --git a/${status === 'A' ? newPath : oldPath} b/${status === 'D' ? oldPath : newPath}\n`;
|
|
272
|
+
if (status === 'A') patch += 'new file mode 100644\n';
|
|
273
|
+
if (status === 'D') patch += 'deleted file mode 100644\n';
|
|
274
|
+
if (status === 'R') patch += `rename from ${oldPath}\nrename to ${newPath}\n`;
|
|
275
|
+
// A pure rename carries the full-index body too: `git apply` then moves
|
|
276
|
+
// the file only while it still holds the recorded bytes.
|
|
277
|
+
patch += binaryPatchBody(before, after, status, objectFormat);
|
|
278
|
+
return { ...base, binary: true, revertable: true, patch: Buffer.from(patch, 'utf8') };
|
|
207
279
|
}
|
|
280
|
+
const { hunks } = lineDiffHunks(oldText, newText, budget, [before, after]);
|
|
208
281
|
const oldName = status === 'A' ? newPath : oldPath;
|
|
209
282
|
const newName = status === 'D' ? oldPath : newPath;
|
|
210
283
|
let patch = `diff --git a/${oldName} b/${newName}\n`;
|
|
@@ -344,25 +417,33 @@ export function turnChangesReply({ status, checkpointId, record = null, patch =
|
|
|
344
417
|
return reply;
|
|
345
418
|
}
|
|
346
419
|
|
|
347
|
-
// ── Session review:
|
|
420
|
+
// ── Session review: first-seen baselines against the disk ───────────────────
|
|
348
421
|
|
|
349
422
|
export const NET_TEXT_MAX_BYTES = 1024 * 1024;
|
|
350
423
|
/** Budget of the merged session patch, in UTF-8 bytes. */
|
|
351
|
-
export const MERGED_PATCH_MAX_BYTES =
|
|
424
|
+
export const MERGED_PATCH_MAX_BYTES = REVIEW_PATCH_MAX_BYTES;
|
|
352
425
|
const VIEW_PATCH_MAX_BYTES = 256 * 1024;
|
|
353
426
|
|
|
354
|
-
/**
|
|
355
|
-
|
|
356
|
-
* Buffers or null (absent). */
|
|
357
|
-
export function namedChangePatch({ before, after, oldName, newName, maxBytes = VIEW_PATCH_MAX_BYTES }) {
|
|
358
|
-
const renamed = oldName !== newName;
|
|
427
|
+
/** Counts and a placeholder-named patch of exact before/after bytes. */
|
|
428
|
+
function measureChange(before, after, renamed, budget) {
|
|
359
429
|
const measured = trackedChangeFile({
|
|
360
430
|
oldPath: renamed ? 'mx-old-file' : 'mx-file',
|
|
361
431
|
newPath: renamed ? 'mx-new-file' : 'mx-file',
|
|
362
432
|
before,
|
|
363
433
|
after,
|
|
434
|
+
budget,
|
|
364
435
|
});
|
|
365
|
-
|
|
436
|
+
return {
|
|
437
|
+
status: measured.status,
|
|
438
|
+
additions: measured.additions,
|
|
439
|
+
deletions: measured.deletions,
|
|
440
|
+
...(measured.binary ? { binary: true } : {}),
|
|
441
|
+
text: measured.patch ? measured.patch.toString('utf8') : '',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** A measured patch with its placeholders replaced by the names a viewer shows. */
|
|
446
|
+
function nameMeasuredPatch(text, oldName, newName) {
|
|
366
447
|
const split = text.search(/\n@@ /);
|
|
367
448
|
const head = (split >= 0 ? text.slice(0, split) : text)
|
|
368
449
|
.split('mx-old-file')
|
|
@@ -371,48 +452,76 @@ export function namedChangePatch({ before, after, oldName, newName, maxBytes = V
|
|
|
371
452
|
.join(newName)
|
|
372
453
|
.split('mx-file')
|
|
373
454
|
.join(newName);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
455
|
+
return head + (split >= 0 ? text.slice(split) : '');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** A git-format patch of exact before/after text under the names a viewer
|
|
459
|
+
* shows (absolute for a file outside the worktree). `before`/`after` are
|
|
460
|
+
* Buffers or null (absent); `budget` the operation's line-diff budget. */
|
|
461
|
+
export function namedChangePatch({
|
|
462
|
+
before,
|
|
463
|
+
after,
|
|
464
|
+
oldName,
|
|
465
|
+
newName,
|
|
466
|
+
maxBytes = VIEW_PATCH_MAX_BYTES,
|
|
467
|
+
budget = createLineDiffBudget(),
|
|
468
|
+
}) {
|
|
469
|
+
const { text, ...measured } = measureChange(before, after, oldName !== newName, budget);
|
|
470
|
+
const patch = nameMeasuredPatch(text, oldName, newName);
|
|
471
|
+
return { ...measured, patch: Buffer.byteLength(patch, 'utf8') > maxBytes ? '' : patch };
|
|
382
472
|
}
|
|
383
473
|
|
|
384
474
|
const slashPath = (value) => clean(value).replace(/\\/g, '/');
|
|
385
475
|
const isOutsidePath = (path) => isAbsolute(path) || /^[A-Za-z]:[\\/]/.test(path);
|
|
386
|
-
const
|
|
476
|
+
const absolutePath = (root, path) => (isOutsidePath(path) ? resolve(path) : resolve(root, path));
|
|
387
477
|
const patchOf = (file) =>
|
|
388
478
|
changePatchBuffer(file)?.toString('utf8') || (typeof file?.viewPatch === 'string' ? file.viewPatch : '');
|
|
389
479
|
|
|
480
|
+
/** A record file's patch as a review shows it: a binary body becomes the
|
|
481
|
+
* `Binary files … differ` line a diff viewer renders. */
|
|
482
|
+
export function changeDisplayPatch(file) {
|
|
483
|
+
const buffer =
|
|
484
|
+
changePatchBuffer(file) || (typeof file?.viewPatch === 'string' ? Buffer.from(file.viewPatch, 'utf8') : null);
|
|
485
|
+
return splitPatchSections(buffer)
|
|
486
|
+
.map(({ oldPath, newPath, patch }) => {
|
|
487
|
+
const text = patch.toString('utf8');
|
|
488
|
+
const body = text.indexOf('\nGIT binary patch\n');
|
|
489
|
+
if (body < 0) return text;
|
|
490
|
+
const head = text.slice(0, body + 1).replace(/^index .*\n/m, '');
|
|
491
|
+
return `${head}Binary files ${oldPath === null ? '/dev/null' : `a/${oldPath}`} and ${newPath === null ? '/dev/null' : `b/${newPath}`} differ\n`;
|
|
492
|
+
})
|
|
493
|
+
.join('');
|
|
494
|
+
}
|
|
495
|
+
|
|
390
496
|
const insideDirectory = (parent, child) => {
|
|
391
497
|
const rel = relative(parent, child);
|
|
392
498
|
return rel === '' || (!isAbsolute(rel) && rel.split(/[\\/]+/)[0] !== '..');
|
|
393
499
|
};
|
|
394
500
|
|
|
395
|
-
/** At most
|
|
396
|
-
*
|
|
397
|
-
async function readBounded(file) {
|
|
501
|
+
/** At most `maxBytes` of a regular file, read through one handle: undefined
|
|
502
|
+
* when it is larger (checked by size first, never read whole). */
|
|
503
|
+
async function readBounded(file, maxBytes = NET_TEXT_MAX_BYTES) {
|
|
398
504
|
const handle = await open(file, 'r');
|
|
399
505
|
try {
|
|
400
506
|
const info = await handle.stat();
|
|
401
|
-
if (!info.isFile() || info.size >
|
|
402
|
-
|
|
507
|
+
if (!info.isFile() || info.size > maxBytes) return undefined;
|
|
508
|
+
// Sized by the file, one byte over: a file that grew since its stat is
|
|
509
|
+
// not read part-way.
|
|
510
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
403
511
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
404
|
-
return bytesRead >
|
|
512
|
+
return bytesRead > info.size ? undefined : buffer.subarray(0, bytesRead);
|
|
405
513
|
} finally {
|
|
406
514
|
await handle.close();
|
|
407
515
|
}
|
|
408
516
|
}
|
|
409
517
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
|
|
518
|
+
const missingFile = (error) => error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
|
|
519
|
+
|
|
520
|
+
/** The recorded file's absolute path when it may be read: null when absent,
|
|
521
|
+
* undefined when it is reached through a link. An in-root file is followed
|
|
522
|
+
* only while it stays inside the recorded root; an outside file is read only
|
|
523
|
+
* at its exact recorded path, never through a link. */
|
|
524
|
+
async function safeCurrentFile(root, path) {
|
|
416
525
|
const outside = isOutsidePath(path);
|
|
417
526
|
const file = outside ? resolve(path) : resolve(root, path);
|
|
418
527
|
try {
|
|
@@ -426,10 +535,22 @@ export async function currentText(root, path) {
|
|
|
426
535
|
const [realRoot, realFile] = await Promise.all([realpath(root), realpath(file)]);
|
|
427
536
|
if (!insideDirectory(realRoot, realFile)) return undefined;
|
|
428
537
|
}
|
|
538
|
+
return file;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
return missingFile(error) ? null : undefined;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** The file's current text: a string, null when absent, undefined when it
|
|
545
|
+
* cannot be netted (unreadable, binary, large, or reached through a link). */
|
|
546
|
+
export async function currentText(root, path) {
|
|
547
|
+
const file = await safeCurrentFile(root, path);
|
|
548
|
+
if (!file) return file;
|
|
549
|
+
try {
|
|
429
550
|
const bytes = await readBounded(file);
|
|
430
551
|
return bytes === undefined ? undefined : (decodeText(bytes) ?? undefined);
|
|
431
552
|
} catch (error) {
|
|
432
|
-
return error
|
|
553
|
+
return missingFile(error) ? null : undefined;
|
|
433
554
|
}
|
|
434
555
|
}
|
|
435
556
|
|
|
@@ -448,268 +569,536 @@ function isUnmeasuredFile(file) {
|
|
|
448
569
|
|
|
449
570
|
/** A legacy record kept no counts or diff for a new file it could not replay
|
|
450
571
|
* (outside the root): measure the file as it is on disk now. Returns the
|
|
451
|
-
* file with its counts and display patch, or the file unchanged.
|
|
452
|
-
|
|
572
|
+
* file with its counts and display patch, or the file unchanged. `budget`:
|
|
573
|
+
* the line-diff budget of the read it is part of. */
|
|
574
|
+
export async function measureLegacyChangeFile(file, root, budget = createLineDiffBudget()) {
|
|
453
575
|
if (!isUnmeasuredFile(file)) return file;
|
|
454
576
|
const text = await currentText(root, file.path);
|
|
455
577
|
if (typeof text !== 'string' || !text) return file;
|
|
456
578
|
const name = slashPath(file.path);
|
|
457
|
-
const named = namedChangePatch({
|
|
579
|
+
const named = namedChangePatch({
|
|
580
|
+
before: null,
|
|
581
|
+
after: Buffer.from(text, 'utf8'),
|
|
582
|
+
oldName: name,
|
|
583
|
+
newName: name,
|
|
584
|
+
budget,
|
|
585
|
+
});
|
|
458
586
|
if (!named.patch) return file;
|
|
459
587
|
return { ...file, additions: named.additions, deletions: named.deletions, viewPatch: named.patch };
|
|
460
588
|
}
|
|
461
589
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
text = result;
|
|
590
|
+
// Largest current file hashed against a baseline whose bytes were not kept.
|
|
591
|
+
const DIGEST_MAX_BYTES = 64 * 1024 * 1024;
|
|
592
|
+
// Largest measured patch a review keeps cached between polls.
|
|
593
|
+
const CACHED_PATCH_MAX_BYTES = 256 * 1024;
|
|
594
|
+
const READ_CONCURRENCY = 16;
|
|
595
|
+
const CURRENT_READ_MAX_BYTES = REVIEW_FILE_PATCH_SHOW_MAX_BYTES;
|
|
596
|
+
/** A file whose current content is past CURRENT_READ_MAX_BYTES (or cannot be
|
|
597
|
+
* read whole): listed without a patch, explicitly too large. */
|
|
598
|
+
const tooLargeChange = (status) => ({ status, additions: 0, deletions: 0, text: '', patchOmitted: true });
|
|
599
|
+
const UNREADABLE_REASON = 'the file cannot be read safely now';
|
|
600
|
+
const SEVERAL_TURNS_REASON = 'edited in several turns before its session baseline was recorded';
|
|
601
|
+
/** Why a listed file has no exact diff against the session's first state. */
|
|
602
|
+
const BASELINE_REASONS = {
|
|
603
|
+
'omitted-binary': "the session's original binary content was not captured",
|
|
604
|
+
'omitted-oversized': "the session's original content was too large to capture",
|
|
605
|
+
evicted: "the session's original content was evicted to bound storage",
|
|
606
|
+
missing: "the session's original state was not recorded",
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
610
|
+
|
|
611
|
+
async function mapLimit(items, limit, task) {
|
|
612
|
+
const results = new Array(items.length);
|
|
613
|
+
let next = 0;
|
|
614
|
+
const worker = async () => {
|
|
615
|
+
while (next < items.length) {
|
|
616
|
+
const index = next++;
|
|
617
|
+
results[index] = await task(items[index]);
|
|
491
618
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
619
|
+
};
|
|
620
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
621
|
+
return results;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** An absolute file under the link rules of currentText: relative to its
|
|
625
|
+
* recorded root when inside it, else its exact path. */
|
|
626
|
+
const fileRef = (root, abs) =>
|
|
627
|
+
root && insideDirectory(root, abs) ? { root, path: relative(root, abs) } : { root: dirname(abs), path: abs };
|
|
628
|
+
|
|
629
|
+
/** `{ state: 'absent' | 'unsafe' }` or `{ state: 'present', file, size, sig }`. */
|
|
630
|
+
async function currentFile({ root, path }) {
|
|
631
|
+
const file = await safeCurrentFile(root, path);
|
|
632
|
+
if (file === null) return { state: 'absent' };
|
|
633
|
+
if (file === undefined) return { state: 'unsafe' };
|
|
634
|
+
try {
|
|
635
|
+
const info = await stat(file);
|
|
636
|
+
if (!info.isFile()) return { state: 'unsafe' };
|
|
637
|
+
return { state: 'present', file, size: info.size, sig: `${info.size}:${info.mtimeMs}:${info.ctimeMs}` };
|
|
638
|
+
} catch (error) {
|
|
639
|
+
return { state: missingFile(error) ? 'absent' : 'unsafe' };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const currentSig = (current) => (current?.state === 'present' ? current.sig : current?.state || '');
|
|
644
|
+
|
|
645
|
+
/** Bytes of a present file up to CURRENT_READ_MAX_BYTES — what one file's
|
|
646
|
+
* own patch read may show, so a file past the reply's per-file cap still has
|
|
647
|
+
* its counts and its patch on request; undefined past it or when it cannot
|
|
648
|
+
* be read. */
|
|
649
|
+
async function currentBytes(current) {
|
|
650
|
+
if (current.size > CURRENT_READ_MAX_BYTES) return undefined;
|
|
651
|
+
try {
|
|
652
|
+
return await readBounded(current.file, CURRENT_READ_MAX_BYTES);
|
|
653
|
+
} catch {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function fileDigest(file) {
|
|
659
|
+
let handle;
|
|
660
|
+
try {
|
|
661
|
+
handle = await open(file, 'r');
|
|
662
|
+
const info = await handle.stat();
|
|
663
|
+
if (!info.isFile() || info.size > DIGEST_MAX_BYTES) return undefined;
|
|
664
|
+
const hash = createHash('sha256');
|
|
665
|
+
const buffer = Buffer.alloc(1024 * 1024);
|
|
666
|
+
for (let position = 0; ; ) {
|
|
667
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
668
|
+
if (!bytesRead) break;
|
|
669
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
670
|
+
position += bytesRead;
|
|
495
671
|
}
|
|
672
|
+
return hash.digest('hex');
|
|
673
|
+
} catch {
|
|
674
|
+
return undefined;
|
|
675
|
+
} finally {
|
|
676
|
+
await handle?.close().catch(() => {});
|
|
496
677
|
}
|
|
497
|
-
return text;
|
|
498
678
|
}
|
|
499
679
|
|
|
500
|
-
/**
|
|
501
|
-
*
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
680
|
+
/** Whether a present file still holds the baseline's bytes: true, false, or
|
|
681
|
+
* undefined when that cannot be told. */
|
|
682
|
+
async function sameAsBaseline(entry, current, after) {
|
|
683
|
+
if (entry.size > 0 && entry.size !== current.size) return false;
|
|
684
|
+
if (!entry.sha256) return undefined;
|
|
685
|
+
const hash = after ? sha256(after) : await fileDigest(current.file);
|
|
686
|
+
return hash === undefined ? undefined : hash === entry.sha256;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** A row the review cannot measure exactly: it never vouches for the set. */
|
|
690
|
+
const partialChange = (status, reason, extra = {}) => ({
|
|
691
|
+
status,
|
|
692
|
+
additions: 0,
|
|
693
|
+
deletions: 0,
|
|
694
|
+
text: '',
|
|
695
|
+
reason,
|
|
696
|
+
partial: true,
|
|
697
|
+
...extra,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const baselineGap = (entry, status) =>
|
|
701
|
+
partialChange(status, BASELINE_REASONS[entry.status] || BASELINE_REASONS.missing, {
|
|
702
|
+
baseline: entry.status,
|
|
703
|
+
...(entry.status === 'omitted-binary' ? { binary: true } : {}),
|
|
515
704
|
});
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
async function
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
});
|
|
546
|
-
const omitted = !net.patch && net.additions + net.deletions > 0;
|
|
547
|
-
return {
|
|
548
|
-
path,
|
|
549
|
-
...identity,
|
|
550
|
-
...(oldName !== path ? { oldPath: oldName } : {}),
|
|
551
|
-
...net,
|
|
552
|
-
...(omitted ? { patchOmitted: true } : {}),
|
|
553
|
-
};
|
|
705
|
+
|
|
706
|
+
/** A file matching the hash of a first state whose bytes were not kept: not
|
|
707
|
+
* listed, but it does not let the review vouch for the set either. */
|
|
708
|
+
const UNVOUCHED_UNCHANGED = Object.freeze({ hidden: true, partial: true });
|
|
709
|
+
|
|
710
|
+
/** One file (or rename) measured from its first-seen state `entry` to the
|
|
711
|
+
* disk now (`current`); null when the session left it as it found it,
|
|
712
|
+
* UNVOUCHED_UNCHANGED when only a hash says so. */
|
|
713
|
+
async function measureBaseline(entry, current, renamed, readBlob, budget) {
|
|
714
|
+
const changed = renamed ? 'R' : 'M';
|
|
715
|
+
if (current.state === 'unsafe') {
|
|
716
|
+
return partialChange(entry.state === 'absent' ? 'A' : changed, UNREADABLE_REASON);
|
|
717
|
+
}
|
|
718
|
+
const present = current.state === 'present';
|
|
719
|
+
const after = present ? await currentBytes(current) : null;
|
|
720
|
+
if (entry.state === 'absent') {
|
|
721
|
+
if (!present) return null;
|
|
722
|
+
if (after === undefined) return tooLargeChange('A');
|
|
723
|
+
return measureChange(null, after, false, budget);
|
|
724
|
+
}
|
|
725
|
+
if (present) {
|
|
726
|
+
const same = await sameAsBaseline(entry, current, after);
|
|
727
|
+
const exact = entry.status === 'captured';
|
|
728
|
+
if (same === true && !renamed) return exact ? null : UNVOUCHED_UNCHANGED;
|
|
729
|
+
if (same === true) {
|
|
730
|
+
const moved = measureChange(Buffer.alloc(0), Buffer.alloc(0), true, budget);
|
|
731
|
+
return exact
|
|
732
|
+
? moved
|
|
733
|
+
: { ...moved, partial: true, baseline: entry.status, reason: BASELINE_REASONS[entry.status] };
|
|
554
734
|
}
|
|
735
|
+
if (same === undefined || !exact) return baselineGap(entry, changed);
|
|
555
736
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
737
|
+
const before = entry.status === 'captured' ? await readBlob(entry) : undefined;
|
|
738
|
+
if (!before)
|
|
739
|
+
return baselineGap(entry.status === 'captured' ? { ...entry, status: 'evicted' } : entry, present ? changed : 'D');
|
|
740
|
+
if (after === undefined) return tooLargeChange(changed);
|
|
741
|
+
return measureChange(before, after, renamed, budget);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** A list measurement as a reply keeps it: text past the per-file inline cap
|
|
745
|
+
* is dropped at once, its counts kept and the file marked too large. */
|
|
746
|
+
function inlineChange(change) {
|
|
747
|
+
if (Buffer.byteLength(change?.text || '', 'utf8') <= REVIEW_FILE_PATCH_MAX_BYTES) return change;
|
|
748
|
+
return { ...change, text: '', patchOmitted: true };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Every baseline not `covered` by another source, measured against the disk
|
|
752
|
+
* within the review's line-diff `budget`. A path holding a renamed file pairs
|
|
753
|
+
* with the path it came from when that one is gone. Measurements are reused
|
|
754
|
+
* while the files' stats are unchanged. With `target` (an entry predicate)
|
|
755
|
+
* only the units it matches are measured, with their whole text; every other
|
|
756
|
+
* unit is returned `nameOnly`, for naming. */
|
|
757
|
+
async function baselineChanges(baselines, covered, unvouched, budget, { target = null } = {}) {
|
|
758
|
+
const entries = new Map();
|
|
759
|
+
for (const entry of baselines.files) if (!covered(entry.path)) entries.set(entry.key, entry);
|
|
760
|
+
const list = [...entries.values()];
|
|
761
|
+
const states = await mapLimit(list, READ_CONCURRENCY, (entry) => currentFile(fileRef(entry.root, entry.path)));
|
|
762
|
+
const current = new Map(list.map((entry, index) => [entry.key, states[index]]));
|
|
763
|
+
const paired = new Set();
|
|
764
|
+
const units = [];
|
|
765
|
+
for (const [destinationKey, originKey] of baselines.renames) {
|
|
766
|
+
const destination = entries.get(destinationKey);
|
|
767
|
+
const origin = entries.get(originKey);
|
|
768
|
+
if (!destination || !origin || paired.has(destinationKey) || paired.has(originKey)) continue;
|
|
769
|
+
if (destination.state !== 'absent' || origin.state === 'absent') continue;
|
|
770
|
+
if (current.get(destinationKey).state === 'absent' || current.get(originKey).state !== 'absent') continue;
|
|
771
|
+
paired.add(destinationKey);
|
|
772
|
+
paired.add(originKey);
|
|
773
|
+
units.push({ key: `${originKey}\0${destinationKey}`, before: origin, after: destination });
|
|
774
|
+
}
|
|
775
|
+
for (const entry of list) if (!paired.has(entry.key)) units.push({ key: entry.key, before: entry, after: entry });
|
|
776
|
+
const measured = await mapLimit(units, READ_CONCURRENCY, async (unit) => {
|
|
777
|
+
const renamed = unit.before !== unit.after;
|
|
778
|
+
const named = {
|
|
779
|
+
key: unit.after.key,
|
|
780
|
+
abs: unit.after.path,
|
|
781
|
+
root: unit.after.root,
|
|
782
|
+
...(renamed ? { oldAbs: unit.before.path, oldRoot: unit.before.root } : {}),
|
|
783
|
+
};
|
|
784
|
+
const now = current.get(unit.after.key);
|
|
785
|
+
const sig = [
|
|
786
|
+
unit.before.state,
|
|
787
|
+
unit.before.status,
|
|
788
|
+
unit.before.sha256 || '',
|
|
789
|
+
unit.before.blob || '',
|
|
790
|
+
unit.before.at,
|
|
791
|
+
unit.after.at,
|
|
792
|
+
currentSig(now),
|
|
793
|
+
currentSig(current.get(unit.before.key)),
|
|
794
|
+
].join('|');
|
|
795
|
+
const cached = baselines.cache?.get(unit.key);
|
|
796
|
+
if (target && !target(unit.after) && !target(unit.before)) {
|
|
797
|
+
// Not measured: it names a file unless the cache knows it is unchanged.
|
|
798
|
+
const unchanged = cached?.sig === sig && (!cached.change || cached.change.hidden);
|
|
799
|
+
return unchanged ? null : { ...named, nameOnly: true };
|
|
800
|
+
}
|
|
801
|
+
// A file's own patch read measures again what was cached without its text.
|
|
802
|
+
let change = cached?.sig === sig && !(target && cached.stripped) ? cached.change : undefined;
|
|
803
|
+
if (change === undefined) {
|
|
804
|
+
const whole = await measureBaseline(unit.before, now, renamed, baselines.readBlob, budget);
|
|
805
|
+
const inline = inlineChange(whole);
|
|
806
|
+
// Once the budget is spent a diff may have been cut short to the
|
|
807
|
+
// replacement hunk: such a measurement is not kept, a later review
|
|
808
|
+
// measures it again.
|
|
809
|
+
if (budget.remainingMs > 0 && Buffer.byteLength(inline?.text || '', 'utf8') <= CACHED_PATCH_MAX_BYTES) {
|
|
810
|
+
baselines.cache?.set(unit.key, { sig, change: inline, ...(inline !== whole ? { stripped: true } : {}) });
|
|
811
|
+
}
|
|
812
|
+
change = target ? whole : inline;
|
|
813
|
+
}
|
|
814
|
+
if (change?.hidden) unvouched.add(unit.after.key);
|
|
815
|
+
if (!change || change.hidden) return null;
|
|
816
|
+
return { ...named, change };
|
|
817
|
+
});
|
|
818
|
+
return measured.filter(Boolean);
|
|
581
819
|
}
|
|
582
820
|
|
|
583
|
-
/**
|
|
584
|
-
*
|
|
585
|
-
|
|
586
|
-
|
|
821
|
+
/**
|
|
822
|
+
* Files of the retained turn records that no baseline lists — a session (or
|
|
823
|
+
* turns) recorded before baselines existed. Never exact: a file edited in one
|
|
824
|
+
* turn keeps that turn's diff, one edited in several is listed unmeasured.
|
|
825
|
+
* Also returns the baseline keys such older records touched: those baselines
|
|
826
|
+
* were first seen after an unrecorded edit.
|
|
827
|
+
*/
|
|
828
|
+
async function legacyChanges(records, { kinds, listed, covered, since, budget }) {
|
|
829
|
+
const groups = new Map();
|
|
830
|
+
const late = new Set();
|
|
587
831
|
for (const record of records) {
|
|
588
832
|
const root = clean(record?.root);
|
|
589
833
|
const entries = [
|
|
590
|
-
...(
|
|
834
|
+
...(kinds.has(record?.kind) ? record.files || [] : []),
|
|
591
835
|
...(Array.isArray(record?.outside) ? record.outside : []),
|
|
592
836
|
];
|
|
593
837
|
for (const file of entries) {
|
|
594
838
|
if (!file?.path || file.state === 'reverted') continue;
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
839
|
+
const abs = absolutePath(root, file.path);
|
|
840
|
+
const key = changePathKey(abs);
|
|
841
|
+
const oldAbs = file.oldPath ? absolutePath(root, file.oldPath) : '';
|
|
842
|
+
if (covered(abs)) continue;
|
|
843
|
+
if (listed.has(key) || (oldAbs && listed.has(changePathKey(oldAbs)))) {
|
|
844
|
+
if (!(Number(record.createdAt) >= since)) late.add(key);
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const prior = groups.get(key) || (oldAbs ? groups.get(changePathKey(oldAbs)) : undefined);
|
|
848
|
+
if (prior) groups.delete(prior.key);
|
|
849
|
+
let origin = {};
|
|
850
|
+
if (prior) origin = { oldAbs: prior.oldAbs, oldRoot: prior.oldRoot };
|
|
851
|
+
else if (oldAbs) origin = { oldAbs, oldRoot: root };
|
|
852
|
+
groups.set(key, {
|
|
853
|
+
key,
|
|
854
|
+
abs,
|
|
855
|
+
root,
|
|
856
|
+
file,
|
|
857
|
+
first: prior?.first || file,
|
|
858
|
+
turns: (prior?.turns || 0) + 1,
|
|
859
|
+
...origin,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const changes = [];
|
|
864
|
+
for (const group of groups.values()) {
|
|
865
|
+
const { file, first } = group;
|
|
866
|
+
if (group.turns > 1 && first.status === 'A' && file.status === 'D') continue;
|
|
867
|
+
const unit = {
|
|
868
|
+
key: group.key,
|
|
869
|
+
abs: group.abs,
|
|
870
|
+
root: group.root,
|
|
871
|
+
...(group.oldAbs ? { oldAbs: group.oldAbs, oldRoot: group.oldRoot } : {}),
|
|
872
|
+
};
|
|
873
|
+
if (group.turns > 1) {
|
|
874
|
+
const status = first.status === 'A' ? 'A' : file.status;
|
|
875
|
+
changes.push({
|
|
876
|
+
...unit,
|
|
877
|
+
change: partialChange(status, SEVERAL_TURNS_REASON, { baseline: 'missing', unmeasured: true }),
|
|
878
|
+
});
|
|
879
|
+
continue;
|
|
602
880
|
}
|
|
881
|
+
const measured = await measureLegacyChangeFile(file, group.root, budget);
|
|
882
|
+
const recordPatch = patchOf(measured);
|
|
883
|
+
const counts = (Number(measured.additions) || 0) + (Number(measured.deletions) || 0);
|
|
884
|
+
changes.push({
|
|
885
|
+
...unit,
|
|
886
|
+
change: {
|
|
887
|
+
status: file.status,
|
|
888
|
+
additions: Number(measured.additions) || 0,
|
|
889
|
+
deletions: Number(measured.deletions) || 0,
|
|
890
|
+
...(measured.binary ? { binary: true } : {}),
|
|
891
|
+
text: '',
|
|
892
|
+
recordPatch,
|
|
893
|
+
recordName: slashPath(file.path),
|
|
894
|
+
partial: true,
|
|
895
|
+
baseline: 'missing',
|
|
896
|
+
...(!recordPatch && counts === 0 && !measured.binary
|
|
897
|
+
? { unmeasured: true, reason: clean(file.reason) || BASELINE_REASONS.missing }
|
|
898
|
+
: {}),
|
|
899
|
+
...(!recordPatch && file.patchOmitted === true ? { patchOmitted: true } : {}),
|
|
900
|
+
},
|
|
901
|
+
});
|
|
603
902
|
}
|
|
604
|
-
return
|
|
903
|
+
return { changes, late };
|
|
605
904
|
}
|
|
606
905
|
|
|
607
|
-
/**
|
|
608
|
-
* lists the
|
|
609
|
-
|
|
610
|
-
const
|
|
906
|
+
/** Files (`{ root, abs }`) the session claims but no baseline or record
|
|
907
|
+
* lists: the ones still present are listed without a diff. */
|
|
908
|
+
async function missingChanges(refs, { listed, covered }) {
|
|
909
|
+
const files = [];
|
|
910
|
+
for (const { root, abs } of refs) {
|
|
911
|
+
const key = changePathKey(abs);
|
|
912
|
+
if (listed.has(key) || covered(abs)) continue;
|
|
913
|
+
listed.add(key);
|
|
914
|
+
files.push({ key, abs, root });
|
|
915
|
+
}
|
|
916
|
+
const states = await mapLimit(files, READ_CONCURRENCY, (file) => currentFile(fileRef(file.root, file.abs)));
|
|
917
|
+
return files.map((file, index) =>
|
|
918
|
+
states[index].state === 'absent'
|
|
919
|
+
? { ...file, gone: true }
|
|
920
|
+
: { ...file, change: partialChange('M', BASELINE_REASONS.missing, { baseline: 'missing' }) }
|
|
921
|
+
);
|
|
922
|
+
}
|
|
611
923
|
|
|
612
924
|
/**
|
|
613
|
-
* The session-wide review
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
925
|
+
* The session-wide review: the Git baseline review (`base`) completed with
|
|
926
|
+
* every file it does not cover — a non-Git root, or a file outside the Git
|
|
927
|
+
* root — measured from its first-seen state in `baselines` (the Lead's and
|
|
928
|
+
* its attributed workers' edits together) against the disk now. `base.source`
|
|
929
|
+
* says what the Git side vouches for (`git`: its root; `empty`: nothing was
|
|
930
|
+
* owned). Retained `records` only list files no baseline knows (a session
|
|
931
|
+
* recorded before baselines existed) and never make the result complete.
|
|
932
|
+
* `complete` is set only when every listed file was measured exactly from a
|
|
933
|
+
* first state the session itself recorded; otherwise the result is `partial`.
|
|
934
|
+
* A file the user undid is back at its first state and is not listed. Each
|
|
935
|
+
* listed file carries an `id` (its absolute path); two roots sharing a
|
|
936
|
+
* relative name both show their absolute path. A file's patch past the
|
|
937
|
+
* per-file cap, or past what the reply's total leaves, is left out
|
|
938
|
+
* (`patchOmitted`, reason PATCH_TOO_LARGE); `diffTooLarge` once nothing fit.
|
|
939
|
+
* `file` (an `id` or listed path) answers that one file's patch instead, up
|
|
940
|
+
* to the file-patch read's cap.
|
|
626
941
|
*/
|
|
627
|
-
export async function
|
|
628
|
-
const source = base
|
|
942
|
+
export async function sessionChangeReview(base, { baselines = null, records = [], file: target = '' } = {}) {
|
|
943
|
+
const { source = '', reason: unavailableReason, owned, diffTooLarge: _baseTooLarge, ...kept } = base || {};
|
|
629
944
|
const gitBacked = source === 'git';
|
|
630
|
-
const cleared = source === 'tool' ? { ...base, files: [], patch: '' } : base;
|
|
631
|
-
const retained = Array.isArray(records) ? records : [];
|
|
632
|
-
const withLive =
|
|
633
|
-
live && !retained.some((record) => record?.checkpointId === live.checkpointId) ? [...retained, live] : retained;
|
|
634
|
-
let inRootKinds = new Set(['tracked']);
|
|
635
|
-
if (gitBacked) inRootKinds = new Set();
|
|
636
|
-
else if (source === 'absent' || source === 'unavailable') inRootKinds = new Set(['git', 'tracked']);
|
|
637
|
-
const baseFiles = Array.isArray(cleared?.files) ? cleared.files : [];
|
|
638
|
-
const basePatch = typeof cleared?.patch === 'string' ? cleared.patch : '';
|
|
639
945
|
const baseRoot = clean(base?.root);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
946
|
+
const covered = (abs) => gitBacked && Boolean(baseRoot) && insideDirectory(baseRoot, abs);
|
|
947
|
+
const listed = new Set((baselines?.files || []).map((entry) => entry.key));
|
|
948
|
+
let kinds = new Set(['tracked']);
|
|
949
|
+
if (gitBacked) kinds = new Set();
|
|
950
|
+
else if (source === 'absent' || source === 'unavailable') kinds = new Set(['git', 'tracked']);
|
|
951
|
+
// Baselines nothing is listed for, yet whose emptiness cannot be vouched for.
|
|
952
|
+
const unvouched = new Set();
|
|
953
|
+
// One line-diff budget for the whole review pass.
|
|
954
|
+
const diffBudget = createLineDiffBudget();
|
|
955
|
+
// A file's own patch read measures only the baselines of that file (by
|
|
956
|
+
// `id` or by its name under its root).
|
|
957
|
+
const targetKey = clean(target) ? changePathKey(target) : '';
|
|
958
|
+
const targeted = (entry) =>
|
|
959
|
+
changePathKey(entry.path) === targetKey ||
|
|
960
|
+
(Boolean(entry.root) &&
|
|
961
|
+
insideDirectory(entry.root, entry.path) &&
|
|
962
|
+
changePathKey(relative(entry.root, entry.path)) === targetKey);
|
|
963
|
+
const baselineUnits = baselines
|
|
964
|
+
? await baselineChanges(baselines, covered, unvouched, diffBudget, { target: targetKey ? targeted : null })
|
|
965
|
+
: [];
|
|
966
|
+
// Units named but not measured still claim their names (see `show`).
|
|
967
|
+
const measured = baselineUnits.filter((unit) => !unit.nameOnly);
|
|
968
|
+
const legacy = await legacyChanges(Array.isArray(records) ? records : [], {
|
|
969
|
+
kinds,
|
|
970
|
+
listed,
|
|
971
|
+
covered,
|
|
972
|
+
since: Number(baselines?.createdAt) || Number.POSITIVE_INFINITY,
|
|
973
|
+
budget: diffBudget,
|
|
974
|
+
});
|
|
975
|
+
for (const change of legacy.changes) listed.add(change.key);
|
|
976
|
+
// Owned files of the non-Git root, and paths whose first state a clear in
|
|
977
|
+
// another process removed (the baselines are incomplete for them).
|
|
978
|
+
const refs = (baselines?.missing || []).map((entry) => ({ root: entry.root, abs: entry.path }));
|
|
979
|
+
if (!gitBacked && Array.isArray(owned)) {
|
|
980
|
+
for (const path of owned) refs.push({ root: baseRoot, abs: absolutePath(baseRoot, path) });
|
|
981
|
+
}
|
|
982
|
+
const missing = await missingChanges(refs, { listed, covered });
|
|
983
|
+
for (const change of measured) {
|
|
984
|
+
if (legacy.late.has(change.key)) change.change = { ...change.change, partial: true, reason: SEVERAL_TURNS_REASON };
|
|
985
|
+
}
|
|
986
|
+
const changes = [...measured, ...legacy.changes, ...missing.filter((file) => file.change)];
|
|
987
|
+
const baseFiles = gitBacked && Array.isArray(base.files) ? base.files : [];
|
|
988
|
+
const basePatch = gitBacked && typeof base.patch === 'string' ? base.patch : '';
|
|
989
|
+
// A relative name shared by different roots (or with a Git file) is shown
|
|
990
|
+
// absolute, so each file stays distinct.
|
|
991
|
+
const nameOf = (abs, root) => (root && insideDirectory(root, abs) ? slashPath(relative(root, abs)) : slashPath(abs));
|
|
648
992
|
const owners = new Map();
|
|
649
|
-
const claim = (
|
|
650
|
-
|
|
651
|
-
|
|
993
|
+
const claim = (name, abs) => {
|
|
994
|
+
if (isOutsidePath(name)) return;
|
|
995
|
+
const key = changePathKey(name);
|
|
996
|
+
owners.set(key, new Set([...(owners.get(key) || []), changePathKey(abs)]));
|
|
652
997
|
};
|
|
653
|
-
for (const file of baseFiles) if (file?.path) claim(file.path,
|
|
654
|
-
for (const
|
|
655
|
-
|
|
656
|
-
claim(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
998
|
+
for (const file of baseFiles) if (file?.path) claim(file.path, resolve(baseRoot, file.path));
|
|
999
|
+
for (const change of [...changes, ...baselineUnits.filter((unit) => unit.nameOnly)]) {
|
|
1000
|
+
claim(nameOf(change.abs, change.root), change.abs);
|
|
1001
|
+
if (change.oldAbs) claim(nameOf(change.oldAbs, change.oldRoot), change.oldAbs);
|
|
1002
|
+
}
|
|
1003
|
+
const show = (abs, root) => {
|
|
1004
|
+
const name = nameOf(abs, root);
|
|
1005
|
+
return isOutsidePath(name) || (owners.get(changePathKey(name))?.size || 0) <= 1 ? name : slashPath(abs);
|
|
1006
|
+
};
|
|
1007
|
+
const rows = changes
|
|
1008
|
+
.map(({ abs, root, oldAbs, oldRoot, change }) => {
|
|
1009
|
+
const path = show(abs, root);
|
|
1010
|
+
const oldName = oldAbs ? show(oldAbs, oldRoot) : path;
|
|
1011
|
+
let patch = '';
|
|
1012
|
+
if (change.text) patch = nameMeasuredPatch(change.text, oldName, path);
|
|
1013
|
+
else if (change.recordPatch && change.recordName === path) patch = change.recordPatch;
|
|
1014
|
+
return {
|
|
1015
|
+
path,
|
|
1016
|
+
id: slashPath(abs),
|
|
1017
|
+
outside: !(root && insideDirectory(root, abs)),
|
|
1018
|
+
...(oldName !== path ? { oldPath: oldName } : {}),
|
|
1019
|
+
status: change.status,
|
|
1020
|
+
additions: change.additions,
|
|
1021
|
+
deletions: change.deletions,
|
|
1022
|
+
...(change.binary ? { binary: true } : {}),
|
|
1023
|
+
...(change.reason ? { reason: change.reason } : {}),
|
|
1024
|
+
...(change.baseline ? { baseline: change.baseline } : {}),
|
|
1025
|
+
...(change.unmeasured ? { unmeasured: true } : {}),
|
|
1026
|
+
// Its patch is past what can be kept or read: listed too large.
|
|
1027
|
+
...(change.patchOmitted === true && !patch ? { patchOmitted: true, reason: PATCH_TOO_LARGE } : {}),
|
|
1028
|
+
patch,
|
|
1029
|
+
};
|
|
1030
|
+
})
|
|
1031
|
+
.sort((left, right) => {
|
|
1032
|
+
if (left.path === right.path) return 0;
|
|
1033
|
+
return left.path < right.path ? -1 : 1;
|
|
1034
|
+
});
|
|
1035
|
+
if (clean(target)) {
|
|
1036
|
+
const key = changePathKey(target);
|
|
1037
|
+
const row = rows.find((entry) => changePathKey(entry.id) === key || changePathKey(entry.path) === key);
|
|
1038
|
+
if (row?.patchOmitted === true && !row.patch) return { ...filePatchReply(row.path, ''), ...patchOmittedFile({}) };
|
|
1039
|
+
return filePatchReply(row?.path || slashPath(target), row?.patch || '');
|
|
1040
|
+
}
|
|
1041
|
+
// The merged response stays bounded: a file past the per-file cap or the
|
|
1042
|
+
// total left is listed with its counts but without its diff.
|
|
1043
|
+
const budget = createReviewPatchBudget(MERGED_PATCH_MAX_BYTES - Buffer.byteLength(basePatch, 'utf8'));
|
|
681
1044
|
const files = [];
|
|
682
1045
|
const patches = [];
|
|
683
|
-
for (const { patch, ...file } of
|
|
684
|
-
|
|
685
|
-
if (patch
|
|
686
|
-
budget -= size;
|
|
687
|
-
patches.push(patch);
|
|
688
|
-
} else if (patch) {
|
|
689
|
-
truncated = true;
|
|
690
|
-
files.push({ ...file, patchOmitted: true });
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
1046
|
+
for (const { patch, ...file } of rows) {
|
|
1047
|
+
if (patch && budget.admit(Buffer.byteLength(patch, 'utf8'))) patches.push(patch);
|
|
1048
|
+
else if (patch) Object.assign(file, patchOmittedFile(file));
|
|
693
1049
|
files.push(file);
|
|
694
1050
|
}
|
|
1051
|
+
// A baseline first seen after an older recorded edit (`late`) was not the
|
|
1052
|
+
// session's first state: even with no row it cannot vouch for the set.
|
|
1053
|
+
// Baselines the disk refused (`unsaved`) are known only to this process.
|
|
1054
|
+
const partial =
|
|
1055
|
+
baselines?.incomplete === true ||
|
|
1056
|
+
baselines?.unsaved === true ||
|
|
1057
|
+
unvouched.size > 0 ||
|
|
1058
|
+
legacy.late.size > 0 ||
|
|
1059
|
+
missing.length > 0 ||
|
|
1060
|
+
changes.some((change) => change.change.partial === true);
|
|
1061
|
+
const vouched = (gitBacked || source === 'empty' || Boolean(baselines)) && !partial;
|
|
1062
|
+
const all = [...baseFiles, ...files];
|
|
1063
|
+
// An unavailable Git baseline with nothing else to list stays unavailable.
|
|
1064
|
+
if (all.length === 0 && !vouched && kept.supported === false) return { ...kept, reason: unavailableReason };
|
|
1065
|
+
const patch = [basePatch, ...patches].filter(Boolean).join(basePatch.endsWith('\n') || !basePatch ? '' : '\n');
|
|
695
1066
|
return {
|
|
696
1067
|
...kept,
|
|
697
1068
|
supported: true,
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1069
|
+
// Emptiness is claimed only when it can be vouched for.
|
|
1070
|
+
authoritative: all.length > 0 || vouched,
|
|
1071
|
+
...(vouched ? { complete: true } : {}),
|
|
1072
|
+
...(partial ? { partial: true } : {}),
|
|
1073
|
+
...(baselines?.unsaved === true ? { baselinesUnsaved: true } : {}),
|
|
1074
|
+
files: all,
|
|
1075
|
+
patch,
|
|
1076
|
+
patchTruncated: gitBacked && kept.patchTruncated === true,
|
|
1077
|
+
...(reviewDiffTooLarge(all, patch) ? { diffTooLarge: true } : {}),
|
|
1078
|
+
snapshotKind: kept.snapshotKind || 'session',
|
|
704
1079
|
};
|
|
705
1080
|
}
|
|
706
1081
|
|
|
707
1082
|
// ── Apply ───────────────────────────────────────────────────────────────────
|
|
708
1083
|
|
|
709
|
-
/** Every path a stored patch touches must be exactly the recorded file's.
|
|
1084
|
+
/** Every path a stored patch touches must be exactly the recorded file's.
|
|
1085
|
+
* Several sections are the file's own segments in write order: each starts
|
|
1086
|
+
* where the one before ended (a foreign edit in between stays out of both),
|
|
1087
|
+
* from the recorded old path to its current one. Resolves to the sections. */
|
|
710
1088
|
function assertPatchMatchesFile(file, buffer) {
|
|
711
1089
|
const sections = splitPatchSections(buffer);
|
|
712
|
-
if (sections.length
|
|
1090
|
+
if (sections.length === 0) throw new Error(`${file.path} has a malformed recorded patch`);
|
|
1091
|
+
const mismatch = () => new Error(`${file.path} has a recorded patch for a different path`);
|
|
1092
|
+
if (sections.length > 1) {
|
|
1093
|
+
let at = file.oldPath || file.path;
|
|
1094
|
+
for (const { oldPath, newPath } of sections) {
|
|
1095
|
+
if ([oldPath, newPath].some((path) => path !== null && !safeChangePath(path))) throw mismatch();
|
|
1096
|
+
if ((oldPath ?? newPath) !== at) throw mismatch();
|
|
1097
|
+
at = newPath ?? oldPath;
|
|
1098
|
+
}
|
|
1099
|
+
if (at !== file.path) throw mismatch();
|
|
1100
|
+
return sections;
|
|
1101
|
+
}
|
|
713
1102
|
const { oldPath, newPath } = sections[0];
|
|
714
1103
|
const expectedOld = file.status === 'A' ? null : file.oldPath || file.path;
|
|
715
1104
|
const expectedNew = file.status === 'D' ? null : file.path;
|
|
@@ -717,10 +1106,9 @@ function assertPatchMatchesFile(file, buffer) {
|
|
|
717
1106
|
[oldPath, expectedOld],
|
|
718
1107
|
[newPath, expectedNew],
|
|
719
1108
|
]) {
|
|
720
|
-
if (expected === null ? actual !== null : actual !== expected || !safeChangePath(actual))
|
|
721
|
-
throw new Error(`${file.path} has a recorded patch for a different path`);
|
|
722
|
-
}
|
|
1109
|
+
if (expected === null ? actual !== null : actual !== expected || !safeChangePath(actual)) throw mismatch();
|
|
723
1110
|
}
|
|
1111
|
+
return sections;
|
|
724
1112
|
}
|
|
725
1113
|
|
|
726
1114
|
/** The recorded root must still be the directory (and, for a Git record, the
|
|
@@ -761,13 +1149,21 @@ export async function applyTurnChangeFiles(record, files, { reverse }) {
|
|
|
761
1149
|
}
|
|
762
1150
|
const buffer = changePatchBuffer(file);
|
|
763
1151
|
if (!buffer?.length) throw new Error(`${file.path} has no recorded patch`);
|
|
764
|
-
assertPatchMatchesFile(file, buffer);
|
|
765
|
-
|
|
1152
|
+
const sections = assertPatchMatchesFile(file, buffer);
|
|
1153
|
+
// `git apply` takes a file's sections in input order, each on the result
|
|
1154
|
+
// of the one before: oldest first forward, newest first for an Undo.
|
|
1155
|
+
return Buffer.concat(
|
|
1156
|
+
(reverse ? [...sections].reverse() : sections).map(({ patch }) =>
|
|
1157
|
+
patch.at(-1) === 0x0a ? patch : Buffer.concat([patch, Buffer.from('\n')])
|
|
1158
|
+
)
|
|
1159
|
+
);
|
|
766
1160
|
});
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
|
|
1161
|
+
// An older Git record (worktree diff) replays with the repository's own
|
|
1162
|
+
// line-ending conversion, the semantics the shadow snapshot hashed with. A
|
|
1163
|
+
// record of the session's own tool bytes (`counts: 'own'`, any kind) holds
|
|
1164
|
+
// raw bytes, so nothing may convert them.
|
|
1165
|
+
const rawBytes = record.kind !== 'git' || record.counts === 'own';
|
|
1166
|
+
const conversion = rawBytes ? ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf'] : [];
|
|
771
1167
|
const args = [
|
|
772
1168
|
...conversion,
|
|
773
1169
|
'-c',
|