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.
Files changed (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /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 (dot dirs included). query[] fans out up to 8 independent facets in parallel.',
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
- // Bound fan-out so a hostile/poisoned query array cannot spawn unbounded subs.
39
- export const MAX_FANOUT_QUERIES = 8;
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
- try { controller.abort(new Error(`explorer timed out after ${timeoutMs}ms`)); } catch { /* ignore */ }
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 Promise.race([computePromise, aborted]).finally(() => {
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
- return `<query>${escapeXml(query)}</query>`;
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
- // The hard deadline has one caller-visible convention regardless of
207
- // whether it expires during provider warmup or child dispatch.
208
- if (/^explorer timed out after \d+ms$/.test(message)) {
209
- return { ok: true, text: 'EXPLORATION_FAILED' };
210
- }
211
- return { ok: false, text: `[explorer error] ${message}` };
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 response' };
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 = Promise.race([computePromise, aborted])
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 !== 'EXPLORATION_FAILED') {
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 { return existsSync(isAbsolute(p) ? p : resolve(cwd, p)); } catch { return true; }
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
- if (/^EXPLORATION_FAILED\b/i.test(line)) return 'EXPLORATION_FAILED';
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
- return real.length ? real.join('\n') : 'EXPLORATION_FAILED';
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
- return 'EXPLORATION_FAILED';
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
- let working = queries;
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
- scheduleExploreCodeGraphPrewarm(resolvedCwd);
605
- scheduleExploreFindPrewarm(resolvedCwd);
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, i) => {
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
- // Interruptible stagger: if the compute signal aborts during the delay,
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
- const out = capNotice + merged;
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