mixdog 0.9.1 → 0.9.3
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 +9 -1
- 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/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- 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/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- 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/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- 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 +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -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 +182 -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 +100 -18
- 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-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- 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/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- 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/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- 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/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- 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-diagnostics.mjs +42 -2
- 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 +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- 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 +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- 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/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/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- 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 +355 -22
- 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 +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- 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 +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- 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 -657
- 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 +80 -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 +75 -27
- 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 +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- 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/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- 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 (300s), matching the upstream WS provider's
|
|
185
|
+
// default stream idle timeout. Mixdog resets one idle timer on every received
|
|
186
|
+
// WS frame (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
|
}
|
|
@@ -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) {
|
|
@@ -98,8 +107,35 @@ function isFiniteInt(v) {
|
|
|
98
107
|
return typeof v === 'number' && Number.isFinite(v) && Math.floor(v) === v;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
|
|
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];
|
|
102
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
|
+
}
|
|
103
139
|
if (!isFiniteInt(value)) {
|
|
104
140
|
return `Error: builtin arg "${field}" must be a finite integer (got ${describeType(value)})`;
|
|
105
141
|
}
|
|
@@ -122,6 +158,38 @@ function hasOwn(o, k) {
|
|
|
122
158
|
return o && Object.prototype.hasOwnProperty.call(o, k);
|
|
123
159
|
}
|
|
124
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
|
+
|
|
125
193
|
// ---- per-tool guards ----
|
|
126
194
|
|
|
127
195
|
function guardGrep(a) {
|
|
@@ -130,6 +198,15 @@ function guardGrep(a) {
|
|
|
130
198
|
// glob (file filter) aliases
|
|
131
199
|
const globKeys = ['glob', 'file_pattern', 'include', 'files'];
|
|
132
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
|
+
|
|
133
210
|
const hasPattern = patternKeys.some((k) => hasOwn(a, k));
|
|
134
211
|
const hasGlob = globKeys.some((k) => hasOwn(a, k));
|
|
135
212
|
if (!hasPattern && !hasGlob) {
|
|
@@ -157,34 +234,93 @@ function guardGrep(a) {
|
|
|
157
234
|
return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
|
|
158
235
|
}
|
|
159
236
|
}
|
|
160
|
-
// numeric clamps (grep context 0..12, mirrors read line-context tightening)
|
|
161
237
|
for (const k of ['head_limit', 'offset']) {
|
|
162
|
-
const err = checkIntInRange(
|
|
238
|
+
const err = checkIntInRange(a, k, 0, MAX_INT);
|
|
163
239
|
if (err) return err;
|
|
164
240
|
}
|
|
165
241
|
for (const k of ['-A', '-B', '-C', 'context']) {
|
|
166
|
-
const err = checkIntInRange(
|
|
242
|
+
const err = checkIntInRange(a, k, 0, GREP_CONTEXT_MAX);
|
|
167
243
|
if (err) return err;
|
|
168
244
|
}
|
|
169
245
|
// output_mode / mode enum
|
|
170
246
|
const modeKeys = ['output_mode', 'mode'];
|
|
171
|
-
const allowed = new Set(['files_with_matches', 'content', 'count']);
|
|
247
|
+
const allowed = new Set(['files_with_matches', 'content', 'content_with_context', 'count']);
|
|
172
248
|
for (const k of modeKeys) {
|
|
173
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
|
+
}
|
|
174
260
|
if (!isString(a[k]) || !allowed.has(a[k])) {
|
|
175
|
-
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])})`;
|
|
176
262
|
}
|
|
177
263
|
}
|
|
178
264
|
}
|
|
179
265
|
return null;
|
|
180
266
|
}
|
|
181
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
|
+
|
|
182
311
|
function guardRead(a) {
|
|
183
312
|
// path / file_path alias OR path may itself be array
|
|
184
313
|
const hasPath = hasOwn(a, 'path') || hasOwn(a, 'file_path');
|
|
185
314
|
if (!hasPath) {
|
|
186
315
|
return 'Error: read requires "path" (or alias file_path).';
|
|
187
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
|
+
}
|
|
188
324
|
// path can be string | string[] | object[]; file_path is string
|
|
189
325
|
if (hasOwn(a, 'path')) {
|
|
190
326
|
const p = a.path;
|
|
@@ -192,16 +328,36 @@ function guardRead(a) {
|
|
|
192
328
|
if (!ok) {
|
|
193
329
|
return `Error: read arg "path" must be string, string[], or object[] (got ${describeType(p)})`;
|
|
194
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
|
+
}
|
|
195
346
|
}
|
|
196
347
|
if (hasOwn(a, 'file_path') && !isNonEmptyString(a.file_path)) {
|
|
197
348
|
return `Error: read arg "file_path" must be a non-empty string (got ${describeType(a.file_path)})`;
|
|
198
349
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
201
357
|
}
|
|
202
358
|
// offset >=0
|
|
203
359
|
{
|
|
204
|
-
const err = checkIntInRange('offset',
|
|
360
|
+
const err = checkIntInRange(a, 'offset', 0, MAX_INT);
|
|
205
361
|
if (err) return err;
|
|
206
362
|
}
|
|
207
363
|
// limit: >=1 = explicit cap; 0 = unlimited sentinel (read-formatting maps 0 to
|
|
@@ -210,12 +366,7 @@ function guardRead(a) {
|
|
|
210
366
|
// break that unbounded-batch contract. A placeholder limit:0 from a symbol
|
|
211
367
|
// read is stripped on the symbol path. Negatives still error.
|
|
212
368
|
{
|
|
213
|
-
const err = checkIntInRange('limit',
|
|
214
|
-
if (err) return err;
|
|
215
|
-
}
|
|
216
|
-
// context 0..200
|
|
217
|
-
{
|
|
218
|
-
const err = checkIntInRange('context', a.context, 0, 200);
|
|
369
|
+
const err = checkIntInRange(a, 'limit', 0, MAX_INT);
|
|
219
370
|
if (err) return err;
|
|
220
371
|
}
|
|
221
372
|
// n 0..10000 — accept 0 rather than erroring: the read-mode handlers coerce
|
|
@@ -224,7 +375,7 @@ function guardRead(a) {
|
|
|
224
375
|
// n is moot. Rejecting 0 only forced a wasted retry turn (the whole point of
|
|
225
376
|
// these reads is to land in one shot). Negatives remain a real error.
|
|
226
377
|
if (hasOwn(a, 'n') && a.n !== undefined && a.n !== null) {
|
|
227
|
-
const err = checkIntInRange('n',
|
|
378
|
+
const err = checkIntInRange(a, 'n', 0, 10000);
|
|
228
379
|
if (err) return err;
|
|
229
380
|
}
|
|
230
381
|
return null;
|
|
@@ -248,7 +399,10 @@ function guardShell(a) {
|
|
|
248
399
|
return 'Error: shell arg "command" must be a non-empty string';
|
|
249
400
|
}
|
|
250
401
|
if (process.platform === 'win32' && !hasOwn(a, 'shell') && hasWindowsDrivePath(a.command)) {
|
|
251
|
-
|
|
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';
|
|
252
406
|
}
|
|
253
407
|
for (const k of ['cwd', 'workdir']) {
|
|
254
408
|
if (hasOwn(a, k) && (a[k] === undefined || a[k] === null || a[k] === '')) {
|
|
@@ -323,12 +477,28 @@ function guardFind(a) {
|
|
|
323
477
|
|
|
324
478
|
function guardGlob(a) {
|
|
325
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
|
+
}
|
|
326
496
|
for (const k of ['path', 'root']) {
|
|
327
497
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
328
498
|
return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
329
499
|
}
|
|
330
500
|
}
|
|
331
|
-
for (const k of
|
|
501
|
+
for (const k of globPatternKeys) {
|
|
332
502
|
if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
|
|
333
503
|
return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
|
|
334
504
|
}
|
|
@@ -351,7 +521,7 @@ function guardGlob(a) {
|
|
|
351
521
|
// find_symbol, references, callers, imports, dependents.
|
|
352
522
|
const CODE_GRAPH_MODES = new Set([
|
|
353
523
|
'overview', 'imports', 'dependents', 'related', 'impact',
|
|
354
|
-
'symbols', 'find_symbol', 'search', 'references', 'callers', 'callees', 'prewarm',
|
|
524
|
+
'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees', 'prewarm',
|
|
355
525
|
]);
|
|
356
526
|
|
|
357
527
|
function guardCodeGraph(a) {
|