mixdog 0.9.0 → 0.9.2
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 +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -161,7 +161,7 @@ function _sessionSummary(session) {
|
|
|
161
161
|
closed: session.closed === true,
|
|
162
162
|
status: String(session.status || (session.closed === true ? 'closed' : 'idle')),
|
|
163
163
|
owner: session.owner || 'user',
|
|
164
|
-
|
|
164
|
+
agent: session.agent || null,
|
|
165
165
|
sourceType: session.sourceType || null,
|
|
166
166
|
sourceName: session.sourceName || null,
|
|
167
167
|
scopeKey: session.scopeKey || null,
|
|
@@ -191,7 +191,7 @@ function _normalizeSummaryRow(row) {
|
|
|
191
191
|
closed: row.closed === true,
|
|
192
192
|
status: String(row.status || (row.closed === true ? 'closed' : 'idle')),
|
|
193
193
|
owner: row.owner || 'user',
|
|
194
|
-
|
|
194
|
+
agent: row.agent || null,
|
|
195
195
|
sourceType: row.sourceType || null,
|
|
196
196
|
sourceName: row.sourceName || null,
|
|
197
197
|
scopeKey: row.scopeKey || null,
|
|
@@ -655,11 +655,11 @@ export function markSessionClosed(id, reason = 'manual') {
|
|
|
655
655
|
const _lifeMs = (typeof existing.createdAt === 'number' && existing.createdAt > 0)
|
|
656
656
|
? (tombstone.updatedAt - existing.createdAt)
|
|
657
657
|
: 0;
|
|
658
|
-
const
|
|
658
|
+
const _agent = existing.agent || '-';
|
|
659
659
|
const _owner = existing.owner || '-';
|
|
660
660
|
void fsp.appendFile(
|
|
661
661
|
join(_dataDir, 'tool-events.log'),
|
|
662
|
-
`[${_ts}] [session-close] owner=${_owner}
|
|
662
|
+
`[${_ts}] [session-close] owner=${_owner} agent=${_agent} reason=${reason} lifeMs=${_lifeMs} id=${id}\n`,
|
|
663
663
|
).catch(() => {});
|
|
664
664
|
}
|
|
665
665
|
} catch { /* logger never breaks the close path */ }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-envelope.mjs — general optional `newMessages` channel for tool results.
|
|
3
|
+
*
|
|
4
|
+
* A tool MAY return either:
|
|
5
|
+
* - legacy: a string (or existing structured media object) — unchanged, OR
|
|
6
|
+
* - an envelope object:
|
|
7
|
+
* { __toolEnvelope: true, result: <string|structured>,
|
|
8
|
+
* newMessages: [{ role:'user', content:'...' }, ...] }
|
|
9
|
+
*
|
|
10
|
+
* The `__toolEnvelope` marker is deliberately namespaced so it can NEVER be
|
|
11
|
+
* confused with the existing structured media content objects that
|
|
12
|
+
* PostToolUse hooks/tests preserve (those use `{ content: [...] }`, never
|
|
13
|
+
* `__toolEnvelope`). `normalizeToolEnvelope` is the single split point: it
|
|
14
|
+
* returns `{ result, newMessages }` so ALL downstream loop code sees only
|
|
15
|
+
* `result` and the loop alone is responsible for flushing `newMessages`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const TOOL_ENVELOPE_MARKER = '__toolEnvelope';
|
|
19
|
+
|
|
20
|
+
export function isToolEnvelope(value) {
|
|
21
|
+
return !!value
|
|
22
|
+
&& typeof value === 'object'
|
|
23
|
+
&& !Array.isArray(value)
|
|
24
|
+
&& value[TOOL_ENVELOPE_MARKER] === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isValidNewMessage(m) {
|
|
28
|
+
return !!m
|
|
29
|
+
&& typeof m === 'object'
|
|
30
|
+
&& m.role === 'user'
|
|
31
|
+
&& typeof m.content === 'string'
|
|
32
|
+
&& m.content.length > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a tool-result envelope. `result` is the short stub/data the model
|
|
37
|
+
* sees as the tool_result; `newMessages` are appended (as their own
|
|
38
|
+
* messages, e.g. role:'user') AFTER the batch's tool results.
|
|
39
|
+
*/
|
|
40
|
+
export function makeToolEnvelope(result, newMessages = []) {
|
|
41
|
+
return {
|
|
42
|
+
[TOOL_ENVELOPE_MARKER]: true,
|
|
43
|
+
result,
|
|
44
|
+
newMessages: Array.isArray(newMessages) ? newMessages.filter(isValidNewMessage) : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Split a tool return value into `{ result, newMessages }`.
|
|
50
|
+
* - legacy string/object → { result: value, newMessages: [] }
|
|
51
|
+
* - envelope → { result, newMessages } (newMessages validated)
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeToolEnvelope(value) {
|
|
54
|
+
if (isToolEnvelope(value)) {
|
|
55
|
+
const newMessages = Array.isArray(value.newMessages)
|
|
56
|
+
? value.newMessages.filter(isValidNewMessage)
|
|
57
|
+
: [];
|
|
58
|
+
return { result: value.result, newMessages };
|
|
59
|
+
}
|
|
60
|
+
return { result: value, newMessages: [] };
|
|
61
|
+
}
|
|
@@ -21,11 +21,16 @@ export const TOOL_RESULT_OFFLOAD_PREFIX = '[tool output offloaded:';
|
|
|
21
21
|
// READ_MAX_SIZE_BYTES cap, so persisting to a sidecar to be re-read would be
|
|
22
22
|
// circular. These values keep context-rich IO tools from turning into "read
|
|
23
23
|
// saved output" loops while bounding the per-call inline footprint per CC.
|
|
24
|
+
// Skill / skill_view bodies stay inline for the same reason — offloading a
|
|
25
|
+
// loaded SKILL.md would force a read loop and defeat the loaded-skill guard.
|
|
24
26
|
const INLINE_THRESHOLD_BY_TOOL = new Map([
|
|
25
27
|
['read', Infinity],
|
|
26
28
|
['head', Infinity],
|
|
27
29
|
['tail', Infinity],
|
|
28
30
|
['diff', Infinity],
|
|
31
|
+
['skill', Infinity],
|
|
32
|
+
['skill_view', Infinity],
|
|
33
|
+
['skills_list', Infinity],
|
|
29
34
|
['grep', TOOL_RESULT_GREP_THRESHOLD_CHARS],
|
|
30
35
|
['glob', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
|
|
31
36
|
['list', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHiddenAgent } from './internal-agents.mjs';
|
|
2
2
|
|
|
3
3
|
const SECOND_MS = 1000;
|
|
4
4
|
const MIN_PROVIDER_TIMEOUT_MS = 30_000;
|
|
@@ -68,7 +68,13 @@ export const DEFAULT_ACTIVITY_HEARTBEAT_MS = resolveTimeoutMs(
|
|
|
68
68
|
|
|
69
69
|
export const PROVIDER_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
|
|
70
70
|
'MIXDOG_PROVIDER_FIRST_BYTE_TIMEOUT_MS',
|
|
71
|
-
|
|
71
|
+
// 2026-07 trace audit: heavy-worker claude-sonnet-5 xhigh TTFT distribution
|
|
72
|
+
// showed avg ~25s, max ~120s (15/75 requests over 60s) — the old 120s
|
|
73
|
+
// window sat exactly on the observed tail and killed/retried healthy slow
|
|
74
|
+
// first bytes (9 first-byte retries in one 75-iteration session, plus one
|
|
75
|
+
// full turn lost). 180s clears the tail while the mid-stream retry ladder
|
|
76
|
+
// and the agent stall watchdog still bound a truly dead socket.
|
|
77
|
+
Math.min(180_000, PROVIDER_MAX_BEFORE_WARN_MS),
|
|
72
78
|
{ minMs: MIN_PROVIDER_TIMEOUT_MS, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
73
79
|
);
|
|
74
80
|
|
|
@@ -125,6 +131,44 @@ export const PROVIDER_SSE_IDLE_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
125
131
|
{ minMs: 10_000, maxMs: STALL_WARN_MS },
|
|
126
132
|
);
|
|
127
133
|
|
|
134
|
+
// Semantic (last-meaningful-event) idle window. Distinct name from the
|
|
135
|
+
// transport-byte SSE idle so provider loops can key their mid-stream abort off
|
|
136
|
+
// SEMANTIC progress (message/content/tool deltas) rather than raw keepalive
|
|
137
|
+
// bytes (Anthropic `:ping`, comment frames). A truly silent stream — one that
|
|
138
|
+
// emits no semantic event for this window — trips it; a live extended-thinking
|
|
139
|
+
// stream (which emits thinking deltas) stays alive. Default 120s, floor 10s,
|
|
140
|
+
// env-overridable and disablable via MIXDOG_ENABLE_STREAM_WATCHDOG=0.
|
|
141
|
+
export const PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS = resolveTimeoutMs(
|
|
142
|
+
['MIXDOG_PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS', 'MIXDOG_PROVIDER_SSE_IDLE_TIMEOUT_MS'],
|
|
143
|
+
// Raised 120s→180s alongside PROVIDER_FIRST_BYTE_TIMEOUT_MS (see comment
|
|
144
|
+
// there): observed healthy inter-event gaps on xhigh reasoning streams
|
|
145
|
+
// brushed the old 120s window; stalls after a tool emit are unretryable,
|
|
146
|
+
// so a false trip there costs the whole turn.
|
|
147
|
+
180_000,
|
|
148
|
+
{ minMs: 10_000, maxMs: STALL_WARN_MS },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Named terminal error for a mid-stream SEMANTIC idle abort. Distinct from a
|
|
152
|
+
// user cancel (signal.aborted): the retry-classifier treats this as a terminal
|
|
153
|
+
// STREAM FAILURE so the owner (Lead) receives a failure notification instead of
|
|
154
|
+
// the task hanging. The message keeps "timed out after …ms of inactivity" so
|
|
155
|
+
// the SSE mid-stream classifier's text match (sse_idle_timeout) still fires,
|
|
156
|
+
// and code 'ESTREAMSTALL' routes it through the transient/notify path.
|
|
157
|
+
export function streamStalledError(label, timeoutMs, { emittedToolCall = false } = {}) {
|
|
158
|
+
const err = new Error(`${label} stream timed out after ${timeoutMs}ms of inactivity`);
|
|
159
|
+
err.name = 'StreamStalledError';
|
|
160
|
+
err.code = 'ESTREAMSTALL';
|
|
161
|
+
err.streamStalled = true;
|
|
162
|
+
// Double-dispatch guard (reviewer High): once a tool call has already been
|
|
163
|
+
// emitted this turn (including a recovered text-leaked call), retrying the
|
|
164
|
+
// request would RE-RUN that side-effecting tool. Mark such a stall
|
|
165
|
+
// unsafe-to-retry so withRetry() throws it straight through to the terminal
|
|
166
|
+
// notify path instead of replaying the turn. A stall BEFORE any tool emit is
|
|
167
|
+
// still safely retryable (nothing has executed yet).
|
|
168
|
+
if (emittedToolCall) err.unsafeToRetry = true;
|
|
169
|
+
return err;
|
|
170
|
+
}
|
|
171
|
+
|
|
128
172
|
export const PROVIDER_WS_HANDSHAKE_TIMEOUT_MS = resolveTimeoutMs(
|
|
129
173
|
'MIXDOG_PROVIDER_WS_HANDSHAKE_TIMEOUT_MS',
|
|
130
174
|
30_000,
|
|
@@ -137,19 +181,23 @@ export const PROVIDER_WS_ACQUIRE_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
137
181
|
{ minMs: 5_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
138
182
|
);
|
|
139
183
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
|
|
184
|
+
// Single inter-chunk idle timer, matching codex's DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
185
|
+
// (300s). codex resets one idle timer on every received WS frame; mixdog does
|
|
186
|
+
// the same (openai-oauth-ws messageHandler resets on every parsed event). There
|
|
187
|
+
// is deliberately no separate "first-meaningful" watchdog — a live stream
|
|
188
|
+
// (including server-side reasoning ACKed via response.created) keeps this timer
|
|
189
|
+
// fresh, and only true socket silence trips it. Env-tunable.
|
|
146
190
|
export const PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS = resolveTimeoutMs(
|
|
147
191
|
'MIXDOG_PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS',
|
|
148
|
-
|
|
149
|
-
{ minMs:
|
|
192
|
+
300_000,
|
|
193
|
+
{ minMs: MIN_PROVIDER_TIMEOUT_MS, maxMs: STALL_ABORT_MS },
|
|
150
194
|
);
|
|
151
195
|
|
|
152
|
-
|
|
196
|
+
// First retry has a small floor (250ms) instead of 0ms: an immediate reissue on
|
|
197
|
+
// a transient 5xx burst lets parallel workers thundering-herd the backend in
|
|
198
|
+
// lockstep (jitter alone can't decluster a 0ms base). Subsequent steps keep the
|
|
199
|
+
// exponential schedule. Env-tunable via the providers' retryJitterRatio.
|
|
200
|
+
export const PROVIDER_RETRY_BACKOFF_MS = Object.freeze([250, 1000, 2000, 4000, 8000]);
|
|
153
201
|
export const PROVIDER_RETRY_MAX_ATTEMPTS = PROVIDER_RETRY_BACKOFF_MS.length;
|
|
154
202
|
export const PROVIDER_RETRY_JITTER_RATIO = (() => {
|
|
155
203
|
const raw = process.env.MIXDOG_PROVIDER_RETRY_JITTER_RATIO;
|
|
@@ -158,8 +206,8 @@ export const PROVIDER_RETRY_JITTER_RATIO = (() => {
|
|
|
158
206
|
return 0.2;
|
|
159
207
|
})();
|
|
160
208
|
|
|
161
|
-
export function resolveAgentStallThresholds(
|
|
162
|
-
const cfg =
|
|
209
|
+
export function resolveAgentStallThresholds(agent, env = process.env) {
|
|
210
|
+
const cfg = agent ? getHiddenAgent(agent) : null;
|
|
163
211
|
const cfgAbort = cfg?.stallCap?.idleSeconds > 0 ? cfg.stallCap.idleSeconds : STALL_ABORT_S;
|
|
164
212
|
const envOverride = envThresholdSeconds(env);
|
|
165
213
|
const abort = envOverride != null ? envOverride : cfgAbort;
|
|
@@ -175,8 +223,8 @@ export function resolveAgentStallThresholds(role, env = process.env) {
|
|
|
175
223
|
return { warn, abort, firstByteAbort };
|
|
176
224
|
}
|
|
177
225
|
|
|
178
|
-
export function resolveAgentToolThresholdSeconds(
|
|
179
|
-
const cfg =
|
|
226
|
+
export function resolveAgentToolThresholdSeconds(agent, thresholdSeconds) {
|
|
227
|
+
const cfg = agent ? getHiddenAgent(agent) : null;
|
|
180
228
|
if (cfg?.stallCap?.toolRunningSeconds > 0) return cfg.stallCap.toolRunningSeconds;
|
|
181
229
|
return thresholdSeconds;
|
|
182
230
|
}
|
|
@@ -44,7 +44,7 @@ import { invalidateBuiltinResultCache, analyzeShellCommandEffects, preflightShel
|
|
|
44
44
|
import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph-state.mjs';
|
|
45
45
|
import { maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
|
|
46
46
|
import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
|
|
47
|
-
import { _captureTrackedMtimes, _trackedDriftNoteAfter,
|
|
47
|
+
import { _captureTrackedMtimes, _trackedDriftNoteAfter, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
|
|
48
48
|
import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
|
|
49
49
|
import { checkExecPolicyMessage } from './bash-policy-scan.mjs';
|
|
50
50
|
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
@@ -9,13 +9,22 @@
|
|
|
9
9
|
// callers may pass either spelling (e.g. `glob` alias for grep, or
|
|
10
10
|
// `file_path` alias for read.path).
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
export const GREP_CONTEXT_MAX = 12;
|
|
12
|
+
import { coerceShapeFlex, hasGlobMagic } from './path-utils.mjs';
|
|
14
13
|
|
|
14
|
+
const MAX_INT = 100000;
|
|
15
|
+
// Explicit grep context should be large enough to frame a function/block without
|
|
16
|
+
// letting one match explode into a huge tool result. `content_with_context` still
|
|
17
|
+
// defaults to 25 lines; this is only the upper bound for caller-supplied -A/-B/-C.
|
|
18
|
+
export const GREP_CONTEXT_MAX = 200;
|
|
19
|
+
|
|
20
|
+
// ripgrep-flavored aliases: models trained on `rg` emit short flags (-A/-B/-C),
|
|
21
|
+
// long flags (--after-context / --before-context / --context), or snake/camel
|
|
22
|
+
// spellings. All fold onto the canonical -A/-B/-C so a caller can write grep
|
|
23
|
+
// the way they would write ripgrep on the shell.
|
|
15
24
|
const GREP_CONTEXT_KEY_GROUPS = [
|
|
16
|
-
['-A', ['-A', 'A', 'after', 'after_context', 'afterContext']],
|
|
17
|
-
['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext']],
|
|
18
|
-
['-C', ['-C', 'C', 'context', 'context_lines', 'contextLines']],
|
|
25
|
+
['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
|
|
26
|
+
['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
|
|
27
|
+
['-C', ['-C', 'C', 'context', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
|
|
19
28
|
];
|
|
20
29
|
|
|
21
30
|
function grepContextKeyPresent(a, k) {
|
|
@@ -76,11 +85,6 @@ function isStringOrStringArray(v) {
|
|
|
76
85
|
return true;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
function hasBackslashPipe(value) {
|
|
80
|
-
if (typeof value === 'string') return false;
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
88
|
function hasUnsupportedRipgrepRegex(value) {
|
|
85
89
|
const values = Array.isArray(value) ? value : [value];
|
|
86
90
|
return values.some((item) => {
|
|
@@ -103,8 +107,35 @@ function isFiniteInt(v) {
|
|
|
103
107
|
return typeof v === 'number' && Number.isFinite(v) && Math.floor(v) === v;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
// Lossless numeric-string coercion for integer-shaped args. Models
|
|
111
|
+
// occasionally emit JSON-schema-violating numeric strings ("850",
|
|
112
|
+
// "850.0") for fields the schema types as number (offset/limit/n/
|
|
113
|
+
// head_limit/-A/-B/-C/context/line). Both are unambiguous integer
|
|
114
|
+
// values, so coerce them to a real number before validating rather
|
|
115
|
+
// than rejecting and forcing a retry turn. Non-integer or non-numeric
|
|
116
|
+
// strings ("3.5", "soon") are left untouched and fall through to the
|
|
117
|
+
// existing rejection below.
|
|
118
|
+
function coerceIntegerString(v) {
|
|
119
|
+
if (typeof v !== 'string') return null;
|
|
120
|
+
const t = v.trim();
|
|
121
|
+
if (t === '' || !/^-?\d+(\.\d+)?$/.test(t)) return null;
|
|
122
|
+
const n = Number(t);
|
|
123
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) return null;
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Mutates a[field] in place when it is a lossless integer string, then
|
|
128
|
+
// validates the (possibly coerced) value against [min, max].
|
|
129
|
+
function checkIntInRange(a, field, min, max) {
|
|
130
|
+
let value = a[field];
|
|
107
131
|
if (value === undefined || value === null) return null;
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
const coerced = coerceIntegerString(value);
|
|
134
|
+
if (coerced !== null) {
|
|
135
|
+
value = coerced;
|
|
136
|
+
a[field] = coerced;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
108
139
|
if (!isFiniteInt(value)) {
|
|
109
140
|
return `Error: builtin arg "${field}" must be a finite integer (got ${describeType(value)})`;
|
|
110
141
|
}
|
|
@@ -127,6 +158,38 @@ function hasOwn(o, k) {
|
|
|
127
158
|
return o && Object.prototype.hasOwnProperty.call(o, k);
|
|
128
159
|
}
|
|
129
160
|
|
|
161
|
+
function isPresent(o, k) {
|
|
162
|
+
return hasOwn(o, k) && o[k] !== undefined && o[k] !== null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isNonEmptyPresent(o, k) {
|
|
166
|
+
return isPresent(o, k) && o[k] !== '';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Strip trailing literal artifacts from a grep pattern: a literal two-char
|
|
170
|
+
// "\n" (backslash + n) that ripgrep rejects outside multiline mode ("the
|
|
171
|
+
// literal \"\\n\" is not allowed"), possibly preceded by concatenation
|
|
172
|
+
// debris like `">` that rides along with it (e.g. a stray closing-tag
|
|
173
|
+
// fragment glued on by string interpolation). `">` is ONLY stripped when
|
|
174
|
+
// it is directly followed by one of those newline artifacts — a bare
|
|
175
|
+
// trailing `">` with no newline riding along (e.g. a legit HTML/JSX
|
|
176
|
+
// attribute pattern like `class="active">`) is a real search target and
|
|
177
|
+
// must survive untouched. A \n in the middle of a pattern is also left
|
|
178
|
+
// untouched; only the tail is ever trimmed.
|
|
179
|
+
function stripTrailingPatternArtifacts(v) {
|
|
180
|
+
if (typeof v !== 'string') return v;
|
|
181
|
+
let out = v;
|
|
182
|
+
let changed = true;
|
|
183
|
+
while (changed) {
|
|
184
|
+
changed = false;
|
|
185
|
+
if (out.endsWith('">\n')) { out = out.slice(0, -3); changed = true; continue; }
|
|
186
|
+
if (out.endsWith('">\\n')) { out = out.slice(0, -4); changed = true; continue; }
|
|
187
|
+
if (out.endsWith('\n')) { out = out.slice(0, -1); changed = true; continue; }
|
|
188
|
+
if (out.endsWith('\\n')) { out = out.slice(0, -2); changed = true; continue; }
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
130
193
|
// ---- per-tool guards ----
|
|
131
194
|
|
|
132
195
|
function guardGrep(a) {
|
|
@@ -135,6 +198,15 @@ function guardGrep(a) {
|
|
|
135
198
|
// glob (file filter) aliases
|
|
136
199
|
const globKeys = ['glob', 'file_pattern', 'include', 'files'];
|
|
137
200
|
|
|
201
|
+
// Lossless cleanup of trailing artifacts before validation (item 5b).
|
|
202
|
+
for (const k of patternKeys) {
|
|
203
|
+
if (hasOwn(a, k)) {
|
|
204
|
+
a[k] = Array.isArray(a[k])
|
|
205
|
+
? a[k].map(stripTrailingPatternArtifacts)
|
|
206
|
+
: stripTrailingPatternArtifacts(a[k]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
138
210
|
const hasPattern = patternKeys.some((k) => hasOwn(a, k));
|
|
139
211
|
const hasGlob = globKeys.some((k) => hasOwn(a, k));
|
|
140
212
|
if (!hasPattern && !hasGlob) {
|
|
@@ -144,9 +216,6 @@ function guardGrep(a) {
|
|
|
144
216
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
145
217
|
return `Error: grep arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
146
218
|
}
|
|
147
|
-
if (hasOwn(a, k) && hasBackslashPipe(a[k])) {
|
|
148
|
-
return `Error: grep arg "${k}" contains \\|. Use pattern:["a","b"] for OR terms, or unescaped | for regex alternation.`;
|
|
149
|
-
}
|
|
150
219
|
if (hasOwn(a, k) && hasUnsupportedRipgrepRegex(a[k])) {
|
|
151
220
|
return `Error: grep arg "${k}" uses regex syntax ripgrep does not support here (lookaround/backrefs). Use plain pattern arrays or simpler regex.`;
|
|
152
221
|
}
|
|
@@ -165,34 +234,93 @@ function guardGrep(a) {
|
|
|
165
234
|
return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
|
|
166
235
|
}
|
|
167
236
|
}
|
|
168
|
-
// numeric clamps (grep context 0..12, mirrors read line-context tightening)
|
|
169
237
|
for (const k of ['head_limit', 'offset']) {
|
|
170
|
-
const err = checkIntInRange(
|
|
238
|
+
const err = checkIntInRange(a, k, 0, MAX_INT);
|
|
171
239
|
if (err) return err;
|
|
172
240
|
}
|
|
173
241
|
for (const k of ['-A', '-B', '-C', 'context']) {
|
|
174
|
-
const err = checkIntInRange(
|
|
242
|
+
const err = checkIntInRange(a, k, 0, GREP_CONTEXT_MAX);
|
|
175
243
|
if (err) return err;
|
|
176
244
|
}
|
|
177
245
|
// output_mode / mode enum
|
|
178
246
|
const modeKeys = ['output_mode', 'mode'];
|
|
179
|
-
const allowed = new Set(['files_with_matches', 'content', 'count']);
|
|
247
|
+
const allowed = new Set(['files_with_matches', 'content', 'content_with_context', 'count']);
|
|
180
248
|
for (const k of modeKeys) {
|
|
181
249
|
if (hasOwn(a, k)) {
|
|
250
|
+
// Some callers concatenate a second field's value onto the enum
|
|
251
|
+
// string with a literal newline (e.g. "content_with_context\ntrue").
|
|
252
|
+
// If the first line/token is a valid enum value, truncate to it
|
|
253
|
+
// losslessly rather than rejecting a shape that unambiguously
|
|
254
|
+
// names a real mode (item 5a).
|
|
255
|
+
if (typeof a[k] === 'string' && a[k].includes('\n')) {
|
|
256
|
+
const firstLine = a[k].split('\n')[0].trim();
|
|
257
|
+
const firstToken = firstLine.split(/\s+/)[0];
|
|
258
|
+
if (allowed.has(firstToken)) a[k] = firstToken;
|
|
259
|
+
}
|
|
182
260
|
if (!isString(a[k]) || !allowed.has(a[k])) {
|
|
183
|
-
return `Error: grep arg "${k}" must be one of
|
|
261
|
+
return `Error: grep arg "${k}" must be one of content_with_context|content|files_with_matches|count (got ${JSON.stringify(a[k])})`;
|
|
184
262
|
}
|
|
185
263
|
}
|
|
186
264
|
}
|
|
187
265
|
return null;
|
|
188
266
|
}
|
|
189
267
|
|
|
268
|
+
// Convert a {line, context} pair into {offset, limit}, matching the
|
|
269
|
+
// read-args.mjs private file#Lx compatibility normalizer's semantics
|
|
270
|
+
// (offset = startLine - 1, limit = endLine - startLine + 1, clamped at 0)
|
|
271
|
+
// generalized to a symmetric window: startLine = line - context,
|
|
272
|
+
// endLine = line + context. offset/limit are treated as authoritative —
|
|
273
|
+
// if either is already present, line/context are dropped unused rather
|
|
274
|
+
// than overriding an explicit window. Mutates obj in place. Returns an
|
|
275
|
+
// error string or null.
|
|
276
|
+
function applyLineContextWindow(obj, labelPrefix) {
|
|
277
|
+
for (const k of ['line', 'context', 'offset', 'limit']) {
|
|
278
|
+
if (isPresent(obj, k) && typeof obj[k] === 'string') {
|
|
279
|
+
const coerced = coerceIntegerString(obj[k]);
|
|
280
|
+
if (coerced !== null) obj[k] = coerced;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const hasLine = isPresent(obj, 'line');
|
|
284
|
+
const hasContext = isPresent(obj, 'context');
|
|
285
|
+
if (!hasLine && !hasContext) return null;
|
|
286
|
+
if (isPresent(obj, 'offset') || isPresent(obj, 'limit')) {
|
|
287
|
+
delete obj.line;
|
|
288
|
+
delete obj.context;
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (!hasLine) {
|
|
292
|
+
return `Error: read arg "${labelPrefix}context" requires "${labelPrefix}line" to compute a window`;
|
|
293
|
+
}
|
|
294
|
+
if (!isFiniteInt(obj.line) || obj.line < 1) {
|
|
295
|
+
return `Error: read arg "${labelPrefix}line" must be a finite integer >= 1 (got ${describeType(obj.line)})`;
|
|
296
|
+
}
|
|
297
|
+
let ctx = 0;
|
|
298
|
+
if (hasContext) {
|
|
299
|
+
if (!isFiniteInt(obj.context) || obj.context < 0) {
|
|
300
|
+
return `Error: read arg "${labelPrefix}context" must be a non-negative finite integer (got ${describeType(obj.context)})`;
|
|
301
|
+
}
|
|
302
|
+
ctx = obj.context;
|
|
303
|
+
}
|
|
304
|
+
obj.offset = Math.max(0, obj.line - 1 - ctx);
|
|
305
|
+
obj.limit = 2 * ctx + 1;
|
|
306
|
+
delete obj.line;
|
|
307
|
+
delete obj.context;
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
190
311
|
function guardRead(a) {
|
|
191
312
|
// path / file_path alias OR path may itself be array
|
|
192
313
|
const hasPath = hasOwn(a, 'path') || hasOwn(a, 'file_path');
|
|
193
314
|
if (!hasPath) {
|
|
194
315
|
return 'Error: read requires "path" (or alias file_path).';
|
|
195
316
|
}
|
|
317
|
+
// Some providers/models send a batched path array as a JSON string despite
|
|
318
|
+
// the schema. The executor already accepts that shape via coerceShapeFlex(),
|
|
319
|
+
// but validation runs first; apply the same lossless shape coercion here so
|
|
320
|
+
// valid batched reads do not waste a retry turn.
|
|
321
|
+
if (hasOwn(a, 'path')) {
|
|
322
|
+
a.path = coerceShapeFlex(a.path);
|
|
323
|
+
}
|
|
196
324
|
// path can be string | string[] | object[]; file_path is string
|
|
197
325
|
if (hasOwn(a, 'path')) {
|
|
198
326
|
const p = a.path;
|
|
@@ -200,16 +328,36 @@ function guardRead(a) {
|
|
|
200
328
|
if (!ok) {
|
|
201
329
|
return `Error: read arg "path" must be string, string[], or object[] (got ${describeType(p)})`;
|
|
202
330
|
}
|
|
331
|
+
if (Array.isArray(p)) {
|
|
332
|
+
for (let i = 0; i < p.length; i++) {
|
|
333
|
+
const entry = p[i];
|
|
334
|
+
if (typeof entry === 'string') continue;
|
|
335
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
336
|
+
return `Error: read arg "path[${i}]" must be string or {path,offset,limit} object (got ${describeType(entry)})`;
|
|
337
|
+
}
|
|
338
|
+
const err = applyLineContextWindow(entry, `path[${i}].`);
|
|
339
|
+
if (err) return err;
|
|
340
|
+
for (const ek of ['offset', 'limit']) {
|
|
341
|
+
const eErr = checkIntInRange(entry, ek, 0, MAX_INT);
|
|
342
|
+
if (eErr) return eErr.replace(`"${ek}"`, `"path[${i}].${ek}"`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
203
346
|
}
|
|
204
347
|
if (hasOwn(a, 'file_path') && !isNonEmptyString(a.file_path)) {
|
|
205
348
|
return `Error: read arg "file_path" must be a non-empty string (got ${describeType(a.file_path)})`;
|
|
206
349
|
}
|
|
207
|
-
|
|
208
|
-
|
|
350
|
+
// Read's public surface is offset/limit, but a top-level line/context pair
|
|
351
|
+
// is a deterministic, lossless spelling of the same window (matching
|
|
352
|
+
// read-args.mjs's internal file:line normalizer semantics); convert it
|
|
353
|
+
// instead of rejecting. offset/limit, if already present, win outright.
|
|
354
|
+
{
|
|
355
|
+
const err = applyLineContextWindow(a, '');
|
|
356
|
+
if (err) return err;
|
|
209
357
|
}
|
|
210
358
|
// offset >=0
|
|
211
359
|
{
|
|
212
|
-
const err = checkIntInRange('offset',
|
|
360
|
+
const err = checkIntInRange(a, 'offset', 0, MAX_INT);
|
|
213
361
|
if (err) return err;
|
|
214
362
|
}
|
|
215
363
|
// limit: >=1 = explicit cap; 0 = unlimited sentinel (read-formatting maps 0 to
|
|
@@ -218,12 +366,7 @@ function guardRead(a) {
|
|
|
218
366
|
// break that unbounded-batch contract. A placeholder limit:0 from a symbol
|
|
219
367
|
// read is stripped on the symbol path. Negatives still error.
|
|
220
368
|
{
|
|
221
|
-
const err = checkIntInRange('limit',
|
|
222
|
-
if (err) return err;
|
|
223
|
-
}
|
|
224
|
-
// context 0..200
|
|
225
|
-
{
|
|
226
|
-
const err = checkIntInRange('context', a.context, 0, 200);
|
|
369
|
+
const err = checkIntInRange(a, 'limit', 0, MAX_INT);
|
|
227
370
|
if (err) return err;
|
|
228
371
|
}
|
|
229
372
|
// n 0..10000 — accept 0 rather than erroring: the read-mode handlers coerce
|
|
@@ -232,7 +375,7 @@ function guardRead(a) {
|
|
|
232
375
|
// n is moot. Rejecting 0 only forced a wasted retry turn (the whole point of
|
|
233
376
|
// these reads is to land in one shot). Negatives remain a real error.
|
|
234
377
|
if (hasOwn(a, 'n') && a.n !== undefined && a.n !== null) {
|
|
235
|
-
const err = checkIntInRange('n',
|
|
378
|
+
const err = checkIntInRange(a, 'n', 0, 10000);
|
|
236
379
|
if (err) return err;
|
|
237
380
|
}
|
|
238
381
|
return null;
|
|
@@ -256,7 +399,10 @@ function guardShell(a) {
|
|
|
256
399
|
return 'Error: shell arg "command" must be a non-empty string';
|
|
257
400
|
}
|
|
258
401
|
if (process.platform === 'win32' && !hasOwn(a, 'shell') && hasWindowsDrivePath(a.command)) {
|
|
259
|
-
|
|
402
|
+
// A Windows drive path (C:\...) is unambiguous evidence the caller
|
|
403
|
+
// wants the Windows shell; default it losslessly instead of forcing
|
|
404
|
+
// a retry turn just to add shell:'powershell'.
|
|
405
|
+
a.shell = 'powershell';
|
|
260
406
|
}
|
|
261
407
|
for (const k of ['cwd', 'workdir']) {
|
|
262
408
|
if (hasOwn(a, k) && (a[k] === undefined || a[k] === null || a[k] === '')) {
|
|
@@ -331,12 +477,28 @@ function guardFind(a) {
|
|
|
331
477
|
|
|
332
478
|
function guardGlob(a) {
|
|
333
479
|
// path alias root; pattern aliases glob/name/file_pattern
|
|
480
|
+
const globPatternKeys = ['pattern', 'glob', 'name', 'file_pattern'];
|
|
481
|
+
const hasAnyPattern = globPatternKeys.some((k) => isNonEmptyPresent(a, k));
|
|
482
|
+
// Skip the default when `path` itself carries glob magic (*?[{) — that
|
|
483
|
+
// shape means "path IS the pattern" and is handled by executeGlobTool's
|
|
484
|
+
// own path-magic fallback (splitting path into baseDir + pattern).
|
|
485
|
+
// Injecting pattern:'*' here would override that fallback and silently
|
|
486
|
+
// change "src/**/*.mjs" into "match everything under src/**/*.mjs".
|
|
487
|
+
const pathHasGlobMagic = Array.isArray(a.path)
|
|
488
|
+
? a.path.some((p) => hasGlobMagic(p))
|
|
489
|
+
: hasGlobMagic(a.path);
|
|
490
|
+
if (!hasAnyPattern && isNonEmptyPresent(a, 'path') && !pathHasGlobMagic) {
|
|
491
|
+
// Missing pattern with a real path is an unambiguous "match
|
|
492
|
+
// everything under this path" request; default it instead of
|
|
493
|
+
// erroring out via globMissingPatternMessage() downstream.
|
|
494
|
+
a.pattern = '*';
|
|
495
|
+
}
|
|
334
496
|
for (const k of ['path', 'root']) {
|
|
335
497
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
336
498
|
return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
337
499
|
}
|
|
338
500
|
}
|
|
339
|
-
for (const k of
|
|
501
|
+
for (const k of globPatternKeys) {
|
|
340
502
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
341
503
|
return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
342
504
|
}
|
|
@@ -359,7 +521,7 @@ function guardGlob(a) {
|
|
|
359
521
|
// find_symbol, references, callers, imports, dependents.
|
|
360
522
|
const CODE_GRAPH_MODES = new Set([
|
|
361
523
|
'overview', 'imports', 'dependents', 'related', 'impact',
|
|
362
|
-
'symbols', 'find_symbol', 'search', 'references', 'callers', 'callees', 'prewarm',
|
|
524
|
+
'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees', 'prewarm',
|
|
363
525
|
]);
|
|
364
526
|
|
|
365
527
|
function guardCodeGraph(a) {
|