mixdog 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +18 -5
- package/src/defaults/skills/visualize/SKILL.md +160 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/help.mjs +2 -0
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +19 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- 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/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +33 -6
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -130
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +125 -21
- package/src/runtime/shared/provider-api-key.mjs +33 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/tool-surface.mjs +7 -0
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +90 -0
- package/src/session-runtime/boot/config.mjs +7 -2
- package/src/session-runtime/boot/facade.mjs +21 -0
- package/src/session-runtime/boot/infrastructure.mjs +30 -18
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +68 -16
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +18 -3
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +45 -7
- package/src/session-runtime/resource-mcp-api.mjs +121 -84
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +23 -5
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +15 -4
- package/src/standalone/daemon.mjs +179 -8
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +21 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +37 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +839 -207
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +9 -0
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +27 -3
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +56 -10
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -4,6 +4,8 @@ import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
|
4
4
|
import {
|
|
5
5
|
_resetTurnWorktreeSnapshotsForTest,
|
|
6
6
|
createTurnWorktreeSnapshot,
|
|
7
|
+
freezeTurnWorktreeChanges,
|
|
8
|
+
noteTurnWorktreeChange,
|
|
7
9
|
refreshTurnWorktreeSnapshot,
|
|
8
10
|
recordTurnWorktreeFileChange,
|
|
9
11
|
resumeTurnWorktreeSnapshot,
|
|
@@ -11,7 +13,27 @@ import {
|
|
|
11
13
|
revertTurnWorktreePaths,
|
|
12
14
|
revertTurnWorktreeSnapshot,
|
|
13
15
|
} from './turn-worktree-snapshot.mjs';
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
loadSessionSnapshotRecords,
|
|
18
|
+
loadTurnChangeRecord,
|
|
19
|
+
loadTurnChangeRecords,
|
|
20
|
+
loadTurnSnapshotRecord,
|
|
21
|
+
saveTurnSnapshotRecord,
|
|
22
|
+
updateTurnChangeRecord,
|
|
23
|
+
upsertTurnChangeRecord,
|
|
24
|
+
} from './turn-snapshot-store.mjs';
|
|
25
|
+
import {
|
|
26
|
+
applyTurnChangeFiles,
|
|
27
|
+
changePathKey,
|
|
28
|
+
findChangeFile,
|
|
29
|
+
measureLegacyChangeFile,
|
|
30
|
+
mergeRecordedSessionChanges,
|
|
31
|
+
namedChangePatch,
|
|
32
|
+
storedChangeFile,
|
|
33
|
+
trackedChangeFile,
|
|
34
|
+
turnChangesReply,
|
|
35
|
+
unknownStateChangeFile,
|
|
36
|
+
} from './turn-changes.mjs';
|
|
15
37
|
import { boundReviewPatch } from './review-diff.mjs';
|
|
16
38
|
import { PATCH_EDIT_MAX_BYTES } from './bounded-file-read.mjs';
|
|
17
39
|
import { cleanString as clean } from './clean.mjs';
|
|
@@ -39,6 +61,14 @@ const MAX_AGENT_REVIEW_BYTES_PER_TURN = 4 * 1024 * 1024;
|
|
|
39
61
|
const _turnsBySession = new Map();
|
|
40
62
|
const _diffTrackersBySession = new Map();
|
|
41
63
|
let _agentTurnSeq = 0;
|
|
64
|
+
// `${sessionId}\0${checkpointId}` → seals in flight. A sealing turn's change
|
|
65
|
+
// record is `pending` and its session refuses Undo/Reapply until it lands.
|
|
66
|
+
const _pendingFreezes = new Map();
|
|
67
|
+
// Per-session Undo/Reapply chain.
|
|
68
|
+
const _changeMutations = new Map();
|
|
69
|
+
// `${sessionId}\0${checkpointId}` → path keys whose worktree state is unknown
|
|
70
|
+
// and whose record could not even be marked so (the store refused every write).
|
|
71
|
+
const _unknownChangeStates = new Map();
|
|
42
72
|
|
|
43
73
|
function trimTurnCache() {
|
|
44
74
|
while (_turnsBySession.size > TURN_CACHE_MAX) {
|
|
@@ -165,9 +195,16 @@ function createDiffTracker(meta = {}) {
|
|
|
165
195
|
ownerGeneration: Number(meta.ownerGeneration) || 0,
|
|
166
196
|
checkpointId: clean(meta.checkpointId),
|
|
167
197
|
worktreeRequest: clean(meta.worktreeRequest),
|
|
198
|
+
// The directory the turn ran in: a non-Git record's patch root.
|
|
199
|
+
worktreeRoot: clean(meta.worktreeRoot),
|
|
168
200
|
worktreeContended: false,
|
|
169
201
|
valid: true,
|
|
170
202
|
sealed: false,
|
|
203
|
+
// Bumped when a completion wake reopens the sealed turn, so a seal that
|
|
204
|
+
// started before the reopen cannot mark the reopened turn sealed.
|
|
205
|
+
sealEpoch: 0,
|
|
206
|
+
// The seal (refresh, record, freeze) in flight, if any.
|
|
207
|
+
freezing: null,
|
|
171
208
|
revision: 0,
|
|
172
209
|
baselineByPath: new Map(),
|
|
173
210
|
currentByPath: new Map(),
|
|
@@ -417,6 +454,8 @@ async function revertTrackedPairs(tracker, worktree, pairs) {
|
|
|
417
454
|
tracker.revision += 1;
|
|
418
455
|
refreshUnifiedDiff(tracker);
|
|
419
456
|
if (!tracker.unifiedDiff) releaseDiffTrackerContent(tracker);
|
|
457
|
+
// The absolute paths now back at their turn-start bytes.
|
|
458
|
+
return [...new Set(validated.flatMap(({ beforePath, afterPath }) => [beforePath, afterPath]))];
|
|
420
459
|
}
|
|
421
460
|
|
|
422
461
|
/** A path's first observed state is its baseline; later changes never replace it. */
|
|
@@ -435,7 +474,16 @@ export function recordTurnDiffChanges(sessionId, changes = []) {
|
|
|
435
474
|
if (!id) return '';
|
|
436
475
|
const tracker = _diffTrackersBySession.get(id) || resetDiffTracker(id);
|
|
437
476
|
if (!tracker?.valid || tracker.sealed) return tracker?.unifiedDiff || '';
|
|
438
|
-
const
|
|
477
|
+
const snapshot = (tracker.ownerSessionId ? _diffTrackersBySession.get(tracker.ownerSessionId) : tracker)
|
|
478
|
+
?.worktreeSnapshot;
|
|
479
|
+
foldTrackedChanges(tracker, Array.isArray(changes) ? changes : [], snapshot);
|
|
480
|
+
return tracker.unifiedDiff;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Fold exact before/after states into `tracker`. `snapshot` — the owning
|
|
484
|
+
* Lead turn's Git baseline, when tools are writing — learns each written
|
|
485
|
+
* path's first-before bytes. */
|
|
486
|
+
function foldTrackedChanges(tracker, list, snapshot) {
|
|
439
487
|
// The content ceiling is enforced BEFORE anything is stored: an over-budget
|
|
440
488
|
// batch invalidates the tracker up front instead of retaining its buffers.
|
|
441
489
|
const incomingBytes = list.reduce(
|
|
@@ -456,8 +504,6 @@ export function recordTurnDiffChanges(sessionId, changes = []) {
|
|
|
456
504
|
// batch only hands the worktree baseline a bounded value.
|
|
457
505
|
const before = storeContent ? contentBuffer(raw?.before) : boundedBaselineContent(raw?.before);
|
|
458
506
|
const destinationPath = clean(raw?.newPath);
|
|
459
|
-
const snapshot = (tracker.ownerSessionId ? _diffTrackersBySession.get(tracker.ownerSessionId) : tracker)
|
|
460
|
-
?.worktreeSnapshot;
|
|
461
507
|
recordTurnWorktreeFileChange(snapshot, sourcePath, before);
|
|
462
508
|
if (destinationPath) recordTurnWorktreeFileChange(snapshot, destinationPath, null);
|
|
463
509
|
// WHICH files this session wrote has to survive releasing their content —
|
|
@@ -522,7 +568,6 @@ export function recordTurnDiffChanges(sessionId, changes = []) {
|
|
|
522
568
|
releaseDiffTrackerContent(tracker);
|
|
523
569
|
}
|
|
524
570
|
refreshUnifiedDiff(tracker);
|
|
525
|
-
return tracker.unifiedDiff;
|
|
526
571
|
}
|
|
527
572
|
|
|
528
573
|
function trimAgentReviews(turn) {
|
|
@@ -569,18 +614,59 @@ function continueTurnSnapshot(ownerSessionId) {
|
|
|
569
614
|
_turnsBySession.delete(ownerSessionId);
|
|
570
615
|
_turnsBySession.set(ownerSessionId, turn);
|
|
571
616
|
tracker.sealed = false;
|
|
617
|
+
tracker.sealEpoch += 1;
|
|
572
618
|
markWorktreeContention(ownerSessionId, tracker);
|
|
573
619
|
return true;
|
|
574
620
|
}
|
|
575
621
|
|
|
576
622
|
/** Start a new user turn and invalidate the prior turn's child review group.
|
|
577
|
-
* `continueTurn` (a completion wake) reopens the current turn instead.
|
|
623
|
+
* `continueTurn` (a completion wake) reopens the current turn instead.
|
|
624
|
+
* Resolves to the turn's `{ generation, checkpointId }`: completing with it
|
|
625
|
+
* can only ever seal this turn, never a newer one. */
|
|
578
626
|
export async function beginTurnSnapshot(worktree, sessionId, options = {}) {
|
|
579
627
|
const ownerSessionId = clean(sessionId);
|
|
580
|
-
if (DISABLED || !ownerSessionId) return;
|
|
581
|
-
|
|
628
|
+
if (DISABLED || !ownerSessionId) return undefined;
|
|
629
|
+
// Turn start and Undo/Reapply share the per-session lock: a start waits for
|
|
630
|
+
// an in-flight mutation, and a mutation queued after this start re-checks
|
|
631
|
+
// under the lock and finds the turn running.
|
|
632
|
+
// A late-worker re-seal still queued for the previous turn runs first, in
|
|
633
|
+
// this same step: the worker's edit stays in that turn's record and nothing
|
|
634
|
+
// the new turn writes can reach it. (A queued re-seal implies a chain.)
|
|
635
|
+
const { token, tracker } = _changeMutations.has(ownerSessionId)
|
|
636
|
+
? await serializeChangeMutation(ownerSessionId, async () => {
|
|
637
|
+
const previous = _diffTrackersBySession.get(ownerSessionId);
|
|
638
|
+
if (previous?.resealQueued) await runQueuedReseal(ownerSessionId, previous).catch(() => {});
|
|
639
|
+
return openTurnSnapshot(worktree, ownerSessionId, options);
|
|
640
|
+
})
|
|
641
|
+
: openTurnSnapshot(worktree, ownerSessionId, options);
|
|
642
|
+
if (!tracker) return token;
|
|
643
|
+
try {
|
|
644
|
+
const snapshot = await createTurnWorktreeSnapshot(worktree);
|
|
645
|
+
if (_diffTrackersBySession.get(ownerSessionId) === tracker) {
|
|
646
|
+
tracker.worktreeSnapshot = snapshot;
|
|
647
|
+
if (snapshot?.root) markWorktreeContention(ownerSessionId, tracker);
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
// Git is optional. Exact apply_patch tracking remains the fallback.
|
|
651
|
+
}
|
|
652
|
+
return token;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** The synchronous half of a turn start: reopen (completion wake) or install
|
|
656
|
+
* the turn's tracker. `tracker` is set only for a new turn, whose Git
|
|
657
|
+
* baseline the caller captures next. */
|
|
658
|
+
function openTurnSnapshot(worktree, ownerSessionId, options) {
|
|
659
|
+
if (options?.continueTurn === true && continueTurnSnapshot(ownerSessionId)) {
|
|
660
|
+
const reopened = _diffTrackersBySession.get(ownerSessionId);
|
|
661
|
+
return { token: { generation: reopened.ownerGeneration, checkpointId: reopened.checkpointId }, tracker: null };
|
|
662
|
+
}
|
|
582
663
|
const generation = (_turnsBySession.get(ownerSessionId)?.generation || 0) + 1;
|
|
583
664
|
const checkpointId = clean(options?.checkpointId);
|
|
665
|
+
// A turn replaced before it sealed (an abort whose delayed cleanup has not
|
|
666
|
+
// run yet) still keeps its edits as a record. Its capture is queued on the
|
|
667
|
+
// worktree lock ahead of this turn's baseline.
|
|
668
|
+
const prior = _diffTrackersBySession.get(ownerSessionId);
|
|
669
|
+
if (prior && prior.ownerGeneration > 0 && !prior.sealed) void sealTurn(ownerSessionId, prior).catch(() => {});
|
|
584
670
|
for (const [trackedSessionId, tracker] of _diffTrackersBySession) {
|
|
585
671
|
if (tracker.ownerSessionId === ownerSessionId) _diffTrackersBySession.delete(trackedSessionId);
|
|
586
672
|
}
|
|
@@ -595,19 +681,19 @@ export async function beginTurnSnapshot(worktree, sessionId, options = {}) {
|
|
|
595
681
|
ownerGeneration: generation,
|
|
596
682
|
checkpointId,
|
|
597
683
|
worktreeRequest,
|
|
684
|
+
worktreeRoot: resolve(clean(worktree) || '.'),
|
|
598
685
|
});
|
|
599
686
|
trimTurnCache();
|
|
600
|
-
if (
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
687
|
+
if (tracker) markWorktreeContention(ownerSessionId, tracker);
|
|
688
|
+
_turnOpenedHookForTest?.(ownerSessionId, checkpointId);
|
|
689
|
+
return { token: { generation, checkpointId }, tracker };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Test seam: runs synchronously the instant a new turn's tracker is installed,
|
|
693
|
+
// i.e. the earliest moment that turn could touch the worktree.
|
|
694
|
+
let _turnOpenedHookForTest = null;
|
|
695
|
+
export function _setTurnOpenedHookForTest(hook) {
|
|
696
|
+
_turnOpenedHookForTest = typeof hook === 'function' ? hook : null;
|
|
611
697
|
}
|
|
612
698
|
|
|
613
699
|
/** Bind one worker execution to the Lead turn that launched it. */
|
|
@@ -673,6 +759,11 @@ export function completeAgentTurnReview(handle, patches = []) {
|
|
|
673
759
|
for (const path of tracker.ownedPaths) ownerTracker.ownedPaths.add(path);
|
|
674
760
|
}
|
|
675
761
|
trimAgentReviews(turn);
|
|
762
|
+
// A worker that finishes after its Lead turn sealed re-seals that turn's
|
|
763
|
+
// change record (its edits are part of the turn's net worktree diff).
|
|
764
|
+
if (ownerTracker && ownerTracker !== tracker && ownerTracker.ownerGeneration === handle.generation) {
|
|
765
|
+
resealTurn(clean(handle.ownerSessionId), ownerTracker);
|
|
766
|
+
}
|
|
676
767
|
return true;
|
|
677
768
|
} finally {
|
|
678
769
|
if (matchingTracker && _diffTrackersBySession.get(key) === tracker) {
|
|
@@ -694,7 +785,7 @@ async function persistTurnSnapshotRecord(ownerSessionId, tracker) {
|
|
|
694
785
|
if (owned.length === 0) return;
|
|
695
786
|
try {
|
|
696
787
|
await saveTurnSnapshotRecord(ownerSessionId, {
|
|
697
|
-
generation: _turnsBySession.get(ownerSessionId)?.generation || 0,
|
|
788
|
+
generation: tracker.ownerGeneration || _turnsBySession.get(ownerSessionId)?.generation || 0,
|
|
698
789
|
checkpointId: tracker.checkpointId,
|
|
699
790
|
root: snapshot.root,
|
|
700
791
|
baselineTree: snapshot.baselineTree,
|
|
@@ -707,39 +798,211 @@ async function persistTurnSnapshotRecord(ownerSessionId, tracker) {
|
|
|
707
798
|
}
|
|
708
799
|
}
|
|
709
800
|
|
|
710
|
-
/**
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
if (!tracker) return
|
|
801
|
+
/** Workers attributed to the tracker's own turn (identities only: their
|
|
802
|
+
* patches stay review attribution and never join the net diff). */
|
|
803
|
+
function attributedAgents(ownerSessionId, tracker) {
|
|
804
|
+
const turn = _turnsBySession.get(ownerSessionId);
|
|
805
|
+
if (!turn || turn.generation !== tracker.ownerGeneration) return [];
|
|
806
|
+
return [...turn.agents.values()].map((review) => ({
|
|
807
|
+
sessionId: review.sessionId,
|
|
808
|
+
agent: review.agent || null,
|
|
809
|
+
tag: review.tag || null,
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** Path of a tracked entry relative to `root`, or '' outside it. */
|
|
814
|
+
function trackedRelativePath(root, entry) {
|
|
815
|
+
const value = clean(entry?.path) || clean(entry?.displayPath);
|
|
816
|
+
if (!value) return '';
|
|
817
|
+
const rel = relative(root, isAbsolute(value) ? resolve(value) : resolve(root, value));
|
|
818
|
+
if (!rel || isAbsolute(rel) || rel.split(/[\\/]+/).includes('..')) return '';
|
|
819
|
+
return rel.replace(/\\/g, '/');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** A record file for an edit the worktree root cannot replay: outside it, or
|
|
823
|
+
* too large/binary to capture. Display-only (`viewPatch`), never undone. */
|
|
824
|
+
function untrackableRecordFile(pair, { before, after, oldPath, newPath, omitted }) {
|
|
825
|
+
let status = 'M';
|
|
826
|
+
if (before === null) status = 'A';
|
|
827
|
+
else if (after === null) status = 'D';
|
|
828
|
+
else if (oldPath !== newPath) status = 'R';
|
|
829
|
+
const shownPath =
|
|
830
|
+
(status === 'D' ? oldPath : newPath) || displayPath(pair.after?.displayPath || pair.before?.displayPath);
|
|
831
|
+
const named = omitted ? null : namedChangePatch({ before, after, oldName: shownPath, newName: shownPath });
|
|
832
|
+
return {
|
|
833
|
+
path: shownPath,
|
|
834
|
+
...(status === 'R' && oldPath ? { oldPath } : {}),
|
|
835
|
+
status,
|
|
836
|
+
additions: named?.additions || 0,
|
|
837
|
+
deletions: named?.deletions || 0,
|
|
838
|
+
...(named?.patch ? { viewPatch: named.patch } : {}),
|
|
839
|
+
...(omitted ? { binary: true } : {}),
|
|
840
|
+
revertable: false,
|
|
841
|
+
reason: omitted ? 'binary/large file content was not captured' : 'file is outside the recorded root',
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** The exact before/after pair of a tracked entry, or null when it changed nothing. */
|
|
846
|
+
function recordedPair(pair, root) {
|
|
847
|
+
const before = pair.before?.content ?? null;
|
|
848
|
+
const after = pair.after?.content ?? null;
|
|
849
|
+
if (before === null && after === null) return null;
|
|
850
|
+
const oldPath = trackedRelativePath(root, pair.before || pair.after);
|
|
851
|
+
const newPath = trackedRelativePath(root, pair.after || pair.before);
|
|
852
|
+
if (oldPath === newPath && sameContent(before, after)) return null;
|
|
853
|
+
return { before, after, oldPath, newPath, omitted: [before, after].some(isOmittedContent) };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** Record files of a non-Git turn, from the exact apply_patch buffers. */
|
|
857
|
+
function trackedRecordFiles(tracker, root) {
|
|
858
|
+
if (!tracker.valid) {
|
|
859
|
+
return [...ownedPathsUnder(root, tracker.ownedPaths)].map((path) => ({
|
|
860
|
+
path,
|
|
861
|
+
status: 'M',
|
|
862
|
+
additions: 0,
|
|
863
|
+
deletions: 0,
|
|
864
|
+
revertable: false,
|
|
865
|
+
reason: 'file content exceeded the undo tracking limit',
|
|
866
|
+
}));
|
|
867
|
+
}
|
|
868
|
+
const files = [];
|
|
869
|
+
for (const pair of trackedPairs(tracker)) {
|
|
870
|
+
const edit = recordedPair(pair, root);
|
|
871
|
+
if (!edit) continue;
|
|
872
|
+
if (edit.omitted || !edit.oldPath || !edit.newPath) {
|
|
873
|
+
files.push(untrackableRecordFile(pair, edit));
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
files.push(
|
|
877
|
+
trackedChangeFile({ oldPath: edit.oldPath, newPath: edit.newPath, before: edit.before, after: edit.after })
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return files;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Edits of a Git turn that lie outside its worktree root: the baseline diff
|
|
884
|
+
* never sees them, so they are recorded view-only beside the Git files. */
|
|
885
|
+
function outsideRecordFiles(tracker, root) {
|
|
886
|
+
if (!tracker.valid) return [];
|
|
887
|
+
const files = [];
|
|
888
|
+
for (const pair of trackedPairs(tracker)) {
|
|
889
|
+
const edit = recordedPair(pair, root);
|
|
890
|
+
if (edit && (!edit.oldPath || !edit.newPath)) {
|
|
891
|
+
files.push(untrackableRecordFile(pair, edit));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return files;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Freeze the turn's net diff into its durable change record. A Git turn
|
|
898
|
+
* records the baseline→end worktree diff (workers' edits included, scoped to
|
|
899
|
+
* session-owned paths when the worktree is shared); a non-Git turn records
|
|
900
|
+
* the Lead's exact apply_patch pairs. */
|
|
901
|
+
async function freezeTurnChanges(ownerSessionId, tracker, agents) {
|
|
902
|
+
const checkpointId = clean(tracker.checkpointId);
|
|
903
|
+
if (!checkpointId) return null;
|
|
904
|
+
let frozen = null;
|
|
715
905
|
if (tracker.worktreeSnapshot) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
906
|
+
const scope = worktreeSnapshotUsable(tracker)
|
|
907
|
+
? null
|
|
908
|
+
: [...ownedPathsUnder(tracker.worktreeSnapshot.root, tracker.ownedPaths)];
|
|
909
|
+
const result = await freezeTurnWorktreeChanges(tracker.worktreeSnapshot, scope);
|
|
910
|
+
if (result) {
|
|
911
|
+
frozen = { kind: 'git', ...result, outside: outsideRecordFiles(tracker, tracker.worktreeSnapshot.root) };
|
|
912
|
+
}
|
|
913
|
+
} else if (tracker.worktreeRoot) {
|
|
914
|
+
frozen = { kind: 'tracked', root: tracker.worktreeRoot, files: trackedRecordFiles(tracker, tracker.worktreeRoot) };
|
|
719
915
|
}
|
|
720
|
-
|
|
721
|
-
await
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
916
|
+
if (!frozen) return null;
|
|
917
|
+
const record = await upsertTurnChangeRecord(ownerSessionId, {
|
|
918
|
+
checkpointId,
|
|
919
|
+
kind: frozen.kind,
|
|
920
|
+
root: frozen.root,
|
|
921
|
+
agents,
|
|
922
|
+
files: frozen.files.map(storedChangeFile),
|
|
923
|
+
outside: (frozen.outside || []).map(storedChangeFile),
|
|
924
|
+
});
|
|
925
|
+
// A fresh freeze measured the disk itself; nothing about it is unknown.
|
|
926
|
+
_unknownChangeStates.delete(pendingFreezeKey(ownerSessionId, checkpointId));
|
|
927
|
+
return record;
|
|
728
928
|
}
|
|
729
929
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
930
|
+
const pendingFreezeKey = (ownerSessionId, checkpointId) => `${ownerSessionId}\0${clean(checkpointId)}`;
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Seal a Lead turn: capture the worktree one last time, persist the review
|
|
934
|
+
* baseline and freeze the change record. Single-flight per tracker; a seal
|
|
935
|
+
* requested while another runs waits for it and then seals again only if a
|
|
936
|
+
* completion wake reopened the turn meanwhile. `reseal` (a late worker) only
|
|
937
|
+
* re-freezes a turn that is still sealed.
|
|
938
|
+
*/
|
|
939
|
+
function sealTurn(ownerSessionId, tracker, { reseal = false } = {}) {
|
|
940
|
+
if (tracker.freezing) {
|
|
941
|
+
if (reseal) return tracker.freezing;
|
|
942
|
+
return tracker.freezing.then(() => (tracker.sealed ? undefined : sealTurn(ownerSessionId, tracker)));
|
|
740
943
|
}
|
|
741
|
-
|
|
742
|
-
|
|
944
|
+
const epoch = tracker.sealEpoch;
|
|
945
|
+
const agents = attributedAgents(ownerSessionId, tracker);
|
|
946
|
+
const key = pendingFreezeKey(ownerSessionId, tracker.checkpointId);
|
|
947
|
+
_pendingFreezes.set(key, (_pendingFreezes.get(key) || 0) + 1);
|
|
948
|
+
const run = (async () => {
|
|
949
|
+
if (tracker.worktreeSnapshot) {
|
|
950
|
+
try {
|
|
951
|
+
await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot, { force: true });
|
|
952
|
+
} catch {
|
|
953
|
+
// Best-effort refresh: seal with the last captured worktree state.
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (tracker.sealEpoch !== epoch || (reseal && !tracker.sealed)) return;
|
|
957
|
+
tracker.sealed = true;
|
|
958
|
+
await persistTurnSnapshotRecord(ownerSessionId, tracker);
|
|
959
|
+
try {
|
|
960
|
+
await freezeTurnChanges(ownerSessionId, tracker, agents);
|
|
961
|
+
} catch {
|
|
962
|
+
/* the change record adds Undo history; it never fails a turn */
|
|
963
|
+
}
|
|
964
|
+
// Only a usable Git baseline can replace the exact apply_patch buffers. A
|
|
965
|
+
// contended worktree — and a worktree with no Git baseline at all — retains
|
|
966
|
+
// this session's bounded before/after content so Review can still undo its
|
|
967
|
+
// own files after the turn completes.
|
|
968
|
+
if (worktreeSnapshotUsable(tracker)) releaseDiffTrackerContent(tracker);
|
|
969
|
+
})().finally(() => {
|
|
970
|
+
if (tracker.freezing === run) tracker.freezing = null;
|
|
971
|
+
const count = (_pendingFreezes.get(key) || 1) - 1;
|
|
972
|
+
if (count > 0) _pendingFreezes.set(key, count);
|
|
973
|
+
else _pendingFreezes.delete(key);
|
|
974
|
+
});
|
|
975
|
+
tracker.freezing = run;
|
|
976
|
+
return run;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function resealTurn(ownerSessionId, tracker) {
|
|
980
|
+
if (!tracker.sealed || tracker.freezing || !clean(tracker.checkpointId) || tracker.resealQueued) return;
|
|
981
|
+
// On the session lock: an in-flight Undo/Reapply lands before the re-freeze
|
|
982
|
+
// reads the worktree. A turn start queued ahead of it runs it before opening
|
|
983
|
+
// (beginTurnSnapshot), so one queued re-seal covers every late worker.
|
|
984
|
+
tracker.resealQueued = true;
|
|
985
|
+
void serializeChangeMutation(ownerSessionId, () => runQueuedReseal(ownerSessionId, tracker)).catch(() => {});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Run `tracker`'s queued re-seal once, whichever lock step reaches it first. */
|
|
989
|
+
function runQueuedReseal(ownerSessionId, tracker) {
|
|
990
|
+
if (!tracker.resealQueued) return undefined;
|
|
991
|
+
tracker.resealQueued = false;
|
|
992
|
+
return sealTurn(ownerSessionId, tracker, { reseal: true });
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Freeze a completed (or cancelled) Lead turn while retaining only its
|
|
996
|
+
* bounded review data. `expected` — the `{ generation }` beginTurnSnapshot
|
|
997
|
+
* resolved to — makes a delayed call a no-op once a newer turn began. */
|
|
998
|
+
export async function completeTurnSnapshot(sessionId, expected = null) {
|
|
999
|
+
const ownerSessionId = clean(sessionId);
|
|
1000
|
+
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1001
|
+
if (!tracker) return false;
|
|
1002
|
+
const generation = Number(expected?.generation) || 0;
|
|
1003
|
+
if (generation && tracker.ownerGeneration !== generation) return false;
|
|
1004
|
+
await sealTurn(ownerSessionId, tracker);
|
|
1005
|
+
return true;
|
|
743
1006
|
}
|
|
744
1007
|
|
|
745
1008
|
/**
|
|
@@ -818,7 +1081,7 @@ function assertCheckpointMatches(expectedCheckpointId, actualCheckpointId) {
|
|
|
818
1081
|
}
|
|
819
1082
|
|
|
820
1083
|
/** Restore recorded paths — every one of them, or just the file the user picked. */
|
|
821
|
-
async function revertFromRecord(
|
|
1084
|
+
async function revertFromRecord(ownerSessionId, file, expectedCheckpointId) {
|
|
822
1085
|
const resumed = await resumeRecordedSnapshot(ownerSessionId);
|
|
823
1086
|
if (!resumed) return null;
|
|
824
1087
|
assertCheckpointMatches(expectedCheckpointId, resumed.record.checkpointId);
|
|
@@ -827,8 +1090,8 @@ async function revertFromRecord(worktree, ownerSessionId, file, expectedCheckpoi
|
|
|
827
1090
|
if (targets.length === 0) {
|
|
828
1091
|
throw new Error('turn review revert cannot attribute this file to the session');
|
|
829
1092
|
}
|
|
830
|
-
await revertTurnWorktreePaths(resumed.snapshot, targets);
|
|
831
|
-
return
|
|
1093
|
+
const paths = await revertTurnWorktreePaths(resumed.snapshot, targets);
|
|
1094
|
+
return { checkpointId: clean(resumed.record.checkpointId), paths };
|
|
832
1095
|
}
|
|
833
1096
|
|
|
834
1097
|
/** Return the authoritative Lead net diff plus attributed child reviews. */
|
|
@@ -846,7 +1109,9 @@ export async function getTurnReviewDiff(_worktree, sessionId, options = {}) {
|
|
|
846
1109
|
const refreshOptions = options.poll === 'background' ? { visible: options.summary !== true } : { force: true };
|
|
847
1110
|
try {
|
|
848
1111
|
await refreshTurnWorktreeSnapshot(isolatedWorktreeSnapshot, refreshOptions);
|
|
849
|
-
} catch {
|
|
1112
|
+
} catch {
|
|
1113
|
+
// Best-effort refresh: the review reads the last captured state.
|
|
1114
|
+
}
|
|
850
1115
|
}
|
|
851
1116
|
const snapshot = isolatedWorktreeSnapshot;
|
|
852
1117
|
// Without a usable baseline the review falls back to this session's own
|
|
@@ -895,6 +1160,39 @@ export async function getSessionReviewDiff(worktree, sessionId) {
|
|
|
895
1160
|
if (DISABLED) return { supported: false, files: [], patch: '' };
|
|
896
1161
|
const ownerSessionId = clean(sessionId);
|
|
897
1162
|
if (!ownerSessionId) return { supported: true, files: [], patch: '', authoritative: true };
|
|
1163
|
+
const review = await baselineSessionReviewDiff(worktree, ownerSessionId);
|
|
1164
|
+
// The same recorded changes the turn-changes cards list: a non-Git session
|
|
1165
|
+
// and files outside the worktree have no baseline above, but do have these.
|
|
1166
|
+
// A Git session whose baseline vouches for its files has nothing to add but
|
|
1167
|
+
// edits outside the root, so its records are read only through the
|
|
1168
|
+
// mtime-keyed cache.
|
|
1169
|
+
const records = await loadTurnChangeRecords(ownerSessionId).catch(() => []);
|
|
1170
|
+
const live = review.source === 'git' ? null : liveTrackedRecord(_diffTrackersBySession.get(ownerSessionId));
|
|
1171
|
+
return mergeRecordedSessionChanges(review, records, live);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const LIVE_RECORD_TTL_MS = 1000;
|
|
1175
|
+
const _liveRecords = new WeakMap();
|
|
1176
|
+
|
|
1177
|
+
/** The running non-Git turn as a record, so its edits join the net review
|
|
1178
|
+
* before the turn seals. Rebuilt at most once a second. */
|
|
1179
|
+
function liveTrackedRecord(tracker) {
|
|
1180
|
+
if (!tracker || tracker.worktreeSnapshot || !tracker.worktreeRoot || !clean(tracker.checkpointId)) return null;
|
|
1181
|
+
const cached = _liveRecords.get(tracker);
|
|
1182
|
+
if (cached && Date.now() - cached.at < LIVE_RECORD_TTL_MS && cached.checkpointId === tracker.checkpointId) {
|
|
1183
|
+
return cached.record;
|
|
1184
|
+
}
|
|
1185
|
+
const record = {
|
|
1186
|
+
checkpointId: clean(tracker.checkpointId),
|
|
1187
|
+
kind: 'tracked',
|
|
1188
|
+
root: tracker.worktreeRoot,
|
|
1189
|
+
files: trackedRecordFiles(tracker, tracker.worktreeRoot).map(storedChangeFile),
|
|
1190
|
+
};
|
|
1191
|
+
_liveRecords.set(tracker, { at: Date.now(), checkpointId: record.checkpointId, record });
|
|
1192
|
+
return record;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async function baselineSessionReviewDiff(worktree, ownerSessionId) {
|
|
898
1196
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
899
1197
|
const scopes = await loadSessionSnapshotRecords(ownerSessionId).catch(() => []);
|
|
900
1198
|
const stored = scopes.find((scope) => scopeMatchesWorktree(scope, worktree, tracker));
|
|
@@ -905,17 +1203,24 @@ export async function getSessionReviewDiff(worktree, sessionId) {
|
|
|
905
1203
|
if (tracker?.worktreeSnapshot && !tracker.sealed) {
|
|
906
1204
|
try {
|
|
907
1205
|
await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot);
|
|
908
|
-
} catch {
|
|
1206
|
+
} catch {
|
|
1207
|
+
// Best-effort refresh: the checkpoint diff uses the last captured state.
|
|
1208
|
+
}
|
|
909
1209
|
}
|
|
910
1210
|
const ownedPaths = new Set(stored?.toolFiles || []);
|
|
911
1211
|
for (const path of sessionAttributedPaths(root, tracker)) ownedPaths.add(path);
|
|
912
1212
|
if (ownedPaths.size === 0) {
|
|
1213
|
+
// Authoritatively empty only when the session left ownership metadata
|
|
1214
|
+
// behind (a stored scope or a live tracker); with neither, the retained
|
|
1215
|
+
// change records are the only memory of what it changed.
|
|
913
1216
|
return {
|
|
914
1217
|
supported: true,
|
|
1218
|
+
root,
|
|
915
1219
|
files: [],
|
|
916
1220
|
patch: '',
|
|
917
1221
|
snapshotKind: 'session',
|
|
918
1222
|
authoritative: true,
|
|
1223
|
+
source: stored || tracker ? 'empty' : 'absent',
|
|
919
1224
|
};
|
|
920
1225
|
}
|
|
921
1226
|
const baselineTree = stored?.baselineTree || tracker?.worktreeSnapshot?.baselineTree;
|
|
@@ -932,11 +1237,13 @@ export async function getSessionReviewDiff(worktree, sessionId) {
|
|
|
932
1237
|
if (snapshot) {
|
|
933
1238
|
return {
|
|
934
1239
|
supported: true,
|
|
1240
|
+
root,
|
|
935
1241
|
files: snapshot.files || [],
|
|
936
1242
|
patch: snapshot.patch || '',
|
|
937
1243
|
patchTruncated: snapshot.patchTruncated === true,
|
|
938
1244
|
snapshotKind: 'session',
|
|
939
1245
|
authoritative: true,
|
|
1246
|
+
source: 'git',
|
|
940
1247
|
};
|
|
941
1248
|
}
|
|
942
1249
|
}
|
|
@@ -951,6 +1258,7 @@ export async function getSessionReviewDiff(worktree, sessionId) {
|
|
|
951
1258
|
patchTruncated: tracker.patchTruncated === true,
|
|
952
1259
|
snapshotKind: 'session-tool',
|
|
953
1260
|
authoritative: true,
|
|
1261
|
+
source: 'tool',
|
|
954
1262
|
};
|
|
955
1263
|
}
|
|
956
1264
|
return {
|
|
@@ -960,6 +1268,7 @@ export async function getSessionReviewDiff(worktree, sessionId) {
|
|
|
960
1268
|
snapshotKind: 'session',
|
|
961
1269
|
authoritative: false,
|
|
962
1270
|
reason: 'session baseline is unavailable',
|
|
1271
|
+
source: 'unavailable',
|
|
963
1272
|
};
|
|
964
1273
|
}
|
|
965
1274
|
|
|
@@ -970,59 +1279,359 @@ function unavailableTrackedRevertReason(tracker) {
|
|
|
970
1279
|
: 'turn review tracked file snapshot is unavailable';
|
|
971
1280
|
}
|
|
972
1281
|
|
|
973
|
-
/** Restore
|
|
974
|
-
|
|
975
|
-
|
|
1282
|
+
/** Restore the latest turn — every file it can attribute (`whole`) or the one
|
|
1283
|
+
* the user picked — to its turn-start state. Resolves to the restored turn's
|
|
1284
|
+
* checkpoint and the absolute paths actually restored: a contended worktree
|
|
1285
|
+
* restores only the Lead's own tracked files, never its workers'. */
|
|
1286
|
+
async function revertLatestTurnUnlocked(worktree, ownerSessionId, file, whole, expectedCheckpointId) {
|
|
976
1287
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
977
1288
|
if (tracker) assertCheckpointMatches(expectedCheckpointId, tracker.checkpointId);
|
|
1289
|
+
const restored = (paths) => ({ checkpointId: clean(tracker.checkpointId), paths });
|
|
1290
|
+
const recordFile = whole ? null : file;
|
|
978
1291
|
if (tracker && !worktreeSnapshotUsable(tracker)) {
|
|
979
1292
|
const pairs = trackedPairs(tracker);
|
|
980
1293
|
if (pairs.length > 0) {
|
|
981
|
-
|
|
982
|
-
if (!
|
|
983
|
-
|
|
984
|
-
|
|
1294
|
+
let selected = pairs;
|
|
1295
|
+
if (!whole) {
|
|
1296
|
+
const pair = pairs.find((entry) => pairMatchesFile(entry, worktree, file));
|
|
1297
|
+
if (!pair) throw new Error('turn review tracked file snapshot is unavailable');
|
|
1298
|
+
selected = [pair];
|
|
1299
|
+
}
|
|
1300
|
+
return restored(await revertTrackedPairs(tracker, worktree, selected));
|
|
985
1301
|
}
|
|
986
|
-
const recorded = await revertFromRecord(
|
|
1302
|
+
const recorded = await revertFromRecord(ownerSessionId, recordFile, expectedCheckpointId);
|
|
987
1303
|
if (recorded) return recorded;
|
|
988
1304
|
throw new Error(unavailableTrackedRevertReason(tracker));
|
|
989
1305
|
}
|
|
990
1306
|
if (tracker?.worktreeSnapshot) {
|
|
991
|
-
|
|
992
|
-
|
|
1307
|
+
return restored(
|
|
1308
|
+
whole
|
|
1309
|
+
? await revertTurnWorktreeSnapshot(tracker.worktreeSnapshot)
|
|
1310
|
+
: await revertTurnWorktreeFile(tracker.worktreeSnapshot, file)
|
|
1311
|
+
);
|
|
993
1312
|
}
|
|
994
|
-
const recorded = await revertFromRecord(
|
|
1313
|
+
const recorded = await revertFromRecord(ownerSessionId, recordFile, expectedCheckpointId);
|
|
995
1314
|
if (recorded) return recorded;
|
|
996
1315
|
throw new Error('turn worktree snapshot is unavailable');
|
|
997
1316
|
}
|
|
998
1317
|
|
|
1318
|
+
/** Record a composer restore in the turn's change record: a record file whose
|
|
1319
|
+
* every side (a rename's old path too) was restored is back at its
|
|
1320
|
+
* turn-start bytes, i.e. `reverted`; every other file keeps its state. */
|
|
1321
|
+
async function recordComposerRevert(ownerSessionId, { checkpointId, paths }) {
|
|
1322
|
+
if (!ownerSessionId || !checkpointId) return;
|
|
1323
|
+
const record = await loadTurnChangeRecord(ownerSessionId, checkpointId);
|
|
1324
|
+
if (!record) return;
|
|
1325
|
+
const restored = new Set(paths.map((path) => changePathKey(relative(record.root, path))));
|
|
1326
|
+
const keys = new Set(
|
|
1327
|
+
(record.files || [])
|
|
1328
|
+
.filter((entry) => [entry.path, entry.oldPath].filter(Boolean).every((side) => restored.has(changePathKey(side))))
|
|
1329
|
+
.map((entry) => changePathKey(entry.path))
|
|
1330
|
+
);
|
|
1331
|
+
if (keys.size === 0) return;
|
|
1332
|
+
try {
|
|
1333
|
+
await writeTurnChangeState(ownerSessionId, checkpointId, keys, 'reverted');
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
await markTurnChangeStateUnknown(ownerSessionId, checkpointId, keys);
|
|
1336
|
+
throw new Error(
|
|
1337
|
+
`the turn's files were restored, but its change record could not be updated (${error.message}); Undo/Reapply of those files is disabled`,
|
|
1338
|
+
{ cause: error }
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Composer Undo of the latest turn, on the per-session change lock shared
|
|
1344
|
+
* with historical Undo/Reapply, recorded in the turn's change record so the
|
|
1345
|
+
* historical view offers Reapply rather than a second Undo. */
|
|
1346
|
+
function revertLatestTurn(worktree, sessionId, file, whole, expectedCheckpointId) {
|
|
1347
|
+
const ownerSessionId = clean(sessionId);
|
|
1348
|
+
return serializeChangeMutation(ownerSessionId, async () => {
|
|
1349
|
+
// A seal still freezing would record the pre-restore disk after this.
|
|
1350
|
+
const freezing = _diffTrackersBySession.get(ownerSessionId)?.freezing;
|
|
1351
|
+
if (freezing) await freezing.catch(() => {});
|
|
1352
|
+
const restored = await revertLatestTurnUnlocked(worktree, ownerSessionId, file, whole, expectedCheckpointId);
|
|
1353
|
+
await recordComposerRevert(ownerSessionId, restored);
|
|
1354
|
+
return await getTurnReviewDiff(worktree, ownerSessionId);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** Restore one reviewed file to this turn's worktree baseline, never to HEAD. */
|
|
1359
|
+
export function revertTurnReviewFile(worktree, sessionId, file, expectedCheckpointId = '') {
|
|
1360
|
+
return revertLatestTurn(worktree, sessionId, file, false, expectedCheckpointId);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
999
1363
|
/** Restore every file in the reviewed turn to its turn-start state. */
|
|
1000
|
-
export
|
|
1364
|
+
export function revertTurnReview(worktree, sessionId, expectedCheckpointId = '') {
|
|
1365
|
+
return revertLatestTurn(worktree, sessionId, null, true, expectedCheckpointId);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ── Historical turn changes (per-turn Undo/Reapply) ─────────────────────────
|
|
1369
|
+
|
|
1370
|
+
/** A Lead turn of this session is running or still sealing. Only a tracker
|
|
1371
|
+
* beginTurnSnapshot opened counts: an orphan tool tracker is no turn. */
|
|
1372
|
+
function sessionTurnBusy(ownerSessionId) {
|
|
1373
|
+
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1374
|
+
if (tracker && tracker.ownerGeneration > 0 && (!tracker.sealed || tracker.freezing)) return true;
|
|
1375
|
+
for (const key of _pendingFreezes.keys()) {
|
|
1376
|
+
if (key.startsWith(`${ownerSessionId}\0`)) return true;
|
|
1377
|
+
}
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function turnChangesPending(ownerSessionId, checkpointId) {
|
|
1382
|
+
if (_pendingFreezes.has(pendingFreezeKey(ownerSessionId, checkpointId))) return true;
|
|
1383
|
+
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1384
|
+
return Boolean(tracker && tracker.ownerGeneration > 0 && !tracker.sealed && tracker.checkpointId === checkpointId);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function requiredCheckpointId(value) {
|
|
1388
|
+
const checkpointId = clean(value);
|
|
1389
|
+
if (!checkpointId) throw new TypeError('checkpointId is required');
|
|
1390
|
+
return checkpointId;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/** One turn's frozen changes by exact checkpoint id (TurnChangesReply). An
|
|
1394
|
+
* unknown checkpoint is `missing`; it never resolves to the latest turn. */
|
|
1395
|
+
export async function getTurnChanges(sessionId, options = {}) {
|
|
1001
1396
|
const ownerSessionId = clean(sessionId);
|
|
1397
|
+
const checkpointId = requiredCheckpointId(options?.checkpointId);
|
|
1398
|
+
let record = ownerSessionId
|
|
1399
|
+
? withUnknownChangeStates(ownerSessionId, await loadTurnChangeRecord(ownerSessionId, checkpointId))
|
|
1400
|
+
: null;
|
|
1401
|
+
// An old record's outside-root file kept no counts: measure it so the card
|
|
1402
|
+
// agrees with the session review and the island.
|
|
1403
|
+
if (record?.files?.length) {
|
|
1404
|
+
record = {
|
|
1405
|
+
...record,
|
|
1406
|
+
files: await Promise.all(record.files.map((file) => measureLegacyChangeFile(file, record.root))),
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
let status = 'missing';
|
|
1410
|
+
if (ownerSessionId && turnChangesPending(ownerSessionId, checkpointId)) status = 'pending';
|
|
1411
|
+
else if (record) status = record.files?.length || record.outside?.length ? 'ready' : 'none';
|
|
1412
|
+
return turnChangesReply({
|
|
1413
|
+
status,
|
|
1414
|
+
checkpointId,
|
|
1415
|
+
record,
|
|
1416
|
+
patch: options?.patch === true,
|
|
1417
|
+
file: clean(options?.file) || null,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function serializeChangeMutation(ownerSessionId, task) {
|
|
1422
|
+
const previous = _changeMutations.get(ownerSessionId) || Promise.resolve();
|
|
1423
|
+
const next = previous.then(task, task);
|
|
1424
|
+
const settled = next.then(
|
|
1425
|
+
() => undefined,
|
|
1426
|
+
() => undefined
|
|
1427
|
+
);
|
|
1428
|
+
_changeMutations.set(ownerSessionId, settled);
|
|
1429
|
+
settled.then(() => {
|
|
1430
|
+
if (_changeMutations.get(ownerSessionId) === settled) _changeMutations.delete(ownerSessionId);
|
|
1431
|
+
});
|
|
1432
|
+
return next;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const fileState = (file) => (file.state === 'reverted' ? 'reverted' : 'applied');
|
|
1436
|
+
|
|
1437
|
+
/** Persist `state` for the record files under `keys`, bumping the revision
|
|
1438
|
+
* when anything changed: the one state write of composer and historical Undo. */
|
|
1439
|
+
function writeTurnChangeState(ownerSessionId, checkpointId, keys, state) {
|
|
1440
|
+
return updateTurnChangeRecord(ownerSessionId, checkpointId, (current) => {
|
|
1441
|
+
let changed = false;
|
|
1442
|
+
const files = (current.files || []).map((entry) => {
|
|
1443
|
+
if (!keys.has(changePathKey(entry.path)) || fileState(entry) === state) return entry;
|
|
1444
|
+
changed = true;
|
|
1445
|
+
return { ...entry, state };
|
|
1446
|
+
});
|
|
1447
|
+
return changed ? files : null;
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const markUnknownStates = (files, keys) =>
|
|
1452
|
+
(files || []).map((entry) => (keys.has(changePathKey(entry.path)) ? unknownStateChangeFile(entry) : entry));
|
|
1453
|
+
|
|
1454
|
+
/** A state write failed after the worktree changed and could not be undone:
|
|
1455
|
+
* mark those files unknown in the record, or — when the store refuses that
|
|
1456
|
+
* write too — for the rest of this runtime, so no reply reads them as
|
|
1457
|
+
* cleanly applied or reverted. */
|
|
1458
|
+
async function markTurnChangeStateUnknown(ownerSessionId, checkpointId, keys) {
|
|
1459
|
+
try {
|
|
1460
|
+
await updateTurnChangeRecord(ownerSessionId, checkpointId, (current) => markUnknownStates(current.files, keys));
|
|
1461
|
+
return;
|
|
1462
|
+
} catch {
|
|
1463
|
+
// The store refused the write: fall back to the in-memory marker below.
|
|
1464
|
+
}
|
|
1465
|
+
const key = pendingFreezeKey(ownerSessionId, checkpointId);
|
|
1466
|
+
_unknownChangeStates.set(key, new Set([...(_unknownChangeStates.get(key) || []), ...keys]));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function withUnknownChangeStates(ownerSessionId, record) {
|
|
1470
|
+
const keys = record && _unknownChangeStates.get(pendingFreezeKey(ownerSessionId, record.checkpointId));
|
|
1471
|
+
return keys ? { ...record, files: markUnknownStates(record.files, keys) } : record;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** The exact-buffer review tracker (non-Git or contended worktree) an
|
|
1475
|
+
* Undo/Reapply must fold into, with the pre-mutation bytes of every moved
|
|
1476
|
+
* file under the tracker's root. Null when the review has a usable Git
|
|
1477
|
+
* baseline (it re-captures instead) or nothing to fold. */
|
|
1478
|
+
async function trackedReviewFold(ownerSessionId, root, targets, reverse) {
|
|
1002
1479
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1003
|
-
if (tracker
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1480
|
+
if (!tracker?.valid || worktreeSnapshotUsable(tracker) || !tracker.worktreeRoot) return null;
|
|
1481
|
+
const inside = (path) => {
|
|
1482
|
+
const rel = relative(tracker.worktreeRoot, path);
|
|
1483
|
+
return Boolean(rel) && !rel.startsWith('..') && !isAbsolute(rel);
|
|
1484
|
+
};
|
|
1485
|
+
const moves = [];
|
|
1486
|
+
for (const entry of targets) {
|
|
1487
|
+
const path = resolve(root, entry.path);
|
|
1488
|
+
const oldPath = entry.oldPath ? resolve(root, entry.oldPath) : path;
|
|
1489
|
+
const move = reverse ? { source: path, destination: oldPath } : { source: oldPath, destination: path };
|
|
1490
|
+
if (inside(move.source) && inside(move.destination)) moves.push(move);
|
|
1013
1491
|
}
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1016
|
-
|
|
1492
|
+
if (moves.length === 0) return null;
|
|
1493
|
+
try {
|
|
1494
|
+
for (const move of moves) move.before = await currentTrackedContent(move.source);
|
|
1495
|
+
} catch {
|
|
1496
|
+
return { tracker, moves, unreadable: true };
|
|
1017
1497
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1498
|
+
return { tracker, moves };
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/** One moved file as a tracked change: its pre-mutation bytes and what the
|
|
1502
|
+
* mutation left at its destination. */
|
|
1503
|
+
async function movedFileChange(root, move) {
|
|
1504
|
+
const change = {
|
|
1505
|
+
path: move.source,
|
|
1506
|
+
displayPath: trackedRelativePath(root, { path: move.source }),
|
|
1507
|
+
before: move.before,
|
|
1508
|
+
after: await currentTrackedContent(move.destination),
|
|
1509
|
+
};
|
|
1510
|
+
if (move.destination === move.source) return change;
|
|
1511
|
+
return {
|
|
1512
|
+
...change,
|
|
1513
|
+
newPath: move.destination,
|
|
1514
|
+
newDisplayPath: trackedRelativePath(root, { path: move.destination }),
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/** Fold an applied Undo/Reapply into the exact tracker. A file the tracker
|
|
1519
|
+
* cannot read back exactly drops the tracked review rather than leaving it
|
|
1520
|
+
* describing the old disk. */
|
|
1521
|
+
async function foldReviewMutation({ tracker, moves, unreadable }) {
|
|
1522
|
+
try {
|
|
1523
|
+
if (unreadable) throw new Error('unreadable');
|
|
1524
|
+
const changes = [];
|
|
1525
|
+
for (const move of moves) changes.push(await movedFileChange(tracker.worktreeRoot, move));
|
|
1526
|
+
foldTrackedChanges(tracker, changes, null);
|
|
1527
|
+
if (!tracker.unifiedDiff) releaseDiffTrackerContent(tracker);
|
|
1528
|
+
} catch {
|
|
1529
|
+
tracker.valid = false;
|
|
1530
|
+
releaseDiffTrackerContent(tracker);
|
|
1531
|
+
refreshUnifiedDiff(tracker);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/** Bring the latest-turn review (composer bar, session Diff) in line with
|
|
1536
|
+
* the disk an Undo/Reapply just changed: signal the moved paths, fold the
|
|
1537
|
+
* mutation into the exact tracker, re-capture the Git snapshot (sealed ones
|
|
1538
|
+
* too) and persist its attribution. Best-effort: the mutation itself
|
|
1539
|
+
* already landed. */
|
|
1540
|
+
async function refreshLatestTurnReview(ownerSessionId, root, targets, fold) {
|
|
1541
|
+
noteTurnWorktreeChange(
|
|
1542
|
+
targets
|
|
1543
|
+
.flatMap((entry) => [entry.path, entry.oldPath])
|
|
1544
|
+
.filter(Boolean)
|
|
1545
|
+
.map((path) => resolve(root, path))
|
|
1546
|
+
);
|
|
1547
|
+
if (fold?.tracker.valid) await foldReviewMutation(fold);
|
|
1548
|
+
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
1549
|
+
if (!tracker?.worktreeSnapshot) return;
|
|
1550
|
+
try {
|
|
1551
|
+
await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot, { force: true });
|
|
1552
|
+
} catch {
|
|
1553
|
+
// Best-effort refresh: persist the last captured state.
|
|
1554
|
+
}
|
|
1555
|
+
if (tracker.sealed) await persistTurnSnapshotRecord(ownerSessionId, tracker);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** Undo (`reverse`) or Reapply one file or the whole turn, all-or-nothing. */
|
|
1559
|
+
async function mutateTurnChanges(sessionId, checkpointIdValue, fileValue, revision, reverse) {
|
|
1560
|
+
const ownerSessionId = clean(sessionId);
|
|
1561
|
+
if (!ownerSessionId) throw new Error('turn changes are unavailable without a session');
|
|
1562
|
+
const checkpointId = requiredCheckpointId(checkpointIdValue);
|
|
1563
|
+
if (!Number.isSafeInteger(revision)) throw new TypeError('revision must be an integer');
|
|
1564
|
+
const file = fileValue === null || fileValue === undefined ? null : clean(fileValue);
|
|
1565
|
+
const verb = reverse ? 'undone' : 'reapplied';
|
|
1566
|
+
return await serializeChangeMutation(ownerSessionId, async () => {
|
|
1567
|
+
if (sessionTurnBusy(ownerSessionId)) {
|
|
1568
|
+
throw new Error('turn changes cannot change while this session is running a turn');
|
|
1569
|
+
}
|
|
1570
|
+
const record = withUnknownChangeStates(ownerSessionId, await loadTurnChangeRecord(ownerSessionId, checkpointId));
|
|
1571
|
+
if (!record) throw new Error('turn changes are unavailable: no record for this turn');
|
|
1572
|
+
if (Number(record.revision) !== revision) throw new Error('turn changes changed; refresh');
|
|
1573
|
+
const from = reverse ? 'applied' : 'reverted';
|
|
1574
|
+
let targets;
|
|
1575
|
+
if (file) {
|
|
1576
|
+
const match = findChangeFile(record, file);
|
|
1577
|
+
if (!match) throw new Error(`${file} is not part of this turn's changes`);
|
|
1578
|
+
if (fileState(match) !== from) throw new Error(`${match.path} is already ${verb}`);
|
|
1579
|
+
targets = [match];
|
|
1580
|
+
} else {
|
|
1581
|
+
targets = (record.files || []).filter((entry) => fileState(entry) === from);
|
|
1582
|
+
if (targets.length === 0) throw new Error(`this turn has no changes to be ${verb}`);
|
|
1583
|
+
}
|
|
1584
|
+
const blocked = targets.find((entry) => entry.revertable !== true);
|
|
1585
|
+
if (blocked) {
|
|
1586
|
+
throw new Error(`${blocked.path} cannot be ${verb}: ${blocked.reason || 'no replayable patch was recorded'}`);
|
|
1587
|
+
}
|
|
1588
|
+
const fold = await trackedReviewFold(ownerSessionId, record.root, targets, reverse);
|
|
1589
|
+
const root = await applyTurnChangeFiles(record, targets, { reverse });
|
|
1590
|
+
// State lands only after git applied every target; a state that cannot
|
|
1591
|
+
// land takes the worktree change back with it.
|
|
1592
|
+
const keys = new Set(targets.map((entry) => changePathKey(entry.path)));
|
|
1593
|
+
let updated;
|
|
1594
|
+
try {
|
|
1595
|
+
updated = await writeTurnChangeState(ownerSessionId, checkpointId, keys, reverse ? 'reverted' : 'applied');
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
try {
|
|
1598
|
+
await applyTurnChangeFiles(record, targets, { reverse: !reverse });
|
|
1599
|
+
} catch (rollbackError) {
|
|
1600
|
+
await markTurnChangeStateUnknown(ownerSessionId, checkpointId, keys);
|
|
1601
|
+
await refreshLatestTurnReview(ownerSessionId, root, targets, fold);
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`turn changes were ${verb} but could not be recorded (${error.message}), and restoring the worktree failed (${rollbackError.message}); Undo/Reapply of those files is disabled`,
|
|
1604
|
+
{ cause: error }
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
await refreshLatestTurnReview(ownerSessionId, root, targets, fold);
|
|
1610
|
+
return turnChangesReply({
|
|
1611
|
+
status: updated ? 'ready' : 'missing',
|
|
1612
|
+
checkpointId,
|
|
1613
|
+
record: withUnknownChangeStates(ownerSessionId, updated),
|
|
1614
|
+
});
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/** Reverse-apply one recorded file (`file`) or every applied file (`null`). */
|
|
1619
|
+
export function revertTurnChanges(sessionId, checkpointId, file, revision) {
|
|
1620
|
+
return mutateTurnChanges(sessionId, checkpointId, file, revision, true);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/** Forward-apply one reverted file (`file`) or every reverted file (`null`). */
|
|
1624
|
+
export function reapplyTurnChanges(sessionId, checkpointId, file, revision) {
|
|
1625
|
+
return mutateTurnChanges(sessionId, checkpointId, file, revision, false);
|
|
1021
1626
|
}
|
|
1022
1627
|
|
|
1023
1628
|
export function _resetTurnSnapshotForTest() {
|
|
1024
1629
|
_turnsBySession.clear();
|
|
1025
1630
|
_diffTrackersBySession.clear();
|
|
1631
|
+
_pendingFreezes.clear();
|
|
1632
|
+
_changeMutations.clear();
|
|
1633
|
+
_unknownChangeStates.clear();
|
|
1634
|
+
_turnOpenedHookForTest = null;
|
|
1026
1635
|
_agentTurnSeq = 0;
|
|
1027
1636
|
_resetTurnWorktreeSnapshotsForTest();
|
|
1028
1637
|
}
|