mixdog 0.9.93 → 0.9.95
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/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -29
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +23 -7
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +103 -92
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +546 -23
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +104 -14
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +151 -18
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +16 -6
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +5 -0
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +22 -1
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -86
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1537 -9718
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +97 -4
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +88 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +84 -4
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1057
- package/src/tui/index.jsx +47 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -14,7 +14,7 @@ import { markSessionToolCall, updateSessionStage } from './manager.mjs';
|
|
|
14
14
|
import { resolveToolSelfDeadlineMs } from '../agent-runtime/agent-progress-watchdog.mjs';
|
|
15
15
|
import { classifyResultKind } from './result-classification.mjs';
|
|
16
16
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
17
|
-
import {
|
|
17
|
+
import { maybeOffloadToolResultBatch } from './tool-result-offload.mjs';
|
|
18
18
|
import {
|
|
19
19
|
tryReadCached, setReadCached, invalidatePathForSession, markPostEdit,
|
|
20
20
|
consumePostEditMark, clearReadDedupSession, extractTouchedPathsFromPatch,
|
|
@@ -48,7 +48,7 @@ export async function processToolBatch(ctx) {
|
|
|
48
48
|
const {
|
|
49
49
|
calls, messages, tools, cwd, sessionId, sessionRef, signal, opts,
|
|
50
50
|
iterations, assistantTurnMsg, pending, epoch, startEagerRun,
|
|
51
|
-
crossTurnCalls, crossTurnCap, sessionAgent,
|
|
51
|
+
crossTurnCalls, crossTurnCap, sessionAgent,
|
|
52
52
|
pushToolResultMessage, throwIfAborted, repeatFailLimit,
|
|
53
53
|
} = ctx;
|
|
54
54
|
const executeToolFn = typeof ctx.executeToolFn === 'function' ? ctx.executeToolFn : executeTool;
|
|
@@ -93,6 +93,16 @@ export async function processToolBatch(ctx) {
|
|
|
93
93
|
// between two tool results of the same multi-tool turn (which would put a
|
|
94
94
|
// user message between tool(A) and tool(B) and break provider pairing).
|
|
95
95
|
const _batchNewMessages = [];
|
|
96
|
+
// Finalize together after execution so the message-level output budget
|
|
97
|
+
// can select the largest non-Read results before cache/history/UI see
|
|
98
|
+
// any body.
|
|
99
|
+
const _batchCompleted = [];
|
|
100
|
+
const _batchToolResultByCallId = new Map();
|
|
101
|
+
const _batchToolResultsWithoutId = [];
|
|
102
|
+
const _stageToolResultMessage = (message) => {
|
|
103
|
+
if (message?.toolCallId) _batchToolResultByCallId.set(message.toolCallId, message);
|
|
104
|
+
else _batchToolResultsWithoutId.push(message);
|
|
105
|
+
};
|
|
96
106
|
// Ordered-mutation batch gate. Each apply_patch is an eager-dispatch
|
|
97
107
|
// barrier: side effects overlap within the current segment, but no
|
|
98
108
|
// later segment starts before the loop crosses the patch in model
|
|
@@ -110,7 +120,7 @@ export async function processToolBatch(ctx) {
|
|
|
110
120
|
if (_duplicateCallIds.has(call.id)) {
|
|
111
121
|
const _firstId = _dupFirstId.get(call.id);
|
|
112
122
|
const _stub = `[intra-turn-dedup] identical read-only \`${call.name}\` call was already executed in this same assistant turn as tool_use_id=${_firstId}. The first call's tool_result is in context immediately above; skipping re-execution to save tokens. If you needed a different slice of the file, narrow the next call (different path / offset / limit / pattern) so it has a distinct signature.`;
|
|
113
|
-
|
|
123
|
+
_stageToolResultMessage({
|
|
114
124
|
role: 'tool',
|
|
115
125
|
content: _stub,
|
|
116
126
|
toolCallId: call.id,
|
|
@@ -136,7 +146,7 @@ export async function processToolBatch(ctx) {
|
|
|
136
146
|
_prior.count += 1;
|
|
137
147
|
dedupStubTotal += 1;
|
|
138
148
|
const _stub = crossTurnDedupStub(call.name, _prior.firstIteration, dedupStubTotal >= 5);
|
|
139
|
-
|
|
149
|
+
_stageToolResultMessage({
|
|
140
150
|
role: 'tool',
|
|
141
151
|
content: _stub,
|
|
142
152
|
toolCallId: call.id,
|
|
@@ -175,7 +185,7 @@ export async function processToolBatch(ctx) {
|
|
|
175
185
|
{
|
|
176
186
|
const _rfg = sessionRef?._repeatFailGuard;
|
|
177
187
|
if (_rfg && _rfg.sig === _repeatFailSig && _rfg.count >= repeatFailLimit) {
|
|
178
|
-
|
|
188
|
+
_stageToolResultMessage({
|
|
179
189
|
role: 'tool',
|
|
180
190
|
content: `[repeat-failure-guard] This exact \`${call.name}\` call (identical arguments) has already failed ${_rfg.count} times in a row; not re-executing because the result will not change. Change approach: use different arguments, a different tool, or skip this step.`,
|
|
181
191
|
toolCallId: call.id,
|
|
@@ -200,7 +210,7 @@ export async function processToolBatch(ctx) {
|
|
|
200
210
|
// structural failure that drives retry/stop-hook behavior.
|
|
201
211
|
if (_orderedMutationFailed && _isOrderedGateSkippable(call.name) && !pending.has(call.id)) {
|
|
202
212
|
if (call?.id) restoreToolCallBodyForId(assistantTurnMsg, calls, call.id);
|
|
203
|
-
|
|
213
|
+
_stageToolResultMessage({
|
|
204
214
|
role: 'tool',
|
|
205
215
|
content: `[prerequisite-failed] an earlier apply_patch in this same tool batch (call index ${_orderedMutationFailed.index + 1}) failed, so this later \`${call.name}\` was skipped without starting. Re-issue it after resolving the patch failure.`,
|
|
206
216
|
toolCallId: call.id,
|
|
@@ -313,6 +323,7 @@ export async function processToolBatch(ctx) {
|
|
|
313
323
|
toolEndedAt = Date.now();
|
|
314
324
|
_resultKind = 'error';
|
|
315
325
|
} else {
|
|
326
|
+
await opts.beforeToolExecution?.();
|
|
316
327
|
result = await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: iterations, deferShellCwdCommit: true });
|
|
317
328
|
toolEndedAt = Date.now();
|
|
318
329
|
// Boundary: tool-return string convention → structural kind.
|
|
@@ -516,37 +527,62 @@ export async function processToolBatch(ctx) {
|
|
|
516
527
|
if (sessionId && _isShellTool(call.name)) {
|
|
517
528
|
clearScopedToolsForSession(sessionId);
|
|
518
529
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
let _nativeToolSearch = null;
|
|
530
|
+
_batchCompleted.push({
|
|
531
|
+
call,
|
|
532
|
+
result,
|
|
533
|
+
toolStartedAt,
|
|
534
|
+
toolEndedAt,
|
|
535
|
+
toolKind,
|
|
536
|
+
resultKind: _resultKind,
|
|
537
|
+
executeOk: _executeOk,
|
|
538
|
+
readCacheHit: _readCacheHit,
|
|
539
|
+
scopedCacheHit: _scopedCacheHit,
|
|
540
|
+
crossTurnSig: _ctSig,
|
|
541
|
+
mutationEpoch: epoch.mutation,
|
|
542
|
+
nativeToolSearch: null,
|
|
543
|
+
postError: null,
|
|
544
|
+
});
|
|
545
|
+
// Soft-cancel after each tool: if close landed during execution,
|
|
546
|
+
// discard the rest of the batch and skip the next provider.send.
|
|
547
|
+
throwIfAborted();
|
|
548
|
+
}
|
|
549
|
+
for (const completed of _batchCompleted) {
|
|
540
550
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
551
|
+
completed.nativeToolSearch = parseNativeToolSearchPayload(completed.call.name, completed.result);
|
|
552
|
+
if (completed.nativeToolSearch?.summary) completed.result = completed.nativeToolSearch.summary;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
completed.postError = error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
let _offloadStates;
|
|
558
|
+
try {
|
|
559
|
+
_offloadStates = await maybeOffloadToolResultBatch(
|
|
560
|
+
sessionId,
|
|
561
|
+
_batchCompleted.map((completed) => ({
|
|
562
|
+
toolCallId: completed.call.id,
|
|
563
|
+
toolName: _stripMcpPrefix(completed.call.name),
|
|
564
|
+
result: completed.postError ? null : completed.result,
|
|
565
|
+
})),
|
|
566
|
+
);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
_offloadStates = _batchCompleted.map(() => ({ result: null, error }));
|
|
569
|
+
}
|
|
570
|
+
// Assistant-message pipeline: offload → compress → trace → cache →
|
|
571
|
+
// push. The cache and transcript therefore receive the same body.
|
|
572
|
+
for (let completedIndex = 0; completedIndex < _batchCompleted.length; completedIndex += 1) {
|
|
573
|
+
const completed = _batchCompleted[completedIndex];
|
|
574
|
+
const {
|
|
575
|
+
call, toolStartedAt, toolEndedAt, toolKind,
|
|
576
|
+
executeOk: _executeOk, resultKind: _resultKind,
|
|
577
|
+
readCacheHit: _readCacheHit, scopedCacheHit: _scopedCacheHit,
|
|
578
|
+
} = completed;
|
|
579
|
+
let _ctSig = completed.crossTurnSig;
|
|
580
|
+
let result = completed.result;
|
|
581
|
+
const _nativeToolSearch = completed.nativeToolSearch;
|
|
582
|
+
try {
|
|
583
|
+
if (completed.postError) throw completed.postError;
|
|
584
|
+
if (_offloadStates[completedIndex]?.error) throw _offloadStates[completedIndex].error;
|
|
585
|
+
result = _offloadStates[completedIndex]?.result;
|
|
550
586
|
result = compressToolResult(call.name, call.arguments, result, { sessionId, toolKind });
|
|
551
587
|
traceAgentTool({
|
|
552
588
|
sessionId,
|
|
@@ -561,55 +597,44 @@ export async function processToolBatch(ctx) {
|
|
|
561
597
|
resultText: result,
|
|
562
598
|
cwd,
|
|
563
599
|
});
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
// tool_use will show the offload stub; LLM can still recover the full
|
|
570
|
-
// body via the disk path in that stub.
|
|
571
|
-
if (sessionId && _executeOk && _resultKind === 'normal') {
|
|
600
|
+
// Deferred writes that predate a later mutation are skipped;
|
|
601
|
+
// immediate writes used to be invalidated by that mutation.
|
|
602
|
+
const _outcomeMap = sessionRef?._scopedCacheOutcomeByCallId instanceof Map
|
|
603
|
+
? sessionRef._scopedCacheOutcomeByCallId : null;
|
|
604
|
+
if (sessionId && _executeOk && _resultKind === 'normal' && completed.mutationEpoch === epoch.mutation) {
|
|
572
605
|
if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
|
|
573
|
-
const _outcomeMap = sessionRef?._scopedCacheOutcomeByCallId instanceof Map
|
|
574
|
-
? sessionRef._scopedCacheOutcomeByCallId : null;
|
|
575
606
|
const _outcome = _outcomeMap?.get(call.id);
|
|
576
607
|
setScopedToolCached({
|
|
577
608
|
sessionId,
|
|
578
|
-
toolName:
|
|
609
|
+
toolName: _stripMcpPrefix(call.name),
|
|
579
610
|
args: call.arguments,
|
|
580
611
|
cwd,
|
|
581
612
|
content: result,
|
|
582
613
|
toolUseId: call.id,
|
|
583
614
|
complete: _outcome ? _outcome.complete : true,
|
|
584
615
|
});
|
|
585
|
-
_outcomeMap?.delete(call.id);
|
|
586
616
|
}
|
|
587
617
|
if (_readCacheHit === null && _isReadTool(call.name)) {
|
|
588
|
-
// Pass tool_use id so future cache-hits can reference the body's location in history.
|
|
589
618
|
setReadCached({ sessionId, args: call.arguments, cwd, content: result, toolUseId: call.id });
|
|
590
619
|
}
|
|
591
620
|
}
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
621
|
+
// A successful scoped lookup from before a later mutation is
|
|
622
|
+
// intentionally not cached, but its per-call completeness
|
|
623
|
+
// record still has to be reclaimed.
|
|
624
|
+
if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
|
|
625
|
+
_outcomeMap?.delete(call.id);
|
|
626
|
+
}
|
|
598
627
|
const _applyPatchUiDiff = _stripMcpPrefix(call.name) === 'apply_patch'
|
|
599
628
|
? takeApplyPatchUiDiff(call.id)
|
|
600
629
|
: null;
|
|
601
|
-
|
|
630
|
+
_stageToolResultMessage({
|
|
602
631
|
role: 'tool',
|
|
603
632
|
content: result,
|
|
604
633
|
toolCallId: call.id,
|
|
605
634
|
toolKind: _resultKind,
|
|
606
635
|
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
607
|
-
...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
|
|
636
|
+
...(_applyPatchUiDiff !== null ? { uiDiff: _applyPatchUiDiff } : {}),
|
|
608
637
|
});
|
|
609
|
-
// Completion-first bookkeeping (Steps 1 & 2). Only successful
|
|
610
|
-
// executions count. Edit/progress = any executed tool whose def
|
|
611
|
-
// lacks readOnlyHint (apply_patch/bash/MCP-write/skill/...).
|
|
612
|
-
// Read-only successful calls seed the cross-turn dedup map.
|
|
613
638
|
if (_executeOk) {
|
|
614
639
|
const _isEager = isEagerDispatchable(call.name, tools);
|
|
615
640
|
if (isToolCallDedupEligible(call.name, tools)) {
|
|
@@ -621,30 +646,19 @@ export async function processToolBatch(ctx) {
|
|
|
621
646
|
crossTurnCalls.delete(_oldest);
|
|
622
647
|
}
|
|
623
648
|
}
|
|
624
|
-
} else if (!_isEager) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
// now return different content, so a post-edit
|
|
628
|
-
// verification read must NOT be stubbed as "unchanged".
|
|
629
|
-
if (isEditProgressTool(call.name, false)) {
|
|
630
|
-
crossTurnCalls.clear();
|
|
631
|
-
editCount += 1;
|
|
632
|
-
}
|
|
649
|
+
} else if (!_isEager && isEditProgressTool(call.name, false)) {
|
|
650
|
+
crossTurnCalls.clear();
|
|
651
|
+
editCount += 1;
|
|
633
652
|
}
|
|
634
653
|
}
|
|
635
654
|
} catch (postErr) {
|
|
636
|
-
// Reviewer fix: the exec itself succeeded — if it was a
|
|
637
|
-
// mutating edit-progress tool, the file changes are real even
|
|
638
|
-
// though post-processing failed, so the cross-turn dedup map
|
|
639
|
-
// must still be invalidated (otherwise a later verification
|
|
640
|
-
// read could be stubbed as "unchanged" against stale sigs).
|
|
641
655
|
if (_executeOk && !isEagerDispatchable(call.name, tools) && isEditProgressTool(call.name, false)) {
|
|
642
656
|
crossTurnCalls.clear();
|
|
643
657
|
editCount += 1;
|
|
644
658
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
659
|
+
if (sessionRef?._scopedCacheOutcomeByCallId instanceof Map && call?.id) {
|
|
660
|
+
sessionRef._scopedCacheOutcomeByCallId.delete(call.id);
|
|
661
|
+
}
|
|
648
662
|
if (call?.id) restoreToolCallBodyForId(assistantTurnMsg, calls, call.id);
|
|
649
663
|
const _postMsg = `Error: tool result post-processing failed for "${call.name}": ${postErr instanceof Error ? postErr.message : String(postErr)}`;
|
|
650
664
|
traceAgentToolFailure({
|
|
@@ -660,24 +674,25 @@ export async function processToolBatch(ctx) {
|
|
|
660
674
|
resultText: _postMsg,
|
|
661
675
|
resultKind: 'error',
|
|
662
676
|
});
|
|
663
|
-
|
|
664
|
-
// tool_use isn't orphaned. Cache writes are placed at the
|
|
665
|
-
// end of the try block (immediately before messages.push),
|
|
666
|
-
// so ANY throw in post-processing reaches this catch before
|
|
667
|
-
// the cache is written — stale/partial results are never
|
|
668
|
-
// cached. The next read on the same path/scope re-executes
|
|
669
|
-
// naturally.
|
|
670
|
-
pushToolResultMessage({
|
|
677
|
+
_stageToolResultMessage({
|
|
671
678
|
role: 'tool',
|
|
672
679
|
content: _postMsg,
|
|
673
680
|
toolCallId: call.id,
|
|
674
681
|
toolKind: 'error',
|
|
675
682
|
});
|
|
676
683
|
}
|
|
677
|
-
// Soft-cancel after each tool: if close landed during execution,
|
|
678
|
-
// discard the rest of the batch and skip the next provider.send.
|
|
679
684
|
throwIfAborted();
|
|
680
685
|
}
|
|
686
|
+
// Early dedup/guard skips and deferred execution results share this
|
|
687
|
+
// ordered flush so provider tool_result blocks always follow the
|
|
688
|
+
// assistant's original tool_use order.
|
|
689
|
+
for (const call of calls) {
|
|
690
|
+
const message = _batchToolResultByCallId.get(call?.id);
|
|
691
|
+
if (message) pushToolResultMessage(message);
|
|
692
|
+
}
|
|
693
|
+
for (const message of _batchToolResultsWithoutId) {
|
|
694
|
+
pushToolResultMessage(message);
|
|
695
|
+
}
|
|
681
696
|
// Flush the per-batch newMessages channel. All tool_results for this
|
|
682
697
|
// assistant turn are now pushed; appending the injected role:'user'
|
|
683
698
|
// messages here (AFTER the last tool_result, BEFORE the next provider
|
|
@@ -716,10 +731,6 @@ export async function processToolBatch(ctx) {
|
|
|
716
731
|
} catch { /* best-effort: PostToolBatch hook must never break the loop */ }
|
|
717
732
|
}
|
|
718
733
|
}
|
|
719
|
-
// Completion-first steering hints (missed-parallelism / all-read-only /
|
|
720
|
-
// serial-rewording). At most ONE hint per turn. The ladder controller
|
|
721
|
-
// owns the cumulative counters and streaks.
|
|
722
|
-
steeringLadder.emitPostBatchSteering(calls, false);
|
|
723
734
|
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
724
735
|
// AFTER any auto-compact pass. Draining here would put the steering
|
|
725
736
|
// user turn after the fresh tool results before compaction runs; then
|
|
@@ -10,6 +10,7 @@ const TOOL_RESULT_PREVIEW_CHARS = 2_000;
|
|
|
10
10
|
const TOOL_RESULT_SHELL_THRESHOLD_CHARS = 30_000;
|
|
11
11
|
const TOOL_RESULT_SEARCH_THRESHOLD_CHARS = 50_000;
|
|
12
12
|
const TOOL_RESULT_GREP_THRESHOLD_CHARS = 20_000;
|
|
13
|
+
export const TOOL_RESULT_MESSAGE_MAX_CHARS = 200_000;
|
|
13
14
|
const TOOL_RESULT_OFFLOAD_PREFIX = '[tool output offloaded:';
|
|
14
15
|
const OFFLOAD_PRUNE_MIN_AGE_MS = 10 * 60 * 1000;
|
|
15
16
|
// clearOffloadSession only runs on a CLEAN session close, so every killed
|
|
@@ -86,6 +87,35 @@ function getOffloadThreshold(toolName) {
|
|
|
86
87
|
return INLINE_THRESHOLD_BY_TOOL.get(key) ?? TOOL_RESULT_OFFLOAD_THRESHOLD_CHARS;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
const AGGREGATE_OFFLOAD_EXCLUDED_TOOLS = new Set([
|
|
91
|
+
'read',
|
|
92
|
+
'head',
|
|
93
|
+
'tail',
|
|
94
|
+
'diff',
|
|
95
|
+
'skill',
|
|
96
|
+
'skill_view',
|
|
97
|
+
'skills_list',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
export function isAggregateOffloadEligible(toolName, result) {
|
|
101
|
+
if (typeof result !== 'string') return false;
|
|
102
|
+
const key = String(toolName || '').toLowerCase();
|
|
103
|
+
return !AGGREGATE_OFFLOAD_EXCLUDED_TOOLS.has(key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function rankAggregateOffloadCandidates(entries) {
|
|
107
|
+
return entries
|
|
108
|
+
.map((entry, index) => ({
|
|
109
|
+
index,
|
|
110
|
+
length: typeof entry?.result === 'string' ? entry.result.length : 0,
|
|
111
|
+
eligible: isAggregateOffloadEligible(entry?.toolName, entry?.result)
|
|
112
|
+
&& !String(entry?.result || '').startsWith(TOOL_RESULT_OFFLOAD_PREFIX),
|
|
113
|
+
}))
|
|
114
|
+
.filter((entry) => entry.eligible)
|
|
115
|
+
.sort((a, b) => b.length - a.length || b.index - a.index)
|
|
116
|
+
.map((entry) => entry.index);
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
// Sanitize sessionId before using it as a path segment. A raw `..` or slash
|
|
90
120
|
// would let the sidecar dir — and clearOffloadSession's readdir+unlink — escape
|
|
91
121
|
// the tool-results root (arbitrary .txt deletion). Strip to [A-Za-z0-9_-];
|
|
@@ -139,15 +169,17 @@ function countLines(text) {
|
|
|
139
169
|
return lines;
|
|
140
170
|
}
|
|
141
171
|
|
|
142
|
-
export async function maybeOffloadToolResult(sessionId, toolCallId, toolName, result) {
|
|
172
|
+
export async function maybeOffloadToolResult(sessionId, toolCallId, toolName, result, options = {}) {
|
|
143
173
|
if (!sessionId || !toolCallId) return result;
|
|
144
174
|
if (typeof result !== 'string') return result;
|
|
145
|
-
if (result.
|
|
175
|
+
if (result.startsWith(TOOL_RESULT_OFFLOAD_PREFIX)) return result;
|
|
176
|
+
const force = options?.force === true;
|
|
177
|
+
if (!force && result.length <= getOffloadThreshold(toolName)) return result;
|
|
146
178
|
// Keep error surfaces inline so the model can self-correct without an
|
|
147
179
|
// extra read turn — but only up to the global default. A giant error
|
|
148
180
|
// (e.g. a megabyte of stack/diff/dump) still offloads so it can't blow up
|
|
149
181
|
// context; small errors (the overwhelming majority) stay inline.
|
|
150
|
-
if (classifyResultKind(result) === 'error'
|
|
182
|
+
if (!force && classifyResultKind(result) === 'error'
|
|
151
183
|
&& result.length <= TOOL_RESULT_OFFLOAD_THRESHOLD_CHARS) return result;
|
|
152
184
|
|
|
153
185
|
// Generate a safe filename — never trust toolCallId as a path component.
|
|
@@ -170,6 +202,69 @@ export async function maybeOffloadToolResult(sessionId, toolCallId, toolName, re
|
|
|
170
202
|
return `${header}\n\n${preview}${suffix}`;
|
|
171
203
|
}
|
|
172
204
|
|
|
205
|
+
// Apply per-tool persistence first, then enforce the message-level
|
|
206
|
+
// budget across the remaining non-Read text results. Selection is
|
|
207
|
+
// largest-first; ties prefer the latest result in the assistant tool batch.
|
|
208
|
+
export async function maybeOffloadToolResultBatch(sessionId, entries, options = {}) {
|
|
209
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
210
|
+
const maxChars = Number(options.maxAggregateChars) > 0
|
|
211
|
+
? Math.trunc(Number(options.maxAggregateChars))
|
|
212
|
+
: TOOL_RESULT_MESSAGE_MAX_CHARS;
|
|
213
|
+
const applyPerToolLimits = options.applyPerToolLimits !== false;
|
|
214
|
+
const offloadResult = typeof options.offloadResult === 'function'
|
|
215
|
+
? options.offloadResult
|
|
216
|
+
: maybeOffloadToolResult;
|
|
217
|
+
const states = source.map((entry) => ({ result: entry?.result, error: null }));
|
|
218
|
+
|
|
219
|
+
if (applyPerToolLimits) {
|
|
220
|
+
await Promise.all(source.map(async (entry, index) => {
|
|
221
|
+
try {
|
|
222
|
+
states[index].result = await offloadResult(
|
|
223
|
+
sessionId,
|
|
224
|
+
entry?.toolCallId,
|
|
225
|
+
entry?.toolName,
|
|
226
|
+
entry?.result,
|
|
227
|
+
{ force: false },
|
|
228
|
+
);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
states[index].error = error;
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const inlineChars = () => states.reduce((total, state, index) => (
|
|
236
|
+
state.error || !isAggregateOffloadEligible(source[index]?.toolName, state.result)
|
|
237
|
+
? total
|
|
238
|
+
: total + state.result.length
|
|
239
|
+
), 0);
|
|
240
|
+
let total = inlineChars();
|
|
241
|
+
const attempted = new Set();
|
|
242
|
+
while (total > maxChars) {
|
|
243
|
+
const ranked = rankAggregateOffloadCandidates(source.map((entry, index) => ({
|
|
244
|
+
toolName: entry?.toolName,
|
|
245
|
+
result: states[index].error || attempted.has(index) ? null : states[index].result,
|
|
246
|
+
})));
|
|
247
|
+
if (ranked.length === 0) break;
|
|
248
|
+
const index = ranked[0];
|
|
249
|
+
attempted.add(index);
|
|
250
|
+
const before = states[index].result;
|
|
251
|
+
try {
|
|
252
|
+
states[index].result = await offloadResult(
|
|
253
|
+
sessionId,
|
|
254
|
+
source[index]?.toolCallId,
|
|
255
|
+
source[index]?.toolName,
|
|
256
|
+
before,
|
|
257
|
+
{ force: true },
|
|
258
|
+
);
|
|
259
|
+
total += String(states[index].result || '').length - before.length;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
states[index].error = error;
|
|
262
|
+
total -= before.length;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return states;
|
|
266
|
+
}
|
|
267
|
+
|
|
173
268
|
// Drop a session's offload sidecars on session close. Unlinks every
|
|
174
269
|
// tool-results/<sessionId>/*.txt entry, rmdirs the directory, and drops
|
|
175
270
|
// the sessionId entry from the module-level counter Map so a long-running
|
|
@@ -144,9 +144,8 @@ export const PROVIDER_SSE_IDLE_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
144
144
|
// to 300s silently collapsed it to 135s, which deterministically beheaded
|
|
145
145
|
// live long-thinking turns (regex-chess / circuit-fibsqrt: ttft ~1.5s, then
|
|
146
146
|
// killed at exactly 135s of silent thinking, 4 attempts ≈ 552s per turn).
|
|
147
|
-
// Reference
|
|
148
|
-
//
|
|
149
|
-
// default and falls back to non-streaming when it does abort. The default is
|
|
147
|
+
// Reference behavior: abort only after a 300s single-gap silence, and fall
|
|
148
|
+
// back to non-streaming instead of killing the turn. The default is
|
|
150
149
|
// therefore a FIXED 300s, decoupled from the warn math. The only remaining
|
|
151
150
|
// coupling is the ordering guarantee: cap at (abort − tick) so the provider
|
|
152
151
|
// layer — which can retry or fall back non-streaming — always fires strictly
|
|
@@ -112,18 +112,18 @@ function smartMiddleTruncate(content) {
|
|
|
112
112
|
const head = fastLines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
|
|
113
113
|
const tail = fastLines.slice(-SMART_BASH_TAIL_LINES).join('\n');
|
|
114
114
|
const middle = fastLines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
|
|
115
|
-
return `${head}\n\n... [
|
|
115
|
+
return `${head}\n\n... [${middle} lines omitted of ${fastLines.length} total — head and tail shown] ...\n\n${tail}`;
|
|
116
116
|
}
|
|
117
117
|
const lines = s.split('\n');
|
|
118
118
|
if (lines.length <= SMART_BASH_MAX_LINES) {
|
|
119
119
|
const head = s.slice(0, SMART_BASH_MAX_BYTES);
|
|
120
|
-
return `${head}\n\n... [
|
|
120
|
+
return `${head}\n\n... [output exceeded ${Math.round(SMART_BASH_MAX_BYTES / 1024)} KB on a single line] ...`;
|
|
121
121
|
}
|
|
122
122
|
const head = lines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
|
|
123
123
|
const tail = lines.slice(-SMART_BASH_TAIL_LINES).join('\n');
|
|
124
124
|
const middle = lines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
|
|
125
125
|
const totalKb = Math.round(s.length / 1024);
|
|
126
|
-
return `${head}\n\n... [
|
|
126
|
+
return `${head}\n\n... [${middle} lines omitted of ${lines.length} total / ${totalKb} KB — head and tail shown] ...\n\n${tail}`;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function _prependDestructiveWarning(command, text) {
|
|
@@ -40,12 +40,12 @@ function pushClampNotice(a, msg) {
|
|
|
40
40
|
|
|
41
41
|
// ripgrep-flavored aliases: models trained on `rg` emit short flags (-A/-B/-C),
|
|
42
42
|
// long flags (--after-context / --before-context / --context), or snake/camel
|
|
43
|
-
// spellings. All fold onto the canonical -A/-B
|
|
43
|
+
// spellings. All fold onto the canonical -A/-B/context so a caller can write grep
|
|
44
44
|
// the way they would write ripgrep on the shell.
|
|
45
45
|
const GREP_CONTEXT_KEY_GROUPS = [
|
|
46
46
|
['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
|
|
47
47
|
['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
|
|
48
|
-
['
|
|
48
|
+
['context', ['context', '-C', 'C', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
|
|
49
49
|
];
|
|
50
50
|
|
|
51
51
|
function grepContextKeyPresent(a, k) {
|
|
@@ -81,7 +81,7 @@ export function applyGrepContextLeadPolicy(args) {
|
|
|
81
81
|
if (!args || typeof args !== 'object' || Array.isArray(args)) return;
|
|
82
82
|
// Idempotent guard: the outer builtin guard (validateBuiltinArgs) and the
|
|
83
83
|
// executor (executeGrepTool) both call this on the SAME args object. The
|
|
84
|
-
// first pass folds aliases onto -A/-B
|
|
84
|
+
// first pass folds aliases onto -A/-B/context and clamps; a second pass would
|
|
85
85
|
// recompute the same folding for no effect. Skip once applied.
|
|
86
86
|
if (args._grepContextPolicyApplied) return;
|
|
87
87
|
for (const [canonical, keys] of GREP_CONTEXT_KEY_GROUPS) {
|
|
@@ -726,7 +726,19 @@ function guardFind(a) {
|
|
|
726
726
|
if (!queryOk) {
|
|
727
727
|
return `Error: find requires non-empty string (or string[]) "query" (got ${describeType(a.query)})`;
|
|
728
728
|
}
|
|
729
|
+
if (hasOwn(a, 'path')) {
|
|
730
|
+
// Absorb: a JSON-stringified array ('["apps/desktop"]') or a
|
|
731
|
+
// single-entry batch (['apps/desktop']). find takes ONE base
|
|
732
|
+
// directory, and a lone entry IS that directory — no ambiguity.
|
|
733
|
+
const coerced = coerceShapeFlex(a.path);
|
|
734
|
+
a.path = (Array.isArray(coerced) && coerced.length === 1 && isString(coerced[0]))
|
|
735
|
+
? coerced[0]
|
|
736
|
+
: coerced;
|
|
737
|
+
}
|
|
729
738
|
if (hasOwn(a, 'path') && !isString(a.path)) {
|
|
739
|
+
if (Array.isArray(a.path)) {
|
|
740
|
+
return `Error: find arg "path" must be a single base directory (got ${a.path.length}) — run find once per base, or widen "query".`;
|
|
741
|
+
}
|
|
730
742
|
return `Error: find arg "path" must be a string (got ${describeType(a.path)})`;
|
|
731
743
|
}
|
|
732
744
|
if (hasOwn(a, 'head_limit') && a.head_limit !== undefined && a.head_limit !== null) {
|
|
@@ -832,6 +844,15 @@ const GUARDS = {
|
|
|
832
844
|
code_graph: guardCodeGraph,
|
|
833
845
|
};
|
|
834
846
|
|
|
847
|
+
// A bridge/provider can materialize an optional schema field as an explicit
|
|
848
|
+
// `undefined`. JSON cannot carry undefined, so it means "not sent" — drop the
|
|
849
|
+
// key instead of failing the call on a field the caller never set.
|
|
850
|
+
function dropUndefinedArgs(args) {
|
|
851
|
+
for (const key of Object.keys(args)) {
|
|
852
|
+
if (args[key] === undefined) delete args[key];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
835
856
|
export function validateBuiltinArgs(toolName, args) {
|
|
836
857
|
const guard = GUARDS[toolName];
|
|
837
858
|
if (!guard) return null;
|
|
@@ -841,6 +862,7 @@ export function validateBuiltinArgs(toolName, args) {
|
|
|
841
862
|
if (typeof args !== 'object' || Array.isArray(args)) {
|
|
842
863
|
return `Error: ${toolName} arguments must be an object (got ${describeType(args)})`;
|
|
843
864
|
}
|
|
865
|
+
dropUndefinedArgs(args);
|
|
844
866
|
if (toolName === 'grep') applyGrepContextLeadPolicy(args);
|
|
845
867
|
try {
|
|
846
868
|
return guard(args) || null;
|
|
@@ -256,7 +256,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
256
256
|
// decode + rm token guard; calling it here applies the same allowlist
|
|
257
257
|
// to both persistent and stateless paths.
|
|
258
258
|
const _rawCmd = String(args && args.command != null ? args.command : '');
|
|
259
|
-
//
|
|
259
|
+
// `apply_patch` typed into the shell (heredoc/argument/bare
|
|
260
260
|
// patch forms) routes to the internal patch engine instead of failing as
|
|
261
261
|
// an unknown binary. Runs BEFORE the exec-policy scan so patch BODY lines
|
|
262
262
|
// (e.g. `+ rm -rf …`) are never misread as shell commands. Dynamic import
|
|
@@ -375,7 +375,10 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
375
375
|
// anywhere in the chain) used to hard-fail via foregroundLongCommandHint
|
|
376
376
|
// (~22 wasted turns/14d measured). Promote them to a background task —
|
|
377
377
|
// the exact remedy the deny message pointed at — same as sleep chains.
|
|
378
|
-
|
|
378
|
+
// Short (2-29s) leading sleep chains stay SYNC: a 5s settle before a
|
|
379
|
+
// verification probe must not detach produce→verify into an async
|
|
380
|
+
// notification round-trip — blocking a few seconds is strictly cheaper.
|
|
381
|
+
const _blockedSleep = detectBlockedSleepPattern(command, 30) || detectLongForegroundReason(command);
|
|
379
382
|
if (_blockedSleep) {
|
|
380
383
|
runInBackground = true;
|
|
381
384
|
autoAsyncReason = _blockedSleep;
|
|
@@ -521,6 +524,9 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
521
524
|
// Per-terminal session stamp: the dispatching terminal's
|
|
522
525
|
// claude.exe pid (server-main threads callerSession.clientHostPid).
|
|
523
526
|
clientHostPid: options?.clientHostPid,
|
|
527
|
+
// Dispatching session: hosts that pool many sessions in one
|
|
528
|
+
// process (desktop) scope the job to its own pane with this.
|
|
529
|
+
ownerSessionId: options?.callerSessionId || options?.sessionId || null,
|
|
524
530
|
...(options?.shellJobRuntime || {}),
|
|
525
531
|
});
|
|
526
532
|
if (job && job.error) {
|
|
@@ -631,6 +637,8 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
631
637
|
// Threaded so an auto-backgrounded foreground job is stamped with
|
|
632
638
|
// the dispatching terminal's claude.exe pid (per-terminal scope).
|
|
633
639
|
clientHostPid: options?.clientHostPid,
|
|
640
|
+
// …and with the dispatching session, for per-pane scoping.
|
|
641
|
+
ownerSessionId: options?.callerSessionId || options?.sessionId || null,
|
|
634
642
|
// MCP live-progress reporter (null unless the client subscribed via
|
|
635
643
|
// callTool onprogress). execShellCommand emits throttled "running
|
|
636
644
|
// Ns" frames while the foreground command runs.
|