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.
- package/package.json +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- 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
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
507
|
+
clearAll();
|
|
470
508
|
reject(err);
|
|
471
509
|
});
|
|
472
510
|
proc.on('close', (code) => {
|
|
473
511
|
if (settled) return;
|
|
474
512
|
settled = true;
|
|
475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
732
|
+
clearAll();
|
|
660
733
|
reject(err);
|
|
661
734
|
});
|
|
662
735
|
proc.on('close', (code) => {
|
|
663
736
|
if (settled) return;
|
|
664
737
|
settled = true;
|
|
665
|
-
|
|
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 {
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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) {
|