mixdog 1.1.1 → 1.2.0
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 +1 -0
- package/package.json +2 -2
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -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/headless-role.mjs +2 -2
- package/src/help.mjs +2 -0
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/70-delivery.md +2 -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/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 +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- 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 +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- 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/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-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -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 +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- 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/registry.mjs +15 -1
- 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 +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
- 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/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- 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/context-utils.mjs +12 -14
- 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 +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -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/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 +1 -5
- 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 +29 -19
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -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-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -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/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- 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 +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- 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/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/trusted-roots.mjs +25 -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-command.mjs +0 -10
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/shared/atomic-file.mjs +8 -2
- 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/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- 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 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -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/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +9 -2
- 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 +161 -11
- package/src/runtime/shared/update-checker.mjs +2 -31
- 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/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +9 -4
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- 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 +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- 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/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +27 -10
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- 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-worker.mjs +1 -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/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-profile-api.mjs +27 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -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/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- 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/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- 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 +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +13 -78
- 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 +17 -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 +11 -3
- 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/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -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/dist/index.mjs +332 -36
- 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/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 +12 -8
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +18 -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 +2 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- 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 +15 -1
- 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/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The bounded cache validator can certify the whole file only within this
|
|
2
|
+
// byte budget. Larger reads must not reuse metadata-only or prefix-only
|
|
3
|
+
// content/line-position evidence: equal stats can hide edits past the prefix.
|
|
4
|
+
export const READ_PREFIX_HASH_BYTES = 65536;
|
|
5
|
+
|
|
6
|
+
export function canCacheReadFile(stat) {
|
|
7
|
+
return Number.isFinite(stat?.size) && stat.size >= 0 && stat.size <= READ_PREFIX_HASH_BYTES;
|
|
8
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { fileVersion, sameFileVersion } from './file-version.mjs';
|
|
4
|
+
import { canCacheReadFile, READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
|
|
4
5
|
import { hashText as sha256Hex } from './hash-utils.mjs';
|
|
5
6
|
import { join, normalize, resolve } from 'node:path';
|
|
6
7
|
import { getPluginData } from '../../config.mjs';
|
|
@@ -83,7 +84,7 @@ function readRangeIndexMatches(row, fullPath, version) {
|
|
|
83
84
|
// `prefixHash` they persist into the anchor index.
|
|
84
85
|
async function computePrefixHashForIndex(fullPath, st, handle = null, prefixBuffer = null) {
|
|
85
86
|
try {
|
|
86
|
-
const cap = Math.min(Number(st?.size) || 0,
|
|
87
|
+
const cap = Math.min(Number(st?.size) || 0, READ_PREFIX_HASH_BYTES);
|
|
87
88
|
if (cap <= 0) return '';
|
|
88
89
|
if (Buffer.isBuffer(prefixBuffer) && prefixBuffer.length === cap) {
|
|
89
90
|
return sha256Hex(prefixBuffer);
|
|
@@ -269,6 +270,8 @@ function freshReadRangeIndex(fullPath, version = null) {
|
|
|
269
270
|
export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffer = null) {
|
|
270
271
|
const key = canonicalCachePath(fullPath);
|
|
271
272
|
let slot = READ_RANGE_INDEX_CACHE.get(key);
|
|
273
|
+
// Reserve ownership before awaiting the real stat even when the hint is
|
|
274
|
+
// large: a now-small file must not renew a lookup invalidated during IO.
|
|
272
275
|
if (!slot) slot = refreshReadRangeIndexSlot(key, freshReadRangeIndex(fullPath));
|
|
273
276
|
const hint = fileVersion(st);
|
|
274
277
|
const readStat = () => (handle ? handle.stat() : stat(fullPath));
|
|
@@ -280,8 +283,18 @@ export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffe
|
|
|
280
283
|
} catch {
|
|
281
284
|
return null;
|
|
282
285
|
}
|
|
283
|
-
if (!version
|
|
286
|
+
if (!version) return null;
|
|
284
287
|
version = Object.freeze(version);
|
|
288
|
+
// Metadata and a bounded prefix cannot certify suffix line positions.
|
|
289
|
+
// Large scans own only this call's handle identity and byte-zero draft:
|
|
290
|
+
// never load, hash-validate, or publish cross-read anchors/counts.
|
|
291
|
+
if (!canCacheReadFile(version)) {
|
|
292
|
+
if (slot && READ_RANGE_INDEX_CACHE.get(key) === slot) dropReadRangeIndexSlot(key);
|
|
293
|
+
const draft = freshReadRangeIndex(fullPath, version);
|
|
294
|
+
READ_RANGE_INDEX_DRAFTS.set(draft, { slot: null, version, fullPath });
|
|
295
|
+
return draft;
|
|
296
|
+
}
|
|
297
|
+
if (READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
|
|
285
298
|
if (!slot.fileVersion) slot.fileVersion = version;
|
|
286
299
|
if (!sameFileVersion(slot.fileVersion, version)) {
|
|
287
300
|
dropReadRangeIndexSlot(key);
|
|
@@ -314,27 +327,33 @@ export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffe
|
|
|
314
327
|
anchors: new Map(source.anchors),
|
|
315
328
|
...(Number.isFinite(source.totalLines) ? { totalLines: source.totalLines } : {}),
|
|
316
329
|
};
|
|
317
|
-
READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version });
|
|
330
|
+
READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version, fullPath });
|
|
318
331
|
return draft;
|
|
319
332
|
}
|
|
320
333
|
|
|
321
|
-
// Successful readers publish once, before closing their handle.
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
334
|
+
// Successful small-file readers publish once, before closing their handle.
|
|
335
|
+
// Large or detached scans remain private, but terminal validation can still
|
|
336
|
+
// certify their producer identity via onValidated. Post-stat failures never
|
|
337
|
+
// authorize relabelling bytes with a later version.
|
|
338
|
+
export async function publishReadRangeIndex(index, handle, signal = null, beforePublish = null, onValidated = null) {
|
|
325
339
|
const owned = READ_RANGE_INDEX_DRAFTS.get(index);
|
|
326
340
|
if (!owned) return false;
|
|
327
341
|
READ_RANGE_INDEX_DRAFTS.delete(index);
|
|
328
|
-
const { slot, version } = owned;
|
|
329
|
-
const key = canonicalCachePath(
|
|
330
|
-
if (signal?.aborted
|
|
342
|
+
const { slot, version, fullPath } = owned;
|
|
343
|
+
const key = canonicalCachePath(fullPath);
|
|
344
|
+
if (signal?.aborted) return false;
|
|
345
|
+
// Detached cache-only publishers must not wait on validation IO. A streamed
|
|
346
|
+
// result requesting producer certification still owes its terminal stats.
|
|
347
|
+
if (slot && READ_RANGE_INDEX_CACHE.get(key) !== slot && !onValidated) return false;
|
|
331
348
|
try {
|
|
332
|
-
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(
|
|
349
|
+
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(fullPath)))
|
|
333
350
|
return false;
|
|
334
351
|
} catch {
|
|
335
352
|
return false;
|
|
336
353
|
}
|
|
337
|
-
if (signal?.aborted
|
|
354
|
+
if (signal?.aborted) return false;
|
|
355
|
+
onValidated?.(fileVersion(version));
|
|
356
|
+
if (!slot || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
|
|
338
357
|
if (slot.prefixHash && index.prefixHash && slot.prefixHash !== index.prefixHash) return false;
|
|
339
358
|
// The reader's synchronous deadline/cancellation decision belongs after all
|
|
340
359
|
// awaited validation and outside its best-effort stat catch. Once committed,
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fsPromises from 'node:fs/promises';
|
|
9
9
|
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
10
11
|
import { getReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
11
12
|
import { snapshotCoversFullFile, statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
12
|
-
|
|
13
|
-
export
|
|
13
|
+
import { canCacheReadFile, READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
|
|
14
|
+
export { READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
|
|
14
15
|
|
|
15
16
|
// A mutation snapshot claims the full file because it knows the bytes it
|
|
16
17
|
// wrote; it proves the session holds the body only when bodyDelivered says so.
|
|
@@ -25,14 +26,12 @@ function unchangedStub(filePath, helpers) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* records the hash of the first 64KiB (the whole body when it fits) and the
|
|
31
|
-
* next hit recomputes it before trusting the entry. '' on any failure.
|
|
29
|
+
* Bounded raw-byte hash. Content caches only admit files that fit this
|
|
30
|
+
* budget, so validation covers their entire body. '' on any failure.
|
|
32
31
|
*/
|
|
33
32
|
export async function readPrefixHash(fullPath, st, hashText) {
|
|
34
33
|
try {
|
|
35
|
-
if (st.size <= READ_PREFIX_HASH_BYTES) return hashText(await readFile(fullPath
|
|
34
|
+
if (st.size <= READ_PREFIX_HASH_BYTES) return hashText(await readFile(fullPath));
|
|
36
35
|
const fh = await fsPromises.open(fullPath, 'r');
|
|
37
36
|
try {
|
|
38
37
|
const buf = Buffer.allocUnsafe(READ_PREFIX_HASH_BYTES);
|
|
@@ -49,15 +48,15 @@ export async function readPrefixHash(fullPath, st, hashText) {
|
|
|
49
48
|
/**
|
|
50
49
|
* A successful edit/apply_patch leaves a session-scoped full-file snapshot.
|
|
51
50
|
* The model already knows the resulting body from the prior body plus its
|
|
52
|
-
* mutation
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* mutation. Keep the existing small-file stat fast path; a large file's
|
|
52
|
+
* matching tuple cannot exclude a deep external rewrite, so read it again.
|
|
53
|
+
* `bodyDelivered` guards that the
|
|
55
54
|
* session actually received this file's body before the mutation: an
|
|
56
55
|
* edit/patch on a file that was never read otherwise answered
|
|
57
56
|
* "[file unchanged]" forever, with the body never delivered to the model.
|
|
58
57
|
*/
|
|
59
58
|
export function mutationUnchangedStub({ fullPath, filePath, st, readStateScope, options }, helpers) {
|
|
60
|
-
if (options?.suppressReadUnchangedStub === true) return null;
|
|
59
|
+
if (options?.suppressReadUnchangedStub === true || !canCacheReadFile(st)) return null;
|
|
61
60
|
const snapshot = readStateScope ? getReadSnapshot(fullPath, readStateScope) : null;
|
|
62
61
|
const source = String(snapshot?.source || '');
|
|
63
62
|
if (
|
|
@@ -71,43 +70,41 @@ export function mutationUnchangedStub({ fullPath, filePath, st, readStateScope,
|
|
|
71
70
|
return null;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// validates whichever hash the entry carries (prefix == full at this size;
|
|
78
|
-
// the exact contentHash is preferred). >64KiB: contentHash may still be
|
|
79
|
-
// stored, but validating it would sha a multi-megabyte body on every check,
|
|
80
|
-
// so only the 64KiB head prefix is checked — it catches same-mtime/same-size
|
|
81
|
-
// rewrites within the first 64KiB (the common case); writes through
|
|
82
|
-
// edit/apply_patch/write invalidate by path and shell mutationMode='global'
|
|
83
|
-
// wipes the caches, bounding stale risk past the head.
|
|
73
|
+
// Only whole-body bounded validation can admit a result-cache hit.
|
|
74
|
+
// The decoded snapshot hash remains a separate contract, never a fallback
|
|
75
|
+
// after a raw-byte hash mismatch.
|
|
84
76
|
async function cachedEntryStillValid(cachedEntry, fullPath, st, hashText) {
|
|
85
77
|
const prefixHash = cachedEntry.contentPrefixHash;
|
|
86
78
|
const snapHash = cachedEntry.readSnapshotMeta?.contentHash;
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
if (prefixHash) {
|
|
80
|
+
const curHash = await readPrefixHash(fullPath, st, hashText);
|
|
81
|
+
return !!curHash && curHash === prefixHash;
|
|
82
|
+
}
|
|
83
|
+
if (!snapHash) return true;
|
|
84
|
+
try {
|
|
85
|
+
const freshHash = hashText(await readFile(fullPath, 'utf-8'));
|
|
86
|
+
return !!freshHash && freshHash === snapHash;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
95
89
|
}
|
|
96
|
-
if (!prefixHash) return true;
|
|
97
|
-
const curHash = await readPrefixHash(fullPath, st, hashText);
|
|
98
|
-
return !!curHash && curHash === prefixHash;
|
|
99
90
|
}
|
|
100
91
|
|
|
101
92
|
/**
|
|
102
93
|
* Validated result-cache hit.
|
|
103
|
-
* @returns {Promise<{ result: unknown } |
|
|
104
|
-
*
|
|
94
|
+
* @returns {Promise<{ result: unknown } | { rejected: true } | null>}
|
|
95
|
+
* A rejected entry requires fresh IO even if an older reader republishes.
|
|
105
96
|
*/
|
|
106
97
|
export async function readCachedResult({ cacheKey, fullPath, filePath, st, readStateScope, options }, helpers) {
|
|
98
|
+
if (!canCacheReadFile(st)) return null;
|
|
107
99
|
const { _cacheGetEntry, _hashText, _recordReadSnapshot, classifyResultKind } = helpers;
|
|
108
100
|
const cachedEntry = _cacheGetEntry(cacheKey);
|
|
109
101
|
if (cachedEntry === null) return null;
|
|
110
|
-
if (!(await cachedEntryStillValid(cachedEntry, fullPath, st, _hashText)))
|
|
102
|
+
if (!(await cachedEntryStillValid(cachedEntry, fullPath, st, _hashText))) {
|
|
103
|
+
// The metadata no longer proves byte identity. Detach raw/in-flight
|
|
104
|
+
// reuse too, or the fresh-read fallback can return the rejected body.
|
|
105
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
106
|
+
return { rejected: true };
|
|
107
|
+
}
|
|
111
108
|
// Cross-session stub guard: RESULT_CACHE is process-global, so a cache hit
|
|
112
109
|
// can be an entry SET BY ANOTHER SESSION whose body this conversation never
|
|
113
110
|
// received. The stub assumes the full body is already in a prior
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as fsPromises from 'node:fs/promises';
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
9
|
import { sameFileVersion } from './file-version.mjs';
|
|
10
|
-
import { READ_PREFIX_HASH_BYTES } from './read-
|
|
10
|
+
import { canCacheReadFile, READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
|
|
11
11
|
import { decodeUtf16Body, isUtf16Encoding } from './snapshot-helpers.mjs';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -133,13 +133,12 @@ function renderReadWindow(content, ctx, helpers) {
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Describe exactly which lines the model saw. A full-file view pins the
|
|
136
|
-
* whole body with one contentHash
|
|
137
|
-
* prefix hash — the same SHA-256 over the identical `content`); anything
|
|
136
|
+
* whole decoded body with one contentHash; anything
|
|
138
137
|
* narrower records rangeHashes over the visible window so
|
|
139
138
|
* snapshotCoversFullFile never green-lights an overwrite against bytes the
|
|
140
139
|
* read never returned.
|
|
141
140
|
*/
|
|
142
|
-
function bufferedReadSnapshotMeta(content, render, ctx, helpers) {
|
|
141
|
+
function bufferedReadSnapshotMeta(content, rawBuf, render, ctx, helpers) {
|
|
143
142
|
const { offset, limit } = ctx;
|
|
144
143
|
const { _hashText, _rangeHashesForReadRanges } = helpers;
|
|
145
144
|
const { lineCount, renderedLineCount, smartTruncated, smartVisibleRanges, byteCapTruncated } = render;
|
|
@@ -158,11 +157,9 @@ function bufferedReadSnapshotMeta(content, render, ctx, helpers) {
|
|
|
158
157
|
...(isFullFileView ? { contentHash: fullContentHash } : {}),
|
|
159
158
|
...(rangeHashes.length > 0 ? { rangeHashes } : {}),
|
|
160
159
|
};
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const head = content.length <= READ_PREFIX_HASH_BYTES ? content : content.slice(0, READ_PREFIX_HASH_BYTES);
|
|
165
|
-
const contentPrefixHash = head === content && fullContentHash ? fullContentHash : _hashText(head);
|
|
160
|
+
// Cache validation hashes on-disk bytes, unlike the decoded snapshot.
|
|
161
|
+
// Use a byte boundary for every encoding, matching the streaming path.
|
|
162
|
+
const contentPrefixHash = canCacheReadFile(ctx.st) ? _hashText(rawBuf.subarray(0, READ_PREFIX_HASH_BYTES)) : '';
|
|
166
163
|
return { snapshotMeta, contentPrefixHash };
|
|
167
164
|
}
|
|
168
165
|
|
|
@@ -189,9 +186,11 @@ export async function bufferedReadResult(ctx, { prefetched, readEnc }, helpers)
|
|
|
189
186
|
}
|
|
190
187
|
const render = renderReadWindow(content, ctx, helpers);
|
|
191
188
|
if (readStable) {
|
|
192
|
-
const { snapshotMeta, contentPrefixHash } = bufferedReadSnapshotMeta(content, render, ctx, helpers);
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
const { snapshotMeta, contentPrefixHash } = bufferedReadSnapshotMeta(content, rawBuf, render, ctx, helpers);
|
|
190
|
+
if (canCacheReadFile(st)) {
|
|
191
|
+
_cacheSet(cacheKey, render.out, { paths: [fullPath], readSnapshotMeta: snapshotMeta, contentPrefixHash });
|
|
192
|
+
_rawContentCacheSet(fullPath, st, rawBuf);
|
|
193
|
+
}
|
|
195
194
|
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
196
195
|
}
|
|
197
196
|
return render.out;
|
|
@@ -6,23 +6,25 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { formatBinaryReadPreviewFromBuffer } from './binary-file.mjs';
|
|
8
8
|
import { readPrefixHash } from './read-single-fast-paths.mjs';
|
|
9
|
+
import { canCacheReadFile } from './read-cache-policy.mjs';
|
|
9
10
|
|
|
10
11
|
export function binaryPreviewResult(ctx, preview, helpers) {
|
|
11
12
|
const { fullPath, filePath, st, cacheKey, readStateScope } = ctx;
|
|
12
13
|
const { normalizeOutputPath, _recordReadSnapshot, _cacheSet } = helpers;
|
|
13
14
|
const { text, snapshotMeta } = formatBinaryReadPreviewFromBuffer(preview, normalizeOutputPath(filePath), st.size);
|
|
14
15
|
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
15
|
-
_cacheSet(cacheKey, text, { paths: [fullPath], readSnapshotMeta: snapshotMeta });
|
|
16
|
+
if (canCacheReadFile(st)) _cacheSet(cacheKey, text, { paths: [fullPath], readSnapshotMeta: snapshotMeta });
|
|
16
17
|
return text;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async function commitStreamResult(ctx, out, snapshotMeta, prefixHash, helpers) {
|
|
20
21
|
const { fullPath, st, cacheKey, readStateScope } = ctx;
|
|
21
22
|
const { _cacheSet, _recordReadSnapshot, _hashText } = helpers;
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Do not reopen a large file just to hash a prefix that cannot certify it.
|
|
24
|
+
if (canCacheReadFile(st)) {
|
|
25
|
+
const contentPrefixHash = prefixHash || (await readPrefixHash(fullPath, st, _hashText));
|
|
26
|
+
_cacheSet(cacheKey, out, { paths: [fullPath], readSnapshotMeta: snapshotMeta, contentPrefixHash });
|
|
27
|
+
}
|
|
26
28
|
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
27
29
|
return out;
|
|
28
30
|
}
|
|
@@ -50,7 +52,8 @@ export async function streamRangeResult(ctx, { source, fileHandle, prefixBuffer
|
|
|
50
52
|
const ranges =
|
|
51
53
|
res.firstEmitted && res.lastEmitted ? [{ startLine: res.firstEmitted, endLine: res.lastEmitted }] : [];
|
|
52
54
|
const snapshotMeta = { source, ranges, rangeHashes: _rangeHashesFromRenderedReadText(res.text, ranges) };
|
|
53
|
-
|
|
55
|
+
if (!res.readSnapshotStat) return res.text;
|
|
56
|
+
return await commitStreamResult({ ...ctx, st: res.readSnapshotStat }, res.text, snapshotMeta, res.prefixHash, helpers);
|
|
54
57
|
} catch (err) {
|
|
55
58
|
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
56
59
|
}
|
|
@@ -21,15 +21,16 @@ import { mutationUnchangedStub, pathSnapshotUnchangedStub, readCachedResult } fr
|
|
|
21
21
|
import { readMediaFile } from './read-single-media.mjs';
|
|
22
22
|
import { binaryPreviewResult, smartStreamResult, streamRangeResult } from './read-single-stream.mjs';
|
|
23
23
|
import { bufferedReadResult } from './read-single-render.mjs';
|
|
24
|
+
import { runReadCachePublication } from './cache-layers.mjs';
|
|
24
25
|
|
|
25
26
|
// Buffered reads take the whole body up front: the raw-content cache first,
|
|
26
27
|
// else one readFile shared with in-flight reads of the same path AND version.
|
|
27
|
-
async function prefetchRawBody(fullPath, st, helpers) {
|
|
28
|
+
async function prefetchRawBody(fullPath, st, helpers, forceFresh = false) {
|
|
28
29
|
const { _rawContentCacheGet, _runRawContentInFlight } = helpers;
|
|
29
|
-
const cachedRaw = _rawContentCacheGet ? _rawContentCacheGet(fullPath, st) : null;
|
|
30
|
+
const cachedRaw = !forceFresh && _rawContentCacheGet ? _rawContentCacheGet(fullPath, st) : null;
|
|
30
31
|
if (cachedRaw) return { buf: cachedRaw, fromCache: true };
|
|
31
32
|
try {
|
|
32
|
-
const buf = _runRawContentInFlight
|
|
33
|
+
const buf = !forceFresh && _runRawContentInFlight
|
|
33
34
|
? await _runRawContentInFlight(fullPath, readFile, st)
|
|
34
35
|
: await readFile(fullPath);
|
|
35
36
|
return { buf, fromCache: false };
|
|
@@ -39,6 +40,12 @@ async function prefetchRawBody(fullPath, st, helpers) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export async function executeSingleReadTool(args, workDir, readStateScope, options = {}, helpers = {}) {
|
|
43
|
+
// A coalesced batch must not publish its union window as delivered evidence.
|
|
44
|
+
// Its internal recorder holds the producer's lease until the caller slots
|
|
45
|
+
// are sliced; ordinary reads retain their existing snapshot path.
|
|
46
|
+
if (typeof options._readSnapshotRecorder === 'function') {
|
|
47
|
+
helpers = { ...helpers, _recordReadSnapshot: options._readSnapshotRecorder };
|
|
48
|
+
}
|
|
42
49
|
const {
|
|
43
50
|
parseLineLimitArg,
|
|
44
51
|
parseOffsetArg,
|
|
@@ -64,69 +71,76 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
64
71
|
const fullPath = resolveAgainstCwd(filePath, workDir);
|
|
65
72
|
const resolvedRejected = readResolvedPathGuard(fullPath, helpers);
|
|
66
73
|
if (resolvedRejected) return resolvedRejected;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
74
|
+
return runReadCachePublication(fullPath, async () => {
|
|
75
|
+
const hasOffsetArg = args.offset !== undefined && args.offset !== null;
|
|
76
|
+
const hasLimitArg = args.limit !== undefined && args.limit !== null;
|
|
77
|
+
const hasRangeArgs = hasOffsetArg || hasLimitArg;
|
|
78
|
+
const wantFull = args.full === true;
|
|
79
|
+
let offset = parseOffsetArg(args.offset);
|
|
80
|
+
// full:true bypasses the default 2000-line cap so the whole file can be
|
|
81
|
+
// returned in one call; the byte cap still emits a compact truncation
|
|
82
|
+
// marker when rendered bytes overflow READ_MAX_OUTPUT_BYTES.
|
|
83
|
+
let limit = parseLineLimitArg(args.limit, wantFull ? Infinity : 2000);
|
|
84
|
+
let widenNote = '';
|
|
85
|
+
const target = await statReadTarget(fullPath, filePath, options, helpers);
|
|
86
|
+
if (target.error) return target.error;
|
|
87
|
+
if (!target.st) return readNotFoundResponse(target.statErr, { fullPath, filePath, workDir }, helpers);
|
|
88
|
+
const st = target.st;
|
|
89
|
+
// Reactive widen + window-history bookkeeping (ranged text reads only).
|
|
90
|
+
if (
|
|
91
|
+
readStateScope &&
|
|
92
|
+
typeof readStateScope === 'object' &&
|
|
93
|
+
hasOffsetArg &&
|
|
94
|
+
hasLimitArg &&
|
|
95
|
+
Number.isFinite(limit) &&
|
|
96
|
+
limit > 0
|
|
97
|
+
) {
|
|
98
|
+
({ offset, limit, widenNote } = widenReadWindow(readStateScope, fullPath, { offset, limit }));
|
|
99
|
+
}
|
|
100
|
+
const unchanged = mutationUnchangedStub({ fullPath, filePath, st, readStateScope, options }, helpers);
|
|
101
|
+
if (unchanged) return unchanged;
|
|
102
|
+
// MEDIA-WINS: media dispatch runs before every cache/snapshot fast path so
|
|
103
|
+
// a stale text entry can never short-circuit a media read.
|
|
104
|
+
const media = await readMediaFile({ fullPath, st, args, readStateScope, options, hasRangeArgs }, helpers);
|
|
105
|
+
if (media) return media.result;
|
|
106
|
+
const cacheKey = `read|${fullPath}|${st.mtimeMs}|${st.ctimeMs}|${st.size}|${hasOffsetArg ? offset : 'd'}|${hasLimitArg ? limit : 'd'}|${wantFull ? 'f' : 's'}|base:${readOffsetBase}|budget:${readMaxOutputBytes}`;
|
|
107
|
+
const ctx = {
|
|
108
|
+
fullPath,
|
|
109
|
+
filePath,
|
|
110
|
+
st,
|
|
111
|
+
cacheKey,
|
|
112
|
+
readStateScope,
|
|
113
|
+
options,
|
|
114
|
+
offset,
|
|
115
|
+
limit,
|
|
116
|
+
hasRangeArgs,
|
|
117
|
+
wantFull,
|
|
118
|
+
readMaxOutputBytes,
|
|
119
|
+
readOffsetBase,
|
|
120
|
+
widenNote,
|
|
121
|
+
};
|
|
122
|
+
const cached = await readCachedResult(ctx, helpers);
|
|
123
|
+
if (cached && !cached.rejected) return cached.result;
|
|
124
|
+
const finishRead = async () => {
|
|
125
|
+
const pathStub = await pathSnapshotUnchangedStub(ctx, helpers);
|
|
126
|
+
if (pathStub) return pathStub;
|
|
127
|
+
// Pre-read size cap: a small error response beats 25K tokens of truncated
|
|
128
|
+
// content (Anthropic #21841 reverted). With offset/limit the
|
|
129
|
+
// requested window streams instead; without range args the cap still
|
|
130
|
+
// refuses so the small-file default path can't pull megabytes by accident.
|
|
131
|
+
const preferRangeStream =
|
|
132
|
+
hasRangeArgs && !wantFull && (options?.forceReadRangeStream === true || st.size > READ_STREAM_RANGE_MIN_BYTES);
|
|
133
|
+
const preferSmartStream = !hasRangeArgs && !wantFull && st.size >= READ_SMART_STREAM_MIN_BYTES;
|
|
134
|
+
const preferBufferedRead = st.size <= READ_MAX_SIZE_BYTES && !preferRangeStream && !preferSmartStream;
|
|
135
|
+
const prefetched = preferBufferedRead
|
|
136
|
+
? await prefetchRawBody(fullPath, st, helpers, cached?.rejected === true)
|
|
137
|
+
: { buf: null, fromCache: false };
|
|
138
|
+
return await readBodyResult(ctx, { prefetched, preferRangeStream, preferSmartStream }, helpers);
|
|
139
|
+
};
|
|
140
|
+
// Rejection invalidates this lease too. Only the fresh continuation gets
|
|
141
|
+
// a new generation; readers still holding earlier bytes remain fenced.
|
|
142
|
+
return cached?.rejected ? runReadCachePublication(fullPath, finishRead) : finishRead();
|
|
143
|
+
});
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
// Body read: binary/encoding inspection, then the streamed (range or smart)
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
statMatchesSnapshot,
|
|
9
9
|
decodeRawBufferForSnapshotCheck,
|
|
10
10
|
} from './snapshot-helpers.mjs';
|
|
11
|
-
import { rawContentCacheGet } from './cache-layers.mjs';
|
|
11
|
+
import { canPublishReadCache, rawContentCacheGet } from './cache-layers.mjs';
|
|
12
12
|
import { fileVersion } from './file-version.mjs';
|
|
13
|
+
import { canCacheReadFile } from './read-cache-policy.mjs';
|
|
13
14
|
import { publishRawContentAfterRead } from './raw-content-publication.mjs';
|
|
14
15
|
import { rememberReadSnapshot, readFilesForScope, readScopeKey, scheduleScopePersist } from './snapshot-store.mjs';
|
|
15
16
|
import {
|
|
@@ -118,11 +119,15 @@ function collectRangeHashRows(existing, sameFile, meta) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
|
|
122
|
+
if (!canPublishReadCache(fullPath)) return;
|
|
121
123
|
const readFiles = readFilesForScope(scope);
|
|
122
124
|
const identity = snapshotStatIdentity(fullPath, st);
|
|
123
125
|
const { mtimeMs, ctimeMs, size, ino, dev } = identity || {};
|
|
124
126
|
const incomingRanges = Array.isArray(meta.ranges) ? meta.ranges : [{ startLine: 1, endLine: Infinity }];
|
|
125
|
-
|
|
127
|
+
// Equal metadata cannot carry a large file's old ranges or full-body proof
|
|
128
|
+
// into a new observation. Batched windows arrive together as one snapshot.
|
|
129
|
+
const freshLargeRead = !canCacheReadFile(identity) && !isMutationSource(meta.source);
|
|
130
|
+
const replaceExisting = meta.replaceExisting === true || freshLargeRead;
|
|
126
131
|
const existing = replaceExisting ? null : readFiles.get(fullPath);
|
|
127
132
|
const sameFile = existing && statMatchesSnapshot(identity, existing) && Array.isArray(existing.ranges);
|
|
128
133
|
// A mutation snapshot claims the full range because it knows the bytes it
|
|
@@ -161,7 +166,7 @@ export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
|
|
|
161
166
|
const bodyDelivered = resolveBodyDelivered({
|
|
162
167
|
next,
|
|
163
168
|
meta,
|
|
164
|
-
priorSnapshot: readFiles.get(fullPath),
|
|
169
|
+
priorSnapshot: freshLargeRead ? null : readFiles.get(fullPath),
|
|
165
170
|
identity,
|
|
166
171
|
incomingIsGrep,
|
|
167
172
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { open } from 'node:fs/promises';
|
|
1
|
+
import { open, stat } from 'node:fs/promises';
|
|
2
2
|
import { hashText } from './hash-utils.mjs';
|
|
3
3
|
import { runResultCacheInFlight } from './cache-layers.mjs';
|
|
4
4
|
import {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
publishReadRangeIndex,
|
|
10
10
|
} from './read-range-index.mjs';
|
|
11
11
|
import { READ_MAX_SCAN_BYTES, READ_STREAM_TIMEOUT_MS } from './read-constants.mjs';
|
|
12
|
+
import { canCacheReadFile } from './read-cache-policy.mjs';
|
|
12
13
|
|
|
13
14
|
// Raw, one-based inclusive source windows. Callers merge/sort intervals first.
|
|
14
15
|
// Unlike the display reader, this never truncates a line into usable patch text.
|
|
@@ -16,6 +17,9 @@ export async function readSourceWindows(path, intervals, { signal } = {}) {
|
|
|
16
17
|
signal?.throwIfAborted();
|
|
17
18
|
if (!intervals.length) return new Map();
|
|
18
19
|
const snapshot = intervals.map(({ start, end }) => ({ start, end }));
|
|
20
|
+
// A later caller must not join a scan that already observed old deep bytes.
|
|
21
|
+
// Preserve small-file fan-out and its per-subscriber cancellation contract.
|
|
22
|
+
if (!canCacheReadFile(await stat(path))) return scanSourceWindows(path, snapshot, signal);
|
|
19
23
|
const key = `source-windows:${canonicalCachePath(path)}:${JSON.stringify(snapshot.map((w) => [w.start, w.end]))}`;
|
|
20
24
|
const shared = await runResultCacheInFlight(
|
|
21
25
|
key,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
2
|
import { sameFileVersion } from './file-version.mjs';
|
|
3
|
+
import { canCacheReadFile, READ_PREFIX_HASH_BYTES } from './read-cache-policy.mjs';
|
|
3
4
|
import * as fsPromises from 'node:fs/promises';
|
|
4
5
|
import { createInterface } from 'node:readline';
|
|
5
6
|
import { capShellOutput } from './shell-output.mjs';
|
|
@@ -55,7 +56,7 @@ async function readRangeFromHandle(fh, fullPath, offset, limit, stHint, hooks) {
|
|
|
55
56
|
const anchor = nearestReadRangeAnchor(rangeIndex, offset);
|
|
56
57
|
// Keep the first read large enough for the existing 64 KiB prefix hash.
|
|
57
58
|
// Grow only while seeking distant lines; nearby windows avoid 1 MiB reads.
|
|
58
|
-
const MIN_CHUNK_BYTES =
|
|
59
|
+
const MIN_CHUNK_BYTES = READ_PREFIX_HASH_BYTES;
|
|
59
60
|
const MAX_CHUNK_BYTES = 1024 * 1024;
|
|
60
61
|
let chunkBytes = MIN_CHUNK_BYTES;
|
|
61
62
|
let buf = Buffer.allocUnsafe(chunkBytes);
|
|
@@ -83,10 +84,14 @@ async function readRangeFromHandle(fh, fullPath, offset, limit, stHint, hooks) {
|
|
|
83
84
|
while (!stop) {
|
|
84
85
|
checkDeadline();
|
|
85
86
|
if (buf.length < chunkBytes) buf = Buffer.allocUnsafe(chunkBytes);
|
|
87
|
+
// Process all bytes inside the absolute cap before rejecting a deeper
|
|
88
|
+
// window. At the cap, a one-byte probe distinguishes EOF from a partial
|
|
89
|
+
// final line; any returned byte is rejected before reaching the collector.
|
|
90
|
+
const readBytes = Math.min(chunkBytes, Math.max(1, READ_MAX_SCAN_BYTES - position));
|
|
86
91
|
const prefix = position === 0 && Buffer.isBuffer(prefixBuffer) ? prefixBuffer : null;
|
|
87
92
|
const bytesRead = prefix?.length
|
|
88
|
-
? prefix.copy(buf, 0, 0, Math.min(prefix.length,
|
|
89
|
-
: (await fh.read(buf, 0,
|
|
93
|
+
? prefix.copy(buf, 0, 0, Math.min(prefix.length, readBytes))
|
|
94
|
+
: (await fh.read(buf, 0, readBytes, position)).bytesRead;
|
|
90
95
|
if (bytesRead === 0) break;
|
|
91
96
|
const chunkStart = position;
|
|
92
97
|
position += bytesRead;
|
|
@@ -94,7 +99,7 @@ async function readRangeFromHandle(fh, fullPath, offset, limit, stHint, hooks) {
|
|
|
94
99
|
throw new Error(`read scan exceeds ${READ_MAX_SCAN_BYTES} bytes`);
|
|
95
100
|
}
|
|
96
101
|
if (!prefixHash && chunkStart === 0) {
|
|
97
|
-
prefixHash = hashText(buf.subarray(0, Math.min(bytesRead,
|
|
102
|
+
prefixHash = hashText(buf.subarray(0, Math.min(bytesRead, READ_PREFIX_HASH_BYTES)));
|
|
98
103
|
if (rangeIndex && rangeIndex.prefixHash !== prefixHash) {
|
|
99
104
|
rangeIndex.prefixHash = prefixHash;
|
|
100
105
|
}
|
|
@@ -131,7 +136,10 @@ async function readRangeFromHandle(fh, fullPath, offset, limit, stHint, hooks) {
|
|
|
131
136
|
if (start < bytesRead) lines.appendPartial(buf.subarray(start, bytesRead));
|
|
132
137
|
}
|
|
133
138
|
if (!stop) lines.finishTail();
|
|
134
|
-
|
|
139
|
+
let readSnapshotStat = null;
|
|
140
|
+
if (!(await publishReadRangeIndex(rangeIndex, fh, null, checkDeadline, (version) => {
|
|
141
|
+
readSnapshotStat = version;
|
|
142
|
+
}))) checkDeadline();
|
|
135
143
|
|
|
136
144
|
ioTraceDone('read_range_stream', traceStart, {
|
|
137
145
|
pathHash: hashText(fullPath).slice(0, 12),
|
|
@@ -147,6 +155,7 @@ async function readRangeFromHandle(fh, fullPath, offset, limit, stHint, hooks) {
|
|
|
147
155
|
firstEmitted: lines.firstEmitted,
|
|
148
156
|
lastEmitted: lines.lastEmitted,
|
|
149
157
|
prefixHash,
|
|
158
|
+
readSnapshotStat,
|
|
150
159
|
};
|
|
151
160
|
}
|
|
152
161
|
|
|
@@ -269,8 +278,8 @@ export async function countLogicalLinesBytesSync(fullPath, size, stHint = null,
|
|
|
269
278
|
const toRead = Math.min(buf.length, size - position);
|
|
270
279
|
const { bytesRead } = await fh.read(buf, 0, toRead, position);
|
|
271
280
|
if (bytesRead <= 0) break;
|
|
272
|
-
if (position === 0 && rangeIndex && !rangeIndex.prefixHash) {
|
|
273
|
-
rangeIndex.prefixHash = hashText(buf.subarray(0, Math.min(bytesRead,
|
|
281
|
+
if (position === 0 && rangeIndex && canCacheReadFile(rangeIndex.fileVersion) && !rangeIndex.prefixHash) {
|
|
282
|
+
rangeIndex.prefixHash = hashText(buf.subarray(0, Math.min(bytesRead, READ_PREFIX_HASH_BYTES)));
|
|
274
283
|
}
|
|
275
284
|
let start = 0;
|
|
276
285
|
while (start < bytesRead) {
|