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
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// only (behavior + argument shapes); usage policy lives in rules/shared/01-tool.md.
|
|
6
6
|
import {
|
|
7
7
|
TOOL_ASYNC_EXECUTION_CONTRACT,
|
|
8
|
-
TOOL_MANUAL_CONTROL_CONTRACT,
|
|
9
8
|
executionModeSchemaDescription,
|
|
10
9
|
} from '../../../../shared/background-tasks.mjs';
|
|
11
10
|
|
|
@@ -28,7 +27,7 @@ function _shellMaxTimeoutMs() {
|
|
|
28
27
|
// process lifetime, so this is evaluated once at module load.
|
|
29
28
|
const _shellSyntaxCheat =
|
|
30
29
|
process.platform === 'win32'
|
|
31
|
-
? ' PowerShell:
|
|
30
|
+
? ' PowerShell: use ; between commands, /c/→C:\\, and note that $PID is reserved.'
|
|
32
31
|
: '';
|
|
33
32
|
|
|
34
33
|
export const BUILTIN_TOOLS = [
|
|
@@ -36,7 +35,7 @@ export const BUILTIN_TOOLS = [
|
|
|
36
35
|
name: 'read',
|
|
37
36
|
title: 'Mixdog Read',
|
|
38
37
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
39
|
-
description: 'Read file contents
|
|
38
|
+
description: 'Read file contents or line ranges; not directories.',
|
|
40
39
|
inputSchema: {
|
|
41
40
|
type: 'object',
|
|
42
41
|
properties: {
|
|
@@ -99,12 +98,12 @@ export const BUILTIN_TOOLS = [
|
|
|
99
98
|
name: 'task',
|
|
100
99
|
title: 'Background Task Control',
|
|
101
100
|
annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
102
|
-
description:
|
|
101
|
+
description: 'Control a shell background task_id; not session or agent ids.',
|
|
103
102
|
inputSchema: {
|
|
104
103
|
type: 'object',
|
|
105
104
|
properties: {
|
|
106
105
|
task_id: { type: 'string', description: 'shell async task_id.' },
|
|
107
|
-
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: '
|
|
106
|
+
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Default list; with task_id, default wait.' },
|
|
108
107
|
timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
|
|
109
108
|
},
|
|
110
109
|
required: [],
|
|
@@ -115,7 +114,7 @@ export const BUILTIN_TOOLS = [
|
|
|
115
114
|
name: 'grep',
|
|
116
115
|
title: 'Mixdog Grep',
|
|
117
116
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
118
|
-
description: '
|
|
117
|
+
description: 'Literal/regex search over file/dir scopes; returns source blocks with context.',
|
|
119
118
|
inputSchema: {
|
|
120
119
|
type: 'object',
|
|
121
120
|
properties: {
|
|
@@ -156,7 +155,7 @@ export const BUILTIN_TOOLS = [
|
|
|
156
155
|
name: 'glob',
|
|
157
156
|
title: 'Mixdog Glob',
|
|
158
157
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
159
|
-
description: 'Match
|
|
158
|
+
description: 'Match glob patterns under base directories.',
|
|
160
159
|
inputSchema: {
|
|
161
160
|
type: 'object',
|
|
162
161
|
properties: {
|
|
@@ -185,7 +184,7 @@ export const BUILTIN_TOOLS = [
|
|
|
185
184
|
name: 'find',
|
|
186
185
|
title: 'Mixdog Find Files',
|
|
187
186
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
188
|
-
description: 'Fuzzy
|
|
187
|
+
description: 'Fuzzy partial path/name lookup; returns paths only.',
|
|
189
188
|
inputSchema: {
|
|
190
189
|
type: 'object',
|
|
191
190
|
properties: {
|
|
@@ -207,7 +206,7 @@ export const BUILTIN_TOOLS = [
|
|
|
207
206
|
name: 'list',
|
|
208
207
|
title: 'Mixdog List Directory',
|
|
209
208
|
annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
210
|
-
description: 'List directory entries (path + type)
|
|
209
|
+
description: 'List directory entries (path + type).',
|
|
211
210
|
inputSchema: {
|
|
212
211
|
type: 'object',
|
|
213
212
|
properties: {
|
|
@@ -238,7 +238,7 @@ export function getCachedReadOnlyStat(fullPath, loader = statSync, now = Date.no
|
|
|
238
238
|
return stat;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
export async function statPathsForMtime(paths, workDir, concurrency =
|
|
241
|
+
export async function statPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
|
|
242
242
|
const items = Array.isArray(paths) ? paths : [];
|
|
243
243
|
const out = new Array(items.length);
|
|
244
244
|
const now = Date.now();
|
|
@@ -302,7 +302,7 @@ export async function statPathsForMtime(paths, workDir, concurrency = 64, opts =
|
|
|
302
302
|
// lstat variant — does NOT follow symlinks. Use for directory-listing
|
|
303
303
|
// surfaces where a symlink to a 200 GB file should report as a symlink,
|
|
304
304
|
// not as the target's size/mtime.
|
|
305
|
-
export async function lstatPathsForMtime(paths, workDir, concurrency =
|
|
305
|
+
export async function lstatPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
|
|
306
306
|
const items = Array.isArray(paths) ? paths : [];
|
|
307
307
|
const out = new Array(items.length);
|
|
308
308
|
const inflight = new Map();
|
|
@@ -19,8 +19,8 @@ export function isBlockedDevicePath(p) {
|
|
|
19
19
|
|
|
20
20
|
// R1: UNC / SMB share paths (\\server\share or //server/share). Reading these
|
|
21
21
|
// causes Windows to auto-authenticate to the remote host, leaking the NTLM
|
|
22
|
-
// hash of the current user to any attacker-controlled SMB target.
|
|
23
|
-
//
|
|
22
|
+
// hash of the current user to any attacker-controlled SMB target. The prefix
|
|
23
|
+
// is rejected before stat. Accepts both
|
|
24
24
|
// backslash and forward-slash leaders so a normalize that picked either form
|
|
25
25
|
// is caught.
|
|
26
26
|
export function isUncPath(p) {
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from './search-input-helpers.mjs';
|
|
18
18
|
|
|
19
19
|
export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT = 10_000;
|
|
20
|
-
const GREP_CONTEXT_READ_CONCURRENCY = 4;
|
|
21
20
|
const GREP_FOCUSED_CONTEXT_RADIUS = 12;
|
|
22
21
|
const GREP_FOCUSED_RAW_BLOCKS = 3;
|
|
23
22
|
// Anchors must stay usable as evidence without a follow-up read: keep the
|
|
@@ -229,21 +228,13 @@ async function readAnchorSources(anchors, radius, signal) {
|
|
|
229
228
|
groups.get(anchor.absolutePath).anchors.push(anchor);
|
|
230
229
|
}
|
|
231
230
|
const entries = [...groups.values()];
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
entry.lines = await readFileWindows(entry, radius, signal);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
entry.error = err;
|
|
240
|
-
}
|
|
231
|
+
await Promise.all(entries.map(async (entry) => {
|
|
232
|
+
try {
|
|
233
|
+
entry.lines = await readFileWindows(entry, radius, signal);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
entry.error = err;
|
|
241
236
|
}
|
|
242
|
-
};
|
|
243
|
-
await Promise.all(Array.from(
|
|
244
|
-
{ length: Math.min(GREP_CONTEXT_READ_CONCURRENCY, entries.length) },
|
|
245
|
-
() => worker(),
|
|
246
|
-
));
|
|
237
|
+
}));
|
|
247
238
|
return groups;
|
|
248
239
|
}
|
|
249
240
|
|
|
@@ -88,16 +88,12 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
88
88
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
89
89
|
if (Array.isArray(args.path)) {
|
|
90
90
|
const list = [...new Set(args.path.map((p) => (typeof p === 'string' ? p.trim() : '')).filter((p) => p.length > 0))];
|
|
91
|
-
const
|
|
92
|
-
const targets = capped ? list.slice(0, 10) : list;
|
|
91
|
+
const targets = list;
|
|
93
92
|
if (targets.length > 1) {
|
|
94
|
-
//
|
|
93
|
+
// Full parallel fan-out: each target is an independent listing
|
|
95
94
|
// (own guard/cache/reachability), so run them concurrently instead
|
|
96
95
|
// of serially. Bodies land in a fixed-index array so the emitted
|
|
97
|
-
// section order still matches the caller's `path[]` order
|
|
98
|
-
// wall-clock cost changes. Concurrency is capped so a 10-path batch
|
|
99
|
-
// cannot exhaust the child-spawn / FS-handle budget.
|
|
100
|
-
const LIST_FANOUT_CONCURRENCY = 4;
|
|
96
|
+
// section order still matches the caller's `path[]` order.
|
|
101
97
|
// Collapse semantic-duplicate targets: two spellings that resolve to
|
|
102
98
|
// the same directory (e.g. `foo` and `./foo`) are stat'd/walked ONCE.
|
|
103
99
|
// Each caller label keeps its own `# list <p>` section reusing the
|
|
@@ -116,24 +112,14 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
116
112
|
if (!repByKey.has(k)) { repByKey.set(k, targets[i]); uniqueKeys.push(k); }
|
|
117
113
|
}
|
|
118
114
|
const bodyByKey = new Map();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const k = uniqueKeys[i];
|
|
125
|
-
try {
|
|
126
|
-
bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
|
|
127
|
-
} catch (err) {
|
|
128
|
-
bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
129
|
-
}
|
|
115
|
+
await Promise.all(uniqueKeys.map(async (k) => {
|
|
116
|
+
try {
|
|
117
|
+
bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
130
120
|
}
|
|
131
|
-
};
|
|
132
|
-
await Promise.all(
|
|
133
|
-
Array.from({ length: Math.min(LIST_FANOUT_CONCURRENCY, uniqueKeys.length) }, runWorker),
|
|
134
|
-
);
|
|
121
|
+
}));
|
|
135
122
|
const sections = targets.map((p, i) => `# list ${p}\n${bodyByKey.get(keyByIndex[i])}`);
|
|
136
|
-
if (capped) sections.push(`... [capped at 10 of ${list.length} paths]`);
|
|
137
123
|
return sections.join('\n\n');
|
|
138
124
|
}
|
|
139
125
|
args.path = targets[0];
|
|
@@ -582,8 +568,7 @@ async function getTargetedFindEnumeration({
|
|
|
582
568
|
export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
583
569
|
if (Array.isArray(args.query)) {
|
|
584
570
|
const list = [...new Set(args.query.map((q) => (typeof q === 'string' ? q.trim() : '')).filter((q) => q.length > 0))];
|
|
585
|
-
const
|
|
586
|
-
const targets = capped ? list.slice(0, 5) : list;
|
|
571
|
+
const targets = list;
|
|
587
572
|
if (targets.length > 1) {
|
|
588
573
|
// head_limit is one call-level result budget, not a multiplier per
|
|
589
574
|
// query. Split it deterministically in caller order; if it cannot
|
|
@@ -601,7 +586,6 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
601
586
|
+ `retry query=${JSON.stringify(omittedByHeadLimit)}]`,
|
|
602
587
|
);
|
|
603
588
|
}
|
|
604
|
-
if (capped) notes.push(`... [capped at 5 of ${list.length} queries]`);
|
|
605
589
|
const sectionHeaders = activeTargets.map((query) => `# find ${query}\n`);
|
|
606
590
|
const separatorBytes = Math.max(0, activeTargets.length + notes.length - 1) * 2;
|
|
607
591
|
const fixedBytes = sectionHeaders.reduce(
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resizeImageBuffer,
|
|
7
7
|
} from './read-image-resize.mjs';
|
|
8
8
|
|
|
9
|
-
// Image extensions
|
|
9
|
+
// Image extensions that Read renders as image blocks.
|
|
10
10
|
const IMAGE_MIME = {
|
|
11
11
|
'.png': 'image/png',
|
|
12
12
|
'.jpg': 'image/jpeg',
|
|
@@ -112,7 +112,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
112
112
|
// R1: UNC / SMB share reject (\\server\share, //server/share). Reading
|
|
113
113
|
// these on Windows auto-authenticates to the remote host and leaks the
|
|
114
114
|
// NTLM hash of the current user to any attacker-controlled SMB target.
|
|
115
|
-
//
|
|
115
|
+
// The prefix is rejected before stat.
|
|
116
116
|
// Must run before resolveAgainstCwd so a relative path can't be coerced
|
|
117
117
|
// into a UNC share by the cwd resolution.
|
|
118
118
|
if (typeof isUncPath === 'function' && isUncPath(filePath))
|
|
@@ -142,7 +142,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
142
142
|
return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(fullPath)}`;
|
|
143
143
|
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath))
|
|
144
144
|
return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(fullPath)}`;
|
|
145
|
-
// Pre-read size cap
|
|
145
|
+
// Pre-read size cap:
|
|
146
146
|
// throw a small error response when the file is too big rather
|
|
147
147
|
// than truncating to 25K tokens of content. Throw is decisively
|
|
148
148
|
// more token-efficient (Anthropic #21841 reverted truncation).
|
|
@@ -712,7 +712,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
712
712
|
out = appendReadContextAdvisory(out, { filePath, lineCount, bytes: st.size });
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
|
-
//
|
|
715
|
+
// An empty file gets a system-reminder instead of
|
|
716
716
|
// a bare `1│` line. The reminder makes the empty-state
|
|
717
717
|
// explicit so the agent doesn't assume content was elided.
|
|
718
718
|
if (content.length === 0) {
|
|
@@ -24,20 +24,11 @@ function _stripLineCoordForReach(s) {
|
|
|
24
24
|
.replace(/:\d+(?:-\d+)?(?::.*)?$/, '');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// Batch fan-out cap: array/object read shapes read only the first N entries
|
|
28
|
-
// (the >N slice below truncates the rest with a `_batchCapNote`). The
|
|
29
|
-
// reachability preflight must honour the SAME cap so a path that will be
|
|
30
|
-
// capped away is never stat-probed and can never reject the batch via a guard.
|
|
31
|
-
const READ_BATCH_PATH_CAP = 10;
|
|
32
|
-
|
|
33
27
|
function _collectReachCandidates(p) {
|
|
34
28
|
const out = [];
|
|
35
29
|
const push = (s) => { if (typeof s === 'string' && s) out.push(s); };
|
|
36
30
|
if (typeof p === 'string') push(p);
|
|
37
|
-
|
|
38
|
-
// preflight only the paths that will actually be read. Capped-away paths
|
|
39
|
-
// (incl. UNC/device) must NOT be probed or allowed to fail the batch.
|
|
40
|
-
else if (Array.isArray(p)) for (const e of p.slice(0, READ_BATCH_PATH_CAP)) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
|
|
31
|
+
else if (Array.isArray(p)) for (const e of p) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
|
|
41
32
|
return out;
|
|
42
33
|
}
|
|
43
34
|
|
|
@@ -181,14 +172,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
181
172
|
// Single turn can touch many files or swap modes without
|
|
182
173
|
// the agent iterating across multiple tool names.
|
|
183
174
|
if (Array.isArray(args.path) && args.path.length > 0 && args.path[0] && typeof args.path[0] === 'object') {
|
|
184
|
-
// Cap batch fan-out: never error on an oversized array, just
|
|
185
|
-
// truncate and note it in the final output (mirrors list-tool.mjs
|
|
186
|
-
// path[] cap pattern).
|
|
187
|
-
if (args.path.length > 10) {
|
|
188
|
-
const _origObjLen = args.path.length;
|
|
189
|
-
args.path = args.path.slice(0, 10);
|
|
190
|
-
args._batchCapNote = `... [capped at 10 of ${_origObjLen} paths]`;
|
|
191
|
-
}
|
|
192
175
|
// Per-file batch: each entry carries its own options.
|
|
193
176
|
// Coalesce same-path entries: multiple chunks for the same
|
|
194
177
|
// file are merged into a single wider read (min offset to max
|
|
@@ -244,11 +227,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
244
227
|
args.mode = undefined; args.n = undefined; args.offset = undefined; args.limit = undefined; args.full = undefined;
|
|
245
228
|
}
|
|
246
229
|
if (Array.isArray(args.path)) {
|
|
247
|
-
if (args.path.length > 10) {
|
|
248
|
-
const _origStrLen = args.path.length;
|
|
249
|
-
args.path = args.path.slice(0, 10);
|
|
250
|
-
if (!args._batchCapNote) args._batchCapNote = `... [capped at 10 of ${_origStrLen} paths]`;
|
|
251
|
-
}
|
|
252
230
|
// Schema is `path: string | string[]` — array entries are
|
|
253
231
|
// strings only. Top-level mode / n / offset / limit / full
|
|
254
232
|
// apply uniformly to every entry in the batch (the only
|
|
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import {
|
|
9
9
|
acquire as acquireChildSpawnSlot,
|
|
10
|
-
|
|
10
|
+
snapshot as childSpawnSnapshot,
|
|
11
11
|
} from '../../../../shared/child-spawn-gate.mjs';
|
|
12
12
|
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
@@ -38,7 +38,7 @@ const execFileAsync = promisify(execFile);
|
|
|
38
38
|
// This does not queue or limit calls; it only prevents per-process
|
|
39
39
|
// oversubscription. Env override remains available for operator tuning.
|
|
40
40
|
let _rgDefaultThreadCap = null;
|
|
41
|
-
export function _rgThreadCap(
|
|
41
|
+
export function _rgThreadCap(activeSearches = 1) {
|
|
42
42
|
const override = Number(process.env.MIXDOG_RG_THREADS);
|
|
43
43
|
if (Number.isFinite(override) && override >= 1) return Math.floor(override);
|
|
44
44
|
if (_rgDefaultThreadCap === null) {
|
|
@@ -47,7 +47,12 @@ export function _rgThreadCap(_spareCapacity = false) {
|
|
|
47
47
|
_rgDefaultThreadCap = cpus || 4;
|
|
48
48
|
}
|
|
49
49
|
const cpus = _rgDefaultThreadCap;
|
|
50
|
-
|
|
50
|
+
const perProcessMax = Math.max(2, Math.min(4, Math.ceil(cpus / 4)));
|
|
51
|
+
const active = typeof activeSearches === 'boolean'
|
|
52
|
+
? 1
|
|
53
|
+
: Math.max(1, Math.floor(Number(activeSearches) || 1));
|
|
54
|
+
const aggregateBudget = Math.max(perProcessMax, cpus - 1);
|
|
55
|
+
return Math.max(1, Math.min(perProcessMax, Math.floor(aggregateBudget / active)));
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// Walk rg options once for all thread-argument helpers. Values of these
|
|
@@ -94,9 +99,18 @@ function _hasRgThreadArg(argsList) {
|
|
|
94
99
|
// (`-j`/`--threads`/`-jN`/`--threads=N`, e.g. the EAGAIN `-j 1` retry). Does
|
|
95
100
|
// not mutate; may return the ORIGINAL array unchanged when already
|
|
96
101
|
// thread-pinned (callers must not assume a fresh copy).
|
|
97
|
-
export function _withRgThreads(argsList,
|
|
102
|
+
export function _withRgThreads(argsList, activeSearches = 1) {
|
|
98
103
|
if (_hasRgThreadArg(argsList)) return argsList;
|
|
99
|
-
return ['--threads', String(_rgThreadCap(
|
|
104
|
+
return ['--threads', String(_rgThreadCap(activeSearches)), ...argsList];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function activeSearchSpawnCount() {
|
|
108
|
+
try {
|
|
109
|
+
const lane = childSpawnSnapshot().lanes.find((entry) => entry.name === 'search');
|
|
110
|
+
return Math.max(1, Number(lane?.inflight) || 1);
|
|
111
|
+
} catch {
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
// Build the EAGAIN single-thread retry args. If the caller already pinned a
|
|
@@ -409,10 +423,10 @@ function spawnRg(argsList, execOptions) {
|
|
|
409
423
|
// spawn below, so queue-wait time is excluded from the 20s deadline. No
|
|
410
424
|
// node guardian is started: rg owns its full SIGTERM→grace→force-kill +
|
|
411
425
|
// force-settle teardown, so the 1:1 guardian process was pure overhead.
|
|
412
|
-
return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
426
|
+
return acquireChildSpawnSlot(gateSignal, 'search').then((releaseSlot) => new Promise((resolve, reject) => {
|
|
413
427
|
// Probe after acquisition: this spawn counts toward inflight. The
|
|
414
428
|
// answer is best-effort and only selects the per-process rg cap.
|
|
415
|
-
const proc = spawn(rgExecutable(), _withRgThreads(argsList,
|
|
429
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
|
|
416
430
|
cwd: execOptions?.cwd,
|
|
417
431
|
env: execOptions?.env || process.env,
|
|
418
432
|
windowsHide: true,
|
|
@@ -591,9 +605,9 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
|
|
|
591
605
|
// Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
|
|
592
606
|
// excluded from the rg timeout (armed after spawn). releaseSlot is fired
|
|
593
607
|
// once via .finally on every settle path.
|
|
594
|
-
return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
608
|
+
return acquireChildSpawnSlot(gateSignal, 'search').then((releaseSlot) => new Promise((resolve, reject) => {
|
|
595
609
|
// As above, probe only after this spawn has acquired its gate slot.
|
|
596
|
-
const proc = spawn(rgExecutable(), _withRgThreads(argsList,
|
|
610
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
|
|
597
611
|
cwd: execOptions?.cwd,
|
|
598
612
|
env: execOptions?.env || process.env,
|
|
599
613
|
windowsHide: true,
|
|
@@ -171,6 +171,13 @@ export async function tryReadFamilyEnoentRedirect({
|
|
|
171
171
|
|
|
172
172
|
export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'ENOENT', cache = null) {
|
|
173
173
|
if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return '';
|
|
174
|
+
// `C:tmpsmp` shape: a Windows path whose separators were eaten by JSON
|
|
175
|
+
// escaping (`"C:\\tmp\\smp"` written as `"C:\tmp\smp"`). Name the cause —
|
|
176
|
+
// the sibling/basename hints below can never resolve such a path.
|
|
177
|
+
const rawMissing = String(missingPath || '');
|
|
178
|
+
if (/^[A-Za-z]:(?![\\/])[^\\/]/.test(rawMissing.replace(/\\/g, '\\'))) {
|
|
179
|
+
return ` The path has a drive letter but no separator after it — the backslashes were probably lost in escaping. Re-issue with forward slashes (e.g. "C:/tmp/smp").`;
|
|
180
|
+
}
|
|
174
181
|
if (resolveUniqueEnoentRedirect(workDir, missingPath, errCode, cache)) return '';
|
|
175
182
|
const elsewhere = cachedFileByBasename(workDir, missingPath, cache);
|
|
176
183
|
if (elsewhere.length) {
|
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
parseGrepCountLine,
|
|
82
82
|
} from './lib/search-input-helpers.mjs';
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// A single glob string may pack multiple filters
|
|
85
85
|
// separated by whitespace or commas, e.g. "*.ts,*.tsx" or "*.ts *.tsx". Split
|
|
86
86
|
// each into its own --glob. Brace patterns ("*.{ts,tsx}") are left intact so
|
|
87
87
|
// their internal commas are not torn apart.
|
|
@@ -127,19 +127,16 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
127
127
|
args = normalizeGrepArgs(args);
|
|
128
128
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
129
129
|
const callContextCharBudget = _grepContextCharBudget(options);
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// assembled below in input order, regardless of completion order.
|
|
130
|
+
// Batch multiple string paths concurrently. Recursive calls pass a single
|
|
131
|
+
// string path, so recursion bottoms out after one level. Results retain
|
|
132
|
+
// input order even though every path starts immediately.
|
|
134
133
|
if (Array.isArray(args.path)) {
|
|
135
|
-
const GREP_PATH_CAP = 10;
|
|
136
|
-
const GREP_PATH_CONCURRENCY = 4;
|
|
137
134
|
const seen = new Set();
|
|
138
135
|
const list = args.path
|
|
139
136
|
.map(p => typeof p === 'string' ? p.trim() : '')
|
|
140
137
|
.filter(p => p && !seen.has(p) && seen.add(p));
|
|
141
138
|
if (list.length > 1) {
|
|
142
|
-
const capped = list
|
|
139
|
+
const capped = list;
|
|
143
140
|
// Combined single-spawn path[] fan-out (mirrors the pattern[]
|
|
144
141
|
// combined path below): ONE rg run with every path as a positional
|
|
145
142
|
// operand, then normalized-prefix attribution rebuilds the
|
|
@@ -151,7 +148,6 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
151
148
|
// per-path ENOENT/UNC diagnostics). MIXDOG_GREP_PATH_COMBINED=0
|
|
152
149
|
// disables.
|
|
153
150
|
combinedPaths: if (process.env.MIXDOG_GREP_PATH_COMBINED !== '0'
|
|
154
|
-
&& list.length <= GREP_PATH_CAP
|
|
155
151
|
&& !args.glob
|
|
156
152
|
&& !args.type
|
|
157
153
|
&& args.multiline !== true
|
|
@@ -340,38 +336,20 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
340
336
|
const configuredOutputCap = Number(options?.toolOutputMaxBytes) > 0
|
|
341
337
|
? Math.trunc(Number(options.toolOutputMaxBytes))
|
|
342
338
|
: Math.trunc(Number(process.env.MIXDOG_TOOL_OUTPUT_MAX_BYTES));
|
|
343
|
-
const bodies =
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
readStateScope,
|
|
355
|
-
nestedOptions,
|
|
356
|
-
);
|
|
357
|
-
} catch (err) {
|
|
358
|
-
bodies[index] = `Error: ${err && err.message ? err.message : err}`;
|
|
359
|
-
}
|
|
339
|
+
const bodies = await Promise.all(capped.map(async (p) => {
|
|
340
|
+
try {
|
|
341
|
+
return await executeGrepTool(
|
|
342
|
+
{ ...args, path: p },
|
|
343
|
+
workDir,
|
|
344
|
+
executeChildBuiltinTool,
|
|
345
|
+
readStateScope,
|
|
346
|
+
nestedOptions,
|
|
347
|
+
);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return `Error: ${err && err.message ? err.message : err}`;
|
|
360
350
|
}
|
|
361
|
-
};
|
|
362
|
-
await Promise.all(
|
|
363
|
-
Array.from(
|
|
364
|
-
{ length: Math.min(GREP_PATH_CONCURRENCY, capped.length) },
|
|
365
|
-
() => runWorker(),
|
|
366
|
-
),
|
|
367
|
-
);
|
|
351
|
+
}));
|
|
368
352
|
const parts = capped.map((p, index) => `# grep ${p}\n${bodies[index]}`);
|
|
369
|
-
if (list.length > GREP_PATH_CAP) {
|
|
370
|
-
parts.push(`[capped at ${GREP_PATH_CAP} of ${list.length} paths]`);
|
|
371
|
-
// Omitted paths mean the returned result cannot cover the whole
|
|
372
|
-
// requested path set — never let it be cached as complete.
|
|
373
|
-
if (options?.scopedCacheOutcome) markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
374
|
-
}
|
|
375
353
|
const output = parts.join('\n\n');
|
|
376
354
|
if (configuredOutputCap > 0
|
|
377
355
|
&& Buffer.byteLength(output, 'utf8') > configuredOutputCap
|
|
@@ -560,6 +560,10 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
|
|
|
560
560
|
const name = String(shellName || '').toLowerCase();
|
|
561
561
|
const isLegacyPS = /powershell/.test(name) && !/pwsh/.test(name);
|
|
562
562
|
const violations = [];
|
|
563
|
+
// Violation kinds drive the bash auto-rescue: a command blocked ONLY for
|
|
564
|
+
// bash syntax can be run in bash unchanged, while a PowerShell-specific
|
|
565
|
+
// violation ($PID reassignment) must stay a hard block.
|
|
566
|
+
const kinds = [];
|
|
563
567
|
// `$PID=` reassignment and `&&` connectors are judged on a quote-masked
|
|
564
568
|
// copy so quoted literals (`echo "a && b"`, `Write-Output '$PID=1'`) and
|
|
565
569
|
// regex/search args are never mistaken for real syntax.
|
|
@@ -572,14 +576,17 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
|
|
|
572
576
|
const first = String(tokens[0] || '').toLowerCase().replace(/^.*[\\/]/, '');
|
|
573
577
|
if (POWERSHELL_BASHISM_HINTS[first]) {
|
|
574
578
|
violations.push(`\`${first}\` is a Unix command: ${POWERSHELL_BASHISM_HINTS[first]}`);
|
|
579
|
+
kinds.push('bashism');
|
|
575
580
|
}
|
|
576
581
|
}
|
|
577
582
|
}
|
|
578
583
|
if (/\$PID\s*=(?!=)/i.test(masked)) {
|
|
579
584
|
violations.push('`$PID` is a reserved PowerShell automatic variable — do not reassign it (use a different name).');
|
|
585
|
+
kinds.push('powershell-only');
|
|
580
586
|
}
|
|
581
587
|
if (isLegacyPS && /(?:^|[^&])&&(?:[^&]|$)/.test(masked)) {
|
|
582
588
|
violations.push('`&&` is not supported in Windows PowerShell 5.1 — use `;` to sequence or issue separate calls.');
|
|
589
|
+
kinds.push('bashism');
|
|
583
590
|
}
|
|
584
591
|
|
|
585
592
|
if (violations.length > 0) {
|
|
@@ -587,12 +594,78 @@ export function preflightPowerShellHygiene(command, { shellType, shellName } = {
|
|
|
587
594
|
return {
|
|
588
595
|
command: rewritten,
|
|
589
596
|
note,
|
|
597
|
+
// The MSYS path rewrite is a PowerShell-targeted normalisation, so
|
|
598
|
+
// a bash rescue must re-run the ORIGINAL text.
|
|
599
|
+
original,
|
|
600
|
+
bashOnly: kinds.length > 0 && kinds.every((kind) => kind === 'bashism'),
|
|
590
601
|
block: `PowerShell preflight blocked this command (bash syntax on a PowerShell host). Fix and retry:\n- ${hints.join('\n- ')}`,
|
|
591
602
|
};
|
|
592
603
|
}
|
|
593
604
|
return { command: rewritten, note, block: null };
|
|
594
605
|
}
|
|
595
606
|
|
|
607
|
+
// PowerShell-only constructs. Used to decide whether a command the PowerShell
|
|
608
|
+
// preflight blocked for bash syntax can simply be RUN in bash: only a command
|
|
609
|
+
// with zero PowerShell-specific syntax qualifies, so a mixed script (bash
|
|
610
|
+
// pipes plus `$env:`/cmdlets/`2>$null`) stays a hard block instead of being
|
|
611
|
+
// silently rerouted into a shell that would mangle it. Deliberately
|
|
612
|
+
// over-inclusive — a false positive only declines the rescue.
|
|
613
|
+
const POWERSHELL_ONLY_SYNTAX = [
|
|
614
|
+
/\$(?:null|true|false|env:|_\b|args\b|host\b|profile\b|pwd\b|psversiontable\b|psscriptroot\b|lastexitcode\b|erroractionpreference\b)/i,
|
|
615
|
+
/\$[A-Za-z_]\w*\s*=/,
|
|
616
|
+
/\b(?:get|set|new|remove|select|where|foreach|invoke|out|write|start|stop|test|measure|sort|join|split|convert|import|export|add|copy|move|rename|resolve|compare|clear|push|pop)-[A-Za-z]+\b/i,
|
|
617
|
+
/\[[A-Za-z_][\w.]*\]::/,
|
|
618
|
+
/(?:^|\s)-(?:eq|ne|gt|lt|ge|le|match|notmatch|like|notlike|contains|replace|not|and|or|is|as|ErrorAction|Recurse|Force|Encoding|TotalCount|Tail|Pattern|Path|LiteralPath)\b/,
|
|
619
|
+
/2>\s*\$/,
|
|
620
|
+
/\|\s*[%?]\s*\{/,
|
|
621
|
+
/@['"][\s\S]*?['"]@/,
|
|
622
|
+
/`\s*$/m,
|
|
623
|
+
];
|
|
624
|
+
export function hasPowerShellOnlySyntax(command) {
|
|
625
|
+
const text = String(command || '');
|
|
626
|
+
if (!text.trim()) return false;
|
|
627
|
+
return POWERSHELL_ONLY_SYNTAX.some((pattern) => pattern.test(text));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── Inline-script hoisting ──────────────────────────────────────────────────
|
|
631
|
+
// `node -e "<body>"` / `python -c "<body>"` hand the script through the host
|
|
632
|
+
// shell's quoting layer, where nested quotes and escapes are the measured top
|
|
633
|
+
// cause of inline-script failures. When the body is quote-LITERAL (the shell
|
|
634
|
+
// would pass it through byte for byte) the same run is expressible as a file
|
|
635
|
+
// invocation, which has no quoting layer at all — so the failure cannot occur.
|
|
636
|
+
//
|
|
637
|
+
// The transform is refused whenever file semantics would differ from `-e`:
|
|
638
|
+
// - a body with $, backtick or a backslash escape (the shell WOULD rewrite it);
|
|
639
|
+
// - anything resolved relative to the script (relative require/import,
|
|
640
|
+
// import.meta, __dirname/__filename);
|
|
641
|
+
// - a body that reads process.argv[1] (undefined under -e, a path in a file).
|
|
642
|
+
const INLINE_SCRIPT_RE = /\b(node(?:\.exe)?|python3?|py)((?:\s+--?[\w-]+(?:=[^\s"']+)?)*)\s+(-e|--eval|-c)\s+"((?:[^"\\]|\\.)*)"/;
|
|
643
|
+
const INLINE_UNSAFE_BODY = /[$`\\]/;
|
|
644
|
+
const INLINE_FILE_RELATIVE = /(?:require|import)\s*\(\s*['"]\.{1,2}\/|from\s+['"]\.{1,2}\/|import\.meta|__dirname|__filename|argv\s*\[\s*1\s*\]/;
|
|
645
|
+
|
|
646
|
+
export function planInlineScriptHoist(command) {
|
|
647
|
+
const text = String(command || '');
|
|
648
|
+
if (!text.trim()) return null;
|
|
649
|
+
const match = text.match(INLINE_SCRIPT_RE);
|
|
650
|
+
if (!match) return null;
|
|
651
|
+
const [whole, exe, rawFlags, , body] = match;
|
|
652
|
+
if (INLINE_UNSAFE_BODY.test(body)) return null;
|
|
653
|
+
if (INLINE_FILE_RELATIVE.test(body)) return null;
|
|
654
|
+
const flags = String(rawFlags || '');
|
|
655
|
+
const isNode = /^node/i.test(exe);
|
|
656
|
+
const esm = /--input-type=module/.test(flags);
|
|
657
|
+
const extension = isNode ? (esm ? '.mjs' : '.cjs') : '.py';
|
|
658
|
+
// `--input-type` only describes an inline body; the extension carries the
|
|
659
|
+
// module kind for a file, so the flag is dropped with the body.
|
|
660
|
+
const keptFlags = flags.replace(/\s+--input-type=\w+/g, '');
|
|
661
|
+
return {
|
|
662
|
+
exe,
|
|
663
|
+
extension,
|
|
664
|
+
body,
|
|
665
|
+
replace: (filePath) => text.replace(whole, `${exe}${keptFlags} "${filePath}"`),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
596
669
|
export async function preflightShellLargeFileProbe(command, cwd) {
|
|
597
670
|
const text = String(command || '').trim();
|
|
598
671
|
let localCwd = resolve(cwd || process.cwd());
|
|
@@ -213,7 +213,7 @@ export function shellJobDonePath(jobId) { return join(getShellJobsDir(), `${jobI
|
|
|
213
213
|
// enforces unconditionally and records detail.timeoutEnforced:true.
|
|
214
214
|
export function shellJobEnforcedPath(jobId) { return join(getShellJobsDir(), `${jobId}.enforced`); }
|
|
215
215
|
// Guardian kill receipt: written by the detached child-guardian immediately
|
|
216
|
-
// before it
|
|
216
|
+
// before it cleans up a job tree whose owning parent exited.
|
|
217
217
|
// Its existence lets refreshShellJob attribute a marker-less death to the
|
|
218
218
|
// guardian instead of reporting an unexplained unknown exit.
|
|
219
219
|
export function shellJobGuardianReceiptPath(jobId) { return join(getShellJobsDir(), `${jobId}.guardian`); }
|
|
@@ -272,7 +272,6 @@ export async function _startBackgroundShellJobImpl({
|
|
|
272
272
|
childPid: child.pid,
|
|
273
273
|
childGroupPid: child.pid,
|
|
274
274
|
label: 'shell-job',
|
|
275
|
-
protectHostMemory: true,
|
|
276
275
|
receiptPath: shellJobGuardianReceiptPath(jobId),
|
|
277
276
|
});
|
|
278
277
|
_installShellJobsExitHook();
|
|
@@ -523,7 +522,6 @@ async function startBackgroundPowerShellJob({
|
|
|
523
522
|
childPid,
|
|
524
523
|
childGroupPid: childPid,
|
|
525
524
|
label: 'shell-job-powershell',
|
|
526
|
-
protectHostMemory: true,
|
|
527
525
|
receiptPath: shellJobGuardianReceiptPath(jobId),
|
|
528
526
|
});
|
|
529
527
|
_installShellJobsExitHook();
|