mixdog 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { statSync } from 'fs';
|
|
2
2
|
import { isAbsolute, resolve } from 'path';
|
|
3
3
|
import { trueCasePath } from './path-utils.mjs';
|
|
4
|
-
import { findBySuffixStrip } from './path-diagnostics.mjs';
|
|
5
4
|
import {
|
|
6
5
|
canonicalizeGlobSlashes,
|
|
7
6
|
coerceShapeFlex,
|
|
@@ -14,13 +13,37 @@ import {
|
|
|
14
13
|
normalizeSearchPattern,
|
|
15
14
|
resolveAgainstCwd,
|
|
16
15
|
} from './path-utils.mjs';
|
|
16
|
+
import {
|
|
17
|
+
_suggestIndexedPaths,
|
|
18
|
+
basePathDiagnostic,
|
|
19
|
+
buildNotFoundHint,
|
|
20
|
+
isUncOrSmbPath,
|
|
21
|
+
relativePathPrefix,
|
|
22
|
+
relativeSearchResultPath,
|
|
23
|
+
resolveSearchScope,
|
|
24
|
+
stripEmbeddedPathQuotes,
|
|
25
|
+
uncRefusalMessage,
|
|
26
|
+
} from './search-path-diagnostics.mjs';
|
|
27
|
+
// Facade re-export: path-diagnostic helpers moved to search-path-diagnostics.mjs;
|
|
28
|
+
// keep prior importers of search-tool.mjs unchanged.
|
|
29
|
+
export {
|
|
30
|
+
_suggestIndexedPaths,
|
|
31
|
+
basePathDiagnostic,
|
|
32
|
+
buildNotFoundHint,
|
|
33
|
+
isUncOrSmbPath,
|
|
34
|
+
relativePathPrefix,
|
|
35
|
+
relativeSearchResultPath,
|
|
36
|
+
resolveSearchScope,
|
|
37
|
+
stripEmbeddedPathQuotes,
|
|
38
|
+
uncRefusalMessage,
|
|
39
|
+
} from './search-path-diagnostics.mjs';
|
|
17
40
|
import {
|
|
18
41
|
buildGlobCacheKey,
|
|
19
42
|
buildGrepCacheKey,
|
|
20
43
|
buildGrepRgArgs,
|
|
21
44
|
DEFAULT_IGNORE_GLOBS,
|
|
22
45
|
} from './search-builders.mjs';
|
|
23
|
-
import { runRg, runRgWindowedLines } from './rg-runner.mjs';
|
|
46
|
+
import { runRg, runRgWindowedLines, rgSupportsPcre2 } from './rg-runner.mjs';
|
|
24
47
|
import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
|
|
25
48
|
import {
|
|
26
49
|
groupGrepContentByFile,
|
|
@@ -35,7 +58,7 @@ import {
|
|
|
35
58
|
statPathsForMtime,
|
|
36
59
|
} from './cache-layers.mjs';
|
|
37
60
|
import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
38
|
-
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
|
|
61
|
+
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from './arg-guard.mjs';
|
|
39
62
|
|
|
40
63
|
// Default surrounding-lines window applied by output_mode:'content_with_context'
|
|
41
64
|
// when the caller does not pass an explicit -A/-B/-C/context. Sized to cover a
|
|
@@ -50,51 +73,6 @@ function expandLegacyEscapedAlternationPattern(rawPattern) {
|
|
|
50
73
|
return parts.length > 1 ? parts : null;
|
|
51
74
|
}
|
|
52
75
|
|
|
53
|
-
// Deterministic ENOENT recovery: when a grep path does not exist, surface
|
|
54
|
-
// indexed files that share the missing path's basename, turning a guessed or
|
|
55
|
-
// misplaced path (e.g. session/result-compression.mjs vs the real
|
|
56
|
-
// tools/result-compression.mjs) into the actual file in one step. Exact
|
|
57
|
-
// basename only — no stem/token fuzzing — so the hint is high-signal and
|
|
58
|
-
// noise-free. Invariant: every ENOENT runs the same basename lookup; there is
|
|
59
|
-
// no "guessed a lot" branch. Returns '' (appends nothing) when no same-named
|
|
60
|
-
// indexed file exists or the glob child is unavailable.
|
|
61
|
-
async function _suggestIndexedPaths(missingPath, executeChildBuiltinTool, workDir) {
|
|
62
|
-
if (typeof executeChildBuiltinTool !== 'function') return '';
|
|
63
|
-
const base = String(missingPath).replace(/\\/g, '/').split('/').pop();
|
|
64
|
-
// Skip when there is no usable basename or it carries glob magic (the
|
|
65
|
-
// pattern, not a literal filename, would not map to a real file).
|
|
66
|
-
if (!base || /[*?[\]{}]/.test(base)) return '';
|
|
67
|
-
try {
|
|
68
|
-
const out = await executeChildBuiltinTool('glob', { pattern: `**/${base}`, head_limit: 6 }, workDir);
|
|
69
|
-
if (typeof out !== 'string') return '';
|
|
70
|
-
// Drop ONLY the exact diagnostic forms glob emits: the empty-result line
|
|
71
|
-
// ("(no files found ...", "(no entries after offset ...") and the
|
|
72
|
-
// suffix/warning lines ("... [N more entries]", "... [warning] ...").
|
|
73
|
-
// A broad startsWith('(' / '...' / 'Error') would wrongly drop real
|
|
74
|
-
// paths like ErrorBoundary.mjs or a "(draft)/x.mjs" leading segment.
|
|
75
|
-
const hits = out.split('\n')
|
|
76
|
-
.filter((s) => s && !/^\(no (?:files found|entries\b)/.test(s) && !/^\.\.\. \[/.test(s))
|
|
77
|
-
.map((s) => s.trim())
|
|
78
|
-
.filter(Boolean)
|
|
79
|
-
.slice(0, 5);
|
|
80
|
-
return hits.length ? `\n[path not found here; same-named indexed file(s): ${hits.join(', ')}]` : '';
|
|
81
|
-
} catch {
|
|
82
|
-
return '';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function relativePathPrefix(pathPrefix, workDir) {
|
|
87
|
-
if (!workDir) return pathPrefix;
|
|
88
|
-
const cwdFwd = workDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
89
|
-
const absFwd = String(pathPrefix || '').replace(/\\/g, '/');
|
|
90
|
-
const haystack = process.platform === 'win32' ? absFwd.toLocaleLowerCase() : absFwd;
|
|
91
|
-
const needle = process.platform === 'win32' ? cwdFwd.toLocaleLowerCase() : cwdFwd;
|
|
92
|
-
if (haystack.startsWith(needle + '/') || haystack === needle) {
|
|
93
|
-
return absFwd.slice(cwdFwd.length + 1) || '.';
|
|
94
|
-
}
|
|
95
|
-
return pathPrefix;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
76
|
function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
|
|
99
77
|
const normalized = normalizeGrepLine(line, pathOnly, outputMode, filenameOmitted);
|
|
100
78
|
if (!workDir) return normalized;
|
|
@@ -113,15 +91,6 @@ function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content
|
|
|
113
91
|
return normalized;
|
|
114
92
|
}
|
|
115
93
|
|
|
116
|
-
function relativeSearchResultPath(path, workDir) {
|
|
117
|
-
const normalizedWorkDir = normalizeOutputPath(workDir);
|
|
118
|
-
const normalizedAbs = normalizeOutputPath(path);
|
|
119
|
-
if (normalizedAbs.startsWith(normalizedWorkDir + '/') || normalizedAbs.startsWith(normalizedWorkDir + '\\')) {
|
|
120
|
-
return normalizedAbs.slice(normalizedWorkDir.length + 1);
|
|
121
|
-
}
|
|
122
|
-
return normalizedAbs;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
94
|
function uniqueStrings(values) {
|
|
126
95
|
return Array.from(new Set(values.filter((value) => typeof value === 'string' && value)));
|
|
127
96
|
}
|
|
@@ -197,32 +166,6 @@ function isRedundantAllFilesGlob(value) {
|
|
|
197
166
|
return g === '**/*' || g === '**';
|
|
198
167
|
}
|
|
199
168
|
|
|
200
|
-
function resolveSearchScope(root, workDir) {
|
|
201
|
-
return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function isUncOrSmbPath(path) {
|
|
205
|
-
if (typeof path !== 'string' || !path) return false;
|
|
206
|
-
return path.startsWith('\\\\') || path.startsWith('//');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function uncRefusalMessage(toolName, original, resolved) {
|
|
210
|
-
const shown = normalizeOutputPath(resolved || original || '');
|
|
211
|
-
return `Error: ${toolName} refuses UNC/SMB path ${JSON.stringify(shown)}; remote share access is blocked to prevent NTLM credential leaks`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function basePathDiagnostic(basePaths, workDir) {
|
|
215
|
-
return basePaths.map((basePath) => {
|
|
216
|
-
const resolved = resolveSearchScope(basePath, workDir);
|
|
217
|
-
try {
|
|
218
|
-
const st = statSync(resolved);
|
|
219
|
-
return `${normalizeOutputPath(basePath)}: ${st.isDirectory() ? 'path exists (dir)' : 'path exists (file)'}`;
|
|
220
|
-
} catch (err) {
|
|
221
|
-
return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`;
|
|
222
|
-
}
|
|
223
|
-
}).join('; ');
|
|
224
|
-
}
|
|
225
|
-
|
|
226
169
|
function grepMissingPatternMessage() {
|
|
227
170
|
return 'Error: grep requires pattern.';
|
|
228
171
|
}
|
|
@@ -276,11 +219,37 @@ function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offs
|
|
|
276
219
|
|
|
277
220
|
export async function executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope = null, options = {}) {
|
|
278
221
|
args = normalizeGrepArgs(args);
|
|
222
|
+
// Fan-out guard: batch multiple string paths sequentially, mirroring
|
|
223
|
+
// code_graph files[] batching. Recursive calls pass a single string
|
|
224
|
+
// path, so recursion bottoms out after one level.
|
|
225
|
+
if (Array.isArray(args.path)) {
|
|
226
|
+
const GREP_PATH_CAP = 10;
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
const list = args.path
|
|
229
|
+
.map(p => typeof p === 'string' ? p.trim() : '')
|
|
230
|
+
.filter(p => p && !seen.has(p) && seen.add(p));
|
|
231
|
+
if (list.length > 1) {
|
|
232
|
+
const capped = list.slice(0, GREP_PATH_CAP);
|
|
233
|
+
const parts = [];
|
|
234
|
+
for (const p of capped) {
|
|
235
|
+
let body;
|
|
236
|
+
try {
|
|
237
|
+
body = await executeGrepTool({ ...args, path: p }, workDir, executeChildBuiltinTool, readStateScope, options);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
body = `Error: ${err && err.message ? err.message : err}`;
|
|
240
|
+
}
|
|
241
|
+
parts.push(`# grep ${p}\n${body}`);
|
|
242
|
+
}
|
|
243
|
+
if (list.length > GREP_PATH_CAP) parts.push(`[capped at ${GREP_PATH_CAP} of ${list.length} paths]`);
|
|
244
|
+
return parts.join('\n\n');
|
|
245
|
+
}
|
|
246
|
+
args.path = list[0];
|
|
247
|
+
}
|
|
279
248
|
// Shape context immediately before deriving rg flags. This keeps the
|
|
280
249
|
// Lead-direct MCP path and direct executeGrepTool callers on the same
|
|
281
250
|
// policy even if they bypass or race the outer builtin arg guard.
|
|
282
251
|
applyGrepContextLeadPolicy(args);
|
|
283
|
-
args.path = normalizeInputPath(args.path);
|
|
252
|
+
args.path = stripEmbeddedPathQuotes(normalizeInputPath(args.path));
|
|
284
253
|
args.pattern = coerceShapeFlex(args.pattern);
|
|
285
254
|
args.glob = coerceShapeFlex(args.glob);
|
|
286
255
|
const rawPattern = args.pattern;
|
|
@@ -303,13 +272,32 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
303
272
|
|
|
304
273
|
const GREP_MULTILINE_PATTERN_CAP = 5;
|
|
305
274
|
const GREP_ARRAY_PATTERN_CAP = 20;
|
|
306
|
-
|
|
275
|
+
// Rescue: a pattern containing a literal two-char "\n" (backslash + n)
|
|
276
|
+
// outside multiline mode makes rg hard-error with "the literal '\"\\n\"'
|
|
277
|
+
// is not allowed in a regex" — rg requires -U/--multiline before a
|
|
278
|
+
// pattern is allowed to match across a newline. Rather than reject-and-
|
|
279
|
+
// retry, auto-enable multiline whenever any pattern carries a literal
|
|
280
|
+
// \n; an explicit multiline:true from the caller still wins outright.
|
|
281
|
+
const patternsWantMultiline = patterns.some((p) => /\\n/.test(p));
|
|
282
|
+
const multilineMode = args.multiline === true || patternsWantMultiline;
|
|
307
283
|
if (multilineMode && patterns.length > GREP_MULTILINE_PATTERN_CAP) {
|
|
308
284
|
return `Error: multiline:true with more than ${GREP_MULTILINE_PATTERN_CAP} patterns is not allowed (got ${patterns.length}); split into separate grep calls`;
|
|
309
285
|
}
|
|
310
286
|
if (patterns.length > GREP_ARRAY_PATTERN_CAP) {
|
|
311
287
|
return `Error: pattern array exceeds the ${GREP_ARRAY_PATTERN_CAP}-pattern cap (got ${patterns.length}); split into separate grep calls`;
|
|
312
288
|
}
|
|
289
|
+
// Rescue: lookaround/backreference patterns are rejected by rg's default
|
|
290
|
+
// Rust regex engine ("look-around ... is not supported" / "backreferences
|
|
291
|
+
// ... not supported"). rg builds compiled with the optional `pcre2`
|
|
292
|
+
// feature accept the same syntax via -P/--pcre2. Probe capability once
|
|
293
|
+
// (cached) and route there instead of hard-erroring; when the installed
|
|
294
|
+
// rg has no PCRE2 support, fall through unchanged and let the existing
|
|
295
|
+
// arg-guard rejection (or rg's own runtime error) stand.
|
|
296
|
+
const patternsWantPcre2 = hasUnsupportedRipgrepRegex(patterns);
|
|
297
|
+
const pcre2Mode = patternsWantPcre2 && await rgSupportsPcre2();
|
|
298
|
+
if (patternsWantPcre2 && !pcre2Mode) {
|
|
299
|
+
return `Error: grep pattern uses regex syntax ripgrep does not support here (lookaround/backrefs), and the installed rg build has no PCRE2 support (-P unavailable). Use plain pattern arrays or simpler regex.`;
|
|
300
|
+
}
|
|
313
301
|
|
|
314
302
|
let searchPath = args.path || '.';
|
|
315
303
|
const rawGlob = args.glob;
|
|
@@ -454,6 +442,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
454
442
|
multilineMode,
|
|
455
443
|
onlyMatching: args['-o'] === true,
|
|
456
444
|
fileType,
|
|
445
|
+
pcre2: pcre2Mode,
|
|
457
446
|
});
|
|
458
447
|
// Single-file grep registers a whole-file read snapshot (parity with
|
|
459
448
|
// apply_patch), satisfying the read-before-edit guard while keeping drift
|
|
@@ -479,10 +468,8 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
479
468
|
try { grepStat = statSync(grepResolvedPath); }
|
|
480
469
|
catch (err) {
|
|
481
470
|
const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
|
|
482
|
-
const
|
|
483
|
-
if (
|
|
484
|
-
return msg + ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". Search that path directly.`;
|
|
485
|
-
}
|
|
471
|
+
const hint = buildNotFoundHint(workDir, grepResolvedPath, 'Search', err?.code);
|
|
472
|
+
if (hint) return msg + hint;
|
|
486
473
|
return msg + await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
|
|
487
474
|
}
|
|
488
475
|
const filenameOmitted = grepStat.isFile();
|
|
@@ -519,6 +506,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
519
506
|
multilineMode,
|
|
520
507
|
fileType,
|
|
521
508
|
onlyMatching: args['-o'] === true,
|
|
509
|
+
pcre2: pcre2Mode,
|
|
522
510
|
});
|
|
523
511
|
let windowed;
|
|
524
512
|
let totalWindowed = 0;
|
|
@@ -736,8 +724,8 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
736
724
|
export async function executeGlobTool(args, workDir, options = {}) {
|
|
737
725
|
args = normalizeGlobArgs(args);
|
|
738
726
|
args.path = Array.isArray(args.path)
|
|
739
|
-
? args.path.map((p) => normalizeInputPath(p)).filter((p) => typeof p === 'string' && p)
|
|
740
|
-
: normalizeInputPath(args.path);
|
|
727
|
+
? args.path.map((p) => stripEmbeddedPathQuotes(normalizeInputPath(p))).filter((p) => typeof p === 'string' && p)
|
|
728
|
+
: stripEmbeddedPathQuotes(normalizeInputPath(args.path));
|
|
741
729
|
if (Array.isArray(args.path) && args.path.length === 0) {
|
|
742
730
|
return 'Error: path array must contain at least one base directory';
|
|
743
731
|
}
|
|
@@ -866,8 +854,9 @@ export async function executeGlobTool(args, workDir, options = {}) {
|
|
|
866
854
|
rgArgs.push('.');
|
|
867
855
|
try { statSync(rgCwd); }
|
|
868
856
|
catch (err) {
|
|
857
|
+
const hint = buildNotFoundHint(workDir, rgCwd, 'Search', err?.code);
|
|
869
858
|
return {
|
|
870
|
-
error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})`,
|
|
859
|
+
error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})${hint}`,
|
|
871
860
|
paths: [],
|
|
872
861
|
stdoutTruncated: false,
|
|
873
862
|
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import * as fsPromises from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getPluginData } from '../../config.mjs';
|
|
5
|
+
|
|
6
|
+
// One-shot sweep of stale shell-job artefacts. Each backgrounded `bash`
|
|
7
|
+
// emits five files (.json/.done/.exit/.stdout.log/.stderr.log); the .done
|
|
8
|
+
// flag is written when the job exits, so a .done file older than
|
|
9
|
+
// SHELL_JOB_STALE_MS is the invariant proof its sibling files are also
|
|
10
|
+
// safe to remove. Active and recently-completed jobs are kept so
|
|
11
|
+
// `task` status/read/wait readers still find them. Runs once per mcp child
|
|
12
|
+
// lifetime on first getShellJobsDir() call. Async dirent walk + parallel
|
|
13
|
+
// stat/unlink keeps the main event loop free; fire-and-forget so the
|
|
14
|
+
// synchronous caller receives `dir` immediately.
|
|
15
|
+
const SHELL_JOB_STALE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
let shellJobsSwept = false;
|
|
17
|
+
async function sweepStaleShellJobs(dir) {
|
|
18
|
+
if (shellJobsSwept) return;
|
|
19
|
+
shellJobsSwept = true;
|
|
20
|
+
const cutoff = Date.now() - SHELL_JOB_STALE_MS;
|
|
21
|
+
let names;
|
|
22
|
+
try { names = await fsPromises.readdir(dir); } catch { return; }
|
|
23
|
+
const expired = [];
|
|
24
|
+
await Promise.all(names.map(async (name) => {
|
|
25
|
+
if (!name.endsWith('.done')) return;
|
|
26
|
+
const p = join(dir, name);
|
|
27
|
+
try {
|
|
28
|
+
const st = await fsPromises.stat(p);
|
|
29
|
+
if (st.mtimeMs < cutoff) expired.push(name.slice(0, -5));
|
|
30
|
+
} catch {}
|
|
31
|
+
}));
|
|
32
|
+
// Orphan reclaim: a crashed wrapper leaves <id>.json with no .done —
|
|
33
|
+
// forever. Invariant proofs of death (either suffices, both gated on the
|
|
34
|
+
// stale cutoff so a young orphan can't race its own spawn):
|
|
35
|
+
// a) deadline: the wrapper enforces timeoutMs, so an entry older than
|
|
36
|
+
// timeoutMs + grace cannot still be running — pid-reuse-proof. Only
|
|
37
|
+
// trusted on runtime proof: detail.timeoutEnforced:true (PS wrapper,
|
|
38
|
+
// unconditional) or the <id>.enforced marker the posix wrapper
|
|
39
|
+
// touches when its `timeout` branch actually runs.
|
|
40
|
+
// b) ESRCH: the recorded pid no longer exists. Alone this misses pids
|
|
41
|
+
// recycled by unrelated live processes, hence (a).
|
|
42
|
+
const ORPHAN_DEADLINE_GRACE_MS = 30 * 60_000;
|
|
43
|
+
const doneSet = new Set(names.filter(n => n.endsWith('.done')).map(n => n.slice(0, -5)));
|
|
44
|
+
await Promise.all(names.map(async (name) => {
|
|
45
|
+
if (!name.endsWith('.json')) return;
|
|
46
|
+
const jobId = name.slice(0, -5);
|
|
47
|
+
if (doneSet.has(jobId)) return;
|
|
48
|
+
const p = join(dir, name);
|
|
49
|
+
try {
|
|
50
|
+
const st = await fsPromises.stat(p);
|
|
51
|
+
if (st.mtimeMs >= cutoff) return;
|
|
52
|
+
const detail = JSON.parse(await fsPromises.readFile(p, 'utf-8'));
|
|
53
|
+
const tmo = Number(detail?.timeoutMs);
|
|
54
|
+
const enforced = detail?.timeoutEnforced === true
|
|
55
|
+
|| existsSync(join(dir, `${jobId}.enforced`));
|
|
56
|
+
const deadlinePassed = enforced
|
|
57
|
+
&& Number.isFinite(tmo) && tmo > 0
|
|
58
|
+
&& (Date.now() - st.mtimeMs) > tmo + ORPHAN_DEADLINE_GRACE_MS;
|
|
59
|
+
if (!deadlinePassed) {
|
|
60
|
+
const pid = Number(detail?.pid);
|
|
61
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
62
|
+
try { process.kill(pid, 0); return; } // alive (or EPERM → treated dead only via ESRCH below)
|
|
63
|
+
catch (e) { if (e?.code !== 'ESRCH') return; }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
expired.push(jobId);
|
|
67
|
+
} catch {}
|
|
68
|
+
}));
|
|
69
|
+
// Owner sidecar markers carry a dynamic `.owner-<pid>` suffix, so they
|
|
70
|
+
// can't sit in the fixed-extension list — map each expired jobId to the
|
|
71
|
+
// marker name(s) actually present in this listing and unlink those too.
|
|
72
|
+
const expiredSet = new Set(expired);
|
|
73
|
+
const ownerMarkers = names.filter((n) => {
|
|
74
|
+
const i = n.lastIndexOf('.owner-');
|
|
75
|
+
return i > 0 && expiredSet.has(n.slice(0, i));
|
|
76
|
+
});
|
|
77
|
+
await Promise.all([
|
|
78
|
+
...expired.flatMap((jobId) =>
|
|
79
|
+
['.json', '.done', '.exit', '.enforced', '.exit.cmd.sh', '.exit.cmd.ps1', '.stdout.log', '.stderr.log'].map((ext) =>
|
|
80
|
+
fsPromises.unlink(join(dir, jobId + ext)).catch(() => {}),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
...ownerMarkers.map((n) => fsPromises.unlink(join(dir, n)).catch(() => {})),
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getShellJobsDir() {
|
|
88
|
+
const dir = join(getPluginData(), 'shell-jobs');
|
|
89
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
90
|
+
sweepStaleShellJobs(dir);
|
|
91
|
+
return dir;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function shellJobDetailPath(jobId) { return join(getShellJobsDir(), `${jobId}.json`); }
|
|
95
|
+
export function shellJobStdoutPath(jobId) { return join(getShellJobsDir(), `${jobId}.stdout.log`); }
|
|
96
|
+
export function shellJobStderrPath(jobId) { return join(getShellJobsDir(), `${jobId}.stderr.log`); }
|
|
97
|
+
export function shellJobExitPath(jobId) { return join(getShellJobsDir(), `${jobId}.exit`); }
|
|
98
|
+
export function shellJobDonePath(jobId) { return join(getShellJobsDir(), `${jobId}.done`); }
|
|
99
|
+
// Runtime proof that the posix wrapper's `timeout` branch actually ran: the
|
|
100
|
+
// wrapper touches this marker immediately before exec'ing `timeout`, so its
|
|
101
|
+
// existence is never optimistic (no spawn-time probe can guarantee the
|
|
102
|
+
// wrapper's own env/cwd resolution). PS jobs don't need it — their wrapper
|
|
103
|
+
// enforces unconditionally and records detail.timeoutEnforced:true.
|
|
104
|
+
export function shellJobEnforcedPath(jobId) { return join(getShellJobsDir(), `${jobId}.enforced`); }
|
|
105
|
+
// Owner sidecar marker: a zero-byte file whose NAME encodes the owning CC host
|
|
106
|
+
// (claude.exe) pid — `<jobId>.owner-<pid>`. It lets the statusline owner-filter
|
|
107
|
+
// jobs from a SINGLE directory listing (no per-job JSON read) so the filter can
|
|
108
|
+
// precede its per-tick scan cap — otherwise other sessions' newer jobs evict
|
|
109
|
+
// this session's live jobs before filtering. Swept alongside the other
|
|
110
|
+
// artefacts.
|
|
111
|
+
export function shellJobOwnerPath(jobId, pid) { return join(getShellJobsDir(), `${jobId}.owner-${pid}`); }
|
|
112
|
+
|
|
113
|
+
// Resolve the CLI host pid that owns a freshly-spawned job. Standalone CLI
|
|
114
|
+
// execution is process-owned: background tasks are scoped to this CLI lifetime,
|
|
115
|
+
// and the pid stamp is only for statusline filtering / legacy sidecars.
|
|
116
|
+
export function resolveJobOwnerHostPid(clientHostPid) {
|
|
117
|
+
const explicit = Number(clientHostPid);
|
|
118
|
+
if (Number.isInteger(explicit) && explicit > 0) return explicit;
|
|
119
|
+
const envPid = Number(process.env.MIXDOG_OWNER_HOST_PID);
|
|
120
|
+
if (Number.isInteger(envPid) && envPid > 0) return envPid;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function writeShellJobDetail(detail) {
|
|
125
|
+
// Session scope stamp: every job record is tagged with the CC host pid
|
|
126
|
+
// that owns it (the claude.exe pid). This is the SAME physical pid the
|
|
127
|
+
// statusline shim passes as --client-host-pid, so the statusline can count
|
|
128
|
+
// only its own session's jobs by exact pid equality (no heuristic). The
|
|
129
|
+
// spawn sites set detail.ownerHostPid from the per-request threaded
|
|
130
|
+
// clientHostPid (resolveJobOwnerHostPid); this fallback only stamps the
|
|
131
|
+
// single-client env value when no spawn-site pid was set, and NEVER
|
|
132
|
+
// overwrites an existing stamp — so the field round-trips through disk on
|
|
133
|
+
// refresh/kill and a correct per-terminal stamp is preserved across rewrites.
|
|
134
|
+
if (detail && detail.ownerHostPid == null) {
|
|
135
|
+
const hostPid = Number(process.env.MIXDOG_OWNER_HOST_PID);
|
|
136
|
+
if (Number.isInteger(hostPid) && hostPid > 0) detail.ownerHostPid = hostPid;
|
|
137
|
+
}
|
|
138
|
+
writeFileSync(shellJobDetailPath(detail.jobId), JSON.stringify(detail, null, 2), 'utf-8');
|
|
139
|
+
// Owner sidecar: encode the resolved owner pid in the marker filename so the
|
|
140
|
+
// statusline can owner-filter from the directory listing alone (before its
|
|
141
|
+
// scan cap). Idempotent zero-byte write — safe to repeat on refresh/kill
|
|
142
|
+
// rewrites. Skipped when no owner is known (legacy/unattributed jobs).
|
|
143
|
+
if (detail && Number.isInteger(detail.ownerHostPid) && detail.ownerHostPid > 0) {
|
|
144
|
+
try { writeFileSync(shellJobOwnerPath(detail.jobId, detail.ownerHostPid), '', 'utf-8'); }
|
|
145
|
+
catch { /* best-effort marker */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function readShellJobDetail(jobId) {
|
|
150
|
+
try {
|
|
151
|
+
const p = shellJobDetailPath(jobId);
|
|
152
|
+
if (!existsSync(p)) return null;
|
|
153
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildJobNotFoundMessage(jobId) {
|
|
160
|
+
return `Error: task not found: ${jobId}`;
|
|
161
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// Process/pid lifecycle helpers for background shell jobs: liveness probing,
|
|
4
|
+
// tree-kill, the module-level live-job pid registry, and the CLI-shutdown exit
|
|
5
|
+
// hook that reaps owned children. Async jobs are intentionally CLI-owned; no
|
|
6
|
+
// restart replay or daemon handoff is attempted.
|
|
7
|
+
|
|
8
|
+
export function isPidAlive(pid) {
|
|
9
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function killProcessTree(pid, signal = 'SIGTERM') {
|
|
19
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
|
|
23
|
+
} else {
|
|
24
|
+
try { process.kill(-pid, signal); }
|
|
25
|
+
catch { process.kill(pid, signal); }
|
|
26
|
+
// SIGKILL escalation: a background child that ignores SIGTERM must
|
|
27
|
+
// not survive (foreground treeKill / persistent _killProcessTree
|
|
28
|
+
// already do this). After a 3s grace, force-kill the group/pid.
|
|
29
|
+
// unref so this backstop never holds the host process open.
|
|
30
|
+
if (signal === 'SIGTERM') {
|
|
31
|
+
const t = setTimeout(() => {
|
|
32
|
+
try { process.kill(-pid, 'SIGKILL'); }
|
|
33
|
+
catch { try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ } }
|
|
34
|
+
}, 3000);
|
|
35
|
+
if (t.unref) t.unref();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Module-level tracking of live background-job pids so CLI shutdown can reap
|
|
45
|
+
// owned children. Async jobs are intentionally CLI-owned; no restart replay or
|
|
46
|
+
// daemon handoff is attempted.
|
|
47
|
+
export const _liveJobPids = new Set();
|
|
48
|
+
export const _liveJobIdsByPid = new Map();
|
|
49
|
+
let _shellJobsExitHookInstalled = false;
|
|
50
|
+
export function _registerLiveJobPid(pid, jobId = null) {
|
|
51
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
52
|
+
_liveJobPids.add(pid);
|
|
53
|
+
if (jobId) _liveJobIdsByPid.set(pid, jobId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function _unregisterLiveJobPid(pid) {
|
|
57
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
58
|
+
_liveJobPids.delete(pid);
|
|
59
|
+
_liveJobIdsByPid.delete(pid);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function _killLiveJobPid(pid, { sync = false } = {}) {
|
|
63
|
+
try {
|
|
64
|
+
if (process.platform === 'win32') {
|
|
65
|
+
if (sync) {
|
|
66
|
+
spawnSync('taskkill.exe', ['/pid', String(pid), '/t', '/f'], {
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
timeout: 1500,
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
try { process.kill(-pid, 'SIGKILL'); }
|
|
76
|
+
catch { try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ } }
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function _sweepLiveJobsSync() {
|
|
84
|
+
for (const pid of _liveJobPids) {
|
|
85
|
+
_killLiveJobPid(pid, { sync: true });
|
|
86
|
+
}
|
|
87
|
+
_liveJobPids.clear();
|
|
88
|
+
_liveJobIdsByPid.clear();
|
|
89
|
+
}
|
|
90
|
+
function _ensureProcessListenerHeadroom(events, extra = 1) {
|
|
91
|
+
try {
|
|
92
|
+
if (typeof process.getMaxListeners !== 'function' || typeof process.setMaxListeners !== 'function') return;
|
|
93
|
+
const current = process.getMaxListeners();
|
|
94
|
+
if (current === 0) return;
|
|
95
|
+
let needed = current;
|
|
96
|
+
for (const event of events) needed = Math.max(needed, process.listenerCount(event) + extra);
|
|
97
|
+
if (needed > current) process.setMaxListeners(needed);
|
|
98
|
+
} catch { /* ignore */ }
|
|
99
|
+
}
|
|
100
|
+
export function _installShellJobsExitHook() {
|
|
101
|
+
if (_shellJobsExitHookInstalled) return;
|
|
102
|
+
_shellJobsExitHookInstalled = true;
|
|
103
|
+
_ensureProcessListenerHeadroom(['exit', 'SIGTERM', 'SIGINT', 'SIGHUP'], 1);
|
|
104
|
+
try { process.on('exit', _sweepLiveJobsSync); } catch { /* ignore */ }
|
|
105
|
+
try { process.on('SIGTERM', _sweepLiveJobsSync); } catch { /* ignore */ }
|
|
106
|
+
try { process.on('SIGINT', _sweepLiveJobsSync); } catch { /* ignore */ }
|
|
107
|
+
try { process.on('SIGHUP', _sweepLiveJobsSync); } catch { /* ignore */ }
|
|
108
|
+
}
|