mixdog 0.9.95 → 0.9.97
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/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -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/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- 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/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- 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 +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -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 +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- 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/session-lock.mjs +38 -2
- 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-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -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 +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- 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/fs-reachability.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 +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -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 +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -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/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- 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 +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -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 +13 -34
- 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 +9 -6
- 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-config-flags.mjs +1 -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-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- 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 +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- 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/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- 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 +31 -33
- 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 +10 -3
- 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/ToolExecution.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/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
2
|
import { executeSingleReadTool } from './read-single-tool.mjs';
|
|
3
3
|
import { imageMimeForPath, readImageAsContent } from './read-image.mjs';
|
|
4
4
|
import { readEntryCoalescedDiskWindow } from './read-batch.mjs';
|
|
@@ -6,6 +6,8 @@ import { readPathStringGuardError } from './read-open.mjs';
|
|
|
6
6
|
import { parseReadLineNumberArg } from './read-args.mjs';
|
|
7
7
|
import { assertPathsReachable } from './fs-reachability.mjs';
|
|
8
8
|
import { coerceReadFamilyPathArg } from './path-utils.mjs';
|
|
9
|
+
import { readIoAdmission } from '../../../../shared/tool-workload-gates.mjs';
|
|
10
|
+
import { currentToolExecutionOwner } from '../../../../shared/tool-execution-owner.mjs';
|
|
9
11
|
|
|
10
12
|
function hasLineCoordinate(path) {
|
|
11
13
|
return typeof path === 'string' && /(?:#L\d+|:\d+(?:-\d+)?(?::|$))/i.test(path);
|
|
@@ -24,20 +26,11 @@ function _stripLineCoordForReach(s) {
|
|
|
24
26
|
.replace(/:\d+(?:-\d+)?(?::.*)?$/, '');
|
|
25
27
|
}
|
|
26
28
|
|
|
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
29
|
function _collectReachCandidates(p) {
|
|
34
30
|
const out = [];
|
|
35
31
|
const push = (s) => { if (typeof s === 'string' && s) out.push(s); };
|
|
36
32
|
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);
|
|
33
|
+
else if (Array.isArray(p)) for (const e of p) push((e && typeof e === 'object') ? (e.path ?? e.file_path) : e);
|
|
41
34
|
return out;
|
|
42
35
|
}
|
|
43
36
|
|
|
@@ -59,15 +52,14 @@ function _guardedReadError(p, helpers) {
|
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
// Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
|
|
62
|
-
// run before any
|
|
63
|
-
//
|
|
64
|
-
// the 630s dispatch ceiling.
|
|
55
|
+
// run before any filesystem access. Besides bounding a dead mount, its Stats
|
|
56
|
+
// objects seed the real read so the common path does not immediately re-stat.
|
|
65
57
|
async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
66
58
|
const {
|
|
67
59
|
normalizeInputPath, resolveAgainstCwd,
|
|
68
60
|
} = helpers;
|
|
69
61
|
// A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
|
|
70
|
-
// REJECTED here, not skipped: skipping would let the later
|
|
62
|
+
// REJECTED here, not skipped: skipping would let the later guard/open
|
|
71
63
|
// path touch it and trigger NTLM/raw-device access or hang. Reject up front
|
|
72
64
|
// with the same message the inline guards emit.
|
|
73
65
|
// normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
|
|
@@ -80,7 +72,7 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
80
72
|
const stripped = _stripLineCoordForReach(normalizeInputPath(raw));
|
|
81
73
|
const full = resolveAgainstCwd(stripped, workDir);
|
|
82
74
|
const guardMsg = _guardedReadError(stripped, helpers) || _guardedReadError(full, helpers);
|
|
83
|
-
if (guardMsg) return guardMsg;
|
|
75
|
+
if (guardMsg) return { error: guardMsg, statsByPath: null };
|
|
84
76
|
// Dedup by resolved path so a batch repeating the same file (or the
|
|
85
77
|
// same union window) issues one stat probe, not one per entry —
|
|
86
78
|
// bounding the preflight's FS work to the distinct target set.
|
|
@@ -88,9 +80,17 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
88
80
|
seenFull.add(full);
|
|
89
81
|
candidates.push(full);
|
|
90
82
|
}
|
|
91
|
-
if (candidates.length === 0) return null;
|
|
92
|
-
try {
|
|
93
|
-
|
|
83
|
+
if (candidates.length === 0) return { error: null, statsByPath: new Map() };
|
|
84
|
+
try {
|
|
85
|
+
const stats = await assertPathsReachable(candidates);
|
|
86
|
+
const statsByPath = new Map();
|
|
87
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
88
|
+
if (stats[i]) statsByPath.set(candidates[i], stats[i]);
|
|
89
|
+
}
|
|
90
|
+
return { error: null, statsByPath };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { error: `Error: ${e?.message || e}`, statsByPath: null };
|
|
93
|
+
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export async function executeReadTool(args, workDir, readStateScope, executeChildBuiltinTool, options = {}, helpers = {}) {
|
|
@@ -127,8 +127,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
130
|
-
// Reachability preflight up front (all shapes) — before
|
|
131
|
-
// image stat
|
|
130
|
+
// Reachability preflight up front (all shapes) — before
|
|
131
|
+
// readPathStringGuardError / image stat.
|
|
132
132
|
// options._skipReachPreflight: set only by the batch dispatcher on its
|
|
133
133
|
// child reads (below). The parent batch call already ran this exact
|
|
134
134
|
// preflight over EVERY candidate path in the array (_collectReachCandidates
|
|
@@ -137,8 +137,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
137
137
|
// child (readPathStringGuardError / image fast-path) — only the async
|
|
138
138
|
// reachability stat is skipped, never the security guards.
|
|
139
139
|
if (options?._skipReachPreflight !== true) {
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
140
|
+
const _preflight = await _readReachPreflight(args.path, workDir, helpers);
|
|
141
|
+
if (_preflight.error) return _preflight.error;
|
|
142
|
+
options = { ...options, _preflightStats: _preflight.statsByPath };
|
|
142
143
|
}
|
|
143
144
|
// Image files (png/jpg/jpeg/gif/webp): return an MCP image block so the
|
|
144
145
|
// model can actually SEE the image. native Read does this, but mixdog's
|
|
@@ -149,7 +150,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
149
150
|
if (options?.mediaTextOnly !== true && typeof args.path === 'string' && imageMimeForPath(args.path)) {
|
|
150
151
|
const _imgNorm = normalizeInputPath(args.path);
|
|
151
152
|
// W1 H: device-file / UNC / Windows-device / ADS guards must run
|
|
152
|
-
// BEFORE the image fast-path so
|
|
153
|
+
// BEFORE the image fast-path so stat/readFile of a
|
|
153
154
|
// UNC/device path can't bypass the checks the normal read path
|
|
154
155
|
// enforces (NTLM hash leak, raw-device access, ADS).
|
|
155
156
|
if (typeof isUncPath === 'function' && isUncPath(_imgNorm))
|
|
@@ -167,7 +168,11 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
167
168
|
return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(_imgFull)}`;
|
|
168
169
|
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(_imgFull))
|
|
169
170
|
return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(_imgFull)}`;
|
|
170
|
-
const _imgResult = await readImageAsContent(
|
|
171
|
+
const _imgResult = await readImageAsContent(
|
|
172
|
+
_imgFull,
|
|
173
|
+
normalizeOutputPath(_imgNorm),
|
|
174
|
+
options?._preflightStats?.get?.(_imgFull) || null,
|
|
175
|
+
);
|
|
171
176
|
if (_imgResult) return _imgResult;
|
|
172
177
|
}
|
|
173
178
|
// Unified-read dispatch (v0.6.283+):
|
|
@@ -181,14 +186,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
181
186
|
// Single turn can touch many files or swap modes without
|
|
182
187
|
// the agent iterating across multiple tool names.
|
|
183
188
|
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
189
|
// Per-file batch: each entry carries its own options.
|
|
193
190
|
// Coalesce same-path entries: multiple chunks for the same
|
|
194
191
|
// file are merged into a single wider read (min offset to max
|
|
@@ -244,11 +241,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
244
241
|
args.mode = undefined; args.n = undefined; args.offset = undefined; args.limit = undefined; args.full = undefined;
|
|
245
242
|
}
|
|
246
243
|
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
244
|
// Schema is `path: string | string[]` — array entries are
|
|
253
245
|
// strings only. Top-level mode / n / offset / limit / full
|
|
254
246
|
// apply uniformly to every entry in the batch (the only
|
|
@@ -421,7 +413,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
421
413
|
let rangeHashes = rangeHashesByPath.get(fullPath) || [];
|
|
422
414
|
if (rangeHashes.length === 0 && mergedRanges.length > 0) {
|
|
423
415
|
try {
|
|
424
|
-
const rawLines =
|
|
416
|
+
const rawLines = (await readFile(fullPath, 'utf-8')).split('\n');
|
|
425
417
|
rangeHashes = mergedRanges.map((range) => {
|
|
426
418
|
const startIdx = Math.max(0, range.startLine - 1);
|
|
427
419
|
const endIdx = Math.min(rawLines.length, range.endLine);
|
|
@@ -630,11 +622,16 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
630
622
|
// Mode routing. A window already dropped any conflicting head/tail/summary
|
|
631
623
|
// glance above (so the window is served by executeSingleReadTool); what
|
|
632
624
|
// remains here is a mode-only read, or count/hex which are not text windows.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
625
|
+
const runReadIo = (task) => readIoAdmission.run(
|
|
626
|
+
options?.callerSessionId || options?.sessionId || currentToolExecutionOwner(),
|
|
627
|
+
task,
|
|
628
|
+
{ signal: options?.signal || options?.abortSignal || null },
|
|
629
|
+
);
|
|
630
|
+
if (args.mode === 'head') return runReadIo(() => executeChildBuiltinTool('head', { path: args.path, n: args.n }, workDir));
|
|
631
|
+
if (args.mode === 'tail') return runReadIo(() => executeChildBuiltinTool('tail', { path: args.path, n: args.n }, workDir));
|
|
632
|
+
if (args.mode === 'count') return runReadIo(() => executeChildBuiltinTool('wc', { path: args.path }, workDir));
|
|
633
|
+
if (args.mode === 'summary') return runReadIo(() => executeChildBuiltinTool('summary', { path: args.path, n: args.n, limit: args.limit }, workDir));
|
|
634
|
+
if (args.mode === 'hex') return runReadIo(() => executeChildBuiltinTool('hex', { path: args.path, n: args.n, offset: args.offset }, workDir));
|
|
635
|
+
return runReadIo(() => executeSingleReadTool(args, workDir, readStateScope, options, helpers));
|
|
639
636
|
|
|
640
637
|
}
|
|
@@ -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,12 @@ 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
|
|
426
|
+
return acquireChildSpawnSlot(gateSignal, 'search', {
|
|
427
|
+
ownerKey: execOptions?.ownerKey || execOptions?.callerSessionId || execOptions?.sessionId,
|
|
428
|
+
}).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
413
429
|
// Probe after acquisition: this spawn counts toward inflight. The
|
|
414
430
|
// answer is best-effort and only selects the per-process rg cap.
|
|
415
|
-
const proc = spawn(rgExecutable(), _withRgThreads(argsList,
|
|
431
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
|
|
416
432
|
cwd: execOptions?.cwd,
|
|
417
433
|
env: execOptions?.env || process.env,
|
|
418
434
|
windowsHide: true,
|
|
@@ -591,9 +607,11 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
|
|
|
591
607
|
// Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
|
|
592
608
|
// excluded from the rg timeout (armed after spawn). releaseSlot is fired
|
|
593
609
|
// once via .finally on every settle path.
|
|
594
|
-
return acquireChildSpawnSlot(gateSignal
|
|
610
|
+
return acquireChildSpawnSlot(gateSignal, 'search', {
|
|
611
|
+
ownerKey: execOptions?.ownerKey || execOptions?.callerSessionId || execOptions?.sessionId,
|
|
612
|
+
}).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
595
613
|
// As above, probe only after this spawn has acquired its gate slot.
|
|
596
|
-
const proc = spawn(rgExecutable(), _withRgThreads(argsList,
|
|
614
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList, activeSearchSpawnCount()), {
|
|
597
615
|
cwd: execOptions?.cwd,
|
|
598
616
|
env: execOptions?.env || process.env,
|
|
599
617
|
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();
|
|
@@ -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; }
|