mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -30,14 +30,14 @@ const execFileAsync = promisify(execFile);
30
30
  // for this change and intentionally not added per-spawn.
31
31
 
32
32
  // ── rg --threads cap ─────────────────────────────────────────────────────
33
- // Each rg process otherwise fans out across EVERY core; with several agents
34
- // running grep at once the box over-saturates and the whole batch trips the
35
- // 20s deadline together. Use the larger half-CPU cap only when the spawn gate
36
- // has spare capacity; otherwise retain the conservative quarter-CPU cap.
37
- // Internal dynamic default; env override only — deliberately NOT exposed on
38
- // any tool schema / parameter surface.
33
+ // Each rg process otherwise fans out across EVERY core. Keep every independent
34
+ // tool call fully parallel, but bound the threads INSIDE each rg process so a
35
+ // broad explorer wave does not multiply half-machine thread pools (8 calls on
36
+ // a 20-core host previously created ~80 rg workers and made every call slower).
37
+ // This does not queue or limit calls; it only prevents per-process
38
+ // oversubscription. Env override remains available for operator tuning.
39
39
  let _rgDefaultThreadCap = null;
40
- export function _rgThreadCap(spareCapacity = false) {
40
+ export function _rgThreadCap(_spareCapacity = false) {
41
41
  const override = Number(process.env.MIXDOG_RG_THREADS);
42
42
  if (Number.isFinite(override) && override >= 1) return Math.floor(override);
43
43
  if (_rgDefaultThreadCap === null) {
@@ -46,9 +46,7 @@ export function _rgThreadCap(spareCapacity = false) {
46
46
  _rgDefaultThreadCap = cpus || 4;
47
47
  }
48
48
  const cpus = _rgDefaultThreadCap;
49
- return spareCapacity
50
- ? Math.max(4, Math.ceil(cpus / 2))
51
- : Math.max(2, Math.ceil(cpus / 4));
49
+ return Math.max(2, Math.min(4, Math.ceil(cpus / 4)));
52
50
  }
53
51
 
54
52
  // Walk rg options once for all thread-argument helpers. Values of these
@@ -370,6 +368,14 @@ function _boxPartialRgString(s, rgStderrText = '') {
370
368
  return boxed;
371
369
  }
372
370
 
371
+ function _rgAbortError(signal) {
372
+ const reason = signal?.reason;
373
+ if (reason instanceof Error) return reason;
374
+ const error = new Error(String(reason || 'rg canceled'));
375
+ error.code = 'ABORT_ERR';
376
+ return error;
377
+ }
378
+
373
379
  function _armRgForceSettle({ timeoutMs, isSettled, timers, proc, onForceSettle }) {
374
380
  if (timers.forceSettle) clearTimeout(timers.forceSettle);
375
381
  timers.forceSettle = setTimeout(() => {
@@ -412,6 +418,15 @@ function spawnRg(argsList, execOptions) {
412
418
  let stdoutBytes = 0;
413
419
  let stdoutTruncated = false;
414
420
  const timers = { timeout: null, killGrace: null, forceSettle: null };
421
+ let aborted = false;
422
+ let onAbort = null;
423
+ const clearAll = () => {
424
+ _clearRgTimers(timers);
425
+ if (onAbort && gateSignal) {
426
+ try { gateSignal.removeEventListener('abort', onAbort); } catch { /* ignore */ }
427
+ onAbort = null;
428
+ }
429
+ };
415
430
  timers.timeout = setTimeout(() => {
416
431
  timedOut = true;
417
432
  killReason = 'timeout';
@@ -430,10 +445,33 @@ function spawnRg(argsList, execOptions) {
430
445
  e.code = 'ETIMEDOUT';
431
446
  if (settled) return;
432
447
  settled = true;
433
- _clearRgTimers(timers);
448
+ clearAll();
434
449
  reject(e);
435
450
  },
436
451
  });
452
+ if (gateSignal) {
453
+ onAbort = () => {
454
+ if (settled || aborted) return;
455
+ aborted = true;
456
+ killReason = 'abort';
457
+ _clearRgTimeoutOnly(timers);
458
+ _killRgProc(proc, timers);
459
+ _armRgForceSettle({
460
+ timeoutMs: 0,
461
+ isSettled: () => settled,
462
+ timers,
463
+ proc,
464
+ onForceSettle: () => {
465
+ if (settled) return;
466
+ settled = true;
467
+ clearAll();
468
+ reject(_rgAbortError(gateSignal));
469
+ },
470
+ });
471
+ };
472
+ if (gateSignal.aborted) onAbort();
473
+ else gateSignal.addEventListener('abort', onAbort, { once: true });
474
+ }
437
475
  proc.stdout.setEncoding('utf-8');
438
476
  proc.stderr.setEncoding('utf-8');
439
477
  proc.stdout.on('data', (d) => {
@@ -466,13 +504,16 @@ function spawnRg(argsList, execOptions) {
466
504
  proc.on('error', (err) => {
467
505
  if (settled) return;
468
506
  settled = true;
469
- _clearRgTimers(timers);
507
+ clearAll();
470
508
  reject(err);
471
509
  });
472
510
  proc.on('close', (code) => {
473
511
  if (settled) return;
474
512
  settled = true;
475
- _clearRgTimers(timers);
513
+ clearAll();
514
+ if (aborted || killReason === 'abort') {
515
+ return reject(_rgAbortError(gateSignal));
516
+ }
476
517
  if (timedOut && killReason === 'timeout') {
477
518
  if (stdout.length > 0) {
478
519
  return resolve(_boxPartialRgString(stdout, stderr.trim()));
@@ -562,6 +603,15 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
562
603
  let settled = false;
563
604
  const timers = { timeout: null, killGrace: null, forceSettle: null };
564
605
  const lines = [];
606
+ let aborted = false;
607
+ let onAbort = null;
608
+ const clearAll = () => {
609
+ _clearRgTimers(timers);
610
+ if (onAbort && gateSignal) {
611
+ try { gateSignal.removeEventListener('abort', onAbort); } catch { /* ignore */ }
612
+ onAbort = null;
613
+ }
614
+ };
565
615
  timers.timeout = setTimeout(() => {
566
616
  timedOut = true;
567
617
  killReason = 'timeout';
@@ -579,7 +629,7 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
579
629
  if (lines.length > 0) {
580
630
  if (settled) return;
581
631
  settled = true;
582
- _clearRgTimers(timers);
632
+ clearAll();
583
633
  resolve({
584
634
  lines,
585
635
  complete: false,
@@ -594,10 +644,33 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
594
644
  e.code = 'ETIMEDOUT';
595
645
  if (settled) return;
596
646
  settled = true;
597
- _clearRgTimers(timers);
647
+ clearAll();
598
648
  reject(e);
599
649
  },
600
650
  });
651
+ if (gateSignal) {
652
+ onAbort = () => {
653
+ if (settled || aborted) return;
654
+ aborted = true;
655
+ killReason = 'abort';
656
+ _clearRgTimeoutOnly(timers);
657
+ _killRgProc(proc, timers);
658
+ _armRgForceSettle({
659
+ timeoutMs: 0,
660
+ isSettled: () => settled,
661
+ timers,
662
+ proc,
663
+ onForceSettle: () => {
664
+ if (settled) return;
665
+ settled = true;
666
+ clearAll();
667
+ reject(_rgAbortError(gateSignal));
668
+ },
669
+ });
670
+ };
671
+ if (gateSignal.aborted) onAbort();
672
+ else gateSignal.addEventListener('abort', onAbort, { once: true });
673
+ }
601
674
  const stopEarly = () => {
602
675
  if (stoppedEarly) return;
603
676
  stoppedEarly = true;
@@ -656,17 +729,20 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
656
729
  proc.on('error', (err) => {
657
730
  if (settled) return;
658
731
  settled = true;
659
- _clearRgTimers(timers);
732
+ clearAll();
660
733
  reject(err);
661
734
  });
662
735
  proc.on('close', (code) => {
663
736
  if (settled) return;
664
737
  settled = true;
665
- _clearRgTimers(timers);
738
+ clearAll();
666
739
  if (!stoppedEarly && buffer.length > 0) {
667
740
  pushLine(buffer);
668
741
  buffer = '';
669
742
  }
743
+ if (aborted || killReason === 'abort') {
744
+ return reject(_rgAbortError(gateSignal));
745
+ }
670
746
  if (timedOut && killReason === 'timeout') {
671
747
  if (lines.length > 0) {
672
748
  return resolve({
@@ -154,12 +154,17 @@ export async function tryReadFamilyEnoentRedirect({
154
154
  errCode,
155
155
  options,
156
156
  rerun,
157
+ onRedirect,
157
158
  cache = null,
158
159
  }) {
159
160
  if (options?._enoentRedirectFrom) return null;
160
161
  const target = resolveUniqueEnoentRedirect(workDir, resolvedPath, errCode, cache);
161
162
  if (!target) return null;
162
163
  const body = await rerun(target, { ...options, _enoentRedirectFrom: resolvedPath });
164
+ if (!(typeof body === 'string' && /^\s*Error[\s:[]/i.test(body))
165
+ && typeof onRedirect === 'function') {
166
+ try { onRedirect(resolvedPath, target); } catch { /* best-effort proof recording */ }
167
+ }
163
168
  const shown = requestedPath ?? resolvedPath;
164
169
  return redirectedFromPrefix(shown) + body;
165
170
  }
@@ -588,7 +588,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
588
588
  ? GREP_CONTEXT_LINE_HARD_CAP
589
589
  : Math.min(GREP_CONTEXT_LINE_HARD_CAP, Math.max(200, blockBudget * Math.max(1, perBlock) + 8));
590
590
  let ctxPartialSuffix = '';
591
- const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd }, { offset: 0, limit: lineCap, summaryLimit: 0 });
591
+ const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd, signal: options.signal }, { offset: 0, limit: lineCap, summaryLimit: 0 });
592
592
  const allLines = streamed.lines;
593
593
  let ctxTotalKnown = streamed.complete;
594
594
  if (streamed.partial) {
@@ -635,7 +635,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
635
635
  let rgPartialSuffix = '';
636
636
  if (effectiveHeadLimit !== Infinity) {
637
637
  const summaryLimit = outputMode === 'content' ? 120 : 0;
638
- const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd }, {
638
+ const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd, signal: options.signal }, {
639
639
  offset,
640
640
  limit: effectiveHeadLimit,
641
641
  summaryLimit,
@@ -652,7 +652,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
652
652
  : '\n[warning] rg exit 2 (partial results)';
653
653
  }
654
654
  } else {
655
- const stdout = await runRg(rgArgs, { cwd: rgSpawnCwd });
655
+ const stdout = await runRg(rgArgs, { cwd: rgSpawnCwd, signal: options.signal });
656
656
  const allLines = String(stdout).split('\n').filter(Boolean);
657
657
  windowed = offset > 0 ? allLines.slice(offset) : allLines;
658
658
  totalWindowed = windowed.length;
@@ -718,7 +718,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
718
718
  fileType,
719
719
  onlyMatching: false,
720
720
  });
721
- const probeOut = await runRg(probeArgs, { cwd: rgSpawnCwd });
721
+ const probeOut = await runRg(probeArgs, { cwd: rgSpawnCwd, signal: options.signal });
722
722
  if (String(probeOut).split('\n').some(Boolean)) {
723
723
  body += ' (case-insensitive would match — try -i)';
724
724
  }
@@ -781,7 +781,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
781
781
  let totalKnown = true;
782
782
  let rgPartialSuffix = '';
783
783
  if (effectiveHeadLimit !== Infinity) {
784
- const streamed = await runRgWindowedLines(fallbackArgs, { cwd: rgSpawnCwd }, {
784
+ const streamed = await runRgWindowedLines(fallbackArgs, { cwd: rgSpawnCwd, signal: options.signal }, {
785
785
  offset,
786
786
  limit: effectiveHeadLimit,
787
787
  summaryLimit: outputMode === 'content' ? 120 : 0,
@@ -798,7 +798,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
798
798
  : '\n[warning] rg exit 2 (partial fixed-string fallback results)';
799
799
  }
800
800
  } else {
801
- const stdout = await runRg(fallbackArgs, { cwd: rgSpawnCwd });
801
+ const stdout = await runRg(fallbackArgs, { cwd: rgSpawnCwd, signal: options.signal });
802
802
  const allLines = String(stdout).split('\n').filter(Boolean);
803
803
  windowed = offset > 0 ? allLines.slice(offset) : allLines;
804
804
  totalWindowed = windowed.length;
@@ -1073,7 +1073,7 @@ export async function executeGlobTool(args, workDir, options = {}) {
1073
1073
  };
1074
1074
  }
1075
1075
  try {
1076
- const stdout = await runRg(rgArgs, { cwd: rgCwd, timeout: 10000 });
1076
+ const stdout = await runRg(rgArgs, { cwd: rgCwd, timeout: 10000, signal: options.signal });
1077
1077
  const stdoutTruncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
1078
1078
  const stdoutPartial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
1079
1079
  const paths = [];
@@ -113,7 +113,7 @@ async function sweepStaleShellJobs(dir) {
113
113
  });
114
114
  await Promise.all([
115
115
  ...expired.flatMap((jobId) =>
116
- ['.json', '.done', '.exit', '.enforced', '.exit.cmd.sh', '.exit.cmd.ps1', '.exit.user.ps1', '.stdout.log', '.stderr.log'].map((ext) =>
116
+ ['.json', '.done', '.exit', '.enforced', '.guardian', '.exit.cmd.sh', '.exit.cmd.ps1', '.exit.user.ps1', '.stdout.log', '.stderr.log'].map((ext) =>
117
117
  fsPromises.unlink(join(dir, jobId + ext)).catch(() => {}),
118
118
  ),
119
119
  ),
@@ -212,6 +212,11 @@ export function shellJobDonePath(jobId) { return join(getShellJobsDir(), `${jobI
212
212
  // wrapper's own env/cwd resolution). PS jobs don't need it — their wrapper
213
213
  // enforces unconditionally and records detail.timeoutEnforced:true.
214
214
  export function shellJobEnforcedPath(jobId) { return join(getShellJobsDir(), `${jobId}.enforced`); }
215
+ // Guardian kill receipt: written by the detached child-guardian immediately
216
+ // before it force-kills the job tree (host memory floor / orphaned parent).
217
+ // Its existence lets refreshShellJob attribute a marker-less death to the
218
+ // guardian instead of reporting an unexplained unknown exit.
219
+ export function shellJobGuardianReceiptPath(jobId) { return join(getShellJobsDir(), `${jobId}.guardian`); }
215
220
  // Owner sidecar marker: a zero-byte file whose NAME encodes the owning CC host
216
221
  // (claude.exe) pid — `<jobId>.owner-<pid>`. It lets the statusline owner-filter
217
222
  // jobs from a SINGLE directory listing (no per-job JSON read) so the filter can
@@ -27,6 +27,7 @@ import {
27
27
  shellJobExitPath,
28
28
  shellJobDonePath,
29
29
  shellJobEnforcedPath,
30
+ shellJobGuardianReceiptPath,
30
31
  resolveJobOwnerHostPid,
31
32
  trimShellJobSpill,
32
33
  writeShellJobDetail,
@@ -71,7 +72,13 @@ globalThis.__mixdogShellJobsRuntimeLoaded = true;
71
72
  // Poll cadence for the adopted-job output-cap self-tick (mirrors the
72
73
  // foreground sizeWatchdog in shell-command.mjs).
73
74
 
74
- import { refreshShellJob, trackChildUntilConfirmedExit, releaseShellJobOwnershipWhenQuiescent, TIMER_MAX_MS } from './shell-jobs.mjs';
75
+ import {
76
+ refreshShellJob,
77
+ trackChildUntilConfirmedExit,
78
+ releaseShellJobOwnershipWhenQuiescent,
79
+ reconcileShellJobAfterQuiescence,
80
+ TIMER_MAX_MS,
81
+ } from './shell-jobs.mjs';
75
82
 
76
83
  export async function _startBackgroundShellJobImpl({
77
84
  command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellArgs,
@@ -261,6 +268,8 @@ export async function _startBackgroundShellJobImpl({
261
268
  childPid: child.pid,
262
269
  childGroupPid: child.pid,
263
270
  label: 'shell-job',
271
+ protectHostMemory: true,
272
+ receiptPath: shellJobGuardianReceiptPath(jobId),
264
273
  });
265
274
  _installShellJobsExitHook();
266
275
  _registerLiveJobPid(child.pid, jobId);
@@ -268,6 +277,10 @@ export async function _startBackgroundShellJobImpl({
268
277
  releaseShellJobOwnershipWhenQuiescent(jobId, child.pid, {
269
278
  allowLateLease: true,
270
279
  deferUntilRootExit: true,
280
+ // Exit-event ownership (CC-aligned): when the whole tree is gone but
281
+ // the wrapper never published markers, settle the job honestly instead
282
+ // of leaving a permanently-'running' or falsely-'failed' record.
283
+ onConfirmed: () => { try { reconcileShellJobAfterQuiescence(jobId); } catch { /* best-effort */ } },
271
284
  });
272
285
  // Deadline cleanup poke only when a timeout is enforced; an unlimited
273
286
  // (timeoutMs<=0) job has no deadline — completion is observed via the
@@ -503,6 +516,8 @@ async function startBackgroundPowerShellJob({
503
516
  childPid,
504
517
  childGroupPid: childPid,
505
518
  label: 'shell-job-powershell',
519
+ protectHostMemory: true,
520
+ receiptPath: shellJobGuardianReceiptPath(jobId),
506
521
  });
507
522
  _installShellJobsExitHook();
508
523
  _registerLiveJobPid(childPid, jobId);
@@ -510,6 +525,7 @@ async function startBackgroundPowerShellJob({
510
525
  releaseShellJobOwnershipWhenQuiescent(jobId, childPid, {
511
526
  allowLateLease: true,
512
527
  deferUntilRootExit: true,
528
+ onConfirmed: () => { try { reconcileShellJobAfterQuiescence(jobId); } catch { /* best-effort */ } },
513
529
  });
514
530
  if (Number(timeoutMs) > 0) {
515
531
  const timer = setTimeout(() => { refreshShellJob(jobId); }, Math.min(TIMER_MAX_MS, timeoutMs + 25));
@@ -27,6 +27,7 @@ import {
27
27
  shellJobExitPath,
28
28
  shellJobDonePath,
29
29
  shellJobEnforcedPath,
30
+ shellJobGuardianReceiptPath,
30
31
  resolveJobOwnerHostPid,
31
32
  trimShellJobSpill,
32
33
  writeShellJobDetail,
@@ -134,6 +135,65 @@ export function killShellJob(jobId) {
134
135
  return { ...attachJobInsights(detail), killed: true };
135
136
  }
136
137
 
138
+ // CC-aligned verdict for a job whose tracked root process died WITHOUT the
139
+ // wrapper publishing the done marker. Evidence order:
140
+ // 1. exit file — the wrapper writes exit BEFORE done, so a kill in that
141
+ // window still left the real exit code; recover it instead of guessing.
142
+ // 2. guardian receipt — the child-guardian records why it force-killed the
143
+ // tree (host memory floor / orphaned parent) before killing.
144
+ // 3. otherwise — honest unknown: terminal 'failed' with exitCode null and
145
+ // an explicit "exit status unknown" error; spilled output is preserved
146
+ // and the completion notification carries its tail.
147
+ function finalizeMarkerlessShellJob(detail) {
148
+ const jobId = detail.jobId;
149
+ detail.finishedAt = new Date().toISOString();
150
+ let recoveredExit = null;
151
+ try {
152
+ const raw = readFileSync(shellJobExitPath(jobId), 'utf-8').trim();
153
+ const parsed = parseInt(raw, 10);
154
+ if (Number.isFinite(parsed)) recoveredExit = parsed;
155
+ } catch { /* no exit file */ }
156
+ if (recoveredExit !== null) {
157
+ detail.status = recoveredExit === 0 ? 'completed' : 'failed';
158
+ detail.exitCode = recoveredExit;
159
+ detail.note = 'done marker missing; exit code recovered from exit file';
160
+ } else {
161
+ let receipt = null;
162
+ try {
163
+ receipt = JSON.parse(readFileSync(shellJobGuardianReceiptPath(jobId), 'utf-8'));
164
+ } catch { /* no guardian receipt */ }
165
+ detail.status = 'failed';
166
+ detail.exitCode = 137;
167
+ if (receipt?.reason === 'host-memory-floor') {
168
+ const freeMb = Math.round(Number(receipt.freeBytes || 0) / (1024 * 1024));
169
+ const floorMb = Math.round(Number(receipt.minFreeMemoryBytes || 0) / (1024 * 1024));
170
+ detail.killedByGuardian = true;
171
+ detail.error = `killed by child guardian: host free memory ${freeMb} MB fell below the ${floorMb} MB floor — job tree force-killed to protect the host; retry with lower concurrency or after memory recovers`;
172
+ } else if (receipt?.reason === 'parent-exit') {
173
+ detail.killedByGuardian = true;
174
+ detail.error = 'killed by child guardian: owning runtime process exited (session cleanup)';
175
+ } else {
176
+ detail.exitCode = null;
177
+ detail.error = 'exit status unknown — tracked process exited without reporting an exit code; spilled output preserved (see stdout tail)';
178
+ }
179
+ }
180
+ trimShellJobSpill(detail);
181
+ writeShellJobDetail(detail);
182
+ releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid);
183
+ return detail;
184
+ }
185
+
186
+ // Quiescence-deferred reconcile for the owning runtime: called when the
187
+ // spawn-time process-tree tracker confirms the whole tree is gone. If the
188
+ // wrapper never published markers (e.g. it was force-killed while detached
189
+ // descendants kept running), settle the job honestly now.
190
+ export function reconcileShellJobAfterQuiescence(jobId) {
191
+ const detail = readShellJobDetail(jobId);
192
+ if (!detail || detail.status !== 'running') return detail;
193
+ if (existsSync(shellJobDonePath(jobId))) return refreshShellJob(jobId);
194
+ return finalizeMarkerlessShellJob(detail);
195
+ }
196
+
137
197
  export function refreshShellJob(jobId) {
138
198
  const detail = readShellJobDetail(jobId);
139
199
  if (!detail) return null;
@@ -207,11 +267,31 @@ export function refreshShellJob(jobId) {
207
267
  return detail;
208
268
  }
209
269
  if (detail.pid && !isPidAlive(detail.pid)) {
210
- detail.status = 'failed';
211
- detail.finishedAt = new Date().toISOString();
212
- detail.error = 'process exited without completion marker';
213
- writeShellJobDetail(detail);
214
- releaseShellJobOwnershipWhenQuiescent(jobId, detail.pid);
270
+ // Root process died without markers. If this runtime owns a live
271
+ // process-tree tracker that still sees descendants (a killed wrapper
272
+ // whose real workload survived — observed with detached grandchildren),
273
+ // the JOB is still running: keep status 'running', flag the root exit,
274
+ // and let tree quiescence settle it via reconcileShellJobAfterQuiescence.
275
+ const trackerEntry = shellJobQuiescenceTrackers.get(jobId);
276
+ if (trackerEntry?.tracker?.pending) {
277
+ if (!detail.rootExitedAt) {
278
+ detail.rootExitedAt = new Date().toISOString();
279
+ detail.note = 'root process exited without an exit report; descendants still running — tracking tree to completion';
280
+ writeShellJobDetail(detail);
281
+ // Belt-and-braces for jobs without a spawn-time onConfirmed
282
+ // (adopted/legacy): settle once the tree is confirmed gone.
283
+ // Guarded by the rootExitedAt stamp so poll ticks cannot grow
284
+ // the callback set unboundedly.
285
+ trackerEntry.callbacks.add(() => reconcileShellJobAfterQuiescence(jobId));
286
+ }
287
+ // Root death is now observed: let the deferred tracker start
288
+ // confirming tree quiescence (idempotent past the first call).
289
+ trackerEntry.tracker.rootExited?.();
290
+ return detail;
291
+ }
292
+ // No live tracker (cross-process reader or tree already quiescent):
293
+ // settle now with the strongest available evidence.
294
+ return finalizeMarkerlessShellJob(detail);
215
295
  }
216
296
  return detail;
217
297
  }
@@ -9,6 +9,16 @@ import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
9
9
  // worker A's read satisfy worker B's edit-gate across sessions via a persisted
10
10
  // __global__.json).
11
11
  const readFilesByScope = new Map(); // scope → Map(fullPath → { mtimeMs, size, ...meta })
12
+ // Same-process, same-scope aliases established only after a successful
13
+ // read-family ENOENT redirect. These are intentionally not persisted: an
14
+ // apply_patch call may reuse a path correction proven by a read in the live
15
+ // session, but must never inherit a guessed redirect from another process.
16
+ const readRedirectsByScope = new Map(); // scope → Map(requestedPathKey → targetPath)
17
+
18
+ function snapshotPathKey(fullPath) {
19
+ const value = String(fullPath || '');
20
+ return process.platform === 'win32' ? value.toLowerCase() : value;
21
+ }
12
22
 
13
23
  // ── Disk-persisted snapshot store ────────────────────────────────────────
14
24
  // Mirror the in-memory readFilesByScope to per-scope JSON files under
@@ -160,6 +170,14 @@ export function deleteReadSnapshotPathEverywhere(fullPath) {
160
170
  for (const [scopeKey, readFiles] of readFilesByScope.entries()) {
161
171
  if (readFiles.delete(fullPath)) scheduleScopePersist(scopeKey);
162
172
  }
173
+ const deletedKey = snapshotPathKey(fullPath);
174
+ for (const redirects of readRedirectsByScope.values()) {
175
+ for (const [requestedKey, targetPath] of redirects.entries()) {
176
+ if (requestedKey === deletedKey || snapshotPathKey(targetPath) === deletedKey) {
177
+ redirects.delete(requestedKey);
178
+ }
179
+ }
180
+ }
163
181
  if (!SNAPSHOT_DIR) return;
164
182
  let entries;
165
183
  try { entries = readdirSync(SNAPSHOT_DIR); }
@@ -209,6 +227,40 @@ export function readScopeKey(scope) {
209
227
  return scope ? String(scope) : null;
210
228
  }
211
229
 
230
+ export function recordReadPathRedirect(requestedFullPath, targetFullPath, scope) {
231
+ const scopeKey = readScopeKey(scope);
232
+ if (scopeKey === null) return false;
233
+ const requested = String(requestedFullPath || '');
234
+ const target = String(targetFullPath || '');
235
+ if (!requested || !target || snapshotPathKey(requested) === snapshotPathKey(target)) return false;
236
+ // The redirect is trusted only after the target produced a scoped read
237
+ // snapshot. A search/list-only redirect cannot authorize a later edit.
238
+ if (!readFilesForScope(scope).has(target)) return false;
239
+ let redirects = readRedirectsByScope.get(scopeKey);
240
+ if (!redirects) {
241
+ redirects = new Map();
242
+ readRedirectsByScope.set(scopeKey, redirects);
243
+ }
244
+ redirects.set(snapshotPathKey(requested), target);
245
+ return true;
246
+ }
247
+
248
+ export function resolveReadPathRedirect(requestedFullPath, scope) {
249
+ const scopeKey = readScopeKey(scope);
250
+ if (scopeKey === null) return null;
251
+ const redirects = readRedirectsByScope.get(scopeKey);
252
+ if (!redirects) return null;
253
+ const requestedKey = snapshotPathKey(requestedFullPath);
254
+ const target = redirects.get(requestedKey);
255
+ if (!target) return null;
256
+ // Fail closed if the proving snapshot was cleared after a mutation.
257
+ if (!readFilesForScope(scope).has(target)) {
258
+ redirects.delete(requestedKey);
259
+ return null;
260
+ }
261
+ return target;
262
+ }
263
+
212
264
  export function readFilesForScope(scope) {
213
265
  const key = readScopeKey(scope);
214
266
  if (key === null) {