mixdog 0.9.22 → 0.9.24

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 (132) hide show
  1. package/README.md +1 -4
  2. package/package.json +1 -1
  3. package/scripts/boot-smoke.mjs +1 -1
  4. package/scripts/channel-daemon-smoke.mjs +483 -0
  5. package/scripts/channel-daemon-stub.mjs +80 -0
  6. package/scripts/debounced-skills-async-save-test.mjs +57 -0
  7. package/scripts/explore-bench-tmp.mjs +17 -0
  8. package/scripts/find-fuzzy-hidden-test.mjs +145 -0
  9. package/scripts/mcp-grace-deferred-test.mjs +149 -0
  10. package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
  11. package/scripts/tool-smoke.mjs +68 -47
  12. package/src/rules/agent/30-explorer.md +6 -0
  13. package/src/rules/shared/01-tool.md +11 -4
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
  15. package/src/runtime/agent/orchestrator/config.mjs +33 -7
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
  17. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +226 -0
  18. package/src/runtime/agent/orchestrator/mcp/client.mjs +184 -33
  19. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
  20. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
  21. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
  24. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
  27. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
  29. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
  31. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
  32. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -3
  33. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
  34. package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
  35. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
  37. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
  38. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
  39. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
  40. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
  41. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
  42. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
  43. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +67 -27
  45. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
  46. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
  47. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
  48. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
  49. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
  50. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +126 -22
  51. package/src/runtime/channels/lib/crash-log.mjs +4 -2
  52. package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
  53. package/src/runtime/channels/lib/owned-runtime.mjs +67 -223
  54. package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -62
  55. package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
  56. package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
  57. package/src/runtime/channels/lib/session-discovery.mjs +2 -1
  58. package/src/runtime/channels/lib/tool-dispatch.mjs +22 -14
  59. package/src/runtime/channels/lib/tool-format.mjs +7 -2
  60. package/src/runtime/channels/lib/worker-main.mjs +42 -30
  61. package/src/runtime/memory/index.mjs +54 -7
  62. package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
  63. package/src/runtime/memory/lib/pg/process.mjs +85 -40
  64. package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
  65. package/src/runtime/memory/lib/query-handlers.mjs +4 -1
  66. package/src/runtime/memory/lib/recall-format.mjs +7 -3
  67. package/src/runtime/memory/tool-defs.mjs +1 -1
  68. package/src/runtime/shared/atomic-file.mjs +150 -6
  69. package/src/runtime/shared/background-tasks.mjs +1 -1
  70. package/src/runtime/shared/config.mjs +53 -1
  71. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  72. package/src/runtime/shared/tool-primitives.mjs +31 -1
  73. package/src/runtime/shared/tool-surface.mjs +42 -13
  74. package/src/runtime/shared/update-checker.mjs +3 -0
  75. package/src/runtime/shared/user-data-guard.mjs +66 -0
  76. package/src/session-runtime/config-lifecycle.mjs +221 -20
  77. package/src/session-runtime/lifecycle-api.mjs +9 -0
  78. package/src/session-runtime/mcp-glue.mjs +93 -1
  79. package/src/session-runtime/resource-api.mjs +62 -8
  80. package/src/session-runtime/runtime-core.mjs +118 -4
  81. package/src/session-runtime/session-text.mjs +41 -0
  82. package/src/session-runtime/session-turn-api.mjs +50 -0
  83. package/src/session-runtime/settings-api.mjs +8 -1
  84. package/src/session-runtime/tool-catalog.mjs +350 -38
  85. package/src/session-runtime/tool-defs.mjs +7 -7
  86. package/src/session-runtime/workflow.mjs +2 -1
  87. package/src/standalone/channel-admin.mjs +32 -3
  88. package/src/standalone/channel-daemon-client.mjs +226 -0
  89. package/src/standalone/channel-daemon-transport.mjs +545 -0
  90. package/src/standalone/channel-daemon.mjs +176 -0
  91. package/src/standalone/channel-worker.mjs +224 -4
  92. package/src/standalone/explore-tool.mjs +87 -15
  93. package/src/standalone/hook-bus.mjs +71 -3
  94. package/src/tui/App.jsx +107 -19
  95. package/src/tui/app/clipboard.mjs +39 -19
  96. package/src/tui/app/doctor.mjs +57 -0
  97. package/src/tui/app/extension-pickers.mjs +53 -9
  98. package/src/tui/app/maintenance-pickers.mjs +0 -5
  99. package/src/tui/app/slash-dispatch.mjs +4 -4
  100. package/src/tui/app/text-layout.mjs +11 -0
  101. package/src/tui/app/use-mouse-input.mjs +235 -51
  102. package/src/tui/app/use-prompt-handlers.mjs +49 -30
  103. package/src/tui/app/use-transcript-scroll.mjs +124 -27
  104. package/src/tui/app/use-transcript-window.mjs +55 -1
  105. package/src/tui/components/Message.jsx +1 -1
  106. package/src/tui/components/PromptInput.jsx +3 -1
  107. package/src/tui/components/QueuedCommands.jsx +21 -10
  108. package/src/tui/components/StatusLine.jsx +3 -3
  109. package/src/tui/components/ToolExecution.jsx +16 -4
  110. package/src/tui/components/TranscriptItem.jsx +1 -1
  111. package/src/tui/dist/index.mjs +820 -326
  112. package/src/tui/engine/agent-job-feed.mjs +5 -0
  113. package/src/tui/engine/notification-plan.mjs +5 -0
  114. package/src/tui/engine/session-api.mjs +23 -8
  115. package/src/tui/engine/session-flow.mjs +6 -0
  116. package/src/tui/engine/tool-card-results.mjs +14 -5
  117. package/src/tui/engine/turn.mjs +9 -2
  118. package/src/tui/engine.mjs +32 -5
  119. package/src/tui/index.jsx +62 -18
  120. package/src/tui/paste-attachments.mjs +26 -0
  121. package/src/ui/statusline-agents.mjs +36 -0
  122. package/src/ui/statusline.mjs +60 -10
  123. package/src/ui/tool-card.mjs +8 -1
  124. package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
  125. package/src/workflows/bench/WORKFLOW.md +46 -0
  126. package/src/workflows/default/WORKFLOW.md +6 -0
  127. package/src/workflows/solo/WORKFLOW.md +5 -0
  128. package/vendor/ink/build/ink.js +23 -1
  129. package/vendor/ink/build/output.js +154 -71
  130. package/vendor/ink/build/render-node-to-output.js +44 -2
  131. package/vendor/ink/build/render.js +4 -0
  132. package/vendor/ink/build/renderer.js +4 -1
@@ -37,7 +37,7 @@ import { startChildGuardian } from '../../../shared/child-guardian.mjs';
37
37
  // transition). shell-jobs.mjs imports stripAnsi from this module, so this is
38
38
  // a static cycle — safe because neither binding is touched at module-eval
39
39
  // time, only when the respective functions actually run.
40
- import { adoptForegroundShellJob } from './builtin/shell-jobs.mjs';
40
+ import { adoptForegroundShellJob, killShellJob } from './builtin/shell-jobs.mjs';
41
41
  import {
42
42
  _maybeEncodePowerShellCommand,
43
43
  extractPowerShellCommandInner,
@@ -433,6 +433,55 @@ async function _execPolicyBlockMessage(command) {
433
433
  return checkExecPolicyMessage(command);
434
434
  }
435
435
 
436
+ // Count of shell spawns currently in-flight (including those parked in an
437
+ // EPERM backoff). Logged with each failed spawn so a Defender-induced storm
438
+ // is reconstructable: activeSpawnCount > 1 means concurrent spawns were
439
+ // racing the AV scan when the failure hit.
440
+ let _activeShellSpawns = 0;
441
+
442
+ function _isPowerShellSpawn(shell, shellArg) {
443
+ return /pwsh|powershell/i.test(String(shell || '')) || shellArg === '-Command';
444
+ }
445
+
446
+ // Windows Defender intermittently fails node→PowerShell spawns with EPERM
447
+ // while it scans the child image (see shell-runtime.mjs Trojan false-positive
448
+ // note). The failure is at spawn() time — before any stdio/side effect — so a
449
+ // short bounded retry is safe and never re-runs a command that already ran.
450
+ // Retry ONLY on EPERM/win32/powershell; everything else throws on first
451
+ // failure. Backoff 100/300/700ms caps added latency at ~1.1s. Every failed
452
+ // attempt logs one diagnostic line for later reconstruction.
453
+ async function _spawnShellWithRetry({ shell, argv, spawnOptions, shellArg, cwd }) {
454
+ const _delays = [100, 300, 700];
455
+ const _isPowerShell = _isPowerShellSpawn(shell, shellArg);
456
+ _activeShellSpawns++;
457
+ try {
458
+ let attempt = 0;
459
+ for (;;) {
460
+ try {
461
+ return spawn(shell, argv, spawnOptions);
462
+ } catch (err) {
463
+ try {
464
+ console.error('[shell-spawn-retry] ' + JSON.stringify({
465
+ code: (err && err.code) || null,
466
+ syscall: (err && err.syscall) || null,
467
+ shell,
468
+ cwd,
469
+ activeSpawnCount: _activeShellSpawns,
470
+ }));
471
+ } catch { /* logging must never mask the spawn error */ }
472
+ const _canRetry = err && err.code === 'EPERM'
473
+ && process.platform === 'win32'
474
+ && _isPowerShell
475
+ && attempt < _delays.length;
476
+ if (!_canRetry) throw err;
477
+ await new Promise((r) => setTimeout(r, _delays[attempt++]));
478
+ }
479
+ }
480
+ } finally {
481
+ _activeShellSpawns--;
482
+ }
483
+ }
484
+
436
485
  export function execShellCommand({
437
486
  shell,
438
487
  shellArg,
@@ -445,6 +494,7 @@ export function execShellCommand({
445
494
  autoBackgroundMs,
446
495
  onProgress,
447
496
  clientHostPid,
497
+ backgroundOnTimeout,
448
498
  }) {
449
499
  return new Promise(async (resolve) => {
450
500
  const taskId = `shell_${randomUUID().slice(0, 8)}`;
@@ -516,7 +566,12 @@ export function execShellCommand({
516
566
  const argv = Array.isArray(shellArgs) && shellArgs.length > 0
517
567
  ? [...shellArgs, _spawnCommand]
518
568
  : [shellArg, _spawnCommand];
519
- child = spawn(shell, argv, {
569
+ child = await _spawnShellWithRetry({
570
+ shell,
571
+ argv,
572
+ shellArg,
573
+ cwd,
574
+ spawnOptions: {
520
575
  env,
521
576
  cwd,
522
577
  windowsHide: true,
@@ -533,6 +588,7 @@ export function execShellCommand({
533
588
  // not unref it after adoption. Windows detached has different console
534
589
  // semantics, so it stays off there.
535
590
  detached: process.platform !== 'win32',
591
+ },
536
592
  });
537
593
  startChildGuardian({
538
594
  childPid: child.pid,
@@ -668,16 +724,20 @@ export function execShellCommand({
668
724
  if (grace.unref) grace.unref();
669
725
  });
670
726
 
671
- // Auto-background transition (CC ASSISTANT_BLOCKING_BUDGET_MS +
672
- // startBackgrounding analogue). Fires once, autoBackgroundMs after spawn,
673
- // IFF the child is still running and the run has not already settled /
674
- // been killed / timed out. It adopts the child into the shell-jobs
675
- // registry while keeping it owned by this CLI process, then resolves the
676
- // call immediately with a 'backgrounded' result so the tool stops hanging.
677
- // The 600 s timeoutMs upper bound is carried into the adopted job detail
678
- // so refreshShellJob still enforces it. Mutually exclusive with settle()
679
- // via the autoBackgrounded flag set synchronously at the top before any await.
680
- const _autoBackground = async () => {
727
+ // Auto-background transition (CC startBackgrounding analogue). Two triggers
728
+ // resolve the call immediately with a 'backgrounded' result while the
729
+ // child keeps running, adopted into the shell-jobs registry but still
730
+ // owned by this CLI process:
731
+ // 1. the optional autoBackgroundMs soft threshold (MIXDOG_SHELL_AUTO_
732
+ // BACKGROUND_MS opt-in) an EARLIER promotion before the timeout, and
733
+ // 2. the foreground timeout deadline (backgroundOnTimeout) the default
734
+ // promote-on-timeout that replaces the old tree-kill.
735
+ // Either way the adopted job runs UNLIMITED (timeoutMs 0, matching the
736
+ // async default): the original foreground timeout no longer bounds it, and
737
+ // the adopted-job cap poll still enforces the 100 MB output ceiling.
738
+ // Mutually exclusive with settle() via the autoBackgrounded flag set
739
+ // synchronously at the top before any await.
740
+ const _autoBackground = async ({ reason = 'threshold' } = {}) => {
681
741
  // Win the race: bail if a terminal transition already happened, and
682
742
  // claim the transition synchronously so a concurrently-queued settle()
683
743
  // (which checks autoBackgrounded) becomes inert.
@@ -685,16 +745,18 @@ export function execShellCommand({
685
745
  if (child.exitCode != null || child.signalCode != null) return;
686
746
  autoBackgrounded = true;
687
747
  // The foreground capture is over; stop the local watchdogs/timers so
688
- // they cannot treeKill the now-adopted child. The 600 s bound lives
689
- // on in the adopted job detail (timeoutMs) for refreshShellJob.
748
+ // they cannot treeKill the now-adopted child. The adopted job runs
749
+ // unlimited; refreshShellJob only enforces the output cap.
690
750
  if (timer) { clearTimeout(timer); timer = null; }
691
751
  _clearProgressTimer();
692
752
  if (sizeWatchdog) { clearInterval(sizeWatchdog); sizeWatchdog = null; }
693
753
  if (autoBgTimer) { clearTimeout(autoBgTimer); autoBgTimer = null; }
694
- if (abortSignal && abortHandler) {
695
- try { abortSignal.removeEventListener('abort', abortHandler); } catch {}
696
- abortHandler = null;
697
- }
754
+ // Keep the abort handler ATTACHED through the promotion window. A user
755
+ // cancel racing in after promotion starts must still bring the adopted
756
+ // child down — the handler's treeKill(child) does exactly that (settle()
757
+ // is inert once autoBackgrounded, but the kill itself still lands, and
758
+ // refreshShellJob then flags the job failed). We only detach on a real
759
+ // settle() or on the adoption-failure fallback below.
698
760
  // Every subsequent stdout/stderr chunk must hit disk — the call is
699
761
  // about to resolve and nobody will drain the in-memory buffers again.
700
762
  try { taskOutput.forceSpill(); } catch {}
@@ -711,7 +773,9 @@ export function execShellCommand({
711
773
  command,
712
774
  cwd,
713
775
  pid: child.pid,
714
- timeoutMs,
776
+ // Unlimited: the promoted job is no longer bounded by the foreground
777
+ // timeout (matches the async omitted-default behavior).
778
+ timeoutMs: 0,
715
779
  mergeStderr: false,
716
780
  stdoutPath,
717
781
  stderrPath,
@@ -722,6 +786,18 @@ export function execShellCommand({
722
786
  } catch {
723
787
  job = null;
724
788
  }
789
+ // Adoption failed AFTER the foreground timers/size-watchdog were already
790
+ // torn down. Do NOT resolve as backgrounded — that would leave the child
791
+ // running unlimited with no task_id and no watcher. Release the claim and
792
+ // fall back to the old kill path so the command never outlives a failed
793
+ // promotion. (The abort handler is still attached, so an in-flight cancel
794
+ // is honored by the kill path too.)
795
+ if (!job) {
796
+ autoBackgrounded = false;
797
+ if (reason === 'timeout') timedOut = true; else killed = true;
798
+ _treeKillForceSettle();
799
+ return;
800
+ }
725
801
  // Wire the lifecycle: on close, write the exit-code file FIRST then
726
802
  // touch donePath STRICTLY AFTER — the exact ordering refreshShellJob()
727
803
  // gates completion on (donePath visible ⇒ exit file fully flushed).
@@ -732,6 +808,14 @@ export function execShellCommand({
732
808
  try { writeFileSync(job.donePath, ''); } catch {}
733
809
  });
734
810
  }
811
+ // Adoption committed. If a cancel already fired (before or during the
812
+ // adoption window), bring the now-adopted child down via its jobId so the
813
+ // promotion can't outlive a user abort. Idempotent with the still-attached
814
+ // abort handler's treeKill.
815
+ if (abortSignal && abortSignal.aborted) {
816
+ try { killShellJob(job.jobId); } catch {}
817
+ try { treeKill(child); } catch {}
818
+ }
735
819
  // Snapshot the partial output captured so far for the immediate result.
736
820
  let stdout = '';
737
821
  let stderr = '';
@@ -740,7 +824,10 @@ export function execShellCommand({
740
824
  try { stderr = await taskOutput.getStderr(); }
741
825
  catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
742
826
  const jobId = job ? job.jobId : null;
743
- const secs = Math.round(autoBackgroundMs / 1000);
827
+ const secs = Math.max(0, Math.round((Date.now() - _startMs) / 1000));
828
+ const _verb = reason === 'timeout'
829
+ ? `moved to background at timeout after ${secs}s`
830
+ : `auto-backgrounded after ${secs}s`;
744
831
  resolve(
745
832
  new ExecResult({
746
833
  stdout,
@@ -759,14 +846,31 @@ export function execShellCommand({
759
846
  backgrounded: true,
760
847
  jobId,
761
848
  backgroundMessage: jobId
762
- ? `auto-backgrounded after ${secs}s; still running — completion will be delivered as a background task notification. Use task with task_id:${jobId} only for manual wait/status/read/cancel.`
763
- : `auto-backgrounded after ${secs}s; still running`,
849
+ ? `${_verb}; still running — completion will be delivered as a background task notification. Use task with task_id:${jobId} only for manual wait/status/read/cancel.`
850
+ : `${_verb}; still running`,
764
851
  }),
765
852
  );
766
853
  };
767
854
 
768
855
  if (timeoutMs > 0) {
769
856
  timer = setTimeout(() => {
857
+ // Promote-on-timeout: if the caller allows backgrounding and the child
858
+ // is still running (not a trailing-`&` detach), adopt it as a tracked
859
+ // background job instead of tree-killing it. Falls through to the old
860
+ // kill path for disallowed/opted-out commands (backgroundOnTimeout
861
+ // false) or when a terminal transition already won the race.
862
+ if (
863
+ backgroundOnTimeout &&
864
+ !_isBackground &&
865
+ !settled &&
866
+ !autoBackgrounded &&
867
+ !killed &&
868
+ child.exitCode == null &&
869
+ child.signalCode == null
870
+ ) {
871
+ _autoBackground({ reason: 'timeout' });
872
+ return;
873
+ }
770
874
  timedOut = true;
771
875
  _treeKillForceSettle();
772
876
  }, timeoutMs);
@@ -23,8 +23,9 @@ function isChannelsDegraded() { return _channelsDegraded; }
23
23
  try {
24
24
  process.stderr.on('error', (e) => {
25
25
  if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
26
+ // Parent stdio pipe loss stops stderr writes only. It is NOT runtime
27
+ // corruption, so it must not degrade tool dispatch (_channelsDegraded).
26
28
  _stderrBroken = true;
27
- _channelsDegraded = true;
28
29
  }
29
30
  });
30
31
  } catch {}
@@ -75,7 +76,8 @@ ${err instanceof Error ? err.stack : ""}
75
76
  _writeCrashLine(crashLog, msg);
76
77
  }
77
78
  if (err instanceof Error && err.message.includes("EPIPE")) {
78
- _channelsDegraded = true;
79
+ // EPIPE = a broken pipe (parent stdio/IPC gone), not corrupted runtime
80
+ // state. Stop writing to the dead pipe but keep serving tool calls.
79
81
  _stderrBroken = true;
80
82
  }
81
83
  crashLogging = false;
@@ -6,6 +6,7 @@ import {
6
6
  formatToolSurface,
7
7
  isExplorerSurface,
8
8
  isMemorySurface,
9
+ stripToolPrefix,
9
10
  } from "../../shared/tool-surface.mjs";
10
11
  import {
11
12
  cwdToProjectSlug,
@@ -726,7 +727,7 @@ ${_bt.trim()}` : _bt.trim();
726
727
  // The non-set checks are inlined rather than delegated to the imported
727
728
  // isHidden, because that helper would re-consult the module-local
728
729
  // HIDDEN_TOOLS Set and ignore the OutputForwarder static.
729
- if (OutputForwarder.HIDDEN_TOOLS.has(name)) return true;
730
+ if (OutputForwarder.HIDDEN_TOOLS.has(name) || OutputForwarder.HIDDEN_TOOLS.has(stripToolPrefix(name))) return true;
730
731
  if (name === "reply" || name === "fetch") return true;
731
732
  return false;
732
733
  };