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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { validateBuiltinArgs } from './arg-guard.mjs';
|
|
4
|
+
|
|
5
|
+
test('read: numeric-string offset/limit coerce losslessly', () => {
|
|
6
|
+
const a = { path: 'x.js', offset: '850', limit: '850.0' };
|
|
7
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
8
|
+
assert.equal(a.offset, 850);
|
|
9
|
+
assert.equal(a.limit, 850);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('read: non-integer numeric string still rejected', () => {
|
|
13
|
+
const a = { path: 'x.js', offset: '3.5' };
|
|
14
|
+
assert.match(validateBuiltinArgs('read', a), /must be a finite integer/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('read: non-numeric string still rejected', () => {
|
|
18
|
+
const a = { path: 'x.js', offset: 'soon' };
|
|
19
|
+
assert.match(validateBuiltinArgs('read', a), /must be a finite integer/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('read: top-level line/context convert to offset/limit', () => {
|
|
23
|
+
const a = { path: 'x.js', line: 100, context: 10 };
|
|
24
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
25
|
+
assert.equal(a.offset, 89);
|
|
26
|
+
assert.equal(a.limit, 21);
|
|
27
|
+
assert.equal('line' in a, false);
|
|
28
|
+
assert.equal('context' in a, false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('read: line without context defaults to a single-line window', () => {
|
|
32
|
+
const a = { path: 'x.js', line: 1 };
|
|
33
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
34
|
+
assert.equal(a.offset, 0);
|
|
35
|
+
assert.equal(a.limit, 1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('read: explicit offset/limit wins over line/context (dropped)', () => {
|
|
39
|
+
const a = { path: 'x.js', line: 100, context: 10, offset: 5, limit: 20 };
|
|
40
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
41
|
+
assert.equal(a.offset, 5);
|
|
42
|
+
assert.equal(a.limit, 20);
|
|
43
|
+
assert.equal('line' in a, false);
|
|
44
|
+
assert.equal('context' in a, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('read: path[] entry object line/context converts the same way', () => {
|
|
48
|
+
const a = { path: [{ path: 'x.js', line: 50, context: 5 }] };
|
|
49
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
50
|
+
assert.equal(a.path[0].offset, 44);
|
|
51
|
+
assert.equal(a.path[0].limit, 11);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('read: path[] entry negative numeric-string offset is rejected after coercion', () => {
|
|
55
|
+
const a = { path: [{ path: 'x.js', offset: '-5' }] };
|
|
56
|
+
const err = validateBuiltinArgs('read', a);
|
|
57
|
+
assert.match(err, /path\[0\]\.offset/);
|
|
58
|
+
assert.match(err, />= 0/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('read: path[] entry negative numeric-string limit is rejected after coercion', () => {
|
|
62
|
+
const a = { path: [{ path: 'x.js', limit: '-1' }] };
|
|
63
|
+
const err = validateBuiltinArgs('read', a);
|
|
64
|
+
assert.match(err, /path\[0\]\.limit/);
|
|
65
|
+
assert.match(err, />= 0/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('read: path[] entry valid numeric-string offset/limit still coerces and passes', () => {
|
|
69
|
+
const a = { path: [{ path: 'x.js', offset: '10', limit: '5' }] };
|
|
70
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
71
|
+
assert.equal(a.path[0].offset, 10);
|
|
72
|
+
assert.equal(a.path[0].limit, 5);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('read: JSON object string path is accepted as a single-entry batch', () => {
|
|
76
|
+
const a = { path: '{"path":"x.js","offset":10,"limit":5}' };
|
|
77
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
78
|
+
assert.equal(Array.isArray(a.path), true);
|
|
79
|
+
assert.equal(a.path[0].path, 'x.js');
|
|
80
|
+
assert.equal(a.path[0].offset, 10);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('shell: win32 drive path with no shell set defaults to powershell', (t) => {
|
|
84
|
+
if (process.platform !== 'win32') { t.skip('win32 only'); return; }
|
|
85
|
+
const a = { command: 'dir C:\\Project\\mixdog' };
|
|
86
|
+
assert.equal(validateBuiltinArgs('shell', a), null);
|
|
87
|
+
assert.equal(a.shell, 'powershell');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('glob: missing pattern with a path defaults pattern to "*"', () => {
|
|
91
|
+
const a = { path: 'src' };
|
|
92
|
+
assert.equal(validateBuiltinArgs('glob', a), null);
|
|
93
|
+
assert.equal(a.pattern, '*');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('glob: path carrying glob magic is left for the path-magic fallback, not overridden', () => {
|
|
97
|
+
const a = { path: 'src/**/*.mjs' };
|
|
98
|
+
assert.equal(validateBuiltinArgs('glob', a), null);
|
|
99
|
+
assert.equal('pattern' in a, false);
|
|
100
|
+
assert.equal(a.path, 'src/**/*.mjs');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('glob: missing pattern and path is still an error surface (guard passes through)', () => {
|
|
104
|
+
const a = {};
|
|
105
|
+
assert.equal(validateBuiltinArgs('glob', a), null);
|
|
106
|
+
assert.equal('pattern' in a, false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('grep: output_mode with newline-concatenated debris truncates to valid enum', () => {
|
|
110
|
+
const a = { pattern: 'foo', output_mode: 'content_with_context\ntrue' };
|
|
111
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
112
|
+
assert.equal(a.output_mode, 'content_with_context');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('grep: trailing literal \\n artifact stripped from pattern', () => {
|
|
116
|
+
const a = { pattern: 'foo bar\\n' };
|
|
117
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
118
|
+
assert.equal(a.pattern, 'foo bar');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('grep: trailing concatenation artifact ">\\n" stripped from pattern', () => {
|
|
122
|
+
const a = { pattern: 'foo bar">\n' };
|
|
123
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
124
|
+
assert.equal(a.pattern, 'foo bar');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('grep: mid-pattern \\n is left untouched', () => {
|
|
128
|
+
const a = { pattern: 'foo\\nbar' };
|
|
129
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
130
|
+
assert.equal(a.pattern, 'foo\\nbar');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('grep: bare trailing ">" with no newline artifact survives untouched', () => {
|
|
134
|
+
const a = { pattern: 'class="active">' };
|
|
135
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
136
|
+
assert.equal(a.pattern, 'class="active">');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('grep: numeric-string -C context coerces losslessly', () => {
|
|
140
|
+
const a = { pattern: 'foo', '-C': '5' };
|
|
141
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
142
|
+
assert.equal(a['-C'], 5);
|
|
143
|
+
});
|
|
@@ -3,7 +3,7 @@ import { execShellCommand, stripAnsi } from '../shell-command.mjs';
|
|
|
3
3
|
import { wrapCommandWithSnapshot } from '../shell-snapshot.mjs';
|
|
4
4
|
import { getDestructiveCommandWarning } from '../destructive-warning.mjs';
|
|
5
5
|
import { maybeRewriteWmicProcessCommand } from '../shell-policy.mjs';
|
|
6
|
-
import { buildBashPolicyScanTargets, checkExecPolicyMessage
|
|
6
|
+
import { buildBashPolicyScanTargets, checkExecPolicyMessage } from '../bash-policy-scan.mjs';
|
|
7
7
|
import { markCodeGraphDirtyPaths, drainCodeGraphCache } from '../code-graph-state.mjs';
|
|
8
8
|
import {
|
|
9
9
|
buildJobNotFoundMessage,
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
cancelBackgroundTask,
|
|
28
28
|
completeBackgroundTask,
|
|
29
29
|
getBackgroundTask,
|
|
30
|
-
listBackgroundTasks,
|
|
31
30
|
registerBackgroundTask,
|
|
32
31
|
renderBackgroundTask,
|
|
33
32
|
renderBackgroundTaskList,
|
|
@@ -48,7 +47,6 @@ import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
|
48
47
|
// closing the "external write -> stale old_string -> code 8" gap when shell is
|
|
49
48
|
// routed through this tool. Bounded to the tracked-read set (capped) so cost
|
|
50
49
|
// stays off the whole-cwd path; emits nothing when no read file changed.
|
|
51
|
-
const _DRIFT_SCAN_CAP = 256;
|
|
52
50
|
export function _captureTrackedMtimes(_scope) {
|
|
53
51
|
return new Map();
|
|
54
52
|
}
|
|
@@ -86,53 +84,12 @@ function _combineAbortSignals(sessionSignal, externalSignal) {
|
|
|
86
84
|
return ctl.signal;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
// Decode ANSI-C $'…' and locale $"…" escapes so the blocklist scan sees the
|
|
90
|
-
// literal command (e.g. $'\x72m' → "rm"). Defensive against quoting bypass.
|
|
91
|
-
function _decodeAnsiCQuotes(s) {
|
|
92
|
-
if (typeof s !== 'string') return '';
|
|
93
|
-
if (s.indexOf('$') === -1) return s;
|
|
94
|
-
return s.replace(/\$(['"])((?:\\.|[^\\])*?)\1/g, (_full, _q, body) =>
|
|
95
|
-
body
|
|
96
|
-
.replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
|
|
97
|
-
.replace(/\\u([0-9a-fA-F]{1,4})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
|
|
98
|
-
.replace(/\\0([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
|
|
99
|
-
.replace(/\\([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
|
|
100
|
-
.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
|
|
101
|
-
.replace(/\\\\/g, '\\').replace(/\\(['"])/g, '$1'),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Extract $(…) and `…` command-substitution bodies so each is re-scanned by
|
|
106
|
-
// isBlockedCommand (e.g. eval $(printf 'rm -rf ~')).
|
|
107
|
-
function _extractSubstitutionBodies(s) {
|
|
108
|
-
if (typeof s !== 'string') return [];
|
|
109
|
-
const out = [];
|
|
110
|
-
const re = /\$\(([^()]*(?:\([^()]*\)[^()]*)*)\)|`([^`]*)`/g;
|
|
111
|
-
let m;
|
|
112
|
-
while ((m = re.exec(s)) !== null) {
|
|
113
|
-
const body = m[1] != null ? m[1] : m[2];
|
|
114
|
-
if (body && body.trim()) out.push(body);
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Combined injection-aware block targets: decoded form + substitution bodies
|
|
120
|
-
// (and their decoded forms). Used on BOTH the persistent and stateless paths.
|
|
121
|
-
export function _injectionBlockTargets(cmd) {
|
|
122
|
-
return injectionBlockTargets(cmd);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
87
|
function _prefixPowerShellUtf8(command) {
|
|
126
88
|
const prefix = '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;';
|
|
127
89
|
const text = String(command || '');
|
|
128
90
|
return text.trimStart().startsWith(prefix) ? text : `${prefix}\n${text}`;
|
|
129
91
|
}
|
|
130
92
|
|
|
131
|
-
const _unquoteSpansForPolicy = (s) => s.replace(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g, (m) => m.slice(1, -1));
|
|
132
|
-
|
|
133
|
-
// Same normalized + decoded target set as hard-block (strip/unquote/shell -c/PS).
|
|
134
|
-
export { buildBashPolicyScanTargets } from '../bash-policy-scan.mjs';
|
|
135
|
-
|
|
136
93
|
export function getDedupedDestructiveWarnings(command) {
|
|
137
94
|
const seenMsg = new Set();
|
|
138
95
|
const warnings = [];
|
|
@@ -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). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit only. Batch paths/regions as real arrays. Dirs use list.',
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: 'object',
|
|
23
23
|
properties: {
|
|
@@ -26,14 +26,26 @@ export const BUILTIN_TOOLS = [
|
|
|
26
26
|
{ type: 'string' },
|
|
27
27
|
{
|
|
28
28
|
type: 'array',
|
|
29
|
-
items: {
|
|
29
|
+
items: {
|
|
30
|
+
anyOf: [
|
|
31
|
+
{ type: 'string' },
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
path: { type: 'string' },
|
|
36
|
+
offset: { type: 'number', minimum: 0 },
|
|
37
|
+
limit: { type: 'number', minimum: 1 },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
30
42
|
minItems: 1,
|
|
31
43
|
},
|
|
32
44
|
],
|
|
33
|
-
description: 'File path or
|
|
45
|
+
description: 'File path, path[], or {path,offset,limit}[] region objects. Pass arrays directly; JSON strings are legacy recovery only. Dirs use list.',
|
|
34
46
|
},
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip before reading; 0 starts at line 1. Continue with offset:N.' },
|
|
48
|
+
limit: { type: 'number', minimum: 1, description: 'Numeric max lines to return after offset.' },
|
|
37
49
|
},
|
|
38
50
|
},
|
|
39
51
|
},
|
|
@@ -53,7 +65,10 @@ export const BUILTIN_TOOLS = [
|
|
|
53
65
|
{
|
|
54
66
|
name: 'open_config',
|
|
55
67
|
title: 'Open Config UI',
|
|
56
|
-
|
|
68
|
+
// agentHidden: a worker/reviewer session has no business popping the
|
|
69
|
+
// settings UI on the user's machine; it also wastes schema bytes on
|
|
70
|
+
// every agent request. Lead keeps it.
|
|
71
|
+
annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, agentHidden: true },
|
|
57
72
|
description: 'Open settings UI; returns URL. No params.',
|
|
58
73
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
59
74
|
},
|
|
@@ -61,7 +76,7 @@ export const BUILTIN_TOOLS = [
|
|
|
61
76
|
name: 'shell',
|
|
62
77
|
title: 'Mixdog Shell',
|
|
63
78
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
64
|
-
description: `Run shell
|
|
79
|
+
description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching, or checking existence go through the dedicated tools, never a shell command. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
65
80
|
inputSchema: {
|
|
66
81
|
type: 'object',
|
|
67
82
|
properties: {
|
|
@@ -101,7 +116,7 @@ export const BUILTIN_TOOLS = [
|
|
|
101
116
|
name: 'grep',
|
|
102
117
|
title: 'Mixdog Grep',
|
|
103
118
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
104
|
-
description: 'Search text/
|
|
119
|
+
description: 'Search file contents by text/regex in a known scope. Use files_with_matches/count for broad anchors, content_with_context for narrow code answers. One concept → one grep.',
|
|
105
120
|
inputSchema: {
|
|
106
121
|
type: 'object',
|
|
107
122
|
properties: {
|
|
@@ -110,19 +125,20 @@ export const BUILTIN_TOOLS = [
|
|
|
110
125
|
{ type: 'string' },
|
|
111
126
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
112
127
|
],
|
|
113
|
-
description: 'Text/regex.
|
|
128
|
+
description: 'Text/regex. Put synonyms in pattern[] as OR in ONE grep; no serial rewording or equivalent repeats.',
|
|
114
129
|
},
|
|
115
|
-
path: { type: 'string', description: '
|
|
130
|
+
path: { type: 'string', description: 'Known narrowest file/dir; broad scopes return paths first, then refine from returned paths.' },
|
|
116
131
|
glob: {
|
|
117
132
|
anyOf: [
|
|
118
133
|
{ type: 'string' },
|
|
119
134
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
120
135
|
],
|
|
121
|
-
description: '
|
|
136
|
+
description: 'Narrow in same grep; no follow-up grep for equivalent scope changes.',
|
|
122
137
|
},
|
|
123
|
-
output_mode: { type: 'string', enum: ['
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad scope: files_with_matches/count. Narrow scope: content_with_context; answer from it, skip read unless span is not shown.' },
|
|
139
|
+
context: { type: 'number', minimum: 0, description: 'Lines before/after each match; keep bounded.' },
|
|
140
|
+
head_limit: { type: 'number', minimum: 0, description: 'Max output lines; keep small.' },
|
|
141
|
+
offset: { type: 'number', minimum: 0, description: 'Skip output lines for paging.' },
|
|
126
142
|
},
|
|
127
143
|
required: [],
|
|
128
144
|
},
|
|
@@ -131,7 +147,7 @@ export const BUILTIN_TOOLS = [
|
|
|
131
147
|
name: 'glob',
|
|
132
148
|
title: 'Mixdog Glob',
|
|
133
149
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
134
|
-
description: 'Find files by glob.',
|
|
150
|
+
description: 'Find files by exact glob. Unknown path/name uses find.',
|
|
135
151
|
inputSchema: {
|
|
136
152
|
type: 'object',
|
|
137
153
|
properties: {
|
|
@@ -140,7 +156,7 @@ export const BUILTIN_TOOLS = [
|
|
|
140
156
|
{ type: 'string' },
|
|
141
157
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
142
158
|
],
|
|
143
|
-
description: '
|
|
159
|
+
description: 'Exact glob pattern(s).',
|
|
144
160
|
},
|
|
145
161
|
path: {
|
|
146
162
|
anyOf: [
|
|
@@ -159,11 +175,11 @@ export const BUILTIN_TOOLS = [
|
|
|
159
175
|
name: 'find',
|
|
160
176
|
title: 'Mixdog Find Files',
|
|
161
177
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
162
|
-
description: '
|
|
178
|
+
description: 'Find files by partial path/name. Exact structure uses glob. Returns verified paths.',
|
|
163
179
|
inputSchema: {
|
|
164
180
|
type: 'object',
|
|
165
181
|
properties: {
|
|
166
|
-
query: { type: 'string', description: 'Partial path/name words.' },
|
|
182
|
+
query: { type: 'string', description: 'Partial path/name words; not file contents.' },
|
|
167
183
|
path: { type: 'string', description: 'Base directory.' },
|
|
168
184
|
head_limit: { type: 'number', description: 'Max paths.' },
|
|
169
185
|
},
|
|
File without changes
|
|
@@ -5,3 +5,13 @@ export function formatMtime(mtimeMs) {
|
|
|
5
5
|
if (!mtimeMs) return '-';
|
|
6
6
|
return new Date(mtimeMs).toISOString().slice(0, 19).replace('T', ' ');
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
// Human-readable size column for list/find. Directories have no meaningful
|
|
10
|
+
// byte size (render '-'); files show KB so the model can spot a large file
|
|
11
|
+
// before reading it. Sub-1KB files round up to 1KB.
|
|
12
|
+
export function formatListSize(type, bytes) {
|
|
13
|
+
if (type !== 'file') return '-';
|
|
14
|
+
const n = Number(bytes) || 0;
|
|
15
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
16
|
+
return `${Math.max(1, Math.round(n / 1024))}KB`;
|
|
17
|
+
}
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
NOISE_DIR_NAMES,
|
|
27
27
|
walkDir,
|
|
28
28
|
} from './glob-walk.mjs';
|
|
29
|
-
import { formatMtime } from './list-formatting.mjs';
|
|
29
|
+
import { formatMtime, formatListSize } from './list-formatting.mjs';
|
|
30
|
+
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
30
31
|
import { runRg } from './rg-runner.mjs';
|
|
31
32
|
import { fuzzyRank } from './fuzzy-match.mjs';
|
|
32
33
|
import { assertPathReachable } from './fs-reachability.mjs';
|
|
@@ -169,7 +170,7 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
const lines = sliced.map(r =>
|
|
172
|
-
`${normalizeOutputPath(r.path)}\t${r.type}\t${r.size}\t${formatMtime(r.mtimeMs)}`);
|
|
173
|
+
`${normalizeOutputPath(r.path)}\t${r.type}\t${formatListSize(r.type, r.size)}\t${formatMtime(r.mtimeMs)}`);
|
|
173
174
|
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${rows.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
174
175
|
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`);
|
|
175
176
|
let emptyMsg = '(empty directory)';
|
|
@@ -282,7 +283,7 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
282
283
|
const totalLabel = body.length >= gatherCap ? `${body.length}+` : `${body.length}`;
|
|
283
284
|
outLines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${totalLabel}; pass offset:${offset + sliced.length} to continue]`);
|
|
284
285
|
}
|
|
285
|
-
const TREE_OUTPUT_CHAR_CAP =
|
|
286
|
+
const TREE_OUTPUT_CHAR_CAP = TOOL_OUTPUT_MAX_BYTES;
|
|
286
287
|
let out = outLines.join('\n');
|
|
287
288
|
let outputCharTruncated = false;
|
|
288
289
|
if (out.length > TREE_OUTPUT_CHAR_CAP) {
|
|
@@ -603,7 +604,7 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
603
604
|
const windowed = offset > 0 ? matches.slice(offset) : matches;
|
|
604
605
|
const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
|
|
605
606
|
const lines = sliced.map(m =>
|
|
606
|
-
`${normalizeOutputPath(m.path)}\t${m.size}\t${formatMtime(m.mtimeMs)}`);
|
|
607
|
+
`${normalizeOutputPath(m.path)}\t${formatListSize('file', m.size)}\t${formatMtime(m.mtimeMs)}`);
|
|
607
608
|
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${matches.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
608
609
|
if (rgStdoutTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; results incomplete');
|
|
609
610
|
if (rgStdoutPartial) lines.push('... [warning] rg exit 2 (partial results); listing may be incomplete');
|
|
@@ -220,5 +220,20 @@ export function coerceShapeFlex(value) {
|
|
|
220
220
|
if (typeof parsed === 'string') return parsed;
|
|
221
221
|
} catch {}
|
|
222
222
|
}
|
|
223
|
+
// A single {path,offset,limit}-shaped object JSON string (e.g. a model
|
|
224
|
+
// emitting `path: "{\"path\":\"x\",\"offset\":10}"` instead of a real
|
|
225
|
+
// object) is a lossless single-entry batch. Wrap it in an array so the
|
|
226
|
+
// downstream object-array batch path (read-tool.mjs) picks it up instead
|
|
227
|
+
// of falling through to the plain-string path guard, which would
|
|
228
|
+
// misdetect the raw JSON text as a literal (and often invalid) filename.
|
|
229
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(trimmed);
|
|
232
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
233
|
+
&& typeof (parsed.path ?? parsed.file_path) === 'string') {
|
|
234
|
+
return [parsed];
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
223
238
|
return value;
|
|
224
239
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { normalizeInputPath, resolveAgainstCwd } from './path-utils.mjs';
|
|
3
|
-
import { parseLineLimitArg } from './read-formatting.mjs';
|
|
4
|
-
|
|
5
|
-
const READ_LINE_CONTEXT_DEFAULT = 20;
|
|
6
|
-
const READ_LINE_CONTEXT_MAX = 200;
|
|
7
3
|
|
|
8
4
|
export function parseReadLineNumberArg(value) {
|
|
9
5
|
const n = Number(value);
|
|
10
6
|
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
export function parseReadLineContextArg(value) {
|
|
14
|
-
const n = Number(value);
|
|
15
|
-
if (!Number.isFinite(n)) return READ_LINE_CONTEXT_DEFAULT;
|
|
16
|
-
return Math.min(READ_LINE_CONTEXT_MAX, Math.max(0, Math.trunc(n)));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
export function parseReadPathLineSpec(rawPath) {
|
|
20
10
|
if (typeof rawPath !== 'string' || rawPath.length === 0) return null;
|
|
21
11
|
const text = rawPath.trim();
|
|
@@ -51,7 +41,6 @@ export function normalizePathAndStripLineCoordinate(rawPath, workDir) {
|
|
|
51
41
|
|
|
52
42
|
export function normaliseReadLineWindowArgs(inputArgs, workDir) {
|
|
53
43
|
const args = { ...inputArgs };
|
|
54
|
-
let lineNo = parseReadLineNumberArg(args.line);
|
|
55
44
|
let pathLineRange = null;
|
|
56
45
|
if (typeof args.path === 'string' && args.path) {
|
|
57
46
|
const spec = resolveExistingPathLineCoordinate(args.path, workDir);
|
|
@@ -61,45 +50,21 @@ export function normaliseReadLineWindowArgs(inputArgs, workDir) {
|
|
|
61
50
|
return args;
|
|
62
51
|
}
|
|
63
52
|
args.path = spec.path;
|
|
64
|
-
if (!lineNo) lineNo = spec.lineNo;
|
|
65
53
|
if (spec.endLine) pathLineRange = { startLine: spec.lineNo, endLine: spec.endLine };
|
|
54
|
+
else pathLineRange = { startLine: spec.lineNo, endLine: spec.lineNo };
|
|
66
55
|
}
|
|
67
56
|
}
|
|
68
57
|
const isFullMode = !args.mode || args.mode === 'full';
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
args._invertedRangeError = `Error: conflicting window args — both line (${lineNo}) and offset (${args.offset}) were given; line/context and offset/limit are alternative window forms, pass only one`;
|
|
76
|
-
return args;
|
|
77
|
-
}
|
|
78
|
-
if (isFullMode && lineNo) {
|
|
79
|
-
if (pathLineRange && args.context === undefined && (args.limit === undefined || args.limit === null)) {
|
|
80
|
-
args.offset = Math.max(0, pathLineRange.startLine - 1);
|
|
58
|
+
// Public Read args are offset/limit only. Keep a narrow private
|
|
59
|
+
// compatibility path for file#Lx/file:line strings by converting them into
|
|
60
|
+
// offset/limit; do not interpret line/context fields here.
|
|
61
|
+
if (isFullMode && pathLineRange && args.offset === undefined) {
|
|
62
|
+
args.offset = Math.max(0, pathLineRange.startLine - 1);
|
|
63
|
+
if (args.limit === undefined) {
|
|
81
64
|
args.limit = Math.max(1, pathLineRange.endLine - pathLineRange.startLine + 1);
|
|
82
|
-
} else {
|
|
83
|
-
const contextExplicit = args.context !== undefined && args.context !== null;
|
|
84
|
-
const limitExplicit = args.limit !== undefined && args.limit !== null;
|
|
85
|
-
const context = parseReadLineContextArg(args.context);
|
|
86
|
-
if (limitExplicit && !contextExplicit) {
|
|
87
|
-
// Explicit limit, no explicit context: anchor the window AT the
|
|
88
|
-
// requested line so it is always included. (Was: offset centered
|
|
89
|
-
// by the default context, which a small limit then truncated to
|
|
90
|
-
// exclude the very line the caller asked for.)
|
|
91
|
-
args.offset = Math.max(0, lineNo - 1);
|
|
92
|
-
args.limit = parseLineLimitArg(args.limit, (context * 2) + 1);
|
|
93
|
-
} else {
|
|
94
|
-
const limit = limitExplicit
|
|
95
|
-
? parseLineLimitArg(args.limit, (context * 2) + 1)
|
|
96
|
-
: (context * 2) + 1;
|
|
97
|
-
args.offset = Math.max(0, lineNo - context - 1);
|
|
98
|
-
args.limit = limit;
|
|
99
|
-
}
|
|
100
65
|
}
|
|
101
|
-
delete args.line;
|
|
102
|
-
delete args.context;
|
|
103
66
|
}
|
|
67
|
+
delete args.line;
|
|
68
|
+
delete args.context;
|
|
104
69
|
return args;
|
|
105
70
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
|
|
2
|
+
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
2
3
|
|
|
3
4
|
// Read tool caps.
|
|
4
5
|
//
|
|
@@ -28,7 +29,7 @@ import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
|
|
|
28
29
|
// cleanly without a tokenizer.
|
|
29
30
|
export const READ_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
30
31
|
export const READ_WHOLE_FILE_MAX_BYTES = 256 * 1024;
|
|
31
|
-
export const READ_MAX_OUTPUT_BYTES =
|
|
32
|
+
export const READ_MAX_OUTPUT_BYTES = TOOL_OUTPUT_MAX_BYTES;
|
|
32
33
|
export const READ_STREAM_RANGE_MIN_BYTES = 128 * 1024;
|
|
33
34
|
export const READ_SMART_STREAM_MIN_BYTES = SMART_READ_MAX_BYTES;
|
|
34
35
|
export const READ_BATCH_RANGE_COALESCE_GAP_LINES = 256;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { mergeReadRanges } from './read-ranges.mjs';
|
|
2
|
+
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Smart-truncate cap: a no-window read returns the file until this cap, past
|
|
5
|
+
// which head+tail are shown and the model pages the rest with offset (footer
|
|
6
|
+
// says how). Byte budget is the shared TOOL_OUTPUT_MAX_BYTES; line/head/tail
|
|
7
|
+
// stay read-specific. Env-overridable for bench: MIXDOG_READ_MAX_LINES/_HEAD/_TAIL.
|
|
8
|
+
function _readEnvInt(name, fallback) {
|
|
9
|
+
const v = parseInt(process.env[name], 10);
|
|
10
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
11
|
+
}
|
|
12
|
+
export const SMART_READ_MAX_BYTES = TOOL_OUTPUT_MAX_BYTES;
|
|
13
|
+
export const SMART_READ_MAX_LINES = _readEnvInt('MIXDOG_READ_MAX_LINES', 2000);
|
|
14
|
+
export const SMART_READ_HEAD_LINES = _readEnvInt('MIXDOG_READ_HEAD_LINES', 1200);
|
|
15
|
+
export const SMART_READ_TAIL_LINES = _readEnvInt('MIXDOG_READ_TAIL_LINES', 400);
|
|
7
16
|
// Only the genuinely large full reads warrant the anti-re-read advisory; below
|
|
8
17
|
// this the smart-truncate path (30 KB) already caps normal reads, so a 16 KB
|
|
9
18
|
// floor mostly fired on full:true mid-size reads where the advisory was pure
|
|
@@ -49,8 +49,7 @@ function _guardedReadError(p, helpers) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
|
|
52
|
-
// run before any sync FS — including
|
|
53
|
-
// in readPathStringGuardError / normaliseReadLineWindowArgs) and the image
|
|
52
|
+
// run before any sync FS — including path normalization and the image
|
|
54
53
|
// stat/read. A dead mount would otherwise freeze the event loop, defeating even
|
|
55
54
|
// the 630s dispatch ceiling.
|
|
56
55
|
async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
@@ -58,10 +57,9 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
58
57
|
normalizeInputPath, resolveAgainstCwd,
|
|
59
58
|
} = helpers;
|
|
60
59
|
// A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
|
|
61
|
-
// REJECTED here, not skipped: skipping would let the later sync
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// hang. Reject up front with the same message the inline guards emit.
|
|
60
|
+
// REJECTED here, not skipped: skipping would let the later sync guard/open
|
|
61
|
+
// path touch it and trigger NTLM/raw-device access or hang. Reject up front
|
|
62
|
+
// with the same message the inline guards emit.
|
|
65
63
|
// normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
|
|
66
64
|
// opens (e.g. /mnt/z/... -> Z:\...). Reachability is per-mount/dir, so the
|
|
67
65
|
// line-coordinate strip only needs to land in the right directory — exact
|
|
@@ -113,7 +111,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
113
111
|
}
|
|
114
112
|
args.path = coerceShapeFlex(args.path);
|
|
115
113
|
// Reachability preflight up front (all shapes) — before readPathStringGuardError /
|
|
116
|
-
//
|
|
114
|
+
// image stat, all of which can touch sync FS.
|
|
117
115
|
{
|
|
118
116
|
const _reErr = await _readReachPreflight(args.path, workDir, helpers);
|
|
119
117
|
if (_reErr) return _reErr;
|
|
@@ -186,8 +184,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
186
184
|
}
|
|
187
185
|
}
|
|
188
186
|
if (r?.limit !== undefined) entry.limit = r.limit;
|
|
189
|
-
if (r?.line !== undefined) entry.line = r.line;
|
|
190
|
-
if (r?.context !== undefined) entry.context = r.context;
|
|
191
187
|
if (r?.full !== undefined) entry.full = r.full;
|
|
192
188
|
entry = normaliseReadLineWindowArgs(entry, workDir);
|
|
193
189
|
return entry;
|
|
@@ -235,8 +231,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
235
231
|
if (args.n !== undefined) entry.n = args.n;
|
|
236
232
|
if (args.offset !== undefined) entry.offset = args.offset;
|
|
237
233
|
if (args.limit !== undefined) entry.limit = args.limit;
|
|
238
|
-
if (args.line !== undefined) entry.line = args.line;
|
|
239
|
-
if (args.context !== undefined) entry.context = args.context;
|
|
240
234
|
if (args.full !== undefined) entry.full = args.full;
|
|
241
235
|
entry = normaliseReadLineWindowArgs(entry, workDir);
|
|
242
236
|
return entry;
|
|
@@ -464,14 +458,13 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
464
458
|
if (span.note) args._symbolReadNote = `symbol ${sym}: ${span.note}`;
|
|
465
459
|
}
|
|
466
460
|
}
|
|
467
|
-
// A window (offset/limit
|
|
461
|
+
// A window (offset/limit or a path:line coordinate) beats a glance
|
|
468
462
|
// mode (head/tail/summary), which would otherwise read from a file end and
|
|
469
|
-
// silently drop the window. Drop the glance mode BEFORE line
|
|
470
|
-
// normalization so
|
|
471
|
-
// offset/limit
|
|
472
|
-
// set). count/hex are not text-window ops and keep their mode.
|
|
463
|
+
// silently drop the window. Drop the glance mode BEFORE path:line
|
|
464
|
+
// compatibility normalization so any path coordinate is converted to
|
|
465
|
+
// offset/limit. count/hex are not text-window ops and keep their mode.
|
|
473
466
|
{
|
|
474
|
-
const _win = args.offset != null || args.limit != null ||
|
|
467
|
+
const _win = args.offset != null || args.limit != null || hasLineCoordinate(args.path);
|
|
475
468
|
if (_win && (args.mode === 'head' || args.mode === 'tail' || args.mode === 'summary')) {
|
|
476
469
|
args = { ...args, mode: undefined };
|
|
477
470
|
}
|