mixdog 1.0.12 → 1.1.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 +75 -181
- package/package.json +4 -2
- package/scripts/build-shipped-history.mjs +131 -0
- package/scripts/lib/run-node-tests.mjs +235 -13
- package/scripts/lib/test-failure-records.mjs +5 -0
- package/scripts/lib/test-failure-summary.mjs +9 -3
- package/scripts/lib/test-spawn-ledger.mjs +19 -0
- package/scripts/prune-embedding-runtime.mjs +9 -5
- package/scripts/test-direct.mjs +16 -1
- package/src/agents/advisor/AGENT.md +38 -0
- package/src/agents/advisor/agent.json +6 -0
- package/src/agents/reviewer/AGENT.md +5 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +381 -0
- package/src/defaults/skills/goal-management/SKILL.md +11 -5
- package/src/defaults/skills/setup/SKILL.md +5 -8
- package/src/defaults/skills/setup/references/actions.md +61 -31
- package/src/defaults/skills/setup/references/surfaces.md +12 -10
- package/src/lib/rules-builder.cjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
- package/src/runtime/agent/orchestrator/config.mjs +102 -21
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
- package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
- package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
- package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
- package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
- package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
- package/src/runtime/channels/lib/config.mjs +3 -8
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
- package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
- package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
- package/src/runtime/computer-bridge/actions.mjs +2 -0
- package/src/runtime/effort-judge/judge-client.mjs +12 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
- package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
- package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
- package/src/runtime/memory/lib/memory-embed.mjs +5 -0
- package/src/runtime/shared/agent-route-config.mjs +50 -2
- package/src/runtime/shared/bounded-file-read.mjs +166 -0
- package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
- package/src/runtime/shared/config.mjs +24 -4
- package/src/runtime/shared/defaults-separation.mjs +263 -0
- package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
- package/src/runtime/shared/llm/model-catalog.mjs +23 -0
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
- package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
- package/src/runtime/shared/orchestration.mjs +14 -6
- package/src/runtime/shared/projects.mjs +65 -48
- package/src/runtime/shared/provider-accounts.mjs +47 -6
- package/src/runtime/shared/quota-resume-store.mjs +255 -0
- package/src/runtime/shared/shipped-definitions.mjs +275 -0
- package/src/runtime/shared/tool-card-model.golden.json +3 -3
- package/src/runtime/shared/tool-result-summary.mjs +86 -15
- package/src/runtime/shared/transcript-metadata.mjs +2 -0
- package/src/runtime/shared/turn-review-reply.mjs +2 -0
- package/src/runtime/shared/turn-snapshot.mjs +65 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
- package/src/session-runtime/boot/apis.mjs +19 -0
- package/src/session-runtime/boot/config.mjs +4 -0
- package/src/session-runtime/boot/facade.mjs +4 -1
- package/src/session-runtime/boot/infrastructure.mjs +21 -2
- package/src/session-runtime/boot/session.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
- package/src/session-runtime/config-lifecycle.mjs +31 -6
- package/src/session-runtime/goal-facade-api.mjs +4 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
- package/src/session-runtime/media-api.mjs +32 -22
- package/src/session-runtime/model-recency.mjs +14 -0
- package/src/session-runtime/model-route/route-tuning.mjs +3 -5
- package/src/session-runtime/model-route/set-route.mjs +49 -21
- package/src/session-runtime/model-route-api.mjs +2 -1
- package/src/session-runtime/notification-bus.mjs +31 -6
- package/src/session-runtime/provider-auth/login.mjs +6 -0
- package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
- package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
- package/src/session-runtime/resource-api.mjs +8 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +20 -1
- package/src/session-runtime/runtime-review-api.mjs +12 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +87 -13
- package/src/session-runtime/services/provider-admin.mjs +256 -63
- package/src/session-runtime/services/seeds.mjs +73 -142
- package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
- package/src/session-runtime/services/usage-dashboard.mjs +7 -0
- package/src/session-runtime/session-address.mjs +125 -0
- package/src/session-runtime/session-create.mjs +57 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
- package/src/session-runtime/settings-compaction-api.mjs +13 -0
- package/src/session-runtime/setup-tool/executor.mjs +92 -40
- package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
- package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
- package/src/session-runtime/tool-surface.mjs +16 -9
- package/src/session-runtime/turn/session-ops.mjs +3 -0
- package/src/session-runtime/turn/share-ops.mjs +4 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
- package/src/session-runtime/usage-stats-api.mjs +15 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
- package/src/standalone/daemon-boot-coordinator.mjs +10 -0
- package/src/standalone/daemon-drain-policy.mjs +9 -0
- package/src/standalone/daemon-stored-session-views.mjs +7 -8
- package/src/standalone/daemon.mjs +21 -5
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client.mjs +52 -7
- package/src/standalone/session-protocol.mjs +8 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
- package/src/standalone/session-service/entries/session-loads.mjs +9 -0
- package/src/standalone/session-service/projection/revisions.mjs +28 -4
- package/src/standalone/session-service/projection.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +140 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
- package/src/standalone/session-service/session-calls.mjs +10 -2
- package/src/standalone/session-service/stored-reader.mjs +26 -1
- package/src/standalone/session-service.mjs +21 -0
- package/src/standalone/session-transport/routes.mjs +2 -0
- package/src/tui/app/doctor.mjs +7 -11
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
- package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
- package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
- package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
- package/src/tui/components/ContextPanel.jsx +5 -1
- package/src/tui/components/Spinner.jsx +34 -319
- package/src/tui/components/SpinnerByline.jsx +256 -0
- package/src/tui/components/use-spinner-progress.mjs +113 -0
- package/src/tui/dist/index.mjs +15540 -14452
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
- package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
- package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
- package/src/tui/session/agent-job-feed.mjs +4 -0
- package/src/tui/session/context-state.mjs +13 -1
- package/src/tui/session/initial-state.mjs +6 -0
- package/src/tui/session/quota-resume.mjs +263 -0
- package/src/tui/session/session-api/intake/abort.mjs +39 -35
- package/src/tui/session/session-api/intake/submission.mjs +13 -21
- package/src/tui/session/session-api/intake.mjs +3 -3
- package/src/tui/session/session-api/integrations.mjs +5 -1
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
- package/src/tui/session/session-api/lifecycle.mjs +71 -2
- package/src/tui/session/session-api/media.mjs +2 -2
- package/src/tui/session/session-api/quota-resume.mjs +21 -0
- package/src/tui/session/session-api/routes.mjs +2 -0
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +14 -6
- package/src/tui/session/session-flow/drain.mjs +52 -5
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
- package/src/tui/session/session-flow/queue.mjs +3 -0
- package/src/tui/session/session-flow/steering.mjs +11 -0
- package/src/tui/session/session-flow.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +7 -2
- package/src/tui/session/transcript-intake.mjs +1 -0
- package/src/tui/session/turn.mjs +40 -3
- package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
// exactly — multiline patterns, type filters, a PCRE-only regex, a binary or
|
|
16
16
|
// oversized file — declines (returns null) and the caller keeps the original
|
|
17
17
|
// native error.
|
|
18
|
-
import {
|
|
18
|
+
import { PATCH_SNAPSHOT_MAX_BYTES, readBoundedFile } from '../../../../../shared/bounded-file-read.mjs';
|
|
19
19
|
|
|
20
20
|
import { formatGrepContextOutput, formatGrepOutput, grepNoMatchesBody } from './grep-output.mjs';
|
|
21
21
|
|
|
22
22
|
/** Reading is the whole point of the rescue, so the file must stay small
|
|
23
|
-
* enough that reading it is cheaper than the failed search.
|
|
24
|
-
|
|
23
|
+
* enough that reading it is cheaper than the failed search. The read is
|
|
24
|
+
* bounded: an oversized (or growing) file is declined without being read. */
|
|
25
|
+
export const MAX_RESCUE_BYTES = PATCH_SNAPSHOT_MAX_BYTES;
|
|
25
26
|
/** Bounds memory on a pathological match rate; a truncated window is reported
|
|
26
27
|
* as incomplete, exactly like a capped native result. */
|
|
27
28
|
const MAX_RESCUE_LINES = 5_000;
|
|
@@ -69,11 +70,13 @@ export async function runGrepSingleFileRescue({
|
|
|
69
70
|
|
|
70
71
|
let raw;
|
|
71
72
|
try {
|
|
72
|
-
|
|
73
|
+
const read = readBoundedFile(filePath, { maxBytes: MAX_RESCUE_BYTES });
|
|
74
|
+
if (read.state !== 'present') return null;
|
|
75
|
+
raw = read.content;
|
|
73
76
|
} catch {
|
|
74
77
|
return null;
|
|
75
78
|
}
|
|
76
|
-
if (raw.
|
|
79
|
+
if (raw.includes(0)) return null;
|
|
77
80
|
const text = raw.toString('utf8');
|
|
78
81
|
const fileLines = text.split(/\r?\n/);
|
|
79
82
|
if (fileLines.length > 0 && fileLines[fileLines.length - 1] === '') fileLines.pop();
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
-
import { StringDecoder } from 'node:string_decoder';
|
|
3
2
|
import { stripAnsi } from '../../shell-exec-output.mjs';
|
|
3
|
+
import { FileEncodingSniffer, ShellStreamDecoder, alignedOffset, oemTextEncoding } from '../../shell-codepage.mjs';
|
|
4
4
|
|
|
5
5
|
const PAGE_BYTES = 8 * 1024;
|
|
6
6
|
const COMPLETION_BYTES = 12 * 1024;
|
|
7
7
|
const cursors = new WeakMap();
|
|
8
8
|
|
|
9
9
|
function newCursor() {
|
|
10
|
-
return {
|
|
10
|
+
return {
|
|
11
|
+
offset: 0,
|
|
12
|
+
preview: '',
|
|
13
|
+
decoder: new ShellStreamDecoder(),
|
|
14
|
+
ansi: '',
|
|
15
|
+
sniffer: new FileEncodingSniffer(),
|
|
16
|
+
committed: false,
|
|
17
|
+
};
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
// Only the introducer is retained, plus the ESC that may open a split OSC
|
|
@@ -85,20 +92,26 @@ export function readShellTaskOutput(task, result = {}, { output = 'new' } = {})
|
|
|
85
92
|
cursor = newCursor();
|
|
86
93
|
streams.set(key, cursor);
|
|
87
94
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
// Raw capture files carry the child's bytes. The stream's encoding is
|
|
96
|
+
// decided once, firmly (or when the task is terminal); until then only
|
|
97
|
+
// the ASCII prefix is read.
|
|
98
|
+
const label = cursor.sniffer.probe(fd, size, terminal);
|
|
99
|
+
if (label && !cursor.committed) {
|
|
100
|
+
cursor.committed = true;
|
|
101
|
+
cursor.decoder.force(label);
|
|
102
|
+
}
|
|
103
|
+
const readEnd = label ? size : cursor.sniffer.safeEnd(size);
|
|
104
|
+
let start = terminal || replay ? Math.max(cursor.offset, readEnd - limit) : cursor.offset;
|
|
105
|
+
if (start > cursor.offset) {
|
|
106
|
+
start = Math.min(readEnd, alignedOffset(fd, start, label || 'utf-8'));
|
|
107
|
+
sections.push(`[${stream}: ${start - cursor.offset} earlier bytes omitted; read ${path} for the original log]`);
|
|
108
|
+
cursor.decoder = new ShellStreamDecoder(oemTextEncoding(), label);
|
|
92
109
|
cursor.ansi = '';
|
|
93
110
|
}
|
|
94
|
-
const buffer = Buffer.alloc(Math.min(limit, Math.max(0,
|
|
111
|
+
const buffer = Buffer.alloc(Math.min(limit, Math.max(0, readEnd - start)));
|
|
95
112
|
const count = buffer.length ? readSync(fd, buffer, 0, buffer.length, start) : 0;
|
|
96
|
-
const prefix = skipped ? utf8Boundary(buffer, 0, count) : 0;
|
|
97
|
-
if (skipped + prefix) {
|
|
98
|
-
sections.push(`[${stream}: ${skipped + prefix} earlier bytes omitted; read ${path} for the original log]`);
|
|
99
|
-
}
|
|
100
113
|
cursor.offset = start + count;
|
|
101
|
-
let decoded = cursor.decoder.write(buffer.subarray(
|
|
114
|
+
let decoded = cursor.decoder.write(buffer.subarray(0, count));
|
|
102
115
|
if (terminal && cursor.offset === size) decoded += cursor.decoder.end();
|
|
103
116
|
const text = visibleText(cursor, decoded);
|
|
104
117
|
if (text) sections.push(`[${stream}]\n${text}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { rawContentCacheSet } from './cache-layers.mjs';
|
|
3
|
+
import { sameFileVersion } from './file-version.mjs';
|
|
4
|
+
|
|
5
|
+
// `before` is an owned version captured BEFORE the fresh read. A failed
|
|
6
|
+
// post-read stat is uncertainty, never evidence that the bytes are stable.
|
|
7
|
+
// Keep the current invocation's observation; do not retry or relabel it.
|
|
8
|
+
export function publishRawContentAfterRead(fullPath, before, rawBuf, readStat = statSync) {
|
|
9
|
+
if (!before || !Buffer.isBuffer(rawBuf) || rawBuf.length !== before.size) return;
|
|
10
|
+
let after;
|
|
11
|
+
try {
|
|
12
|
+
after = readStat(fullPath);
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (sameFileVersion(before, after)) rawContentCacheSet(fullPath, before, rawBuf);
|
|
17
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { fileVersion, sameFileVersion } from './file-version.mjs';
|
|
2
4
|
import { hashText as sha256Hex } from './hash-utils.mjs';
|
|
3
5
|
import { join, normalize, resolve } from 'node:path';
|
|
4
6
|
import { getPluginData } from '../../config.mjs';
|
|
@@ -11,7 +13,10 @@ const READ_RANGE_INDEX_MAX_PERSISTED_ANCHORS = 8192;
|
|
|
11
13
|
const READ_RANGE_INDEX_PERSIST_DEBOUNCE_MS = 750;
|
|
12
14
|
const READ_RANGE_INDEX_SWEEP_CONCURRENCY = 8;
|
|
13
15
|
|
|
16
|
+
// Slots own publication rights; readers only mutate private drafts. Deleting
|
|
17
|
+
// or replacing a slot detaches outstanding lookups/scans without a tombstone log.
|
|
14
18
|
const READ_RANGE_INDEX_CACHE = new Map();
|
|
19
|
+
const READ_RANGE_INDEX_DRAFTS = new WeakMap();
|
|
15
20
|
const READ_RANGE_INDEX_PERSIST_PENDING = new Map();
|
|
16
21
|
let readRangeIndexDiskSwept = false;
|
|
17
22
|
|
|
@@ -43,7 +48,7 @@ function readRangeIndexFilePath(fullPath) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
function serialiseReadRangeIndex(index) {
|
|
46
|
-
if (!index?.
|
|
51
|
+
if (!index?.published || !sameFileVersion(index.fileVersion, index.fileVersion)) return null;
|
|
47
52
|
const anchors = [...index.anchors.entries()]
|
|
48
53
|
.filter(
|
|
49
54
|
([line, byteOffset]) => Number.isFinite(line) && Number.isFinite(byteOffset) && line >= 0 && byteOffset >= 0
|
|
@@ -52,25 +57,22 @@ function serialiseReadRangeIndex(index) {
|
|
|
52
57
|
.slice(-READ_RANGE_INDEX_MAX_PERSISTED_ANCHORS);
|
|
53
58
|
if (!anchors.some(([line]) => line === 0)) anchors.unshift([0, 0]);
|
|
54
59
|
return {
|
|
55
|
-
version:
|
|
60
|
+
version: 2,
|
|
56
61
|
path: index.fullPath,
|
|
57
|
-
|
|
58
|
-
mtimeMs: index.mtimeMs,
|
|
62
|
+
fileVersion: index.fileVersion,
|
|
59
63
|
prefixHash: index.prefixHash || '',
|
|
60
64
|
totalLines: Number.isFinite(index.totalLines) ? index.totalLines : undefined,
|
|
61
65
|
anchors,
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
function readRangeIndexMatches(row, fullPath,
|
|
69
|
+
function readRangeIndexMatches(row, fullPath, version) {
|
|
70
|
+
// Version-1 rows lack replacement identity. Never upgrade their anchors by
|
|
71
|
+
// attaching today's metadata; rebuild them from current bytes instead.
|
|
66
72
|
return (
|
|
67
|
-
row &&
|
|
68
|
-
row.version === 1 &&
|
|
73
|
+
row?.version === 2 &&
|
|
69
74
|
row.path === fullPath &&
|
|
70
|
-
|
|
71
|
-
row.size === st.size &&
|
|
72
|
-
Number.isFinite(row.mtimeMs) &&
|
|
73
|
-
Math.abs(row.mtimeMs - st.mtimeMs) <= 1 &&
|
|
75
|
+
sameFileVersion(row.fileVersion, version) &&
|
|
74
76
|
Array.isArray(row.anchors)
|
|
75
77
|
);
|
|
76
78
|
}
|
|
@@ -123,21 +125,10 @@ async function loadReadRangeIndexFromDisk(fullPath, st, handle, prefixBuffer) {
|
|
|
123
125
|
try {
|
|
124
126
|
const row = JSON.parse(await readFile(file, 'utf-8'));
|
|
125
127
|
if (!readRangeIndexMatches(row, fullPath, st)) return null;
|
|
126
|
-
//
|
|
127
|
-
// rewrite (touch-restore, in-place edit of equal-length content)
|
|
128
|
-
// leaves the stat gate above happy but invalidates every persisted
|
|
129
|
-
// byte offset, causing seeks to land on the wrong line. When the
|
|
130
|
-
// persisted index carries a prefixHash, the current file's prefix
|
|
131
|
-
// must match; on mismatch drop the on-disk row so the caller
|
|
132
|
-
// rebuilds from scratch.
|
|
128
|
+
// The prefix is an extra check, not proof that later byte offsets survived.
|
|
133
129
|
if (typeof row.prefixHash === 'string' && row.prefixHash) {
|
|
134
130
|
const cur = await computePrefixHashForIndex(fullPath, st, handle, prefixBuffer);
|
|
135
|
-
if (!cur || cur !== row.prefixHash)
|
|
136
|
-
try {
|
|
137
|
-
await unlink(file);
|
|
138
|
-
} catch {}
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
131
|
+
if (!cur || cur !== row.prefixHash) return null;
|
|
141
132
|
}
|
|
142
133
|
const anchors = new Map([[0, 0]]);
|
|
143
134
|
for (const entry of row.anchors) {
|
|
@@ -149,8 +140,7 @@ async function loadReadRangeIndexFromDisk(fullPath, st, handle, prefixBuffer) {
|
|
|
149
140
|
}
|
|
150
141
|
const loaded = {
|
|
151
142
|
fullPath,
|
|
152
|
-
|
|
153
|
-
mtimeMs: st.mtimeMs,
|
|
143
|
+
fileVersion: fileVersion(st),
|
|
154
144
|
prefixHash: typeof row.prefixHash === 'string' ? row.prefixHash : '',
|
|
155
145
|
anchors,
|
|
156
146
|
};
|
|
@@ -166,36 +156,41 @@ async function loadReadRangeIndexFromDisk(fullPath, st, handle, prefixBuffer) {
|
|
|
166
156
|
}
|
|
167
157
|
}
|
|
168
158
|
|
|
159
|
+
function currentReadRangeIndex(index) {
|
|
160
|
+
return !!index?.published && READ_RANGE_INDEX_CACHE.get(canonicalCachePath(index.fullPath)) === index;
|
|
161
|
+
}
|
|
162
|
+
|
|
169
163
|
function persistReadRangeIndexSync(index) {
|
|
170
164
|
const file = readRangeIndexFilePath(index?.fullPath);
|
|
171
|
-
if (!file) return;
|
|
172
|
-
const row = serialiseReadRangeIndex(index);
|
|
173
|
-
if (!row) return;
|
|
165
|
+
if (!file || !currentReadRangeIndex(index)) return;
|
|
174
166
|
try {
|
|
175
|
-
|
|
167
|
+
if (!sameFileVersion(index.fileVersion, statSync(index.fullPath))) return;
|
|
168
|
+
const row = serialiseReadRangeIndex(index);
|
|
169
|
+
if (row) writeJsonAtomicSync(file, row, { compact: true, lock: true, fsync: false });
|
|
176
170
|
} catch {}
|
|
177
171
|
}
|
|
178
172
|
|
|
179
|
-
async function persistReadRangeIndex(index) {
|
|
173
|
+
async function persistReadRangeIndex(index, evictedRow = null) {
|
|
180
174
|
const file = readRangeIndexFilePath(index?.fullPath);
|
|
181
|
-
if (!file) return;
|
|
182
|
-
const row = serialiseReadRangeIndex(index);
|
|
183
|
-
if (!row) return;
|
|
175
|
+
if (!file || (!evictedRow && !currentReadRangeIndex(index))) return;
|
|
184
176
|
try {
|
|
177
|
+
if (!sameFileVersion(evictedRow?.fileVersion || index.fileVersion, await stat(index.fullPath))) return;
|
|
185
178
|
await mkdir(READ_RANGE_INDEX_DISK_DIR, { recursive: true });
|
|
186
|
-
|
|
179
|
+
if (!evictedRow && !currentReadRangeIndex(index)) return;
|
|
180
|
+
const row = evictedRow || serialiseReadRangeIndex(index);
|
|
181
|
+
if (row) await writeJsonAtomicAsync(file, row, { compact: true, lock: true, fsync: false });
|
|
187
182
|
} catch {}
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
export function scheduleReadRangeIndexPersist(index) {
|
|
191
|
-
if (!READ_RANGE_INDEX_DISK_DIR || !index
|
|
186
|
+
if (!READ_RANGE_INDEX_DISK_DIR || !currentReadRangeIndex(index)) return;
|
|
192
187
|
const key = canonicalCachePath(index.fullPath);
|
|
193
188
|
if (READ_RANGE_INDEX_PERSIST_PENDING.has(key)) return;
|
|
194
189
|
const t = setTimeout(() => {
|
|
195
190
|
READ_RANGE_INDEX_PERSIST_PENDING.delete(key);
|
|
196
191
|
void persistReadRangeIndex(index);
|
|
197
192
|
}, READ_RANGE_INDEX_PERSIST_DEBOUNCE_MS);
|
|
198
|
-
|
|
193
|
+
t.unref();
|
|
199
194
|
READ_RANGE_INDEX_PERSIST_PENDING.set(key, t);
|
|
200
195
|
}
|
|
201
196
|
|
|
@@ -210,16 +205,14 @@ export function flushReadRangeIndexesSync() {
|
|
|
210
205
|
}
|
|
211
206
|
}
|
|
212
207
|
|
|
213
|
-
|
|
214
|
-
const key = canonicalCachePath(fullPath);
|
|
208
|
+
function dropReadRangeIndexSlot(key) {
|
|
215
209
|
READ_RANGE_INDEX_CACHE.delete(key);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
210
|
+
clearTimeout(READ_RANGE_INDEX_PERSIST_PENDING.get(key));
|
|
211
|
+
READ_RANGE_INDEX_PERSIST_PENDING.delete(key);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function deleteReadRangeIndexForPath(fullPath) {
|
|
215
|
+
dropReadRangeIndexSlot(canonicalCachePath(fullPath));
|
|
223
216
|
const file = readRangeIndexFilePath(fullPath);
|
|
224
217
|
if (file) void unlink(file).catch(() => {});
|
|
225
218
|
}
|
|
@@ -248,46 +241,114 @@ export async function sweepStaleReadRangeIndexes(directory = READ_RANGE_INDEX_DI
|
|
|
248
241
|
await Promise.all(Array.from({ length: Math.min(READ_RANGE_INDEX_SWEEP_CONCURRENCY, entries.length) }, worker));
|
|
249
242
|
}
|
|
250
243
|
|
|
251
|
-
// Re-insert so the entry moves to the LRU tail
|
|
244
|
+
// Re-insert so the entry moves to the LRU tail. All insertion paths obey the
|
|
245
|
+
// same cap, including disk hits. Eviction also revokes unfinished readers.
|
|
252
246
|
function refreshReadRangeIndexSlot(key, index) {
|
|
253
247
|
READ_RANGE_INDEX_CACHE.delete(key);
|
|
254
248
|
READ_RANGE_INDEX_CACHE.set(key, index);
|
|
249
|
+
while (READ_RANGE_INDEX_CACHE.size > READ_RANGE_INDEX_MAX_ENTRIES) {
|
|
250
|
+
const oldest = READ_RANGE_INDEX_CACHE.keys().next().value;
|
|
251
|
+
const evicted = READ_RANGE_INDEX_CACHE.get(oldest);
|
|
252
|
+
// Preserve only committed anchors before cancelling the debounce. The
|
|
253
|
+
// owned row may finish saving after eviction, but outstanding drafts lose
|
|
254
|
+
// their slot immediately. Disk IO must not delay the foreground lookup.
|
|
255
|
+
const row = serialiseReadRangeIndex(evicted);
|
|
256
|
+
dropReadRangeIndexSlot(oldest);
|
|
257
|
+
if (row) void persistReadRangeIndex(evicted, row);
|
|
258
|
+
}
|
|
255
259
|
return index;
|
|
256
260
|
}
|
|
257
261
|
|
|
258
|
-
|
|
262
|
+
function freshReadRangeIndex(fullPath, version = null) {
|
|
263
|
+
return { fullPath, fileVersion: version, prefixHash: '', anchors: new Map([[0, 0]]), published: false };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// A supplied handle is borrowed; validation uses positional IO and never closes
|
|
267
|
+
// it. The stat argument is only a hint for a previously read prefix, not proof
|
|
268
|
+
// of the current handle/path version. A cache miss always starts at byte zero.
|
|
259
269
|
export async function getReadRangeIndex(fullPath, st, handle = null, prefixBuffer = null) {
|
|
260
|
-
if (!st) return null;
|
|
261
270
|
const key = canonicalCachePath(fullPath);
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const cur = await computePrefixHashForIndex(fullPath, st, handle, prefixBuffer);
|
|
274
|
-
if (cur && cur === cached.prefixHash) return refreshReadRangeIndexSlot(key, cached);
|
|
275
|
-
READ_RANGE_INDEX_CACHE.delete(key);
|
|
271
|
+
let slot = READ_RANGE_INDEX_CACHE.get(key);
|
|
272
|
+
if (!slot) slot = refreshReadRangeIndexSlot(key, freshReadRangeIndex(fullPath));
|
|
273
|
+
const hint = fileVersion(st);
|
|
274
|
+
const readStat = () => (handle ? handle.stat() : stat(fullPath));
|
|
275
|
+
let version;
|
|
276
|
+
try {
|
|
277
|
+
const observed = await readStat();
|
|
278
|
+
if (!observed.isFile()) return null;
|
|
279
|
+
version = fileVersion(observed);
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
276
282
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
283
|
+
if (!version || READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
|
|
284
|
+
version = Object.freeze(version);
|
|
285
|
+
if (!slot.fileVersion) slot.fileVersion = version;
|
|
286
|
+
if (!sameFileVersion(slot.fileVersion, version)) {
|
|
287
|
+
dropReadRangeIndexSlot(key);
|
|
288
|
+
slot = refreshReadRangeIndexSlot(key, freshReadRangeIndex(fullPath, version));
|
|
281
289
|
}
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
290
|
+
const prefix = sameFileVersion(hint, version) ? prefixBuffer : null;
|
|
291
|
+
let source = slot;
|
|
292
|
+
if (slot.published && slot.prefixHash) {
|
|
293
|
+
const cur = await computePrefixHashForIndex(fullPath, version, handle, prefix);
|
|
294
|
+
if (READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
|
|
295
|
+
if (!cur || cur !== slot.prefixHash) {
|
|
296
|
+
dropReadRangeIndexSlot(key);
|
|
297
|
+
slot = refreshReadRangeIndexSlot(key, freshReadRangeIndex(fullPath, version));
|
|
298
|
+
source = slot;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!slot.published) source = (await loadReadRangeIndexFromDisk(fullPath, version, handle, prefix)) || slot;
|
|
302
|
+
// Neither a delayed disk lookup nor a changed source can authorize offsets.
|
|
303
|
+
try {
|
|
304
|
+
if (!sameFileVersion(version, await readStat())) return null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (READ_RANGE_INDEX_CACHE.get(key) !== slot) return null;
|
|
309
|
+
refreshReadRangeIndexSlot(key, slot);
|
|
310
|
+
const draft = {
|
|
311
|
+
fullPath,
|
|
312
|
+
fileVersion: version,
|
|
313
|
+
prefixHash: source.prefixHash,
|
|
314
|
+
anchors: new Map(source.anchors),
|
|
315
|
+
...(Number.isFinite(source.totalLines) ? { totalLines: source.totalLines } : {}),
|
|
316
|
+
};
|
|
317
|
+
READ_RANGE_INDEX_DRAFTS.set(draft, { slot, version });
|
|
318
|
+
return draft;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Successful readers publish once, before closing their handle. A failed,
|
|
322
|
+
// cancelled or detached scan keeps its observation private. Post-stat failures
|
|
323
|
+
// are cache misses, not reasons to retry the operation or relabel its anchors.
|
|
324
|
+
export async function publishReadRangeIndex(index, handle, signal = null, beforePublish = null) {
|
|
325
|
+
const owned = READ_RANGE_INDEX_DRAFTS.get(index);
|
|
326
|
+
if (!owned) return false;
|
|
327
|
+
READ_RANGE_INDEX_DRAFTS.delete(index);
|
|
328
|
+
const { slot, version } = owned;
|
|
329
|
+
const key = canonicalCachePath(slot.fullPath);
|
|
330
|
+
if (signal?.aborted || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
|
|
331
|
+
try {
|
|
332
|
+
if (!sameFileVersion(version, await handle.stat()) || !sameFileVersion(version, await stat(slot.fullPath)))
|
|
333
|
+
return false;
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
289
336
|
}
|
|
290
|
-
return
|
|
337
|
+
if (signal?.aborted || READ_RANGE_INDEX_CACHE.get(key) !== slot) return false;
|
|
338
|
+
if (slot.prefixHash && index.prefixHash && slot.prefixHash !== index.prefixHash) return false;
|
|
339
|
+
// The reader's synchronous deadline/cancellation decision belongs after all
|
|
340
|
+
// awaited validation and outside its best-effort stat catch. Once committed,
|
|
341
|
+
// the caller must not make a second deadline decision for this scan.
|
|
342
|
+
beforePublish?.();
|
|
343
|
+
// Same-version concurrent windows may contribute disjoint anchors. No draft
|
|
344
|
+
// shares this map, including the one whose successful read is being committed.
|
|
345
|
+
slot.anchors = new Map([...slot.anchors, ...index.anchors]);
|
|
346
|
+
slot.prefixHash = index.prefixHash || slot.prefixHash;
|
|
347
|
+
if (Number.isFinite(index.totalLines)) slot.totalLines = index.totalLines;
|
|
348
|
+
slot.published = true;
|
|
349
|
+
refreshReadRangeIndexSlot(key, slot);
|
|
350
|
+
scheduleReadRangeIndexPersist(slot);
|
|
351
|
+
return true;
|
|
291
352
|
}
|
|
292
353
|
|
|
293
354
|
export function nearestReadRangeAnchor(index, offset) {
|
|
@@ -309,6 +370,5 @@ export function maybeRecordReadRangeAnchor(index, line, byteOffset) {
|
|
|
309
370
|
if (line === 0 || line % READ_RANGE_INDEX_STRIDE_LINES === 0) {
|
|
310
371
|
if (index.anchors.get(line) === byteOffset) return;
|
|
311
372
|
index.anchors.set(line, byteOffset);
|
|
312
|
-
scheduleReadRangeIndexPersist(index);
|
|
313
373
|
}
|
|
314
374
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fsPromises from 'node:fs/promises';
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { sameFileVersion } from './file-version.mjs';
|
|
9
10
|
import { READ_PREFIX_HASH_BYTES } from './read-single-fast-paths.mjs';
|
|
10
11
|
import { decodeUtf16Body, isUtf16Encoding } from './snapshot-helpers.mjs';
|
|
11
12
|
|
|
@@ -174,27 +175,24 @@ export async function bufferedReadResult(ctx, { prefetched, readEnc }, helpers)
|
|
|
174
175
|
const { _cacheSet, _rawContentCacheSet, _recordReadSnapshot } = helpers;
|
|
175
176
|
const rawBuf = prefetched.buf || (await readFile(fullPath));
|
|
176
177
|
const content = decodeReadBuffer(rawBuf, readEnc);
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// bytes
|
|
180
|
-
|
|
181
|
-
let
|
|
178
|
+
// A cache hit was checked against this exact version. Fresh IO must
|
|
179
|
+
// also agree after reading, including ctime and replacement identity.
|
|
180
|
+
// Never relabel observed bytes with a later stat or certify a failed stat.
|
|
181
|
+
const st = ctx.st;
|
|
182
|
+
let readStable = sameFileVersion(st, st) && rawBuf.length === st.size;
|
|
182
183
|
if (!prefetched.fromCache) {
|
|
183
|
-
let stPostRead;
|
|
184
184
|
try {
|
|
185
|
-
|
|
185
|
+
readStable = readStable && sameFileVersion(st, await fsPromises.stat(fullPath));
|
|
186
186
|
} catch {
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
if (stPostRead.mtimeMs !== st.mtimeMs || stPostRead.size !== st.size) {
|
|
190
|
-
st = stPostRead;
|
|
191
|
-
readStableForRawCache = false;
|
|
187
|
+
readStable = false;
|
|
192
188
|
}
|
|
193
189
|
}
|
|
194
|
-
const render = renderReadWindow(content,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
const render = renderReadWindow(content, ctx, helpers);
|
|
191
|
+
if (readStable) {
|
|
192
|
+
const { snapshotMeta, contentPrefixHash } = bufferedReadSnapshotMeta(content, render, ctx, helpers);
|
|
193
|
+
_cacheSet(cacheKey, render.out, { paths: [fullPath], readSnapshotMeta: snapshotMeta, contentPrefixHash });
|
|
194
|
+
_rawContentCacheSet(fullPath, st, rawBuf);
|
|
195
|
+
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
196
|
+
}
|
|
199
197
|
return render.out;
|
|
200
198
|
}
|
|
@@ -23,13 +23,15 @@ import { binaryPreviewResult, smartStreamResult, streamRangeResult } from './rea
|
|
|
23
23
|
import { bufferedReadResult } from './read-single-render.mjs';
|
|
24
24
|
|
|
25
25
|
// Buffered reads take the whole body up front: the raw-content cache first,
|
|
26
|
-
// else one readFile shared with
|
|
26
|
+
// else one readFile shared with in-flight reads of the same path AND version.
|
|
27
27
|
async function prefetchRawBody(fullPath, st, helpers) {
|
|
28
28
|
const { _rawContentCacheGet, _runRawContentInFlight } = helpers;
|
|
29
29
|
const cachedRaw = _rawContentCacheGet ? _rawContentCacheGet(fullPath, st) : null;
|
|
30
30
|
if (cachedRaw) return { buf: cachedRaw, fromCache: true };
|
|
31
31
|
try {
|
|
32
|
-
const buf = _runRawContentInFlight
|
|
32
|
+
const buf = _runRawContentInFlight
|
|
33
|
+
? await _runRawContentInFlight(fullPath, readFile, st)
|
|
34
|
+
: await readFile(fullPath);
|
|
33
35
|
return { buf, fromCache: false };
|
|
34
36
|
} catch {
|
|
35
37
|
return { buf: null, fromCache: false };
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
statMatchesSnapshot,
|
|
9
9
|
decodeRawBufferForSnapshotCheck,
|
|
10
10
|
} from './snapshot-helpers.mjs';
|
|
11
|
-
import { rawContentCacheGet
|
|
11
|
+
import { rawContentCacheGet } from './cache-layers.mjs';
|
|
12
|
+
import { fileVersion } from './file-version.mjs';
|
|
13
|
+
import { publishRawContentAfterRead } from './raw-content-publication.mjs';
|
|
12
14
|
import { rememberReadSnapshot, readFilesForScope, readScopeKey, scheduleScopePersist } from './snapshot-store.mjs';
|
|
13
15
|
import {
|
|
14
16
|
isSnapshotStale as isSnapshotStaleImpl,
|
|
@@ -38,25 +40,24 @@ export function readTextForSnapshotCheck(fullPath, cache = null, st = null) {
|
|
|
38
40
|
return cache.content;
|
|
39
41
|
}
|
|
40
42
|
const cachedRaw = getCachedRaw();
|
|
43
|
+
const before = fileVersion(statForRawCache);
|
|
41
44
|
const rawBuf = cachedRaw || readFileSync(fullPath);
|
|
42
45
|
const content = decodeRawBufferForSnapshotCheck(rawBuf);
|
|
43
46
|
if (cache) {
|
|
44
47
|
cache.rawBuf = rawBuf;
|
|
45
48
|
cache.content = content;
|
|
46
49
|
}
|
|
47
|
-
if (!cachedRaw
|
|
50
|
+
if (!cachedRaw) publishRawContentAfterRead(fullPath, before, rawBuf);
|
|
48
51
|
return content;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
54
|
+
// Own the observed version. Supplied incomplete evidence must not be
|
|
55
|
+
// replaced by a later stat, which could relabel previously observed bytes.
|
|
53
56
|
function snapshotStatIdentity(fullPath, st) {
|
|
54
57
|
try {
|
|
55
|
-
|
|
56
|
-
return { mtimeMs: source.mtimeMs, ctimeMs: source.ctimeMs, size: source.size };
|
|
58
|
+
return fileVersion(st ?? statSync(fullPath));
|
|
57
59
|
} catch {
|
|
58
|
-
|
|
59
|
-
return { mtimeMs: now, ctimeMs: now, size: 0 };
|
|
60
|
+
return null;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -119,7 +120,7 @@ function collectRangeHashRows(existing, sameFile, meta) {
|
|
|
119
120
|
export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
|
|
120
121
|
const readFiles = readFilesForScope(scope);
|
|
121
122
|
const identity = snapshotStatIdentity(fullPath, st);
|
|
122
|
-
const { mtimeMs, ctimeMs, size } = identity;
|
|
123
|
+
const { mtimeMs, ctimeMs, size, ino, dev } = identity || {};
|
|
123
124
|
const incomingRanges = Array.isArray(meta.ranges) ? meta.ranges : [{ startLine: 1, endLine: Infinity }];
|
|
124
125
|
const replaceExisting = meta.replaceExisting === true;
|
|
125
126
|
const existing = replaceExisting ? null : readFiles.get(fullPath);
|
|
@@ -146,7 +147,7 @@ export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
|
|
|
146
147
|
preMutationStat: _omitPreMutationStat,
|
|
147
148
|
...restMeta
|
|
148
149
|
} = meta;
|
|
149
|
-
const next = { ...restMeta, mtimeMs, ctimeMs, size, ranges: merged };
|
|
150
|
+
const next = { ...restMeta, mtimeMs, ctimeMs, size, ino, dev, ranges: merged };
|
|
150
151
|
if (!next.contentHash && sameFile && existing.contentHash) {
|
|
151
152
|
next.contentHash = existing.contentHash;
|
|
152
153
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getReadRangeIndex,
|
|
7
7
|
nearestReadRangeAnchor,
|
|
8
8
|
maybeRecordReadRangeAnchor,
|
|
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
12
|
|
|
@@ -74,9 +74,8 @@ async function scanSourceWindows(path, intervals, signal) {
|
|
|
74
74
|
}
|
|
75
75
|
scanned += bytesRead;
|
|
76
76
|
if (scanned > READ_MAX_SCAN_BYTES) throw new Error('source context scan exceeds byte budget');
|
|
77
|
-
if (position === 0 && !index.prefixHash) {
|
|
77
|
+
if (position === 0 && index && !index.prefixHash) {
|
|
78
78
|
index.prefixHash = hashText(buffer.subarray(0, bytesRead));
|
|
79
|
-
scheduleReadRangeIndexPersist(index);
|
|
80
79
|
}
|
|
81
80
|
let start = 0;
|
|
82
81
|
while (start < bytesRead && interval < intervals.length) {
|
|
@@ -93,6 +92,9 @@ async function scanSourceWindows(path, intervals, signal) {
|
|
|
93
92
|
}
|
|
94
93
|
position += bytesRead;
|
|
95
94
|
}
|
|
95
|
+
// Publication checks after its async stats, immediately before committing.
|
|
96
|
+
// An unpublished result still owes a final deadline/cancellation check.
|
|
97
|
+
if (!(await publishReadRangeIndex(index, handle, signal, check))) check();
|
|
96
98
|
return lines;
|
|
97
99
|
} finally {
|
|
98
100
|
await handle.close();
|