mixdog 0.9.111 → 0.9.113
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 +10 -58
- package/src/rules/shared/01-tool.md +15 -6
- package/src/runtime/agent/orchestrator/config.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +74 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +26 -7
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -5
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +14 -30
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +0 -5
- package/src/session-runtime/settings-api.mjs +9 -1
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/standalone/daemon.mjs +1 -0
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/tui/session/session-api.mjs +2 -1
- package/src/tui/session/session-flow.mjs +25 -1
- package/src/ui/statusline-segments.mjs +20 -5
- package/scripts/run-suite.mjs +0 -101
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +0 -188
|
@@ -52,13 +52,15 @@ import {
|
|
|
52
52
|
} from '../../../../shared/background-tasks.mjs';
|
|
53
53
|
import { resolveShellFor } from './shell-runtime.mjs';
|
|
54
54
|
import { prewarmPwshStandbyPool } from '../lib/pwsh-standby-pool.mjs';
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
renderBackgroundPartialOutput,
|
|
57
|
+
smartMiddleTruncate,
|
|
58
|
+
} from './shell-output.mjs';
|
|
56
59
|
import { normalizeOutputPath } from './path-utils.mjs';
|
|
57
60
|
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
58
61
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
59
62
|
import { resolveOptionalCwd } from './cwd-utils.mjs';
|
|
60
63
|
import { scrubLoaderVars, scrubProviderSecrets, scrubRuntimeRootVars } from '../env-scrub.mjs';
|
|
61
|
-
import { resolveSessionCwd, stateFilePath, wrapPowerShellWithCwdProbe, wrapBashWithCwdProbe } from '../shell-state.mjs';
|
|
62
64
|
import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
|
|
63
65
|
|
|
64
66
|
// Post-exec drift detection. After a foreground shell command, compare the
|
|
@@ -256,12 +258,10 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
256
258
|
const requestedCwd = args.cwd ?? args.workdir;
|
|
257
259
|
const cwdResult = resolveOptionalCwd(requestedCwd, workDir);
|
|
258
260
|
if (cwdResult.error) return formatShellToolFailure(cwdResult.error);
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
const
|
|
263
|
-
const _sessionCwdKey = options?.sessionId ?? options?.readStateScope ?? options?.callerSessionId ?? null;
|
|
264
|
-
const bashWorkDir = resolveSessionCwd(_sessionCwdKey, _hasExplicitCwd ? cwdResult.cwd : null, cwdResult.cwd);
|
|
261
|
+
// One-shot shell calls always start from the current Project root unless
|
|
262
|
+
// this call supplies cwd/workdir. A command-local `cd` must not create a
|
|
263
|
+
// second session cwd authority beside the dedicated cwd tool.
|
|
264
|
+
const bashWorkDir = cwdResult.cwd;
|
|
265
265
|
const _readStateScope = options?.readStateScope ?? options?.sessionId ?? null;
|
|
266
266
|
const executionMode = resolveExecutionMode(args || {}, args?.run_in_background === true ? 'async' : 'sync');
|
|
267
267
|
let runInBackground = executionMode === 'async';
|
|
@@ -671,23 +671,6 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
671
671
|
try { bashAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
|
|
672
672
|
catch { bashAbortSignal = null; }
|
|
673
673
|
combinedBashAbort = _combineAbortSignals(bashAbortSignal, options?.abortSignal || null);
|
|
674
|
-
// Sync path only: chain a trailing cwd probe so the session's final
|
|
675
|
-
// working directory persists to the next shell call. Async jobs run
|
|
676
|
-
// detached and are intentionally excluded (they never reach here). The
|
|
677
|
-
// probe captures the command's exit status first and re-exits with it,
|
|
678
|
-
// so the exit code the model sees is unchanged.
|
|
679
|
-
let syncCommand = wrappedCommand;
|
|
680
|
-
try {
|
|
681
|
-
const _stateFile = stateFilePath(
|
|
682
|
-
_sessionCwdKey,
|
|
683
|
-
options?.deferShellCwdCommit === true ? options?.toolCallId : null,
|
|
684
|
-
);
|
|
685
|
-
if (_stateFile) {
|
|
686
|
-
syncCommand = (process.platform === 'win32' && shellType === 'powershell')
|
|
687
|
-
? wrapPowerShellWithCwdProbe(wrappedCommand, _stateFile)
|
|
688
|
-
: wrapBashWithCwdProbe(wrappedCommand, _stateFile);
|
|
689
|
-
}
|
|
690
|
-
} catch { syncCommand = wrappedCommand; }
|
|
691
674
|
// Promote-at-timeout (CC shouldAutoBackground parity). When a
|
|
692
675
|
// foreground one-shot hits its timeout and is still running, adopt it
|
|
693
676
|
// as a background job (task_id + notify) instead of tree-killing it.
|
|
@@ -696,7 +679,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
696
679
|
// MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS env. Never applies to
|
|
697
680
|
// run_in_background (already detached, handled above).
|
|
698
681
|
const result = await execShellCommand({
|
|
699
|
-
shell, shellArg, shellArgs, command:
|
|
682
|
+
shell, shellArg, shellArgs, command: wrappedCommand,
|
|
700
683
|
env: spawnEnv,
|
|
701
684
|
cwd: bashWorkDir,
|
|
702
685
|
timeoutMs: timeout,
|
|
@@ -765,14 +748,15 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
765
748
|
});
|
|
766
749
|
} catch { /* best effort */ }
|
|
767
750
|
}
|
|
768
|
-
const
|
|
769
|
-
|
|
751
|
+
const partialOutput = renderBackgroundPartialOutput(
|
|
752
|
+
stripAnsi(result.stdout || ''),
|
|
753
|
+
stripAnsi(result.stderr || ''),
|
|
754
|
+
);
|
|
770
755
|
const lines = [
|
|
771
756
|
task ? renderBackgroundTask(task) : (result.jobId ? `[task_id: ${result.jobId}]` : null),
|
|
772
757
|
'',
|
|
773
758
|
result.backgroundMessage || 'auto-backgrounded; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.',
|
|
774
|
-
|
|
775
|
-
(!mergeStderr && partialStderr) ? `\n[partial stderr]\n${partialStderr}` : '',
|
|
759
|
+
partialOutput ? `\n${partialOutput}` : '',
|
|
776
760
|
].filter((l) => l !== null && l !== '');
|
|
777
761
|
return _prependDestructiveWarning(command, lines.join('\n'));
|
|
778
762
|
}
|
|
@@ -24,7 +24,7 @@ function _shellMaxTimeoutMs() {
|
|
|
24
24
|
// Platform-specific command syntax belongs next to the command argument.
|
|
25
25
|
const _shellSyntaxCheat =
|
|
26
26
|
process.platform === 'win32'
|
|
27
|
-
? ' PowerShell: use ; between independent commands; use if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } between dependent commands; /c/→C:\\; $PID is reserved.'
|
|
27
|
+
? ' PowerShell: use ; between independent commands; use if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } between dependent commands; single-quote inline scripts, avoid nested double quotes; /c/→C:\\; $PID is reserved.'
|
|
28
28
|
: ' Bash: use && between dependent commands.';
|
|
29
29
|
|
|
30
30
|
export const BUILTIN_TOOLS = [
|
|
@@ -80,7 +80,7 @@ export const BUILTIN_TOOLS = [
|
|
|
80
80
|
type: 'object',
|
|
81
81
|
properties: {
|
|
82
82
|
command: { type: 'string', description: `Command.${_shellSyntaxCheat}` },
|
|
83
|
-
cwd: { type: 'string', description: 'Omit
|
|
83
|
+
cwd: { type: 'string', description: 'Omit to use the current Project root; use a project-relative subdir or explicit external path for this call only.' },
|
|
84
84
|
timeout: {
|
|
85
85
|
type: 'number',
|
|
86
86
|
description: `Timeout ms; default ${_shellDefaultTimeoutMs()}. Explicit values are deadlines; sync may return task_id.`,
|
|
@@ -172,6 +172,7 @@ export const BUILTIN_TOOLS = [
|
|
|
172
172
|
description: 'Project-relative base dir(s); omit for project root; path[] batches; absolute only outside.',
|
|
173
173
|
},
|
|
174
174
|
limit: { type: 'number', description: 'Max entries; default 100; 0 unlimited.' },
|
|
175
|
+
offset: { type: 'number', minimum: 0, description: 'Entry offset.' },
|
|
175
176
|
},
|
|
176
177
|
required: ['pattern'],
|
|
177
178
|
additionalProperties: false,
|
|
@@ -194,6 +195,7 @@ export const BUILTIN_TOOLS = [
|
|
|
194
195
|
},
|
|
195
196
|
path: { type: 'string', description: 'Project-relative base; omit for project root; absolute only outside.' },
|
|
196
197
|
limit: { type: 'number', description: 'Max paths across the call. Defaults to 25.' },
|
|
198
|
+
include_noise: { type: 'boolean', description: 'Also search gitignored/dependency trees.' },
|
|
197
199
|
},
|
|
198
200
|
required: ['query'],
|
|
199
201
|
additionalProperties: false,
|
|
@@ -210,7 +210,7 @@ export function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimi
|
|
|
210
210
|
const truncated = (remaining > 0 || !totalKnown)
|
|
211
211
|
? (totalKnown
|
|
212
212
|
? `\n[Showing ${shown} of ${total} results; pass offset:${offset + shown} for more]`
|
|
213
|
-
: `\n[Showing ${shown} (more matches exist — use
|
|
213
|
+
: `\n[Showing ${shown} (more matches exist — use mode:'count' for the exact total on ${scopePath}); pass offset:${offset + shown} for more]`)
|
|
214
214
|
: '';
|
|
215
215
|
|
|
216
216
|
let countSummary = '';
|
|
@@ -625,7 +625,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
625
625
|
const notes = [];
|
|
626
626
|
if (omittedByHeadLimit.length) {
|
|
627
627
|
notes.push(
|
|
628
|
-
`... [
|
|
628
|
+
`... [limit ${totalHeadLimit} exhausted across query[]; `
|
|
629
629
|
+ `retry query=${JSON.stringify(omittedByHeadLimit)}]`,
|
|
630
630
|
);
|
|
631
631
|
}
|
|
@@ -704,7 +704,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
704
704
|
const capFindResult = (value) => capLineOrientedToolOutput(
|
|
705
705
|
value,
|
|
706
706
|
_findOutputBudgetBytes(options),
|
|
707
|
-
() => `... [find result budget reached for query=${JSON.stringify(query)}; narrow path/
|
|
707
|
+
() => `... [find result budget reached for query=${JSON.stringify(query)}; narrow path/limit]`,
|
|
708
708
|
);
|
|
709
709
|
if (cached !== null) return capFindResult(cached);
|
|
710
710
|
// Common discovery respects .gitignore even outside a Git repository.
|
|
@@ -825,7 +825,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
825
825
|
// report "(no fuzzy match …)" as if the tree were exhaustively searched.
|
|
826
826
|
const noMatch = ranked.length === 0;
|
|
827
827
|
const lines = noMatch ? [`(no fuzzy match for "${query}")`] : ranked.map((r) => r.item.path);
|
|
828
|
-
if (!noMatch && hasMore) lines.push(`... (top ${headLimit}; raise
|
|
828
|
+
if (!noMatch && hasMore) lines.push(`... (top ${headLimit}; raise limit for more)`);
|
|
829
829
|
if (rgTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; broad ranking incomplete (exact-name hits still merged)');
|
|
830
830
|
if (rgPartial && !rgTruncated) lines.push('... [warning] rg exit 2 (partial results); broad ranking may be incomplete');
|
|
831
831
|
if (!targetedProbeRan && headLimit > 0 && passOneRanked.length >= headLimit) {
|
|
@@ -491,7 +491,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
491
491
|
const headLimitRaw = args.head_limit;
|
|
492
492
|
const headLimitCoerced = coerceNonNegInt(headLimitRaw);
|
|
493
493
|
if (Number.isNaN(headLimitCoerced)) {
|
|
494
|
-
return `Error: invalid
|
|
494
|
+
return `Error: invalid limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
|
|
495
495
|
}
|
|
496
496
|
const headLimit = headLimitCoerced === null
|
|
497
497
|
? _grepDefaultHeadLimit()
|
|
@@ -1514,7 +1514,7 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
1514
1514
|
const headLimitRaw = args.head_limit;
|
|
1515
1515
|
const headLimitCoerced = coerceNonNegInt(headLimitRaw);
|
|
1516
1516
|
if (Number.isNaN(headLimitCoerced)) {
|
|
1517
|
-
return `Error: invalid
|
|
1517
|
+
return `Error: invalid limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
|
|
1518
1518
|
}
|
|
1519
1519
|
const headLimit = headLimitCoerced === null
|
|
1520
1520
|
? _globDefaultHeadLimit()
|
|
@@ -7,6 +7,46 @@ export const SMART_BASH_MAX_LINES = 400;
|
|
|
7
7
|
export const SMART_BASH_MAX_BYTES = TOOL_OUTPUT_MAX_BYTES;
|
|
8
8
|
export const SMART_BASH_HEAD_LINES = 80;
|
|
9
9
|
export const SMART_BASH_TAIL_LINES = 80;
|
|
10
|
+
export const BACKGROUND_PARTIAL_OUTPUT_MAX_BYTES = 10 * 1024;
|
|
11
|
+
export const BACKGROUND_PARTIAL_OUTPUT_HEAD_BYTES = 2 * 1024;
|
|
12
|
+
|
|
13
|
+
const BACKGROUND_PARTIAL_TRUNCATION_MARKER =
|
|
14
|
+
'\n\n... [partial output truncated; head and tail shown] ...\n\n';
|
|
15
|
+
|
|
16
|
+
function utf8Prefix(value, maxBytes) {
|
|
17
|
+
const buffer = Buffer.from(String(value ?? ''), 'utf8');
|
|
18
|
+
if (buffer.length <= maxBytes) return buffer.toString('utf8');
|
|
19
|
+
let end = maxBytes;
|
|
20
|
+
while (end > 0 && end < buffer.length && (buffer[end] & 0xC0) === 0x80) end -= 1;
|
|
21
|
+
return buffer.subarray(0, end).toString('utf8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function utf8Suffix(value, maxBytes) {
|
|
25
|
+
const buffer = Buffer.from(String(value ?? ''), 'utf8');
|
|
26
|
+
if (buffer.length <= maxBytes) return buffer.toString('utf8');
|
|
27
|
+
let start = buffer.length - maxBytes;
|
|
28
|
+
while (start < buffer.length && (buffer[start] & 0xC0) === 0x80) start += 1;
|
|
29
|
+
return buffer.subarray(start).toString('utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function renderBackgroundPartialOutput(stdout, stderr) {
|
|
33
|
+
const sections = [];
|
|
34
|
+
const out = String(stdout ?? '');
|
|
35
|
+
const err = String(stderr ?? '');
|
|
36
|
+
if (out) sections.push(`[partial stdout]\n${out}`);
|
|
37
|
+
if (err) sections.push(`[partial stderr]\n${err}`);
|
|
38
|
+
const merged = sections.join('\n\n');
|
|
39
|
+
if (!merged) return '';
|
|
40
|
+
if (Buffer.byteLength(merged, 'utf8') <= BACKGROUND_PARTIAL_OUTPUT_MAX_BYTES) return merged;
|
|
41
|
+
|
|
42
|
+
const markerBytes = Buffer.byteLength(BACKGROUND_PARTIAL_TRUNCATION_MARKER, 'utf8');
|
|
43
|
+
const tailBytes = BACKGROUND_PARTIAL_OUTPUT_MAX_BYTES
|
|
44
|
+
- BACKGROUND_PARTIAL_OUTPUT_HEAD_BYTES
|
|
45
|
+
- markerBytes;
|
|
46
|
+
return utf8Prefix(merged, BACKGROUND_PARTIAL_OUTPUT_HEAD_BYTES)
|
|
47
|
+
+ BACKGROUND_PARTIAL_TRUNCATION_MARKER
|
|
48
|
+
+ utf8Suffix(merged, tailBytes);
|
|
49
|
+
}
|
|
10
50
|
|
|
11
51
|
export function smartMiddleTruncate(content) {
|
|
12
52
|
const s = typeof content === 'string' ? content : String(content ?? '');
|
|
@@ -46,7 +46,6 @@ import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
|
46
46
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
47
47
|
import { resolveOptionalCwd } from './cwd-utils.mjs';
|
|
48
48
|
import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
|
|
49
|
-
import { resolveSessionCwd, stateFilePath, wrapPowerShellWithCwdProbe, wrapBashWithCwdProbe } from '../shell-state.mjs';
|
|
50
49
|
import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
|
|
51
50
|
|
|
52
51
|
// Post-exec drift detection. After a foreground shell command, compare the
|
|
@@ -45,7 +45,7 @@ const APPLY_PATCH_JSON_DESCRIPTION = [
|
|
|
45
45
|
'[file sections]',
|
|
46
46
|
'*** End Patch',
|
|
47
47
|
'Each section starts with exactly one: *** Add File: <path> (+ lines), *** Delete File: <path> (header only), or *** Update File: <path> (optional *** Move to: <new path>).',
|
|
48
|
-
'Hunks start with @@ or @@ <symbol|1-based line>; lines start space, -, or +; optional *** End of File.',
|
|
48
|
+
'Hunks start with @@ or @@ <symbol|1-based line>; lines start space, -, or +; every Update hunk needs >=1 +/- line; optional *** End of File.',
|
|
49
49
|
'Use 3 verbatim context lines from newest output (post-patch body after edits); avoid overlap; stack @@ only if ambiguous.',
|
|
50
50
|
'Project-relative paths; explicit absolute paths only outside the project; + every added line. Never send compacted-history markers; re-read first.',
|
|
51
51
|
].join('\n');
|
|
@@ -5,7 +5,6 @@ const LEGACY_AGENT_ROUTE_SLOTS = Object.freeze({
|
|
|
5
5
|
|
|
6
6
|
const REDUNDANT_WORKFLOW_PRESET_IDS = new Set([
|
|
7
7
|
'workflow-agent',
|
|
8
|
-
'workflow-explorer',
|
|
9
8
|
'workflow-memory',
|
|
10
9
|
]);
|
|
11
10
|
|
|
@@ -65,8 +64,6 @@ export function canonicalizeAgentRoutes(config = {}) {
|
|
|
65
64
|
.find((route) => isCompleteAgentRoute(route));
|
|
66
65
|
if (candidate) agents[id] = candidate;
|
|
67
66
|
}
|
|
68
|
-
delete agents.explore;
|
|
69
|
-
delete agents.explorer;
|
|
70
67
|
delete agents.maintenance;
|
|
71
68
|
// Older settings saves generated one preset per fixed/custom agent. Promote
|
|
72
69
|
// a preset only when no higher-priority canonical/legacy route exists, then
|
|
@@ -92,7 +89,6 @@ export function canonicalizeAgentRouteStorage(config = {}) {
|
|
|
92
89
|
...rest
|
|
93
90
|
} = config || {};
|
|
94
91
|
const maintenance = { ...record(config?.maintenance) };
|
|
95
|
-
delete maintenance.explore;
|
|
96
92
|
delete maintenance.memory;
|
|
97
93
|
const presets = Array.isArray(config?.presets)
|
|
98
94
|
? config.presets.filter((preset) => !isRedundantGeneratedRoutePreset(preset, config?.default))
|
|
@@ -110,7 +106,6 @@ export function agentRouteStorageNeedsMigration(config = {}) {
|
|
|
110
106
|
const maintenance = record(config?.maintenance);
|
|
111
107
|
return Object.prototype.hasOwnProperty.call(config || {}, 'workflowRoutes')
|
|
112
108
|
|| Object.prototype.hasOwnProperty.call(agents, 'maintenance')
|
|
113
|
-
|| Object.prototype.hasOwnProperty.call(maintenance, 'explore')
|
|
114
109
|
|| Object.prototype.hasOwnProperty.call(maintenance, 'memory')
|
|
115
110
|
|| Object.values(agents).some((route) => !isCompleteAgentRoute(route))
|
|
116
111
|
|| (Array.isArray(config?.presets)
|
|
@@ -241,8 +241,16 @@ export function createSettingsApi({
|
|
|
241
241
|
},
|
|
242
242
|
setMemoryToolsEnabled(enabled) {
|
|
243
243
|
const config = getConfig();
|
|
244
|
-
|
|
244
|
+
const memoryEnabled = enabled !== false;
|
|
245
|
+
// General → Memory is the user-facing master: model tools, core-memory
|
|
246
|
+
// injection, and background recap cycles move together.
|
|
247
|
+
const nextConfig = setRecapEnabledInConfig(
|
|
248
|
+
setMemoryToolsEnabledInConfig({ ...config }, memoryEnabled),
|
|
249
|
+
memoryEnabled,
|
|
250
|
+
);
|
|
251
|
+
saveConfigAndAdopt(nextConfig);
|
|
245
252
|
invalidatePreSessionToolSurface();
|
|
253
|
+
invalidateContextStatusCache();
|
|
246
254
|
return this.getToolModuleSettings();
|
|
247
255
|
},
|
|
248
256
|
getChannelSettings(options = {}) {
|
|
@@ -13,7 +13,7 @@ export const TOOL_SEARCH_TOOL = {
|
|
|
13
13
|
openWorldHint: false,
|
|
14
14
|
agentHidden: true,
|
|
15
15
|
},
|
|
16
|
-
description: 'Deferred-tool activation status; direct calls auto-load.',
|
|
16
|
+
description: 'Deferred-tool activation status; direct calls auto-load — no pre-call needed.',
|
|
17
17
|
inputSchema: {
|
|
18
18
|
type: 'object',
|
|
19
19
|
properties: {
|
|
@@ -34,7 +34,7 @@ export const CWD_TOOL = {
|
|
|
34
34
|
openWorldHint: false,
|
|
35
35
|
agentHidden: true,
|
|
36
36
|
},
|
|
37
|
-
description: 'Show or set the session work project for tool execution.',
|
|
37
|
+
description: 'Show or set the session work project for tool execution. Session Cwd is already active; set only to change it.',
|
|
38
38
|
inputSchema: {
|
|
39
39
|
type: 'object',
|
|
40
40
|
properties: {
|
|
@@ -512,6 +512,7 @@ async function main() {
|
|
|
512
512
|
},
|
|
513
513
|
loadProjects: () => import('./projects.mjs'),
|
|
514
514
|
loadSessionStore: () => import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
|
|
515
|
+
loadStatuslineSegments: () => import('../ui/statusline-segments.mjs'),
|
|
515
516
|
loadConfig: () => import('../runtime/shared/config.mjs'),
|
|
516
517
|
loadCommitCompletion: () => import(
|
|
517
518
|
'../runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs'
|
|
@@ -48,7 +48,7 @@ export function createSessionApi(bag) {
|
|
|
48
48
|
|
|
49
49
|
export function createSessionApiA(bag) {
|
|
50
50
|
const {
|
|
51
|
-
runtime, nextId, flags, pending, listeners, getState, getPublishedState = getState, set, flushEmitImmediate, pushItem, patchItem, replaceItems, restoreOlderTranscript, restoreNewerTranscript, settleStreamingTail, clearStreamingTail, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, resetStatsAndSyncContext, drain, flushDeferredExecutionPendingResumeKick, discardExecutionPendingResume,
|
|
51
|
+
runtime, nextId, flags, pending, listeners, getState, getPublishedState = getState, set, flushEmitImmediate, pushItem, patchItem, replaceItems, restoreOlderTranscript, restoreNewerTranscript, settleStreamingTail, clearStreamingTail, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, prioritizeQueued, resetStatsAndSyncContext, drain, flushDeferredExecutionPendingResumeKick, discardExecutionPendingResume,
|
|
52
52
|
} = bag;
|
|
53
53
|
const submission = (text, options = {}) => {
|
|
54
54
|
const t = promptDisplayText(text, options).trim();
|
|
@@ -129,6 +129,7 @@ export function createSessionApiA(bag) {
|
|
|
129
129
|
return String(getState().sessionId || '') === String(id);
|
|
130
130
|
},
|
|
131
131
|
restoreQueued,
|
|
132
|
+
prioritizeQueued,
|
|
132
133
|
// Claude Code's message selector ("jump back to a previous message"):
|
|
133
134
|
// rewind the conversation to just before a user prompt and hand its text
|
|
134
135
|
// back for editing. Idle-only — a live turn must be interrupted first.
|
|
@@ -571,6 +571,30 @@ export function createSessionFlow(bag) {
|
|
|
571
571
|
};
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
+
// Claude Code `now` parity: promote one visible queued prompt ahead of its
|
|
575
|
+
// siblings. The desktop follows this configure call with the normal abort
|
|
576
|
+
// lane, so interruption keeps the existing recovery/requeue guarantees.
|
|
577
|
+
function prioritizeQueued(selectedId = '') {
|
|
578
|
+
const targetId = String(selectedId || '').trim();
|
|
579
|
+
if (!targetId) return { count: 0, ids: [], priority: 'now' };
|
|
580
|
+
const index = pending.findIndex((entry) =>
|
|
581
|
+
isQueuedEntryEditable(entry)
|
|
582
|
+
&& !isSlashQueuedEntry(entry)
|
|
583
|
+
&& String(entry.id || '') === targetId);
|
|
584
|
+
if (index < 0) return { count: 0, ids: [], priority: 'now' };
|
|
585
|
+
const [entry] = pending.splice(index, 1);
|
|
586
|
+
entry.priority = 'now';
|
|
587
|
+
pending.unshift(entry);
|
|
588
|
+
const visible = getState().queued.map((queuedEntry) =>
|
|
589
|
+
String(queuedEntry?.id || '') === targetId
|
|
590
|
+
? { ...queuedEntry, priority: 'now' }
|
|
591
|
+
: queuedEntry);
|
|
592
|
+
set({ queued: visible });
|
|
593
|
+
flushEmitImmediate?.();
|
|
594
|
+
void drain();
|
|
595
|
+
return { count: 1, ids: [targetId], priority: 'now' };
|
|
596
|
+
}
|
|
597
|
+
|
|
574
598
|
const resetStats = () => {
|
|
575
599
|
const stats = createSessionStats();
|
|
576
600
|
set({ stats });
|
|
@@ -695,5 +719,5 @@ export function createSessionFlow(bag) {
|
|
|
695
719
|
return getState().stats;
|
|
696
720
|
};
|
|
697
721
|
|
|
698
|
-
return { leadSessionId, shouldMirrorSteeringEntry, commitSteeringQueueEntries, makeQueueEntry, removeQueuedEntries, requeueEntriesFront, dequeueQueueBatch, drain, enqueue, drainPendingSteering, restoreLeadSteeringFromDisk, autoClearBeforeSubmit, performSessionClear, restoreQueued, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext };
|
|
722
|
+
return { leadSessionId, shouldMirrorSteeringEntry, commitSteeringQueueEntries, makeQueueEntry, removeQueuedEntries, requeueEntriesFront, dequeueQueueBatch, drain, enqueue, drainPendingSteering, restoreLeadSteeringFromDisk, autoClearBeforeSubmit, performSessionClear, restoreQueued, prioritizeQueued, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext };
|
|
699
723
|
}
|
|
@@ -21,8 +21,13 @@ const SHELL_JOBS_SEGMENT_CACHE_MS = 1000;
|
|
|
21
21
|
// Session buckets ride along with the owner-wide totals: one host process can
|
|
22
22
|
// own many sessions (the desktop pools every pane's engine), so a pane must be
|
|
23
23
|
// able to ask for ITS OWN jobs instead of the process aggregate.
|
|
24
|
-
const EMPTY_SHELL_JOBS_SESSION = Object.freeze({ count: 0, elapsedLabel: '' });
|
|
25
|
-
const EMPTY_SHELL_JOBS = Object.freeze({
|
|
24
|
+
const EMPTY_SHELL_JOBS_SESSION = Object.freeze({ count: 0, elapsedLabel: '', jobs: Object.freeze([]) });
|
|
25
|
+
const EMPTY_SHELL_JOBS = Object.freeze({
|
|
26
|
+
count: 0,
|
|
27
|
+
elapsedLabel: '',
|
|
28
|
+
jobs: Object.freeze([]),
|
|
29
|
+
sessions: Object.freeze({}),
|
|
30
|
+
});
|
|
26
31
|
|
|
27
32
|
let _shellJobsSegmentCache = { ownerPid: 0, at: 0, value: EMPTY_SHELL_JOBS };
|
|
28
33
|
let _shellJobsRefreshInFlight = false;
|
|
@@ -136,7 +141,8 @@ async function refreshShellJobsStatus(ownerPid) {
|
|
|
136
141
|
.slice(0, 30);
|
|
137
142
|
let count = 0;
|
|
138
143
|
let oldestMs = Infinity;
|
|
139
|
-
|
|
144
|
+
const jobs = [];
|
|
145
|
+
// sessionId -> { count, oldestMs, jobs }. Jobs with no session stamp (legacy
|
|
140
146
|
// records, plain CLI runs) still count toward the owner total but belong to
|
|
141
147
|
// no pane, so they never light up a session's indicator.
|
|
142
148
|
const bySession = new Map();
|
|
@@ -153,10 +159,18 @@ async function refreshShellJobsStatus(ownerPid) {
|
|
|
153
159
|
if (st.mtimeMs < oldestMs) oldestMs = st.mtimeMs;
|
|
154
160
|
} catch {}
|
|
155
161
|
const owner = String(detail?.ownerSessionId ?? '').trim();
|
|
162
|
+
const job = {
|
|
163
|
+
taskId: id,
|
|
164
|
+
command: String(detail?.command || '').trim(),
|
|
165
|
+
cwd: String(detail?.cwd || '').trim(),
|
|
166
|
+
startedAt: detail?.startedAt || (Number.isFinite(jobMs) ? jobMs : null),
|
|
167
|
+
};
|
|
168
|
+
jobs.push(job);
|
|
156
169
|
if (!owner) continue;
|
|
157
|
-
const bucket = bySession.get(owner) || { count: 0, oldestMs: Infinity };
|
|
170
|
+
const bucket = bySession.get(owner) || { count: 0, oldestMs: Infinity, jobs: [] };
|
|
158
171
|
bucket.count += 1;
|
|
159
172
|
if (jobMs < bucket.oldestMs) bucket.oldestMs = jobMs;
|
|
173
|
+
bucket.jobs.push(job);
|
|
160
174
|
bySession.set(owner, bucket);
|
|
161
175
|
}
|
|
162
176
|
if (count) {
|
|
@@ -167,9 +181,10 @@ async function refreshShellJobsStatus(ownerPid) {
|
|
|
167
181
|
sessions[owner] = {
|
|
168
182
|
count: bucket.count,
|
|
169
183
|
elapsedLabel: Number.isFinite(bucket.oldestMs) ? formatElapsed(now - bucket.oldestMs) : '',
|
|
184
|
+
jobs: bucket.jobs,
|
|
170
185
|
};
|
|
171
186
|
}
|
|
172
|
-
value = { count, elapsedLabel, sessions };
|
|
187
|
+
value = { count, elapsedLabel, jobs, sessions };
|
|
173
188
|
}
|
|
174
189
|
} catch {
|
|
175
190
|
value = EMPTY_SHELL_JOBS;
|
package/scripts/run-suite.mjs
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Named test suites, so package.json keeps one entry per suite instead of a
|
|
3
|
-
// 2KB command line. Files listed here are RUN; anything under scripts/ that is
|
|
4
|
-
// not in a suite (or another npm script) is dead weight by definition.
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
|
|
11
|
-
// contract: the cheap, always-true invariants (tool args, session/steering
|
|
12
|
-
// persistence, memory rules, routing sanitizers). Live-model, UI-frame and
|
|
13
|
-
// bench suites deliberately stay out — they belong to smoke:*/bench:*.
|
|
14
|
-
export const SUITES = {
|
|
15
|
-
contract: [
|
|
16
|
-
'abort-queued-drain-kick-test.mjs',
|
|
17
|
-
'agent-dispatch-abort-compose-test.mjs',
|
|
18
|
-
'agent-loop-policy-test.mjs',
|
|
19
|
-
'agent-trace-io-test.mjs',
|
|
20
|
-
'anthropic-admission-retry-integration-test.mjs',
|
|
21
|
-
'anthropic-maxtokens-test.mjs',
|
|
22
|
-
'arg-guard-test.mjs',
|
|
23
|
-
'async-notify-settlement-test.mjs',
|
|
24
|
-
'background-task-meta-smoke.mjs',
|
|
25
|
-
'dead-owner-attach-test.mjs',
|
|
26
|
-
'debounced-skills-async-save-test.mjs',
|
|
27
|
-
'dispatch-persist-recovery-test.mjs',
|
|
28
|
-
'find-fuzzy-hidden-test.mjs',
|
|
29
|
-
'ingest-pure-conversation-smoke.mjs',
|
|
30
|
-
'internal-tools-normalization-test.mjs',
|
|
31
|
-
'legacy-config-cleanup-test.mjs',
|
|
32
|
-
'lifecycle-api-test.mjs',
|
|
33
|
-
'live-share-test.mjs',
|
|
34
|
-
'max-output-recovery-persist-test.mjs',
|
|
35
|
-
'mcp-client-normalization-test.mjs',
|
|
36
|
-
'mcp-grace-deferred-test.mjs',
|
|
37
|
-
'memory-core-input-test.mjs',
|
|
38
|
-
'memory-meta-concurrency-test.mjs',
|
|
39
|
-
'memory-retention-test.mjs',
|
|
40
|
-
'memory-rule-contract-test.mjs',
|
|
41
|
-
'memory-worker-stability-test.mjs',
|
|
42
|
-
'model-list-sanitize-test.mjs',
|
|
43
|
-
'notify-completion-mirror-test.mjs',
|
|
44
|
-
'openai-oauth-refresh-race-test.mjs',
|
|
45
|
-
'openai-ws-early-settle-test.mjs',
|
|
46
|
-
'parent-abort-link-test.mjs',
|
|
47
|
-
'path-suffix-test.mjs',
|
|
48
|
-
'pending-completion-drop-test.mjs',
|
|
49
|
-
'pending-messages-lock-nonblocking-test.mjs',
|
|
50
|
-
'pretool-ask-runtime-test.mjs',
|
|
51
|
-
'prompt-input-parity-test.mjs',
|
|
52
|
-
'reactive-compact-persist-smoke.mjs',
|
|
53
|
-
'reasoning-replay-policy-test.mjs',
|
|
54
|
-
'repl-stream-finalize-test.mjs',
|
|
55
|
-
'result-classification-test.mjs',
|
|
56
|
-
'rg-runner-test.mjs',
|
|
57
|
-
'sanitize-tool-pairs-test.mjs',
|
|
58
|
-
'save-worker-delta-test.mjs',
|
|
59
|
-
'session-ingest-smoke.mjs',
|
|
60
|
-
'session-title-controller-test.mjs',
|
|
61
|
-
'set-effort-config-test.mjs',
|
|
62
|
-
'shell-jobs-windows-hide-test.mjs',
|
|
63
|
-
'spawn-ws-prewarm-test.mjs',
|
|
64
|
-
'spinner-meta-test.mjs',
|
|
65
|
-
'statusline-agents-test.mjs',
|
|
66
|
-
'statusline-quota-hysteresis-test.mjs',
|
|
67
|
-
'steering-fold-provenance-test.mjs',
|
|
68
|
-
'steering-persist-orphan-prune-test.mjs',
|
|
69
|
-
'stop-hook-informational-exit1-test.mjs',
|
|
70
|
-
'stream-stall-budget-test.mjs',
|
|
71
|
-
'title-completion-test.mjs',
|
|
72
|
-
'tool-output-budget-test.mjs',
|
|
73
|
-
'tool-result-hook-test.mjs',
|
|
74
|
-
'turn-snapshot-test.mjs',
|
|
75
|
-
'usage-metrics-epoch-smoke.mjs',
|
|
76
|
-
'web-fetch-routing-test.mjs',
|
|
77
|
-
'webhook-smoke.mjs',
|
|
78
|
-
'worker-notify-rejection-test.mjs',
|
|
79
|
-
'write-backpressure-test.mjs',
|
|
80
|
-
],
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const name = process.argv[2];
|
|
84
|
-
const files = SUITES[name];
|
|
85
|
-
if (!files) {
|
|
86
|
-
process.stderr.write(`unknown suite: ${name}. known: ${Object.keys(SUITES).join(', ')}
|
|
87
|
-
`);
|
|
88
|
-
process.exit(2);
|
|
89
|
-
}
|
|
90
|
-
// Bounded concurrency: the default (one worker per core) ran ~60 node processes
|
|
91
|
-
// at once, which spiked memory and made lock-contending suites (OAuth keychain,
|
|
92
|
-
// config RMW) fail from load rather than from a real regression.
|
|
93
|
-
const concurrency = Number(process.env.MIXDOG_SUITE_CONCURRENCY) > 0
|
|
94
|
-
? Math.floor(Number(process.env.MIXDOG_SUITE_CONCURRENCY))
|
|
95
|
-
: 4;
|
|
96
|
-
const result = spawnSync(
|
|
97
|
-
process.execPath,
|
|
98
|
-
['--test', `--test-concurrency=${concurrency}`, ...files.map((f) => join(here, f))],
|
|
99
|
-
{ stdio: 'inherit' },
|
|
100
|
-
);
|
|
101
|
-
process.exit(result.status ?? 1);
|