mixdog 0.9.93 → 0.9.95
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/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -29
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +23 -7
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +103 -92
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +546 -23
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +104 -14
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +151 -18
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +16 -6
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +5 -0
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +22 -1
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -86
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1537 -9718
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +97 -4
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +88 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +84 -4
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1057
- package/src/tui/index.jsx +47 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { normalizeOutputPath } from '../path-utils.mjs';
|
|
6
6
|
import {
|
|
7
7
|
groupGrepContentByFile,
|
|
8
|
+
parseGrepContextHeader,
|
|
8
9
|
splitGrepLineNumberOnlyPrefix,
|
|
9
10
|
splitGrepLinePrefix,
|
|
10
11
|
} from '../grep-formatting.mjs';
|
|
12
|
+
import { relativePathPrefix } from '../search-path-diagnostics.mjs';
|
|
11
13
|
import { relativeGrepLine } from './search-input-helpers.mjs';
|
|
12
14
|
|
|
13
15
|
export function grepMissingPatternMessage() {
|
|
@@ -22,30 +24,84 @@ export function globMissingPatternMessage() {
|
|
|
22
24
|
// In context mode (explicit -A/-B/-C or content_with_context auto), head_limit
|
|
23
25
|
// and offset count MATCH BLOCKS, not raw output lines, and truncation keeps a
|
|
24
26
|
// head+tail slice with a middle marker instead of dropping the tail.
|
|
25
|
-
function
|
|
27
|
+
function grepContextSourceLine(line, filenameOmitted, fallbackPath = '') {
|
|
26
28
|
if (filenameOmitted) {
|
|
27
29
|
const p = splitGrepLineNumberOnlyPrefix(line);
|
|
28
|
-
return p
|
|
30
|
+
return p ? {
|
|
31
|
+
path: fallbackPath,
|
|
32
|
+
lineNo: p.lineNo,
|
|
33
|
+
delimiter: p.delimiter,
|
|
34
|
+
content: String(line).slice(p.markerEnd),
|
|
35
|
+
} : null;
|
|
29
36
|
}
|
|
30
37
|
const s = splitGrepLinePrefix(line);
|
|
31
|
-
return s
|
|
38
|
+
return s ? {
|
|
39
|
+
path: s.path,
|
|
40
|
+
lineNo: s.lineNo,
|
|
41
|
+
delimiter: s.delimiter,
|
|
42
|
+
content: String(line).slice(s.markerEnd),
|
|
43
|
+
} : null;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
function
|
|
46
|
+
function grepBlockMatchAnchor(line, filenameOmitted, fallbackPath) {
|
|
47
|
+
const parsed = grepContextSourceLine(line, filenameOmitted, fallbackPath);
|
|
48
|
+
return parsed && parsed.delimiter === ':' ? `${parsed.path}\0${parsed.lineNo}` : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseRenderableGrepContextBlock(block, filenameOmitted, fallbackPath) {
|
|
52
|
+
const parsed = block.lines.map((line) => grepContextSourceLine(line, filenameOmitted, fallbackPath));
|
|
53
|
+
const contiguous = parsed.length > 0
|
|
54
|
+
&& parsed.every((entry, index) => entry
|
|
55
|
+
&& entry.path
|
|
56
|
+
&& entry.path === parsed[0].path
|
|
57
|
+
&& (index === 0 || entry.lineNo === parsed[index - 1].lineNo + 1));
|
|
58
|
+
const match = contiguous ? parsed.find((entry) => entry.delimiter === ':') : null;
|
|
59
|
+
if (!match) return { raw: block.lines.join('\n') };
|
|
60
|
+
return {
|
|
61
|
+
path: match.path,
|
|
62
|
+
matchLine: match.lineNo,
|
|
63
|
+
startLine: parsed[0].lineNo,
|
|
64
|
+
endLine: parsed.at(-1).lineNo,
|
|
65
|
+
contents: parsed.map((entry) => entry.content),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderGrepContextBlocks(blocks, filenameOmitted, fallbackPath) {
|
|
70
|
+
const merged = [];
|
|
71
|
+
for (const block of blocks) {
|
|
72
|
+
const current = parseRenderableGrepContextBlock(block, filenameOmitted, fallbackPath);
|
|
73
|
+
const previous = merged.at(-1);
|
|
74
|
+
if (previous?.path
|
|
75
|
+
&& current.path === previous.path
|
|
76
|
+
&& current.startLine <= previous.endLine + 1) {
|
|
77
|
+
const overlap = Math.max(0, previous.endLine - current.startLine + 1);
|
|
78
|
+
previous.contents.push(...current.contents.slice(overlap));
|
|
79
|
+
previous.endLine = Math.max(previous.endLine, current.endLine);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
merged.push(current);
|
|
83
|
+
}
|
|
84
|
+
return merged.map((block) => {
|
|
85
|
+
if (!block.path) return block.raw;
|
|
86
|
+
return `# ${block.path}:${block.matchLine} [lines ${block.startLine}-${block.endLine}]\n${block.contents.join('\n')}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseGrepContextBlocks(lines, filenameOmitted, fallbackPath) {
|
|
35
91
|
const blocks = [];
|
|
36
92
|
let pending = [];
|
|
37
93
|
let i = 0;
|
|
38
94
|
while (i < lines.length) {
|
|
39
95
|
const line = lines[i];
|
|
40
96
|
if (line === '--') { pending = []; i++; continue; }
|
|
41
|
-
const anchor = grepBlockMatchAnchor(line, filenameOmitted);
|
|
97
|
+
const anchor = grepBlockMatchAnchor(line, filenameOmitted, fallbackPath);
|
|
42
98
|
if (anchor) {
|
|
43
99
|
const blockLines = pending.concat([line]);
|
|
44
100
|
pending = [];
|
|
45
101
|
i++;
|
|
46
102
|
while (i < lines.length) {
|
|
47
103
|
const next = lines[i];
|
|
48
|
-
if (next === '--' || grepBlockMatchAnchor(next, filenameOmitted)) break;
|
|
104
|
+
if (next === '--' || grepBlockMatchAnchor(next, filenameOmitted, fallbackPath)) break;
|
|
49
105
|
blockLines.push(next);
|
|
50
106
|
i++;
|
|
51
107
|
}
|
|
@@ -58,9 +114,12 @@ function parseGrepContextBlocks(lines, filenameOmitted) {
|
|
|
58
114
|
return blocks;
|
|
59
115
|
}
|
|
60
116
|
|
|
61
|
-
export function formatGrepContextOutput({ allLines, workDir, outputMode, filenameOmitted, headLimit, offset, totalKnown = true }) {
|
|
117
|
+
export function formatGrepContextOutput({ allLines, workDir, outputMode, filenameOmitted, headLimit, offset, searchPath, totalKnown = true }) {
|
|
62
118
|
const norm = allLines.map((l) => (l === '--' ? '--' : relativeGrepLine(l, workDir, false, outputMode, filenameOmitted)));
|
|
63
|
-
const
|
|
119
|
+
const fallbackPath = filenameOmitted
|
|
120
|
+
? relativePathPrefix(normalizeOutputPath(searchPath), workDir)
|
|
121
|
+
: '';
|
|
122
|
+
const blocks = parseGrepContextBlocks(norm, filenameOmitted, fallbackPath);
|
|
64
123
|
const total = blocks.length;
|
|
65
124
|
if (total === 0) return { text: '', total: 0, shown: 0, omitted: 0 };
|
|
66
125
|
// Finding 2/3: denominator is the PRE-offset grand total; on a partial rg
|
|
@@ -78,7 +137,7 @@ export function formatGrepContextOutput({ allLines, workDir, outputMode, filenam
|
|
|
78
137
|
}
|
|
79
138
|
const shown = headLimit === Infinity ? afterOffset.length : Math.min(headLimit, afterOffset.length);
|
|
80
139
|
const omitted = afterOffset.length - shown;
|
|
81
|
-
const render = (arr) => arr
|
|
140
|
+
const render = (arr) => renderGrepContextBlocks(arr, filenameOmitted, fallbackPath);
|
|
82
141
|
let segments;
|
|
83
142
|
let nextOffset = offset + shown;
|
|
84
143
|
if (omitted > 0 && shown > 0) {
|
|
@@ -99,16 +158,35 @@ export function formatGrepContextOutput({ allLines, workDir, outputMode, filenam
|
|
|
99
158
|
const notice = (omitted > 0 || !totalKnown)
|
|
100
159
|
? `\n[Showing ${shown} of ${totalStr} matches${totalKnown ? '' : ' (results partial)'}; pass offset:${nextOffset} for more]`
|
|
101
160
|
: '';
|
|
102
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
text: `${segments.join('\n')}${notice}`,
|
|
163
|
+
total,
|
|
164
|
+
shown,
|
|
165
|
+
omitted,
|
|
166
|
+
};
|
|
103
167
|
}
|
|
104
168
|
|
|
105
|
-
// Part 1: drop
|
|
106
|
-
// a pattern[] fan-out.
|
|
169
|
+
// Part 1: drop a numbered match line or patch-ready context block already
|
|
170
|
+
// emitted by an earlier pattern in a pattern[] fan-out.
|
|
107
171
|
export function dedupeFanoutMatchLines(body, seen) {
|
|
108
172
|
const text = String(body);
|
|
109
173
|
if (/^Error:/.test(text)) return text;
|
|
110
174
|
const out = [];
|
|
111
|
-
|
|
175
|
+
const lines = text.split('\n');
|
|
176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
177
|
+
const line = lines[i];
|
|
178
|
+
const header = parseGrepContextHeader(line);
|
|
179
|
+
if (header) {
|
|
180
|
+
const key = `${header.path}\0${header.matchLine}`;
|
|
181
|
+
const duplicate = seen.has(key);
|
|
182
|
+
seen.add(key);
|
|
183
|
+
if (!duplicate) out.push(line);
|
|
184
|
+
for (let sourceIndex = 0; sourceIndex < header.sourceLineCount && i + 1 < lines.length; sourceIndex++) {
|
|
185
|
+
i++;
|
|
186
|
+
if (!duplicate) out.push(lines[i]);
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
112
190
|
const s = splitGrepLinePrefix(line);
|
|
113
191
|
if (s && s.delimiter === ':') {
|
|
114
192
|
const key = `${s.path}\0${s.lineNo}`;
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
getCachedReadOnlyStat,
|
|
21
21
|
statCacheSet,
|
|
22
22
|
statPathsForMtime,
|
|
23
|
-
lstatPathsForMtime,
|
|
24
23
|
registerCacheInvalidationListener,
|
|
25
24
|
} from './cache-layers.mjs';
|
|
26
25
|
import {
|
|
@@ -28,8 +27,11 @@ import {
|
|
|
28
27
|
NOISE_DIR_NAMES,
|
|
29
28
|
walkDir,
|
|
30
29
|
} from './glob-walk.mjs';
|
|
31
|
-
import {
|
|
32
|
-
|
|
30
|
+
import {
|
|
31
|
+
capLineOrientedToolOutput,
|
|
32
|
+
LOCATOR_OUTPUT_MAX_BYTES,
|
|
33
|
+
TOOL_OUTPUT_MAX_BYTES,
|
|
34
|
+
} from './tool-output-limit.mjs';
|
|
33
35
|
import { runRg } from './rg-runner.mjs';
|
|
34
36
|
import { hasSpareCapacity as childSpawnHasSpareCapacity } from '../../../../shared/child-spawn-gate.mjs';
|
|
35
37
|
import { fuzzyRank } from './fuzzy-match.mjs';
|
|
@@ -52,6 +54,36 @@ function _findDefaultHeadLimit(fallback) {
|
|
|
52
54
|
return parsed > 0 ? parsed : fallback;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function _findOutputBudgetBytes(options = {}) {
|
|
58
|
+
const requested = Number(options?.__findOutputBudgetBytes);
|
|
59
|
+
const explicit = Number(options?.toolOutputMaxBytes);
|
|
60
|
+
return Math.max(1, Math.min(
|
|
61
|
+
LOCATOR_OUTPUT_MAX_BYTES,
|
|
62
|
+
Number.isFinite(requested) && requested > 0 ? Math.trunc(requested) : LOCATOR_OUTPUT_MAX_BYTES,
|
|
63
|
+
Number.isFinite(explicit) && explicit > 0 ? Math.trunc(explicit) : LOCATOR_OUTPUT_MAX_BYTES,
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _splitFindBudget(total, count, index) {
|
|
68
|
+
if (!(count > 0)) return 0;
|
|
69
|
+
const base = Math.floor(total / count);
|
|
70
|
+
return base + (index < (total % count) ? 1 : 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Entry paths render relative to the listed root — the caller supplied the
|
|
74
|
+
// base via `path`, so repeating the absolute prefix on every row is pure
|
|
75
|
+
// duplication (~2KB per 80 rows). Falls back to the absolute form when the
|
|
76
|
+
// entry escapes the root (drive change / `..`), where a relative form would
|
|
77
|
+
// be ambiguous. Cache-safe: keys already include the absolute root, and the
|
|
78
|
+
// rendered rows no longer depend on the session workDir.
|
|
79
|
+
function displayRelPath(entPath, rootPath) {
|
|
80
|
+
const rel = relative(rootPath, entPath);
|
|
81
|
+
if (!rel || rel === '.' || rel.startsWith('..') || /^[A-Za-z]:/.test(rel)) {
|
|
82
|
+
return normalizeOutputPath(entPath);
|
|
83
|
+
}
|
|
84
|
+
return normalizeOutputPath(rel);
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
export async function executeListTool(args, workDir, options = {}) {
|
|
56
88
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
57
89
|
if (Array.isArray(args.path)) {
|
|
@@ -209,20 +241,10 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
209
241
|
|
|
210
242
|
const windowed = offset > 0 ? rows.slice(offset) : rows;
|
|
211
243
|
const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const stats = await lstatPathsForMtime(sliced.map((row) => row.fullPath), workDir, 64, { deadlineMs: 5000 });
|
|
217
|
-
for (let i = 0; i < sliced.length; i++) {
|
|
218
|
-
const item = stats[i];
|
|
219
|
-
if (!item?.stat) continue;
|
|
220
|
-
sliced[i].size = item.size;
|
|
221
|
-
sliced[i].mtimeMs = item.mtimeMs;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const lines = sliced.map(r =>
|
|
225
|
-
`${normalizeOutputPath(r.path)}\t${r.type}\t${formatListSize(r.type, r.size)}\t${formatMtime(r.mtimeMs)}`);
|
|
244
|
+
// Paths and entry types are the list contract. Size/mtime columns were
|
|
245
|
+
// neither exposed in the model schema nor used by routing, yet consumed
|
|
246
|
+
// roughly 28–40% of live list output and forced a stat per visible entry.
|
|
247
|
+
const lines = sliced.map(r => `${displayRelPath(r.path, fullPath)}\t${r.type}`);
|
|
226
248
|
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${rows.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
227
249
|
if (truncatedByCap) lines.push(`... walk truncated at ${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms timeout; narrow the path or lower depth for a complete listing`);
|
|
228
250
|
let emptyMsg = '(empty directory)';
|
|
@@ -563,24 +585,60 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
563
585
|
const capped = list.length > 5;
|
|
564
586
|
const targets = capped ? list.slice(0, 5) : list;
|
|
565
587
|
if (targets.length > 1) {
|
|
588
|
+
// head_limit is one call-level result budget, not a multiplier per
|
|
589
|
+
// query. Split it deterministically in caller order; if it cannot
|
|
590
|
+
// fund one result per query, return the remaining queries exactly.
|
|
591
|
+
const totalHeadLimit = normalizeListHeadLimit(args.head_limit, _findDefaultHeadLimit(25));
|
|
592
|
+
const activeCount = totalHeadLimit > 0
|
|
593
|
+
? Math.min(targets.length, totalHeadLimit)
|
|
594
|
+
: targets.length;
|
|
595
|
+
const activeTargets = targets.slice(0, activeCount);
|
|
596
|
+
const omittedByHeadLimit = targets.slice(activeCount);
|
|
597
|
+
const notes = [];
|
|
598
|
+
if (omittedByHeadLimit.length) {
|
|
599
|
+
notes.push(
|
|
600
|
+
`... [head_limit ${totalHeadLimit} exhausted across query[]; `
|
|
601
|
+
+ `retry query=${JSON.stringify(omittedByHeadLimit)}]`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
if (capped) notes.push(`... [capped at 5 of ${list.length} queries]`);
|
|
605
|
+
const sectionHeaders = activeTargets.map((query) => `# find ${query}\n`);
|
|
606
|
+
const separatorBytes = Math.max(0, activeTargets.length + notes.length - 1) * 2;
|
|
607
|
+
const fixedBytes = sectionHeaders.reduce(
|
|
608
|
+
(sum, header) => sum + Buffer.byteLength(header, 'utf8'),
|
|
609
|
+
separatorBytes + notes.reduce((sum, note) => sum + Buffer.byteLength(note, 'utf8'), 0),
|
|
610
|
+
);
|
|
611
|
+
const bodyBudget = Math.max(activeTargets.length, _findOutputBudgetBytes(options) - fixedBytes);
|
|
566
612
|
// Full parallelism is intentional. All sections share one common
|
|
567
613
|
// broad enumeration and one unioned ignored/truncated targeted pass,
|
|
568
|
-
//
|
|
614
|
+
// while each query receives a fair share of the call-level count and
|
|
615
|
+
// byte budgets before any aggregate result is assembled.
|
|
569
616
|
const batchContext = {
|
|
570
|
-
queries:
|
|
617
|
+
queries: activeTargets,
|
|
571
618
|
targetedRuns: new Map(),
|
|
572
619
|
itemsByFiles: new WeakMap(),
|
|
573
620
|
};
|
|
574
|
-
const
|
|
575
|
-
|
|
621
|
+
const bodies = await Promise.all(activeTargets.map(async (query, index) => {
|
|
622
|
+
const queryHeadLimit = totalHeadLimit > 0
|
|
623
|
+
? _splitFindBudget(totalHeadLimit, activeTargets.length, index)
|
|
624
|
+
: 0;
|
|
625
|
+
const nestedOptions = {
|
|
626
|
+
...options,
|
|
627
|
+
__findBatchContext: batchContext,
|
|
628
|
+
__findOutputBudgetBytes: _splitFindBudget(bodyBudget, activeTargets.length, index),
|
|
629
|
+
};
|
|
576
630
|
try {
|
|
577
|
-
return await executeFuzzyFindTool(
|
|
631
|
+
return await executeFuzzyFindTool(
|
|
632
|
+
{ ...args, query, head_limit: queryHeadLimit },
|
|
633
|
+
workDir,
|
|
634
|
+
nestedOptions,
|
|
635
|
+
);
|
|
578
636
|
} catch (err) {
|
|
579
637
|
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
580
638
|
}
|
|
581
639
|
}));
|
|
582
|
-
const sections =
|
|
583
|
-
|
|
640
|
+
const sections = activeTargets.map((q, i) => `${sectionHeaders[i]}${bodies[i]}`);
|
|
641
|
+
sections.push(...notes);
|
|
584
642
|
return sections.join('\n\n');
|
|
585
643
|
}
|
|
586
644
|
args.query = targets[0];
|
|
@@ -616,7 +674,12 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
616
674
|
includeNoise,
|
|
617
675
|
});
|
|
618
676
|
const cached = cacheGet(cacheKey);
|
|
619
|
-
|
|
677
|
+
const capFindResult = (value) => capLineOrientedToolOutput(
|
|
678
|
+
value,
|
|
679
|
+
_findOutputBudgetBytes(options),
|
|
680
|
+
() => `... [find result budget reached for query=${JSON.stringify(query)}; narrow path/head_limit]`,
|
|
681
|
+
);
|
|
682
|
+
if (cached !== null) return capFindResult(cached);
|
|
620
683
|
// Common discovery respects .gitignore even outside a Git repository.
|
|
621
684
|
// include_noise deliberately retains the old hardened --no-ignore behavior.
|
|
622
685
|
// Noise dirs stay excluded via DEFAULT_IGNORE_GLOBS below.
|
|
@@ -751,7 +814,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
751
814
|
if (typeof options?.onProgress === 'function') {
|
|
752
815
|
try { options.onProgress(`${ranked.length} candidates`); } catch { /* best-effort */ }
|
|
753
816
|
}
|
|
754
|
-
return result;
|
|
817
|
+
return capFindResult(result);
|
|
755
818
|
}
|
|
756
819
|
|
|
757
820
|
export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
@@ -1014,7 +1077,7 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
1014
1077
|
const windowed = offset > 0 ? matches.slice(offset) : matches;
|
|
1015
1078
|
const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
|
|
1016
1079
|
const lines = sliced.map(m =>
|
|
1017
|
-
`${
|
|
1080
|
+
`${displayRelPath(m.path, fullPath)}\t${formatListSize('file', m.size)}\t${formatMtime(m.mtimeMs)}`);
|
|
1018
1081
|
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${matches.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
1019
1082
|
if (rgStdoutTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; results incomplete');
|
|
1020
1083
|
if (rgStdoutPartial) lines.push('... [warning] rg exit 2 (partial results); listing may be incomplete');
|
|
@@ -103,6 +103,11 @@ function firstPresentArg(args, names) {
|
|
|
103
103
|
return undefined;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// Shared semantic default for grep calls that omit output_mode/context.
|
|
107
|
+
// Scoped-cache canonicalization imports this so omitted context and an
|
|
108
|
+
// explicit context:25 resolve to the same cache key.
|
|
109
|
+
export const GREP_AUTO_CONTEXT_LINES = 25;
|
|
110
|
+
|
|
106
111
|
export function normalizeGrepArgs(args) {
|
|
107
112
|
if (!args || typeof args !== 'object') return args;
|
|
108
113
|
if (args.pattern === undefined || args.pattern === null || args.pattern === '') {
|
|
@@ -119,7 +124,7 @@ export function normalizeGrepArgs(args) {
|
|
|
119
124
|
}
|
|
120
125
|
if ((args.output_mode === undefined || args.output_mode === null || args.output_mode === '') && typeof args.mode === 'string') {
|
|
121
126
|
const mode = args.mode.trim();
|
|
122
|
-
if (['files_with_matches', 'content', 'count'].includes(mode)) args.output_mode = mode;
|
|
127
|
+
if (['files_with_matches', 'content', 'content_with_context', 'count'].includes(mode)) args.output_mode = mode;
|
|
123
128
|
}
|
|
124
129
|
return args;
|
|
125
130
|
}
|
|
@@ -284,6 +289,20 @@ export function coerceReadFamilyPathArg(path, workDir = null) {
|
|
|
284
289
|
} catch { /* not a literal bracket path — allow JSON coercion */ }
|
|
285
290
|
}
|
|
286
291
|
}
|
|
292
|
+
// Absorb: a truncated JSON path array ('["C:\\a\\b.mjs' — a streamed
|
|
293
|
+
// argument cut mid-emit). Repaired only when exactly ONE path survives,
|
|
294
|
+
// so a real batch can never be silently narrowed to its first entry.
|
|
295
|
+
if (typeof path === 'string') {
|
|
296
|
+
const trimmed = path.trim();
|
|
297
|
+
if (trimmed.startsWith('["') && !trimmed.endsWith(']')) {
|
|
298
|
+
try {
|
|
299
|
+
const repaired = JSON.parse(`${trimmed}"]`);
|
|
300
|
+
if (Array.isArray(repaired) && repaired.length === 1 && typeof repaired[0] === 'string') {
|
|
301
|
+
return repaired[0];
|
|
302
|
+
}
|
|
303
|
+
} catch { /* not a repairable truncation — fall through */ }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
287
306
|
const coerced = coerceShapeFlex(path);
|
|
288
307
|
if (!Array.isArray(coerced)) return coerced;
|
|
289
308
|
const list = coerced
|
|
@@ -54,7 +54,7 @@ export function sliceReadBodyByLines(body, origOffset, origLimit) {
|
|
|
54
54
|
const _remaining = haveTotal ? totalNum - emittedLast : null;
|
|
55
55
|
const continuationPart = moreToRead && Number.isFinite(emittedLast)
|
|
56
56
|
? (_remaining !== null && _remaining > 0 && _remaining <= 600
|
|
57
|
-
? `; ${_remaining}
|
|
57
|
+
? `; ${_remaining} left — ONE window: offset:${emittedLast} limit:${_remaining}`
|
|
58
58
|
: `; pass offset:${emittedLast} to continue`)
|
|
59
59
|
: '';
|
|
60
60
|
const newFooter = `[lines ${firstLine}-${emittedLast}${totalPart}${continuationPart}]`;
|
|
@@ -18,11 +18,11 @@ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
|
18
18
|
// truncation (proceed with cap) rather than refusing. Hard in-memory cap
|
|
19
19
|
// for loading a full file remains READ_MAX_SIZE_BYTES (10 MiB).
|
|
20
20
|
//
|
|
21
|
-
// READ_MAX_OUTPUT_BYTES (30 KB) — output-truncation cap. Lead-facing default tightened from 50k.
|
|
22
|
-
//
|
|
21
|
+
// READ_MAX_OUTPUT_BYTES (30 KB) — output-truncation cap. Lead-facing default tightened from 50k. Follows the
|
|
22
|
+
// common max-output-size intent and the throw-vs-truncate
|
|
23
23
|
// trade-off Anthropic chose in #21841 (throw is more token-efficient).
|
|
24
|
-
// NOTE:
|
|
25
|
-
//
|
|
24
|
+
// NOTE: the usual alternative is a ~25,000-token output budget
|
|
25
|
+
// enforced post-read via a tokenizer throw. mixdog
|
|
26
26
|
// caps on BYTES at read time instead (no tokenizer in the hot path);
|
|
27
27
|
// 30 KB ≈ well under a 25k-token budget, so the byte cap is left as-is
|
|
28
28
|
// rather than converted to a token count that can't be expressed
|
|
@@ -15,8 +15,8 @@ export const API_IMAGE_MAX_BASE64_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
|
15
15
|
export const IMAGE_TARGET_RAW_SIZE = (API_IMAGE_MAX_BASE64_SIZE * 3) / 4; // 3.75 MB
|
|
16
16
|
const IMAGE_MAX_WIDTH = 2000;
|
|
17
17
|
const IMAGE_MAX_HEIGHT = 2000;
|
|
18
|
-
// Token budget for a single image. est tokens = base64.length * 0.125 (
|
|
19
|
-
// per-image heuristic). Default aligns to the 5MB base64 API ceiling so the
|
|
18
|
+
// Token budget for a single image. est tokens = base64.length * 0.125 (the
|
|
19
|
+
// common per-image heuristic). Default aligns to the 5MB base64 API ceiling so the
|
|
20
20
|
// dimension/raw-size resize governs the common case and the token gate only
|
|
21
21
|
// fires on pathologically dense images.
|
|
22
22
|
const DEFAULT_IMAGE_MAX_TOKENS = Math.ceil(API_IMAGE_MAX_BASE64_SIZE * 0.125);
|
|
@@ -99,6 +99,9 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
99
99
|
_rawContentCacheSet,
|
|
100
100
|
_recordReadSnapshot,
|
|
101
101
|
} = helpers;
|
|
102
|
+
const _readMaxOutputBytes = Number(options?.readOutputBudgetBytes) > 0
|
|
103
|
+
? Math.min(READ_MAX_OUTPUT_BYTES, Math.trunc(Number(options.readOutputBudgetBytes)))
|
|
104
|
+
: READ_MAX_OUTPUT_BYTES;
|
|
102
105
|
// Normalize path (strip whitespace, expand ~, posix→windows) up front so
|
|
103
106
|
// LLM-injected stray spaces don't trigger an ENOENT retry that pollutes
|
|
104
107
|
// the conversation history and breaks the cache prefix on later turns.
|
|
@@ -454,7 +457,10 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
454
457
|
return withSymbolReadNote(text, args);
|
|
455
458
|
}
|
|
456
459
|
try {
|
|
457
|
-
const _streamRes = await streamReadRange(fullPath, offset, limit, st, {
|
|
460
|
+
const _streamRes = await streamReadRange(fullPath, offset, limit, st, {
|
|
461
|
+
displayPath: filePath,
|
|
462
|
+
maxOutputBytes: _readMaxOutputBytes,
|
|
463
|
+
});
|
|
458
464
|
const out = _streamRes.text;
|
|
459
465
|
// W1 H: snapshot only emitted line bounds, not the
|
|
460
466
|
// requested window — byte-cap truncation can stop short.
|
|
@@ -547,9 +553,13 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
547
553
|
// Fall through to the regular read path; it still enforces output caps.
|
|
548
554
|
}
|
|
549
555
|
}
|
|
550
|
-
if (!_isUtf16 && hasRangeArgs && !wantFull
|
|
556
|
+
if (!_isUtf16 && hasRangeArgs && !wantFull
|
|
557
|
+
&& (options?.forceReadRangeStream === true || st.size > READ_STREAM_RANGE_MIN_BYTES)) {
|
|
551
558
|
try {
|
|
552
|
-
const _streamRes = await streamReadRange(fullPath, offset, limit, st, {
|
|
559
|
+
const _streamRes = await streamReadRange(fullPath, offset, limit, st, {
|
|
560
|
+
displayPath: filePath,
|
|
561
|
+
maxOutputBytes: _readMaxOutputBytes,
|
|
562
|
+
});
|
|
553
563
|
const out = _streamRes.text;
|
|
554
564
|
const _emittedRanges = (_streamRes.firstEmitted && _streamRes.lastEmitted)
|
|
555
565
|
? [{ startLine: _streamRes.firstEmitted, endLine: _streamRes.lastEmitted }]
|
|
@@ -649,19 +659,19 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
649
659
|
_smartTruncated = true;
|
|
650
660
|
_smartVisibleRanges = Array.isArray(smart.ranges) ? smart.ranges : null;
|
|
651
661
|
_renderedLineCount = 0;
|
|
652
|
-
} else if (Buffer.byteLength(rendered, 'utf8') >
|
|
662
|
+
} else if (Buffer.byteLength(rendered, 'utf8') > _readMaxOutputBytes) {
|
|
653
663
|
let lo = 0;
|
|
654
664
|
let hi = rendered.length;
|
|
655
665
|
while (lo < hi) {
|
|
656
666
|
const mid = Math.ceil((lo + hi) / 2);
|
|
657
|
-
if (Buffer.byteLength(rendered.slice(0, mid), 'utf8') <=
|
|
667
|
+
if (Buffer.byteLength(rendered.slice(0, mid), 'utf8') <= _readMaxOutputBytes) lo = mid;
|
|
658
668
|
else hi = mid - 1;
|
|
659
669
|
}
|
|
660
670
|
const slice = rendered.slice(0, lo);
|
|
661
671
|
const completeRenderedLines = Math.max(0, slice.split('\n').length - 1);
|
|
662
672
|
_renderedLineCount = completeRenderedLines;
|
|
663
673
|
_byteCapTruncated = true;
|
|
664
|
-
out = slice + `\n\n... [output truncated at ${Math.round(
|
|
674
|
+
out = slice + `\n\n... [output truncated at ${Math.round(_readMaxOutputBytes/1024)} KB] ...`;
|
|
665
675
|
} else {
|
|
666
676
|
out = rendered;
|
|
667
677
|
}
|
|
@@ -671,10 +681,10 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
671
681
|
} else if (_byteCapTruncated) {
|
|
672
682
|
const emittedStart = offset + 1;
|
|
673
683
|
const emittedEnd = offset + _renderedLineCount;
|
|
674
|
-
const capKb = Math.round(
|
|
684
|
+
const capKb = Math.round(_readMaxOutputBytes / 1024);
|
|
675
685
|
const footer = `[lines ${emittedStart}-${emittedEnd} of ${lineCount}; output truncated at ${capKb} KB${emittedEnd < lineCount ? `; pass offset:${emittedEnd} to continue` : ''}]`;
|
|
676
686
|
out += `${out ? '\n' : ''}${footer}`;
|
|
677
|
-
} else if (Buffer.byteLength(rendered, 'utf8') <=
|
|
687
|
+
} else if (Buffer.byteLength(rendered, 'utf8') <= _readMaxOutputBytes) {
|
|
678
688
|
const emittedStart = offset + 1;
|
|
679
689
|
const emittedEnd = offset + sliced.length;
|
|
680
690
|
// Anti-fragmentation: when the remainder is modest, hand the
|
|
@@ -683,7 +693,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
683
693
|
const _remaining = lineCount - emittedEnd;
|
|
684
694
|
const _cont = emittedEnd < lineCount
|
|
685
695
|
? (_remaining <= 600
|
|
686
|
-
? `; ${_remaining}
|
|
696
|
+
? `; ${_remaining} left — ONE window: offset:${emittedEnd} limit:${_remaining}`
|
|
687
697
|
: `; pass offset:${emittedEnd} to continue`)
|
|
688
698
|
: '';
|
|
689
699
|
const footer = `[lines ${emittedStart}-${emittedEnd} of ${lineCount}${_cont}]`;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
rawContentCacheSet,
|
|
13
13
|
} from './cache-layers.mjs';
|
|
14
14
|
import {
|
|
15
|
+
rememberReadSnapshot,
|
|
15
16
|
readFilesForScope,
|
|
16
17
|
readScopeKey,
|
|
17
18
|
scheduleScopePersist,
|
|
@@ -146,7 +147,7 @@ export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
|
|
|
146
147
|
} else if (!batchSliced && sameFile && Number.isFinite(existing?.fileLineCount) && existing.fileLineCount >= 0) {
|
|
147
148
|
next.fileLineCount = Math.trunc(existing.fileLineCount);
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
rememberReadSnapshot(fullPath, next, scope, readFiles);
|
|
150
151
|
scheduleScopePersist(readScopeKey(scope));
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -8,8 +8,8 @@ const requireCjs = createRequire(import.meta.url);
|
|
|
8
8
|
const DEFAULT_READ_MAX_OUTPUT_BYTES = 100 * 1024;
|
|
9
9
|
|
|
10
10
|
// PDFs at or under this size are emitted as an Anthropic base64 document
|
|
11
|
-
// block (the model reads the rendered PDF directly)
|
|
12
|
-
//
|
|
11
|
+
// block (the model reads the rendered PDF directly): 20MB raw → ~27MB
|
|
12
|
+
// base64, which stays under the 32MB request cap.
|
|
13
13
|
// Larger PDFs fall back to pdf-parse TEXT extraction.
|
|
14
14
|
const PDF_DOCUMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ const PDF_DOCUMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
|
19
19
|
const PDF_MAGIC = Buffer.from('%PDF-', 'latin1');
|
|
20
20
|
|
|
21
21
|
// Per-output text ceiling inside a notebook. A single cell output larger than
|
|
22
|
-
// this is replaced with a jq hint (
|
|
22
|
+
// this is replaced with a jq hint (large-notebook guidance) so a
|
|
23
23
|
// runaway stdout / data dump can't blow the read budget.
|
|
24
24
|
const IPYNB_OUTPUT_MAX_CHARS = 10_000;
|
|
25
25
|
|
|
@@ -62,6 +62,10 @@ export async function streamReadRange(fullPath, offset, limit, stHint = null, ho
|
|
|
62
62
|
let pendingBytes = 0;
|
|
63
63
|
let lineCollectCapped = false;
|
|
64
64
|
let prefixHash = rangeIndex?.prefixHash || '';
|
|
65
|
+
const maxOutputBytes = Number(hooks.maxOutputBytes) > 0
|
|
66
|
+
? Math.min(READ_MAX_OUTPUT_BYTES, Math.trunc(Number(hooks.maxOutputBytes)))
|
|
67
|
+
: READ_MAX_OUTPUT_BYTES;
|
|
68
|
+
const bodyOutputBytes = Math.max(1, maxOutputBytes - Math.min(384, Math.floor(maxOutputBytes / 3)));
|
|
65
69
|
const deadline = Date.now() + READ_STREAM_TIMEOUT_MS;
|
|
66
70
|
let bytesScanned = anchor.byteOffset;
|
|
67
71
|
|
|
@@ -85,7 +89,7 @@ export async function streamReadRange(fullPath, offset, limit, stHint = null, ho
|
|
|
85
89
|
// Truncation drops the entire rendered line (codepoint-safe by
|
|
86
90
|
// construction), so no mid-codepoint cut can occur here.
|
|
87
91
|
collectedBytes += Buffer.byteLength(rendered, 'utf8') + 1;
|
|
88
|
-
if (collectedBytes >
|
|
92
|
+
if (collectedBytes > bodyOutputBytes) {
|
|
89
93
|
truncated = true;
|
|
90
94
|
return false;
|
|
91
95
|
}
|
|
@@ -207,7 +211,8 @@ export async function streamReadRange(fullPath, offset, limit, stHint = null, ho
|
|
|
207
211
|
|
|
208
212
|
let out = collected.join('\n');
|
|
209
213
|
if (truncated) {
|
|
210
|
-
|
|
214
|
+
const nextOffset = lastEmitted || offset;
|
|
215
|
+
out += `\n\n... [output truncated at ${Math.max(1, Math.round(maxOutputBytes/1024))} KB; pass offset:${nextOffset} to continue] ...`;
|
|
211
216
|
} else if (stoppedAtLimit) {
|
|
212
217
|
out += `${out ? '\n' : ''}... [range limit reached; next offset: ${offset + collected.length}]`;
|
|
213
218
|
} else if (!out && offset >= lineIdx) {
|