mixdog 0.9.93 → 0.9.94
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/package.json +1 -1
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/shared/01-tool.md +23 -18
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +100 -86
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
- 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 +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- 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/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- 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 +4 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -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/patch/dispatch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/shared/background-tasks.mjs +10 -3
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- 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-surface.mjs +7 -2
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/app/use-transcript-window.mjs +7 -1
- package/src/tui/components/Spinner.jsx +18 -9
- package/src/tui/dist/index.mjs +111 -25
- package/src/tui/engine/live-share.mjs +23 -3
- package/src/tui/engine/session-api.mjs +7 -0
- package/src/tui/engine/turn.mjs +77 -4
- package/src/tui/engine.mjs +14 -6
- package/src/tui/index.jsx +7 -0
|
@@ -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
|
}
|
|
@@ -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}]`;
|
|
@@ -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}]`;
|
|
@@ -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) {
|
|
@@ -113,6 +113,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
113
113
|
_rangeHashesFromRenderedReadText,
|
|
114
114
|
_readEntryLineWindow,
|
|
115
115
|
_recordReadSnapshot,
|
|
116
|
+
READ_MAX_OUTPUT_BYTES,
|
|
116
117
|
} = helpers;
|
|
117
118
|
// CC `file_path` alias — official SDK schema uses `file_path`;
|
|
118
119
|
// mixdog has historically used `path`. Honor `file_path` so a
|
|
@@ -310,6 +311,21 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
310
311
|
if (a.offset !== b.offset) return a.offset - b.offset;
|
|
311
312
|
return a.index - b.index;
|
|
312
313
|
});
|
|
314
|
+
// A path[] Read shares one byte ceiling. Reserve room for headers and
|
|
315
|
+
// split the remaining body budget across distinct disk windows.
|
|
316
|
+
const _readCallBudget = Number(options?.readOutputBudgetBytes) > 0
|
|
317
|
+
? Math.min(READ_MAX_OUTPUT_BYTES, Math.trunc(Number(options.readOutputBudgetBytes)))
|
|
318
|
+
: READ_MAX_OUTPUT_BYTES;
|
|
319
|
+
const _headerReserve = Math.min(
|
|
320
|
+
Math.max(2_048, entries.reduce((sum, entry) => (
|
|
321
|
+
sum + Buffer.byteLength(String(normalizeOutputPath(entry?.path || '(missing-path)')), 'utf8') + 128
|
|
322
|
+
), 0)),
|
|
323
|
+
Math.floor(_readCallBudget / 2),
|
|
324
|
+
);
|
|
325
|
+
const _perTaskOutputBudget = Math.max(
|
|
326
|
+
256,
|
|
327
|
+
Math.floor((_readCallBudget - _headerReserve) / Math.max(1, tasks.length)),
|
|
328
|
+
);
|
|
313
329
|
const results = new Array(entries.length);
|
|
314
330
|
const readChains = new Map();
|
|
315
331
|
await Promise.all(tasks.map(({ entry, index }) => {
|
|
@@ -319,9 +335,15 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
319
335
|
}
|
|
320
336
|
const run = async () => {
|
|
321
337
|
const _diskWin = readEntryCoalescedDiskWindow(entry);
|
|
322
|
-
|
|
338
|
+
let readEntry = _diskWin
|
|
323
339
|
? { ...entry, offset: _diskWin.offset, limit: _diskWin.limit }
|
|
324
340
|
: entry;
|
|
341
|
+
if ((!readEntry.mode || readEntry.mode === 'full')
|
|
342
|
+
&& readEntry.offset == null
|
|
343
|
+
&& readEntry.limit == null
|
|
344
|
+
&& readEntry.full !== true) {
|
|
345
|
+
readEntry = { ...readEntry, offset: 0, limit: 2000 };
|
|
346
|
+
}
|
|
325
347
|
// Full image children retain their rich blocks; the aggregate
|
|
326
348
|
// assembler below flattens them without stringification.
|
|
327
349
|
// Other media (PDF/notebook) remains text-only in a batch so
|
|
@@ -332,6 +354,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
332
354
|
suppressReadUnchangedStub: true,
|
|
333
355
|
mediaTextOnly: !richImage,
|
|
334
356
|
_skipReachPreflight: true,
|
|
357
|
+
forceReadRangeStream: true,
|
|
358
|
+
readOutputBudgetBytes: _perTaskOutputBudget,
|
|
359
|
+
toolOutputMaxBytes: _perTaskOutputBudget,
|
|
335
360
|
});
|
|
336
361
|
results[index] = { path: entry.path, mode: entry.mode || 'full', n: entry.n, body };
|
|
337
362
|
};
|
|
@@ -470,11 +495,13 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
470
495
|
const r = orderedResults[i];
|
|
471
496
|
const path = normalizeOutputPath(r.path);
|
|
472
497
|
const mode = r.n !== undefined ? `${r.mode} n=${r.n}` : r.mode;
|
|
498
|
+
// Default full mode carries no information — tag only non-default modes.
|
|
499
|
+
const modeTag = mode && mode !== 'full' ? ` [${mode}]` : '';
|
|
473
500
|
const status = bodyFailed(r.body) ? 'error' : 'ok';
|
|
474
501
|
const textBody = bodyTextFor(r.body);
|
|
475
502
|
const match = /\[TRUNCATED (?:—|-) file is (\d+) lines \/ (\d+) KB\./.exec(textBody);
|
|
476
503
|
const suffix = match ? ` (truncated ${match[1]}L/${match[2]}KB)` : '';
|
|
477
|
-
const entryHeader = `${path}
|
|
504
|
+
const entryHeader = `${path}${modeTag} [${status}]${suffix}`;
|
|
478
505
|
const richParts = richPartsFor(r.body);
|
|
479
506
|
let priorIdx;
|
|
480
507
|
if (richParts) {
|
|
@@ -513,16 +540,17 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
513
540
|
const body = orderedResults.map((r, _i) => {
|
|
514
541
|
const path = normalizeOutputPath(r.path);
|
|
515
542
|
const mode = r.n !== undefined ? `${r.mode} n=${r.n}` : r.mode;
|
|
543
|
+
const modeTag = mode && mode !== 'full' ? ` [${mode}]` : '';
|
|
516
544
|
const status = classifyResultKind(String(r.body || '')) === 'error' ? 'error' : 'ok';
|
|
517
545
|
const dupKey = JSON.stringify([path, mode, r.body || '']);
|
|
518
546
|
const priorIdx = _seenEntryBody.get(dupKey);
|
|
519
547
|
if (priorIdx !== undefined) {
|
|
520
|
-
return `${path}
|
|
548
|
+
return `${path}${modeTag} [${status}] [= entry #${priorIdx + 1}, identical result omitted]`;
|
|
521
549
|
}
|
|
522
550
|
_seenEntryBody.set(dupKey, _i);
|
|
523
551
|
const match = /\[TRUNCATED (?:—|-) file is (\d+) lines \/ (\d+) KB\./.exec(r.body || '');
|
|
524
552
|
const suffix = match ? ` (truncated ${match[1]}L/${match[2]}KB)` : '';
|
|
525
|
-
return `${path}
|
|
553
|
+
return `${path}${modeTag} [${status}]${suffix}\n${r.body}`;
|
|
526
554
|
}).join('\n\n');
|
|
527
555
|
return `${header}\n\n${body}${args._batchCapNote ? `\n\n${args._batchCapNote}` : ''}`;
|
|
528
556
|
}
|
|
@@ -31,7 +31,9 @@ export function buildGrepCacheKey(parts) {
|
|
|
31
31
|
onlyMatching,
|
|
32
32
|
pcre2,
|
|
33
33
|
withFilename,
|
|
34
|
+
contextCharBudget = 0,
|
|
34
35
|
patternCapTotal = 0,
|
|
36
|
+
candidatesKey = '',
|
|
35
37
|
} = parts;
|
|
36
38
|
return [
|
|
37
39
|
'grep',
|
|
@@ -51,10 +53,15 @@ export function buildGrepCacheKey(parts) {
|
|
|
51
53
|
Array.isArray(fileType) ? fileType.join('\x01') : (fileType || ''),
|
|
52
54
|
pcre2 ? 'p1' : 'p0',
|
|
53
55
|
withFilename ? 'H1' : 'H0',
|
|
56
|
+
'cb' + String(contextCharBudget || 0),
|
|
54
57
|
// Cap total keeps a capped request (first-N of M patterns, carrying the
|
|
55
58
|
// "[capped at N of M]" notice) from colliding with an exact N-pattern
|
|
56
59
|
// request or with a differently-capped one (of 15 vs of 20).
|
|
57
60
|
'pc' + String(patternCapTotal || 0),
|
|
61
|
+
// Candidate-file scoping (fan-out prefilter) changes which files rg
|
|
62
|
+
// visits and their order; scoped results get their own cache slot so
|
|
63
|
+
// they never collide with an unscoped run of the same pattern.
|
|
64
|
+
'cd' + String(candidatesKey || ''),
|
|
58
65
|
].join('|');
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -75,6 +82,7 @@ export function buildGrepRgArgs(parts) {
|
|
|
75
82
|
fixedStrings = false,
|
|
76
83
|
pcre2 = false,
|
|
77
84
|
withFilename = false,
|
|
85
|
+
candidateFiles = null,
|
|
78
86
|
} = parts;
|
|
79
87
|
// `--hidden`: search dotfiles/dot-dirs (.github, .mixdog) that
|
|
80
88
|
// rg skips by default. The DEFAULT_IGNORE_GLOBS below still exclude .git and
|
|
@@ -127,7 +135,14 @@ export function buildGrepRgArgs(parts) {
|
|
|
127
135
|
// `--type` is treated as a positional path, not parsed as an rg
|
|
128
136
|
// option. Patterns already use `-e`, so the separator only needs to
|
|
129
137
|
// guard the trailing path operand.
|
|
130
|
-
|
|
138
|
+
if (Array.isArray(candidateFiles) && candidateFiles.length > 0) {
|
|
139
|
+
// Fan-out prefilter scoping: search ONLY the candidate files (paths
|
|
140
|
+
// relative to the spawn cwd) instead of walking searchPath — explicit
|
|
141
|
+
// file operands skip the directory walk entirely.
|
|
142
|
+
rgArgs.push('--', ...candidateFiles);
|
|
143
|
+
} else {
|
|
144
|
+
rgArgs.push('--', searchPath);
|
|
145
|
+
}
|
|
131
146
|
return rgArgs;
|
|
132
147
|
}
|
|
133
148
|
|