mixdog 0.9.39 → 0.9.41
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 +2 -1
- package/scripts/agent-terminal-reap-test.mjs +6 -6
- package/scripts/anthropic-oauth-refresh-race-test.mjs +338 -0
- package/scripts/compact-pressure-test.mjs +128 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -8
- package/scripts/execution-resume-esc-integration-test.mjs +4 -2
- package/scripts/internal-comms-smoke.mjs +66 -25
- package/scripts/max-output-recovery-persist-test.mjs +81 -0
- package/scripts/max-output-recovery-test.mjs +222 -0
- package/scripts/provider-toolcall-test.mjs +32 -0
- package/scripts/steering-drain-buckets-test.mjs +140 -5
- package/scripts/tui-transcript-perf-test.mjs +279 -0
- package/src/agents/reviewer/AGENT.md +4 -0
- package/src/rules/lead/lead-brief.md +11 -3
- package/src/rules/lead/lead-tool.md +0 -2
- package/src/rules/shared/01-tool.md +4 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +20 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +7 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +144 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -2
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -25
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +54 -12
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +106 -8
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +22 -7
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +31 -4
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +45 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +49 -34
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -10
- package/src/runtime/shared/buffered-appender.mjs +13 -2
- package/src/session-runtime/config-helpers.mjs +6 -6
- package/src/session-runtime/lifecycle-api.mjs +4 -0
- package/src/session-runtime/runtime-core.mjs +1 -0
- package/src/session-runtime/session-turn-api.mjs +12 -0
- package/src/standalone/agent-tool.mjs +8 -1
- package/src/tui/App.jsx +2 -1
- package/src/tui/app/transcript-window.mjs +9 -10
- package/src/tui/app/use-transcript-window.mjs +38 -56
- package/src/tui/dist/index.mjs +354 -163
- package/src/tui/engine/agent-job-feed.mjs +76 -6
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine/tool-card-results.mjs +10 -5
- package/src/tui/engine/turn.mjs +96 -36
- package/src/tui/engine.mjs +136 -37
- package/src/tui/index.jsx +2 -2
- package/src/workflows/bench/WORKFLOW.md +51 -27
- package/src/workflows/default/WORKFLOW.md +29 -24
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { readdir, rmdir, unlink, writeFile } from 'fs/promises';
|
|
2
|
+
import { readdir, rmdir, stat, unlink, writeFile } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { getPluginData } from '../config.mjs';
|
|
5
5
|
import { normalizeOutputPath } from '../tools/builtin/path-utils.mjs';
|
|
@@ -11,6 +11,7 @@ const TOOL_RESULT_SHELL_THRESHOLD_CHARS = 30_000;
|
|
|
11
11
|
const TOOL_RESULT_SEARCH_THRESHOLD_CHARS = 50_000;
|
|
12
12
|
const TOOL_RESULT_GREP_THRESHOLD_CHARS = 20_000;
|
|
13
13
|
export const TOOL_RESULT_OFFLOAD_PREFIX = '[tool output offloaded:';
|
|
14
|
+
const OFFLOAD_PRUNE_MIN_AGE_MS = 10 * 60 * 1000;
|
|
14
15
|
|
|
15
16
|
// Per-tool persistence limits mirror reference per-tool maxResultSizeChars
|
|
16
17
|
// rather than a single global value: Grep persists at 20k (CC GrepTool), Glob
|
|
@@ -147,6 +148,47 @@ export async function clearOffloadSession(sessionId) {
|
|
|
147
148
|
} catch { /* best-effort */ }
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// Remove sidecars that no longer occur in the live transcript. A serialized
|
|
152
|
+
// path match is conservative: if messages cannot be serialized, or a path is
|
|
153
|
+
// mentioned anywhere in a message, retain the file rather than risk deleting
|
|
154
|
+
// one that can still be read by the model.
|
|
155
|
+
export async function pruneOffloadSession(sessionId, getMessages) {
|
|
156
|
+
if (!sessionId || typeof getMessages !== 'function') return;
|
|
157
|
+
const dir = join(getPluginData(), 'tool-results', safeSessionSegment(sessionId));
|
|
158
|
+
if (!existsSync(dir)) return;
|
|
159
|
+
let candidates;
|
|
160
|
+
try {
|
|
161
|
+
const entries = await readdir(dir);
|
|
162
|
+
candidates = (await Promise.all(entries
|
|
163
|
+
.filter((name) => name.endsWith('.txt'))
|
|
164
|
+
.map(async (name) => {
|
|
165
|
+
const filePath = join(dir, name);
|
|
166
|
+
try {
|
|
167
|
+
const fileStat = await stat(filePath);
|
|
168
|
+
if (Date.now() - fileStat.mtimeMs < OFFLOAD_PRUNE_MIN_AGE_MS) return null;
|
|
169
|
+
return { name, filePath };
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
)).filter(Boolean);
|
|
175
|
+
} catch { /* best-effort */ }
|
|
176
|
+
if (!candidates) return;
|
|
177
|
+
let serialized;
|
|
178
|
+
try { serialized = JSON.stringify(getMessages()); } catch { return; }
|
|
179
|
+
const haystack = process.platform === 'win32' ? serialized.toLowerCase() : serialized;
|
|
180
|
+
await Promise.all(candidates
|
|
181
|
+
.filter(({ name, filePath }) => {
|
|
182
|
+
const normalizedPath = normalizeOutputPath(filePath);
|
|
183
|
+
const needles = [normalizedPath, name];
|
|
184
|
+
return !needles.some((needle) => {
|
|
185
|
+
const value = process.platform === 'win32' ? needle.toLowerCase() : needle;
|
|
186
|
+
return haystack.includes(value);
|
|
187
|
+
});
|
|
188
|
+
})
|
|
189
|
+
.map(({ filePath }) => unlink(filePath).catch(() => { /* best-effort */ })));
|
|
190
|
+
}
|
|
191
|
+
|
|
150
192
|
export function isOffloadedToolResultText(text) {
|
|
151
193
|
return typeof text === 'string' && text.startsWith(TOOL_RESULT_OFFLOAD_PREFIX);
|
|
152
194
|
}
|
|
@@ -124,23 +124,31 @@ export function _isBenignSearchExitOne(command, exitCode, signal, stderr) {
|
|
|
124
124
|
function _combineAbortSignals(sessionSignal, externalSignal) {
|
|
125
125
|
const a = sessionSignal || null;
|
|
126
126
|
const b = externalSignal || null;
|
|
127
|
-
if (!a && !b) return null;
|
|
128
|
-
if (!a) return b;
|
|
129
|
-
if (!b) return a;
|
|
130
|
-
if (a === b) return a;
|
|
127
|
+
if (!a && !b) return { signal: null, cleanup() {} };
|
|
128
|
+
if (!a) return { signal: b, cleanup() {} };
|
|
129
|
+
if (!b) return { signal: a, cleanup() {} };
|
|
130
|
+
if (a === b) return { signal: a, cleanup() {} };
|
|
131
131
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.any === 'function') {
|
|
132
|
-
try { return AbortSignal.any([a, b]); } catch { /* fall through */ }
|
|
132
|
+
try { return { signal: AbortSignal.any([a, b]), cleanup() {} }; } catch { /* fall through */ }
|
|
133
133
|
}
|
|
134
134
|
const ctl = new AbortController();
|
|
135
135
|
const onAbort = (sig) => {
|
|
136
136
|
if (ctl.signal.aborted) return;
|
|
137
137
|
try { ctl.abort(sig?.reason); } catch { try { ctl.abort(); } catch {} }
|
|
138
138
|
};
|
|
139
|
-
if (a.aborted) { onAbort(a); return ctl.signal; }
|
|
140
|
-
if (b.aborted) { onAbort(b); return ctl.signal; }
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
if (a.aborted) { onAbort(a); return { signal: ctl.signal, cleanup() {} }; }
|
|
140
|
+
if (b.aborted) { onAbort(b); return { signal: ctl.signal, cleanup() {} }; }
|
|
141
|
+
const onAbortA = () => onAbort(a);
|
|
142
|
+
const onAbortB = () => onAbort(b);
|
|
143
|
+
try { a.addEventListener('abort', onAbortA, { once: true }); } catch {}
|
|
144
|
+
try { b.addEventListener('abort', onAbortB, { once: true }); } catch {}
|
|
145
|
+
return {
|
|
146
|
+
signal: ctl.signal,
|
|
147
|
+
cleanup() {
|
|
148
|
+
try { a.removeEventListener('abort', onAbortA); } catch {}
|
|
149
|
+
try { b.removeEventListener('abort', onAbortB); } catch {}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
function _prefixPowerShellUtf8(command) {
|
|
@@ -228,7 +236,11 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
228
236
|
const userProvidedSession = typeof args.session_id === 'string' && args.session_id.trim().length > 0;
|
|
229
237
|
const shouldCreate = args.create === true || !userProvidedSession;
|
|
230
238
|
effectiveArgs = { ...effectiveArgs, create: shouldCreate };
|
|
231
|
-
|
|
239
|
+
try {
|
|
240
|
+
return await executeBashSessionTool('bash_session', effectiveArgs, bashWorkDir, { abortSignal: combinedPersistAbort.signal, sessionId: options?.sessionId });
|
|
241
|
+
} finally {
|
|
242
|
+
combinedPersistAbort.cleanup();
|
|
243
|
+
}
|
|
232
244
|
}
|
|
233
245
|
|
|
234
246
|
let command = args.command;
|
|
@@ -276,6 +288,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
let shellEffects;
|
|
291
|
+
let combinedBashAbort = null;
|
|
279
292
|
try {
|
|
280
293
|
shellEffects = await analyzeShellCommandEffects(command, bashWorkDir);
|
|
281
294
|
} catch (err) {
|
|
@@ -284,10 +297,9 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
284
297
|
// Keep foreground commands on a long tool-owned timeout. The MCP dispatch
|
|
285
298
|
// layer must not add a shorter fallback ceiling when timeout is omitted.
|
|
286
299
|
// Reference-CLI parity (opencode/codex/claude-code): sync-first, no hard
|
|
287
|
-
// upper ceiling on a caller-provided timeout. Default 120 s (2 min)
|
|
288
|
-
// omitted; BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS env overrides
|
|
289
|
-
//
|
|
290
|
-
// default — an explicit args.timeout is honored UNCAPPED.
|
|
300
|
+
// upper ceiling on a caller-provided total timeout. Default 120 s (2 min)
|
|
301
|
+
// when omitted; BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS env overrides
|
|
302
|
+
// bound the blocking window when timeout promotion is available.
|
|
291
303
|
const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
|
|
292
304
|
const DEFAULT_BASH_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
|
|
293
305
|
// Background (async / run_in_background) jobs get NO omitted default: 0
|
|
@@ -302,8 +314,14 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
302
314
|
: DEFAULT_BASH_TIMEOUT_MS;
|
|
303
315
|
const hasExplicitTimeout = typeof args.timeout === 'number' && args.timeout > 0;
|
|
304
316
|
const timeoutMs = hasExplicitTimeout ? args.timeout : defaultTimeoutMs;
|
|
305
|
-
|
|
306
|
-
|
|
317
|
+
const _bgTasksDisabled = /^(1|true|yes|on)$/i.test(
|
|
318
|
+
String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
|
|
319
|
+
);
|
|
320
|
+
const backgroundOnTimeout = !runInBackground
|
|
321
|
+
&& !_bgTasksDisabled
|
|
322
|
+
&& isAutobackgroundingAllowed(command, resolvedSpec.shellType);
|
|
323
|
+
// Explicit caller timeout remains the total deadline. When promotion is
|
|
324
|
+
// available, cap only its foreground blocking portion at MAX.
|
|
307
325
|
// JS timers (setTimeout) and PS WaitForExit(ms) are 32-bit: a delay above
|
|
308
326
|
// 2^31-1 wraps to a tiny/negative value and fires immediately. Clamp the
|
|
309
327
|
// uncapped explicit timeout once here (~24.8 days ceiling) so every
|
|
@@ -312,11 +330,15 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
312
330
|
const TIMER_MAX_MS = 2_147_483_647;
|
|
313
331
|
// timeoutMs <= 0 (omitted background default) means unlimited: pass it
|
|
314
332
|
// through untouched — the min() clamps below must not turn 0 into a bound.
|
|
315
|
-
const
|
|
333
|
+
const totalTimeout = timeoutMs <= 0
|
|
316
334
|
? 0
|
|
317
|
-
: (hasExplicitTimeout
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
: Math.min(timeoutMs, wmicRewrite?.timeoutMs || (hasExplicitTimeout ? TIMER_MAX_MS : MAX_BASH_TIMEOUT_MS));
|
|
336
|
+
const timeout = hasExplicitTimeout && backgroundOnTimeout
|
|
337
|
+
? Math.min(totalTimeout, MAX_BASH_TIMEOUT_MS)
|
|
338
|
+
: totalTimeout;
|
|
339
|
+
const promotedTimeoutMs = hasExplicitTimeout && backgroundOnTimeout
|
|
340
|
+
? Math.max(0, totalTimeout - timeout)
|
|
341
|
+
: 0;
|
|
320
342
|
const mergeStderr = args.merge_stderr === true;
|
|
321
343
|
const longForegroundHint = foregroundLongCommandHint(command, timeout, { ...args, run_in_background: runInBackground });
|
|
322
344
|
if (longForegroundHint) return formatShellToolFailure(longForegroundHint);
|
|
@@ -423,7 +445,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
423
445
|
let bashAbortSignal = null;
|
|
424
446
|
try { bashAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
|
|
425
447
|
catch { bashAbortSignal = null; }
|
|
426
|
-
|
|
448
|
+
combinedBashAbort = _combineAbortSignals(bashAbortSignal, options?.abortSignal || null);
|
|
427
449
|
// Sync path only: chain a trailing cwd probe so the session's final
|
|
428
450
|
// working directory persists to the next shell call. Async jobs run
|
|
429
451
|
// detached and are intentionally excluded (they never reach here). The
|
|
@@ -445,22 +467,17 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
445
467
|
// base commands (isAutobackgroundingAllowed), (b) the truthy
|
|
446
468
|
// MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS env. Never applies to
|
|
447
469
|
// run_in_background (already detached, handled above).
|
|
448
|
-
const _bgTasksDisabled = /^(1|true|yes|on)$/i.test(
|
|
449
|
-
String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
|
|
450
|
-
);
|
|
451
|
-
const backgroundOnTimeout = !runInBackground
|
|
452
|
-
&& !_bgTasksDisabled
|
|
453
|
-
&& isAutobackgroundingAllowed(command, shellType);
|
|
454
470
|
const result = await execShellCommand({
|
|
455
471
|
shell, shellArg, shellArgs, command: syncCommand,
|
|
456
472
|
env: spawnEnv,
|
|
457
473
|
cwd: bashWorkDir,
|
|
458
474
|
timeoutMs: timeout,
|
|
459
|
-
abortSignal: combinedBashAbort,
|
|
475
|
+
abortSignal: combinedBashAbort.signal,
|
|
460
476
|
autoBackgroundMs,
|
|
461
477
|
// On a foreground timeout, promote the still-running child to a
|
|
462
478
|
// tracked background job (unlimited) instead of killing it.
|
|
463
479
|
backgroundOnTimeout,
|
|
480
|
+
promotedTimeoutMs,
|
|
464
481
|
// Threaded so an auto-backgrounded foreground job is stamped with
|
|
465
482
|
// the dispatching terminal's claude.exe pid (per-terminal scope).
|
|
466
483
|
clientHostPid: options?.clientHostPid,
|
|
@@ -494,10 +511,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
494
511
|
stdout: result.stdoutPath ? normalizeOutputPath(result.stdoutPath) : null,
|
|
495
512
|
stderr: (!mergeStderr && result.stderrPath) ? normalizeOutputPath(result.stderrPath) : null,
|
|
496
513
|
cwd: bashWorkDir,
|
|
497
|
-
|
|
498
|
-
// — matches the async default; the original
|
|
499
|
-
// foreground timeout no longer bounds the promoted job.
|
|
500
|
-
timeoutMs: 0,
|
|
514
|
+
timeoutMs: result.backgroundTimeoutMs || 0,
|
|
501
515
|
},
|
|
502
516
|
resultType: 'shell_task_result',
|
|
503
517
|
cancel: () => killShellJob(result.jobId),
|
|
@@ -517,7 +531,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
517
531
|
const lines = [
|
|
518
532
|
task ? renderBackgroundTask(task) : (result.jobId ? `[task_id: ${result.jobId}]` : null),
|
|
519
533
|
'',
|
|
520
|
-
result.backgroundMessage || 'auto-backgrounded; still running',
|
|
534
|
+
result.backgroundMessage || 'auto-backgrounded; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.',
|
|
521
535
|
partialStdout ? `\n[partial stdout]\n${partialStdout}` : '',
|
|
522
536
|
(!mergeStderr && partialStderr) ? `\n[partial stderr]\n${partialStderr}` : '',
|
|
523
537
|
].filter((l) => l !== null && l !== '');
|
|
@@ -592,6 +606,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
592
606
|
return _prependDestructiveWarning(command, warningBlock ? `${warningBlock}\n${payload}` : payload);
|
|
593
607
|
}
|
|
594
608
|
finally {
|
|
609
|
+
combinedBashAbort?.cleanup?.();
|
|
595
610
|
if (shellEffects.mutationMode === 'paths') {
|
|
596
611
|
invalidateBuiltinResultCache(shellEffects.paths);
|
|
597
612
|
markCodeGraphDirtyPaths(shellEffects.paths);
|
|
@@ -83,7 +83,7 @@ export const BUILTIN_TOOLS = [
|
|
|
83
83
|
properties: {
|
|
84
84
|
command: { type: 'string', description: 'Command.' },
|
|
85
85
|
cwd: { type: 'string', description: 'Working directory; persists across calls. Omit to reuse; absolute path changes it.' },
|
|
86
|
-
timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min) when omitted
|
|
86
|
+
timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min) when omitted. On sync timeout the command is MOVED TO BACKGROUND (a task_id you can wait/status/read/cancel) and keeps running instead of being killed; an explicit timeout blocks for at most BASH_MAX_TIMEOUT_MS (default 10 min), and only its remainder beyond that cap is still enforced as a background deadline. Sleep-like commands and MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS opt out (killed with a [timeout] marker after blocking for the full explicit timeout). async/background runs with timeout omitted have NO timeout (run until done/cancelled); an explicit timeout is still enforced.` },
|
|
87
87
|
merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
|
|
88
88
|
mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
|
|
89
89
|
shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
|
|
@@ -500,6 +500,7 @@ export function execShellCommand({
|
|
|
500
500
|
onProgress,
|
|
501
501
|
clientHostPid,
|
|
502
502
|
backgroundOnTimeout,
|
|
503
|
+
promotedTimeoutMs = 0,
|
|
503
504
|
}) {
|
|
504
505
|
return new Promise(async (resolve) => {
|
|
505
506
|
const taskId = `shell_${randomUUID().slice(0, 8)}`;
|
|
@@ -741,9 +742,8 @@ export function execShellCommand({
|
|
|
741
742
|
// BACKGROUND_MS opt-in) — an EARLIER promotion before the timeout, and
|
|
742
743
|
// 2. the foreground timeout deadline (backgroundOnTimeout) — the default
|
|
743
744
|
// promote-on-timeout that replaces the old tree-kill.
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
// the adopted-job cap poll still enforces the 100 MB output ceiling.
|
|
745
|
+
// A capped explicit foreground timeout supplies its remaining deadline to
|
|
746
|
+
// the adopted job; otherwise adoption remains unlimited as before.
|
|
747
747
|
// Mutually exclusive with settle() via the autoBackgrounded flag set
|
|
748
748
|
// synchronously at the top before any await.
|
|
749
749
|
const _autoBackground = async ({ reason = 'threshold' } = {}) => {
|
|
@@ -754,8 +754,7 @@ export function execShellCommand({
|
|
|
754
754
|
if (child.exitCode != null || child.signalCode != null) return;
|
|
755
755
|
autoBackgrounded = true;
|
|
756
756
|
// The foreground capture is over; stop the local watchdogs/timers so
|
|
757
|
-
// they cannot treeKill the now-adopted child.
|
|
758
|
-
// unlimited; refreshShellJob only enforces the output cap.
|
|
757
|
+
// they cannot treeKill the now-adopted child.
|
|
759
758
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
760
759
|
_clearProgressTimer();
|
|
761
760
|
if (sizeWatchdog) { clearInterval(sizeWatchdog); sizeWatchdog = null; }
|
|
@@ -777,14 +776,13 @@ export function execShellCommand({
|
|
|
777
776
|
const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
|
|
778
777
|
const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
|
|
779
778
|
let job = null;
|
|
779
|
+
const adoptedTimeoutMs = reason === 'timeout' ? promotedTimeoutMs : 0;
|
|
780
780
|
try {
|
|
781
781
|
job = adoptForegroundShellJob({
|
|
782
782
|
command,
|
|
783
783
|
cwd,
|
|
784
784
|
pid: child.pid,
|
|
785
|
-
|
|
786
|
-
// timeout (matches the async omitted-default behavior).
|
|
787
|
-
timeoutMs: 0,
|
|
785
|
+
timeoutMs: adoptedTimeoutMs,
|
|
788
786
|
mergeStderr: false,
|
|
789
787
|
stdoutPath,
|
|
790
788
|
stderrPath,
|
|
@@ -854,9 +852,10 @@ export function execShellCommand({
|
|
|
854
852
|
outputCaptureError: taskOutput.writeError,
|
|
855
853
|
backgrounded: true,
|
|
856
854
|
jobId,
|
|
855
|
+
backgroundTimeoutMs: adoptedTimeoutMs,
|
|
857
856
|
backgroundMessage: jobId
|
|
858
|
-
? `${_verb}; still running —
|
|
859
|
-
: `${_verb}; still running
|
|
857
|
+
? `${_verb}; still running. Waiting is a decision, not a default: judge from the partial output whether this will finish within your remaining budget — if progress looks slow or stalled, diagnose the cause and pursue an alternative instead of waiting. Completion will be delivered as a background task notification; use task with task_id:${jobId} only for manual wait/status/read/cancel.`
|
|
858
|
+
: `${_verb}; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.`,
|
|
860
859
|
}),
|
|
861
860
|
);
|
|
862
861
|
};
|
|
@@ -38,6 +38,13 @@ function scheduleFlush(path, q) {
|
|
|
38
38
|
q.timer.unref?.();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function deleteIfIdle(path, q) {
|
|
42
|
+
if (!q.timer && !q.flushing && !q.inFlight && q.chunks.length === 0
|
|
43
|
+
&& queues.get(path) === q) {
|
|
44
|
+
queues.delete(path);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
function _flush(path, q) {
|
|
42
49
|
if (q.flushing) return;
|
|
43
50
|
if (q.chunks.length === 0) return;
|
|
@@ -70,7 +77,7 @@ function _flush(path, q) {
|
|
|
70
77
|
if (q.chunks.length > 0) {
|
|
71
78
|
if (q.bytes >= BUFFER_FLUSH_BYTES) _flush(path, q);
|
|
72
79
|
else scheduleFlush(path, q);
|
|
73
|
-
}
|
|
80
|
+
} else deleteIfIdle(path, q);
|
|
74
81
|
});
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -120,7 +127,10 @@ export function drainPathSync(path) {
|
|
|
120
127
|
}
|
|
121
128
|
q.inFlight = null;
|
|
122
129
|
}
|
|
123
|
-
if (q.chunks.length === 0)
|
|
130
|
+
if (q.chunks.length === 0) {
|
|
131
|
+
deleteIfIdle(path, q);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
124
134
|
const data = q.chunks.join('');
|
|
125
135
|
q.chunks = [];
|
|
126
136
|
q.bytes = 0;
|
|
@@ -129,6 +139,7 @@ export function drainPathSync(path) {
|
|
|
129
139
|
} catch {
|
|
130
140
|
// Best-effort exit drain; nothing to recover from here.
|
|
131
141
|
}
|
|
142
|
+
deleteIfIdle(path, q);
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
/**
|
|
@@ -176,16 +176,16 @@ export function autoClearProviderDefaults(providerIdleMs = null) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Agent terminal retention is deliberately narrower than Auto Clear's general
|
|
179
|
-
// resolution:
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// Null means leave the completed agent alone (no live timer, row expiry, or
|
|
183
|
-
// durable store sweep).
|
|
179
|
+
// resolution: listed providers use their provider cache lifetime. Unlisted
|
|
180
|
+
// providers use the Advanced "default" row, or the built-in default when no
|
|
181
|
+
// override is configured, so completed agents are still eventually reclaimed.
|
|
184
182
|
export function resolveAgentTerminalReapMs(config, provider) {
|
|
185
183
|
const key = clean(provider).toLowerCase();
|
|
186
|
-
if (!key || key === 'default' || !Object.hasOwn(AUTO_CLEAR_PROVIDER_IDLE_MS, key)) return null;
|
|
187
184
|
const raw = config?.autoClear && typeof config.autoClear === 'object' ? config.autoClear : {};
|
|
188
185
|
const overrides = normalizeAutoClearProviderIdleMs(raw.providerIdleMs);
|
|
186
|
+
if (!key || key === 'default' || !Object.hasOwn(AUTO_CLEAR_PROVIDER_IDLE_MS, key)) {
|
|
187
|
+
return overrides.default ?? AUTO_CLEAR_PROVIDER_IDLE_MS.default;
|
|
188
|
+
}
|
|
189
189
|
return overrides[key] ?? AUTO_CLEAR_PROVIDER_IDLE_MS[key];
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -30,6 +30,7 @@ export function createLifecycleApi(deps) {
|
|
|
30
30
|
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
31
31
|
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, standaloneTools,
|
|
32
32
|
pushTranscriptRebind,
|
|
33
|
+
notificationListeners, remoteStateListeners,
|
|
33
34
|
} = deps;
|
|
34
35
|
return {
|
|
35
36
|
async close(reason = 'cli-exit', options = {}) {
|
|
@@ -132,6 +133,9 @@ export function createLifecycleApi(deps) {
|
|
|
132
133
|
ok = mgr.closeSession(session.id, reason, { tombstone });
|
|
133
134
|
setSession(null);
|
|
134
135
|
}
|
|
136
|
+
invalidateContextStatusCache();
|
|
137
|
+
notificationListeners?.clear?.();
|
|
138
|
+
remoteStateListeners?.clear?.();
|
|
135
139
|
const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
|
|
136
140
|
? import('../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
|
|
137
141
|
.then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
|
|
@@ -33,6 +33,18 @@ export function createSessionTurnApi(deps) {
|
|
|
33
33
|
awaitInitialMcpConnect,
|
|
34
34
|
} = deps;
|
|
35
35
|
return {
|
|
36
|
+
getTurnLiveness() {
|
|
37
|
+
const sessionId = getSession()?.id;
|
|
38
|
+
if (!sessionId || typeof mgr.getSessionProgressSnapshot !== 'function') return null;
|
|
39
|
+
const snapshot = mgr.getSessionProgressSnapshot(sessionId);
|
|
40
|
+
if (!snapshot) return null;
|
|
41
|
+
return {
|
|
42
|
+
stage: snapshot.stage,
|
|
43
|
+
lastProgressAt: snapshot.lastProgressAt,
|
|
44
|
+
toolStartedAt: snapshot.toolStartedAt,
|
|
45
|
+
toolSelfDeadlineMs: snapshot.toolSelfDeadlineMs,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
36
48
|
async ask(prompt, options = {}) {
|
|
37
49
|
setActiveTurnCount(getActiveTurnCount() + 1);
|
|
38
50
|
// Lazy code-graph prewarm: kick off the build ONCE, on the first real
|
|
@@ -509,7 +509,11 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
509
509
|
for (const [tag, sessionId] of [...tags.entries()]) {
|
|
510
510
|
if (indexedKeys.has(`${tag}\0${sessionId}`)) continue;
|
|
511
511
|
const session = getLiveSession(sessionId);
|
|
512
|
-
if (!session || session.closed)
|
|
512
|
+
if (!session || session.closed) {
|
|
513
|
+
tags.delete(tag);
|
|
514
|
+
tagAgents.delete(tag);
|
|
515
|
+
tagCwds.delete(tag);
|
|
516
|
+
}
|
|
513
517
|
}
|
|
514
518
|
if (!scanSessions) return;
|
|
515
519
|
for (const session of mgr.listSessions({ includeClosed: false }) || []) {
|
|
@@ -1514,6 +1518,9 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1514
1518
|
}
|
|
1515
1519
|
for (const timer of reapTimers.values()) clearTimeout(timer);
|
|
1516
1520
|
reapTimers.clear();
|
|
1521
|
+
tags.clear();
|
|
1522
|
+
tagAgents.clear();
|
|
1523
|
+
tagCwds.clear();
|
|
1517
1524
|
flushWorkerIndexMutations();
|
|
1518
1525
|
writeWorkerRows((byKey) => byKey.clear());
|
|
1519
1526
|
return { closed, failed };
|
package/src/tui/App.jsx
CHANGED
|
@@ -107,7 +107,6 @@ import {
|
|
|
107
107
|
transcriptItemVariantKey,
|
|
108
108
|
transcriptMeasuredRowsCache,
|
|
109
109
|
buildTranscriptRowIndex,
|
|
110
|
-
transcriptStructureSignature,
|
|
111
110
|
transcriptRenderWindow,
|
|
112
111
|
} from './app/transcript-window.mjs';
|
|
113
112
|
import {
|
|
@@ -2951,6 +2950,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
2951
2950
|
transcriptMeasureRef,
|
|
2952
2951
|
} = useTranscriptWindow({
|
|
2953
2952
|
items: state.items,
|
|
2953
|
+
structureRevision: state.structureRevision,
|
|
2954
|
+
streamingTail: state.streamingTail,
|
|
2954
2955
|
themeEpoch: state.themeEpoch,
|
|
2955
2956
|
frameColumns,
|
|
2956
2957
|
toolOutputExpanded,
|
|
@@ -832,7 +832,7 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
832
832
|
suppressMeasuredRowHeights = false,
|
|
833
833
|
measuredRowsVersion = 0,
|
|
834
834
|
cacheRef = null,
|
|
835
|
-
|
|
835
|
+
prefixRevision = null,
|
|
836
836
|
} = {}) {
|
|
837
837
|
const allItems = Array.isArray(items) ? items : [];
|
|
838
838
|
const tail = trailingStreamingItem(allItems);
|
|
@@ -850,11 +850,10 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
850
850
|
}
|
|
851
851
|
const prefixLen = allItems.length - 1;
|
|
852
852
|
const cache = holder.current;
|
|
853
|
-
// Prefix identity is carried by
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
// Fast path: same prefix length + tail id + prefixSig, and columns/expanded/
|
|
853
|
+
// Prefix identity is carried by the engine's monotonic structure revision.
|
|
854
|
+
// Every settled-item mutation, including same-length middle tool-card patches,
|
|
855
|
+
// bumps it without requiring a render-time walk over the transcript.
|
|
856
|
+
// Fast path: same prefix length + tail id + revision, and columns/expanded/
|
|
858
857
|
// suppress/version all match. Only the tail row can differ → recompute + append.
|
|
859
858
|
if (cache
|
|
860
859
|
&& cache.columns === columns
|
|
@@ -862,10 +861,10 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
862
861
|
&& cache.suppress === suppressMeasuredRowHeights
|
|
863
862
|
&& cache.version === measuredRowsVersion
|
|
864
863
|
&& cache.prefixLen === prefixLen
|
|
865
|
-
&&
|
|
866
|
-
&& cache.
|
|
864
|
+
&& prefixRevision != null
|
|
865
|
+
&& cache.prefixRevision === prefixRevision
|
|
867
866
|
&& cache.tailId === tail.id) {
|
|
868
|
-
//
|
|
867
|
+
// revision matches → prefix rows provably unchanged; NO prefix walk / no
|
|
869
868
|
// re-estimation. Only the tail row can differ, recompute it.
|
|
870
869
|
{
|
|
871
870
|
const tailMeasured = suppressMeasuredRowHeights
|
|
@@ -894,7 +893,7 @@ export function buildTranscriptRowIndexIncremental(items, {
|
|
|
894
893
|
suppress: suppressMeasuredRowHeights,
|
|
895
894
|
version: measuredRowsVersion,
|
|
896
895
|
prefixLen,
|
|
897
|
-
|
|
896
|
+
prefixRevision,
|
|
898
897
|
tailId: tail.id,
|
|
899
898
|
prefixRowsArr: full.rows.slice(0, prefixLen),
|
|
900
899
|
prefixPrefixRows: full.prefixRows.slice(0, prefixLen + 1),
|