mixdog 0.8.0 → 0.9.0
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/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,15 +1,105 @@
|
|
|
1
|
-
import { spawn,
|
|
1
|
+
import { spawn, execFile } from 'child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { acquire as acquireChildSpawnSlot } from '../../../../shared/child-spawn-gate.mjs';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
// ── rg orphan-reap policy (accepted risk) ────────────────────────────────
|
|
11
|
+
// The previous design started a 1:1 node child-guardian per rg spawn so a
|
|
12
|
+
// hard daemon kill (SIGKILL, no chance to run our own cleanup) could not leave
|
|
13
|
+
// an rg orphaned. That per-process guardian is exactly what over-saturated the
|
|
14
|
+
// box (each grep doubled the process count), so it has been REMOVED here and
|
|
15
|
+
// deliberately NOT replaced with another per-rg guardian.
|
|
16
|
+
//
|
|
17
|
+
// Accepted residual risk: on a hard daemon SIGKILL an in-flight rg can briefly
|
|
18
|
+
// outlive the daemon. This is judged acceptable because (a) on every NORMAL
|
|
19
|
+
// path rg is reaped by this module's own timeout → SIGTERM → grace → force-kill
|
|
20
|
+
// plus the force-settle backstop, so it is always collected when the daemon is
|
|
21
|
+
// alive to settle the promise; (b) rg is a short-lived, self-terminating
|
|
22
|
+
// (bounded by the 20s timeout) read-only process, not a long-running server; a
|
|
23
|
+
// stray instance exits on its own shortly after. A shared/OS-job-based reaper
|
|
24
|
+
// (one guardian for the whole daemon, or a Windows Job Object / POSIX process
|
|
25
|
+
// group kill on daemon teardown) is the right long-term fix but is out of scope
|
|
26
|
+
// for this change and intentionally not added per-spawn.
|
|
27
|
+
|
|
28
|
+
// ── rg --threads cap ─────────────────────────────────────────────────────
|
|
29
|
+
// Each rg process otherwise fans out across EVERY core; with several agents
|
|
30
|
+
// running grep at once the box over-saturates and the whole batch trips the
|
|
31
|
+
// 20s deadline together. Cap rg's worker threads to a fraction of the host so
|
|
32
|
+
// concurrent greps share the machine. Internal dynamic default; env override
|
|
33
|
+
// only — deliberately NOT exposed on any tool schema / parameter surface.
|
|
34
|
+
function _rgThreadCap() {
|
|
35
|
+
const override = Number(process.env.MIXDOG_RG_THREADS);
|
|
36
|
+
if (Number.isFinite(override) && override >= 1) return Math.floor(override);
|
|
37
|
+
let cpus = 0;
|
|
38
|
+
try { cpus = os.cpus()?.length || 0; } catch { cpus = 0; }
|
|
39
|
+
return Math.max(2, Math.ceil((cpus || 4) / 4));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Single source of truth for "did the caller already pin rg's thread count?".
|
|
43
|
+
// Detects every form rg accepts: separated (`-j`, `--threads`) where the count
|
|
44
|
+
// is the NEXT arg, short-attached (`-j8`), and long-attached (`--threads=8`).
|
|
45
|
+
// Used by both the _withRgThreads injection guard and the EAGAIN retry guard
|
|
46
|
+
// so the two never disagree.
|
|
47
|
+
function _hasRgThreadArg(argsList) {
|
|
48
|
+
for (const a of argsList) {
|
|
49
|
+
if (typeof a !== 'string') continue;
|
|
50
|
+
if (a === '-j' || a === '--threads') return true;
|
|
51
|
+
if (/^-j\d+$/.test(a)) return true;
|
|
52
|
+
if (a.startsWith('--threads=')) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inject `--threads N` unless the caller already pinned thread count
|
|
58
|
+
// (`-j`/`--threads`/`-jN`/`--threads=N`, e.g. the EAGAIN `-j 1` retry). Does
|
|
59
|
+
// not mutate; may return the ORIGINAL array unchanged when already
|
|
60
|
+
// thread-pinned (callers must not assume a fresh copy).
|
|
61
|
+
function _withRgThreads(argsList) {
|
|
62
|
+
if (_hasRgThreadArg(argsList)) return argsList;
|
|
63
|
+
return ['--threads', String(_rgThreadCap()), ...argsList];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build the EAGAIN single-thread retry args. If the caller already pinned a
|
|
67
|
+
// thread count we strip the existing flag(s) first, then prepend `-j 1`, so the
|
|
68
|
+
// retry is unambiguously single-threaded (never `-j 8 ... -j 1`). Drops both the
|
|
69
|
+
// separated form (`-j N` / `--threads N` → flag + following count) and the
|
|
70
|
+
// attached forms (`-jN` / `--threads=N`).
|
|
71
|
+
function _rgEagainRetryArgs(argsList) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (let i = 0; i < argsList.length; i++) {
|
|
74
|
+
const a = argsList[i];
|
|
75
|
+
if (a === '-j' || a === '--threads') { i++; continue; } // skip flag + its count
|
|
76
|
+
if (typeof a === 'string' && (/^-j\d+$/.test(a) || a.startsWith('--threads='))) continue;
|
|
77
|
+
out.push(a);
|
|
78
|
+
}
|
|
79
|
+
return ['-j', '1', ...out];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// True only when the args are ALREADY exactly single-threaded — i.e. an EAGAIN
|
|
83
|
+
// retry would change nothing. Matches the separated `-j 1` / `--threads 1` form
|
|
84
|
+
// and the attached `-j1` / `--threads=1` form. Any other thread pin (e.g. -j8)
|
|
85
|
+
// is NOT "single-thread pinned": the retry should still run and downshift it.
|
|
86
|
+
function _isRgSingleThreadPinned(argsList) {
|
|
87
|
+
for (let i = 0; i < argsList.length; i++) {
|
|
88
|
+
const a = argsList[i];
|
|
89
|
+
if (typeof a !== 'string') continue;
|
|
90
|
+
if ((a === '-j' || a === '--threads') && String(argsList[i + 1]).trim() === '1') return true;
|
|
91
|
+
if (a === '-j1' || a === '--threads=1') return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
5
95
|
|
|
6
96
|
let _rgExecutableResolved = null;
|
|
7
97
|
|
|
8
|
-
function _resolveRgExecutable() {
|
|
98
|
+
async function _resolveRgExecutable() {
|
|
9
99
|
const isWin = process.platform === 'win32';
|
|
10
100
|
try {
|
|
11
101
|
const cmd = isWin ? 'where' : 'which';
|
|
12
|
-
const out =
|
|
102
|
+
const { stdout: out } = await execFileAsync(cmd, ['rg'], { encoding: 'utf8', windowsHide: true });
|
|
13
103
|
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
14
104
|
if (first && existsSync(first)) return first;
|
|
15
105
|
} catch { /* fall through to PATH scan */ }
|
|
@@ -33,11 +123,27 @@ function _resolveRgExecutable() {
|
|
|
33
123
|
return 'rg';
|
|
34
124
|
}
|
|
35
125
|
|
|
126
|
+
// Synchronous accessor for the spawn call sites — only valid AFTER
|
|
127
|
+
// ensureRgResolved() has run (runRg/runRgWindowedLines await it first). Falls
|
|
128
|
+
// back to the bare 'rg' if somehow called before resolution.
|
|
36
129
|
function rgExecutable() {
|
|
37
|
-
|
|
38
|
-
|
|
130
|
+
return _rgExecutableResolved ?? 'rg';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Async resolver: the `where`/`which` lookup used to run via execFileSync,
|
|
134
|
+
// blocking the event loop ~90ms on the first grep/glob/find of a session
|
|
135
|
+
// (freezing the TUI). Resolve asynchronously and cache the result; subsequent
|
|
136
|
+
// calls return immediately.
|
|
137
|
+
let _rgResolvePromise = null;
|
|
138
|
+
async function ensureRgResolved() {
|
|
139
|
+
if (_rgExecutableResolved !== null) return _rgExecutableResolved;
|
|
140
|
+
if (!_rgResolvePromise) {
|
|
141
|
+
_rgResolvePromise = _resolveRgExecutable().then((resolved) => {
|
|
142
|
+
_rgExecutableResolved = resolved;
|
|
143
|
+
return resolved;
|
|
144
|
+
});
|
|
39
145
|
}
|
|
40
|
-
return
|
|
146
|
+
return _rgResolvePromise;
|
|
41
147
|
}
|
|
42
148
|
|
|
43
149
|
// When _resolveRgExecutable() exhausts `where`/`which` and the PATH scan and
|
|
@@ -48,13 +154,13 @@ function rgExecutable() {
|
|
|
48
154
|
const RG_FALLBACK_FAIL_TTL_MS = 30000;
|
|
49
155
|
let _rgFallbackUsable = null;
|
|
50
156
|
let _rgFallbackFailAt = 0;
|
|
51
|
-
function rgFallbackUsable() {
|
|
157
|
+
async function rgFallbackUsable() {
|
|
52
158
|
if (_rgFallbackUsable === true) return true;
|
|
53
159
|
if (_rgFallbackUsable === false && (Date.now() - _rgFallbackFailAt) < RG_FALLBACK_FAIL_TTL_MS) {
|
|
54
160
|
return false;
|
|
55
161
|
}
|
|
56
162
|
try {
|
|
57
|
-
|
|
163
|
+
await execFileAsync('rg', ['--version'], { windowsHide: true, timeout: 3000 });
|
|
58
164
|
_rgFallbackUsable = true;
|
|
59
165
|
} catch {
|
|
60
166
|
_rgFallbackUsable = false;
|
|
@@ -65,10 +171,11 @@ function rgFallbackUsable() {
|
|
|
65
171
|
|
|
66
172
|
// Throws a clear error only when the bare 'rg' fallback was reached AND it is
|
|
67
173
|
// not actually runnable. A real resolved path short-circuits with no probe, so
|
|
68
|
-
// behavior is unchanged whenever rg exists.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
174
|
+
// behavior is unchanged whenever rg exists. Async so the underlying lookups
|
|
175
|
+
// (`where`/`which`, `rg --version`) never block the event loop.
|
|
176
|
+
async function assertRgAvailable() {
|
|
177
|
+
if (await ensureRgResolved() !== 'rg') return;
|
|
178
|
+
if (await rgFallbackUsable()) return;
|
|
72
179
|
const e = new Error('ripgrep (rg) not found on PATH — install ripgrep or add it to PATH');
|
|
73
180
|
e.code = 'ERG_NOT_FOUND';
|
|
74
181
|
throw e;
|
|
@@ -168,14 +275,19 @@ function _armRgForceSettle({ timeoutMs, isSettled, timers, proc, onForceSettle }
|
|
|
168
275
|
// surfaced as empty stdout so callers can render "(no matches)" uniformly.
|
|
169
276
|
function spawnRg(argsList, execOptions) {
|
|
170
277
|
const timeoutMs = Number(execOptions?.timeout ?? 20000);
|
|
171
|
-
|
|
172
|
-
|
|
278
|
+
const gateSignal = execOptions?.abortSignal || execOptions?.signal || null;
|
|
279
|
+
// Gate the spawn (not the whole call) so over-saturation queues here while
|
|
280
|
+
// the cap's worth of rg keep running. The rg timeout timer is armed AFTER
|
|
281
|
+
// spawn below, so queue-wait time is excluded from the 20s deadline. No
|
|
282
|
+
// node guardian is started: rg owns its full SIGTERM→grace→force-kill +
|
|
283
|
+
// force-settle teardown, so the 1:1 guardian process was pure overhead.
|
|
284
|
+
return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
285
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList), {
|
|
173
286
|
cwd: execOptions?.cwd,
|
|
174
287
|
env: execOptions?.env || process.env,
|
|
175
288
|
windowsHide: true,
|
|
176
289
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
177
290
|
});
|
|
178
|
-
startChildGuardian({ childPid: proc.pid, label: 'rg-runner' });
|
|
179
291
|
let stdout = '';
|
|
180
292
|
let stderr = '';
|
|
181
293
|
let timedOut = false;
|
|
@@ -282,17 +394,23 @@ function spawnRg(argsList, execOptions) {
|
|
|
282
394
|
e.stderr = stderr;
|
|
283
395
|
reject(e);
|
|
284
396
|
});
|
|
285
|
-
|
|
397
|
+
// Release the gate slot exactly once on ANY settle path (resolve/reject:
|
|
398
|
+
// close, error, or force-settle). releaseSlot is idempotent.
|
|
399
|
+
}).finally(() => releaseSlot()));
|
|
286
400
|
}
|
|
287
401
|
|
|
288
402
|
export async function runRg(argsList, execOptions = {}) {
|
|
289
|
-
assertRgAvailable();
|
|
403
|
+
await assertRgAvailable();
|
|
290
404
|
try {
|
|
291
405
|
return await spawnRg(argsList, execOptions);
|
|
292
406
|
} catch (err) {
|
|
293
407
|
const msg = String(err?.message || err?.stderr || '');
|
|
294
|
-
|
|
295
|
-
|
|
408
|
+
// Retry single-threaded on EAGAIN. Skip only when the caller already
|
|
409
|
+
// pinned threads AND that pin is already -j 1 (nothing to gain); any
|
|
410
|
+
// other pin (-j8/--threads=N) is rewritten to a clean -j 1 by
|
|
411
|
+
// _rgEagainRetryArgs so we never emit conflicting thread flags.
|
|
412
|
+
if (/EAGAIN/i.test(msg) && !_isRgSingleThreadPinned(argsList)) {
|
|
413
|
+
return spawnRg(_rgEagainRetryArgs(argsList), execOptions);
|
|
296
414
|
}
|
|
297
415
|
throw err;
|
|
298
416
|
}
|
|
@@ -304,14 +422,17 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
|
|
|
304
422
|
const lineLimit = Number.isFinite(opts.limit) ? Math.max(0, Math.floor(Number(opts.limit) || 0)) : Infinity;
|
|
305
423
|
const summaryLimit = Math.max(0, Math.floor(Number(opts.summaryLimit) || 0));
|
|
306
424
|
const collectLimit = lineLimit === Infinity ? Infinity : Math.max(lineLimit, summaryLimit);
|
|
307
|
-
|
|
308
|
-
|
|
425
|
+
const gateSignal = execOptions?.abortSignal || execOptions?.signal || null;
|
|
426
|
+
// Same gate + thread-cap + no-guardian treatment as spawnRg; queue-wait is
|
|
427
|
+
// excluded from the rg timeout (armed after spawn). releaseSlot is fired
|
|
428
|
+
// once via .finally on every settle path.
|
|
429
|
+
return acquireChildSpawnSlot(gateSignal).then((releaseSlot) => new Promise((resolve, reject) => {
|
|
430
|
+
const proc = spawn(rgExecutable(), _withRgThreads(argsList), {
|
|
309
431
|
cwd: execOptions?.cwd,
|
|
310
432
|
env: execOptions?.env || process.env,
|
|
311
433
|
windowsHide: true,
|
|
312
434
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
313
435
|
});
|
|
314
|
-
startChildGuardian({ childPid: proc.pid, label: 'rg-runner-windowed' });
|
|
315
436
|
let buffer = '';
|
|
316
437
|
let stderr = '';
|
|
317
438
|
let skipped = 0;
|
|
@@ -448,17 +569,17 @@ function spawnRgWindowedLines(argsList, execOptions, opts = {}) {
|
|
|
448
569
|
e.stderr = stderr;
|
|
449
570
|
reject(e);
|
|
450
571
|
});
|
|
451
|
-
});
|
|
572
|
+
}).finally(() => releaseSlot()));
|
|
452
573
|
}
|
|
453
574
|
|
|
454
575
|
export async function runRgWindowedLines(argsList, execOptions = {}, opts = {}) {
|
|
455
|
-
assertRgAvailable();
|
|
576
|
+
await assertRgAvailable();
|
|
456
577
|
try {
|
|
457
578
|
return await spawnRgWindowedLines(argsList, execOptions, opts);
|
|
458
579
|
} catch (err) {
|
|
459
580
|
const msg = String(err?.message || err?.stderr || '');
|
|
460
|
-
if (/EAGAIN/i.test(msg) && !argsList
|
|
461
|
-
return spawnRgWindowedLines(
|
|
581
|
+
if (/EAGAIN/i.test(msg) && !_isRgSingleThreadPinned(argsList)) {
|
|
582
|
+
return spawnRgWindowedLines(_rgEagainRetryArgs(argsList), execOptions, opts);
|
|
462
583
|
}
|
|
463
584
|
throw err;
|
|
464
585
|
}
|
|
@@ -114,13 +114,13 @@ export function buildGrepRgArgs(parts) {
|
|
|
114
114
|
return rgArgs;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore }) {
|
|
117
|
+
export function buildGlobCacheKey({ patterns, basePath, headLimit, offset, extraIgnore, sort }) {
|
|
118
118
|
// extraIgnore (rg ignore globs from _extraIgnoreDirs) alters which files
|
|
119
119
|
// match, so it MUST partake in the key — otherwise calls that differ only
|
|
120
120
|
// by extra ignores collide and return stale over-/under-filtered results.
|
|
121
121
|
// Sorted so the same ignore set in any order maps to one key.
|
|
122
122
|
const extra = Array.isArray(extraIgnore) && extraIgnore.length ? [...extraIgnore].sort().join('\x01') : '';
|
|
123
|
-
return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', extra].join('|');
|
|
123
|
+
return ['glob', patterns.join('\x01'), basePath, headLimit ?? '', offset ?? '', sort || 'natural', extra].join('|');
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export function buildListCacheKey(parts) {
|
|
@@ -30,11 +30,14 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
cacheGet,
|
|
32
32
|
cacheSet,
|
|
33
|
+
runResultCacheInFlight,
|
|
33
34
|
statPathsForMtime,
|
|
34
35
|
} from './cache-layers.mjs';
|
|
35
36
|
import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
36
37
|
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
|
|
37
38
|
|
|
39
|
+
const MIXDOG_GREP_CASE_HINT_PROBE = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_GREP_CASE_HINT_PROBE || ''));
|
|
40
|
+
|
|
38
41
|
function expandLegacyEscapedAlternationPattern(rawPattern) {
|
|
39
42
|
if (typeof rawPattern !== 'string' || !rawPattern.includes('\\|')) return null;
|
|
40
43
|
const parts = rawPattern.split('\\|').map((part) => part.trim()).filter(Boolean);
|
|
@@ -180,6 +183,14 @@ function splitGlobString(value) {
|
|
|
180
183
|
return out;
|
|
181
184
|
}
|
|
182
185
|
|
|
186
|
+
function isRedundantAllFilesGlob(value) {
|
|
187
|
+
const g = canonicalizeGlobSlashes(String(value || '').trim())
|
|
188
|
+
.replace(/^\.\//, '')
|
|
189
|
+
.replace(/^\/+/, '')
|
|
190
|
+
.replace(/\/+$/, '');
|
|
191
|
+
return g === '**/*' || g === '**';
|
|
192
|
+
}
|
|
193
|
+
|
|
183
194
|
function resolveSearchScope(root, workDir) {
|
|
184
195
|
return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
|
|
185
196
|
}
|
|
@@ -226,46 +237,6 @@ function parseGrepCountLine(line) {
|
|
|
226
237
|
return { path, count };
|
|
227
238
|
}
|
|
228
239
|
|
|
229
|
-
// Per-pattern file-count probe for array grby: runs ONE rg --files-with-matches
|
|
230
|
-
// per pattern (reusing the same scope/glob/type/flags as the merged search) so
|
|
231
|
-
// the summary line can surface patterns that matched zero files — otherwise the
|
|
232
|
-
// merged result hides which member of the array contributed nothing.
|
|
233
|
-
async function _perPatternFileCounts({ patterns, searchPath, globPatterns, caseInsensitive, multilineMode, fileType, workDir }) {
|
|
234
|
-
const results = await Promise.all(patterns.map(async (pattern) => {
|
|
235
|
-
try {
|
|
236
|
-
const rgArgs = buildGrepRgArgs({
|
|
237
|
-
patterns: [pattern],
|
|
238
|
-
searchPath,
|
|
239
|
-
globPatterns,
|
|
240
|
-
outputMode: 'files_with_matches',
|
|
241
|
-
caseInsensitive,
|
|
242
|
-
showLineNumbers: false,
|
|
243
|
-
beforeN: null,
|
|
244
|
-
afterN: null,
|
|
245
|
-
contextN: null,
|
|
246
|
-
multilineMode,
|
|
247
|
-
fileType,
|
|
248
|
-
onlyMatching: false,
|
|
249
|
-
});
|
|
250
|
-
const stdout = await runRg(rgArgs, { cwd: workDir });
|
|
251
|
-
// A boxed partial/truncated stdout means the count is not the true
|
|
252
|
-
// total — render '?' rather than a misleadingly-low or zero number.
|
|
253
|
-
if (stdout && typeof stdout === 'object' && (stdout.partial || stdout.truncated)) return null;
|
|
254
|
-
return String(stdout).split('\n').filter(Boolean).length;
|
|
255
|
-
} catch {
|
|
256
|
-
// Probe threw → count unknown, NOT zero.
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
}));
|
|
260
|
-
// Quote first (so newline/control chars can't inject footer lines), then
|
|
261
|
-
// truncate the quoted string to 40 chars — matches the no-match path.
|
|
262
|
-
const trunc = (p) => {
|
|
263
|
-
const q = JSON.stringify(p);
|
|
264
|
-
return q.length > 40 ? `${q.slice(0, 40)}...` : q;
|
|
265
|
-
};
|
|
266
|
-
return `\n# per-pattern: ${patterns.map((p, i) => `${trunc(p)}=${results[i] === null ? '?' : `${results[i]} files`}`).join(', ')}`;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
240
|
function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns: _patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath: _grepResolvedPath, workDir, globPatterns: _globPatterns, fileType: _fileType, filenameOmitted = false, prefix = '', broadAdvisory: _broadAdvisory = true }) {
|
|
270
241
|
const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
|
|
271
242
|
const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
|
|
@@ -378,7 +349,9 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
378
349
|
// ripgrep `--glob` uses forward slashes on all platforms; canonicalize
|
|
379
350
|
// `\`→`/` (win32 only) so a `**\*.ts` filter matches instead of being
|
|
380
351
|
// parsed as an escape sequence.
|
|
381
|
-
const normalizedGlobPatterns = uniqueStrings(globPatterns
|
|
352
|
+
const normalizedGlobPatterns = uniqueStrings(globPatterns
|
|
353
|
+
.map(canonicalizeGlobSlashes)
|
|
354
|
+
.filter((g) => !isRedundantAllFilesGlob(g)));
|
|
382
355
|
|
|
383
356
|
const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'count']);
|
|
384
357
|
const rawOutputMode = typeof args.output_mode === 'string' ? args.output_mode.trim() : '';
|
|
@@ -484,6 +457,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
484
457
|
// drift detection. So only the fresh-compute path (below) records a read.
|
|
485
458
|
if (cached !== null) return cached;
|
|
486
459
|
|
|
460
|
+
return await runResultCacheInFlight(cacheKey, async () => {
|
|
487
461
|
let grepStat;
|
|
488
462
|
try { grepStat = statSync(grepResolvedPath); }
|
|
489
463
|
catch (err) {
|
|
@@ -597,7 +571,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
597
571
|
// pre-offset matches) just means the window skipped past real
|
|
598
572
|
// case-sensitive hits, so the hint would be misleading.
|
|
599
573
|
const trueZeroMatch = offset === 0 && totalWindowed === 0;
|
|
600
|
-
if (trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
|
|
574
|
+
if (MIXDOG_GREP_CASE_HINT_PROBE && trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
|
|
601
575
|
try {
|
|
602
576
|
const probeArgs = buildGrepRgArgs({
|
|
603
577
|
patterns,
|
|
@@ -620,21 +594,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
620
594
|
} catch { /* best-effort hint */ }
|
|
621
595
|
}
|
|
622
596
|
}
|
|
623
|
-
|
|
624
|
-
// zero-hit members of the merged result are not silently hidden.
|
|
625
|
-
let perPatternSummary = '';
|
|
626
|
-
if (patterns.length > 1) {
|
|
627
|
-
perPatternSummary = await _perPatternFileCounts({
|
|
628
|
-
patterns,
|
|
629
|
-
searchPath,
|
|
630
|
-
globPatterns: normalizedGlobPatterns,
|
|
631
|
-
caseInsensitive,
|
|
632
|
-
multilineMode,
|
|
633
|
-
fileType,
|
|
634
|
-
workDir: rgSpawnCwd,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
const out = body + rgPartialSuffix + perPatternSummary;
|
|
597
|
+
const out = body + rgPartialSuffix;
|
|
638
598
|
const shownLines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
|
|
639
599
|
const remaining = Math.max(0, totalWindowed - shownLines.length);
|
|
640
600
|
// Mirrors formatGrepOutput truncation / totalKnown semantics.
|
|
@@ -749,6 +709,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
749
709
|
const msg = stderr || err?.message || String(err);
|
|
750
710
|
return `Error: ${msg.slice(0, 500)}`;
|
|
751
711
|
}
|
|
712
|
+
});
|
|
752
713
|
}
|
|
753
714
|
|
|
754
715
|
export async function executeGlobTool(args, workDir, options = {}) {
|
|
@@ -835,6 +796,8 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
835
796
|
return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
|
|
836
797
|
}
|
|
837
798
|
const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
|
|
799
|
+
const rawSort = typeof args.sort === 'string' ? args.sort.trim() : '';
|
|
800
|
+
const sortMode = rawSort === 'mtime' ? 'mtime' : 'natural';
|
|
838
801
|
// Internal-only ignore extension (see normalizeGlobArgs). Caller (e.g.
|
|
839
802
|
// ai-wrapped-dispatch broad-cwd preflight) appends basename ignore globs
|
|
840
803
|
// so head_limit bounds SOURCE entries rather than artifact noise.
|
|
@@ -860,10 +823,11 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
860
823
|
.map((root) => normalizeOutputPath(resolvedForSearchRoot(root)))
|
|
861
824
|
.sort()
|
|
862
825
|
.join('\x01');
|
|
863
|
-
const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs });
|
|
826
|
+
const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs, sort: sortMode });
|
|
864
827
|
const cached = cacheGet(cacheKey);
|
|
865
828
|
if (cached !== null) return cached;
|
|
866
829
|
|
|
830
|
+
return await runResultCacheInFlight(cacheKey, async () => {
|
|
867
831
|
const globGroups = [...groups.entries()];
|
|
868
832
|
|
|
869
833
|
const allFiles = [];
|
|
@@ -929,23 +893,26 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
929
893
|
}
|
|
930
894
|
|
|
931
895
|
const unique = Array.from(new Set(allFiles));
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
896
|
+
let orderedPaths;
|
|
897
|
+
if (sortMode === 'mtime') {
|
|
898
|
+
// Opt-in mtime sorting is intentionally slower: it stats every match.
|
|
899
|
+
// Bound the post-rg stat phase so a hung mount cannot pin glob until
|
|
900
|
+
// the agent stall watchdog fires.
|
|
901
|
+
const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
902
|
+
const withStat = withStatAll.filter((entry) => entry?.stat != null);
|
|
903
|
+
withStat.sort((a, b) => {
|
|
904
|
+
const dm = b.mtime - a.mtime;
|
|
905
|
+
if (dm !== 0) return dm;
|
|
906
|
+
return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
|
|
907
|
+
});
|
|
908
|
+
orderedPaths = withStat.map((entry) => entry.full || resolveAgainstCwd(entry.path, workDir));
|
|
909
|
+
} else {
|
|
910
|
+
orderedPaths = unique.map((entry) => isAbsolute(entry) ? resolve(entry) : resolveAgainstCwd(entry, workDir));
|
|
911
|
+
}
|
|
912
|
+
const totalBeforeOffset = orderedPaths.length;
|
|
913
|
+
const windowed = offset > 0 ? orderedPaths.slice(offset) : orderedPaths;
|
|
914
|
+
const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit))
|
|
915
|
+
.map((abs) => relativeSearchResultPath(abs, workDir));
|
|
949
916
|
const remaining = windowed.length - capped.length;
|
|
950
917
|
const truncSuffix = accumTruncated
|
|
951
918
|
? '\n... [truncated at accumulation cap (50000)]'
|
|
@@ -979,4 +946,5 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
979
946
|
try { options.onProgress(`found ${totalBeforeOffset} files`); } catch { /* best-effort */ }
|
|
980
947
|
}
|
|
981
948
|
return out;
|
|
949
|
+
});
|
|
982
950
|
}
|