mixdog 0.9.89 → 0.9.91
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 +5 -1
- package/src/output-styles/detailed.md +27 -0
- package/src/output-styles/extreme-minimal.md +8 -8
- package/src/output-styles/minimal.md +7 -10
- package/src/output-styles/simple.md +13 -21
- package/src/rules/lead/01-general.md +9 -1
- package/src/rules/lead/lead-brief.md +15 -14
- package/src/rules/shared/01-tool.md +31 -37
- package/src/runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +35 -1
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +28 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +116 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -11
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -10
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +16 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +176 -16
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +30 -1
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +96 -5
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +72 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +48 -9
- package/src/runtime/channels/backends/discord.mjs +21 -1
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/trace-store.mjs +25 -3
- package/src/runtime/memory/tool-defs.mjs +5 -5
- package/src/runtime/search/tool-defs.mjs +2 -18
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/session-runtime/output-styles.mjs +7 -6
- package/src/session-runtime/tool-defs.mjs +0 -1
- package/src/session-runtime/tool-surface.mjs +9 -0
- package/src/session-runtime/workflow.mjs +7 -0
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/workflows/default/WORKFLOW.md +2 -1
- package/src/workflows/solo/WORKFLOW.md +7 -5
- package/src/output-styles/default.md +0 -40
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { statSync } from 'fs';
|
|
1
|
+
import { closeSync, openSync, readSync, statSync, unlinkSync } from 'fs';
|
|
2
2
|
import { assertPathReachable, assertPathsReachable } from './fs-reachability.mjs';
|
|
3
|
-
import { isAbsolute, resolve } from 'path';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
4
6
|
import { WRAPPER_NAMES } from '../shell-policy.mjs';
|
|
5
7
|
import {
|
|
6
8
|
cwdRelativePath,
|
|
@@ -761,7 +763,10 @@ export async function analyzeShellCommandEffects(command, cwd) {
|
|
|
761
763
|
// its deadline. Only the first top-level segment is considered — sleeps inside
|
|
762
764
|
// pipelines/subshells/scripts are legitimate pacing and pass through. Float
|
|
763
765
|
// durations (sleep 0.5) are allowed, mirroring the reference CLI.
|
|
764
|
-
|
|
766
|
+
// Exported: bash-tool promotes a detected sleep-chain to a background task
|
|
767
|
+
// BEFORE foregroundLongCommandHint runs, so in the one-shot shell path this
|
|
768
|
+
// deny only survives for callers that do not promote (task-tool preflight).
|
|
769
|
+
export function detectBlockedSleepPattern(command) {
|
|
765
770
|
const cmd = String(command || '').trim();
|
|
766
771
|
if (!cmd) return null;
|
|
767
772
|
const sep = cmd.match(/&&|\|\||;|\|/);
|
|
@@ -782,22 +787,62 @@ function detectBlockedSleepPattern(command) {
|
|
|
782
787
|
return rest ? `sleep ${secs} followed by: ${rest.slice(0, 80)}` : `standalone sleep ${secs}`;
|
|
783
788
|
}
|
|
784
789
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
790
|
+
// Codex-parity shell interception (core maybe_parse_apply_patch_verified):
|
|
791
|
+
// codex-trained models type `apply_patch <<'EOF' … EOF` INTO THE SHELL. No
|
|
792
|
+
// such binary exists here, so the invocation is extracted and routed to the
|
|
793
|
+
// internal apply_patch engine instead of dying as "command not found".
|
|
794
|
+
// Returns null when the command is not an apply_patch invocation, { patch }
|
|
795
|
+
// when one was extracted, or { error } for a recognized-but-malformed call.
|
|
796
|
+
export function extractShellApplyPatchInvocation(command) {
|
|
797
|
+
let cmd = String(command || '').trim();
|
|
798
|
+
if (!cmd) return null;
|
|
799
|
+
// Unwrap one `bash|sh|zsh -lc '<script>'` / `-c "<script>"` layer.
|
|
800
|
+
const wrap = /^(?:bash|sh|zsh)\s+-l?c\s+(['"])([\s\S]*)\1\s*$/.exec(cmd);
|
|
801
|
+
if (wrap) cmd = wrap[2].trim();
|
|
802
|
+
// A bare patch pasted into the shell (codex ImplicitInvocation case) is
|
|
803
|
+
// unambiguous — route it to the engine directly.
|
|
804
|
+
if (cmd.startsWith('*** Begin Patch')) return { patch: cmd };
|
|
805
|
+
if (!/^apply_patch(?:\s|$)/.test(cmd)) return null;
|
|
806
|
+
const rest = cmd.slice('apply_patch'.length).trim();
|
|
807
|
+
// Heredoc form: apply_patch <<'EOF' \n <patch> \n EOF
|
|
808
|
+
const heredoc = /^<<-?\s*(['"]?)(\w+)\1\s*\n([\s\S]*)$/.exec(rest);
|
|
809
|
+
if (heredoc) {
|
|
810
|
+
const terminator = heredoc[2];
|
|
811
|
+
const lines = heredoc[3].split('\n');
|
|
812
|
+
const endIdx = lines.findIndex((l) => l.trim() === terminator);
|
|
813
|
+
const patch = (endIdx === -1 ? lines : lines.slice(0, endIdx)).join('\n').trim();
|
|
814
|
+
if (patch) return { patch };
|
|
815
|
+
return { error: 'apply_patch heredoc contained no patch body' };
|
|
816
|
+
}
|
|
817
|
+
// Single-argument form: apply_patch '<patch>' / "<patch>" / bare.
|
|
818
|
+
if (rest) {
|
|
819
|
+
const q = rest[0];
|
|
820
|
+
let patch = rest;
|
|
821
|
+
if ((q === "'" || q === '"') && rest.length >= 2 && rest.endsWith(q)) {
|
|
822
|
+
patch = rest.slice(1, -1);
|
|
823
|
+
if (q === '"') patch = patch.replace(/\\([\\"$`])/g, '$1');
|
|
824
|
+
else patch = patch.replace(/'\\''/g, "'");
|
|
795
825
|
}
|
|
826
|
+
patch = patch.trim();
|
|
827
|
+
if (patch.startsWith('*** Begin Patch')) return { patch };
|
|
828
|
+
return { error: 'apply_patch argument did not contain a V4A patch (expected "*** Begin Patch")' };
|
|
796
829
|
}
|
|
797
|
-
|
|
830
|
+
return { error: 'apply_patch requires the patch text (heredoc or single argument)' };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Watch-like / long-blocking-sleep detection shared by bash-tool's auto-async
|
|
834
|
+
// promotion and foregroundLongCommandHint. Returns a short human reason when
|
|
835
|
+
// the command would block the foreground for a long time (dev server, watcher,
|
|
836
|
+
// or a 30s+ sleep anywhere in the chain), else null. bash-tool promotes these
|
|
837
|
+
// to a background task instead of failing the call — measured 2026-08: the
|
|
838
|
+
// old hard deny fired ~22×/14d and every hit was a wasted turn.
|
|
839
|
+
export function detectLongForegroundReason(command) {
|
|
840
|
+
const cmd = String(command || '').trim();
|
|
841
|
+
if (!cmd) return null;
|
|
798
842
|
const watchLike = /^\s*gh\s+run\s+watch(?:\s|$)/i.test(cmd)
|
|
799
843
|
|| /^\s*(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|watch|serve)(?:\s|$)/i.test(cmd)
|
|
800
844
|
|| /^\s*(?:vite|webpack-dev-server|next|nuxt|astro)\s+(?:dev|start)(?:\s|$)/i.test(cmd);
|
|
845
|
+
if (watchLike) return 'watch-like long-running command (dev server / watcher)';
|
|
801
846
|
const longPowerShellSleep = [...cmd.matchAll(/\bStart-Sleep\b([^;&|\r\n]*)/gi)].some((m) => {
|
|
802
847
|
const part = String(m[1] || '');
|
|
803
848
|
const ms = part.match(/-(?:Milliseconds|m)\s+(\d+)/i);
|
|
@@ -806,8 +851,23 @@ export function foregroundLongCommandHint(command, timeoutMs, args = {}, opts =
|
|
|
806
851
|
return sec ? Number(sec[1]) >= 30 : false;
|
|
807
852
|
});
|
|
808
853
|
const longSleep = /\bsleep\s+(?:[3-9]\d|\d{3,}|\d+[mh])\b/i.test(cmd) || longPowerShellSleep;
|
|
809
|
-
if (
|
|
810
|
-
|
|
854
|
+
if (longSleep) return 'long blocking sleep (30s+) in command';
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function foregroundLongCommandHint(command, timeoutMs, args = {}, opts = {}) {
|
|
859
|
+
if (args.run_in_background === true) return '';
|
|
860
|
+
const cmd = String(command || '').trim();
|
|
861
|
+
if (!cmd) return '';
|
|
862
|
+
// CC validateInput parity: block only while background tasks are enabled —
|
|
863
|
+
// the remedy we point at (async mode / completion notification) must exist.
|
|
864
|
+
if (opts.backgroundTasksDisabled !== true) {
|
|
865
|
+
const blocked = detectBlockedSleepPattern(cmd);
|
|
866
|
+
if (blocked) {
|
|
867
|
+
return `Error: blocked — ${blocked}. Run blocking commands as a background task (mode:"async") and act on the completion notification, or use task wait on an existing task_id. If you genuinely need a delay (rate limiting, pacing), keep it under 2 seconds.`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (!detectLongForegroundReason(cmd)) return '';
|
|
811
871
|
return 'Error: long foreground command detected. Use mode:"async" (background task) or task wait instead of blocking sleeps/watch loops.';
|
|
812
872
|
}
|
|
813
873
|
|
|
@@ -870,3 +930,103 @@ export function isAutobackgroundingAllowed(command, shellType) {
|
|
|
870
930
|
}
|
|
871
931
|
return true;
|
|
872
932
|
}
|
|
933
|
+
|
|
934
|
+
// ---------------------------------------------------------------------------
|
|
935
|
+
// Filter-swallow rescue (PowerShell one-shot path). Measured 2026-08: ~37
|
|
936
|
+
// failures/14d were `<producer> 2>&1 | Select-String … | Select-Object …`
|
|
937
|
+
// pipelines where the producer failed but the trailing filters matched
|
|
938
|
+
// nothing — the model saw `[exit code: 1]` + `(no output)` and burned turns
|
|
939
|
+
// re-running with different capture tricks. Plan: insert a Tee-Object spill
|
|
940
|
+
// of the UNFILTERED producer stream so a failing run can attach the original
|
|
941
|
+
// tail in the SAME call. Applies only to the exactly-recognized shape; any
|
|
942
|
+
// ambiguity returns null and the original command runs untouched:
|
|
943
|
+
// - single-line command, no backticks / $( ) / ${ } / here-strings / procsub
|
|
944
|
+
// - no existing Tee-Object
|
|
945
|
+
// - last top-level segment is a byte-exact suffix of the command
|
|
946
|
+
// - >= 2 pipeline stages; stage 0 is NOT a pure filter; stages 1..n are ALL
|
|
947
|
+
// known pure pass-through filters; no stage redirects to a file
|
|
948
|
+
// Tee-Object passes objects through unchanged and does not touch
|
|
949
|
+
// $LASTEXITCODE, so success-path output and exit semantics are identical.
|
|
950
|
+
const _PS_PURE_FILTER_HEADS = new Set([
|
|
951
|
+
'select-string', 'sls', 'select-object', 'select',
|
|
952
|
+
'where-object', 'where', 'foreach-object', '%', 'findstr',
|
|
953
|
+
]);
|
|
954
|
+
const _TEE_AMBIGUOUS_RE = /[`]|\$\(|\$\{|@['"]|<\(|>\(/;
|
|
955
|
+
|
|
956
|
+
function _teeStageHead(tokens) {
|
|
957
|
+
return String(tokens?.[0] || '').toLowerCase().replace(/^.*[\\/]/, '').replace(/\.exe$/, '');
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function _stageRedirectsToFile(tokens) {
|
|
961
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
962
|
+
if (tokens[i] === '>' || tokens[i] === '>>') {
|
|
963
|
+
const next = String(tokens[i + 1] || '');
|
|
964
|
+
if (!next.startsWith('&')) return true; // `> file` (2>&1-style stays allowed)
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function buildPowerShellFilterTeePlan(command) {
|
|
971
|
+
const cmd = String(command || '');
|
|
972
|
+
if (!cmd.trim()) return null;
|
|
973
|
+
if (/\r|\n/.test(cmd)) return null;
|
|
974
|
+
if (/tee-object/i.test(cmd)) return null;
|
|
975
|
+
if (_TEE_AMBIGUOUS_RE.test(cmd)) return null;
|
|
976
|
+
const segments = shellSplitSegments(cmd);
|
|
977
|
+
if (segments.length === 0) return null;
|
|
978
|
+
const last = segments[segments.length - 1];
|
|
979
|
+
const trimmed = cmd.trimEnd();
|
|
980
|
+
if (!trimmed.endsWith(last)) return null;
|
|
981
|
+
const stages = shellSplitPipelineSegments(last);
|
|
982
|
+
if (stages.length < 2) return null;
|
|
983
|
+
const tokenized = [];
|
|
984
|
+
for (const stage of stages) {
|
|
985
|
+
const tokens = shellTokenize(stage);
|
|
986
|
+
if (!tokens || tokens.length === 0) return null;
|
|
987
|
+
if (_stageRedirectsToFile(tokens)) return null;
|
|
988
|
+
const stripped = stripShellProbeWrappers(tokens);
|
|
989
|
+
if (stripped.length === 0) return null;
|
|
990
|
+
tokenized.push(stripped);
|
|
991
|
+
}
|
|
992
|
+
if (_PS_PURE_FILTER_HEADS.has(_teeStageHead(tokenized[0]))) return null;
|
|
993
|
+
for (let i = 1; i < tokenized.length; i++) {
|
|
994
|
+
if (!_PS_PURE_FILTER_HEADS.has(_teeStageHead(tokenized[i]))) return null;
|
|
995
|
+
}
|
|
996
|
+
const teePath = join(tmpdir(), `mixdog-tee-${randomUUID().slice(0, 8)}.log`);
|
|
997
|
+
if (teePath.includes("'")) return null;
|
|
998
|
+
const head = trimmed.slice(0, trimmed.length - last.length);
|
|
999
|
+
const rewritten = `${head}${stages[0]} | Tee-Object -FilePath '${teePath}' | ${stages.slice(1).join(' | ')}`;
|
|
1000
|
+
return { command: rewritten, teePath };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Read the tail of a tee spill file (BOM-sniffed: Windows PowerShell 5.1
|
|
1004
|
+
// writes UTF-16LE, pwsh 7 writes UTF-8) and ALWAYS delete the file. Returns
|
|
1005
|
+
// null when the file is missing/empty/unreadable.
|
|
1006
|
+
export function consumeFilterTeeCapture(teePath, { maxBytes = 16384 } = {}) {
|
|
1007
|
+
let out = null;
|
|
1008
|
+
try {
|
|
1009
|
+
const st = statSync(teePath);
|
|
1010
|
+
if (st.size > 0) {
|
|
1011
|
+
const fd = openSync(teePath, 'r');
|
|
1012
|
+
try {
|
|
1013
|
+
const bom = Buffer.alloc(2);
|
|
1014
|
+
readSync(fd, bom, 0, 2, 0);
|
|
1015
|
+
const utf16 = bom[0] === 0xff && bom[1] === 0xfe;
|
|
1016
|
+
let start = Math.max(0, st.size - maxBytes);
|
|
1017
|
+
if (utf16 && start % 2 === 1) start += 1;
|
|
1018
|
+
const len = st.size - start;
|
|
1019
|
+
const buf = Buffer.alloc(len);
|
|
1020
|
+
readSync(fd, buf, 0, len, start);
|
|
1021
|
+
out = buf.toString(utf16 ? 'utf16le' : 'utf8');
|
|
1022
|
+
if (start === 0 && out.charCodeAt(0) === 0xfeff) out = out.slice(1);
|
|
1023
|
+
} finally {
|
|
1024
|
+
closeSync(fd);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
} catch {
|
|
1028
|
+
out = null;
|
|
1029
|
+
}
|
|
1030
|
+
try { unlinkSync(teePath); } catch { /* best-effort cleanup */ }
|
|
1031
|
+
return out && out.trim() ? out : null;
|
|
1032
|
+
}
|
|
@@ -3,7 +3,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
3
3
|
name: 'code_graph',
|
|
4
4
|
title: 'Code Graph',
|
|
5
5
|
annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
6
|
-
description: 'Repo code structure/flow over
|
|
6
|
+
description: 'Repo code structure/flow over source files only. File modes take files[]; symbol modes find_symbol/symbol_search/search/references/callers/callees take symbols[]. Exact identifiers use find_symbol/references/callers/callees and keywords use symbol_search/search. Unsupported target arrays are omitted, never silently mixed.',
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -48,6 +48,12 @@ const EXPECTED_ARGS = Object.freeze(['-NoLogo', '-NoProfile', '-NonInteractive',
|
|
|
48
48
|
// with the same `$?`→0/1 exit mapping pwsh -Command applies implicitly, then
|
|
49
49
|
// emits `<<<MIXDOG_DONE_<nonce>:<code>>>>` on stdout and
|
|
50
50
|
// `<<<MIXDOG_DONE_<nonce>>>>` on stderr (both streams provably drained).
|
|
51
|
+
// Each command pipes through Microsoft.PowerShell.Core\Out-Default: the whole
|
|
52
|
+
// marker loop is ONE top-level pipeline, so without an explicit per-command
|
|
53
|
+
// formatting terminator Format-Table kept object output buffered PAST the done
|
|
54
|
+
// sentinel and captures came back empty (`Get-ChildItem | Select-Object` →
|
|
55
|
+
// no output). The per-command Out-Default flushes formatted output before the
|
|
56
|
+
// sentinel is written, restoring one-shot `pwsh -Command` parity.
|
|
51
57
|
// stdin EOF exits the process with the LAST command's code — that is exactly
|
|
52
58
|
// what background-promotion relies on for its exit-file. A user script that
|
|
53
59
|
// calls `exit N` terminates the whole process with N (identical code to the
|
|
@@ -63,6 +69,20 @@ function _standbyBootScript(nonce) {
|
|
|
63
69
|
'$__baseEnv = @{}',
|
|
64
70
|
'Get-ChildItem env: | ForEach-Object { $__baseEnv[$_.Name] = $_.Value }',
|
|
65
71
|
'$__baseLoc = (Get-Location).ProviderPath',
|
|
72
|
+
// Session-isolation baselines: env+cwd alone leaked GLOBAL functions,
|
|
73
|
+
// aliases, variables, modules, and preference values across commands
|
|
74
|
+
// (the pool is daemon-global, so across SESSIONS too). Only global
|
|
75
|
+
// scope can leak — a scriptblock's local defs die with `& $__sc`.
|
|
76
|
+
'$__baseFn = @{}',
|
|
77
|
+
'Get-ChildItem function: | ForEach-Object { $__baseFn[$_.Name] = $true }',
|
|
78
|
+
'$__baseAlias = @{}',
|
|
79
|
+
'Get-ChildItem alias: | ForEach-Object { $__baseAlias[$_.Name] = $_.Definition }',
|
|
80
|
+
'$__baseVar = @{}',
|
|
81
|
+
'Get-Variable -Scope Global -ErrorAction SilentlyContinue | ForEach-Object { $__baseVar[$_.Name] = $true }',
|
|
82
|
+
'$__baseMod = @{}',
|
|
83
|
+
'Get-Module | ForEach-Object { $__baseMod[$_.Name] = $true }',
|
|
84
|
+
'$__basePref = @{}',
|
|
85
|
+
"foreach ($__p in @('ErrorActionPreference','ProgressPreference','VerbosePreference','WarningPreference','InformationPreference','DebugPreference','ConfirmPreference')) { $__basePref[$__p] = Get-Variable -Name $__p -ValueOnly -Scope Global -ErrorAction SilentlyContinue }",
|
|
66
86
|
'$__code = 0',
|
|
67
87
|
'while ($true) {',
|
|
68
88
|
' $__buf = New-Object System.Text.StringBuilder',
|
|
@@ -74,12 +94,21 @@ function _standbyBootScript(nonce) {
|
|
|
74
94
|
' }',
|
|
75
95
|
' foreach ($__n in @(Get-ChildItem env: | ForEach-Object Name)) { if (-not $__baseEnv.ContainsKey($__n)) { Remove-Item ("env:$__n") -ErrorAction SilentlyContinue } }',
|
|
76
96
|
' foreach ($__k in $__baseEnv.Keys) { Set-Item ("env:$__k") $__baseEnv[$__k] }',
|
|
97
|
+
// Strip global state the previous command left behind; restore
|
|
98
|
+
// overwritten alias definitions and preference values. `__*` variable
|
|
99
|
+
// names are the loop's own internals (created after baseline) and are
|
|
100
|
+
// never removed.
|
|
101
|
+
' foreach ($__n in @(Get-ChildItem function: | ForEach-Object Name)) { if (-not $__baseFn.ContainsKey($__n)) { Remove-Item ("function:$__n") -Force -ErrorAction SilentlyContinue } }',
|
|
102
|
+
' foreach ($__a in @(Get-ChildItem alias:)) { if (-not $__baseAlias.ContainsKey($__a.Name)) { Remove-Item ("alias:$($__a.Name)") -Force -ErrorAction SilentlyContinue } elseif ($__a.Definition -ne $__baseAlias[$__a.Name]) { Set-Alias -Name $__a.Name -Value $__baseAlias[$__a.Name] -Scope Global -Force -ErrorAction SilentlyContinue } }',
|
|
103
|
+
' foreach ($__n in @(Get-Variable -Scope Global -ErrorAction SilentlyContinue | ForEach-Object Name)) { if ($__n -notlike "__*" -and -not $__baseVar.ContainsKey($__n)) { Remove-Variable -Name $__n -Scope Global -Force -ErrorAction SilentlyContinue } }',
|
|
104
|
+
' foreach ($__n in @(Get-Module | ForEach-Object Name)) { if (-not $__baseMod.ContainsKey($__n)) { Remove-Module -Name $__n -Force -ErrorAction SilentlyContinue } }',
|
|
105
|
+
' foreach ($__n in @($__basePref.Keys)) { Set-Variable -Name $__n -Value $__basePref[$__n] -Scope Global -Force -ErrorAction SilentlyContinue }',
|
|
77
106
|
' Set-Location -LiteralPath $__baseLoc',
|
|
78
107
|
' $global:LASTEXITCODE = 0',
|
|
79
108
|
' $global:__MIXDOG_RC = 1',
|
|
80
109
|
' try {',
|
|
81
110
|
" $__sc = [ScriptBlock]::Create($__buf.ToString() + [Environment]::NewLine + 'if ($?) { $global:__MIXDOG_RC = 0 } else { $global:__MIXDOG_RC = 1 }')",
|
|
82
|
-
|
|
111
|
+
' & $__sc | Microsoft.PowerShell.Core\\Out-Default',
|
|
83
112
|
' } catch {',
|
|
84
113
|
' $global:__MIXDOG_RC = 1',
|
|
85
114
|
" [Console]::Error.WriteLine('Exception: ' + $_.Exception.Message)",
|
|
@@ -383,7 +383,7 @@ export function splitBufferLinesForPatch(buf) {
|
|
|
383
383
|
return lines;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
function longestCommonSubstringLen(a, b, cap = 4000) {
|
|
386
|
+
export function longestCommonSubstringLen(a, b, cap = 4000) {
|
|
387
387
|
if (!a || !b) return 0;
|
|
388
388
|
const A = a.length > cap ? a.slice(0, cap) : a;
|
|
389
389
|
const B = b.length > cap ? b.slice(0, cap) : b;
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
withBuiltinPathLocks,
|
|
15
15
|
} from '../builtin.mjs';
|
|
16
16
|
import { withAdvisoryLocks } from '../builtin/advisory-lock.mjs';
|
|
17
|
-
import { wrapMutationRouteOutput } from '../mutation-planner.mjs';
|
|
18
17
|
import { getPluginData } from '../../config.mjs';
|
|
19
18
|
import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
|
|
20
19
|
import { prepareInput, isV4APatchInput, hasUnifiedBareV4AHunk, canFallbackCountedUnified, parseV4APatch, parseUnifiedBareV4APatch, parseUnifiedCountedAsV4APatch, isCompactedPlaceholderPatch, salvageV4AOpening } from './parsing.mjs';
|
|
@@ -509,9 +508,100 @@ function planApplyPatchMutationRoute(args, patchStr, requestedFormat) {
|
|
|
509
508
|
};
|
|
510
509
|
}
|
|
511
510
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
511
|
+
// Model-surface success outputs deliberately DROP the `mutation_route:`
|
|
512
|
+
// diagnostic header (codex parity: `Success. Updated the following files:`).
|
|
513
|
+
// Errors were never wrapped, the UI diff side-channel is independent, and no
|
|
514
|
+
// script/UI parses the header — measured saving ~30 tokens x ~125 successful
|
|
515
|
+
// patch calls/day. Route diagnostics stay available via plan/extras callers.
|
|
516
|
+
function wrapPatchMutationOutput(text, _plan, _extras = {}) {
|
|
517
|
+
return text;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Post-patch excerpt: a successful patch result carries the changed span's
|
|
521
|
+
// CURRENT on-disk lines (numbered, verbatim). This embeds the follow-up
|
|
522
|
+
// look-up INSIDE the mutation turn — the next patch to the same region has
|
|
523
|
+
// byte-exact context in-session without spending a read/grep turn, which
|
|
524
|
+
// removes the fuel for stale-own-edit context misses (the measured top
|
|
525
|
+
// remaining failure class).
|
|
526
|
+
//
|
|
527
|
+
// REPEAT-GATED: batched one-shot patches (the common case under the
|
|
528
|
+
// all-edits-in-one-patch rule) rarely get a follow-up, so the FIRST patch of
|
|
529
|
+
// a file in a session appends nothing — zero cost. Only a repeat patch of
|
|
530
|
+
// the same file (the iterative fix-loop signal, exactly where stale-own-edit
|
|
531
|
+
// misses happen) pays for the excerpt.
|
|
532
|
+
const _patchedFilesByScope = new Map(); // scopeKey -> Map(fileKey -> ts)
|
|
533
|
+
const PATCHED_FILES_SCOPE_CAP = 64;
|
|
534
|
+
const PATCHED_FILES_PER_SCOPE_CAP = 300;
|
|
535
|
+
function _scopePatchedFiles(scopeKey) {
|
|
536
|
+
let seen = _patchedFilesByScope.get(scopeKey);
|
|
537
|
+
if (!seen) {
|
|
538
|
+
seen = new Map();
|
|
539
|
+
_patchedFilesByScope.set(scopeKey, seen);
|
|
540
|
+
while (_patchedFilesByScope.size > PATCHED_FILES_SCOPE_CAP) {
|
|
541
|
+
_patchedFilesByScope.delete(_patchedFilesByScope.keys().next().value);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return seen;
|
|
545
|
+
}
|
|
546
|
+
const POST_PATCH_EXCERPT_MAX_FILES = 3;
|
|
547
|
+
const POST_PATCH_EXCERPT_MAX_LINES = 10;
|
|
548
|
+
const POST_PATCH_EXCERPT_MAX_CHARS = 1400;
|
|
549
|
+
export function appendPostPatchExcerpts(outputText, patchStr, requestedFormat, basePath, readStateScope) {
|
|
550
|
+
try {
|
|
551
|
+
if (isPatchErrorText(outputText)) return outputText;
|
|
552
|
+
if (!isV4APatchInput(patchStr, requestedFormat)) return outputText;
|
|
553
|
+
const sections = parseV4APatch(patchStr).filter((s) => s.kind === 'update' && s.hunks?.length);
|
|
554
|
+
if (!sections.length) return outputText;
|
|
555
|
+
const seen = _scopePatchedFiles(String(readStateScope || 'global'));
|
|
556
|
+
const repeats = [];
|
|
557
|
+
for (const section of sections) {
|
|
558
|
+
const target = section.movePath || section.path;
|
|
559
|
+
let fileKey;
|
|
560
|
+
try { fileKey = resolveV4AEntryPath(basePath, target); } catch { continue; }
|
|
561
|
+
if (process.platform === 'win32') fileKey = fileKey.toLowerCase();
|
|
562
|
+
if (seen.has(fileKey)) repeats.push(section);
|
|
563
|
+
seen.set(fileKey, Date.now());
|
|
564
|
+
while (seen.size > PATCHED_FILES_PER_SCOPE_CAP) seen.delete(seen.keys().next().value);
|
|
565
|
+
}
|
|
566
|
+
if (!repeats.length) return outputText;
|
|
567
|
+
const chunks = [];
|
|
568
|
+
let chars = 0;
|
|
569
|
+
for (const section of repeats.slice(0, POST_PATCH_EXCERPT_MAX_FILES)) {
|
|
570
|
+
const target = section.movePath || section.path;
|
|
571
|
+
let fileLines;
|
|
572
|
+
try {
|
|
573
|
+
fileLines = readFileSync(resolveV4AEntryPath(basePath, target), 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
574
|
+
} catch { continue; }
|
|
575
|
+
const hunk = section.hunks[0];
|
|
576
|
+
const newSide = (hunk.lines || [])
|
|
577
|
+
.filter((l) => l && (l[0] === ' ' || l[0] === '+'))
|
|
578
|
+
.map((l) => l.slice(1));
|
|
579
|
+
if (!newSide.length) continue;
|
|
580
|
+
let at = -1;
|
|
581
|
+
outer: for (let i = 0; i + newSide.length <= fileLines.length; i++) {
|
|
582
|
+
for (let k = 0; k < newSide.length; k++) {
|
|
583
|
+
if (fileLines[i + k] !== newSide[k]) continue outer;
|
|
584
|
+
}
|
|
585
|
+
at = i; break;
|
|
586
|
+
}
|
|
587
|
+
if (at < 0) continue;
|
|
588
|
+
const shown = Math.min(newSide.length, POST_PATCH_EXCERPT_MAX_LINES);
|
|
589
|
+
const rows = [];
|
|
590
|
+
for (let i = 0; i < shown; i++) {
|
|
591
|
+
rows.push(`${String(at + i + 1).padStart(5, ' ')}| ${fileLines[at + i]}`);
|
|
592
|
+
}
|
|
593
|
+
if (newSide.length > shown) rows.push(' | …');
|
|
594
|
+
const more = section.hunks.length > 1 ? ` (+${section.hunks.length - 1} more hunk${section.hunks.length > 2 ? 's' : ''})` : '';
|
|
595
|
+
const block = `${String(target).replace(/\\/g, '/')} lines ${at + 1}-${at + shown}${more}:\n${rows.join('\n')}`;
|
|
596
|
+
chars += block.length;
|
|
597
|
+
if (chars > POST_PATCH_EXCERPT_MAX_CHARS) break;
|
|
598
|
+
chunks.push(block);
|
|
599
|
+
}
|
|
600
|
+
if (!chunks.length) return outputText;
|
|
601
|
+
return `${outputText}\npost-patch state (verbatim — use for follow-up patches):\n${chunks.join('\n')}`;
|
|
602
|
+
} catch {
|
|
603
|
+
return outputText;
|
|
604
|
+
}
|
|
515
605
|
}
|
|
516
606
|
|
|
517
607
|
const APPLY_PATCH_SCHEMA_KEYS = new Set(['patch', 'format', 'base_path', 'dry_run', 'reject_partial', 'fuzzy', 'sequence', 'mode']);
|
|
@@ -583,11 +673,12 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
583
673
|
const legacyBulkMode = args?.sequence === false
|
|
584
674
|
|| ['atomic', 'bulk'].includes(patchMode);
|
|
585
675
|
if (!legacyBulkMode) {
|
|
586
|
-
|
|
676
|
+
const seqOut = await applyPatchSequence(patchStr, requestedFormat, basePath, {
|
|
587
677
|
v4aConvertOpts, dryRun, fuzz, fuzzy, rejectPartial,
|
|
588
678
|
readStateScope, abortSignal, mutationPlan,
|
|
589
679
|
rollbackOnFailure: patchMode !== 'partial',
|
|
590
680
|
});
|
|
681
|
+
return dryRun ? seqOut : appendPostPatchExcerpts(seqOut, patchStr, requestedFormat, basePath, readStateScope);
|
|
591
682
|
}
|
|
592
683
|
let v4aRenamePlan = null;
|
|
593
684
|
if (isV4APatchInput(patchStr, requestedFormat)) {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
findLineSequence,
|
|
29
29
|
findLineSequenceEscapeEquiv,
|
|
30
|
+
longestCommonSubstringLen,
|
|
30
31
|
splitTextLinesForPatch,
|
|
31
32
|
firstMeaningfulPatchLine,
|
|
32
33
|
nearestPatchLineHint,
|
|
@@ -42,6 +43,7 @@ function v4AHunkLineStats(hunk) {
|
|
|
42
43
|
let newCount = 0;
|
|
43
44
|
const oldLines = [];
|
|
44
45
|
const newLines = [];
|
|
46
|
+
const oldTags = [];
|
|
45
47
|
for (const raw of hunk.lines || []) {
|
|
46
48
|
if (!raw) continue;
|
|
47
49
|
const tag = raw[0];
|
|
@@ -51,15 +53,17 @@ function v4AHunkLineStats(hunk) {
|
|
|
51
53
|
newCount++;
|
|
52
54
|
oldLines.push(body);
|
|
53
55
|
newLines.push(body);
|
|
56
|
+
oldTags.push(' ');
|
|
54
57
|
} else if (tag === '-') {
|
|
55
58
|
oldCount++;
|
|
56
59
|
oldLines.push(body);
|
|
60
|
+
oldTags.push('-');
|
|
57
61
|
} else if (tag === '+') {
|
|
58
62
|
newCount++;
|
|
59
63
|
newLines.push(body);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
|
-
return { oldCount, newCount, oldLines, newLines };
|
|
66
|
+
return { oldCount, newCount, oldLines, newLines, oldTags };
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
function findAnchorLine(lines, anchors, fromLine) {
|
|
@@ -164,6 +168,46 @@ function eofInsertionIndex(sourceLines) {
|
|
|
164
168
|
return len > 0 && sourceLines[len - 1] === '' ? len - 1 : len;
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
// Bounded context-tolerance tier (fuzzy, non-EOF, last resort before the
|
|
172
|
+
// context-miss error). Recovers the measured top remaining failure class —
|
|
173
|
+
// 1-2 nearby context lines drifted since the model last saw the file — under
|
|
174
|
+
// guards that make mis-application practically impossible:
|
|
175
|
+
// - every '-' (deletion) line must match the file byte-exactly;
|
|
176
|
+
// - only ' ' (context) lines may mismatch, at most 2, and each drifted
|
|
177
|
+
// line must still resemble the file line (shared substring >= half);
|
|
178
|
+
// - at least 2 exact non-blank matches must anchor the window;
|
|
179
|
+
// - the qualifying window must be UNIQUE across the ENTIRE file;
|
|
180
|
+
// - the caller REMAPS tolerated context to the file's on-disk lines, so a
|
|
181
|
+
// drifted patch line never overwrites file content it did not target.
|
|
182
|
+
function findContextTolerantWindow(sourceLines, oldLines, oldTags) {
|
|
183
|
+
const n = oldLines.length;
|
|
184
|
+
if (n < 3 || !Array.isArray(oldTags) || oldTags.length !== n) return null;
|
|
185
|
+
const collapse = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
186
|
+
const similar = (a, b) => {
|
|
187
|
+
const ca = collapse(a);
|
|
188
|
+
const cb = collapse(b);
|
|
189
|
+
if (!ca || !cb) return false;
|
|
190
|
+
return longestCommonSubstringLen(ca, cb) >= Math.max(4, Math.ceil(Math.max(ca.length, cb.length) / 2));
|
|
191
|
+
};
|
|
192
|
+
const windows = [];
|
|
193
|
+
for (let i = 0; i + n <= sourceLines.length && windows.length < 2; i++) {
|
|
194
|
+
let mismatches = 0;
|
|
195
|
+
let exactNonBlank = 0;
|
|
196
|
+
let ok = true;
|
|
197
|
+
for (let k = 0; k < n; k++) {
|
|
198
|
+
const pat = oldLines[k];
|
|
199
|
+
const src = sourceLines[i + k];
|
|
200
|
+
if (src === pat) {
|
|
201
|
+
if (collapse(pat)) exactNonBlank++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (oldTags[k] !== ' ' || ++mismatches > 2 || !similar(pat, src)) { ok = false; break; }
|
|
205
|
+
}
|
|
206
|
+
if (ok && mismatches > 0 && exactNonBlank >= 2) windows.push(i);
|
|
207
|
+
}
|
|
208
|
+
return windows.length === 1 ? { start: windows[0] } : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
167
211
|
function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {}) {
|
|
168
212
|
const stats = v4AHunkLineStats(hunk);
|
|
169
213
|
if (stats.oldCount === 0 && stats.newCount === 0) return { skip: true };
|
|
@@ -242,6 +286,33 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
242
286
|
}
|
|
243
287
|
}
|
|
244
288
|
}
|
|
289
|
+
// Context-tolerance tier: see findContextTolerantWindow. Tolerated context
|
|
290
|
+
// lines are remapped to the file's actual lines on BOTH the old and new
|
|
291
|
+
// side (same shape as the escape-equiv remap above); remapping bails on any
|
|
292
|
+
// string ambiguity so an unrelated occurrence can never be rewritten.
|
|
293
|
+
if (oldStartIdx < 0 && fuzzy && !eof && oldLinesPattern.length >= 3) {
|
|
294
|
+
const tolTags = trimmedTrailing ? stats.oldTags.slice(0, -1) : stats.oldTags;
|
|
295
|
+
const tol = findContextTolerantWindow(sourceLines, oldLinesPattern, tolTags);
|
|
296
|
+
if (tol) {
|
|
297
|
+
const remapped = new Map();
|
|
298
|
+
let ambiguous = false;
|
|
299
|
+
for (let k = 0; k < oldLinesPattern.length; k++) {
|
|
300
|
+
const pat = oldLinesPattern[k];
|
|
301
|
+
const src = sourceLines[tol.start + k];
|
|
302
|
+
if (src === pat) continue;
|
|
303
|
+
if (
|
|
304
|
+
oldLinesPattern.filter((l) => l === pat).length > 1
|
|
305
|
+
|| (remapped.has(pat) && remapped.get(pat) !== src)
|
|
306
|
+
) { ambiguous = true; break; }
|
|
307
|
+
remapped.set(pat, src);
|
|
308
|
+
}
|
|
309
|
+
if (!ambiguous) {
|
|
310
|
+
newLinesPattern = newLinesPattern.map((l) => remapped.get(l) ?? l);
|
|
311
|
+
oldLinesPattern = oldLinesPattern.map((_, k) => sourceLines[tol.start + k]);
|
|
312
|
+
oldStartIdx = tol.start;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
245
316
|
if (oldStartIdx < 0) {
|
|
246
317
|
const msg = `V4A hunk context not found: ${formatV4AHunkLocator(hunk)};${formatV4AContextMissHint(sourceLines, stats, anchorLine)}`;
|
|
247
318
|
return { error: msg };
|
|
@@ -48,7 +48,7 @@ const APPLY_PATCH_JSON_DESCRIPTION = [
|
|
|
48
48
|
'- *** Delete File: <path> with nothing after it.',
|
|
49
49
|
'- *** Update File: <path>, optionally followed by *** Move to: <new path>.',
|
|
50
50
|
'Updates contain hunks introduced by @@ or @@ <enclosing class/function>. Prefix every hunk line with space (context), - (remove), or + (add); a hunk at end of file may close with *** End of File.',
|
|
51
|
-
'Copy 3 context lines above and below verbatim from the file. Do not duplicate overlapping context. If context is not unique, add one or more enclosing @@ headers.',
|
|
51
|
+
'Copy 3 context lines above and below verbatim from the newest tool output of the file (never retype from memory; after your own patch, use post-patch content). Do not duplicate overlapping context. If context is not unique, add one or more enclosing @@ headers.',
|
|
52
52
|
'Use project-relative paths. Every added file line needs +. Never submit a compacted-history marker; re-read and create a fresh patch.',
|
|
53
53
|
].join('\n');
|
|
54
54
|
|
|
@@ -115,9 +115,39 @@ function _admissionSaturationError(admission, waitMs) {
|
|
|
115
115
|
return error;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
// Memory-pressure rejections from resource-admission are TRANSIENT: they clear
|
|
119
|
+
// when concurrent work finishes releasing RSS/host memory. Measured 2026-08:
|
|
120
|
+
// ~25 shell calls/14d failed instantly on `[resource pressure]` during
|
|
121
|
+
// parallel bench/build waves. Instead of failing the tool call, retry the
|
|
122
|
+
// acquire on a short backoff inside the existing admission deadline so most
|
|
123
|
+
// of those become slightly-delayed successes. Non-memory errors (queue full,
|
|
124
|
+
// detached-dependency, saturation deadline, aborts) still throw immediately.
|
|
125
|
+
function _isMemoryPressureError(err) {
|
|
126
|
+
if (!err || err.code !== 'ERESOURCEPRESSURE') return false;
|
|
127
|
+
if (err.metric === 'rss' || err.metric === 'free-memory') return true;
|
|
128
|
+
return /memory metrics unavailable/i.test(String(err.message || ''));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _abortableDelay(ms, signal) {
|
|
132
|
+
return new Promise((resolveDelay) => {
|
|
133
|
+
let onAbort = null;
|
|
134
|
+
// NOT unref'd: this delay is awaited foreground work — an unref'd timer
|
|
135
|
+
// lets the event loop drain and strands the retry loop forever.
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
if (onAbort && signal) { try { signal.removeEventListener('abort', onAbort); } catch {} }
|
|
138
|
+
resolveDelay();
|
|
139
|
+
}, ms);
|
|
140
|
+
if (signal) {
|
|
141
|
+
onAbort = () => { clearTimeout(timer); resolveDelay(); };
|
|
142
|
+
if (signal.aborted) onAbort();
|
|
143
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function acquireShellLeaseBounded(admission, { abortSignal, label, dependency = 'scoped' } = {}) {
|
|
119
149
|
if (!(SHELL_ADMISSION_WAIT_MS > 0)) {
|
|
120
|
-
return admission.acquire('shell', { signal: abortSignal || null, label });
|
|
150
|
+
return admission.acquire('shell', { signal: abortSignal || null, label, dependency });
|
|
121
151
|
}
|
|
122
152
|
const ctl = new AbortController();
|
|
123
153
|
const onAbort = () => {
|
|
@@ -127,17 +157,26 @@ async function _acquireShellLeaseBounded(admission, { abortSignal, label }) {
|
|
|
127
157
|
if (abortSignal.aborted) onAbort();
|
|
128
158
|
else abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
129
159
|
}
|
|
160
|
+
const deadlineAt = Date.now() + SHELL_ADMISSION_WAIT_MS;
|
|
130
161
|
const deadline = setTimeout(() => {
|
|
131
162
|
try { ctl.abort(_admissionSaturationError(admission, SHELL_ADMISSION_WAIT_MS)); } catch {}
|
|
132
163
|
}, SHELL_ADMISSION_WAIT_MS);
|
|
133
164
|
if (deadline.unref) deadline.unref();
|
|
134
165
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
for (;;) {
|
|
167
|
+
try {
|
|
168
|
+
const lease = await admission.acquire('shell', { signal: ctl.signal, label, dependency });
|
|
169
|
+
// Hand governance back to the caller's signal: the internal deadline
|
|
170
|
+
// controller may still fire in a lost race after grant, and a stale
|
|
171
|
+
// aborted signal on the lease would poison later parent-restore paths.
|
|
172
|
+
lease.signal = abortSignal || null;
|
|
173
|
+
return lease;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const remainingMs = deadlineAt - Date.now();
|
|
176
|
+
if (!_isMemoryPressureError(err) || ctl.signal.aborted || remainingMs <= 1_200) throw err;
|
|
177
|
+
await _abortableDelay(Math.min(1_000, remainingMs), ctl.signal);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
141
180
|
} finally {
|
|
142
181
|
clearTimeout(deadline);
|
|
143
182
|
if (abortSignal) { try { abortSignal.removeEventListener('abort', onAbort); } catch {} }
|
|
@@ -320,7 +359,7 @@ export function execShellCommand({
|
|
|
320
359
|
let _markerFilters = null;
|
|
321
360
|
let _onChildErrorRef = null;
|
|
322
361
|
try {
|
|
323
|
-
resourceLease = await
|
|
362
|
+
resourceLease = await acquireShellLeaseBounded(admission, {
|
|
324
363
|
abortSignal,
|
|
325
364
|
label: String(command || '').slice(0, 120),
|
|
326
365
|
});
|