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.
Files changed (31) hide show
  1. package/package.json +10 -58
  2. package/src/rules/shared/01-tool.md +15 -6
  3. package/src/runtime/agent/orchestrator/config.mjs +0 -1
  4. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  5. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +3 -1
  6. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +11 -1
  7. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -2
  8. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +74 -3
  9. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +26 -7
  10. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -1
  11. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  12. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -5
  13. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +7 -7
  14. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +14 -30
  15. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
  16. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +3 -3
  18. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +2 -2
  19. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +40 -0
  20. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +0 -1
  21. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  22. package/src/runtime/shared/agent-route-config.mjs +0 -5
  23. package/src/session-runtime/settings-api.mjs +9 -1
  24. package/src/session-runtime/tool-defs.mjs +2 -2
  25. package/src/standalone/daemon.mjs +1 -0
  26. package/src/standalone/session-protocol.mjs +1 -0
  27. package/src/tui/session/session-api.mjs +2 -1
  28. package/src/tui/session/session-flow.mjs +25 -1
  29. package/src/ui/statusline-segments.mjs +20 -5
  30. package/scripts/run-suite.mjs +0 -101
  31. 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 { smartMiddleTruncate } from './shell-output.mjs';
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
- // Session cwd carry-over (no live shell): when the model
260
- // passes an explicit cwd it wins and updates the store on the next probe;
261
- // otherwise reuse the last stored cwd for this session if it still exists.
262
- const _hasExplicitCwd = typeof requestedCwd === 'string' && requestedCwd.trim() !== '';
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: syncCommand,
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 partialStdout = smartMiddleTruncate(stripAnsi(result.stdout || ''));
769
- const partialStderr = stripAnsi(result.stderr || '');
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
- partialStdout ? `\n[partial stdout]\n${partialStdout}` : '',
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 for current directory; use a project-relative subdir or explicit external path.' },
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 output_mode:'count' for the exact total on ${scopePath}); pass offset:${offset + shown} for more]`)
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
- `... [head_limit ${totalHeadLimit} exhausted across query[]; `
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/head_limit]`,
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 head_limit for more)`);
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 head_limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
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 head_limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
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
- saveConfigAndAdopt(setMemoryToolsEnabledInConfig({ ...config }, enabled !== false));
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'
@@ -102,6 +102,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
102
102
  'removePlugin',
103
103
  'renameSessionTitle',
104
104
  'resizeImage',
105
+ 'prioritizeQueued',
105
106
  'restoreNewerTranscript',
106
107
  'restoreOlderTranscript',
107
108
  'restoreQueued',
@@ -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({ count: 0, elapsedLabel: '', sessions: 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
- // sessionId -> { count, oldestMs }. Jobs with no session stamp (legacy
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;
@@ -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);