mixdog 1.1.2 → 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 -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 +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/help.mjs +2 -0
- 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 +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/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/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/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/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/attachments/store.mjs +38 -0
- 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/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 +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 +161 -11
- 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/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 +4 -0
- 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 +20 -7
- 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/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/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-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/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/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-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 +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 +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 +16 -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/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 +2 -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 +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
|
@@ -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) {
|
|
@@ -142,7 +142,8 @@ export async function dispatchBatchReads({
|
|
|
142
142
|
readOutputBudgetBytes: outputBudget,
|
|
143
143
|
toolOutputMaxBytes: outputBudget,
|
|
144
144
|
});
|
|
145
|
-
|
|
145
|
+
// Retain producer identity when caller slots are restored from a union.
|
|
146
|
+
results[index] = { path: entry.path, mode: entry.mode || 'full', n: entry.n, body, readEntry };
|
|
146
147
|
};
|
|
147
148
|
const key = entry.path || `#missing-${index}`;
|
|
148
149
|
const prev = readChains.get(key) ?? Promise.resolve();
|