mixdog 0.9.36 → 0.9.37
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 +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +36 -6
- package/scripts/explore-prompt-policy-test.mjs +27 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +15 -7
- package/src/rules/lead/01-general.md +4 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +13 -2
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +467 -151
- package/src/tui/engine/agent-job-feed.mjs +26 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +78 -28
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +232 -156
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -179,22 +179,24 @@ function _bumpCounter(sessionId, field) {
|
|
|
179
179
|
* null on miss. On hit returns the full entry
|
|
180
180
|
* { content, firstToolUseId, ts }.
|
|
181
181
|
*/
|
|
182
|
-
export function tryScopedToolCached({ sessionId, toolName, args, cwd }) {
|
|
182
|
+
export function tryScopedToolCached({ sessionId, toolName, args, cwd, countStats = true, touch = true } = {}) {
|
|
183
183
|
if (!sessionId || !toolName) return null;
|
|
184
184
|
const map = _scopedBySession.get(sessionId);
|
|
185
185
|
if (!map) {
|
|
186
|
-
_bumpCounter(sessionId, 'misses');
|
|
186
|
+
if (countStats) _bumpCounter(sessionId, 'misses');
|
|
187
187
|
return null;
|
|
188
188
|
}
|
|
189
189
|
const key = _scopedKey(toolName, args, cwd);
|
|
190
190
|
const entry = map.get(key);
|
|
191
191
|
if (!entry) {
|
|
192
|
-
_bumpCounter(sessionId, 'misses');
|
|
192
|
+
if (countStats) _bumpCounter(sessionId, 'misses');
|
|
193
193
|
return null;
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
if (touch) {
|
|
196
|
+
map.delete(key);
|
|
197
|
+
map.set(key, entry);
|
|
198
|
+
}
|
|
199
|
+
if (countStats) _bumpCounter(sessionId, 'hits');
|
|
198
200
|
return { content: entry.content, firstToolUseId: entry.firstToolUseId || null, ts: entry.ts };
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// identical to the inline closures it replaced.
|
|
7
7
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
8
8
|
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
9
|
-
import { _intraTurnSig } from './loop/tool-classify.mjs';
|
|
9
|
+
import { _intraTurnSig, _isReadTool, _isScopedCacheableTool, _stripMcpPrefix } from './loop/tool-classify.mjs';
|
|
10
|
+
import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
|
|
10
11
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
11
12
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
12
13
|
import { crossTurnSignature } from './loop/completion-guards.mjs';
|
|
@@ -63,6 +64,23 @@ export function createEagerDispatcher({
|
|
|
63
64
|
const _prior = crossTurnCalls.get(_ctSig);
|
|
64
65
|
if (_prior && _prior.firstIteration < getIterations()) return null;
|
|
65
66
|
}
|
|
67
|
+
// Cache short-circuit (mirrors the serial-body lookup at
|
|
68
|
+
// tool-batch.mjs). If this read / scoped-cacheable call would be
|
|
69
|
+
// served from the session cache in the serial for-body, do NOT
|
|
70
|
+
// execute it eagerly — the serial path returns the cached body
|
|
71
|
+
// (read cache is stat-validated; scoped cache is dep-root evicted).
|
|
72
|
+
// Returning null here skips redundant IO under concurrent agents
|
|
73
|
+
// and, combined with the non-barrier `continue` in startEagerRun,
|
|
74
|
+
// never blocks a later independent eager read behind a cache stub.
|
|
75
|
+
// If the entry is invalidated before the serial body re-checks,
|
|
76
|
+
// that call simply executes serially — correctness is preserved.
|
|
77
|
+
if (sessionId) {
|
|
78
|
+
if (_isReadTool(call.name)) {
|
|
79
|
+
if (tryReadCached({ sessionId, args: call.arguments, cwd }) !== null) return null;
|
|
80
|
+
} else if (_isScopedCacheableTool(call.name)) {
|
|
81
|
+
if (tryScopedToolCached({ sessionId, toolName: _stripMcpPrefix(call.name), args: call.arguments, cwd, countStats: false, touch: false }) !== null) return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
66
84
|
const toolKind = getToolKind(call.name);
|
|
67
85
|
// Shared pre-dispatch deny: identical predicate runs in the
|
|
68
86
|
// serial path below. If any role/permission guard would reject
|
|
@@ -137,7 +155,15 @@ export function createEagerDispatcher({
|
|
|
137
155
|
const call = calls[j];
|
|
138
156
|
if (!call?.id || !isEagerDispatchable(call.name, tools)) break;
|
|
139
157
|
if (dupSet && dupSet.has(call.id)) continue;
|
|
140
|
-
|
|
158
|
+
// A null return here is NOT a state barrier: the loop above
|
|
159
|
+
// already breaks at the first non-eager (mutation/bash/unknown)
|
|
160
|
+
// tool, so every call reached here is read-only. A null means a
|
|
161
|
+
// non-barrier stub — intra-turn in-flight dup, repeat-failure /
|
|
162
|
+
// cross-turn dedup, pre-dispatch-deny, invalid-args, or a cache
|
|
163
|
+
// short-circuit. `continue` (not `break`) so a stub in the
|
|
164
|
+
// middle of a contiguous eager run does not stop LATER
|
|
165
|
+
// independent eager reads from starting early.
|
|
166
|
+
if (!startEagerTool(call) && !pending.has(call.id)) continue;
|
|
141
167
|
}
|
|
142
168
|
};
|
|
143
169
|
let _streamEagerBlocked = false;
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// loop.mjs. These drive cross-turn read dedup, scoped caching, shell routing,
|
|
3
3
|
// and duplicate-call detection. Strips the MCP prefix so direct calls and
|
|
4
4
|
// MCP-wrapped calls share the same cache.
|
|
5
|
-
import { createHash } from 'crypto';
|
|
6
|
-
|
|
7
5
|
export const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
|
|
8
6
|
|
|
9
7
|
export function _stripMcpPrefix(name) {
|
|
@@ -73,5 +71,5 @@ export function _canonicalArgs(args) {
|
|
|
73
71
|
} catch { return String(args); }
|
|
74
72
|
}
|
|
75
73
|
export function _intraTurnSig(name, args) {
|
|
76
|
-
return
|
|
74
|
+
return `${name}:${_canonicalArgs(args)}`;
|
|
77
75
|
}
|
|
@@ -16,10 +16,23 @@ import { isAgentOwner } from '../../agent-owner.mjs';
|
|
|
16
16
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
17
17
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
18
18
|
// stream. Writes, bash, MCP and skills stay serial after send() returns.
|
|
19
|
+
// Memoized: the read-only name Set is built once per distinct `tools` array
|
|
20
|
+
// (keyed by identity via a module-level WeakMap) so repeated per-call lookups
|
|
21
|
+
// are O(1) instead of O(N) tools.find scans.
|
|
22
|
+
const _eagerNameSetByTools = new WeakMap();
|
|
19
23
|
export function isEagerDispatchable(name, tools) {
|
|
20
24
|
if (!Array.isArray(tools)) return false;
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
let set = _eagerNameSetByTools.get(tools);
|
|
26
|
+
if (set === undefined) {
|
|
27
|
+
set = new Set();
|
|
28
|
+
for (const t of tools) {
|
|
29
|
+
if (t?.annotations?.readOnlyHint === true && typeof t.name === 'string') {
|
|
30
|
+
set.add(t.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
_eagerNameSetByTools.set(tools, set);
|
|
34
|
+
}
|
|
35
|
+
return set.has(name);
|
|
23
36
|
}
|
|
24
37
|
export function messagesArrayChanged(before, after) {
|
|
25
38
|
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
@@ -340,25 +340,24 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
340
340
|
onToolResult: typeof askOpts?.onToolResult === 'function' ? askOpts.onToolResult : undefined,
|
|
341
341
|
onToolApproval: typeof askOpts?.onToolApproval === 'function' ? askOpts.onToolApproval : undefined,
|
|
342
342
|
onCompactEvent: typeof askOpts?.onCompactEvent === 'function' ? askOpts.onCompactEvent : undefined,
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
// whole task to finish. The post-turn _pendingTail drain
|
|
350
|
-
// below still handles "followUp" input that lands after the
|
|
351
|
-
// agent would otherwise stop. Same queue, two drain points.
|
|
352
|
-
drainSteering: (sid) => {
|
|
343
|
+
// Claude Code parity: mid-chain queued prompt/notification
|
|
344
|
+
// drain is owned by agentLoop at provider-continuation
|
|
345
|
+
// boundaries (after a tool batch, before the next send).
|
|
346
|
+
// The post-loop _pendingTail drain below still handles
|
|
347
|
+
// items that arrive after the model would otherwise stop.
|
|
348
|
+
drainSteering: (sid, drainOptions = {}) => {
|
|
353
349
|
const out = [];
|
|
354
350
|
if (typeof askOpts?.drainSteering === 'function') {
|
|
355
351
|
try {
|
|
356
|
-
const drained = askOpts.drainSteering(sid || sessionId);
|
|
352
|
+
const drained = askOpts.drainSteering(sid || sessionId, drainOptions);
|
|
357
353
|
if (Array.isArray(drained)) out.push(...drained);
|
|
358
354
|
} catch { /* best-effort steering drain */ }
|
|
359
355
|
}
|
|
360
|
-
|
|
361
|
-
|
|
356
|
+
// Do NOT drain manager/pending-messages here: those
|
|
357
|
+
// entries have no mode/priority/slash metadata, so
|
|
358
|
+
// draining them mid-chain would bypass Claude Code's
|
|
359
|
+
// queued_command filters. They are consumed by the
|
|
360
|
+
// post-loop _pendingTail drain below.
|
|
362
361
|
return out;
|
|
363
362
|
},
|
|
364
363
|
onSteerMessage: typeof askOpts?.onSteerMessage === 'function' ? askOpts.onSteerMessage : undefined,
|
|
@@ -574,6 +573,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
574
573
|
// owning background task in `running` and suppress its completion
|
|
575
574
|
// notification. Cap the wait; let a slow write finish in the
|
|
576
575
|
// background instead of holding askSession() open indefinitely.
|
|
576
|
+
let terminalSaveTimedOut = false;
|
|
577
577
|
{
|
|
578
578
|
const savePromise = saveSessionAsync(session, { expectedGeneration: askGeneration });
|
|
579
579
|
let saveTimer = null;
|
|
@@ -587,6 +587,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
587
587
|
saveTimeout,
|
|
588
588
|
]);
|
|
589
589
|
if (outcome === '__save_timeout__') {
|
|
590
|
+
terminalSaveTimedOut = true;
|
|
590
591
|
try { process.stderr.write(`[session] terminal save exceeded ${TERMINAL_SAVE_TIMEOUT_MS}ms; continuing best-effort (${sessionId})\n`); } catch {}
|
|
591
592
|
// Don't drop the write — let it settle in the background.
|
|
592
593
|
savePromise.catch((err) => {
|
|
@@ -674,10 +675,18 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
674
675
|
const _mergedTail = _mergePendingMessageEntries(_drained);
|
|
675
676
|
if (_mergedTail?.content) {
|
|
676
677
|
_pendingTail.push(_mergedTail.content);
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
678
|
+
if (terminalSaveTimedOut) {
|
|
679
|
+
// The disk snapshot may still be stale; carry the just-
|
|
680
|
+
// committed in-memory session into the follow-up turn so
|
|
681
|
+
// the queued tail sees the preceding assistant/tool context.
|
|
682
|
+
activeSession = session;
|
|
683
|
+
runtime.session = session;
|
|
684
|
+
} else {
|
|
685
|
+
const refreshed = loadSession(sessionId);
|
|
686
|
+
if (refreshed && refreshed.closed !== true) {
|
|
687
|
+
activeSession = refreshed;
|
|
688
|
+
runtime.session = refreshed;
|
|
689
|
+
}
|
|
681
690
|
}
|
|
682
691
|
continue;
|
|
683
692
|
}
|
|
@@ -73,13 +73,13 @@ function temporalPromptText(content) {
|
|
|
73
73
|
function promptNeedsDateReminder(content) {
|
|
74
74
|
const text = temporalPromptText(content);
|
|
75
75
|
if (!text) return false;
|
|
76
|
-
return /(
|
|
76
|
+
return /(?:\uC624\uB298|\uB0B4\uC77C|\uC5B4\uC81C|\uBAA8\uB808|\uADF8\uC800\uAED8|\uC694\uC998|\uCD5C\uADFC|\uBC29\uAE08|\uC544\uAE4C|\uD604\uC7AC\s*(?:\uB0A0\uC9DC|\uC2DC\uAC04|\uC2DC\uAC01)|\uC9C0\uAE08\s*(?:\uBA87\s*\uC2DC|\uC2DC\uAC04|\uB0A0\uC9DC|\uC694\uC77C)|\uBA87\s*\uC6D4\s*\uBA87\s*\uC77C|\uBA87\s*\uC2DC|\uBB34\uC2A8\s*\uC694\uC77C|\uC694\uC77C|\uB0A0\uC9DC|\uC774\uBC88\s*(?:\uC8FC|\uB2EC|\uC6D4|\uB144)|\uC9C0\uB09C\s*(?:\uC8FC|\uB2EC|\uC6D4|\uB144)|\uB2E4\uC74C\s*(?:\uC8FC|\uB2EC|\uC6D4|\uB144)|\uC62C\uD574|\uC791\uB144|\uB0B4\uB144|today|tomorrow|yesterday|recently|current\s+(?:date|time)|what\s+(?:date|time)|which\s+day|weekday|this\s+(?:week|month|year)|last\s+(?:week|month|year)|next\s+(?:week|month|year))/i.test(text);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function promptNeedsTimeReminder(content) {
|
|
80
80
|
const text = temporalPromptText(content);
|
|
81
81
|
if (!text) return false;
|
|
82
|
-
return /(
|
|
82
|
+
return /(?:\uD604\uC7AC\s*(?:\uC2DC\uAC04|\uC2DC\uAC01)|\uC9C0\uAE08\s*(?:\uBA87\s*\uC2DC|\uC2DC\uAC04)|\uBA87\s*\uC2DC|\uC2DC\uAC01|\uC2DC\uAC04|current\s+time|what\s+time|time\s+is\s+it)/i.test(text);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export function buildCurrentTimeBlock(content) {
|
|
@@ -34,6 +34,27 @@ import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent
|
|
|
34
34
|
import { mintSessionId } from './session-id.mjs';
|
|
35
35
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
36
36
|
|
|
37
|
+
function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
|
|
38
|
+
// Keep this in sync with createSession's provider-cache policy: only
|
|
39
|
+
// explicit-breakpoint providers get BP cache opts here; OpenAI/key-prefix
|
|
40
|
+
// providers use promptCacheKey and request-time strategy instead.
|
|
41
|
+
if (cacheCapabilityForProvider(providerName) !== 'explicit-breakpoint') return null;
|
|
42
|
+
try {
|
|
43
|
+
let autoClear = null;
|
|
44
|
+
if (!agent || agent === 'lead') {
|
|
45
|
+
const loadedConfig = loadConfig({ secrets: false });
|
|
46
|
+
const normalizedAutoClear = normalizeAutoClearConfig(loadedConfig?.autoClear);
|
|
47
|
+
autoClear = {
|
|
48
|
+
...normalizedAutoClear,
|
|
49
|
+
idleMs: resolveAutoClearIdleMs(loadedConfig, providerName),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return buildProviderCacheOpts(providerName, sessionId, agent, { autoClear });
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
// --- agent spawn (createSession) ---
|
|
38
59
|
// opts can pass either a `preset` object (from config.presets) or raw provider/model.
|
|
39
60
|
// Preset shape: { name, provider, model, effort?, fast?, tools? }
|
|
@@ -107,22 +128,7 @@ export function createSession(opts) {
|
|
|
107
128
|
// cacheRetention:'24h' shape) were never exercised by createSession
|
|
108
129
|
// before this change, and are left untouched to avoid altering live
|
|
109
130
|
// OpenAI/other-provider request shape as a side effect of this fix.
|
|
110
|
-
if (!providerCacheOpts
|
|
111
|
-
try {
|
|
112
|
-
let autoClear = null;
|
|
113
|
-
if (!opts.agent || opts.agent === 'lead') {
|
|
114
|
-
const loadedConfig = loadConfig({ secrets: false });
|
|
115
|
-
const normalizedAutoClear = normalizeAutoClearConfig(loadedConfig?.autoClear);
|
|
116
|
-
autoClear = {
|
|
117
|
-
...normalizedAutoClear,
|
|
118
|
-
idleMs: resolveAutoClearIdleMs(loadedConfig, providerName),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
providerCacheOpts = buildProviderCacheOpts(providerName, id, opts.agent, { autoClear });
|
|
122
|
-
} catch {
|
|
123
|
-
providerCacheOpts = null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
131
|
+
if (!providerCacheOpts) providerCacheOpts = buildSessionProviderCacheOpts(providerName, id, opts.agent);
|
|
126
132
|
const messages = [];
|
|
127
133
|
const ownerIsAgent = isAgentOwner(opts.owner);
|
|
128
134
|
const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
|
|
@@ -425,6 +431,8 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
425
431
|
|| (route.model && route.model !== previousModel);
|
|
426
432
|
if (routeChanged) {
|
|
427
433
|
const now = Date.now();
|
|
434
|
+
session.promptCacheKey = providerCacheKey(session.provider);
|
|
435
|
+
session.providerCacheOpts = buildSessionProviderCacheOpts(session.provider, session.id, session.agent) || null;
|
|
428
436
|
session.lastInputTokens = 0;
|
|
429
437
|
session.lastOutputTokens = 0;
|
|
430
438
|
session.lastCachedReadTokens = 0;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Matches documented tool-return error conventions:
|
|
10
10
|
* "Error: ..." — Node/MCP tool errors (grep, find_symbol, read, code_graph, etc.)
|
|
11
|
+
* "Error: [shell-tool-failed] ..." — shell tool/control-plane failure
|
|
12
|
+
* "Error: [shell-run-failed] ..." — shell command process failure
|
|
11
13
|
* "Error [code N]:" — structured builtin tool errors
|
|
12
14
|
* "[error ..." — bracketed error format
|
|
13
15
|
* "[exit code: ..." — bash non-zero exit
|
|
@@ -96,6 +96,9 @@ export async function processToolBatch(ctx) {
|
|
|
96
96
|
if (isBuiltinTool(call.name)) {
|
|
97
97
|
call.name = canonicalizeBuiltinToolName(call.name);
|
|
98
98
|
}
|
|
99
|
+
// Per-call cross-turn signature, computed at most once (lazily, only
|
|
100
|
+
// when the call is eager — both consumer sites are eager-gated).
|
|
101
|
+
let _ctSig = null;
|
|
99
102
|
if (_duplicateCallIds.has(call.id)) {
|
|
100
103
|
const _firstId = _dupFirstId.get(call.id);
|
|
101
104
|
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,7 +116,7 @@ export async function processToolBatch(ctx) {
|
|
|
113
116
|
// escalation tail once the session has emitted 5+ dedup stubs total.
|
|
114
117
|
// Never applies to write/bash/MCP/skill tools (not eager-dispatchable).
|
|
115
118
|
if (isEagerDispatchable(call.name, tools)) {
|
|
116
|
-
|
|
119
|
+
_ctSig = crossTurnSignature(call.name, call.arguments);
|
|
117
120
|
const _prior = crossTurnCalls.get(_ctSig);
|
|
118
121
|
if (_prior && _prior.firstIteration < iterations) {
|
|
119
122
|
_prior.count += 1;
|
|
@@ -576,7 +579,7 @@ export async function processToolBatch(ctx) {
|
|
|
576
579
|
if (_executeOk) {
|
|
577
580
|
const _isEager = isEagerDispatchable(call.name, tools);
|
|
578
581
|
if (_isEager) {
|
|
579
|
-
|
|
582
|
+
if (_ctSig === null) _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
580
583
|
if (!crossTurnCalls.has(_ctSig)) {
|
|
581
584
|
crossTurnCalls.set(_ctSig, { count: 1, firstIteration: iterations });
|
|
582
585
|
if (crossTurnCalls.size > crossTurnCap) {
|
|
@@ -219,6 +219,24 @@ export const PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
219
219
|
{ minMs: 10_000, maxMs: STALL_WARN_MS },
|
|
220
220
|
);
|
|
221
221
|
|
|
222
|
+
// WS-specific semantic idle window. Do NOT reuse
|
|
223
|
+
// PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS here: that shared SSE default was raised to
|
|
224
|
+
// ~285s to avoid false-aborting Anthropic effort-mode streams that legitimately
|
|
225
|
+
// produce no deltas while thinking. The WS transport is different: a silent
|
|
226
|
+
// OpenAI/xAI websocket turn can sit with only connection/metadata activity while
|
|
227
|
+
// the TUI lead-turn watchdog is also 300s. If the provider waits ~285-300s
|
|
228
|
+
// before surfacing the stall, the UI watchdog often wins and the user only sees
|
|
229
|
+
// "Turn timed out after 300s" instead of a provider retry/fallback.
|
|
230
|
+
//
|
|
231
|
+
// Keep WS hangs bounded well below the TUI watchdog while still allowing normal
|
|
232
|
+
// long reasoning streams that emit reasoning/text/tool deltas (those reset this
|
|
233
|
+
// timer). Env-overridable for experiments.
|
|
234
|
+
export const PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS = resolveTimeoutMs(
|
|
235
|
+
['MIXDOG_PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS', 'MIXDOG_PROVIDER_WS_OUTPUT_IDLE_TIMEOUT_MS'],
|
|
236
|
+
120_000,
|
|
237
|
+
{ minMs: 10_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
238
|
+
);
|
|
239
|
+
|
|
222
240
|
// First retry has a small floor (250ms) instead of 0ms: an immediate reissue on
|
|
223
241
|
// a transient 5xx burst lets parallel workers thundering-herd the backend in
|
|
224
242
|
// lockstep (jitter alone can't decluster a 0ms base). Subsequent steps keep the
|
|
@@ -168,10 +168,15 @@ function _prependDestructiveWarning(command, text) {
|
|
|
168
168
|
return `${warnings.map((w) => `⚠️ ${w}`).join('\n')}\n${text}`;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
export function formatShellToolFailure(message) {
|
|
172
|
+
const text = String(message ?? '').replace(/^Error:\s*/i, '').trim() || 'shell tool failed';
|
|
173
|
+
return `Error: [shell-tool-failed] ${text}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
export async function executeBashTool(args, workDir, options = {}) {
|
|
172
177
|
const requestedCwd = args.cwd ?? args.workdir;
|
|
173
178
|
const cwdResult = resolveOptionalCwd(requestedCwd, workDir);
|
|
174
|
-
if (cwdResult.error) return cwdResult.error;
|
|
179
|
+
if (cwdResult.error) return formatShellToolFailure(cwdResult.error);
|
|
175
180
|
// Session cwd carry-over (no live shell): when the model
|
|
176
181
|
// passes an explicit cwd it wins and updates the store on the next probe;
|
|
177
182
|
// otherwise reuse the last stored cwd for this session if it still exists.
|
|
@@ -199,7 +204,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
199
204
|
// persistent:true. Run the full sweep here so both paths share the
|
|
200
205
|
// same blocklist before dispatch.
|
|
201
206
|
const _policyBlock = checkExecPolicyMessage(_rawCmd);
|
|
202
|
-
if (_policyBlock) return _policyBlock;
|
|
207
|
+
if (_policyBlock) return formatShellToolFailure(_policyBlock);
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
// An empty-string session_id is NOT a persistent-session request: `typeof
|
|
@@ -208,7 +213,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
208
213
|
// error, which models then retry in a loop. Require a non-blank id.
|
|
209
214
|
if (args.persistent === true || (typeof args.session_id === 'string' && args.session_id.trim().length > 0)) {
|
|
210
215
|
if (process.platform === 'win32') {
|
|
211
|
-
return '
|
|
216
|
+
return formatShellToolFailure('persistent shell sessions are disabled on Windows native-shell mode; run one-shot PowerShell commands without persistent/session_id.');
|
|
212
217
|
}
|
|
213
218
|
const { executeBashSessionTool } = await import('../bash-session.mjs');
|
|
214
219
|
let persistAbort = null;
|
|
@@ -227,7 +232,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
let command = args.command;
|
|
230
|
-
if (!command) return '
|
|
235
|
+
if (!command) return formatShellToolFailure('command is required');
|
|
231
236
|
|
|
232
237
|
// Resolve the shell up front so shell-type-specific handling (PS-only wmic
|
|
233
238
|
// rewrite, PS UTF-8 prefix) can gate on it. kind 'default' is byte-identical
|
|
@@ -238,9 +243,9 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
238
243
|
const resolvedSpec = resolveShellFor(shellKind);
|
|
239
244
|
if (!resolvedSpec) {
|
|
240
245
|
if (shellKind === 'bash') {
|
|
241
|
-
return "
|
|
246
|
+
return formatShellToolFailure("Git Bash not found — install Git for Windows or omit shell:'bash'.");
|
|
242
247
|
}
|
|
243
|
-
return "
|
|
248
|
+
return formatShellToolFailure("pwsh (PowerShell) not found — install PowerShell or omit shell:'powershell'.");
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
// wmic→PowerShell rewrite is PowerShell-only; never mangle a command bound
|
|
@@ -251,7 +256,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
251
256
|
const wmicRewrite = resolvedSpec.shellType === 'powershell'
|
|
252
257
|
? maybeRewriteWmicProcessCommand(command)
|
|
253
258
|
: null;
|
|
254
|
-
if (wmicRewrite?.error) return
|
|
259
|
+
if (wmicRewrite?.error) return formatShellToolFailure(wmicRewrite.error);
|
|
255
260
|
if (wmicRewrite?.command) command = wmicRewrite.command;
|
|
256
261
|
|
|
257
262
|
// PowerShell hygiene preflight (Windows PS-only; POSIX no-op): losslessly
|
|
@@ -262,19 +267,19 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
262
267
|
shellType: resolvedSpec.shellType,
|
|
263
268
|
shellName: resolvedSpec.shell,
|
|
264
269
|
});
|
|
265
|
-
if (psHygiene.block) return psHygiene.block;
|
|
270
|
+
if (psHygiene.block) return formatShellToolFailure(psHygiene.block);
|
|
266
271
|
command = psHygiene.command;
|
|
267
272
|
|
|
268
273
|
const _execPolicyBlock = checkExecPolicyMessage(command);
|
|
269
274
|
if (_execPolicyBlock) {
|
|
270
|
-
return _execPolicyBlock;
|
|
275
|
+
return formatShellToolFailure(_execPolicyBlock);
|
|
271
276
|
}
|
|
272
277
|
|
|
273
278
|
let shellEffects;
|
|
274
279
|
try {
|
|
275
280
|
shellEffects = await analyzeShellCommandEffects(command, bashWorkDir);
|
|
276
281
|
} catch (err) {
|
|
277
|
-
return
|
|
282
|
+
return formatShellToolFailure(normalizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
278
283
|
}
|
|
279
284
|
// Keep foreground commands on a long tool-owned timeout. The MCP dispatch
|
|
280
285
|
// layer must not add a shorter fallback ceiling when timeout is omitted.
|
|
@@ -314,7 +319,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
314
319
|
: Math.min(timeoutMs, wmicRewrite?.timeoutMs || MAX_BASH_TIMEOUT_MS));
|
|
315
320
|
const mergeStderr = args.merge_stderr === true;
|
|
316
321
|
const longForegroundHint = foregroundLongCommandHint(command, timeout, { ...args, run_in_background: runInBackground });
|
|
317
|
-
if (longForegroundHint) return longForegroundHint;
|
|
322
|
+
if (longForegroundHint) return formatShellToolFailure(longForegroundHint);
|
|
318
323
|
// Auto-background threshold. Reference-CLI parity: sync commands run to
|
|
319
324
|
// their timeout without any default auto-promotion, so the default is 0
|
|
320
325
|
// (disabled) for ALL callers. It is an explicit opt-in only: set
|
|
@@ -347,7 +352,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
347
352
|
try {
|
|
348
353
|
wrappedCommand = await wrapCommandWithSnapshot(shell, command);
|
|
349
354
|
} catch (wrapErr) {
|
|
350
|
-
return `
|
|
355
|
+
return formatShellToolFailure(`shell snapshot wrapper failed — ${normalizeErrorMessage(wrapErr instanceof Error ? wrapErr.message : String(wrapErr))}`);
|
|
351
356
|
}
|
|
352
357
|
} else {
|
|
353
358
|
wrappedCommand = command;
|
|
@@ -367,7 +372,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
367
372
|
// claude.exe pid (server-main threads callerSession.clientHostPid).
|
|
368
373
|
clientHostPid: options?.clientHostPid,
|
|
369
374
|
});
|
|
370
|
-
if (job && job.error) return
|
|
375
|
+
if (job && job.error) return formatShellToolFailure(job.error);
|
|
371
376
|
let task;
|
|
372
377
|
try {
|
|
373
378
|
task = registerBackgroundTask({
|
|
@@ -395,7 +400,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
395
400
|
});
|
|
396
401
|
} catch (err) {
|
|
397
402
|
try { killShellJob(job.jobId); } catch { /* best effort cleanup */ }
|
|
398
|
-
return
|
|
403
|
+
return formatShellToolFailure(normalizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
399
404
|
}
|
|
400
405
|
// Wire a one-shot completion push so the dispatching session learns
|
|
401
406
|
// the background task finished (no polling tool is auto-driven). The
|
|
@@ -525,7 +530,9 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
525
530
|
: (result.killed ? 'SIGKILL' : (result.signal || null));
|
|
526
531
|
const exitCode = signal ? null : result.exitCode;
|
|
527
532
|
const benignExitOne = _isBenignSearchExitOne(command, exitCode, signal, stderr);
|
|
528
|
-
const
|
|
533
|
+
const shellToolFailed = result.failurePhase === 'tool' || !!result.outputCaptureError;
|
|
534
|
+
const shellRunFailed = !shellToolFailed && (!!signal || (exitCode !== 0 && exitCode !== null && !benignExitOne));
|
|
535
|
+
const isReallyErrored = shellToolFailed || shellRunFailed;
|
|
529
536
|
const _driftNote = '';
|
|
530
537
|
// Distinct timeout marker so callers see "killed by timeout after Nms"
|
|
531
538
|
// vs an external signal (e.g. user Ctrl-C, OOM kill). result.timedOut
|
|
@@ -538,11 +545,16 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
538
545
|
const timeoutHint = result.timedOut
|
|
539
546
|
? ` — command killed after ${timeout} ms; if it legitimately needs longer, retry with a larger timeout`
|
|
540
547
|
: '';
|
|
541
|
-
const
|
|
542
|
-
? `[
|
|
543
|
-
: (
|
|
544
|
-
? `[signal: ${signal}]`
|
|
545
|
-
: (
|
|
548
|
+
const statusDetail = shellToolFailed
|
|
549
|
+
? `[${result.outputCaptureError ? 'output capture failed' : (result.failureReason || 'tool failed')}]`
|
|
550
|
+
: (result.timedOut
|
|
551
|
+
? `[timeout: ${timeout}ms signal: ${signal || 'SIGTERM'}]${timeoutHint}`
|
|
552
|
+
: (signal
|
|
553
|
+
? `[signal: ${signal}]`
|
|
554
|
+
: (shellRunFailed ? `[exit code: ${exitCode}]` : '')));
|
|
555
|
+
const statusMarker = shellToolFailed
|
|
556
|
+
? `[shell-tool-failed] ${statusDetail}`
|
|
557
|
+
: (shellRunFailed ? `[shell-run-failed] ${statusDetail}` : '');
|
|
546
558
|
const errorPrefix = isReallyErrored ? 'Error: ' : '';
|
|
547
559
|
if (mergeStderr) {
|
|
548
560
|
// Post-exit concatenation. True chunk-level interleaving would
|
|
@@ -569,6 +581,9 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
569
581
|
const sizeKb = Math.round((result.stderrFileSize || 0) / 1024);
|
|
570
582
|
spillBlock += `\n[stderr: ${normalizeOutputPath(result.stderrPath)} (${sizeKb} KB)]`;
|
|
571
583
|
}
|
|
584
|
+
if (result.outputCaptureError) {
|
|
585
|
+
spillBlock += `\n[tool capture error: ${normalizeErrorMessage(result.outputCaptureError?.message || String(result.outputCaptureError))}]`;
|
|
586
|
+
}
|
|
572
587
|
const warningBlock = [
|
|
573
588
|
wmicRewrite?.note || '',
|
|
574
589
|
].filter(Boolean).join('\n');
|
|
@@ -31,7 +31,7 @@ function _shellMaxTimeoutMs() {
|
|
|
31
31
|
// the process lifetime, so this is evaluated once at module load.
|
|
32
32
|
const _shellSyntaxCheat =
|
|
33
33
|
process.platform === 'win32'
|
|
34
|
-
? ' PowerShell: grep→Select-String, tail→Get-Content -Tail, head→Get-Content -TotalCount, /c/→C:\\, &&
|
|
34
|
+
? ' PowerShell: grep→Select-String, tail→Get-Content -Tail, head→Get-Content -TotalCount, /c/→C:\\, if && is unsupported use ;, $PID is reserved.'
|
|
35
35
|
: '';
|
|
36
36
|
|
|
37
37
|
export const BUILTIN_TOOLS = [
|
|
@@ -109,7 +109,7 @@ export const BUILTIN_TOOLS = [
|
|
|
109
109
|
name: 'grep',
|
|
110
110
|
title: 'Mixdog Grep',
|
|
111
111
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
112
|
-
description: 'Exact text/regex in
|
|
112
|
+
description: 'Exact text/regex in verified scope. Unknown path/name → find first; no path "." + guessed src/**. Broad: files_with_matches/count; narrow: content_with_context.',
|
|
113
113
|
inputSchema: {
|
|
114
114
|
type: 'object',
|
|
115
115
|
properties: {
|
|
@@ -125,14 +125,14 @@ export const BUILTIN_TOOLS = [
|
|
|
125
125
|
{ type: 'string' },
|
|
126
126
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
127
127
|
],
|
|
128
|
-
description: 'Verified file
|
|
128
|
+
description: 'Verified file/dir only; unknown → find first.',
|
|
129
129
|
},
|
|
130
130
|
glob: {
|
|
131
131
|
anyOf: [
|
|
132
132
|
{ type: 'string' },
|
|
133
133
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
134
134
|
],
|
|
135
|
-
description: 'Glob filter
|
|
135
|
+
description: 'Glob filter; no guessed src/** under path ".".',
|
|
136
136
|
},
|
|
137
137
|
output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad: files_with_matches/count; narrow: content_with_context.' },
|
|
138
138
|
head_limit: { type: 'number', minimum: 0, description: 'Max results.' },
|
|
@@ -148,7 +148,7 @@ export const BUILTIN_TOOLS = [
|
|
|
148
148
|
name: 'glob',
|
|
149
149
|
title: 'Mixdog Glob',
|
|
150
150
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
151
|
-
description: '
|
|
151
|
+
description: 'Exact glob from verified roots. Unknown root/name → find first; no path "." + guessed src/**. Batch arrays.',
|
|
152
152
|
inputSchema: {
|
|
153
153
|
type: 'object',
|
|
154
154
|
properties: {
|
|
@@ -164,7 +164,7 @@ export const BUILTIN_TOOLS = [
|
|
|
164
164
|
{ type: 'string' },
|
|
165
165
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
166
166
|
],
|
|
167
|
-
description: '
|
|
167
|
+
description: 'Verified base dir(s); unknown → find first. Batch path[].',
|
|
168
168
|
},
|
|
169
169
|
head_limit: { type: 'number', description: 'Max entries.' },
|
|
170
170
|
offset: { type: 'number', description: 'Skip entries.' },
|
|
@@ -176,7 +176,7 @@ export const BUILTIN_TOOLS = [
|
|
|
176
176
|
name: 'find',
|
|
177
177
|
title: 'Mixdog Find Files',
|
|
178
178
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
179
|
-
description: '
|
|
179
|
+
description: 'Partial path/name lookup incl dot dirs; verify roots before grep/glob. Batch query[].',
|
|
180
180
|
inputSchema: {
|
|
181
181
|
type: 'object',
|
|
182
182
|
properties: {
|