mixdog 0.9.17 → 0.9.19
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 +3 -2
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/recall-bench-cases.json +11 -0
- package/scripts/recall-bench.mjs +91 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +5 -2
- package/scripts/tool-smoke.mjs +101 -27
- package/src/agents/debugger/AGENT.md +3 -3
- package/src/agents/heavy-worker/AGENT.md +7 -10
- package/src/agents/maintainer/AGENT.md +1 -2
- package/src/agents/reviewer/AGENT.md +1 -2
- package/src/agents/worker/AGENT.md +5 -8
- package/src/defaults/agents.json +3 -0
- package/src/mixdog-session-runtime.mjs +23 -6
- package/src/rules/agent/00-core.md +4 -3
- package/src/rules/agent/30-explorer.md +53 -22
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +24 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
- package/src/runtime/channels/backends/discord-attachments.mjs +2 -2
- package/src/runtime/channels/backends/discord-gateway.mjs +26 -3
- package/src/runtime/channels/backends/discord.mjs +139 -7
- package/src/runtime/channels/index.mjs +290 -76
- package/src/runtime/channels/lib/crash-log.mjs +21 -3
- package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
- package/src/runtime/channels/lib/memory-client.mjs +32 -14
- package/src/runtime/channels/lib/output-forwarder.mjs +156 -14
- package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
- package/src/runtime/channels/lib/runtime-paths.mjs +48 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
- package/src/runtime/channels/lib/voice-transcription.mjs +4 -2
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
- package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
- package/src/runtime/memory/lib/query-handlers.mjs +36 -4
- package/src/runtime/memory/lib/recall-format.mjs +106 -6
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +10 -4
- package/src/runtime/shared/background-tasks.mjs +4 -2
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +30 -1
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/cwd-plugins.mjs +46 -3
- package/src/session-runtime/mcp-glue.mjs +24 -3
- package/src/session-runtime/output-styles.mjs +44 -10
- package/src/session-runtime/workflow.mjs +16 -1
- package/src/standalone/channel-worker.mjs +88 -7
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +57 -77
- package/src/tui/app/channel-pickers.mjs +45 -0
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/transcript-window.mjs +66 -1
- package/src/tui/app/use-mouse-input.mjs +9 -2
- package/src/tui/app/use-prompt-handlers.mjs +7 -94
- package/src/tui/app/use-transcript-scroll.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +74 -8
- package/src/tui/components/PromptInput.jsx +33 -64
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +4744 -4806
- package/src/tui/engine.mjs +109 -20
- package/src/tui/lib/voice-setup.mjs +166 -0
- package/src/tui/paste-attachments.mjs +12 -5
- package/src/tui/prompt-history-store.mjs +125 -12
- package/scripts/bench/cache-probe-tasks.json +0 -8
- package/scripts/bench/lead-review-tasks-r3.json +0 -20
- package/scripts/bench/lead-review-tasks.json +0 -20
- package/scripts/bench/r4-mixed-tasks.json +0 -20
- package/scripts/bench/r5-orchestrated-task.json +0 -7
- package/scripts/bench/review-tasks.json +0 -20
- package/scripts/bench/round-codex.json +0 -114
- package/scripts/bench/round-mixdog-lead-r3.json +0 -269
- package/scripts/bench/round-mixdog-lead.json +0 -269
- package/scripts/bench/round-mixdog.json +0 -126
- package/scripts/bench/round-r10-bigsample.json +0 -679
- package/scripts/bench/round-r11-codexalign.json +0 -257
- package/scripts/bench/round-r13-clientmeta.json +0 -464
- package/scripts/bench/round-r14-betafeatures.json +0 -466
- package/scripts/bench/round-r15-fulldefault.json +0 -462
- package/scripts/bench/round-r16-sessionid.json +0 -466
- package/scripts/bench/round-r17-wirebytes.json +0 -456
- package/scripts/bench/round-r18-prewarm.json +0 -468
- package/scripts/bench/round-r19-clean.json +0 -472
- package/scripts/bench/round-r20-prewarm-clean.json +0 -475
- package/scripts/bench/round-r21-delta-retry.json +0 -473
- package/scripts/bench/round-r22-full-probe.json +0 -693
- package/scripts/bench/round-r23-itemprobe.json +0 -701
- package/scripts/bench/round-r24-shapefix.json +0 -677
- package/scripts/bench/round-r25-serial.json +0 -464
- package/scripts/bench/round-r26-parallel3.json +0 -671
- package/scripts/bench/round-r27-parallel10.json +0 -894
- package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
- package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
- package/scripts/bench/round-r30-instid.json +0 -253
- package/scripts/bench/round-r31-upgradeprobe.json +0 -256
- package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
- package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
- package/scripts/bench/round-r34-orchestrated.json +0 -120
- package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
- package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
- package/scripts/bench/round-r4-codex.json +0 -114
- package/scripts/bench/round-r4-mixed.json +0 -225
- package/scripts/bench/round-r5-gpt-lead.json +0 -259
- package/scripts/bench/round-r6-codex.json +0 -114
- package/scripts/bench/round-r6-solo.json +0 -257
- package/scripts/bench/round-r7-full.json +0 -254
- package/scripts/bench/round-r8-fulldefault.json +0 -255
- package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
- package/src/tui/lib/voice-recorder.mjs +0 -469
|
@@ -114,7 +114,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
114
114
|
const requestedCwd = args.cwd ?? args.workdir;
|
|
115
115
|
const cwdResult = resolveOptionalCwd(requestedCwd, workDir);
|
|
116
116
|
if (cwdResult.error) return cwdResult.error;
|
|
117
|
-
// Session cwd carry-over (
|
|
117
|
+
// Session cwd carry-over (no live shell): when the model
|
|
118
118
|
// passes an explicit cwd it wins and updates the store on the next probe;
|
|
119
119
|
// otherwise reuse the last stored cwd for this session if it still exists.
|
|
120
120
|
const _hasExplicitCwd = typeof requestedCwd === 'string' && requestedCwd.trim() !== '';
|
|
@@ -231,7 +231,15 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
231
231
|
// run_in_background (already detached) or persistent sessions (handled
|
|
232
232
|
// far above). Capped below the hard timeout so the 600 s upper bound
|
|
233
233
|
// stays a separate, later ceiling.
|
|
234
|
-
|
|
234
|
+
// Soft config: reuse the sync task-wait budget (30 s, see waitForShellJob
|
|
235
|
+
// default in executeTaskTool) as the default promotion threshold. Override
|
|
236
|
+
// with MIXDOG_SHELL_AUTO_BACKGROUND_MS (positive ms). This is a soft hint,
|
|
237
|
+
// not a hard cap — the value is clamped below `timeout` so the hard ceiling
|
|
238
|
+
// stays a separate, later bound.
|
|
239
|
+
const _autoBgEnvMs = Number(process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS);
|
|
240
|
+
const DEFAULT_AUTO_BACKGROUND_MS = Number.isFinite(_autoBgEnvMs) && _autoBgEnvMs > 0
|
|
241
|
+
? Math.floor(_autoBgEnvMs)
|
|
242
|
+
: 30_000;
|
|
235
243
|
const autoBackgroundMs = runInBackground
|
|
236
244
|
? 0
|
|
237
245
|
: Math.min(DEFAULT_AUTO_BACKGROUND_MS, timeout);
|
|
@@ -17,7 +17,7 @@ export const BUILTIN_TOOLS = [
|
|
|
17
17
|
name: 'read',
|
|
18
18
|
title: 'Mixdog Read',
|
|
19
19
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
20
|
-
description: 'Read known file path(s)
|
|
20
|
+
description: 'Read known file path(s) with optional numeric offset+limit windows. Batch paths/regions as real arrays in one call. Not for directory listing.',
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: 'object',
|
|
23
23
|
properties: {
|
|
@@ -53,7 +53,7 @@ export const BUILTIN_TOOLS = [
|
|
|
53
53
|
name: 'shell',
|
|
54
54
|
title: 'Mixdog Shell',
|
|
55
55
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
56
|
-
description: `Run
|
|
56
|
+
description: `Run programs or change system state. Set shell: powershell or bash. Not for reading, listing, or searching files. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
57
57
|
inputSchema: {
|
|
58
58
|
type: 'object',
|
|
59
59
|
properties: {
|
|
@@ -87,7 +87,7 @@ export const BUILTIN_TOOLS = [
|
|
|
87
87
|
name: 'grep',
|
|
88
88
|
title: 'Mixdog Grep',
|
|
89
89
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
90
|
-
description: '
|
|
90
|
+
description: 'Exact text/regex in a KNOWN scope. files_with_matches/count for broad anchors, content_with_context for narrow answers.',
|
|
91
91
|
inputSchema: {
|
|
92
92
|
type: 'object',
|
|
93
93
|
properties: {
|
|
@@ -96,30 +96,28 @@ export const BUILTIN_TOOLS = [
|
|
|
96
96
|
{ type: 'string' },
|
|
97
97
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
98
98
|
],
|
|
99
|
-
description: 'Text/regex.
|
|
99
|
+
description: 'Text/regex. Array = variants in one call.',
|
|
100
100
|
},
|
|
101
101
|
path: {
|
|
102
102
|
anyOf: [
|
|
103
103
|
{ type: 'string' },
|
|
104
104
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
105
105
|
],
|
|
106
|
-
description: 'Known
|
|
106
|
+
description: 'Known file or dir. Array = several scopes.',
|
|
107
107
|
},
|
|
108
108
|
glob: {
|
|
109
109
|
anyOf: [
|
|
110
110
|
{ type: 'string' },
|
|
111
111
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
112
112
|
],
|
|
113
|
-
description: '
|
|
113
|
+
description: 'Glob filter to narrow scope.',
|
|
114
114
|
},
|
|
115
|
-
output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad
|
|
116
|
-
head_limit: { type: 'number', minimum: 0, description: 'Max
|
|
117
|
-
offset: { type: 'number', minimum: 0, description: 'Skip
|
|
115
|
+
output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad: files_with_matches/count; narrow: content_with_context.' },
|
|
116
|
+
head_limit: { type: 'number', minimum: 0, description: 'Max results.' },
|
|
117
|
+
offset: { type: 'number', minimum: 0, description: 'Skip results for paging.' },
|
|
118
118
|
'-A': { type: 'number', minimum: 0, description: 'Lines after each match.' },
|
|
119
119
|
'-B': { type: 'number', minimum: 0, description: 'Lines before each match.' },
|
|
120
|
-
|
|
121
|
-
// grep→read same-file re-open round-trips (2026-07 diag).
|
|
122
|
-
'-C': { type: 'number', minimum: 0, description: 'Lines before/after each match. Content modes: pass -C 5+ so the hit is edit-ready; skipping it forces a follow-up read.' },
|
|
120
|
+
'-C': { type: 'number', minimum: 0, description: 'Lines before/after each match.' },
|
|
123
121
|
},
|
|
124
122
|
required: [],
|
|
125
123
|
},
|
|
@@ -128,7 +126,7 @@ export const BUILTIN_TOOLS = [
|
|
|
128
126
|
name: 'glob',
|
|
129
127
|
title: 'Mixdog Glob',
|
|
130
128
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
131
|
-
description: 'Find files by exact glob.
|
|
129
|
+
description: 'Find files by exact glob. Batch patterns and roots as arrays in one call.',
|
|
132
130
|
inputSchema: {
|
|
133
131
|
type: 'object',
|
|
134
132
|
properties: {
|
|
@@ -156,7 +154,7 @@ export const BUILTIN_TOOLS = [
|
|
|
156
154
|
name: 'find',
|
|
157
155
|
title: 'Mixdog Find Files',
|
|
158
156
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
159
|
-
description: 'Find files by partial path/name.
|
|
157
|
+
description: 'Find files by partial path/name. Returns verified paths. Batch names as query[].',
|
|
160
158
|
inputSchema: {
|
|
161
159
|
type: 'object',
|
|
162
160
|
properties: {
|
|
@@ -177,7 +175,7 @@ export const BUILTIN_TOOLS = [
|
|
|
177
175
|
name: 'list',
|
|
178
176
|
title: 'Mixdog List Directory',
|
|
179
177
|
annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
180
|
-
description: 'List known directory entries.
|
|
178
|
+
description: 'List known directory entries. Batch dirs as path[].',
|
|
181
179
|
inputSchema: {
|
|
182
180
|
type: 'object',
|
|
183
181
|
properties: {
|
|
@@ -49,8 +49,13 @@ export function sliceReadBodyByLines(body, origOffset, origLimit) {
|
|
|
49
49
|
: firstLine));
|
|
50
50
|
const totalPart = haveTotal ? ` of ${totalNum}` : '';
|
|
51
51
|
const moreToRead = haveTotal ? emittedLast < totalNum : finiteLast;
|
|
52
|
+
// Anti-fragmentation: modest remainders get the exact one-window
|
|
53
|
+
// continuation (offset+limit) instead of an open-ended "continue".
|
|
54
|
+
const _remaining = haveTotal ? totalNum - emittedLast : null;
|
|
52
55
|
const continuationPart = moreToRead && Number.isFinite(emittedLast)
|
|
53
|
-
?
|
|
56
|
+
? (_remaining !== null && _remaining > 0 && _remaining <= 600
|
|
57
|
+
? `; ${_remaining} lines left — take the rest in ONE window: offset:${emittedLast}, limit:${_remaining}`
|
|
58
|
+
: `; pass offset:${emittedLast} to continue`)
|
|
54
59
|
: '';
|
|
55
60
|
const newFooter = `[lines ${firstLine}-${emittedLast}${totalPart}${continuationPart}]`;
|
|
56
61
|
return kept.join('\n') + (kept.length ? '\n' : '') + newFooter;
|
|
@@ -49,6 +49,20 @@ async function detectReadEncoding(fullPath) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Reactive anti-fragmentation merge: per-session (readStateScope) memory of
|
|
53
|
+
// the last requested window per file. When the next windowed read of the
|
|
54
|
+
// same file starts shortly AFTER the previous window (gap <= 200 lines) and
|
|
55
|
+
// the widened result stays modest (<= 400 lines), widen the request to
|
|
56
|
+
// include the gap so the model never needs a third paging call.
|
|
57
|
+
const _readWindowHistoryByScope = new WeakMap();
|
|
58
|
+
function _readWindowHistory(scope) {
|
|
59
|
+
let m = _readWindowHistoryByScope.get(scope);
|
|
60
|
+
if (!m) { m = new Map(); _readWindowHistoryByScope.set(scope, m); }
|
|
61
|
+
return m;
|
|
62
|
+
}
|
|
63
|
+
const _WIDEN_GAP_MAX_LINES = 200;
|
|
64
|
+
const _WIDEN_RESULT_MAX_LINES = 400;
|
|
65
|
+
|
|
52
66
|
export async function executeSingleReadTool(args, workDir, readStateScope, options = {}, helpers = {}) {
|
|
53
67
|
const {
|
|
54
68
|
appendReadContextAdvisory,
|
|
@@ -136,12 +150,13 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
136
150
|
const hasLimitArg = args.limit !== undefined && args.limit !== null;
|
|
137
151
|
const hasRangeArgs = hasOffsetArg || hasLimitArg;
|
|
138
152
|
const wantFull = args.full === true;
|
|
139
|
-
|
|
153
|
+
let offset = parseOffsetArg(args.offset);
|
|
140
154
|
// full:true bypasses the default 2000-line cap so the whole file
|
|
141
155
|
// can be returned in one call; the byte-cap path below still
|
|
142
156
|
// emits a compact truncation marker when rendered bytes overflow
|
|
143
157
|
// READ_MAX_OUTPUT_BYTES.
|
|
144
|
-
|
|
158
|
+
let limit = parseLineLimitArg(args.limit, wantFull ? Infinity : 2000);
|
|
159
|
+
let _widenNote = '';
|
|
145
160
|
let st;
|
|
146
161
|
let _statErr;
|
|
147
162
|
try {
|
|
@@ -200,6 +215,23 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
200
215
|
}
|
|
201
216
|
} catch { /* lstat failure is non-fatal; the original `st` is authoritative */ }
|
|
202
217
|
}
|
|
218
|
+
// Reactive widen + window-history bookkeeping (ranged text reads only).
|
|
219
|
+
if (st && readStateScope && typeof readStateScope === 'object'
|
|
220
|
+
&& hasOffsetArg && hasLimitArg && Number.isFinite(limit) && limit > 0) {
|
|
221
|
+
const _wins = _readWindowHistory(readStateScope);
|
|
222
|
+
const prev = _wins.get(fullPath);
|
|
223
|
+
if (prev && offset > prev.end && limit <= _WIDEN_RESULT_MAX_LINES) {
|
|
224
|
+
const gap = offset - prev.end; // unread lines between windows
|
|
225
|
+
const mergedLines = (offset + limit) - prev.end;
|
|
226
|
+
if (gap <= _WIDEN_GAP_MAX_LINES && mergedLines <= _WIDEN_RESULT_MAX_LINES) {
|
|
227
|
+
_widenNote = `[widened: included the ${gap}-line gap after your last window (lines ${prev.start}-${prev.end}) — one span instead of another page]`;
|
|
228
|
+
offset = prev.end;
|
|
229
|
+
limit = mergedLines;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
_wins.set(fullPath, { start: offset + 1, end: offset + limit });
|
|
233
|
+
if (_wins.size > 64) _wins.delete(_wins.keys().next().value);
|
|
234
|
+
}
|
|
203
235
|
if (!st) {
|
|
204
236
|
const err = _statErr;
|
|
205
237
|
const redirected = await tryReadFamilyEnoentRedirect({
|
|
@@ -639,8 +671,18 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
639
671
|
} else if (Buffer.byteLength(rendered, 'utf8') <= READ_MAX_OUTPUT_BYTES) {
|
|
640
672
|
const emittedStart = offset + 1;
|
|
641
673
|
const emittedEnd = offset + sliced.length;
|
|
642
|
-
|
|
674
|
+
// Anti-fragmentation: when the remainder is modest, hand the
|
|
675
|
+
// model the exact one-window continuation instead of inviting
|
|
676
|
+
// another small page.
|
|
677
|
+
const _remaining = lineCount - emittedEnd;
|
|
678
|
+
const _cont = emittedEnd < lineCount
|
|
679
|
+
? (_remaining <= 600
|
|
680
|
+
? `; ${_remaining} lines left — take the rest in ONE window: offset:${emittedEnd}, limit:${_remaining}`
|
|
681
|
+
: `; pass offset:${emittedEnd} to continue`)
|
|
682
|
+
: '';
|
|
683
|
+
const footer = `[lines ${emittedStart}-${emittedEnd} of ${lineCount}${_cont}]`;
|
|
643
684
|
out += `${out ? '\n' : ''}${footer}`;
|
|
685
|
+
if (_widenNote) out += `\n${_widenNote}`;
|
|
644
686
|
}
|
|
645
687
|
}
|
|
646
688
|
// Smart cap. Only engages when the caller asked for
|
|
@@ -163,10 +163,13 @@ export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'E
|
|
|
163
163
|
const parentRel = nearestExistingParentRel(workDir, missingPath);
|
|
164
164
|
if (parentRel) {
|
|
165
165
|
const resolvedParent = parentRel === '.' ? workDir : resolveAgainstCwd(parentRel, workDir);
|
|
166
|
-
|
|
166
|
+
// Compact hint: drop log/artifact noise, keep at most 3 candidates.
|
|
167
|
+
const siblings = listSiblings(resolvedParent, 12)
|
|
168
|
+
.filter((n) => !/\.(log|log\.\d+|tmp|bak)$/i.test(n))
|
|
169
|
+
.slice(0, 3);
|
|
167
170
|
if (siblings.length) {
|
|
168
171
|
const shown = parentRel === '.' ? '.' : normalizeOutputPath(parentRel);
|
|
169
|
-
return ` Not found
|
|
172
|
+
return ` Not found; under "${shown}" try: ${siblings.map((n) => `"${n}"`).join(', ')}.`;
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
}
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
normalizeGrepLine,
|
|
54
54
|
splitGrepCountPrefix,
|
|
55
55
|
splitGrepLinePrefix,
|
|
56
|
+
splitGrepLineNumberOnlyPrefix,
|
|
56
57
|
} from './grep-formatting.mjs';
|
|
57
58
|
import {
|
|
58
59
|
cacheGet,
|
|
@@ -66,6 +67,8 @@ import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRege
|
|
|
66
67
|
// Default surrounding-lines window applied by output_mode:'content_with_context'
|
|
67
68
|
// when the caller does not pass an explicit -A/-B/-C/context. Sized to cover a
|
|
68
69
|
// typical function/block so a match arrives readable without a follow-up read.
|
|
70
|
+
// Explicit -A/-B/-C is honored up to the generic GREP_CONTEXT_MAX (no tighter
|
|
71
|
+
// context-mode clamp by policy); only head_limit blocks are context-clamped.
|
|
69
72
|
const GREP_AUTO_CONTEXT_LINES = 25;
|
|
70
73
|
|
|
71
74
|
const MIXDOG_GREP_CASE_HINT_PROBE = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_GREP_CASE_HINT_PROBE || ''));
|
|
@@ -361,12 +364,116 @@ function buildGrepChunkMergePrefix(patternChunkCount, truncated, aggregateBudget
|
|
|
361
364
|
return `[${parts.join('; ')}]\n`;
|
|
362
365
|
}
|
|
363
366
|
|
|
367
|
+
// --- context-mode match-block windowing (Parts 2 & 3) ---------------------
|
|
368
|
+
// In context mode (explicit -A/-B/-C or content_with_context auto), head_limit
|
|
369
|
+
// and offset count MATCH BLOCKS, not raw output lines, and truncation keeps a
|
|
370
|
+
// head+tail slice with a middle marker instead of dropping the tail.
|
|
371
|
+
function grepBlockMatchAnchor(line, filenameOmitted) {
|
|
372
|
+
if (filenameOmitted) {
|
|
373
|
+
const p = splitGrepLineNumberOnlyPrefix(line);
|
|
374
|
+
return p && p.delimiter === ':' ? `#${p.lineNo}` : '';
|
|
375
|
+
}
|
|
376
|
+
const s = splitGrepLinePrefix(line);
|
|
377
|
+
return s && s.delimiter === ':' ? `${s.path}\0${s.lineNo}` : '';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function parseGrepContextBlocks(lines, filenameOmitted) {
|
|
381
|
+
const blocks = [];
|
|
382
|
+
let pending = [];
|
|
383
|
+
let i = 0;
|
|
384
|
+
while (i < lines.length) {
|
|
385
|
+
const line = lines[i];
|
|
386
|
+
if (line === '--') { pending = []; i++; continue; }
|
|
387
|
+
const anchor = grepBlockMatchAnchor(line, filenameOmitted);
|
|
388
|
+
if (anchor) {
|
|
389
|
+
const blockLines = pending.concat([line]);
|
|
390
|
+
pending = [];
|
|
391
|
+
i++;
|
|
392
|
+
while (i < lines.length) {
|
|
393
|
+
const next = lines[i];
|
|
394
|
+
if (next === '--' || grepBlockMatchAnchor(next, filenameOmitted)) break;
|
|
395
|
+
blockLines.push(next);
|
|
396
|
+
i++;
|
|
397
|
+
}
|
|
398
|
+
blocks.push({ anchor, lines: blockLines });
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
pending.push(line);
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
return blocks;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function formatGrepContextOutput({ allLines, workDir, outputMode, filenameOmitted, headLimit, offset, totalKnown = true }) {
|
|
408
|
+
const norm = allLines.map((l) => (l === '--' ? '--' : relativeGrepLine(l, workDir, false, outputMode, filenameOmitted)));
|
|
409
|
+
const blocks = parseGrepContextBlocks(norm, filenameOmitted);
|
|
410
|
+
const total = blocks.length;
|
|
411
|
+
if (total === 0) return { text: '', total: 0, shown: 0, omitted: 0 };
|
|
412
|
+
// Finding 2/3: denominator is the PRE-offset grand total; on a partial rg
|
|
413
|
+
// read (stdout cap / stream cap) it is a lower bound, so print ">=T".
|
|
414
|
+
const totalStr = totalKnown ? `${total}` : `>=${total}`;
|
|
415
|
+
const afterOffset = offset > 0 ? blocks.slice(offset) : blocks;
|
|
416
|
+
if (afterOffset.length === 0) {
|
|
417
|
+
// On a partial stream (line cap / timeout) the parsed blocks are a
|
|
418
|
+
// lower bound — an offset beyond them is NOT proven past the last
|
|
419
|
+
// match, so steer toward narrowing instead of claiming "past end".
|
|
420
|
+
const text = totalKnown
|
|
421
|
+
? `[Showing 0 of ${totalStr} matches; offset ${offset} past end]`
|
|
422
|
+
: `[Showing 0 of ${totalStr} matches (results partial); offset ${offset} is beyond the streamed window — matches past it may exist. Narrow path/glob/pattern instead of paging deeper.]`;
|
|
423
|
+
return { text, total, shown: 0, omitted: 0 };
|
|
424
|
+
}
|
|
425
|
+
const shown = headLimit === Infinity ? afterOffset.length : Math.min(headLimit, afterOffset.length);
|
|
426
|
+
const omitted = afterOffset.length - shown;
|
|
427
|
+
const render = (arr) => arr.map((b) => b.lines.join('\n'));
|
|
428
|
+
let segments;
|
|
429
|
+
let nextOffset = offset + shown;
|
|
430
|
+
if (omitted > 0 && shown > 0) {
|
|
431
|
+
// Keep head + tail so both ends of the match range stay visible.
|
|
432
|
+
const headCount = Math.max(1, Math.ceil(shown / 2));
|
|
433
|
+
const tailCount = shown - headCount;
|
|
434
|
+
const head = render(afterOffset.slice(0, headCount));
|
|
435
|
+
const tail = tailCount > 0 ? render(afterOffset.slice(afterOffset.length - tailCount)) : [];
|
|
436
|
+
segments = [...head, `…${omitted} matches omitted…`, ...tail];
|
|
437
|
+
// Paging must resume at the first OMITTED block (right after the head
|
|
438
|
+
// slice): offset+shown would permanently skip the middle blocks that
|
|
439
|
+
// the tail slice displaced. Tail blocks re-appear on later pages —
|
|
440
|
+
// duplication is acceptable, silent loss is not.
|
|
441
|
+
nextOffset = offset + headCount;
|
|
442
|
+
} else {
|
|
443
|
+
segments = render(afterOffset.slice(0, shown));
|
|
444
|
+
}
|
|
445
|
+
const notice = (omitted > 0 || !totalKnown)
|
|
446
|
+
? `\n[Showing ${shown} of ${totalStr} matches${totalKnown ? '' : ' (results partial)'}; pass offset:${nextOffset} for more]`
|
|
447
|
+
: '';
|
|
448
|
+
return { text: segments.join('\n--\n') + notice, total, shown, omitted };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Part 1: drop path:line match lines already emitted by an earlier pattern in
|
|
452
|
+
// a pattern[] fan-out. Context ('-') lines and non-match lines pass through.
|
|
453
|
+
function dedupeFanoutMatchLines(body, seen) {
|
|
454
|
+
const text = String(body);
|
|
455
|
+
if (/^Error:/.test(text)) return text;
|
|
456
|
+
const out = [];
|
|
457
|
+
for (const line of text.split('\n')) {
|
|
458
|
+
const s = splitGrepLinePrefix(line);
|
|
459
|
+
if (s && s.delimiter === ':') {
|
|
460
|
+
const key = `${s.path}\0${s.lineNo}`;
|
|
461
|
+
if (seen.has(key)) continue;
|
|
462
|
+
seen.add(key);
|
|
463
|
+
}
|
|
464
|
+
out.push(line);
|
|
465
|
+
}
|
|
466
|
+
return out.join('\n');
|
|
467
|
+
}
|
|
468
|
+
|
|
364
469
|
function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns: _patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath: _grepResolvedPath, workDir, globPatterns: _globPatterns, fileType: _fileType, filenameOmitted = false, prefix = '', broadAdvisory: _broadAdvisory = true, disableContentGrouping = false }) {
|
|
365
470
|
const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
|
|
366
471
|
const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
|
|
367
472
|
const remaining = Math.max(0, totalWindowed - lines.length);
|
|
368
473
|
const shown = lines.length;
|
|
369
|
-
|
|
474
|
+
// Finding 3: PRE-offset grand total so the denominator matches the
|
|
475
|
+
// context-mode notice (offset==0 leaves this unchanged).
|
|
476
|
+
const total = offset + totalWindowed;
|
|
370
477
|
const scopePath = JSON.stringify(normalizeOutputPath(searchPath));
|
|
371
478
|
const truncated = (remaining > 0 || !totalKnown)
|
|
372
479
|
? (totalKnown
|
|
@@ -604,6 +711,33 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
604
711
|
if (fileTypes.length > 1) fileType = fileTypes;
|
|
605
712
|
else if (fileTypes.length === 1) fileType = fileTypes[0];
|
|
606
713
|
|
|
714
|
+
// Part 1: pattern[] fan-out. Two or more patterns in a content search run
|
|
715
|
+
// as INDEPENDENT greps (mirroring the path[] batching above): each pattern
|
|
716
|
+
// keeps its own full head_limit budget and its own truncation notice, and
|
|
717
|
+
// identical path:line match lines are de-duplicated across patterns. The
|
|
718
|
+
// single-pattern path and the non-content modes (files_with_matches/count)
|
|
719
|
+
// keep the combined single-rg behavior. `_grepPatternFanout` guards the
|
|
720
|
+
// recursive single-pattern calls from re-entering the fan-out; the internal
|
|
721
|
+
// chunk-merge recursion is likewise skipped.
|
|
722
|
+
if (patterns.length > 1
|
|
723
|
+
&& outputMode === 'content'
|
|
724
|
+
&& !options._grepChunkMerge
|
|
725
|
+
&& !options._grepPatternFanout) {
|
|
726
|
+
const seen = new Set();
|
|
727
|
+
const subOptions = { ...options, _grepPatternFanout: true };
|
|
728
|
+
const parts = [];
|
|
729
|
+
for (const p of patterns) {
|
|
730
|
+
let sub;
|
|
731
|
+
try {
|
|
732
|
+
sub = await executeGrepTool({ ...args, pattern: p }, workDir, executeChildBuiltinTool, readStateScope, subOptions);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
sub = `Error: ${err && err.message ? err.message : err}`;
|
|
735
|
+
}
|
|
736
|
+
parts.push(`# grep pattern:${JSON.stringify(p)}\n${dedupeFanoutMatchLines(sub, seen)}`);
|
|
737
|
+
}
|
|
738
|
+
return patternCapNote + parts.join('\n\n');
|
|
739
|
+
}
|
|
740
|
+
|
|
607
741
|
const patternChunkCap = multilineMode ? GREP_MULTILINE_PATTERN_CAP : GREP_ARRAY_PATTERN_CAP;
|
|
608
742
|
if (patterns.length > patternChunkCap) {
|
|
609
743
|
const patternChunks = chunkPatternList(patterns, patternChunkCap);
|
|
@@ -676,7 +810,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
676
810
|
});
|
|
677
811
|
}
|
|
678
812
|
|
|
679
|
-
const forceGrepFilename = !!options._grepChunkMerge;
|
|
813
|
+
const forceGrepFilename = !!options._grepChunkMerge || !!options._grepPatternFanout;
|
|
680
814
|
const cacheKey = buildGrepCacheKey({
|
|
681
815
|
patterns,
|
|
682
816
|
searchPath: normalizeOutputPath(grepResolvedPath),
|
|
@@ -775,6 +909,64 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
775
909
|
pcre2: pcre2Mode,
|
|
776
910
|
withFilename: forceGrepFilename,
|
|
777
911
|
});
|
|
912
|
+
// Parts 2 & 3: context mode windows MATCH BLOCKS (not raw lines); the
|
|
913
|
+
// total match count and tail blocks come from the streamed (memory-
|
|
914
|
+
// bounded) collection below.
|
|
915
|
+
const contextMode = outputMode === 'content' && (beforeN > 0 || afterN > 0 || contextN > 0);
|
|
916
|
+
if (contextMode) {
|
|
917
|
+
// Finding 1: stream only enough lines to satisfy the block window
|
|
918
|
+
// (offset + head_limit + tail reserve), so rg is stopped early and a
|
|
919
|
+
// broad content_with_context never retains a full 20MB stdout copy.
|
|
920
|
+
// A cap hit → complete:false → partial (lower-bound) phrasing below.
|
|
921
|
+
const GREP_CONTEXT_LINE_HARD_CAP = 4000;
|
|
922
|
+
const perBlock = 2 + (beforeN || 0) + (afterN || 0) + 2 * (contextN || 0);
|
|
923
|
+
const blockBudget = headLimit === Infinity ? Infinity : offset + headLimit + 4;
|
|
924
|
+
const lineCap = blockBudget === Infinity
|
|
925
|
+
? GREP_CONTEXT_LINE_HARD_CAP
|
|
926
|
+
: Math.min(GREP_CONTEXT_LINE_HARD_CAP, Math.max(200, blockBudget * Math.max(1, perBlock) + 8));
|
|
927
|
+
let ctxPartialSuffix = '';
|
|
928
|
+
const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd }, { offset: 0, limit: lineCap, summaryLimit: 0 });
|
|
929
|
+
const allLines = streamed.lines;
|
|
930
|
+
let ctxTotalKnown = streamed.complete;
|
|
931
|
+
if (streamed.partial) {
|
|
932
|
+
ctxTotalKnown = false;
|
|
933
|
+
ctxPartialSuffix = streamed.timeout
|
|
934
|
+
? '\n[warning] rg timed out; partial results shown. Narrow path/glob/pattern for a complete result.'
|
|
935
|
+
: streamed.rgStderr
|
|
936
|
+
? `\n[warning] rg exit 2 (partial results): ${String(streamed.rgStderr).trim().slice(0, 300)}`
|
|
937
|
+
: '\n[warning] rg exit 2 (partial results)';
|
|
938
|
+
} else if (!streamed.complete) {
|
|
939
|
+
ctxPartialSuffix = `\n[warning] context output capped at ${lineCap} lines to bound memory; results partial — narrow path/glob/pattern for the full match set.`;
|
|
940
|
+
}
|
|
941
|
+
const ctx = formatGrepContextOutput({
|
|
942
|
+
allLines,
|
|
943
|
+
workDir,
|
|
944
|
+
outputMode,
|
|
945
|
+
filenameOmitted,
|
|
946
|
+
headLimit,
|
|
947
|
+
offset,
|
|
948
|
+
totalKnown: ctxTotalKnown,
|
|
949
|
+
});
|
|
950
|
+
let ctxBody = ctx.text;
|
|
951
|
+
if (!ctxBody) {
|
|
952
|
+
const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
|
|
953
|
+
const globStr = normalizedGlobPatterns.length > 0 ? ` glob=${JSON.stringify(normalizedGlobPatterns)}` : '';
|
|
954
|
+
const pathInfo = grepStat.isDirectory() ? 'path exists (dir)' : 'path exists (file)';
|
|
955
|
+
ctxBody = `(no matches) pattern=${patternStr} path=${searchPath}${globStr}; ${pathInfo}`;
|
|
956
|
+
}
|
|
957
|
+
const ctxOut = patternCapNote + ctxBody + ctxPartialSuffix;
|
|
958
|
+
if (options?.scopedCacheOutcome && (!ctxTotalKnown || ctx.omitted > 0)) {
|
|
959
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
960
|
+
}
|
|
961
|
+
recordGrepReadSnapshot(grepStat);
|
|
962
|
+
if (ctxTotalKnown && ctx.omitted === 0) {
|
|
963
|
+
cacheSet(cacheKey, ctxOut, { scopes: [grepResolvedPath] });
|
|
964
|
+
}
|
|
965
|
+
if (typeof options?.onProgress === 'function') {
|
|
966
|
+
try { options.onProgress(`found ${ctx.total} matches`); } catch { /* best-effort */ }
|
|
967
|
+
}
|
|
968
|
+
return ctxOut;
|
|
969
|
+
}
|
|
778
970
|
let windowed;
|
|
779
971
|
let totalWindowed = 0;
|
|
780
972
|
let totalKnown = true;
|
|
@@ -832,7 +1024,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
832
1024
|
globPatterns: normalizedGlobPatterns,
|
|
833
1025
|
fileType,
|
|
834
1026
|
filenameOmitted,
|
|
835
|
-
disableContentGrouping: !!options._grepChunkMerge,
|
|
1027
|
+
disableContentGrouping: !!options._grepChunkMerge || !!options._grepPatternFanout,
|
|
836
1028
|
});
|
|
837
1029
|
if (!body) {
|
|
838
1030
|
const pathInfo = grepStat.isDirectory() ? 'path exists (dir)' : 'path exists (file)';
|
|
@@ -297,6 +297,7 @@ const EXTERNAL_TOOL_REDIRECTS = new Map([
|
|
|
297
297
|
['codebase_search', 'use the `grep` or `code_graph` tool'],
|
|
298
298
|
['semanticsearch', 'use the `grep` or `code_graph` tool'],
|
|
299
299
|
['semantic_search', 'use the `grep` or `code_graph` tool'],
|
|
300
|
+
['explore', 'not available in this role — use `grep` or `code_graph` for repo search'],
|
|
300
301
|
]);
|
|
301
302
|
|
|
302
303
|
export function canonicalizeBuiltinToolName(name) {
|
|
@@ -520,7 +521,22 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
|
|
|
520
521
|
}
|
|
521
522
|
})();
|
|
522
523
|
const _withNudge = _maybeAppendGrepSweepNudge(toolName, args, _toolResult, readStateScope);
|
|
523
|
-
return capToolOutput(_withNudge, options);
|
|
524
|
+
return capToolOutput(_appendClampNotices(args, _withNudge), options);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Surface arg-guard clamp notices (args._clampNotices, see pushClampNotice in
|
|
528
|
+
// arg-guard.mjs) on the result text so the caller learns its request was
|
|
529
|
+
// capped. Delete to consume: prevents double-append when a child-builtin
|
|
530
|
+
// result is embedded in the parent's, and leaves no `_clampNotices` residue
|
|
531
|
+
// on the recorded tool-call args in the transcript.
|
|
532
|
+
function _appendClampNotices(args, result) {
|
|
533
|
+
let notices = null;
|
|
534
|
+
if (args && Array.isArray(args._clampNotices)) {
|
|
535
|
+
notices = args._clampNotices;
|
|
536
|
+
delete args._clampNotices;
|
|
537
|
+
}
|
|
538
|
+
if (!notices || notices.length === 0 || typeof result !== 'string') return result;
|
|
539
|
+
return `${result}\n\n${notices.map((n) => `[arg-guard] ${n}`).join('\n')}`;
|
|
524
540
|
}
|
|
525
541
|
/**
|
|
526
542
|
* Check if a tool name is a builtin tool.
|
|
@@ -51,6 +51,36 @@ function _collectGraphSymbolList(args) {
|
|
|
51
51
|
|
|
52
52
|
const CODE_GRAPH_FILE_BATCH_CAP = 20;
|
|
53
53
|
|
|
54
|
+
// Absorb: file/files arriving as a JSON-stringified array
|
|
55
|
+
// (file:"[\"a.mjs\",\"b.mjs\"]") — parse to a real array so the graph lookup
|
|
56
|
+
// batches per file instead of treating the JSON text as one (missing) path.
|
|
57
|
+
function _parseJsonArrayString(v) {
|
|
58
|
+
if (typeof v !== 'string') return null;
|
|
59
|
+
const t = v.trim();
|
|
60
|
+
if (!t.startsWith('[') || !t.endsWith(']')) return null;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(t);
|
|
63
|
+
if (Array.isArray(parsed)) return parsed.map((x) => String(x || '').trim()).filter(Boolean);
|
|
64
|
+
} catch { /* not JSON — leave untouched */ }
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _normalizeGraphFileArgs(args) {
|
|
69
|
+
if (!args || typeof args !== 'object') return args;
|
|
70
|
+
const fileArr = _parseJsonArrayString(args.file);
|
|
71
|
+
const filesArr = _parseJsonArrayString(args.files);
|
|
72
|
+
if (!fileArr && !filesArr) return args;
|
|
73
|
+
const out = { ...args };
|
|
74
|
+
if (fileArr) { out.files = Array.isArray(out.files) ? [...fileArr, ...out.files] : fileArr; delete out.file; }
|
|
75
|
+
if (filesArr) out.files = filesArr;
|
|
76
|
+
// Collapse a lone entry back to the single-file field for the fast path.
|
|
77
|
+
if (Array.isArray(out.files) && out.files.length === 1 && !out.file) {
|
|
78
|
+
out.file = out.files[0];
|
|
79
|
+
delete out.files;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
54
84
|
function _collectGraphFileList(args) {
|
|
55
85
|
const split = (s) => String(s || '').split(/,+/).map((t) => t.trim()).filter(Boolean);
|
|
56
86
|
const list = [...new Set([
|
|
@@ -70,6 +100,13 @@ async function codeGraph(args, cwd, signal = null, options = {}) {
|
|
|
70
100
|
let mode = String(args?.mode || '').trim();
|
|
71
101
|
if (!mode) throw new Error('code_graph: "mode" is required');
|
|
72
102
|
if (mode === 'search') mode = 'symbol_search';
|
|
103
|
+
// Name-only "symbols" calls (symbols[]/symbol without a file) are symbol
|
|
104
|
+
// lookups, not a file outline — absorb into symbol_search instead of
|
|
105
|
+
// erroring "file not found in graph: (missing file)".
|
|
106
|
+
if (mode === 'symbols' && !String(args?.file || '').trim()
|
|
107
|
+
&& ((Array.isArray(args?.symbols) && args.symbols.length) || String(args?.symbol || '').trim())) {
|
|
108
|
+
mode = 'symbol_search';
|
|
109
|
+
}
|
|
73
110
|
|
|
74
111
|
if (mode === 'prewarm') {
|
|
75
112
|
const _splitMulti = (s) => String(s || '').split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
|
|
@@ -436,6 +473,7 @@ export async function resolveSymbolReadSpan(cwd, { symbol, path = null, language
|
|
|
436
473
|
|
|
437
474
|
export async function executeCodeGraphTool(name, args, cwd, signal = null, options = {}) {
|
|
438
475
|
if (!cwd) throw new Error('find_symbol/code_graph requires cwd — caller did not provide a working directory');
|
|
476
|
+
args = _normalizeGraphFileArgs(args);
|
|
439
477
|
const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
|
|
440
478
|
const baseCwd = (args && typeof args.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
|
|
441
479
|
let effectiveCwd = baseCwd;
|
|
@@ -3,13 +3,13 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
3
3
|
name: 'code_graph',
|
|
4
4
|
title: 'Code Graph',
|
|
5
5
|
annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
6
|
-
description: 'Repo-local code structure/flow lookup
|
|
6
|
+
description: 'Repo-local code structure/flow lookup (not web): symbols/references/calls/deps. Batch targets as symbols[]/files[] in one call.',
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
|
-
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers'], description: 'Repo-local operation
|
|
11
|
-
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file(s)
|
|
12
|
-
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s)
|
|
10
|
+
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers'], description: 'Repo-local graph operation.' },
|
|
11
|
+
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file(s); array fans out per file. `file` alias too.' },
|
|
12
|
+
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s); array batches in one call. `symbol` alias too.' },
|
|
13
13
|
symbol: { type: 'string', description: 'Singular alias for symbols.' },
|
|
14
14
|
file: { type: 'string', description: 'Singular alias for files.' },
|
|
15
15
|
body: { type: 'boolean', description: 'Include body.' },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Shell state carry-over for the one-shot shell tool.
|
|
3
3
|
//
|
|
4
4
|
// No live shell process is kept. Instead, after each sync command we chain a
|
|
5
5
|
// trailing cwd probe that writes the final working directory to a per-session
|
|
@@ -5,13 +5,13 @@ import { join, sep } from "path";
|
|
|
5
5
|
|
|
6
6
|
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
7
7
|
|
|
8
|
-
async function downloadSingleAttachment(att, inboxDir) {
|
|
8
|
+
async function downloadSingleAttachment(att, inboxDir, { timeoutMs = 180_000 } = {}) {
|
|
9
9
|
if (att.size > MAX_ATTACHMENT_BYTES) {
|
|
10
10
|
throw new Error(
|
|
11
11
|
`attachment too large: ${(att.size / 1024 / 1024).toFixed(1)}MB, max ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB`
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
|
-
const res = await fetch(att.url, { signal: AbortSignal.timeout(
|
|
14
|
+
const res = await fetch(att.url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
15
15
|
if (!res.ok) {
|
|
16
16
|
throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
17
17
|
}
|