mixdog 0.9.95 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/package.json +12 -9
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/shared/01-tool.md +20 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -97
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-surface.mjs +0 -5
- package/src/runtime/shared/turn-snapshot.mjs +31 -5
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/runtime-core.mjs +11 -11
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/session-lifecycle.mjs +9 -2
- package/src/session-runtime/session-turn-api.mjs +41 -9
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/backend-daemon.mjs +97 -36
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +90 -8
- package/src/standalone/engine-daemon-client.mjs +380 -205
- package/src/standalone/engine-daemon-protocol.mjs +3 -4
- package/src/standalone/engine-daemon-service.mjs +378 -134
- package/src/standalone/engine-daemon-transport.mjs +268 -77
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +30 -17
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +1 -2
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +46 -41
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +6 -8
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +9 -2
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +920 -1592
- package/src/tui/engine/live-share.mjs +34 -0
- package/src/tui/engine/session-api-ext.mjs +1 -4
- package/src/tui/engine/session-api.mjs +103 -33
- package/src/tui/engine/session-flow.mjs +7 -1
- package/src/tui/engine/tool-card-results.mjs +4 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +9 -69
- package/src/tui/engine-local-session.mjs +47 -31
- package/src/tui/index.jsx +1 -2
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
|
@@ -140,8 +140,8 @@ export function killShellJob(jobId) {
|
|
|
140
140
|
// wrapper publishing the done marker. Evidence order:
|
|
141
141
|
// 1. exit file — the wrapper writes exit BEFORE done, so a kill in that
|
|
142
142
|
// window still left the real exit code; recover it instead of guessing.
|
|
143
|
-
// 2. guardian receipt — the child-guardian records
|
|
144
|
-
//
|
|
143
|
+
// 2. guardian receipt — the child-guardian records an orphaned parent before
|
|
144
|
+
// cleaning up the owned tree.
|
|
145
145
|
// 3. otherwise — honest unknown: terminal 'failed' with exitCode null and
|
|
146
146
|
// an explicit "exit status unknown" error; spilled output is preserved
|
|
147
147
|
// and the completion notification carries its tail.
|
|
@@ -165,12 +165,7 @@ function finalizeMarkerlessShellJob(detail) {
|
|
|
165
165
|
} catch { /* no guardian receipt */ }
|
|
166
166
|
detail.status = 'failed';
|
|
167
167
|
detail.exitCode = 137;
|
|
168
|
-
if (receipt?.reason === '
|
|
169
|
-
const freeMb = Math.round(Number(receipt.freeBytes || 0) / (1024 * 1024));
|
|
170
|
-
const floorMb = Math.round(Number(receipt.minFreeMemoryBytes || 0) / (1024 * 1024));
|
|
171
|
-
detail.killedByGuardian = true;
|
|
172
|
-
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`;
|
|
173
|
-
} else if (receipt?.reason === 'parent-exit') {
|
|
168
|
+
if (receipt?.reason === 'parent-exit') {
|
|
174
169
|
detail.killedByGuardian = true;
|
|
175
170
|
detail.error = 'killed by child guardian: owning runtime process exited (session cleanup)';
|
|
176
171
|
} else {
|
|
@@ -66,10 +66,40 @@ function firstExistingPathFromPath(commandName, excludeRe = null) {
|
|
|
66
66
|
.find((candidate) => !excludeRe || !excludeRe.test(candidate)) || null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Well-known pwsh (PowerShell 7+) install roots, probed on the filesystem when
|
|
70
|
+
// PATH does not carry pwsh.exe. A long-lived daemon frequently inherits an env
|
|
71
|
+
// without the PowerShell 7 directory, which silently downgraded every command
|
|
72
|
+
// to Windows PowerShell 5.1 — the shell that lacks `&&` and drives the bashism
|
|
73
|
+
// preflight blocks. Newest major first, then the Store alias.
|
|
74
|
+
function pwshFromKnownWindowsRoots() {
|
|
75
|
+
const env = process.env;
|
|
76
|
+
const bases = [env.ProgramW6432, env.ProgramFiles, env['ProgramFiles(x86)'], 'C:\\Program Files', 'C:\\Program Files (x86)']
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map((base) => join(base, 'PowerShell'));
|
|
79
|
+
const versions = ['7', '7-preview', '8', '9'];
|
|
80
|
+
for (const base of bases) {
|
|
81
|
+
for (const version of versions) {
|
|
82
|
+
const candidate = join(base, version, 'pwsh.exe');
|
|
83
|
+
if (existsSync(candidate)) return candidate;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (env.LOCALAPPDATA) {
|
|
87
|
+
const storeAlias = join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps', 'pwsh.exe');
|
|
88
|
+
if (existsSync(storeAlias)) return storeAlias;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
function resolveWindowsPowerShell() {
|
|
94
|
+
// Priority: pwsh on PATH → pwsh in a known install root → bundled 5.1 →
|
|
95
|
+
// any powershell.exe on PATH. PowerShell 7 is preferred wherever it exists
|
|
96
|
+
// because 5.1 rejects `&&` chains and several modern operators.
|
|
70
97
|
const pwsh = firstExistingPathFromPath('pwsh.exe');
|
|
71
98
|
if (pwsh) return shellSpec(pwsh, 'powershell');
|
|
72
99
|
|
|
100
|
+
const installed = pwshFromKnownWindowsRoots();
|
|
101
|
+
if (installed) return shellSpec(installed, 'powershell');
|
|
102
|
+
|
|
73
103
|
const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
|
|
74
104
|
const bundled = join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
75
105
|
if (existsSync(bundled)) return shellSpec(bundled, 'powershell');
|
|
@@ -146,9 +146,9 @@ export async function _runDiskCodeGraphFastPath({
|
|
|
146
146
|
signal = null,
|
|
147
147
|
loadDiskEntry,
|
|
148
148
|
consumeDirty = () => {},
|
|
149
|
-
// Long-lived
|
|
150
|
-
//
|
|
151
|
-
acquireSlot = (signal) => acquireChildSpawnSlot(signal, '
|
|
149
|
+
// Long-lived code-graph lane so a cold graph build never blocks short rg
|
|
150
|
+
// processes in the independent search lane.
|
|
151
|
+
acquireSlot = (signal) => acquireChildSpawnSlot(signal, 'code-graph'),
|
|
152
152
|
validateDiskHit = _validateDiskCodeGraphHit,
|
|
153
153
|
spawnWorker,
|
|
154
154
|
maxFiles = CODE_GRAPH_MAX_FILES,
|
|
@@ -344,7 +344,7 @@ export async function buildCodeGraphAsync(cwd, signal = null, {
|
|
|
344
344
|
}
|
|
345
345
|
// Non-competing prewarm: the signature-validation manifest also needs a
|
|
346
346
|
// child-spawn slot, so warmers skip before either it or a Worker can queue.
|
|
347
|
-
if (bestEffort && !childSpawnHasSpareCapacity()) return null;
|
|
347
|
+
if (bestEffort && !childSpawnHasSpareCapacity('code-graph')) return null;
|
|
348
348
|
const _genAtStart = _getCodeGraphGen(graphCwd);
|
|
349
349
|
const promise = (async () => {
|
|
350
350
|
// Loading the compact disk manifest/one candidate entry is synchronous but
|
|
@@ -422,7 +422,7 @@ export function _spawnCodeGraphWorker(
|
|
|
422
422
|
if (val instanceof Error) reject(val);
|
|
423
423
|
else resolve(val);
|
|
424
424
|
};
|
|
425
|
-
(preAcquiredRelease ? Promise.resolve(preAcquiredRelease) : acquireChildSpawnSlot(signal || null, '
|
|
425
|
+
(preAcquiredRelease ? Promise.resolve(preAcquiredRelease) : acquireChildSpawnSlot(signal || null, 'code-graph')).then((release) => {
|
|
426
426
|
_releaseSlot = release;
|
|
427
427
|
if (settled) { release(); _releaseSlot = null; return; }
|
|
428
428
|
if (signal?.aborted) { settle(new Error('aborted')); return; }
|
|
@@ -51,7 +51,8 @@ import {
|
|
|
51
51
|
|
|
52
52
|
const CODE_GRAPH_BATCHABLE_MODES = new Set(['symbol', 'find_symbol', 'symbol_search', 'callers', 'callees', 'references']);
|
|
53
53
|
const CODE_GRAPH_FILE_BATCHABLE_MODES = new Set(['imports', 'dependents', 'related', 'impact', 'symbols', 'overview']);
|
|
54
|
-
const
|
|
54
|
+
const CODE_GRAPH_MAX_BATCH_SYMBOLS = 20;
|
|
55
|
+
const CODE_GRAPH_FILE_BATCH_CAP = 20;
|
|
55
56
|
|
|
56
57
|
function _collectGraphSymbolList(args) {
|
|
57
58
|
const split = (s) => String(s || '').split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
|
|
@@ -60,18 +61,21 @@ function _collectGraphSymbolList(args) {
|
|
|
60
61
|
...(typeof args?.symbols === 'string' ? split(args.symbols) : []),
|
|
61
62
|
...(typeof args?.symbol === 'string' ? split(args.symbol) : []),
|
|
62
63
|
])];
|
|
63
|
-
|
|
64
|
-
const capped = list.slice(0, CODE_GRAPH_SYMBOL_BATCH_CAP);
|
|
65
|
-
capped._omitted = list.slice(CODE_GRAPH_SYMBOL_BATCH_CAP);
|
|
66
|
-
return capped;
|
|
64
|
+
return list;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
const CODE_GRAPH_FILE_BATCH_CAP = 20;
|
|
70
67
|
const _AGGREGATE_FILE_WILDCARD_RE = /[*?[\]{}]/;
|
|
71
68
|
const ROOT_FEDERATED_MODES = new Set([
|
|
72
69
|
'overview', 'symbol', 'find_symbol', 'symbol_search', 'search',
|
|
73
70
|
'references', 'callers', 'callees', 'symbols', 'prewarm',
|
|
74
71
|
]);
|
|
72
|
+
// Fan-out cap when federation targets are DISCOVERED (immediate child project
|
|
73
|
+
// roots of a sentinel-free cwd) rather than registered. Bounds the cost of
|
|
74
|
+
// answering a query at a multi-repo parent; registered roots are unaffected.
|
|
75
|
+
const CODE_GRAPH_DISCOVERED_FEDERATION_CAP = (() => {
|
|
76
|
+
const raw = parseInt(process.env.MIXDOG_CODE_GRAPH_FEDERATION_CAP ?? '', 10);
|
|
77
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 8;
|
|
78
|
+
})();
|
|
75
79
|
|
|
76
80
|
export async function _runCodeGraphFederation(roots, runOne, projectArgs) {
|
|
77
81
|
return Promise.all((roots || []).map(async (root) => {
|
|
@@ -132,6 +136,25 @@ function _hasAggregateFileArgs(args) {
|
|
|
132
136
|
|| (typeof args?.files === 'string' && args.files.trim());
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
// Aggregate anchors that ALL resolve to the cwd itself ('.', './', the cwd
|
|
140
|
+
// path) add no scope — they mean "search here". Detected so the call can take
|
|
141
|
+
// the plain-cwd route, which adopts a sentinel-free single tree (a vendored
|
|
142
|
+
// reference checkout) as its own root while still refusing an unbounded or
|
|
143
|
+
// multi-project parent. Without this, `files:"."` turned every reference tree
|
|
144
|
+
// into a hard "not inside a project" refusal.
|
|
145
|
+
function _aggregateAnchorsAreCwd(args, baseCwd) {
|
|
146
|
+
if (!_hasAggregateFileArgs(args)) return false;
|
|
147
|
+
const files = _collectGraphFileList(args);
|
|
148
|
+
if (files.length === 0) return false;
|
|
149
|
+
return files.every((file) => {
|
|
150
|
+
const trimmed = String(file || '').trim();
|
|
151
|
+
if (!trimmed || _AGGREGATE_FILE_WILDCARD_RE.test(trimmed)) return false;
|
|
152
|
+
try {
|
|
153
|
+
return pathResolve(isAbsolute(trimmed) ? trimmed : pathResolve(baseCwd, trimmed)) === pathResolve(baseCwd);
|
|
154
|
+
} catch { return false; }
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
135
158
|
// An invalid caller cwd may be recovered for an explicit files aggregate only
|
|
136
159
|
// when every supplied anchor points at the same detectable project. Do not use
|
|
137
160
|
// the batch cap here: an omitted anchor could belong to another project.
|
|
@@ -156,9 +179,35 @@ function _resolveAggregateFileProjectRoot(args, baseCwd) {
|
|
|
156
179
|
const root = isDirectory ? _findDirProjectRoot(abs) : _resolveFileProjectRoot(abs);
|
|
157
180
|
if (!root) return null;
|
|
158
181
|
roots.add(pathResolve(root));
|
|
159
|
-
if (roots.size > 1) return null;
|
|
160
182
|
}
|
|
161
|
-
|
|
183
|
+
if (roots.size === 0) return null;
|
|
184
|
+
if (roots.size === 1) return [...roots][0];
|
|
185
|
+
// Monorepo anchors legitimately resolve to DIFFERENT sentinels: a workspace
|
|
186
|
+
// package (apps/desktop/package.json) is nearer than the repo root, so one
|
|
187
|
+
// call spanning `apps/desktop/...` and `src/...` yields two roots even though
|
|
188
|
+
// both live in exactly one project. When one candidate contains every other,
|
|
189
|
+
// that outermost root IS the single detectable project — adopt it instead of
|
|
190
|
+
// refusing. Genuinely unrelated trees share no such candidate and still fail.
|
|
191
|
+
return _outermostContainingRoot([...roots]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The candidate that contains (or equals) every other candidate, else null.
|
|
195
|
+
// Only an EXISTING candidate can win: a bare common ancestor that has no
|
|
196
|
+
// sentinel of its own is never adopted as a project root.
|
|
197
|
+
function _outermostContainingRoot(roots) {
|
|
198
|
+
for (const candidate of roots) {
|
|
199
|
+
if (roots.every((root) => _isSameOrInside(root, candidate))) return candidate;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function _isSameOrInside(child, parent) {
|
|
205
|
+
try {
|
|
206
|
+
const rel = pathRelative(pathResolve(parent), pathResolve(child));
|
|
207
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
162
211
|
}
|
|
163
212
|
|
|
164
213
|
// Aggregate recovery resolves relative anchors against the caller's original
|
|
@@ -624,6 +673,11 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
624
673
|
args = { ...args };
|
|
625
674
|
delete args.file;
|
|
626
675
|
}
|
|
676
|
+
if (symbolMode && _aggregateAnchorsAreCwd(args, baseCwd)) {
|
|
677
|
+
args = { ...args };
|
|
678
|
+
delete args.files;
|
|
679
|
+
delete args.file;
|
|
680
|
+
}
|
|
627
681
|
const fileArg = (args && typeof args.file === 'string' && args.file.trim()) ? args.file.trim() : '';
|
|
628
682
|
const hasAggregateFileArgs = _hasAggregateFileArgs(args);
|
|
629
683
|
let effectiveCwd = baseCwd;
|
|
@@ -647,6 +701,20 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
647
701
|
? []
|
|
648
702
|
: collectTrustedCodeGraphRoots(baseCwd)
|
|
649
703
|
.filter((root) => pathResolve(root) !== pathResolve(baseCwd));
|
|
704
|
+
// Trust registration is a ROUTING PREFERENCE, not an admission gate. A
|
|
705
|
+
// sentinel-free cwd whose children are obvious project roots (a refs/ folder
|
|
706
|
+
// of checkouts, a multi-repo parent) is answerable: federate over those
|
|
707
|
+
// children instead of refusing the call. Cost stays bounded by the existing
|
|
708
|
+
// per-project graph timeout and the federation fan-out cap — the same way the
|
|
709
|
+
// reference CLIs bound a wide search (time/output caps, never a scope
|
|
710
|
+
// refusal). Registered roots keep priority; discovered children only fill in
|
|
711
|
+
// when registration yields nothing.
|
|
712
|
+
const federationRootsAtBase = trustedRootsAtBase.length
|
|
713
|
+
? trustedRootsAtBase
|
|
714
|
+
: (baseProjectRoot
|
|
715
|
+
? []
|
|
716
|
+
: _childProjectRoots(baseCwd, { cap: CODE_GRAPH_DISCOVERED_FEDERATION_CAP })
|
|
717
|
+
.filter((root) => pathResolve(root) !== pathResolve(baseCwd)));
|
|
650
718
|
// A sentinel-free cwd (multi-repo parent, vendored reference tree) is still
|
|
651
719
|
// routable when trusted project roots live under it — federate over those
|
|
652
720
|
// instead of refusing the call outright.
|
|
@@ -656,9 +724,12 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
656
724
|
&& !fileArg
|
|
657
725
|
&& !hasAggregateFileArgs
|
|
658
726
|
&& ROOT_FEDERATED_MODES.has(rawModeAtBase)
|
|
659
|
-
&&
|
|
727
|
+
&& federationRootsAtBase.length > 0;
|
|
660
728
|
if (filesystemRootCwd || parentDotFederation || sentinelFreeFederation) {
|
|
661
|
-
|
|
729
|
+
// A filesystem root stays on the REGISTERED set only: fanning out over
|
|
730
|
+
// every project directory on a whole drive is a different cost class than
|
|
731
|
+
// fanning out over one folder's children.
|
|
732
|
+
const trustedRoots = filesystemRootCwd ? trustedRootsAtBase : federationRootsAtBase;
|
|
662
733
|
const files = exactDotFederation ? [] : aggregateFilesAtBase;
|
|
663
734
|
const rawMode = rawModeAtBase;
|
|
664
735
|
const canFederate = files.length > 0 || ROOT_FEDERATED_MODES.has(rawMode);
|
|
@@ -818,7 +889,8 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
818
889
|
? findSymbolTool(_stripEmptyArgs(a), effectiveCwd, signal, options)
|
|
819
890
|
: codeGraph(a, effectiveCwd, signal, options));
|
|
820
891
|
if (CODE_GRAPH_BATCHABLE_MODES.has(batchMode)) {
|
|
821
|
-
const
|
|
892
|
+
const collectedSymbols = _collectGraphSymbolList(args);
|
|
893
|
+
const symbolList = collectedSymbols.slice(0, CODE_GRAPH_MAX_BATCH_SYMBOLS);
|
|
822
894
|
if (symbolList.length > 1) {
|
|
823
895
|
return (async () => {
|
|
824
896
|
// Concurrent fan-out: the underlying graph build is single-flight
|
|
@@ -831,10 +903,10 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
831
903
|
catch (e) { body = `Error: ${e?.message || String(e)}`; }
|
|
832
904
|
return `# ${batchMode} ${sym}\n${body}`;
|
|
833
905
|
}));
|
|
834
|
-
if (symbolList.
|
|
906
|
+
if (collectedSymbols.length > symbolList.length) {
|
|
835
907
|
sections.push(
|
|
836
|
-
`
|
|
837
|
-
|
|
908
|
+
`[arg-guard] notice: symbol list capped at ${CODE_GRAPH_MAX_BATCH_SYMBOLS} `
|
|
909
|
+
+ `(${collectedSymbols.length - symbolList.length} omitted)`,
|
|
838
910
|
);
|
|
839
911
|
}
|
|
840
912
|
return sections.join('\n\n');
|
|
@@ -847,8 +919,8 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
847
919
|
if (CODE_GRAPH_FILE_BATCHABLE_MODES.has(batchMode)) {
|
|
848
920
|
const fileList = _collectGraphFileList(args);
|
|
849
921
|
if (fileList.length > 1) {
|
|
850
|
-
const capped = fileList._capped;
|
|
851
922
|
return (async () => {
|
|
923
|
+
const capped = fileList._capped;
|
|
852
924
|
const sections = await Promise.all(fileList.map(async (f) => {
|
|
853
925
|
let body;
|
|
854
926
|
try { body = await dispatchOne({ ...args, file: f, files: undefined }); }
|
|
@@ -269,21 +269,12 @@ export async function _prewarmReferenceSourceText(graph, symbol, language) {
|
|
|
269
269
|
}
|
|
270
270
|
if (uncached.length === 0) return candidateNodes;
|
|
271
271
|
const { readFile } = await import('fs/promises');
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const node = uncached[index];
|
|
279
|
-
try {
|
|
280
|
-
const text = await readFile(node.abs, 'utf8');
|
|
281
|
-
graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
|
|
282
|
-
} catch { /* skip unreadable file */ }
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const workerCount = Math.min(Math.max(1, concurrency), uncached.length);
|
|
286
|
-
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
272
|
+
await Promise.all(uncached.map(async (node) => {
|
|
273
|
+
try {
|
|
274
|
+
const text = await readFile(node.abs, 'utf8');
|
|
275
|
+
graph._sourceTextCache?.set(node.rel, { fingerprint: node.fingerprint || '', text });
|
|
276
|
+
} catch { /* skip unreadable file */ }
|
|
277
|
+
}));
|
|
287
278
|
return candidateNodes;
|
|
288
279
|
}
|
|
289
280
|
|
|
@@ -3,13 +3,13 @@ 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: '
|
|
6
|
+
description: 'Source-file structure, symbol relations, and flow. File modes take files[]; symbol modes take symbols[] — exact identifiers via find_symbol/references/callers/callees, keywords via symbol_search/search (symbol-index terms). Unsupported target arrays are omitted, never mixed.',
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
10
|
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes={overview,imports,dependents,related,impact}; symbols with files[]=file outline; the rest are symbol modes.' },
|
|
11
11
|
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file path(s); supported targets only.' },
|
|
12
|
-
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers or keywords; batch multiple in one symbols[].' },
|
|
12
|
+
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers or keywords; keywords are symbol-index terms; batch multiple in one symbols[].' },
|
|
13
13
|
body: { type: 'boolean', description: 'Include body.' },
|
|
14
14
|
limit: { type: 'number', minimum: 1, description: 'Max results.' },
|
|
15
15
|
depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
|
|
@@ -26,18 +26,51 @@
|
|
|
26
26
|
import { spawn } from 'node:child_process';
|
|
27
27
|
import { basename } from 'node:path';
|
|
28
28
|
import { createHash, randomBytes } from 'node:crypto';
|
|
29
|
+
import { availableParallelism, freemem } from 'node:os';
|
|
29
30
|
import { killProcessTree, windowsPidHasDescendants } from '../builtin/shell-job-process.mjs';
|
|
30
31
|
|
|
32
|
+
const MB = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
export function _resolvePwshPoolTarget({
|
|
35
|
+
env = process.env,
|
|
36
|
+
platform = process.platform,
|
|
37
|
+
parallelism = null,
|
|
38
|
+
freeMemoryBytes = null,
|
|
39
|
+
} = {}) {
|
|
40
|
+
const configured = env?.MIXDOG_PWSH_STANDBY_POOL;
|
|
41
|
+
if (configured != null && configured !== '') {
|
|
42
|
+
const n = Math.floor(Number(configured));
|
|
43
|
+
if (Number.isFinite(n) && n >= 0) return Math.min(8, n);
|
|
44
|
+
}
|
|
45
|
+
if (platform !== 'win32') return 0;
|
|
46
|
+
// Tools now execute in the machine-global backend daemon. Keep client/test
|
|
47
|
+
// processes at zero by default, but let that ONE host maintain a short-lived
|
|
48
|
+
// warm burst sized to the machine instead of making every shell call pay the
|
|
49
|
+
// ~300ms pwsh startup cost.
|
|
50
|
+
if (env?.MIXDOG_ENGINE_DAEMON_HOST !== '1' && env?.MIXDOG_CHANNEL_DAEMON !== '1') return 0;
|
|
51
|
+
let cores = Number(parallelism);
|
|
52
|
+
if (!Number.isFinite(cores) || cores < 1) {
|
|
53
|
+
try { cores = availableParallelism(); } catch { cores = 1; }
|
|
54
|
+
}
|
|
55
|
+
let free = freeMemoryBytes == null ? Number.NaN : Number(freeMemoryBytes);
|
|
56
|
+
if (!Number.isFinite(free) || free < 0) {
|
|
57
|
+
try { free = freemem(); } catch { free = 0; }
|
|
58
|
+
}
|
|
59
|
+
const cpuTarget = Math.max(1, Math.min(4, Math.floor(cores) - 1 || 1));
|
|
60
|
+
if (free < 768 * MB) return 0;
|
|
61
|
+
if (free < 1536 * MB) return Math.min(1, cpuTarget);
|
|
62
|
+
if (free < 3072 * MB) return Math.min(2, cpuTarget);
|
|
63
|
+
return cpuTarget;
|
|
64
|
+
}
|
|
65
|
+
|
|
31
66
|
function _poolTarget() {
|
|
32
|
-
|
|
33
|
-
if (Number.isFinite(n) && n >= 0) return Math.min(8, n);
|
|
34
|
-
return 2;
|
|
67
|
+
return _resolvePwshPoolTarget();
|
|
35
68
|
}
|
|
36
69
|
const POOL_TARGET = _poolTarget();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
const _envIdleMs = Math.floor(Number(process.env.MIXDOG_PWSH_STANDBY_IDLE_MS));
|
|
71
|
+
const STANDBY_IDLE_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0
|
|
72
|
+
? _envIdleMs
|
|
73
|
+
: 60_000;
|
|
41
74
|
const EXPECTED_ARGS = Object.freeze(['-NoLogo', '-NoProfile', '-NonInteractive', '-Command']);
|
|
42
75
|
|
|
43
76
|
// Marker-loop bootstrap: the standby serves MANY commands over its lifetime.
|
|
@@ -132,6 +165,7 @@ function _standbyBootScript(nonce) {
|
|
|
132
165
|
}
|
|
133
166
|
|
|
134
167
|
const _idle = [];
|
|
168
|
+
const _taken = new Set();
|
|
135
169
|
let _envSig = null;
|
|
136
170
|
let _exitHookInstalled = false;
|
|
137
171
|
|
|
@@ -144,6 +178,10 @@ function _envSignature(env) {
|
|
|
144
178
|
}
|
|
145
179
|
|
|
146
180
|
function _killEntry(entry) {
|
|
181
|
+
if (entry.idleTimer) {
|
|
182
|
+
clearTimeout(entry.idleTimer);
|
|
183
|
+
entry.idleTimer = null;
|
|
184
|
+
}
|
|
147
185
|
try { entry.child.kill(); } catch { /* already down */ }
|
|
148
186
|
}
|
|
149
187
|
|
|
@@ -152,6 +190,19 @@ function _discardIdle(entry) {
|
|
|
152
190
|
if (i !== -1) _idle.splice(i, 1);
|
|
153
191
|
}
|
|
154
192
|
|
|
193
|
+
function _armIdleExpiry(entry) {
|
|
194
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
195
|
+
entry.idleTimer = null;
|
|
196
|
+
if (STANDBY_IDLE_MS <= 0) return;
|
|
197
|
+
entry.idleTimer = setTimeout(() => {
|
|
198
|
+
entry.idleTimer = null;
|
|
199
|
+
if (entry.taken) return;
|
|
200
|
+
_discardIdle(entry);
|
|
201
|
+
_killEntry(entry);
|
|
202
|
+
}, STANDBY_IDLE_MS);
|
|
203
|
+
entry.idleTimer.unref?.();
|
|
204
|
+
}
|
|
205
|
+
|
|
155
206
|
function _installExitHook() {
|
|
156
207
|
if (_exitHookInstalled) return;
|
|
157
208
|
_exitHookInstalled = true;
|
|
@@ -173,7 +224,7 @@ function _spawnStandby(shell, env, sig) {
|
|
|
173
224
|
detached: false,
|
|
174
225
|
});
|
|
175
226
|
} catch { return; }
|
|
176
|
-
const entry = { child, sig, nonce,
|
|
227
|
+
const entry = { child, sig, nonce, taken: false, consumed: false, idleTimer: null };
|
|
177
228
|
child.on('error', () => { if (!entry.taken) { _discardIdle(entry); _killEntry(entry); } });
|
|
178
229
|
child.on('exit', () => { if (!entry.taken) _discardIdle(entry); });
|
|
179
230
|
// A dead standby's pending stdin write must never crash the daemon.
|
|
@@ -187,10 +238,25 @@ function _spawnStandby(shell, env, sig) {
|
|
|
187
238
|
child.stderr?.unref?.();
|
|
188
239
|
} catch { /* best-effort */ }
|
|
189
240
|
_idle.push(entry);
|
|
241
|
+
_armIdleExpiry(entry);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _poolSizeForSignature(sig) {
|
|
245
|
+
let count = _idle.reduce((total, entry) => total + (entry.sig === sig ? 1 : 0), 0);
|
|
246
|
+
for (const entry of _taken) {
|
|
247
|
+
if (entry.sig === sig) count += 1;
|
|
248
|
+
}
|
|
249
|
+
return count;
|
|
190
250
|
}
|
|
191
251
|
|
|
192
252
|
function _topUp(shell, env, sig) {
|
|
193
|
-
while (
|
|
253
|
+
while (_poolSizeForSignature(sig) < POOL_TARGET) _spawnStandby(shell, env, sig);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function _releaseTaken(entry) {
|
|
257
|
+
if (!_taken.delete(entry)) return false;
|
|
258
|
+
entry.taken = false;
|
|
259
|
+
return true;
|
|
194
260
|
}
|
|
195
261
|
|
|
196
262
|
/**
|
|
@@ -215,11 +281,9 @@ export function takePwshStandby({ shell, shellArgs, env }) {
|
|
|
215
281
|
_envSig = sig;
|
|
216
282
|
}
|
|
217
283
|
_topUp(shell, env, sig);
|
|
218
|
-
const now = Date.now();
|
|
219
284
|
let entry = null;
|
|
220
285
|
for (let i = 0; i < _idle.length; i += 1) {
|
|
221
286
|
const cand = _idle[i];
|
|
222
|
-
if (now - cand.spawnedAt < STANDBY_READY_AGE_MS) continue;
|
|
223
287
|
if (cand.child.exitCode !== null || cand.child.signalCode !== null) continue;
|
|
224
288
|
if (!cand.child.stdin || cand.child.stdin.destroyed) continue;
|
|
225
289
|
_idle.splice(i, 1);
|
|
@@ -229,6 +293,11 @@ export function takePwshStandby({ shell, shellArgs, env }) {
|
|
|
229
293
|
if (!entry) return null;
|
|
230
294
|
entry.taken = true;
|
|
231
295
|
entry.consumed = false;
|
|
296
|
+
_taken.add(entry);
|
|
297
|
+
if (entry.idleTimer) {
|
|
298
|
+
clearTimeout(entry.idleTimer);
|
|
299
|
+
entry.idleTimer = null;
|
|
300
|
+
}
|
|
232
301
|
try {
|
|
233
302
|
entry.child.ref();
|
|
234
303
|
entry.child.stdin?.ref?.();
|
|
@@ -271,6 +340,7 @@ export function takePwshStandby({ shell, shellArgs, env }) {
|
|
|
271
340
|
// job's close-wired exit file then records the REAL command exit.
|
|
272
341
|
endStdin() {
|
|
273
342
|
entry.consumed = true;
|
|
343
|
+
_releaseTaken(entry);
|
|
274
344
|
try { entry.child.stdin.end(); } catch { /* already gone */ }
|
|
275
345
|
},
|
|
276
346
|
// Post-command recycle: return the standby to the pool ONLY when the
|
|
@@ -281,6 +351,7 @@ export function takePwshStandby({ shell, shellArgs, env }) {
|
|
|
281
351
|
async recycle() {
|
|
282
352
|
if (entry.consumed) return false;
|
|
283
353
|
entry.consumed = true;
|
|
354
|
+
_releaseTaken(entry);
|
|
284
355
|
const c = entry.child;
|
|
285
356
|
const alive = () => c.exitCode === null && c.signalCode === null;
|
|
286
357
|
let dirty = true;
|
|
@@ -309,9 +380,7 @@ export function takePwshStandby({ shell, shellArgs, env }) {
|
|
|
309
380
|
process.stderr.write(`[pwsh-standby] recycle drained ${_staleBytes}B stale output (pid=${c.pid}) — late flush escaped the previous command's capture\n`);
|
|
310
381
|
} catch { /* diagnostics only */ }
|
|
311
382
|
}
|
|
312
|
-
entry.taken = false;
|
|
313
383
|
entry.consumed = false;
|
|
314
|
-
entry.spawnedAt = Date.now() - STANDBY_READY_AGE_MS;
|
|
315
384
|
try {
|
|
316
385
|
c.unref();
|
|
317
386
|
c.stdin?.unref?.();
|
|
@@ -319,6 +388,7 @@ export function takePwshStandby({ shell, shellArgs, env }) {
|
|
|
319
388
|
c.stderr?.unref?.();
|
|
320
389
|
} catch { /* best-effort */ }
|
|
321
390
|
_idle.push(entry);
|
|
391
|
+
_armIdleExpiry(entry);
|
|
322
392
|
return true;
|
|
323
393
|
},
|
|
324
394
|
};
|
|
@@ -334,7 +334,10 @@ function rustTrim(s) {
|
|
|
334
334
|
return s.replace(RUST_TRIM_RE, '');
|
|
335
335
|
}
|
|
336
336
|
export function normalizeTypographic(s) {
|
|
337
|
-
|
|
337
|
+
// NFC first: a decomposed sequence (Hangul jamo, or "e" + combining acute
|
|
338
|
+
// from a file authored on another platform) is visually identical to its
|
|
339
|
+
// composed form but byte-different, which failed an otherwise exact context.
|
|
340
|
+
return rustTrim(String(s ?? '').normalize('NFC'))
|
|
338
341
|
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, '-')
|
|
339
342
|
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
340
343
|
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
@@ -405,6 +408,41 @@ export function longestCommonSubstringLen(a, b, cap = 4000) {
|
|
|
405
408
|
return best;
|
|
406
409
|
}
|
|
407
410
|
|
|
411
|
+
// Total character drift tolerated per context line by the edit-distance tier.
|
|
412
|
+
// Deliberately tiny: 3 context lines buy exactly 1 character of slack, so the
|
|
413
|
+
// tier rescues a typo, never a genuinely different block.
|
|
414
|
+
export const EDIT_DISTANCE_ALLOWANCE_PER_LINE = 0.34;
|
|
415
|
+
|
|
416
|
+
// Levenshtein distance with an early exit: returns the exact distance while it
|
|
417
|
+
// stays within `max`, otherwise `max + 1` as soon as that is certain. The
|
|
418
|
+
// length pre-check and the per-row minimum keep a whole-file scan cheap.
|
|
419
|
+
export function boundedEditDistance(a, b, max) {
|
|
420
|
+
const s = String(a ?? '');
|
|
421
|
+
const t = String(b ?? '');
|
|
422
|
+
if (s === t) return 0;
|
|
423
|
+
if (!(max > 0)) return 1;
|
|
424
|
+
if (Math.abs(s.length - t.length) > max) return max + 1;
|
|
425
|
+
const n = s.length;
|
|
426
|
+
const m = t.length;
|
|
427
|
+
let prev = new Int32Array(m + 1);
|
|
428
|
+
let curr = new Int32Array(m + 1);
|
|
429
|
+
for (let j = 0; j <= m; j++) prev[j] = j;
|
|
430
|
+
for (let i = 1; i <= n; i++) {
|
|
431
|
+
curr[0] = i;
|
|
432
|
+
let rowMin = curr[0];
|
|
433
|
+
const ci = s.charCodeAt(i - 1);
|
|
434
|
+
for (let j = 1; j <= m; j++) {
|
|
435
|
+
const cost = ci === t.charCodeAt(j - 1) ? 0 : 1;
|
|
436
|
+
const value = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
437
|
+
curr[j] = value;
|
|
438
|
+
if (value < rowMin) rowMin = value;
|
|
439
|
+
}
|
|
440
|
+
if (rowMin > max) return max + 1;
|
|
441
|
+
const tmp = prev; prev = curr; curr = tmp;
|
|
442
|
+
}
|
|
443
|
+
return prev[m] > max ? max + 1 : prev[m];
|
|
444
|
+
}
|
|
445
|
+
|
|
408
446
|
export function findLineSequence(lines, needle, fromLine, preferredLine = 0, options = {}) {
|
|
409
447
|
if (!Array.isArray(needle) || needle.length === 0) return Math.max(0, preferredLine || fromLine || 0);
|
|
410
448
|
const eof = options?.eof === true;
|
|
@@ -703,7 +703,7 @@ export function appendPostPatchExcerpts(outputText, patchStr, requestedFormat, b
|
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
-
const APPLY_PATCH_SCHEMA_KEYS = new Set(['patch', 'format', 'base_path', 'dry_run', 'reject_partial', 'fuzzy', 'sequence', 'mode']);
|
|
706
|
+
const APPLY_PATCH_SCHEMA_KEYS = new Set(['patch', 'format', 'base_path', 'root', 'dry_run', 'reject_partial', 'fuzzy', 'sequence', 'mode']);
|
|
707
707
|
function salvageShatteredV4APatchArgs(args) {
|
|
708
708
|
if (!args || typeof args !== 'object') return args;
|
|
709
709
|
const rawPatch = typeof args.patch === 'string' ? args.patch : '';
|
|
@@ -755,6 +755,27 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
755
755
|
} catch (err) {
|
|
756
756
|
return `Error: ${err?.message || String(err)}`;
|
|
757
757
|
}
|
|
758
|
+
// Write-root gate: a patch may only touch paths inside a DECLARED root —
|
|
759
|
+
// the session directory by default, or `root`/`base_path` when the caller
|
|
760
|
+
// names one. Anything outside is refused before a single byte is written, so
|
|
761
|
+
// a mis-scoped working directory can never silently rewrite another tree.
|
|
762
|
+
// Editing outside stays possible, but only as a deliberate act: name the
|
|
763
|
+
// root that contains those targets in the same call.
|
|
764
|
+
const writeRoot = resolveBasePath(cwd, args?.root ?? args?.base_path ?? null);
|
|
765
|
+
const outsideTargets = [];
|
|
766
|
+
for (const rel of patchTargetPaths(patchStr, basePath)) {
|
|
767
|
+
const abs = isAbsolute(rel) ? pathResolve(rel) : pathResolve(basePath, rel);
|
|
768
|
+
if (isResolvedPathOutsideBase(abs, writeRoot)) outsideTargets.push(normalizeOutputPath(abs));
|
|
769
|
+
}
|
|
770
|
+
if (outsideTargets.length > 0) {
|
|
771
|
+
const shown = [...new Set(outsideTargets)];
|
|
772
|
+
const head = shown.slice(0, 3).join(', ');
|
|
773
|
+
const more = shown.length > 3 ? ` (+${shown.length - 3} more)` : '';
|
|
774
|
+
throw new Error(
|
|
775
|
+
`apply_patch: ${shown.length} target(s) fall outside the write root ${normalizeOutputPath(writeRoot)}: ${head}${more}. `
|
|
776
|
+
+ 'Check the paths first; if they are intended, re-issue the same patch with root set to the directory that contains them.',
|
|
777
|
+
);
|
|
778
|
+
}
|
|
758
779
|
const rejectPartial = args?.reject_partial !== false;
|
|
759
780
|
const dryRun = args?.dry_run === true;
|
|
760
781
|
const fuzzy = args?.fuzzy !== false;
|
|
@@ -1051,6 +1072,13 @@ function patchTargetPaths(patchStr, basePath) {
|
|
|
1051
1072
|
const rel = m[1].trim();
|
|
1052
1073
|
if (rel) out.push(rel);
|
|
1053
1074
|
}
|
|
1075
|
+
// A rename WRITES its destination, so the destination is a target too — the
|
|
1076
|
+
// write-root gate and the replay snapshot both need it.
|
|
1077
|
+
const mre = /^\*\*\* Move to:\s*(.+)$/gm;
|
|
1078
|
+
while ((m = mre.exec(String(patchStr || '')))) {
|
|
1079
|
+
const rel = m[1].trim();
|
|
1080
|
+
if (rel) out.push(rel);
|
|
1081
|
+
}
|
|
1054
1082
|
const ure = /^\+\+\+ (?:b\/)?(.+)$/gm;
|
|
1055
1083
|
while ((m = ure.exec(String(patchStr || '')))) {
|
|
1056
1084
|
const rel = m[1].trim();
|
|
@@ -283,6 +283,15 @@ export function parseV4APatch(patchStr) {
|
|
|
283
283
|
if (rawLine.startsWith('@@')) {
|
|
284
284
|
const anchor = normaliseV4AAnchor(rawLine.slice(2));
|
|
285
285
|
if (currentHunk && currentHunk.lines.length > 0) finishHunk();
|
|
286
|
+
// Consecutive `@@` lines narrow ONE hunk's position (`@@ class Foo`
|
|
287
|
+
// then `@@ def bar`): append to the open hunk's anchor chain instead
|
|
288
|
+
// of replacing the hunk, which dropped every anchor but the last and
|
|
289
|
+
// let the edit resolve against the wrong occurrence.
|
|
290
|
+
if (currentHunk && currentHunk.lines.length === 0) {
|
|
291
|
+
if (anchor) currentHunk.anchors.push(anchor);
|
|
292
|
+
pendingAnchors = [];
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
286
295
|
pendingAnchors.push(anchor);
|
|
287
296
|
currentHunk = { anchors: pendingAnchors.slice(), lines: [] };
|
|
288
297
|
pendingAnchors = [];
|
|
@@ -374,6 +383,13 @@ function parseUnifiedAsV4APatch(patchStr, { label, resolveAnchor }) {
|
|
|
374
383
|
if (rawLine.startsWith('@@')) {
|
|
375
384
|
const anchor = resolveAnchor(rawLine);
|
|
376
385
|
if (currentHunk && currentHunk.lines.length > 0) finishHunk();
|
|
386
|
+
// Anchor chain (see the V4A parser above): consecutive `@@` lines refine
|
|
387
|
+
// one hunk's position and must all be kept.
|
|
388
|
+
if (currentHunk && currentHunk.lines.length === 0) {
|
|
389
|
+
if (anchor) currentHunk.anchors.push(anchor);
|
|
390
|
+
pendingAnchors = [];
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
377
393
|
pendingAnchors.push(anchor);
|
|
378
394
|
currentHunk = { anchors: pendingAnchors.slice(), lines: [] };
|
|
379
395
|
pendingAnchors = [];
|