mixdog 0.9.95 → 0.9.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { makeAgentDispatch, resolveMaintenanceRoute } from '../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
|
|
5
5
|
import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
|
|
@@ -18,12 +18,16 @@ export const EXPLORE_TOOL = {
|
|
|
18
18
|
name: 'explore',
|
|
19
19
|
title: 'Explore',
|
|
20
20
|
annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
21
|
-
description: 'Repo- or machine-wide locator
|
|
21
|
+
description: 'Repo- or machine-wide coordinate locator.',
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: 'object',
|
|
24
24
|
properties: {
|
|
25
25
|
query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Narrow locator query; array = independent facets fanned out in parallel.' },
|
|
26
26
|
cwd: { type: 'string', description: 'Project/root directory.' },
|
|
27
|
+
roots: {
|
|
28
|
+
anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }],
|
|
29
|
+
description: 'Search root(s); defaults to cwd. Use multiple roots for cross-project or machine lookup.',
|
|
30
|
+
},
|
|
27
31
|
},
|
|
28
32
|
required: [],
|
|
29
33
|
additionalProperties: false,
|
|
@@ -35,8 +39,10 @@ export const EXPLORE_TOOL = {
|
|
|
35
39
|
// the merged text in-turn, so we keep a sane bound to protect the Lead context.
|
|
36
40
|
const EXPLORE_OUTPUT_CHAR_CAP = 24_000;
|
|
37
41
|
const EXPLORE_TRUNCATION_MARKER = '\n\n[explore: output capped; remainder omitted]';
|
|
38
|
-
//
|
|
39
|
-
|
|
42
|
+
// Compatibility exports: fan-out is intentionally unbounded at the tool layer.
|
|
43
|
+
// Every requested facet/root starts immediately; output remains context-capped.
|
|
44
|
+
export const MAX_FANOUT_QUERIES = Infinity;
|
|
45
|
+
export const MAX_EXPLORE_ROOTS = Infinity;
|
|
40
46
|
// Mechanical turn cap per explorer sub-session: 3 free turns (contract: tool
|
|
41
47
|
// turn 1 = whole batched search, turns 2-3 = miss recovery), then the agent
|
|
42
48
|
// loop forces one tool-less final-answer turn. Overridable for tuning.
|
|
@@ -61,18 +67,6 @@ export const EXPLORE_COMPUTE_HARD_TIMEOUT_MS = (() => {
|
|
|
61
67
|
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 60_000;
|
|
62
68
|
})();
|
|
63
69
|
const EXPLORE_RESULT_CACHE = new Map(); // key -> { ts, value?, promise? }
|
|
64
|
-
// Fan-out launch stagger: when >1 query, dispatch the first sub-session
|
|
65
|
-
// immediately and delay the rest by this many ms so they can reuse the first
|
|
66
|
-
// sub's provider prompt-cache write instead of racing cold. Default 0 (off):
|
|
67
|
-
// live A/B (2026-07-07) showed 800ms produced zero cross-sub iter1 cache
|
|
68
|
-
// reads (the first sub's write lands only after its iter1 completes, ~2.5s+)
|
|
69
|
-
// while costing every later sub the full delay. Cross-BATCH reuse works
|
|
70
|
-
// without stagger now that iter1 writes the prefix breakpoint. Env knob kept
|
|
71
|
-
// for tuning experiments.
|
|
72
|
-
const EXPLORE_FANOUT_STAGGER_MS = (() => {
|
|
73
|
-
const raw = Number(process.env.MIXDOG_EXPLORE_FANOUT_STAGGER_MS);
|
|
74
|
-
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 0;
|
|
75
|
-
})();
|
|
76
70
|
|
|
77
71
|
// Build a promise that rejects the MOMENT `signal` aborts (immediately if it is
|
|
78
72
|
// already aborted), plus a `cancel` to detach the listener once the caller has
|
|
@@ -102,12 +96,47 @@ function abortRejectionPromise(signal) {
|
|
|
102
96
|
function armExploreHardTimeout(controller, timeoutMs = EXPLORE_COMPUTE_HARD_TIMEOUT_MS) {
|
|
103
97
|
if (!(timeoutMs > 0)) return () => {};
|
|
104
98
|
const timer = setTimeout(() => {
|
|
105
|
-
|
|
99
|
+
// The deadline is a HAND-OFF point, not a discard: `salvagePartial` tells
|
|
100
|
+
// agent-dispatch to return the assistant text the explorer already produced
|
|
101
|
+
// (usually the anchors) instead of throwing the whole run away.
|
|
102
|
+
const err = new Error(`explorer timed out after ${timeoutMs}ms`);
|
|
103
|
+
err.salvagePartial = true;
|
|
104
|
+
err.exploreHardTimeout = true;
|
|
105
|
+
try { controller.abort(err); } catch { /* ignore */ }
|
|
106
106
|
}, timeoutMs);
|
|
107
107
|
if (typeof timer.unref === 'function') timer.unref();
|
|
108
108
|
return () => clearTimeout(timer);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Bounded window granted to a hard-timed-out compute to settle with its
|
|
112
|
+
// salvaged partial answer before the timeout error is surfaced. Without it the
|
|
113
|
+
// abort rejection wins the race instantly and the salvage never arrives.
|
|
114
|
+
// 0 disables salvage entirely (pre-existing behaviour).
|
|
115
|
+
const EXPLORE_TIMEOUT_SALVAGE_GRACE_MS = (() => {
|
|
116
|
+
const raw = Number(process.env.MIXDOG_EXPLORE_SALVAGE_GRACE_MS);
|
|
117
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 2000;
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
// Race the compute against its abort, EXCEPT for an opted-in salvage abort
|
|
121
|
+
// (the wall-clock hard timeout): there, wait up to the grace window for the
|
|
122
|
+
// compute to settle with the partial answer and return that. A caller
|
|
123
|
+
// cancellation (no salvagePartial) still rejects immediately, so ESC keeps
|
|
124
|
+
// releasing the tool call at once.
|
|
125
|
+
function settleWithSalvageGrace(computePromise, abortedPromise) {
|
|
126
|
+
const salvaged = abortedPromise.catch(async (err) => {
|
|
127
|
+
if (!(err && err.salvagePartial === true) || !(EXPLORE_TIMEOUT_SALVAGE_GRACE_MS > 0)) throw err;
|
|
128
|
+
const EXPIRED = Symbol('explore-salvage-grace');
|
|
129
|
+
const grace = new Promise((resolve) => {
|
|
130
|
+
const t = setTimeout(() => resolve(EXPIRED), EXPLORE_TIMEOUT_SALVAGE_GRACE_MS);
|
|
131
|
+
if (typeof t.unref === 'function') t.unref();
|
|
132
|
+
});
|
|
133
|
+
const settled = await Promise.race([computePromise.catch(() => EXPIRED), grace]);
|
|
134
|
+
if (settled === EXPIRED) throw err;
|
|
135
|
+
return settled;
|
|
136
|
+
});
|
|
137
|
+
return Promise.race([computePromise, salvaged]);
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
// Cascade a parent AbortSignal into a compute controller (immediately if the
|
|
112
141
|
// parent is already aborted). Returns a detach fn for the listener, or null.
|
|
113
142
|
function linkParentAbort(parentSignal, controller) {
|
|
@@ -157,7 +186,7 @@ export function runExploreComputeWithAbort(compute, parentSignal = null, timeout
|
|
|
157
186
|
const disarm = armExploreHardTimeout(controller, timeoutMs);
|
|
158
187
|
const { promise: aborted } = abortRejectionPromise(controller.signal);
|
|
159
188
|
const computePromise = Promise.resolve().then(() => compute(controller.signal));
|
|
160
|
-
return
|
|
189
|
+
return settleWithSalvageGrace(computePromise, aborted).finally(() => {
|
|
161
190
|
disarm();
|
|
162
191
|
if (detachParent) detachParent();
|
|
163
192
|
});
|
|
@@ -169,8 +198,11 @@ function escapeXml(str) {
|
|
|
169
198
|
|
|
170
199
|
// Each explorer receives only its own query. The full routing and behavioral
|
|
171
200
|
// contract lives at system level (rules/agent/30-explorer.md).
|
|
172
|
-
export function buildExplorerPrompt(query) {
|
|
173
|
-
|
|
201
|
+
export function buildExplorerPrompt(query, roots = []) {
|
|
202
|
+
const queryXml = `<query>${escapeXml(query)}</query>`;
|
|
203
|
+
if (!Array.isArray(roots) || roots.length === 0) return queryXml;
|
|
204
|
+
const rootsXml = roots.map((root) => ` <root>${escapeXml(root)}</root>`).join('\n');
|
|
205
|
+
return `${queryXml}\n<roots>\n${rootsXml}\n</roots>`;
|
|
174
206
|
}
|
|
175
207
|
export function normalizeExploreQueries(rawQuery) {
|
|
176
208
|
let raw = rawQuery;
|
|
@@ -190,6 +222,40 @@ export function normalizeExploreQueries(rawQuery) {
|
|
|
190
222
|
.filter(Boolean);
|
|
191
223
|
}
|
|
192
224
|
|
|
225
|
+
export function normalizeExploreRoots(rawRoots, cwd) {
|
|
226
|
+
if (rawRoots === undefined || rawRoots === null || rawRoots === '') return [];
|
|
227
|
+
let raw = rawRoots;
|
|
228
|
+
if (typeof raw === 'string') {
|
|
229
|
+
const trimmed = raw.trim();
|
|
230
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(trimmed);
|
|
233
|
+
if (Array.isArray(parsed)) raw = parsed;
|
|
234
|
+
} catch { /* keep as one root */ }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const roots = Array.isArray(raw) ? raw : [raw];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const value of roots) {
|
|
241
|
+
if (typeof value !== 'string' || !value.trim()) continue;
|
|
242
|
+
const trimmed = value.trim();
|
|
243
|
+
const expanded = trimmed.startsWith('~') ? trimmed.replace(/^~/, homedir()) : trimmed;
|
|
244
|
+
const absolute = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
245
|
+
try {
|
|
246
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) continue;
|
|
247
|
+
} catch {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const key = process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
251
|
+
if (seen.has(key)) continue;
|
|
252
|
+
seen.add(key);
|
|
253
|
+
out.push(absolute);
|
|
254
|
+
if (out.length >= MAX_EXPLORE_ROOTS) break;
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
193
259
|
function resolveExploreCwd(input, callerCwd) {
|
|
194
260
|
const base = (typeof callerCwd === 'string' && callerCwd) ? callerCwd : process.cwd();
|
|
195
261
|
if (typeof input === 'string' && input.trim()) {
|
|
@@ -200,18 +266,21 @@ function resolveExploreCwd(input, callerCwd) {
|
|
|
200
266
|
return base;
|
|
201
267
|
}
|
|
202
268
|
|
|
203
|
-
function settledExplorerResult(result, cwd = null) {
|
|
269
|
+
export function settledExplorerResult(result, cwd = null, roots = []) {
|
|
204
270
|
if (result?.status !== 'fulfilled') {
|
|
205
271
|
const message = result?.reason?.message || String(result?.reason);
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
272
|
+
// Tagged failure classes: the caller must be able to tell a wedged
|
|
273
|
+
// compute (timeout) from a cancellation or a dispatch/provider fault
|
|
274
|
+
// without parsing free-form provider text.
|
|
275
|
+
const tag = /timed out after \d+ms/i.test(message)
|
|
276
|
+
? 'timeout'
|
|
277
|
+
: /abort|cancel/i.test(message)
|
|
278
|
+
? 'canceled'
|
|
279
|
+
: 'dispatch-failed';
|
|
280
|
+
return { ok: false, text: `[explorer error] [${tag}] ${message}` };
|
|
212
281
|
}
|
|
213
|
-
const text = cleanExplorerText(typeof result.value === 'string' ? result.value : responseText(result.value), cwd);
|
|
214
|
-
if (!text) return { ok: false, text: '[explorer error] empty
|
|
282
|
+
const text = cleanExplorerText(typeof result.value === 'string' ? result.value : responseText(result.value), cwd, roots);
|
|
283
|
+
if (!text) return { ok: false, text: '[explorer error] [empty-response] explorer returned no text' };
|
|
215
284
|
return { ok: true, text };
|
|
216
285
|
}
|
|
217
286
|
|
|
@@ -232,10 +301,10 @@ function fatalExploreError(settled) {
|
|
|
232
301
|
return (settled || []).find((r) => r?.status === 'rejected' && isFatalExploreError(r.reason))?.reason || null;
|
|
233
302
|
}
|
|
234
303
|
|
|
235
|
-
function mergeSettled(settled, queries, cwd = null) {
|
|
304
|
+
function mergeSettled(settled, queries, cwd = null, roots = []) {
|
|
236
305
|
const single = queries.length === 1;
|
|
237
306
|
if (single) {
|
|
238
|
-
const { text: body } = settledExplorerResult(settled[0], cwd);
|
|
307
|
+
const { text: body } = settledExplorerResult(settled[0], cwd, roots);
|
|
239
308
|
return body.length > EXPLORE_OUTPUT_CHAR_CAP
|
|
240
309
|
? body.slice(0, EXPLORE_OUTPUT_CHAR_CAP) + EXPLORE_TRUNCATION_MARKER
|
|
241
310
|
: body;
|
|
@@ -246,7 +315,7 @@ function mergeSettled(settled, queries, cwd = null) {
|
|
|
246
315
|
let truncated = false;
|
|
247
316
|
for (let i = 0; i < settled.length; i++) {
|
|
248
317
|
const header = `Q${i + 1}: ${String(queries[i] ?? '').replace(/\s+/g, ' ').slice(0, 60)}`;
|
|
249
|
-
const { text: body } = settledExplorerResult(settled[i], cwd);
|
|
318
|
+
const { text: body } = settledExplorerResult(settled[i], cwd, roots);
|
|
250
319
|
const piece = `${header}\n${body}`;
|
|
251
320
|
const addLen = (parts.length === 0 ? 0 : sep.length) + piece.length;
|
|
252
321
|
if (total + addLen > EXPLORE_OUTPUT_CHAR_CAP) {
|
|
@@ -279,9 +348,10 @@ function exploreResultCacheEnabled() {
|
|
|
279
348
|
&& !/^(?:0|false|off|no)$/i.test(String(process.env.MIXDOG_EXPLORE_RESULT_CACHE || '1'));
|
|
280
349
|
}
|
|
281
350
|
|
|
282
|
-
export function exploreResultCacheKey({ cwd, route, query }) {
|
|
351
|
+
export function exploreResultCacheKey({ cwd, roots = [], route, query }) {
|
|
283
352
|
return JSON.stringify({
|
|
284
353
|
cwd: String(cwd || ''),
|
|
354
|
+
roots: Array.isArray(roots) ? roots.map((root) => String(root)) : [],
|
|
285
355
|
provider: clean(route?.provider),
|
|
286
356
|
model: clean(route?.model),
|
|
287
357
|
effort: clean(route?.effort),
|
|
@@ -388,7 +458,7 @@ function startSharedCompute(key, compute, timeoutMs = EXPLORE_COMPUTE_HARD_TIMEO
|
|
|
388
458
|
const disarm = armExploreHardTimeout(controller, timeoutMs);
|
|
389
459
|
const { promise: aborted } = abortRejectionPromise(controller.signal);
|
|
390
460
|
const computePromise = Promise.resolve().then(() => compute(controller.signal));
|
|
391
|
-
entry.promise =
|
|
461
|
+
entry.promise = settleWithSalvageGrace(computePromise, aborted)
|
|
392
462
|
.then((value) => {
|
|
393
463
|
// Identity-guard EVERY eventual write: only cache/purge when THIS entry is
|
|
394
464
|
// still the live one. A hard-timeout TTL eviction (runExploreCached) may
|
|
@@ -397,7 +467,7 @@ function startSharedCompute(key, compute, timeoutMs = EXPLORE_COMPUTE_HARD_TIMEO
|
|
|
397
467
|
if (EXPLORE_RESULT_CACHE.get(key) !== entry) return value;
|
|
398
468
|
const text = typeof value === 'string' ? value : responseText(value);
|
|
399
469
|
const cleaned = cleanExplorerText(text);
|
|
400
|
-
if (cleaned && cleaned
|
|
470
|
+
if (cleaned && !isExplorationFailure(cleaned)) {
|
|
401
471
|
EXPLORE_RESULT_CACHE.set(key, { ts: Date.now(), value: text });
|
|
402
472
|
trimExploreResultCache();
|
|
403
473
|
} else {
|
|
@@ -508,6 +578,34 @@ const TURN_COUNTER_LINE_RE = /^turn\s+\d+\s*\/\s*\d+\b/i;
|
|
|
508
578
|
const EXPLORE_MAX_ANCHOR_LINES = 3;
|
|
509
579
|
const FAILED_RE = /\b(?:EXPLORATION_FAILED|exploration failed|no credible (?:anchor|location)|no relevant (?:anchor|location)|not found|could(?: not|n't) find)\b/i;
|
|
510
580
|
|
|
581
|
+
// Failure taxonomy: every miss carries a machine-stable tag plus the next
|
|
582
|
+
// move, so a caller can tell "the explorer gave up" from "the post-filter
|
|
583
|
+
// dropped an unverifiable answer" instead of reading one bare token.
|
|
584
|
+
const EXPLORE_FAIL_HINT = {
|
|
585
|
+
reported: 'explorer spent its budget with zero anchors; retry once with changed tokens or an explicit roots[]',
|
|
586
|
+
'no-anchor': 'explorer answered without a path:line anchor; retry once with different code tokens',
|
|
587
|
+
'unverified-anchors': 'every cited path is missing on disk; retry with changed tokens or pass the real root via roots[]',
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
export function isExplorationFailure(text) {
|
|
591
|
+
return /^EXPLORATION_FAILED\b/i.test(String(text || '').trimStart());
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function explorationFailure(tag, detail = '') {
|
|
595
|
+
const tail = [detail, EXPLORE_FAIL_HINT[tag] || ''].filter(Boolean).join(' — ');
|
|
596
|
+
return tail ? `EXPLORATION_FAILED [${tag}] — ${tail}` : `EXPLORATION_FAILED [${tag}]`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Filter-drop trace: when the post-filter turns a NON-empty explorer answer
|
|
600
|
+
// into a failure, the raw text is the only evidence of why it was dropped.
|
|
601
|
+
// Gated behind MIXDOG_EXPLORE_DEBUG so normal runs stay silent.
|
|
602
|
+
function traceExploreDrop(tag, raw) {
|
|
603
|
+
if (!/^(?:1|true|on|yes)$/i.test(String(process.env.MIXDOG_EXPLORE_DEBUG || ''))) return;
|
|
604
|
+
try {
|
|
605
|
+
process.stderr.write(`[explore] filter drop (${tag}): ${String(raw || '').replace(/\s+/g, ' ').slice(0, 800)}\n`);
|
|
606
|
+
} catch { /* best-effort */ }
|
|
607
|
+
}
|
|
608
|
+
|
|
511
609
|
function normalizeExplorerLine(line) {
|
|
512
610
|
return String(line || '')
|
|
513
611
|
.trim()
|
|
@@ -521,14 +619,18 @@ function normalizeExplorerLine(line) {
|
|
|
521
619
|
|
|
522
620
|
// Anchor existence filter: a locator answer citing a file that does not exist
|
|
523
621
|
// is a hallucination — drop the line. Only enforced when cwd is known.
|
|
524
|
-
function anchorLineExists(line, cwd, captureRe = ANCHOR_PATH_CAPTURE_RE) {
|
|
622
|
+
function anchorLineExists(line, cwd, roots = [], captureRe = ANCHOR_PATH_CAPTURE_RE) {
|
|
525
623
|
const m = line.match(captureRe);
|
|
526
624
|
if (!m) return true;
|
|
527
625
|
const p = m[1].trim();
|
|
528
|
-
try {
|
|
626
|
+
try {
|
|
627
|
+
if (isAbsolute(p)) return existsSync(p);
|
|
628
|
+
const bases = [cwd, ...(Array.isArray(roots) ? roots : [])].filter(Boolean);
|
|
629
|
+
return bases.some((base) => existsSync(resolve(base, p)));
|
|
630
|
+
} catch { return true; }
|
|
529
631
|
}
|
|
530
632
|
|
|
531
|
-
function cleanExplorerText(text, cwd = null) {
|
|
633
|
+
function cleanExplorerText(text, cwd = null, roots = []) {
|
|
532
634
|
const raw = String(text || '').trim();
|
|
533
635
|
if (!raw) return '';
|
|
534
636
|
const anchors = [];
|
|
@@ -538,7 +640,11 @@ function cleanExplorerText(text, cwd = null) {
|
|
|
538
640
|
for (const sourceLine of raw.split(/\r?\n/)) {
|
|
539
641
|
const line = normalizeExplorerLine(sourceLine);
|
|
540
642
|
if (!line || /^-{3,}$/.test(line) || /^#+\s+/.test(line) || TURN_COUNTER_LINE_RE.test(line)) continue;
|
|
541
|
-
|
|
643
|
+
// Already-tagged failure (explorer prompt or the loop's iteration-cap
|
|
644
|
+
// synthesis) keeps its own tag; an untagged one becomes [reported].
|
|
645
|
+
if (isExplorationFailure(line)) {
|
|
646
|
+
return /^EXPLORATION_FAILED\s*\[[a-z-]+\]/i.test(line) ? line : explorationFailure('reported');
|
|
647
|
+
}
|
|
542
648
|
const coord = line.match(ANCHOR_COORD_CAPTURE_RE)?.[1]
|
|
543
649
|
?? line.match(ANCHOR_PATHONLY_CAPTURE_RE)?.[1];
|
|
544
650
|
const key = (coord ?? line).toLowerCase().replace(/\\/g, '/');
|
|
@@ -549,19 +655,31 @@ function cleanExplorerText(text, cwd = null) {
|
|
|
549
655
|
else passthrough.push(sourceLine);
|
|
550
656
|
}
|
|
551
657
|
if (anchors.length) {
|
|
552
|
-
const real = (cwd ? anchors.filter((l) => anchorLineExists(l, cwd)) : anchors)
|
|
658
|
+
const real = (cwd ? anchors.filter((l) => anchorLineExists(l, cwd, roots)) : anchors)
|
|
553
659
|
.slice(0, EXPLORE_MAX_ANCHOR_LINES);
|
|
554
660
|
// All anchors pointed at nonexistent paths: fail harmlessly instead of
|
|
555
661
|
// forwarding hallucinated locations.
|
|
556
|
-
|
|
662
|
+
if (real.length) return real.join('\n');
|
|
663
|
+
traceExploreDrop('unverified-anchors', raw);
|
|
664
|
+
return explorationFailure(
|
|
665
|
+
'unverified-anchors',
|
|
666
|
+
`dropped ${anchors.length} unverified anchor(s): ${anchors.slice(0, EXPLORE_MAX_ANCHOR_LINES).map((l) => l.match(ANCHOR_COORD_CAPTURE_RE)?.[1] || l).join(', ')}`,
|
|
667
|
+
);
|
|
557
668
|
}
|
|
558
669
|
if (pathOnly.length) {
|
|
559
|
-
const real = (cwd ? pathOnly.filter((l) => anchorLineExists(l, cwd, ANCHOR_PATHONLY_CAPTURE_RE)) : pathOnly)
|
|
670
|
+
const real = (cwd ? pathOnly.filter((l) => anchorLineExists(l, cwd, roots, ANCHOR_PATHONLY_CAPTURE_RE)) : pathOnly)
|
|
560
671
|
.slice(0, EXPLORE_MAX_ANCHOR_LINES);
|
|
561
672
|
if (real.length) return real.join('\n');
|
|
562
|
-
|
|
673
|
+
traceExploreDrop('unverified-anchors', raw);
|
|
674
|
+
return explorationFailure(
|
|
675
|
+
'unverified-anchors',
|
|
676
|
+
`dropped ${pathOnly.length} unverified path(s): ${pathOnly.slice(0, EXPLORE_MAX_ANCHOR_LINES).map((l) => l.match(ANCHOR_PATHONLY_CAPTURE_RE)?.[1] || l).join(', ')}`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (FAILED_RE.test(raw)) {
|
|
680
|
+
traceExploreDrop('no-anchor', raw);
|
|
681
|
+
return explorationFailure('no-anchor');
|
|
563
682
|
}
|
|
564
|
-
if (FAILED_RE.test(raw)) return 'EXPLORATION_FAILED';
|
|
565
683
|
return passthrough.join('\n').trim() || raw;
|
|
566
684
|
}
|
|
567
685
|
|
|
@@ -575,7 +693,7 @@ function cleanExplorerText(text, cwd = null) {
|
|
|
575
693
|
*
|
|
576
694
|
* Runs synchronously and returns the bounded locator result in this tool call.
|
|
577
695
|
*
|
|
578
|
-
* @param {object} args — tool args ({ query, cwd }).
|
|
696
|
+
* @param {object} args — tool args ({ query, cwd, roots }).
|
|
579
697
|
* @param {object} ctx — { callerCwd, callerSessionId }.
|
|
580
698
|
*/
|
|
581
699
|
export async function runExplore(args = {}, ctx = {}) {
|
|
@@ -593,16 +711,26 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
593
711
|
return fail('explore canceled before dispatch');
|
|
594
712
|
}
|
|
595
713
|
|
|
596
|
-
|
|
597
|
-
let capNotice = '';
|
|
598
|
-
if (working.length > MAX_FANOUT_QUERIES) {
|
|
599
|
-
capNotice = `[capped ${working.length}->${MAX_FANOUT_QUERIES} queries]\n`;
|
|
600
|
-
working = working.slice(0, MAX_FANOUT_QUERIES);
|
|
601
|
-
}
|
|
714
|
+
const working = queries;
|
|
602
715
|
|
|
603
716
|
const resolvedCwd = resolveExploreCwd(args.cwd, ctx.callerCwd);
|
|
604
|
-
|
|
605
|
-
|
|
717
|
+
const explicitRoots = normalizeExploreRoots(args.roots, resolvedCwd);
|
|
718
|
+
const rootsWereRequested = args.roots !== undefined && args.roots !== null && args.roots !== '';
|
|
719
|
+
if (rootsWereRequested && explicitRoots.length === 0) {
|
|
720
|
+
return fail('[explorer error] no valid search roots');
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
if (!existsSync(resolvedCwd) || !statSync(resolvedCwd).isDirectory()) {
|
|
724
|
+
return fail(`[explorer error] search cwd is not a directory: ${resolvedCwd}`);
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
return fail(`[explorer error] search cwd is not a directory: ${resolvedCwd}`);
|
|
728
|
+
}
|
|
729
|
+
const scopeRoots = explicitRoots.length ? explicitRoots : [resolvedCwd];
|
|
730
|
+
if (explicitRoots.length === 0) {
|
|
731
|
+
scheduleExploreCodeGraphPrewarm(resolvedCwd);
|
|
732
|
+
scheduleExploreFindPrewarm(resolvedCwd);
|
|
733
|
+
}
|
|
606
734
|
const config = loadConfig();
|
|
607
735
|
const route = resolveExploreRoute(config);
|
|
608
736
|
const parentSignal = ctx.signal instanceof AbortSignal ? ctx.signal : null;
|
|
@@ -625,38 +753,27 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
625
753
|
maxLoopIterations: EXPLORE_MAX_LOOP_ITERATIONS,
|
|
626
754
|
});
|
|
627
755
|
|
|
628
|
-
// Stagger cold sub-session launches so later subs reuse the first sub's
|
|
629
|
-
// provider prompt-cache write. Index 0 fires immediately; a cached-result
|
|
630
|
-
// query resolves without delay regardless of index.
|
|
631
|
-
const stagger = working.length > 1 ? EXPLORE_FANOUT_STAGGER_MS : 0;
|
|
632
756
|
// Caller cancellation (ESC / owner-session abort) cascades into every child
|
|
633
757
|
// dispatch: the per-compute AbortController links this signal (and the hard
|
|
634
758
|
// timeout) so aborting the explore tool call tears down all fan-out subs and
|
|
635
759
|
// their provider calls at once.
|
|
636
|
-
const settled = await Promise.allSettled(working.map((q
|
|
637
|
-
const key = exploreResultCacheKey({ cwd: resolvedCwd, route, query: q });
|
|
760
|
+
const settled = await Promise.allSettled(working.map((q) => {
|
|
761
|
+
const key = exploreResultCacheKey({ cwd: resolvedCwd, roots: scopeRoots, route, query: q });
|
|
638
762
|
return runExploreCached(key, async (computeSignal) => {
|
|
639
763
|
// Provider initialization is part of the timed compute: a wedged init
|
|
640
764
|
// must consume the same 60s wall-clock budget as dispatch, never block
|
|
641
765
|
// runExplore before its hard-timeout AbortController has been armed.
|
|
642
766
|
await ensureExploreProviderReady(config, route, computeSignal);
|
|
643
|
-
|
|
644
|
-
// exploreStaggerDelay rejects and the (now-pointless) child dispatch is
|
|
645
|
-
// never launched.
|
|
646
|
-
const delay = (stagger > 0 && i > 0) ? exploreStaggerDelay(stagger, computeSignal) : null;
|
|
647
|
-
return await (delay
|
|
648
|
-
? delay.then(() => llm({ prompt: buildExplorerPrompt(q), parentSignal: computeSignal }))
|
|
649
|
-
: llm({ prompt: buildExplorerPrompt(q), parentSignal: computeSignal }));
|
|
767
|
+
return await llm({ prompt: buildExplorerPrompt(q, explicitRoots), parentSignal: computeSignal });
|
|
650
768
|
}, parentSignal);
|
|
651
769
|
}));
|
|
652
770
|
|
|
653
771
|
const fatal = fatalExploreError(settled);
|
|
654
772
|
if (fatal) return fail(presentErrorText(fatal, { surface: 'explore' }));
|
|
655
773
|
|
|
656
|
-
const merged = mergeSettled(settled, working, resolvedCwd);
|
|
657
|
-
const allFailed = settled.every((r) => !settledExplorerResult(r, resolvedCwd).ok);
|
|
658
|
-
|
|
659
|
-
return allFailed ? fail(out) : ok(out);
|
|
774
|
+
const merged = mergeSettled(settled, working, resolvedCwd, scopeRoots);
|
|
775
|
+
const allFailed = settled.every((r) => !settledExplorerResult(r, resolvedCwd, scopeRoots).ok);
|
|
776
|
+
return allFailed ? fail(merged) : ok(merged);
|
|
660
777
|
}
|
|
661
778
|
|
|
662
779
|
// Test-only hook: inspect the explore result cache so cancellation/timeout
|