mixdog 0.9.92 → 0.9.94
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/README.md +147 -51
- package/package.json +6 -5
- package/scripts/code-graph-description-contract.mjs +6 -8
- package/scripts/tmp-cdp-errors.mjs +41 -0
- package/scripts/tmp-cdp-inspect.mjs +41 -0
- package/scripts/tui-transcript-jitter-harness.mjs +2 -18
- package/src/rules/agent/00-core.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/lead/01-general.md +1 -0
- package/src/rules/shared/01-tool.md +27 -25
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +16 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
- package/src/runtime/agent/orchestrator/agent-trace.mjs +17 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +8 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +79 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +44 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +35 -29
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +5 -6
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +31 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +60 -31
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +119 -109
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -21
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +16 -10
- 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 +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- 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/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- 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 +4 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -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-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +47 -14
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +100 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +32 -2
- package/src/runtime/channels/backends/discord-gateway.mjs +6 -32
- package/src/runtime/channels/lib/inbound-handler.mjs +19 -3
- package/src/runtime/channels/lib/scheduler.mjs +51 -3
- package/src/runtime/channels/lib/worker-main.mjs +4 -0
- package/src/runtime/channels/tool-defs.mjs +4 -2
- package/src/runtime/memory/lib/query-handlers.mjs +11 -3
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/memory/tool-defs.mjs +5 -5
- package/src/runtime/shared/background-tasks.mjs +10 -3
- package/src/runtime/shared/channel-notification-routing.mjs +8 -2
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/llm/http-agent.mjs +11 -0
- 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-surface.mjs +7 -2
- package/src/session-runtime/lifecycle-api.mjs +26 -1
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/session-runtime/tool-catalog-data.mjs +5 -2
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/tag-registry.mjs +5 -1
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/tui/app/use-transcript-window.mjs +7 -1
- package/src/tui/components/Spinner.jsx +18 -9
- package/src/tui/dist/index.mjs +179 -70
- package/src/tui/engine/agent-envelope.mjs +52 -3
- package/src/tui/engine/live-share.mjs +23 -3
- package/src/tui/engine/session-api.mjs +7 -0
- package/src/tui/engine/turn.mjs +85 -13
- package/src/tui/engine.mjs +32 -47
- package/src/tui/index.jsx +7 -0
- package/src/workflows/solo/WORKFLOW.md +0 -6
- package/src/workflows/solo-bench/WORKFLOW.md +17 -0
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* (scripts/openai-oauth-http-sse-toolcall-smoke.mjs) and fallback headers.
|
|
8
8
|
*/
|
|
9
9
|
import { randomBytes } from 'crypto';
|
|
10
|
+
import zlib from 'node:zlib';
|
|
10
11
|
import {
|
|
12
|
+
extractCacheWriteTokens,
|
|
13
|
+
extractCachedTokens,
|
|
11
14
|
traceAgentFetch,
|
|
12
15
|
traceAgentSse,
|
|
13
16
|
traceAgentUsage,
|
|
@@ -47,6 +50,21 @@ const CODEX_REQUEST_MAX_RETRIES = 4;
|
|
|
47
50
|
const CODEX_REQUEST_BACKOFF_MS = Object.freeze([200, 400, 800, 1600]);
|
|
48
51
|
const CODEX_RETRY_JITTER_RATIO = 0.1;
|
|
49
52
|
|
|
53
|
+
// Request-body zstd gate (see sendViaHttpSse). Namespace import: on Node
|
|
54
|
+
// runtimes without zlib zstd bindings the named export would fail at module
|
|
55
|
+
// load, so the call site typeof-guards zlib.zstdCompressSync instead.
|
|
56
|
+
const OPENAI_REQ_ZSTD_MIN_BYTES = 8 * 1024;
|
|
57
|
+
let _openaiReqZstdLatch = false;
|
|
58
|
+
function _openaiReqZstdDisabled() {
|
|
59
|
+
return _openaiReqZstdLatch || process.env.MIXDOG_OPENAI_REQ_ZSTD === '0';
|
|
60
|
+
}
|
|
61
|
+
function _disableOpenaiReqZstd() { _openaiReqZstdLatch = true; }
|
|
62
|
+
function _zstdHeaders(headers, bodyForSend) {
|
|
63
|
+
return bodyForSend.encoding
|
|
64
|
+
? { ...headers, 'Content-Encoding': bodyForSend.encoding }
|
|
65
|
+
: headers;
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
export function _envFlag(name, fallback = true) {
|
|
51
69
|
const raw = process.env[name];
|
|
52
70
|
if (raw == null || raw === '') return fallback;
|
|
@@ -72,11 +90,6 @@ function _parseJsonObject(value) {
|
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
|
|
75
|
-
function _extractCachedTokens(usage) {
|
|
76
|
-
const details = usage?.input_tokens_details || usage?.prompt_tokens_details || {};
|
|
77
|
-
return Number(details.cached_tokens ?? details.cached ?? usage?.cached_tokens ?? 0) || 0;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
93
|
function _sseEventsFromBuffer(buffer) {
|
|
81
94
|
const frames = [];
|
|
82
95
|
let rest = buffer.replace(/\r\n/g, '\n');
|
|
@@ -231,6 +244,19 @@ export async function sendViaHttpSse({
|
|
|
231
244
|
const responsesUrl = auth?.type === 'openai-direct'
|
|
232
245
|
? OPENAI_DIRECT_RESPONSES_URL
|
|
233
246
|
: CODEX_RESPONSES_URL;
|
|
247
|
+
// Request-body zstd (codex parity: core client.rs
|
|
248
|
+
// responses_request_compression enables zstd for the codex backend on the
|
|
249
|
+
// OpenAI provider — the server decompresses Content-Encoding: zstd).
|
|
250
|
+
// openai-direct is excluded: only the codex backend is verified. Env
|
|
251
|
+
// kill-switch plus a process-wide latch flipped on the first 400 seen on
|
|
252
|
+
// a compressed request, which then replays that attempt uncompressed.
|
|
253
|
+
const _rawReqBytes = Buffer.from(JSON.stringify(body));
|
|
254
|
+
let _reqBodyForSend = auth?.type !== 'openai-direct'
|
|
255
|
+
&& !_openaiReqZstdDisabled()
|
|
256
|
+
&& typeof zlib.zstdCompressSync === 'function'
|
|
257
|
+
&& _rawReqBytes.length >= OPENAI_REQ_ZSTD_MIN_BYTES
|
|
258
|
+
? { bytes: zlib.zstdCompressSync(_rawReqBytes), encoding: 'zstd' }
|
|
259
|
+
: { bytes: _rawReqBytes, encoding: null };
|
|
234
260
|
let response;
|
|
235
261
|
for (let attempt = 0; attempt <= CODEX_REQUEST_MAX_RETRIES; attempt++) {
|
|
236
262
|
const headerTimeout = createTimeoutSignal(
|
|
@@ -253,8 +279,8 @@ export async function sendViaHttpSse({
|
|
|
253
279
|
} catch {}
|
|
254
280
|
response = await fetchFn(responsesUrl, {
|
|
255
281
|
method: 'POST',
|
|
256
|
-
headers,
|
|
257
|
-
body:
|
|
282
|
+
headers: _zstdHeaders(headers, _reqBodyForSend),
|
|
283
|
+
body: _reqBodyForSend.bytes,
|
|
258
284
|
signal: headerTimeout.signal,
|
|
259
285
|
dispatcher: getLlmDispatcher(),
|
|
260
286
|
});
|
|
@@ -267,6 +293,15 @@ export async function sendViaHttpSse({
|
|
|
267
293
|
headerTimeout.cleanup();
|
|
268
294
|
}
|
|
269
295
|
|
|
296
|
+
// zstd rejection fallback: a 400 on a compressed request latches
|
|
297
|
+
// compression OFF process-wide and replays this attempt uncompressed.
|
|
298
|
+
if (response && response.status === 400 && _reqBodyForSend.encoding) {
|
|
299
|
+
_disableOpenaiReqZstd();
|
|
300
|
+
_reqBodyForSend = { bytes: _rawReqBytes, encoding: null };
|
|
301
|
+
await response.arrayBuffer().catch(() => {});
|
|
302
|
+
response = undefined;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
270
305
|
const retryableStatus = response && response.status >= 500 && response.status <= 599;
|
|
271
306
|
// Typed transient transport failures only (errno / SDK connection
|
|
272
307
|
// type). An unknown pre-response failure throws immediately instead of
|
|
@@ -783,7 +818,8 @@ export async function sendViaHttpSse({
|
|
|
783
818
|
usage = {
|
|
784
819
|
inputTokens: resp.usage.input_tokens || 0,
|
|
785
820
|
outputTokens: resp.usage.output_tokens || 0,
|
|
786
|
-
cachedTokens:
|
|
821
|
+
cachedTokens: extractCachedTokens(resp.usage),
|
|
822
|
+
cacheWriteTokens: extractCacheWriteTokens(resp.usage),
|
|
787
823
|
promptTokens: resp.usage.input_tokens || 0,
|
|
788
824
|
raw: serviceTier ? { ...resp.usage, service_tier: serviceTier } : resp.usage,
|
|
789
825
|
};
|
|
@@ -39,6 +39,9 @@ export function _combineUsageWithWarmup(actual, warmup, { separateMainContext =
|
|
|
39
39
|
inputTokens: _usageNum(actual.inputTokens) + _usageNum(warmup.inputTokens),
|
|
40
40
|
outputTokens: _usageNum(actual.outputTokens) + _usageNum(warmup.outputTokens),
|
|
41
41
|
cachedTokens: _usageNum(actual.cachedTokens) + _usageNum(warmup.cachedTokens),
|
|
42
|
+
...(actual.cacheWriteTokens != null || warmup.cacheWriteTokens != null
|
|
43
|
+
? { cacheWriteTokens: _usageNum(actual.cacheWriteTokens) + _usageNum(warmup.cacheWriteTokens) }
|
|
44
|
+
: {}),
|
|
42
45
|
promptTokens: _usageNum(actual.promptTokens) + _usageNum(warmup.promptTokens),
|
|
43
46
|
warmupInputTokens: _usageNum(warmup.inputTokens),
|
|
44
47
|
warmupCachedTokens: _usageNum(warmup.cachedTokens),
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { randomBytes } from 'crypto';
|
|
17
17
|
import { performance } from 'node:perf_hooks';
|
|
18
18
|
import {
|
|
19
|
+
extractCacheWriteTokens,
|
|
19
20
|
extractCachedTokens,
|
|
20
21
|
appendAgentTrace,
|
|
21
22
|
} from '../agent-trace.mjs';
|
|
@@ -1090,6 +1091,7 @@ export async function _streamResponse({
|
|
|
1090
1091
|
inputTokens: u.input_tokens || 0,
|
|
1091
1092
|
outputTokens: u.output_tokens || 0,
|
|
1092
1093
|
cachedTokens: extractCachedTokens(u),
|
|
1094
|
+
cacheWriteTokens: extractCacheWriteTokens(u),
|
|
1093
1095
|
// openai-oauth reports input_tokens as the total
|
|
1094
1096
|
// prompt volume (cached portion is a subset, not
|
|
1095
1097
|
// additive). Alias into the cross-provider
|
|
@@ -227,7 +227,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
227
227
|
// turn's push (deferBodies below) collapse to markers now — the model has
|
|
228
228
|
// already seen them on that turn's follow-up send. Failed bodies stay
|
|
229
229
|
// verbatim for retry.
|
|
230
|
-
|
|
230
|
+
// Out-of-loop transcript mutations (post-turn/manual compaction in
|
|
231
|
+
// manager/compaction-runner.mjs) run where no send opts exist; they park a
|
|
232
|
+
// one-shot intent on the session so the FIRST send of the next turn tags
|
|
233
|
+
// its expected cache break instead of an unexplained prefix mismatch.
|
|
234
|
+
if (!opts.cacheBreakIntent && typeof sessionRef?.pendingCacheBreakIntent === 'string') {
|
|
235
|
+
opts.cacheBreakIntent = sessionRef.pendingCacheBreakIntent;
|
|
236
|
+
delete sessionRef.pendingCacheBreakIntent;
|
|
237
|
+
}
|
|
238
|
+
if (compactSettledToolCallBodies(messages) && !opts.cacheBreakIntent) {
|
|
239
|
+
opts.cacheBreakIntent = 'deferred_body_compaction';
|
|
240
|
+
}
|
|
231
241
|
// ---- Codex turn stop hook (refs/codex core/src/session/turn.rs:372-404) --
|
|
232
242
|
// A no-tool assistant message is TERMINAL. Only a structured provider
|
|
233
243
|
// follow-up signal (end_turn=false / pause_turn), pending input, tool
|
|
@@ -526,7 +536,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
526
536
|
const _m = messages[_i];
|
|
527
537
|
if (_m && _m.role === 'tool' && typeof _m.content === 'string' && _m.content.includes('⚠')) {
|
|
528
538
|
const _stripped = stripSoftWarns(_m.content);
|
|
529
|
-
if (_stripped !== _m.content)
|
|
539
|
+
if (_stripped !== _m.content) {
|
|
540
|
+
_m.content = _stripped;
|
|
541
|
+
if (!opts.cacheBreakIntent) opts.cacheBreakIntent = 'soft_warn_strip';
|
|
542
|
+
}
|
|
530
543
|
}
|
|
531
544
|
}
|
|
532
545
|
sendTools = snapshotProviderRequestTools({
|
|
@@ -645,14 +658,16 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
645
658
|
);
|
|
646
659
|
const _sendEndedAt = Date.now();
|
|
647
660
|
if (_sendResult.action === 'retry') {
|
|
648
|
-
|
|
661
|
+
// Keep opts.cacheBreakIntent: the failed send never consumed the
|
|
662
|
+
// tag, and the reactive-compact retry that follows IS the tagged
|
|
663
|
+
// transition — deleting it here made retry-side cache_break rows
|
|
664
|
+
// log intentional_transition: null.
|
|
649
665
|
contextOverflowRetryUsed = true;
|
|
650
666
|
reactiveOverflowRetryPending = true;
|
|
651
667
|
continue;
|
|
652
668
|
}
|
|
653
669
|
if (_sendResult.action === 'retry_transport') {
|
|
654
670
|
_transportRetriesUsed += 1;
|
|
655
|
-
delete opts.cacheBreakIntent;
|
|
656
671
|
continue;
|
|
657
672
|
}
|
|
658
673
|
response = _sendResult.response;
|
|
@@ -1021,7 +1036,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1021
1036
|
// Settle earlier deferred bodies before this turn's message lands:
|
|
1022
1037
|
// every previous call already has its result row, so successful bodies
|
|
1023
1038
|
// compact to markers while failed ones keep their full retry text.
|
|
1024
|
-
compactSettledToolCallBodies(messages)
|
|
1039
|
+
if (compactSettledToolCallBodies(messages) && !opts.cacheBreakIntent) {
|
|
1040
|
+
opts.cacheBreakIntent = 'deferred_body_compaction';
|
|
1041
|
+
}
|
|
1025
1042
|
messages.push(_assistantTurnMsg);
|
|
1026
1043
|
try { opts.onAssistantMessageCommitted?.(_assistantTurnMsg); } catch {}
|
|
1027
1044
|
const _callsToExecute = calls;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join, resolve as _pathResolve, isAbsolute as _pathIsAbs, normalize as _pathNorm } from 'node:path';
|
|
6
6
|
import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
|
|
7
7
|
import { _normalizeCacheKey } from './util.mjs';
|
|
8
|
+
import { GREP_AUTO_CONTEXT_LINES } from '../../tools/builtin/path-utils.mjs';
|
|
8
9
|
|
|
9
10
|
const MAX_PER_SESSION = 100;
|
|
10
11
|
|
|
@@ -43,6 +44,22 @@ function _firstArg(args, names) {
|
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
const _GREP_CONTEXT_GROUPS = [
|
|
48
|
+
['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
|
|
49
|
+
['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
|
|
50
|
+
['context', ['context', '-C', 'C', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function _canonicalizeGrepContextArgs(args) {
|
|
54
|
+
for (const [canonical, aliases] of _GREP_CONTEXT_GROUPS) {
|
|
55
|
+
const value = _firstArg(args, aliases);
|
|
56
|
+
for (const alias of aliases) delete args[alias];
|
|
57
|
+
if (value === undefined) continue;
|
|
58
|
+
const numeric = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
59
|
+
args[canonical] = Number.isFinite(numeric) && Number.isInteger(numeric) ? numeric : value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
function _canonicalToolArgs(toolName, args) {
|
|
47
64
|
if (!args || typeof args !== 'object') return args;
|
|
48
65
|
const next = { ...args };
|
|
@@ -59,12 +76,35 @@ function _canonicalToolArgs(toolName, args) {
|
|
|
59
76
|
const alias = _firstArg(next, ['root', 'directory', 'dir']);
|
|
60
77
|
if (alias !== undefined) next.path = alias;
|
|
61
78
|
}
|
|
79
|
+
_canonicalizeGrepContextArgs(next);
|
|
62
80
|
if ((next.output_mode === undefined || next.output_mode === null || next.output_mode === '') && typeof next.mode === 'string') {
|
|
63
81
|
const mode = next.mode.trim();
|
|
64
|
-
if (['files_with_matches', 'content', 'count'].includes(mode)) next.output_mode = mode;
|
|
82
|
+
if (['files_with_matches', 'content', 'content_with_context', 'count'].includes(mode)) next.output_mode = mode;
|
|
65
83
|
}
|
|
66
84
|
for (const k of ['query', 'regex', 'regexp', 'needle', 'search', 'literal', 'file_pattern', 'filePattern', 'include', 'includes', 'files', 'root', 'directory', 'dir']) delete next[k];
|
|
67
|
-
|
|
85
|
+
delete next.mode;
|
|
86
|
+
|
|
87
|
+
// Canonicalize by execution semantics, not caller spelling:
|
|
88
|
+
// omitted/content_with_context => content + automatic 25-line context;
|
|
89
|
+
// context:0 => bare content; context flags are ignored in count/files.
|
|
90
|
+
const requestedMode = typeof next.output_mode === 'string' ? next.output_mode.trim() : '';
|
|
91
|
+
if (requestedMode === 'files_with_matches' || requestedMode === 'count') {
|
|
92
|
+
next.output_mode = requestedMode;
|
|
93
|
+
delete next['-A'];
|
|
94
|
+
delete next['-B'];
|
|
95
|
+
delete next.context;
|
|
96
|
+
} else {
|
|
97
|
+
const hasExplicitContext = ['-A', '-B', 'context']
|
|
98
|
+
.some((key) => Object.prototype.hasOwnProperty.call(next, key));
|
|
99
|
+
next.output_mode = 'content';
|
|
100
|
+
if ((requestedMode === '' || requestedMode === 'content_with_context') && !hasExplicitContext) {
|
|
101
|
+
next.context = GREP_AUTO_CONTEXT_LINES;
|
|
102
|
+
}
|
|
103
|
+
if (next.context === 0 && !Object.prototype.hasOwnProperty.call(next, '-A')
|
|
104
|
+
&& !Object.prototype.hasOwnProperty.call(next, '-B')) {
|
|
105
|
+
delete next.context;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
68
108
|
} else if (toolName === 'glob') {
|
|
69
109
|
if (next.pattern === undefined || next.pattern === null || next.pattern === '') {
|
|
70
110
|
const alias = _firstArg(next, ['glob', 'file_pattern', 'filePattern', 'name', 'include', 'includes', 'files']);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// Eager tool-dispatch controller, extracted from agent-loop.mjs. Owns the
|
|
2
2
|
// per-turn pending promise map, the intra-turn in-flight signature set, and
|
|
3
|
-
// the mutation epoch.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// the mutation epoch. Read-only calls may start while the provider is still
|
|
4
|
+
// streaming. Side-effect calls stream-start too until the first apply_patch
|
|
5
|
+
// appears in the stream (they are definitively in segment 0); after that they
|
|
6
|
+
// wait for their segment bounded by apply_patch barriers. Patches execute
|
|
7
|
+
// serially as barriers, so a failed patch can skip every later side effect.
|
|
8
8
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
9
9
|
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
10
|
-
import { _intraTurnSig, _isMutationTool, _isReadTool, _isScopedCacheableTool,
|
|
10
|
+
import { _intraTurnSig, _isMutationTool, _isOrderedGateSkippable, _isReadTool, _isScopedCacheableTool, _stripMcpPrefix } from './loop/tool-classify.mjs';
|
|
11
11
|
import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
|
|
12
12
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
13
13
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
14
14
|
import { crossTurnSignature } from './loop/completion-guards.mjs';
|
|
15
|
-
import { getToolKind, isParallelDispatchable, isToolCallDedupEligible } from './loop/tool-helpers.mjs';
|
|
15
|
+
import { getToolKind, isEagerDispatchable, isParallelDispatchable, isToolCallDedupEligible } from './loop/tool-helpers.mjs';
|
|
16
16
|
|
|
17
17
|
export function createEagerDispatcher({
|
|
18
18
|
tools, cwd, sessionId, sessionRef, signal, opts,
|
|
@@ -40,18 +40,11 @@ export function createEagerDispatcher({
|
|
|
40
40
|
// resets at the turn boundary without leaking across getIterations().
|
|
41
41
|
const _eagerInFlightSigs = new Map();
|
|
42
42
|
const epoch = { mutation: 0 };
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
let _streamSawMutation = false;
|
|
49
|
-
const _hasEarlierMutation = (calls, index) => {
|
|
50
|
-
for (let k = 0; k < index; k += 1) {
|
|
51
|
-
if (_isMutationTool(calls[k]?.name)) return true;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
};
|
|
43
|
+
// True once an apply_patch tool_use has been seen in THIS turn's
|
|
44
|
+
// stream. Before that, every side-effect call streamed so far sits
|
|
45
|
+
// before the first patch in call order (segment 0), so it may start
|
|
46
|
+
// immediately — a patch that arrives later cannot gate an EARLIER call.
|
|
47
|
+
let _streamSeenMutation = false;
|
|
55
48
|
const startEagerTool = (call) => {
|
|
56
49
|
if (!call?.id || pending.has(call.id) || !isParallelDispatchable(call.name)) return null;
|
|
57
50
|
// Never eager-execute a call whose arguments failed to parse
|
|
@@ -107,7 +100,7 @@ export function createEagerDispatcher({
|
|
|
107
100
|
if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
|
|
108
101
|
entry.promise = (async () => {
|
|
109
102
|
try {
|
|
110
|
-
return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration() }) };
|
|
103
|
+
return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration(), deferShellCwdCommit: true }) };
|
|
111
104
|
} catch (error) {
|
|
112
105
|
return { ok: false, error };
|
|
113
106
|
}
|
|
@@ -168,16 +161,21 @@ export function createEagerDispatcher({
|
|
|
168
161
|
return entry;
|
|
169
162
|
};
|
|
170
163
|
const startEagerRun = (calls, startIndex, dupSet) => {
|
|
164
|
+
const _nextOrderedMutationIndex = calls.findIndex(
|
|
165
|
+
(call, index) => index >= startIndex && _isMutationTool(call?.name),
|
|
166
|
+
);
|
|
171
167
|
for (let j = startIndex; j < calls.length; j += 1) {
|
|
172
168
|
const call = calls[j];
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
169
|
+
// Side effects may eager-start in the current segment but not
|
|
170
|
+
// across the next apply_patch barrier. Known read-only work
|
|
171
|
+
// may cross it and is protected by mutation-epoch re-execution.
|
|
176
172
|
if (!call?.id || !isParallelDispatchable(call.name)) continue;
|
|
177
173
|
if (dupSet && dupSet.has(call.id)) continue;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
if (
|
|
175
|
+
_nextOrderedMutationIndex >= 0
|
|
176
|
+
&& j >= _nextOrderedMutationIndex
|
|
177
|
+
&& _isOrderedGateSkippable(call.name)
|
|
178
|
+
) continue;
|
|
181
179
|
// A null return here is NOT a state barrier. It means a
|
|
182
180
|
// non-barrier stub — intra-turn in-flight dup, repeat-failure /
|
|
183
181
|
// cross-turn dedup, pre-dispatch-deny, invalid-args, or a cache
|
|
@@ -188,9 +186,17 @@ export function createEagerDispatcher({
|
|
|
188
186
|
}
|
|
189
187
|
};
|
|
190
188
|
const onToolCall = (call) => {
|
|
191
|
-
if (_isMutationTool(call?.name)) {
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
if (_isMutationTool(call?.name)) {
|
|
190
|
+
_streamSeenMutation = true;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Declared read-only calls always overlap streaming (epoch guard
|
|
194
|
+
// re-executes them if a later patch lands). Side-effect calls may
|
|
195
|
+
// stream-start only while NO apply_patch has streamed yet: their
|
|
196
|
+
// call-order position is already fixed before the first barrier,
|
|
197
|
+
// matching exactly what startEagerRun would do post-batch. Once a
|
|
198
|
+
// patch has streamed, later side effects wait for their segment.
|
|
199
|
+
if (!isEagerDispatchable(call?.name, tools) && _streamSeenMutation) return;
|
|
194
200
|
startEagerTool(call);
|
|
195
201
|
};
|
|
196
202
|
return { pending, epoch, startEagerTool, startEagerRun, onToolCall };
|
|
@@ -59,6 +59,9 @@ function compactStoredToolArgString(value, key = '', opts = {}) {
|
|
|
59
59
|
const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
|
|
60
60
|
const limit = isLong ? STORED_TOOL_ARG_LIMIT : Infinity;
|
|
61
61
|
if (value.length <= limit) return value;
|
|
62
|
+
// A marker is about to replace verbatim text — report the mutation so
|
|
63
|
+
// sweep callers can tag the resulting prefix-cache break.
|
|
64
|
+
try { opts.onCompacted?.(); } catch { /* observability only */ }
|
|
62
65
|
const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
63
66
|
// Body markers carry the recovery instruction inline: the compaction
|
|
64
67
|
// detectors only require the `[mixdog compacted ...]` shape (no ']' or
|
|
@@ -115,14 +118,19 @@ export function compactToolCallsForHistory(calls, opts = {}) {
|
|
|
115
118
|
// contract as restoreToolCallBodyForId);
|
|
116
119
|
// - a call with no result row yet (current batch / interrupted turn) is
|
|
117
120
|
// left untouched.
|
|
121
|
+
// Returns true when at least one body was actually collapsed to a marker
|
|
122
|
+
// (i.e. the transcript prefix changed), so callers can tag the intentional
|
|
123
|
+
// cache break instead of logging an unexplained input_prefix_mismatch.
|
|
118
124
|
export function compactSettledToolCallBodies(messages) {
|
|
119
|
-
if (!Array.isArray(messages)) return;
|
|
125
|
+
if (!Array.isArray(messages)) return false;
|
|
120
126
|
const resultKinds = new Map();
|
|
121
127
|
for (const message of messages) {
|
|
122
128
|
if (message?.role === 'tool' && message.toolCallId) {
|
|
123
129
|
resultKinds.set(message.toolCallId, message.toolKind || 'normal');
|
|
124
130
|
}
|
|
125
131
|
}
|
|
132
|
+
let changed = false;
|
|
133
|
+
const sweepOpts = { onCompacted: () => { changed = true; } };
|
|
126
134
|
for (const message of messages) {
|
|
127
135
|
if (message?.role !== 'assistant' || !Array.isArray(message.toolCalls)) continue;
|
|
128
136
|
for (const call of message.toolCalls) {
|
|
@@ -130,9 +138,10 @@ export function compactSettledToolCallBodies(messages) {
|
|
|
130
138
|
if (!call.arguments || typeof call.arguments !== 'object') continue;
|
|
131
139
|
const kind = call.id ? resultKinds.get(call.id) : undefined;
|
|
132
140
|
if (kind === undefined || kind === 'error') continue;
|
|
133
|
-
call.arguments = compactStoredToolArgValue(call.arguments);
|
|
141
|
+
call.arguments = compactStoredToolArgValue(call.arguments, '', 0, sweepOpts);
|
|
134
142
|
}
|
|
135
143
|
}
|
|
144
|
+
return changed;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
// Restore retry-safe long command/script text for ONE failed tool call inside a
|
|
@@ -15,12 +15,11 @@ export function _isMutationTool(name) {
|
|
|
15
15
|
const n = _stripMcpPrefix(name);
|
|
16
16
|
return n === 'apply_patch';
|
|
17
17
|
}
|
|
18
|
-
// Side-effect-free read-only tools that
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// apply_patch-only for epoch-mutation counting and eager-dispatch gating.
|
|
18
|
+
// Side-effect-free read-only tools that may eager-start across an upcoming
|
|
19
|
+
// apply_patch barrier and may keep running after an earlier patch fails.
|
|
20
|
+
// Everything NOT in this set waits for its side-effect segment and is skipped
|
|
21
|
+
// after a failed patch. Kept separate from _isMutationTool, which stays
|
|
22
|
+
// apply_patch-only for epoch counting.
|
|
24
23
|
const ORDERED_GATE_SAFE_READONLY_TOOLS = new Set([
|
|
25
24
|
'read',
|
|
26
25
|
'find',
|
|
@@ -226,8 +226,37 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
226
226
|
return result;
|
|
227
227
|
}
|
|
228
228
|
if (name === 'apply_patch') {
|
|
229
|
-
const patchArgs = typeof args === 'string' ? { patch: args } : args;
|
|
230
|
-
|
|
229
|
+
const patchArgs = typeof args === 'string' ? { patch: args } : { ...(args || {}) };
|
|
230
|
+
// post_shell: optional verification command executed through the normal
|
|
231
|
+
// one-shot shell path ONLY after the patch applies cleanly; a failed
|
|
232
|
+
// patch skips it. Patch text and shell output return as ONE tool
|
|
233
|
+
// result, so patch+verify costs a single call. Runtime-only knob:
|
|
234
|
+
// stripped before executePatchTool sees the args.
|
|
235
|
+
const postShell = typeof patchArgs.post_shell === 'string' && patchArgs.post_shell.trim()
|
|
236
|
+
? patchArgs.post_shell.trim()
|
|
237
|
+
: null;
|
|
238
|
+
delete patchArgs.post_shell;
|
|
239
|
+
const patchResult = await executePatchTool(name, patchArgs, cwd, {
|
|
240
|
+
sessionId: callerSessionId,
|
|
241
|
+
toolCallId: executeOpts.toolCallId || null,
|
|
242
|
+
});
|
|
243
|
+
if (!postShell) return patchResult;
|
|
244
|
+
const patchNorm = normalizeToolEnvelope(patchResult);
|
|
245
|
+
const patchText = typeof patchNorm.result === 'string' ? patchNorm.result : String(patchNorm.result ?? '');
|
|
246
|
+
// Text-based failure detection only: legacy string returns normalize
|
|
247
|
+
// to explicitSuccess:false even on success, so that flag is unusable here.
|
|
248
|
+
const patchFailed = /^Error[\s:[]/.test(patchText.trimStart());
|
|
249
|
+
if (patchFailed) {
|
|
250
|
+
return `${patchText}\n--- post_shell skipped: patch failed ---`;
|
|
251
|
+
}
|
|
252
|
+
const shellRes = await executeBuiltinTool('shell', { command: postShell }, cwd, completionToolOpts);
|
|
253
|
+
const shellNorm = normalizeToolEnvelope(shellRes);
|
|
254
|
+
const shellText = typeof shellNorm.result === 'string' ? shellNorm.result : String(shellNorm.result ?? '');
|
|
255
|
+
const shellFailed = /^Error[\s:[]/.test(shellText.trimStart());
|
|
256
|
+
const header = shellFailed
|
|
257
|
+
? '--- post_shell FAILED (patch is applied; fix and re-verify) ---'
|
|
258
|
+
: '--- post_shell ---';
|
|
259
|
+
return `${patchText}\n\n${header}\n${shellText}`;
|
|
231
260
|
}
|
|
232
261
|
if (isBuiltinTool(name)) {
|
|
233
262
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
compactTypeForSession,
|
|
26
26
|
} from './context-meta.mjs';
|
|
27
27
|
import { resolveSemanticSummaryModel } from '../loop/compact-policy.mjs';
|
|
28
|
+
import { traceAgentCompact, messagePrefixHash } from '../../agent-trace.mjs';
|
|
28
29
|
import { uncachedInputTokensForProvider } from './usage-metrics.mjs';
|
|
29
30
|
import { pruneOffloadSession } from '../tool-result-offload.mjs';
|
|
30
31
|
import { _getPendingMessagesForSession } from './pending-messages.mjs';
|
|
@@ -302,6 +303,7 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
302
303
|
semanticCompact: false,
|
|
303
304
|
};
|
|
304
305
|
const budget = targetBudgetTokens;
|
|
306
|
+
const compactStartedAt = Date.now();
|
|
305
307
|
try { await opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
306
308
|
const provider = opts.provider || getProvider(session.provider) || null;
|
|
307
309
|
let compacted;
|
|
@@ -486,6 +488,31 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
486
488
|
lastRecallFastTrackError: recallFastTrackError?.message || null,
|
|
487
489
|
lastError: compactError?.message || semanticCompactError?.message || recallFastTrackError?.message || String(compactError || semanticCompactError || recallFastTrackError || 'compact failed'),
|
|
488
490
|
};
|
|
491
|
+
// compact_meta parity with the loop's pre-send pass: the out-of-loop
|
|
492
|
+
// (post-turn/manual) compaction failure was previously invisible to
|
|
493
|
+
// trace analytics.
|
|
494
|
+
traceAgentCompact({
|
|
495
|
+
sessionId: opts.sessionId || session.id || null,
|
|
496
|
+
stage: mode === 'auto' ? 'post_turn' : 'manual',
|
|
497
|
+
trigger: mode,
|
|
498
|
+
compact_type: compactType,
|
|
499
|
+
compact_changed: false,
|
|
500
|
+
before_count: messages.length,
|
|
501
|
+
after_count: messages.length,
|
|
502
|
+
context_window: positiveContextWindow(session.contextWindow) || null,
|
|
503
|
+
budget_tokens: boundary,
|
|
504
|
+
boundary_tokens: boundary,
|
|
505
|
+
target_budget_tokens: budget,
|
|
506
|
+
reserve_tokens: reserveTokens,
|
|
507
|
+
pressure_tokens: pressureTokens,
|
|
508
|
+
trigger_tokens: triggerTokens,
|
|
509
|
+
message_tokens_est: beforeMessageTokens,
|
|
510
|
+
duration_ms: Date.now() - compactStartedAt,
|
|
511
|
+
provider: session.provider || null,
|
|
512
|
+
model: session.model || null,
|
|
513
|
+
error: session.compaction.lastError,
|
|
514
|
+
error_code: 'compact_failed',
|
|
515
|
+
});
|
|
489
516
|
return {
|
|
490
517
|
changed: false,
|
|
491
518
|
error: session.compaction.lastError,
|
|
@@ -574,6 +601,39 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
574
601
|
compactCount: (session.compaction?.compactCount || 0) + (changed ? 1 : 0),
|
|
575
602
|
};
|
|
576
603
|
if (changed) invalidateProviderContextBaseline(session);
|
|
604
|
+
// Observability parity with the loop's pre-send pass: record the
|
|
605
|
+
// out-of-loop mutation as compact_meta and park a one-shot intent so the
|
|
606
|
+
// next turn's first send tags its cache break instead of logging an
|
|
607
|
+
// unexplained input_prefix_mismatch (observed live: a 403k→10k post-turn
|
|
608
|
+
// compact traced as intentional_transition: null with no compact_meta).
|
|
609
|
+
let beforePrefixHash = null;
|
|
610
|
+
try { beforePrefixHash = messagePrefixHash(messages); } catch { /* best-effort */ }
|
|
611
|
+
traceAgentCompact({
|
|
612
|
+
sessionId: pruneSessionId || null,
|
|
613
|
+
stage: mode === 'auto' ? 'post_turn' : 'manual',
|
|
614
|
+
trigger: mode,
|
|
615
|
+
compact_type: compactType,
|
|
616
|
+
compact_changed: changed,
|
|
617
|
+
input_prefix_hash: beforePrefixHash,
|
|
618
|
+
before_count: messages.length,
|
|
619
|
+
after_count: compacted.length,
|
|
620
|
+
before_bytes: beforeEncoded ? Buffer.byteLength(beforeEncoded, 'utf8') : null,
|
|
621
|
+
after_bytes: afterEncoded ? Buffer.byteLength(afterEncoded, 'utf8') : null,
|
|
622
|
+
context_window: positiveContextWindow(session.contextWindow) || null,
|
|
623
|
+
budget_tokens: boundary,
|
|
624
|
+
boundary_tokens: boundary,
|
|
625
|
+
target_budget_tokens: budget,
|
|
626
|
+
reserve_tokens: reserveTokens,
|
|
627
|
+
pressure_tokens: pressureTokens,
|
|
628
|
+
trigger_tokens: triggerTokens,
|
|
629
|
+
message_tokens_est: beforeMessageTokens,
|
|
630
|
+
duration_ms: Date.now() - compactStartedAt,
|
|
631
|
+
provider: session.provider || null,
|
|
632
|
+
model: session.model || null,
|
|
633
|
+
});
|
|
634
|
+
if (changed) {
|
|
635
|
+
session.pendingCacheBreakIntent = mode === 'auto' ? 'post_turn_compaction' : 'manual_compaction';
|
|
636
|
+
}
|
|
577
637
|
return {
|
|
578
638
|
changed,
|
|
579
639
|
reason: unchangedReason,
|