mixdog 0.9.32 → 0.9.34
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 +4 -2
- package/scripts/compact-trigger-migration-smoke.mjs +37 -31
- package/scripts/provider-toolcall-test.mjs +535 -36
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +2 -3
- package/src/repl.mjs +9 -1
- package/src/rules/shared/01-tool.md +4 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +21 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -18
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -12
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +131 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +102 -26
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +194 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +39 -6
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +15 -23
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +16 -8
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +50 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +53 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +270 -47
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +7 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +5 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +18 -3
- package/src/runtime/memory/lib/memory-embed.mjs +89 -8
- package/src/session-runtime/context-status.mjs +7 -6
- package/src/session-runtime/mcp-glue.mjs +5 -5
- package/src/session-runtime/plugin-mcp.mjs +36 -1
- package/src/session-runtime/resource-api.mjs +14 -9
- package/src/session-runtime/runtime-core.mjs +0 -10
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/model-picker.mjs +48 -12
- package/src/tui/app/route-pickers.mjs +4 -0
- package/src/tui/app/slash-dispatch.mjs +6 -1
- package/src/tui/app/use-transcript-window.mjs +13 -1
- package/src/tui/components/StatusLine.jsx +5 -5
- package/src/tui/dist/index.mjs +46 -15
- package/src/ui/statusline.mjs +4 -10
- package/src/vendor/statusline/bin/statusline-route.mjs +4 -5
- package/src/vendor/statusline/src/gateway/route-meta.mjs +3 -2
|
@@ -27,6 +27,9 @@ export const SUMMARY_OUTPUT_TOKENS = 4_096;
|
|
|
27
27
|
// - recall-fasttrack injection cap (loop.mjs recallTokenCap)
|
|
28
28
|
// Keep them in lockstep; do not fork per-consumer ratios without a decision.
|
|
29
29
|
export const CONTEXT_SHARE_RATIO = 0.10;
|
|
30
|
+
export const DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT = 90;
|
|
31
|
+
export const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
32
|
+
export const COMPACT_SAFETY_PERCENT = 1.00;
|
|
30
33
|
// Floor for the recall-injection cap so small-context models still get a
|
|
31
34
|
// usable recall slice (cap = max(floor, contextWindow * CONTEXT_SHARE_RATIO)).
|
|
32
35
|
export const RECALL_TOKEN_CAP_FLOOR_TOKENS = 2_048;
|
|
@@ -22,6 +22,9 @@ export {
|
|
|
22
22
|
SUMMARY_PREFIX,
|
|
23
23
|
SUMMARY_OUTPUT_TOKENS,
|
|
24
24
|
CONTEXT_SHARE_RATIO,
|
|
25
|
+
DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT,
|
|
26
|
+
COMPACT_TARGET_MIN_TOKENS,
|
|
27
|
+
COMPACT_SAFETY_PERCENT,
|
|
25
28
|
RECALL_TOKEN_CAP_FLOOR_TOKENS,
|
|
26
29
|
COMPACT_SUMMARY_MIN_ROOM_TOKENS,
|
|
27
30
|
COMPACT_TYPE_SEMANTIC,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isOffloadedToolResultText } from './tool-result-offload.mjs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { isAgentOwner } from '../agent-owner.mjs';
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Conservative, Unicode-aware token estimator.
|
|
@@ -225,13 +226,45 @@ export function resolveCompactBufferTokens(boundaryTokens, cfg = {}, opts = {})
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
export function resolveCompactTriggerTokens(sessionOrConfig = {}, boundaryTokens = 0) {
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
return resolveSessionCompactPolicy(sessionOrConfig, boundaryTokens).triggerTokens;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Single source of truth for per-session compaction policy math. Manager
|
|
233
|
+
// (compactTriggerForSession), the turn loop (resolveWorkerCompactPolicy), and
|
|
234
|
+
// the /context gauge all derive their trigger/buffer from here so the numbers
|
|
235
|
+
// never diverge. Rules:
|
|
236
|
+
// - a truly-explicit sub-boundary auto-compact limit always wins
|
|
237
|
+
// (trigger = limit) for every session type;
|
|
238
|
+
// - agent-owned semantic sessions otherwise keep the default early-trigger
|
|
239
|
+
// buffer (config-driven, default 10% -> compact at 90% of the boundary);
|
|
240
|
+
// - main/user recall-fasttrack sessions have NO default buffer and compact on
|
|
241
|
+
// the boundary itself (100%).
|
|
242
|
+
// Returns the sanitized explicit limit (null when absent/legacy full-window)
|
|
243
|
+
// plus triggerTokens / bufferTokens / bufferRatio for the given boundary.
|
|
244
|
+
export function resolveSessionCompactPolicy(sessionOrConfig = {}, boundaryTokens = 0) {
|
|
230
245
|
const cfg = sessionOrConfig?.compaction || sessionOrConfig || {};
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
const boundary = positiveTokenInt(boundaryTokens);
|
|
247
|
+
if (!boundary) {
|
|
248
|
+
return {
|
|
249
|
+
autoCompactTokenLimit: null,
|
|
250
|
+
triggerTokens: null,
|
|
251
|
+
bufferTokens: 0,
|
|
252
|
+
bufferRatio: resolveCompactBufferRatio(cfg),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const rawLimit = positiveTokenInt(sessionOrConfig?.autoCompactTokenLimit ?? cfg?.autoCompactTokenLimit);
|
|
256
|
+
const explicitLimit = rawLimit && rawLimit < boundary ? rawLimit : null;
|
|
257
|
+
let triggerTokens;
|
|
258
|
+
if (explicitLimit) {
|
|
259
|
+
triggerTokens = explicitLimit;
|
|
260
|
+
} else if (isAgentOwner(sessionOrConfig)) {
|
|
261
|
+
triggerTokens = Math.max(1, boundary - resolveCompactBufferTokens(boundary, cfg));
|
|
262
|
+
} else {
|
|
263
|
+
triggerTokens = boundary;
|
|
264
|
+
}
|
|
265
|
+
const bufferTokens = Math.max(0, boundary - triggerTokens);
|
|
266
|
+
const bufferRatio = bufferTokens / boundary;
|
|
267
|
+
return { autoCompactTokenLimit: explicitLimit, triggerTokens, bufferTokens, bufferRatio };
|
|
235
268
|
}
|
|
236
269
|
|
|
237
270
|
function stripSystemReminder(text) {
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
// against live session state).
|
|
5
5
|
import {
|
|
6
6
|
estimateRequestReserveTokens,
|
|
7
|
-
|
|
8
|
-
resolveCompactBufferTokens,
|
|
7
|
+
resolveSessionCompactPolicy,
|
|
9
8
|
} from '../context-utils.mjs';
|
|
10
9
|
import {
|
|
11
10
|
compactTypeIsRecallFastTrack,
|
|
@@ -13,18 +12,17 @@ import {
|
|
|
13
12
|
DEFAULT_COMPACT_TYPE,
|
|
14
13
|
DEFAULT_COMPACTION_KEEP_TOKENS,
|
|
15
14
|
CONTEXT_SHARE_RATIO,
|
|
15
|
+
COMPACT_TARGET_MIN_TOKENS,
|
|
16
|
+
COMPACT_SAFETY_PERCENT,
|
|
16
17
|
COMPACT_TYPE_RECALL_FASTTRACK,
|
|
17
18
|
} from '../compact.mjs';
|
|
18
19
|
import { positiveTokenInt, envFlag, envTokenInt } from './env.mjs';
|
|
19
20
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
20
21
|
|
|
21
|
-
const COMPACT_SAFETY_PERCENT = 1.00;
|
|
22
22
|
// Unified context-share rule (compact/constants.mjs CONTEXT_SHARE_RATIO): the
|
|
23
23
|
// post-compaction target is 10% of the boundary/context window — the same 10%
|
|
24
24
|
// the recall-fasttrack injection cap uses (loop.mjs recallTokenCap). One
|
|
25
25
|
// number governs every "share of model context" budget.
|
|
26
|
-
const COMPACT_TARGET_RATIO = CONTEXT_SHARE_RATIO;
|
|
27
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
28
26
|
|
|
29
27
|
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
30
28
|
// Types are hard-locked (agent -> semantic, main/user -> recall-fasttrack).
|
|
@@ -55,9 +53,9 @@ function resolveCompactTargetRatio(cfg = {}) {
|
|
|
55
53
|
?? cfg.targetFraction
|
|
56
54
|
?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
57
55
|
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
58
|
-
??
|
|
56
|
+
?? CONTEXT_SHARE_RATIO;
|
|
59
57
|
const n = Number(raw);
|
|
60
|
-
if (!Number.isFinite(n) || n <= 0) return
|
|
58
|
+
if (!Number.isFinite(n) || n <= 0) return CONTEXT_SHARE_RATIO;
|
|
61
59
|
return n > 1 ? n / 100 : n;
|
|
62
60
|
}
|
|
63
61
|
function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
@@ -92,22 +90,16 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
92
90
|
: (explicitBoundary || contextWindow || autoLimit);
|
|
93
91
|
if (!boundaryTokens) return null;
|
|
94
92
|
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const explicitAutoCompactTokenLimit = autoTriggerTokens;
|
|
106
|
-
const bufferTokens = autoTriggerTokens
|
|
107
|
-
? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
|
|
108
|
-
: resolveCompactBufferTokens(compactBoundaryTokens, cfg);
|
|
109
|
-
const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
|
|
110
|
-
const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
|
|
93
|
+
// Shared session-compaction policy (context-utils): agent semantic keeps the
|
|
94
|
+
// default early-trigger buffer (90%); main/user compact on the boundary
|
|
95
|
+
// (100%); a truly-explicit sub-boundary limit wins. explicitAutoCompactTokenLimit
|
|
96
|
+
// is the sanitized (null when legacy full-window) value so telemetry never
|
|
97
|
+
// re-persists a boundary-collapsing limit.
|
|
98
|
+
const policy = resolveSessionCompactPolicy(sessionRef, compactBoundaryTokens);
|
|
99
|
+
const explicitAutoCompactTokenLimit = policy.autoCompactTokenLimit;
|
|
100
|
+
const bufferTokens = policy.bufferTokens;
|
|
101
|
+
const bufferRatio = policy.bufferRatio;
|
|
102
|
+
const triggerTokens = policy.triggerTokens;
|
|
111
103
|
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
112
104
|
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
113
105
|
|| 0;
|
|
@@ -36,3 +36,31 @@ export function agentContextOverflowError({ stage, sessionId, sessionRef, model,
|
|
|
36
36
|
messageTokensEst,
|
|
37
37
|
}, cause);
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// Distinct from AgentContextOverflowError: the compaction pipeline itself
|
|
41
|
+
// failed (semantic-summary error, dead memory runtime, recall-fasttrack bail,
|
|
42
|
+
// etc.). This is NOT "latest turn cannot fit the context budget" — masking it
|
|
43
|
+
// as AGENT_CONTEXT_OVERFLOW hides the real failure and misroutes downstream
|
|
44
|
+
// overflow handling. Genuine provider send overflow keeps AgentContextOverflowError.
|
|
45
|
+
export class AgentCompactFailedError extends Error {
|
|
46
|
+
constructor({ stage, sessionId, provider, model }, cause) {
|
|
47
|
+
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
48
|
+
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
49
|
+
super(`agent compact failed (${target}, stage=${stage || 'compact'})${causeMsg}`);
|
|
50
|
+
this.name = 'AgentCompactFailedError';
|
|
51
|
+
this.code = 'AGENT_COMPACT_FAILED';
|
|
52
|
+
this.sessionId = sessionId || null;
|
|
53
|
+
this.provider = provider || null;
|
|
54
|
+
this.model = model || null;
|
|
55
|
+
if (cause) this.cause = cause;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function agentCompactFailedError({ stage, sessionId, sessionRef, model }, cause) {
|
|
60
|
+
return new AgentCompactFailedError({
|
|
61
|
+
stage,
|
|
62
|
+
sessionId,
|
|
63
|
+
provider: sessionRef?.provider || null,
|
|
64
|
+
model: sessionRef?.model || model || null,
|
|
65
|
+
}, cause);
|
|
66
|
+
}
|
|
@@ -128,6 +128,11 @@ export async function runRecallFastTrackCompact({ sessionRef, messages, compactB
|
|
|
128
128
|
messages,
|
|
129
129
|
cwd: sessionRef?.cwd,
|
|
130
130
|
limit: hydrateLimit,
|
|
131
|
+
// Pre-send fast-track compaction: these rows are about to be
|
|
132
|
+
// summarized away, so skip the bounded synchronous embedding-flush
|
|
133
|
+
// wait — kick the flush fire-and-forget. Mirrors the manual/auto-clear
|
|
134
|
+
// runner (manager/compaction-runner.mjs) embedWait:false policy.
|
|
135
|
+
embedWait: false,
|
|
131
136
|
}, callerCtx);
|
|
132
137
|
} catch (err) {
|
|
133
138
|
ingestFailed = true;
|
|
@@ -17,6 +17,31 @@ export function _isMutationTool(name) {
|
|
|
17
17
|
const n = _stripMcpPrefix(name);
|
|
18
18
|
return n === 'apply_patch';
|
|
19
19
|
}
|
|
20
|
+
// Side-effect-free read-only tools that stay parallel even after an earlier
|
|
21
|
+
// ordered mutation failed in the same batch. Everything NOT in this set is
|
|
22
|
+
// treated as ordered-gate-skippable (see _isOrderedGateSkippable): apply_patch,
|
|
23
|
+
// shell/bash_session, write/edit-style tools, and any (non-mixdog) MCP tool
|
|
24
|
+
// whose effects are unknown. Kept separate from _isMutationTool, which stays
|
|
25
|
+
// apply_patch-only for epoch-mutation counting and eager-dispatch gating.
|
|
26
|
+
export const ORDERED_GATE_SAFE_READONLY_TOOLS = new Set([
|
|
27
|
+
'read',
|
|
28
|
+
'find',
|
|
29
|
+
'glob',
|
|
30
|
+
'list',
|
|
31
|
+
'grep',
|
|
32
|
+
'code_graph',
|
|
33
|
+
'explore',
|
|
34
|
+
'search',
|
|
35
|
+
'web_fetch',
|
|
36
|
+
]);
|
|
37
|
+
// True when a LATER call in a batch must be skipped because an earlier ordered
|
|
38
|
+
// mutation (apply_patch) already failed. Conservative: only the known
|
|
39
|
+
// side-effect-free read-only tools above keep running; shell, write tools, and
|
|
40
|
+
// unknown MCP tools are skipped so they cannot act on unchanged/partially
|
|
41
|
+
// changed state.
|
|
42
|
+
export function _isOrderedGateSkippable(name) {
|
|
43
|
+
return !ORDERED_GATE_SAFE_READONLY_TOOLS.has(_stripMcpPrefix(name));
|
|
44
|
+
}
|
|
20
45
|
export const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
21
46
|
'code_graph',
|
|
22
47
|
'grep',
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// Context-window sizing, compaction target math, and session context-meta
|
|
2
2
|
// resolution. Extracted verbatim from manager.mjs (behavior-preserving).
|
|
3
3
|
import { getModelMetadataSync } from '../../providers/model-catalog.mjs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { resolveSessionCompactPolicy } from '../context-utils.mjs';
|
|
5
|
+
import {
|
|
6
|
+
COMPACT_TYPE_SEMANTIC,
|
|
7
|
+
COMPACT_TYPE_RECALL_FASTTRACK,
|
|
8
|
+
CONTEXT_SHARE_RATIO,
|
|
9
|
+
DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT,
|
|
10
|
+
COMPACT_TARGET_MIN_TOKENS,
|
|
11
|
+
} from '../compact.mjs';
|
|
6
12
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
7
13
|
|
|
8
14
|
// Known context windows for the current-generation models this plugin
|
|
@@ -89,14 +95,12 @@ export function preserveBufferConfigFields(cfg = {}) {
|
|
|
89
95
|
}
|
|
90
96
|
return out;
|
|
91
97
|
}
|
|
92
|
-
const COMPACT_TARGET_RATIO = CONTEXT_SHARE_RATIO;
|
|
93
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
94
98
|
function compactTargetRatio() {
|
|
95
99
|
const raw = process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
96
100
|
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
97
|
-
??
|
|
101
|
+
?? CONTEXT_SHARE_RATIO;
|
|
98
102
|
const n = Number(raw);
|
|
99
|
-
if (!Number.isFinite(n) || n <= 0) return
|
|
103
|
+
if (!Number.isFinite(n) || n <= 0) return CONTEXT_SHARE_RATIO;
|
|
100
104
|
return n > 1 ? n / 100 : n;
|
|
101
105
|
}
|
|
102
106
|
function compactTargetTokensForBoundary(boundaryTokens) {
|
|
@@ -115,7 +119,8 @@ function defaultEffectiveContextWindowPercent(provider) {
|
|
|
115
119
|
// Gateway/statusline route metadata reserves a universal 10% headroom from
|
|
116
120
|
// the raw catalog window. Keep session compaction on the same effective
|
|
117
121
|
// capacity so /context, the TUI statusline, and gateway telemetry agree.
|
|
118
|
-
|
|
122
|
+
void provider;
|
|
123
|
+
return DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT;
|
|
119
124
|
}
|
|
120
125
|
const PROVIDER_SYNTHETIC_CONTEXT_DEFAULT = 1_000_000;
|
|
121
126
|
function providerRawContextWindow(info, catalogInfo) {
|
|
@@ -201,7 +206,10 @@ export function resolveSessionContextMeta(provider, model, seed = {}) {
|
|
|
201
206
|
};
|
|
202
207
|
}
|
|
203
208
|
export function compactTriggerForSession(session, boundaryTokens) {
|
|
204
|
-
|
|
209
|
+
// Delegates to the shared session-compaction policy (context-utils):
|
|
210
|
+
// agent semantic -> 90% (default buffer), main/user -> 100% (boundary),
|
|
211
|
+
// truly-explicit sub-boundary limit wins.
|
|
212
|
+
return resolveSessionCompactPolicy(session, boundaryTokens).triggerTokens;
|
|
205
213
|
}
|
|
206
214
|
export function compactTargetBudget(boundaryTokens, reserveTokens, _sourceTokens = null, _ratio = null) {
|
|
207
215
|
const boundary = positiveContextWindow(boundaryTokens);
|
|
@@ -26,6 +26,8 @@ import { estimateMessagesTokensSafe } from './loop/compact-debug.mjs';
|
|
|
26
26
|
import { messagesArrayChanged } from './loop/tool-helpers.mjs';
|
|
27
27
|
import { normalizeUsage, addUsage } from './loop/usage.mjs';
|
|
28
28
|
import { agentContextOverflowError } from './loop/context-overflow.mjs';
|
|
29
|
+
import { agentCompactFailedError } from './loop/context-overflow.mjs';
|
|
30
|
+
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
29
31
|
import { traceAgentCompact, messagePrefixHash } from '../agent-trace.mjs';
|
|
30
32
|
import { bumpUsageMetricsEpoch } from './manager.mjs';
|
|
31
33
|
|
|
@@ -252,6 +254,31 @@ export async function runPreSendCompactPass(state) {
|
|
|
252
254
|
);
|
|
253
255
|
} catch { /* best-effort */ }
|
|
254
256
|
} else {
|
|
257
|
+
// A genuine cancellation/abort surfaced from the compact
|
|
258
|
+
// pipeline is NOT a context overflow. The recall-fasttrack
|
|
259
|
+
// pipeline (loop/recall-fasttrack.mjs) deliberately rethrows
|
|
260
|
+
// the original abort error unchanged so the session records a
|
|
261
|
+
// clean cancellation — and the manual/auto-clear runner
|
|
262
|
+
// (manager/compaction-runner.mjs) likewise never fabricates an
|
|
263
|
+
// AGENT_CONTEXT_OVERFLOW for an aborted compact. Mirror that
|
|
264
|
+
// here: preserve the real error (code/name/cause intact)
|
|
265
|
+
// instead of masking it as overflow. Detection is narrow on
|
|
266
|
+
// purpose — signal.aborted or a true AbortError — so the
|
|
267
|
+
// recall pipeline's SYNTHETIC "…aborted: memory … ; head
|
|
268
|
+
// preserved" failure (a real compact failure, message text
|
|
269
|
+
// aside) still escalates to overflow below.
|
|
270
|
+
if (signal?.aborted === true
|
|
271
|
+
|| compactErr?.name === 'AbortError'
|
|
272
|
+
|| compactErr?.code === 'ABORT_ERR'
|
|
273
|
+
|| compactErr?.code === 'ABORT') {
|
|
274
|
+
try {
|
|
275
|
+
process.stderr.write(
|
|
276
|
+
`[loop] pre-send compact cancelled (sess=${sessionId || 'unknown'}): ` +
|
|
277
|
+
`${compactErr?.message || compactErr}\n`,
|
|
278
|
+
);
|
|
279
|
+
} catch { /* best-effort */ }
|
|
280
|
+
throw compactErr;
|
|
281
|
+
}
|
|
255
282
|
const compactFailMsg = compactErr && compactErr.message ? compactErr.message : String(compactErr);
|
|
256
283
|
const semanticFailMsg = semanticCompactError?.message || null;
|
|
257
284
|
const recallFailMsg = recallFastTrackError?.message || null;
|
|
@@ -324,14 +351,33 @@ export async function runPreSendCompactPass(state) {
|
|
|
324
351
|
durationMs: Date.now() - compactStartedAt,
|
|
325
352
|
error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
|
|
326
353
|
});
|
|
327
|
-
|
|
354
|
+
// Only a GENUINE provider context-overflow surfaced from the
|
|
355
|
+
// compact pipeline (e.g. the semantic-summary send itself
|
|
356
|
+
// overflowed the model window) deserves AGENT_CONTEXT_OVERFLOW.
|
|
357
|
+
// Every other compact-stage failure (dead memory runtime,
|
|
358
|
+
// recall-fasttrack bail, semantic summary error) is a compact
|
|
359
|
+
// failure, not "latest turn cannot fit" — mislabeling it as
|
|
360
|
+
// overflow hides the real cause and misroutes downstream
|
|
361
|
+
// overflow handling. Surface an explicit compact-failed error.
|
|
362
|
+
const genuineOverflow = compactErr?.code === 'AGENT_CONTEXT_OVERFLOW'
|
|
363
|
+
|| compactErr?.name === 'AgentContextOverflowError'
|
|
364
|
+
|| isContextOverflowError(compactErr);
|
|
365
|
+
if (genuineOverflow) {
|
|
366
|
+
throw agentContextOverflowError({
|
|
367
|
+
stage: 'pre_send',
|
|
368
|
+
sessionId,
|
|
369
|
+
sessionRef,
|
|
370
|
+
model,
|
|
371
|
+
budgetTokens: compactBudgetTokens,
|
|
372
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
373
|
+
messageTokensEst,
|
|
374
|
+
}, compactErr);
|
|
375
|
+
}
|
|
376
|
+
throw agentCompactFailedError({
|
|
328
377
|
stage: 'pre_send',
|
|
329
378
|
sessionId,
|
|
330
379
|
sessionRef,
|
|
331
380
|
model,
|
|
332
|
-
budgetTokens: compactBudgetTokens,
|
|
333
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
334
|
-
messageTokensEst,
|
|
335
381
|
}, compactErr);
|
|
336
382
|
}
|
|
337
383
|
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
25
25
|
import {
|
|
26
26
|
_stripMcpPrefix, _isReadTool, _isMutationTool, _isScopedCacheableTool,
|
|
27
|
-
_isShellTool, _intraTurnSig,
|
|
27
|
+
_isShellTool, _isOrderedGateSkippable, _intraTurnSig,
|
|
28
28
|
} from './loop/tool-classify.mjs';
|
|
29
29
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
30
30
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
@@ -80,6 +80,17 @@ export async function processToolBatch(ctx) {
|
|
|
80
80
|
// between two tool results of the same multi-tool turn (which would put a
|
|
81
81
|
// user message between tool(A) and tool(B) and break provider pairing).
|
|
82
82
|
const _batchNewMessages = [];
|
|
83
|
+
// Ordered-mutation batch gate. apply_patch is a mutation tool (never
|
|
84
|
+
// eager-dispatchable), so multiple apply_patch calls in ONE assistant
|
|
85
|
+
// turn already execute serially in call-index order via this loop.
|
|
86
|
+
// This flag records the FIRST ordered mutation whose execution failed
|
|
87
|
+
// in this batch; every LATER apply_patch in the same batch is then
|
|
88
|
+
// skipped (not executed) because its edits may depend on the failed
|
|
89
|
+
// one and applying them against unchanged/partially-changed files
|
|
90
|
+
// risks corrupt or misplaced writes. Only apply_patch is gated —
|
|
91
|
+
// non-mutation tools (reads/grep/shell/...) keep their normal
|
|
92
|
+
// parallelism and behavior. Reset per batch (per assistant turn).
|
|
93
|
+
let _orderedMutationFailed = null;
|
|
83
94
|
for (let callIndex = 0; callIndex < calls.length; callIndex += 1) {
|
|
84
95
|
const call = calls[callIndex];
|
|
85
96
|
if (isBuiltinTool(call.name)) {
|
|
@@ -151,6 +162,22 @@ export async function processToolBatch(ctx) {
|
|
|
151
162
|
continue;
|
|
152
163
|
}
|
|
153
164
|
}
|
|
165
|
+
// Ordered-mutation skip: an earlier apply_patch in THIS batch failed,
|
|
166
|
+
// so this later apply_patch is skipped rather than executed. Restore
|
|
167
|
+
// its full patch body first (this call never ran) so the model can
|
|
168
|
+
// re-issue it cleanly in a new turn instead of copying back a
|
|
169
|
+
// `[mixdog compacted …]` placeholder. Emits a matching is_error
|
|
170
|
+
// tool_result so the assistant tool_use is not orphaned.
|
|
171
|
+
if (_orderedMutationFailed && _isOrderedGateSkippable(call.name)) {
|
|
172
|
+
if (call?.id) restoreToolCallBodyForId(assistantTurnMsg, calls, call.id);
|
|
173
|
+
pushToolResultMessage({
|
|
174
|
+
role: 'tool',
|
|
175
|
+
content: `Error: [ordered-mutation-skip] an earlier apply_patch in this same tool batch (call index ${_orderedMutationFailed.index + 1}) failed; ordered mutations in one batch are all-or-nothing after a failure, so this later \`${call.name}\` was NOT executed — it may depend on the failed mutation and running it now against unchanged/partially-changed state could corrupt files. Re-issue it in a new turn after resolving the earlier failure.`,
|
|
176
|
+
toolCallId: call.id,
|
|
177
|
+
toolKind: 'error',
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
154
181
|
if (sessionId) markSessionToolCall(sessionId, call.name, resolveToolSelfDeadlineMs(call.name, call.arguments));
|
|
155
182
|
let result;
|
|
156
183
|
let toolStartedAt;
|
|
@@ -416,9 +443,34 @@ export async function processToolBatch(ctx) {
|
|
|
416
443
|
// This block runs unconditionally (not gated on _executeOk or _resultKind).
|
|
417
444
|
if (sessionId && (!_executeOk || _resultKind === 'error') && _stripMcpPrefix(call.name) === 'apply_patch') {
|
|
418
445
|
clearReadDedupSession(sessionId);
|
|
446
|
+
// Scoped caches (grep/glob/list/code_graph) are refreshed only in
|
|
447
|
+
// the success-gated block above, so a FAILED/errored patch would
|
|
448
|
+
// otherwise leave later non-mutation tools in this batch reading
|
|
449
|
+
// stale scoped-cache entries for the (possibly partially-written)
|
|
450
|
+
// files. Invalidate targeted paths when the diff parses, else full
|
|
451
|
+
// wipe — file state is unknown after an error exit.
|
|
452
|
+
const _failBaseArg = call.arguments?.base_path;
|
|
453
|
+
const _failBase = (typeof _failBaseArg === 'string' && _failBaseArg.length > 0)
|
|
454
|
+
? (isAbsolute(_failBaseArg) ? _failBaseArg : resolvePath(cwd || process.cwd(), _failBaseArg))
|
|
455
|
+
: (cwd || process.cwd());
|
|
456
|
+
const _failTouched = extractTouchedPathsFromPatch(call.arguments?.patch);
|
|
457
|
+
if (_failTouched.length > 0) {
|
|
458
|
+
clearScopedToolsForSessionPaths(sessionId, _failTouched, _failBase);
|
|
459
|
+
for (const _p of _failTouched) invalidatePrefetchCache(_p, _failBase);
|
|
460
|
+
} else {
|
|
461
|
+
clearScopedToolsForSession(sessionId);
|
|
462
|
+
}
|
|
419
463
|
}
|
|
420
464
|
if (_isMutationTool(call.name)) {
|
|
421
465
|
epoch.mutation += 1;
|
|
466
|
+
// Record the first failed ordered mutation in this batch so any
|
|
467
|
+
// LATER apply_patch is skipped by the gate at the top of the
|
|
468
|
+
// loop. Keyed on exec outcome (not post-processing): a mutation
|
|
469
|
+
// whose write succeeded but post-processing threw still landed
|
|
470
|
+
// on disk, so it must NOT block subsequent ordered patches.
|
|
471
|
+
if ((!_executeOk || _resultKind === 'error') && !_orderedMutationFailed) {
|
|
472
|
+
_orderedMutationFailed = { index: callIndex, callId: call.id, name: call.name };
|
|
473
|
+
}
|
|
422
474
|
}
|
|
423
475
|
// Bash always clears scoped cache UNCONDITIONALLY — a mutating bash
|
|
424
476
|
// that throws or fails partway can still leave stale find_symbol / grep entries.
|
|
@@ -567,6 +567,16 @@ async function waitForGenericTask(taskId, { timeoutMs = 30_000, pollMs = 250, co
|
|
|
567
567
|
};
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
function renderTaskCancelSuccess(taskId, task) {
|
|
571
|
+
const surface = task?.surface || 'task';
|
|
572
|
+
const operation = task?.operation || 'run';
|
|
573
|
+
return [
|
|
574
|
+
'status: completed',
|
|
575
|
+
`task_id: ${taskId}`,
|
|
576
|
+
`cancelled: ${surface}/${operation}`,
|
|
577
|
+
].join('\n');
|
|
578
|
+
}
|
|
579
|
+
|
|
570
580
|
export async function executeTaskTool(args, options = {}) {
|
|
571
581
|
const action = typeof args.action === 'string' ? args.action.toLowerCase() : (args.task_id ? 'wait' : 'list');
|
|
572
582
|
if (action === 'list') return renderBackgroundTaskList({ context: options });
|
|
@@ -597,10 +607,10 @@ export async function executeTaskTool(args, options = {}) {
|
|
|
597
607
|
cancelBackgroundShellJobWatch(taskId);
|
|
598
608
|
clearShellJobNotifyCtx(taskId);
|
|
599
609
|
cancelBackgroundTask(taskId, 'cancelled by task control');
|
|
600
|
-
return job ?
|
|
610
|
+
return job ? renderTaskCancelSuccess(taskId, getBackgroundTask(taskId, { context: options }) || task) : buildJobNotFoundMessage(taskId);
|
|
601
611
|
}
|
|
602
612
|
cancelBackgroundTask(taskId, 'cancelled by task control');
|
|
603
|
-
return
|
|
613
|
+
return renderTaskCancelSuccess(taskId, getBackgroundTask(taskId, { context: options }) || task);
|
|
604
614
|
}
|
|
605
615
|
|
|
606
616
|
if (action !== 'wait') {
|