mixdog 0.9.130 → 0.9.131

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.130",
3
+ "version": "0.9.131",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { appendFile, readFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ function payloadJobs(payload) {
8
+ const pages = Array.isArray(payload) ? payload : [payload];
9
+ return pages.flatMap((page) => Array.isArray(page?.jobs) ? page.jobs : []);
10
+ }
11
+
12
+ function elapsedSeconds(startedAt, completedAt) {
13
+ const start = Date.parse(startedAt || '');
14
+ const end = Date.parse(completedAt || '');
15
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
16
+ return Math.round((end - start) / 1000);
17
+ }
18
+
19
+ function timingRows(payload) {
20
+ return payloadJobs(payload).flatMap((job) => (job.steps || []).flatMap((step) => {
21
+ const seconds = elapsedSeconds(step.started_at, step.completed_at);
22
+ if (seconds === null) return [];
23
+ return [{
24
+ key: `${job.name} / ${step.name}`,
25
+ job: job.name,
26
+ step: step.name,
27
+ seconds,
28
+ }];
29
+ }));
30
+ }
31
+
32
+ function workflowSeconds(payload) {
33
+ const jobs = payloadJobs(payload);
34
+ const starts = jobs.map((job) => Date.parse(job.started_at || '')).filter(Number.isFinite);
35
+ const ends = jobs.map((job) => Date.parse(job.completed_at || '')).filter(Number.isFinite);
36
+ if (starts.length === 0 || ends.length === 0) return null;
37
+ return Math.round((Math.max(...ends) - Math.min(...starts)) / 1000);
38
+ }
39
+
40
+ function percentChange(current, baseline) {
41
+ if (!(baseline > 0)) return null;
42
+ return Math.round(((current - baseline) / baseline) * 100);
43
+ }
44
+
45
+ export function buildReleaseTimingReport(currentPayload, baselinePayload = []) {
46
+ const currentRows = timingRows(currentPayload);
47
+ const baselineRows = timingRows(baselinePayload);
48
+ const baselineByKey = new Map(baselineRows.map((row) => [row.key, row]));
49
+ const regressions = currentRows.flatMap((row) => {
50
+ const baseline = baselineByKey.get(row.key);
51
+ if (!baseline) return [];
52
+ const percent = percentChange(row.seconds, baseline.seconds);
53
+ if (percent === null || percent <= 10 || row.seconds - baseline.seconds < 15) return [];
54
+ return [{ ...row, baselineSeconds: baseline.seconds, percent }];
55
+ }).sort((a, b) => b.percent - a.percent || b.seconds - a.seconds);
56
+
57
+ const currentWorkflowSeconds = workflowSeconds(currentPayload);
58
+ const baselineWorkflowSeconds = workflowSeconds(baselinePayload);
59
+ const workflowPercent = currentWorkflowSeconds !== null && baselineWorkflowSeconds !== null
60
+ ? percentChange(currentWorkflowSeconds, baselineWorkflowSeconds)
61
+ : null;
62
+ const slowest = [...currentRows].sort((a, b) => b.seconds - a.seconds).slice(0, 15);
63
+ const markdown = [
64
+ '## Release timing',
65
+ '',
66
+ `- Current workflow span: ${currentWorkflowSeconds ?? 'n/a'}s`,
67
+ `- Previous successful span: ${baselineWorkflowSeconds ?? 'n/a'}s`,
68
+ `- Material step regressions (>10% and ≥15s): ${regressions.length}`,
69
+ '',
70
+ '| Slowest step | Current | Previous | Change |',
71
+ '| --- | ---: | ---: | ---: |',
72
+ ...slowest.map((row) => {
73
+ const baseline = baselineByKey.get(row.key);
74
+ const change = baseline ? percentChange(row.seconds, baseline.seconds) : null;
75
+ return `| ${row.key.replaceAll('|', '\\|')} | ${row.seconds}s | ${baseline ? `${baseline.seconds}s` : 'n/a'} | ${change === null ? 'n/a' : `${change > 0 ? '+' : ''}${change}%`} |`;
76
+ }),
77
+ '',
78
+ ].join('\n');
79
+
80
+ return {
81
+ currentWorkflowSeconds,
82
+ baselineWorkflowSeconds,
83
+ workflowPercent,
84
+ regressions,
85
+ markdown,
86
+ };
87
+ }
88
+
89
+ function commandValue(value) {
90
+ return String(value).replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A');
91
+ }
92
+
93
+ const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
94
+ if (invokedPath === import.meta.url) {
95
+ const currentPath = process.argv[2];
96
+ const baselinePath = process.argv[3];
97
+ if (!currentPath) throw new Error('Usage: release-timing-report.mjs <current-jobs.json> [baseline-jobs.json]');
98
+ const current = JSON.parse(await readFile(currentPath, 'utf8'));
99
+ const baseline = baselinePath ? JSON.parse(await readFile(baselinePath, 'utf8')) : [];
100
+ const report = buildReleaseTimingReport(current, baseline);
101
+ if (process.env.GITHUB_STEP_SUMMARY) {
102
+ await appendFile(process.env.GITHUB_STEP_SUMMARY, `${report.markdown}\n`);
103
+ }
104
+ if (report.workflowPercent > 10
105
+ && report.currentWorkflowSeconds - report.baselineWorkflowSeconds >= 30) {
106
+ console.log(`::warning title=Release duration regression::Workflow span increased ${report.workflowPercent}% to ${report.currentWorkflowSeconds}s`);
107
+ }
108
+ for (const row of report.regressions) {
109
+ console.log(`::warning title=Release step regression::${commandValue(row.key)} increased ${row.percent}% (${row.baselineSeconds}s to ${row.seconds}s)`);
110
+ }
111
+ console.log(`Release timing recorded: ${report.currentWorkflowSeconds ?? 'n/a'}s, ${report.regressions.length} material step regressions.`);
112
+ }
@@ -62,17 +62,16 @@
62
62
  execution, runtime/state operations, calculations, data transformation, file
63
63
  generation, or formats unsupported by file tools. Do not use `shell` instead
64
64
  of an available file tool for ordinary file-content inspection.
65
- - Start shell commands in the foreground. Set `run_in_background:true` only when their result is not needed
66
- immediately and completion notification is sufficient. Tracked sync/async
67
- commands belong to the current run. Only when the request explicitly requires
65
+ - Shell commands start in the foreground. If still running after 10 seconds,
66
+ the call returns a tracked `task_id` and completion arrives by notification.
67
+ Only when the request explicitly requires
68
68
  a service to survive after the run exits, detach it at shell level (for
69
69
  example, `nohup ... &`); never detach ordinary jobs merely to avoid tracking.
70
70
  A sync call may return a `task_id` and partial output after its blocking
71
71
  budget. Do not poll: completion resumes automatically. When intermediate output must drive
72
- decisions or the user requests monitoring, use `task check_after` with an
73
- explicit `after_ms` to schedule one non-blocking progress snapshot: normally
74
- 10–30s for active progress and 30–60s for long downloads/builds. Schedule
75
- another only after inspecting that snapshot; completion cancels a pending
76
- check.
72
+ decisions or the user explicitly requests monitoring, call `task read` once
73
+ to return the current status and output snapshot. If it is still running,
74
+ await the completion notification; do not
75
+ call `task` again unless the user explicitly asks for another snapshot.
77
76
  Omit timeout by default, including for long jobs; set it only for a real total
78
77
  deadline, since it kills even async jobs.
@@ -138,8 +138,8 @@ const TOOL_ARG_KEYS = {
138
138
  recall: ['query', 'limit', 'session_id', 'cwd'],
139
139
  search: ['query', 'limit', 'cwd'],
140
140
  code_graph: ['mode', 'file', 'files', 'symbol', 'symbols', 'body', 'language', 'limit', 'depth', 'page', 'cwd'],
141
- shell: ['command', 'cwd', 'timeout', 'mode', 'run_in_background', 'persistent', 'session_id'],
142
- task: ['task_id', 'action', 'after_ms'],
141
+ shell: ['command', 'timeout_ms'],
142
+ task: ['action', 'task_id'],
143
143
  edit: ['path', 'replace_all', 'edits'],
144
144
  edit_many: ['edits'],
145
145
  write: ['path'],
@@ -633,10 +633,10 @@ function guardRead(a) {
633
633
  }
634
634
 
635
635
  function guardShell(a) {
636
- const allowed = new Set(['command', 'timeout_ms', 'run_in_background']);
636
+ const allowed = new Set(['command', 'timeout_ms']);
637
637
  const unsupported = Object.keys(a).find((key) => !allowed.has(key));
638
638
  if (unsupported) {
639
- return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, and run_in_background`;
639
+ return `Error: shell arg "${unsupported}" is unsupported; use only command and timeout_ms`;
640
640
  }
641
641
  if (!hasOwn(a, 'command')) {
642
642
  return 'Error: shell requires "command"';
@@ -650,18 +650,18 @@ function guardShell(a) {
650
650
  if (hasOwn(a, 'timeout_ms') && (typeof a.timeout_ms !== 'number' || !Number.isFinite(a.timeout_ms) || a.timeout_ms < 0)) {
651
651
  return `Error: shell arg "timeout_ms" must be a non-negative number (got ${describeType(a.timeout_ms)})`;
652
652
  }
653
- if (hasOwn(a, 'run_in_background') && typeof a.run_in_background !== 'boolean') {
654
- return `Error: shell arg "run_in_background" must be a boolean (got ${describeType(a.run_in_background)})`;
655
- }
656
653
  return null;
657
654
  }
658
655
 
659
656
  function guardTask(a) {
660
657
  const action = typeof a.action === 'string'
661
658
  ? a.action.trim().toLowerCase()
662
- : (hasOwn(a, 'action') ? a.action : (hasOwn(a, 'task_id') ? 'status' : 'list'));
663
- if (hasOwn(a, 'action') && !['list', 'status', 'read', 'check_after', 'cancel'].includes(action)) {
664
- return `Error: task arg "action" must be one of list|status|read|check_after|cancel (got ${JSON.stringify(a.action)})`;
659
+ : (hasOwn(a, 'action') ? a.action : '');
660
+ if (!hasOwn(a, 'action')) {
661
+ return 'Error: task requires explicit "action"';
662
+ }
663
+ if (!['list', 'read', 'cancel'].includes(action)) {
664
+ return `Error: task arg "action" must be one of list|read|cancel (got ${JSON.stringify(a.action)})`;
665
665
  }
666
666
  if (action === 'list') return null;
667
667
  if (!hasOwn(a, 'task_id')) {
@@ -670,12 +670,6 @@ function guardTask(a) {
670
670
  if (typeof a.task_id !== 'string' || a.task_id.trim().length === 0) {
671
671
  return `Error: task arg "task_id" must be a non-empty string (got ${describeType(a.task_id)})`;
672
672
  }
673
- if (action === 'check_after') {
674
- if (!hasOwn(a, 'after_ms')) {
675
- return 'Error: task action "check_after" requires explicit "after_ms"';
676
- }
677
- return checkIntInRange(a, 'after_ms', 1, 2_147_483_647);
678
- }
679
673
  return null;
680
674
  }
681
675
 
@@ -5,7 +5,7 @@ import { delimiter as pathDelimiter } from 'node:path';
5
5
  import { join as pathJoin } from 'node:path';
6
6
  import { isLegitimateShellExit } from '../../session/result-classification.mjs';
7
7
  import { makeToolEnvelope } from '../../session/tool-envelope.mjs';
8
- import { acquireShellLeaseBounded, execShellCommand, stripAnsi } from '../shell-command.mjs';
8
+ import { execShellCommand, stripAnsi } from '../shell-command.mjs';
9
9
  import { wrapCommandWithSnapshot } from '../shell-snapshot.mjs';
10
10
  import { getDestructiveCommandWarning } from '../destructive-warning.mjs';
11
11
  import { maybeRewriteWmicProcessCommand } from '../shell-policy.mjs';
@@ -13,7 +13,6 @@ import { buildBashPolicyScanTargets, checkExecPolicyMessage } from '../bash-poli
13
13
  import { markCodeGraphDirtyPaths, drainCodeGraphCache } from '../code-graph-state.mjs';
14
14
  import {
15
15
  buildJobNotFoundMessage,
16
- startBackgroundShellJob,
17
16
  waitForShellJob,
18
17
  peekShellJob,
19
18
  killShellJob,
@@ -23,16 +22,13 @@ import {
23
22
  endShellJobWait,
24
23
  clearShellJobNotifyCtx,
25
24
  shellJobPublicTaskResult,
26
- attachShellJobResourceLease,
27
25
  } from './shell-jobs.mjs';
28
26
  import {
29
27
  analyzeShellCommandEffects,
30
28
  buildPowerShellFilterTeePlan,
31
29
  consumeFilterTeeCapture,
32
30
  extractShellApplyPatchInvocation,
33
- foregroundLongCommandHint,
34
31
  hasPowerShellOnlySyntax,
35
- isAutobackgroundingAllowed,
36
32
  planInlineScriptHoist,
37
33
  preflightPowerShellHygiene,
38
34
  shellSplitSegments,
@@ -41,7 +37,6 @@ import {
41
37
  stripShellProbeWrappers,
42
38
  } from './shell-analysis.mjs';
43
39
  import {
44
- cancelBackgroundTask,
45
40
  completeBackgroundTask,
46
41
  getBackgroundTask,
47
42
  registerBackgroundTask,
@@ -62,16 +57,15 @@ import { normalizeOutputPath } from './path-utils.mjs';
62
57
  import { normalizeErrorMessage } from './path-diagnostics.mjs';
63
58
  import { invalidateBuiltinResultCache } from './cache-layers.mjs';
64
59
  import { applyShellEgressPolicy, scrubLoaderVars, scrubProviderSecrets, scrubRuntimeRootVars } from '../env-scrub.mjs';
65
- import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
66
60
  import {
67
61
  findPathExecutable,
68
62
  SHELL_RUNTIME_CANDIDATES,
69
63
  } from './runtime-capabilities.mjs';
70
64
  import { planDirectExeSpawn } from './shell-direct-exe.mjs';
71
65
 
72
- // Claude Code assistant-mode parity: short foreground commands stay inline;
73
- // only work still running after the 15 s coordination budget is promoted.
74
- export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 15_000;
66
+ // Commands start in the foreground. Only work still running after the
67
+ // 10 s coordination budget is promoted to a tracked background task.
68
+ export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 10_000;
75
69
 
76
70
  // Post-exec drift detection. After a foreground shell command, compare the
77
71
  // live mtime+size of files mixdog has already read this session against their
@@ -351,8 +345,6 @@ export async function executeBashTool(args, workDir, options = {}) {
351
345
  // never creates a second session cwd authority beside the dedicated cwd tool.
352
346
  const bashWorkDir = workDir;
353
347
  const _readStateScope = options?.readStateScope ?? options?.sessionId ?? null;
354
- let runInBackground = args?.run_in_background === true;
355
-
356
348
  // Run hard-block policy before any shell dispatch.
357
349
  const _rawCmd = String(args && args.command != null ? args.command : '');
358
350
  // `apply_patch` typed into the shell (heredoc/argument/bare
@@ -428,25 +420,22 @@ export async function executeBashTool(args, workDir, options = {}) {
428
420
  if (_execPolicyBlock) {
429
421
  return formatShellToolFailure(_execPolicyBlock);
430
422
  }
431
- // Inline-script hoisting (sync runs only a background job would outlive
432
- // the temp file). The body is written verbatim and the invocation becomes a
423
+ // Inline-script hoisting. The body is written verbatim and the invocation becomes a
433
424
  // file run, so the host shell never has to carry the script through its
434
425
  // quoting layer. planInlineScriptHoist refuses every case where file
435
426
  // semantics would differ, so this is a transport change only.
436
427
  let _inlineHoistPath = null;
437
- if (!runInBackground) {
438
- const hoist = planInlineScriptHoist(command);
439
- if (hoist) {
440
- try {
441
- const file = pathJoin(
442
- tmpdir(),
443
- `mixdog-inline-${process.pid}-${Date.now().toString(36)}${hoist.extension}`,
444
- );
445
- writeFileSync(file, hoist.body, 'utf8');
446
- _inlineHoistPath = file;
447
- command = hoist.replace(file.replace(/\\/g, '/'));
448
- } catch { _inlineHoistPath = null; }
449
- }
428
+ const hoist = planInlineScriptHoist(command);
429
+ if (hoist) {
430
+ try {
431
+ const file = pathJoin(
432
+ tmpdir(),
433
+ `mixdog-inline-${process.pid}-${Date.now().toString(36)}${hoist.extension}`,
434
+ );
435
+ writeFileSync(file, hoist.body, 'utf8');
436
+ _inlineHoistPath = file;
437
+ command = hoist.replace(file.replace(/\\/g, '/'));
438
+ } catch { _inlineHoistPath = null; }
450
439
  }
451
440
 
452
441
  const _bgTasksDisabled = /^(1|true|yes|on)$/i.test(
@@ -468,11 +457,6 @@ export async function executeBashTool(args, workDir, options = {}) {
468
457
  // bound the blocking window when timeout promotion is available.
469
458
  const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
470
459
  const DEFAULT_BASH_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
471
- // Background (async / run_in_background) jobs get NO omitted default: 0
472
- // means "unlimited" and flows unchanged through startBackgroundShellJob →
473
- // task meta (detail.timeoutMs 0). An explicit args.timeout_ms is still honored
474
- // and enforced exactly as before. Sync path keeps the 120s omitted default.
475
- const DEFAULT_BACKGROUND_BASH_TIMEOUT_MS = 0;
476
460
  const _envMaxTimeout = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
477
461
  // Foreground blocking cap when timeout promotion is available. 600s let a
478
462
  // caller-supplied 10-15 min timeout hold the conversation synchronously
@@ -481,14 +465,10 @@ export async function executeBashTool(args, workDir, options = {}) {
481
465
  // detaches as a tracked job with the REMAINDER of the explicit timeout as
482
466
  // its background deadline (user decision: 2 minutes).
483
467
  const MAX_BASH_TIMEOUT_MS = Math.max(_envMaxTimeout > 0 ? _envMaxTimeout : 120_000, DEFAULT_BASH_TIMEOUT_MS);
484
- const defaultTimeoutMs = runInBackground
485
- ? DEFAULT_BACKGROUND_BASH_TIMEOUT_MS
486
- : DEFAULT_BASH_TIMEOUT_MS;
468
+ const defaultTimeoutMs = DEFAULT_BASH_TIMEOUT_MS;
487
469
  const hasExplicitTimeout = typeof args.timeout_ms === 'number' && args.timeout_ms > 0;
488
470
  const timeoutMs = hasExplicitTimeout ? args.timeout_ms : defaultTimeoutMs;
489
- const backgroundOnTimeout = !runInBackground
490
- && !_bgTasksDisabled
491
- && isAutobackgroundingAllowed(command, resolvedSpec.shellType);
471
+ const backgroundOnTimeout = !_bgTasksDisabled;
492
472
  // Explicit caller timeout remains the total deadline. When promotion is
493
473
  // available, cap only its foreground blocking portion at MAX.
494
474
  // JS timers (setTimeout) and PS WaitForExit(ms) are 32-bit: a delay above
@@ -514,19 +494,11 @@ export async function executeBashTool(args, workDir, options = {}) {
514
494
  const promoteAtTimeout = backgroundOnTimeout
515
495
  && (!hasExplicitTimeout || promotedTimeoutMs > 0);
516
496
  const mergeStderr = true;
517
- const longForegroundHint = foregroundLongCommandHint(
518
- command,
519
- timeout,
520
- { run_in_background: runInBackground },
521
- { backgroundTasksDisabled: _bgTasksDisabled },
522
- );
523
- if (longForegroundHint) return formatShellToolFailure(longForegroundHint);
524
497
  // Main-agent blocking budget. A timeout is the command's total deadline,
525
498
  // not permission to hold the conversation open for that whole duration:
526
- // after 15 s a still-running command becomes a tracked background task and
499
+ // after 10 s a still-running command becomes a tracked background task and
527
500
  // completion is pushed to the owner. Explicit timeouts keep their remaining
528
- // deadline after promotion. Never applies to run_in_background (already
529
- // detached) or commands barred from backgrounding.
501
+ // deadline after promotion.
530
502
  // MIXDOG_SHELL_AUTO_BACKGROUND_MS overrides; an explicit 0 disables.
531
503
  const _autoBgEnvRaw = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
532
504
  const _autoBgEnvMs = Number(_autoBgEnvRaw);
@@ -534,10 +506,7 @@ export async function executeBashTool(args, workDir, options = {}) {
534
506
  && Number.isFinite(_autoBgEnvMs) && _autoBgEnvMs >= 0)
535
507
  ? Math.floor(_autoBgEnvMs)
536
508
  : DEFAULT_SHELL_AUTO_BACKGROUND_MS;
537
- // Gate on backgroundOnTimeout rather than just `runInBackground`: it already
538
- // encodes "promotion is available for this command" (not detached, background
539
- // tasks enabled, command shape allows it), so the soft threshold can never
540
- // promote something the hard timeout would refuse to.
509
+ // Gate on backgroundOnTimeout so disabled background tasks remain foreground.
541
510
  const autoBackgroundMs = (!backgroundOnTimeout || DEFAULT_AUTO_BACKGROUND_MS <= 0)
542
511
  ? 0
543
512
  : Math.min(DEFAULT_AUTO_BACKGROUND_MS, timeout);
@@ -556,14 +525,12 @@ export async function executeBashTool(args, workDir, options = {}) {
556
525
  let execShellArg = shellArg;
557
526
  let execShellArgs = shellArgs;
558
527
  let directArgv = null;
559
- const directPlan = !runInBackground
560
- ? planDirectExeSpawn(command, {
561
- shellType,
562
- cwd: bashWorkDir,
563
- pathValue: spawnEnv.PATH,
564
- env: spawnEnv,
565
- })
566
- : null;
528
+ const directPlan = planDirectExeSpawn(command, {
529
+ shellType,
530
+ cwd: bashWorkDir,
531
+ pathValue: spawnEnv.PATH,
532
+ env: spawnEnv,
533
+ });
567
534
  // PowerShell UTF-8 prefix is PS-only: the Windows Git Bash path
568
535
  // (shellType==='posix') must NOT receive it. Snapshot wrapper stays
569
536
  // POSIX-host-only for now — no snapshot for Windows Git Bash initially.
@@ -574,14 +541,12 @@ export async function executeBashTool(args, workDir, options = {}) {
574
541
  execShellArgs = [];
575
542
  directArgv = directPlan.argv;
576
543
  } else if (process.platform === 'win32' && shellType === 'powershell') {
577
- // Filter-swallow rescue (sync path only): tee the unfiltered
544
+ // Filter-swallow rescue: tee the unfiltered
578
545
  // producer stream of an exactly-recognized filter pipeline so a
579
546
  // failing run can attach the original output tail in THIS call
580
547
  // instead of returning `[exit code: N]` + `(no output)`. Any
581
548
  // ambiguity yields a null plan and the command runs untouched.
582
- if (!runInBackground) {
583
- try { _teePlan = buildPowerShellFilterTeePlan(command); } catch { _teePlan = null; }
584
- }
549
+ try { _teePlan = buildPowerShellFilterTeePlan(command); } catch { _teePlan = null; }
585
550
  wrappedCommand = _prefixPowerShellUtf8(_teePlan ? _teePlan.command : command);
586
551
  } else if (process.platform !== 'win32' && (shell.includes('bash') || shell.includes('zsh'))) {
587
552
  try {
@@ -592,108 +557,6 @@ export async function executeBashTool(args, workDir, options = {}) {
592
557
  } else {
593
558
  wrappedCommand = command;
594
559
  }
595
- if (runInBackground) {
596
- let asyncAbortSignal = null;
597
- try { asyncAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
598
- catch { asyncAbortSignal = null; }
599
- const combinedAsyncAbort = _combineAbortSignals(asyncAbortSignal, options?.abortSignal || null);
600
- let asyncLease = null;
601
- let job;
602
- try {
603
- asyncLease = await acquireShellLeaseBounded(options?.resourceAdmission || resourceAdmission, {
604
- abortSignal: combinedAsyncAbort.signal,
605
- label: String(command).replace(/\s+/g, ' ').slice(0, 120),
606
- dependency: 'detached',
607
- ownerKey: options?.callerSessionId || options?.sessionId || null,
608
- });
609
- if (combinedAsyncAbort.signal?.aborted) {
610
- throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before spawn');
611
- }
612
- job = await startBackgroundShellJob({
613
- command: wrappedCommand,
614
- timeoutMs: timeout,
615
- workDir: bashWorkDir,
616
- mergeStderr,
617
- spawnEnv,
618
- shell,
619
- shellArg,
620
- shellArgs,
621
- shellType,
622
- // Per-terminal session stamp: the dispatching terminal's
623
- // claude.exe pid (server-main threads callerSession.clientHostPid).
624
- clientHostPid: options?.clientHostPid,
625
- // Dispatching session: hosts that pool many sessions in one
626
- // process (desktop) scope the job to its own pane with this.
627
- ownerSessionId: options?.callerSessionId || options?.sessionId || null,
628
- ...(options?.shellJobRuntime || {}),
629
- });
630
- if (job && job.error) {
631
- if (job.rollbackPending && attachShellJobResourceLease(job.jobId, asyncLease, { allowUnpersisted: true })) {
632
- asyncLease = null;
633
- }
634
- return formatShellToolFailure(job.error);
635
- }
636
- if (combinedAsyncAbort.signal?.aborted) {
637
- try { killShellJob(job.jobId); } catch {}
638
- throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before registration');
639
- }
640
- if (job && !job.error && attachShellJobResourceLease(job.jobId, asyncLease)) {
641
- asyncLease = null;
642
- }
643
- const task = registerBackgroundTask({
644
- taskId: job.jobId,
645
- surface: 'shell',
646
- operation: 'shell',
647
- label: String(command).replace(/\s+/g, ' ').slice(0, 120),
648
- input: { command, cwd: bashWorkDir },
649
- context: {
650
- notifyFn: typeof options?.notifyFn === 'function' ? options.notifyFn : null,
651
- callerSessionId: options?.callerSessionId || options?.sessionId || null,
652
- routingSessionId: options?.routingSessionId || options?.sessionId || null,
653
- clientHostPid: options?.clientHostPid,
654
- },
655
- meta: {
656
- task_id: job.jobId,
657
- pid: job.pid,
658
- stdout: normalizeOutputPath(job.stdoutPath),
659
- stderr: mergeStderr ? null : normalizeOutputPath(job.stderrPath),
660
- cwd: bashWorkDir,
661
- timeoutMs: timeout,
662
- },
663
- resultType: 'shell_task_result',
664
- cancel: () => killShellJob(job.jobId),
665
- });
666
- if (combinedAsyncAbort.signal?.aborted) {
667
- try { killShellJob(job.jobId); } catch {}
668
- cancelBackgroundTask(job.jobId, 'cancelled before background registration completed');
669
- throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled during registration');
670
- }
671
- // Wire a one-shot completion push so the dispatching session learns
672
- // the background task finished (no polling tool is auto-driven).
673
- try {
674
- watchBackgroundShellJob(job.jobId, {
675
- notifyFn: typeof options?.notifyFn === 'function' ? options.notifyFn : null,
676
- callerSessionId: options?.callerSessionId || options?.sessionId,
677
- routingSessionId: options?.routingSessionId,
678
- clientHostPid: options?.clientHostPid,
679
- });
680
- } catch { /* watcher arm is best-effort; never blocks the spawn */ }
681
- return _prependDestructiveWarning(command, [
682
- renderBackgroundTask(task),
683
- '',
684
- 'You will be notified when it completes; do not poll.',
685
- ].join('\n'));
686
- } catch (error) {
687
- if (job?.jobId && !job.error) {
688
- try { killShellJob(job.jobId); } catch {}
689
- }
690
- return formatShellToolFailure(normalizeErrorMessage(error instanceof Error ? error.message : String(error)));
691
- } finally {
692
- combinedAsyncAbort.cleanup();
693
- try { await asyncLease?.release(); } catch {}
694
- }
695
- }
696
-
697
560
  let bashAbortSignal = null;
698
561
  try { bashAbortSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
699
562
  catch { bashAbortSignal = null; }
@@ -701,10 +564,8 @@ export async function executeBashTool(args, workDir, options = {}) {
701
564
  // Promote-at-timeout (CC shouldAutoBackground parity). When a
702
565
  // foreground one-shot hits its timeout and is still running, adopt it
703
566
  // as a background job (task_id + notify) instead of tree-killing it.
704
- // Opt-outs restore the old kill behavior: (a) disallowed sleep-like
705
- // base commands (isAutobackgroundingAllowed), (b) the truthy
706
- // MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS env. Never applies to
707
- // run_in_background (already detached, handled above).
567
+ // The truthy MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS env restores the old
568
+ // foreground-only behavior.
708
569
  const foregroundStartedAtMs = Date.now();
709
570
  const result = await execShellCommand({
710
571
  shell: execShell, shellArg: execShellArg, shellArgs: execShellArgs,
@@ -12,8 +12,8 @@ const _shellSyntaxCheat =
12
12
  export const BUILTIN_TOOLS = [
13
13
  {
14
14
  name: 'read',
15
- title: 'Mixdog Read',
16
- annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
15
+ title: 'Read',
16
+ annotations: { title: 'Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
17
17
  description: 'Known-file contents or line ranges. Images render for viewing; not directories. Replaces cat/head/tail.',
18
18
  inputSchema: {
19
19
  type: 'object',
@@ -39,9 +39,9 @@ export const BUILTIN_TOOLS = [
39
39
  },
40
40
  {
41
41
  name: 'shell',
42
- title: 'Mixdog Shell',
43
- annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
44
- description: 'Run programs and runtime/state operations; perform calculations, transform data, generate computed files, or inspect formats unsupported by file tools. Do not use for ordinary file-content inspection. A command that may alter source evidence may run only after a separate completed tool round has preserved the original or established verified read-only access. Tracked sync/async commands belong to the current run; only a service explicitly required after the run exits should be shell-detached (for example, nohup ... &).',
42
+ title: 'Shell',
43
+ annotations: { title: 'Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
44
+ description: 'Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. After 10s, a running command returns task_id and completes by notification.',
45
45
  inputSchema: {
46
46
  type: 'object',
47
47
  properties: {
@@ -50,7 +50,6 @@ export const BUILTIN_TOOLS = [
50
50
  type: 'number',
51
51
  description: 'Optional total deadline.',
52
52
  },
53
- run_in_background: { type: 'boolean', description: 'Run immediately as a tracked background task; returns task_id and sends a completion notification.' },
54
53
  },
55
54
  required: ['command'],
56
55
  additionalProperties: false,
@@ -58,24 +57,23 @@ export const BUILTIN_TOOLS = [
58
57
  },
59
58
  {
60
59
  name: 'task',
61
- title: 'Background Task Control',
62
- annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
63
- description: 'Schedule one progress check or manually inspect/cancel a shell background task_id; normal completion arrives by notification. Not for session or agent ids.',
60
+ title: 'Task',
61
+ annotations: { title: 'Task', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
62
+ description: 'List shell tasks, read one current output snapshot, or cancel by task_id; completion arrives by notification.',
64
63
  inputSchema: {
65
64
  type: 'object',
66
65
  properties: {
67
- task_id: { type: 'string', description: 'Shell task_id.' },
68
- action: { type: 'string', enum: ['list', 'status', 'read', 'check_after', 'cancel'], description: 'Default list; task_id alone defaults to non-blocking status. check_after schedules one non-blocking progress notification.' },
69
- after_ms: { type: 'number', description: 'Required explicitly for check_after; one-shot delay before the progress snapshot, not the task deadline.' },
66
+ task_id: { type: 'string', description: 'Shell task_id; required for read/cancel.' },
67
+ action: { type: 'string', enum: ['list', 'read', 'cancel'], description: 'list all; read snapshot; cancel task.' },
70
68
  },
71
- required: [],
69
+ required: ['action'],
72
70
  additionalProperties: false,
73
71
  },
74
72
  },
75
73
  {
76
74
  name: 'grep',
77
- title: 'Mixdog Grep',
78
- annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
75
+ title: 'Grep',
76
+ annotations: { title: 'Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
79
77
  description: 'Search file contents for literal or regex matches; contextual path:line blocks are directly usable—read only omitted lines. Replaces grep/rg.',
80
78
  inputSchema: {
81
79
  type: 'object',
@@ -103,8 +101,8 @@ export const BUILTIN_TOOLS = [
103
101
  },
104
102
  {
105
103
  name: 'glob',
106
- title: 'Mixdog Glob',
107
- annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
104
+ title: 'Glob',
105
+ annotations: { title: 'Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
108
106
  description: 'Return wildcard-matching paths under a known base when those paths are needed. Replaces find -name.',
109
107
  inputSchema: {
110
108
  type: 'object',
@@ -126,8 +124,8 @@ export const BUILTIN_TOOLS = [
126
124
  },
127
125
  {
128
126
  name: 'find',
129
- title: 'Mixdog Find Files',
130
- annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
127
+ title: 'Find Files',
128
+ annotations: { title: 'Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
131
129
  description: 'Fuzzy filename/directory path lookup when the location itself is unknown; returns paths only. No source-content, symbol, value, or line search.',
132
130
  inputSchema: {
133
131
  type: 'object',
@@ -146,8 +144,8 @@ export const BUILTIN_TOOLS = [
146
144
  },
147
145
  {
148
146
  name: 'list',
149
- title: 'Mixdog List Directory',
150
- annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
147
+ title: 'List Directory',
148
+ annotations: { title: 'List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
151
149
  description: "Return a known directory's immediate entries (path + type) when the entry list itself is needed; not a prerequisite for another tool on that directory. No wildcard; meta:true adds size/mtime/mode.",
152
150
  inputSchema: {
153
151
  type: 'object',
@@ -3,7 +3,6 @@ import { assertPathReachable, assertPathsReachable } from './fs-reachability.mjs
3
3
  import { isAbsolute, join, resolve } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import { randomUUID } from 'crypto';
6
- import { WRAPPER_NAMES } from '../shell-policy.mjs';
7
6
  import {
8
7
  cwdRelativePath,
9
8
  normalizeInputPath,
@@ -830,36 +829,6 @@ export async function analyzeShellCommandEffects(command, cwd) {
830
829
  return { mutationMode: 'none', paths: [], finalCwd: localCwd };
831
830
  }
832
831
 
833
- // CC detectBlockedSleepPattern parity: a LEADING integer `sleep N` (or the
834
- // PowerShell `Start-Sleep`/`sleep` alias forms) of 2s or more is blocked
835
- // preflight with an instructive error instead of running and being killed at
836
- // its deadline. Only the first top-level segment is considered — sleeps inside
837
- // pipelines/subshells/scripts are legitimate pacing and pass through. Float
838
- // durations (sleep 0.5) are allowed, mirroring the reference CLI.
839
- // Explicit async execution bypasses this validation. Sync callers receive the
840
- // same corrective direction as the reference CLI instead of being silently
841
- // changed to a different execution mode.
842
- export function detectBlockedSleepPattern(command, minSecs = 2) {
843
- const cmd = String(command || '').trim();
844
- if (!cmd) return null;
845
- const sep = cmd.match(/&&|\|\||;|\|/);
846
- const first = (sep ? cmd.slice(0, sep.index) : cmd).trim();
847
- let secs = null;
848
- const posix = /^sleep\s+(\d+)\s*$/.exec(first);
849
- if (posix) secs = Number.parseInt(posix[1], 10);
850
- if (secs == null) {
851
- const ps = /^(?:start-sleep|sleep)\s+(?:-(?:seconds|s)\s+)?(\d+)\s*$/i.exec(first);
852
- if (ps) secs = Number.parseInt(ps[1], 10);
853
- }
854
- if (secs == null) {
855
- const psMs = /^start-sleep\s+-(?:milliseconds|m)\s+(\d+)\s*$/i.exec(first);
856
- if (psMs) secs = Math.floor(Number.parseInt(psMs[1], 10) / 1000);
857
- }
858
- if (secs == null || secs < minSecs) return null;
859
- const rest = sep ? cmd.slice(sep.index).replace(/^(?:&&|\|\||;|\|)\s*/, '').trim() : '';
860
- return rest ? `sleep ${secs} followed by: ${rest.slice(0, 80)}` : `standalone sleep ${secs}`;
861
- }
862
-
863
832
  // Shell interception: patch-trained models type `apply_patch <<'EOF' … EOF`
864
833
  // INTO THE SHELL. No
865
834
  // such binary exists here, so the invocation is extracted and routed to the
@@ -903,81 +872,6 @@ export function extractShellApplyPatchInvocation(command) {
903
872
  return { error: 'apply_patch requires the patch text (heredoc or single argument)' };
904
873
  }
905
874
 
906
- export function foregroundLongCommandHint(command, timeoutMs, args = {}, opts = {}) {
907
- if (args.run_in_background === true) return '';
908
- const cmd = String(command || '').trim();
909
- if (!cmd) return '';
910
- // CC validateInput parity: block only while background tasks are enabled —
911
- // the remedy we point at (background execution / completion notification) must exist.
912
- if (opts.backgroundTasksDisabled !== true) {
913
- const blocked = detectBlockedSleepPattern(cmd);
914
- if (blocked) {
915
- return `Error: blocked — ${blocked}. Set run_in_background:true and act on the completion notification. If you genuinely need a delay (rate limiting, pacing), keep it under 2 seconds.`;
916
- }
917
- }
918
- return '';
919
- }
920
-
921
- // Commands that must NOT be promoted to background on a foreground timeout —
922
- // they run in the foreground and are killed at their deadline instead. Mirrors
923
- // the reference CLI DISALLOWED_AUTO_BACKGROUND_COMMANDS: `sleep` on posix,
924
- // `start-sleep`/`sleep` (the PS built-in alias) on PowerShell. A `sleep`/
925
- // `Start-Sleep` the caller wants to survive must be launched with
926
- // run_in_background:true explicitly.
927
- const _DISALLOWED_AUTO_BACKGROUND_POSIX = ['sleep'];
928
- const _DISALLOWED_AUTO_BACKGROUND_PS = ['start-sleep', 'sleep'];
929
-
930
- // Whether one shell segment reaches a disallowed sleep-like command. Strips a
931
- // leading subshell/paren/brace/`&`, then walks tokens skipping VAR=val and
932
- // option flags (quotes stripped so `'Start-Sleep'` resolves). A disallowed base
933
- // name found anywhere reachable blocks promotion. Once a command-runner wrapper
934
- // (env/sudo/timeout/nice/…, shell-policy.mjs's WRAPPER_NAMES) has been seen we
935
- // do NOT model that wrapper's flag arity — we keep scanning EVERY later bare
936
- // token for a hidden `sleep`, so `sudo -u nobody sleep 5` / `setpriv --reuid
937
- // user sleep` are caught (erring toward NOT promoting). Without a wrapper the
938
- // first real command token decides the segment.
939
- function _segmentDisallowed(segment, disallowed) {
940
- const stripped = String(segment || '').replace(/^[\s(){}&]+/, '');
941
- const tokens = stripped.split(/\s+/).filter(Boolean);
942
- let sawWrapper = false;
943
- for (let tok of tokens) {
944
- tok = tok.replace(/^['"]+|['"]+$/g, '');
945
- if (!tok) continue;
946
- if (/^[A-Za-z_]\w*=/.test(tok)) continue; // VAR=val assignment
947
- if (tok.startsWith('-')) continue; // option flag
948
- const stem = tok.toLowerCase().replace(/^.*[\\/]/, '').replace(/\.exe$/, '');
949
- if (disallowed.includes(stem)) return true; // sleep reachable here
950
- if (WRAPPER_NAMES.has(stem)) { sawWrapper = true; continue; }
951
- if (/^\d+(?:\.\d+)?[smhd]?$/i.test(tok)) continue; // numeric/duration arg
952
- // First real command token. Without a preceding wrapper it IS the base
953
- // command and it wasn't disallowed → segment is safe. With a wrapper,
954
- // this may be a wrapper value arg (`sudo -u nobody …`); keep scanning
955
- // for a hidden sleep instead of stopping here.
956
- if (!sawWrapper) return false;
957
- }
958
- return false;
959
- }
960
-
961
- // Whether a still-running foreground one-shot may be promoted to a background
962
- // job when it hits its timeout (CC isAutobackgroundingAllowed analogue).
963
- // Scans EVERY segment of a &&/||/;/|/& chain and, per segment, peels wrappers
964
- // before reading the base command — so a sleep-like hidden behind a chain
965
- // (`cd x && sleep 5`) or a runner wrapper (`timeout 5 sleep`, `env sleep`) is
966
- // still caught. Errs toward NOT promoting: any disallowed base command in any
967
- // segment blocks promotion. shellType 'powershell' uses the PS disallow list.
968
- export function isAutobackgroundingAllowed(command, shellType) {
969
- const cmd = String(command || '').trim();
970
- if (!cmd) return true;
971
- const disallowed = shellType === 'powershell'
972
- ? _DISALLOWED_AUTO_BACKGROUND_PS
973
- : _DISALLOWED_AUTO_BACKGROUND_POSIX;
974
- const segments = cmd.split(/(?:&&|\|\||[;\n|&])/);
975
- for (const seg of segments) {
976
- if (_segmentDisallowed(seg, disallowed)) return false;
977
- }
978
- return true;
979
- }
980
-
981
875
  // ---------------------------------------------------------------------------
982
876
  // Filter-swallow rescue (PowerShell one-shot path). Measured 2026-08: ~37
983
877
  // failures/14d were `<producer> 2>&1 | Select-String … | Select-Object …`
@@ -11,7 +11,6 @@ import {
11
11
  cancelBackgroundTask,
12
12
  completeBackgroundTask,
13
13
  getBackgroundTask,
14
- notifyBackgroundTaskProgress,
15
14
  renderBackgroundTask,
16
15
  renderBackgroundTaskList,
17
16
  } from '../../../../shared/background-tasks.mjs';
@@ -54,38 +53,9 @@ function renderTaskCancelSuccess(taskId, task) {
54
53
  ].join('\n');
55
54
  }
56
55
 
57
- function scheduleShellProgressCheck(task, afterMs) {
58
- const replaced = Boolean(task.progressCheckTimer);
59
- if (task.progressCheckTimer) {
60
- try { clearTimeout(task.progressCheckTimer); } catch {}
61
- }
62
- const scheduledAt = Date.now();
63
- const timer = setTimeout(() => {
64
- const current = getBackgroundTask(task.taskId);
65
- if (!current || current.progressCheckTimer !== timer) return;
66
- current.progressCheckTimer = null;
67
- if (current.status !== 'running') return;
68
- const job = peekShellJob(task.taskId);
69
- if (!job || job.status !== 'running') return;
70
- const snapshot = shellJobPublicTaskResult(job);
71
- notifyBackgroundTaskProgress(current, {
72
- text: [
73
- renderBackgroundTask(current),
74
- '',
75
- JSON.stringify(snapshot, null, 2),
76
- ].join('\n'),
77
- resultType: 'shell_task_progress',
78
- instruction: `The scheduled progress check for shell task ${task.taskId} is ready; inspect this snapshot and schedule another check_after only if needed.`,
79
- key: `scheduled-progress-${scheduledAt}`,
80
- });
81
- }, afterMs);
82
- if (typeof timer.unref === 'function') timer.unref();
83
- task.progressCheckTimer = timer;
84
- return replaced;
85
- }
86
-
87
56
  export async function executeTaskTool(args, options = {}) {
88
- const action = typeof args.action === 'string' ? args.action.toLowerCase() : (args.task_id ? 'status' : 'list');
57
+ const action = typeof args.action === 'string' ? args.action.toLowerCase() : '';
58
+ if (!action) return 'Error: task action is required';
89
59
  if (action === 'list') return renderBackgroundTaskList({ context: options });
90
60
 
91
61
  const taskId = typeof args.task_id === 'string' ? args.task_id.trim() : '';
@@ -102,10 +72,16 @@ export async function executeTaskTool(args, options = {}) {
102
72
  if (!task) return `Error: task not found: ${taskId}`;
103
73
  const isShellTask = task.surface === 'shell';
104
74
 
105
- if (action === 'status' || action === 'read') {
106
- if (isShellTask) refreshShellTask(taskId, { includeRunning: action === 'read' });
75
+ if (action === 'read') {
76
+ if (isShellTask) refreshShellTask(taskId, { includeRunning: true });
107
77
  const latest = getBackgroundTask(taskId, { context: options }) || task;
108
- return renderBackgroundTask(latest, { includeResult: action === 'read' });
78
+ const rendered = renderBackgroundTask(latest, { includeResult: true });
79
+ if (latest.status !== 'running') return rendered;
80
+ return [
81
+ rendered,
82
+ '',
83
+ 'Still running. Completion will be delivered automatically; do not poll or call task again unless the user explicitly asks for another snapshot.',
84
+ ].join('\n');
109
85
  }
110
86
 
111
87
  if (action === 'cancel') {
@@ -120,26 +96,5 @@ export async function executeTaskTool(args, options = {}) {
120
96
  return renderTaskCancelSuccess(taskId, getBackgroundTask(taskId, { context: options }) || task);
121
97
  }
122
98
 
123
- if (action === 'check_after') {
124
- if (!Number.isInteger(args.after_ms) || args.after_ms <= 0 || args.after_ms > 2_147_483_647) {
125
- return 'Error: task action "check_after" requires explicit positive integer "after_ms"';
126
- }
127
- if (!isShellTask) return 'Error: task action "check_after" supports shell task_id values only';
128
- const job = peekShellJob(taskId);
129
- if (!job) return buildJobNotFoundMessage(taskId);
130
- if (job.status !== 'running') {
131
- refreshShellTask(taskId);
132
- return renderBackgroundTask(getBackgroundTask(taskId, { context: options }) || task, { includeResult: true });
133
- }
134
- const replaced = scheduleShellProgressCheck(task, args.after_ms);
135
- return [
136
- 'status: running',
137
- `task_id: ${taskId}`,
138
- 'progress_check_scheduled: true',
139
- `after_ms: ${args.after_ms}`,
140
- replaced ? 'replaced_previous_check: true' : null,
141
- ].filter(Boolean).join('\n');
142
- }
143
-
144
- return `Error: task action must be one of list|status|read|check_after|cancel (got ${JSON.stringify(args.action)})`;
99
+ return `Error: task action must be one of list|read|cancel (got ${JSON.stringify(args.action)})`;
145
100
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.1.12",
2
+ "version": "0.1.13",
3
3
  "_comment": "Synced from immutable graph-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.12/mixdog-graph-darwin-arm64",
7
- "sha256": "c8fa189949ddbcb4794f4f44a6ac355ecb520c31a23e9a94bc364baa18d9c01e"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.13/mixdog-graph-darwin-arm64",
7
+ "sha256": "356ddd4c591ff96f6dea32af360798568db7cf474215692a218622e25276b923"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.12/mixdog-graph-darwin-x64",
11
- "sha256": "949262757f112e0bf3da58593f3b67d48192aeeb69a1e9e953cc0d553312b7c5"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.13/mixdog-graph-darwin-x64",
11
+ "sha256": "54cb3aaaa3c0b0d8e5d5defc4c8f6f701a2e4e574095f5d377773692ce33fbeb"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.12/mixdog-graph-linux-arm64",
15
- "sha256": "a60c7ecc5bbfc7f8537b73f0184abc7d1c5414a0775b43282a99bcc4e4e23bc2"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.13/mixdog-graph-linux-arm64",
15
+ "sha256": "c14a2df85d881af3436dc56622fbd4a2078bd2e3d1a615a23332e92efefc7f0f"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.12/mixdog-graph-linux-x64",
19
- "sha256": "919b5859ea6d74147439d4182ac3c2faf5bc30451ecfbf901276a49830bf713d"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.13/mixdog-graph-linux-x64",
19
+ "sha256": "c616be0a16547ea9568b487d2ca0e6e4515c9a740f27b73ec09e6bc368461b3f"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.12/mixdog-graph-win32-x64.exe",
23
- "sha256": "59d9c7337f853e80575d582cfac277983eb58ef0506518d2ce768c7d2d0a0f53"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.13/mixdog-graph-win32-x64.exe",
23
+ "sha256": "132cab2c467800a87f0ac05cd5ef5023db8f3b0c12ad2047b5368ee49a8d1e60"
24
24
  }
25
25
  }
26
26
  }
@@ -320,10 +320,6 @@ export function completeBackgroundTask(taskId, {
320
320
  const task = getBackgroundTask(taskId);
321
321
  if (!task) return null;
322
322
  if (TERMINAL_STATUSES.has(task.status)) return task;
323
- if (task.progressCheckTimer) {
324
- try { clearTimeout(task.progressCheckTimer); } catch {}
325
- task.progressCheckTimer = null;
326
- }
327
323
  const now = Date.now();
328
324
  task.status = normalizeStatus(status);
329
325
  task.finishedAtMs = now;
@@ -5,7 +5,7 @@ export const TOOL_ASYNC_EXECUTION_CONTRACT =
5
5
  'Runs sync inline by default; async returns a background task_id and delivers a completion notification.';
6
6
 
7
7
  export const TOOL_MANUAL_CONTROL_CONTRACT =
8
- 'check_after schedules one progress notification; read/status/cancel are for manual recovery.';
8
+ 'read returns one current output snapshot; cancel is for manual recovery, while normal completion arrives by notification.';
9
9
 
10
10
  function clean(value) {
11
11
  return String(value ?? '').trim();
@@ -82,6 +82,7 @@ export function createTagRegistry({
82
82
  const add = (session, fallbackTag = '') => {
83
83
  const tag = agentTagOf(session) || clean(fallbackTag);
84
84
  if (!tag || !session?.id || session.closed === true) return;
85
+ if (isLeadPoolAgent(session.agent)) return;
85
86
  if (!sessionMatchesContext(session, context)) return;
86
87
  if (seen.has(session.id)) return;
87
88
  seen.add(session.id);
@@ -158,6 +159,7 @@ export function createTagRegistry({
158
159
  }
159
160
  if (!scanSessions) return;
160
161
  for (const session of mgr.listSessions({ includeClosed: false }) || []) {
162
+ if (isLeadPoolAgent(session?.agent)) continue;
161
163
  const tag = agentTagOf(session);
162
164
  if (!tag || tags.has(tag)) continue;
163
165
  if (!sessionMatchesContext(session, context)) continue;
@@ -282,6 +282,17 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
282
282
  const rows = readWorkerRows(context);
283
283
  for (const row of rows) {
284
284
  if (!row.tag || !row.sessionId) continue;
285
+ if (isLeadPoolAgent(row.agent)) {
286
+ // Lead pool rows are status projections, not agent-tool children.
287
+ // Purge a projection left by older code so closeAll cannot mistake
288
+ // the owning desktop session for a worker and close it.
289
+ if (tags.get(row.tag) === row.sessionId) {
290
+ tags.delete(row.tag);
291
+ tagAgents.delete(row.tag);
292
+ tagCwds.delete(row.tag);
293
+ }
294
+ continue;
295
+ }
285
296
  tags.set(row.tag, row.sessionId);
286
297
  if (row.agent) tagAgents.set(row.tag, row.agent);
287
298
  if (row.cwd) tagCwds.set(row.tag, row.cwd);
@@ -450,6 +450,9 @@ export function createStandaloneAgent({
450
450
  }
451
451
  throw new Error(`agent close: target "${target}" not found`);
452
452
  }
453
+ if (isLeadPoolAgent(getLiveSession(sessionId)?.agent)) {
454
+ throw new Error(`agent close: target "${target}" is a Lead session`);
455
+ }
453
456
  cancelReap(sessionId);
454
457
  const tag = tagForSession(sessionId);
455
458
  forgetTerminalSession(tag, sessionId);