mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -22,20 +22,15 @@ import {
22
22
  updateSection,
23
23
  } from '../runtime/shared/config.mjs';
24
24
  import { resolvePluginData } from '../runtime/shared/plugin-paths.mjs';
25
- import { readMarkdownDocument } from '../runtime/shared/markdown-frontmatter.mjs';
25
+ import { readMarkdownDocument, serializeFrontmatterDoc } from '../runtime/shared/markdown-frontmatter.mjs';
26
26
 
27
27
  const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
28
28
  const DEFAULT_CHANNELS = Object.freeze({
29
29
  backend: 'discord',
30
30
  discord: {},
31
31
  access: { dmPolicy: 'allowlist', allowFrom: [], channels: {} },
32
- mainChannel: 'main',
33
- channelsConfig: {
34
- main: { channelId: '', mode: 'interactive' },
35
- },
36
- quiet: { schedule: '23:00-09:00', holidays: false },
37
- schedules: { respectQuiet: true },
38
- webhook: { enabled: true, respectQuiet: false, port: 3333 },
32
+ channel: { channelId: '', discordChannelId: '', telegramChatId: '' },
33
+ webhook: { enabled: true, port: 3333 },
39
34
  });
40
35
 
41
36
  function dataDir() {
@@ -61,24 +56,6 @@ function writeTextAtomic(path, text) {
61
56
  renameSync(tmp, path);
62
57
  }
63
58
 
64
- // Serialize a single-file frontmatter markdown document:
65
- // ---
66
- // key: value
67
- // ---
68
- //
69
- // <body>
70
- // Values are stringified as-is (enabled:false -> "false"); the reader casts
71
- // types back. Mirrors the WORKFLOW.md / SKILL.md single-file convention.
72
- function serializeFrontmatterDoc(meta = {}, body = '') {
73
- const lines = ['---'];
74
- for (const [key, value] of Object.entries(meta)) {
75
- if (value === undefined || value === null) continue;
76
- lines.push(`${key}: ${value}`);
77
- }
78
- lines.push('---', '');
79
- return `${lines.join('\n')}\n${String(body || '').trim()}\n`;
80
- }
81
-
82
59
  function assertName(name, kind = 'name') {
83
60
  const value = String(name || '').trim();
84
61
  if (!NAME_RE.test(value) || value !== basename(value)) {
@@ -93,9 +70,7 @@ function normalizeChannelsConfig(raw = {}) {
93
70
  ...(raw && typeof raw === 'object' ? raw : {}),
94
71
  discord: { ...DEFAULT_CHANNELS.discord, ...(raw?.discord || {}) },
95
72
  access: { ...DEFAULT_CHANNELS.access, ...(raw?.access || {}) },
96
- channelsConfig: { ...DEFAULT_CHANNELS.channelsConfig, ...(raw?.channelsConfig || {}) },
97
- quiet: { ...DEFAULT_CHANNELS.quiet, ...(raw?.quiet || {}) },
98
- schedules: { ...DEFAULT_CHANNELS.schedules, ...(raw?.schedules || {}) },
73
+ channel: { ...DEFAULT_CHANNELS.channel, ...(raw?.channel || {}) },
99
74
  webhook: { ...DEFAULT_CHANNELS.webhook, ...(raw?.webhook || {}) },
100
75
  };
101
76
  }
@@ -116,11 +91,42 @@ function seedBackendChannelIds(entry = {}, backend = 'discord') {
116
91
  return next;
117
92
  }
118
93
 
94
+ // Resolve the single-channel entry from the config: prefer the new `channel`
95
+ // object; fall back (read-side only, no on-disk migration) to the legacy
96
+ // channelsConfig[mainChannel ?? 'main'] entry, then the first entry with an id.
97
+ function resolveChannelEntry(cfg = {}) {
98
+ if (cfg.channel && typeof cfg.channel === 'object'
99
+ && (cfg.channel.channelId || cfg.channel.discordChannelId || cfg.channel.telegramChatId)) {
100
+ return cfg.channel;
101
+ }
102
+ const legacy = cfg.channelsConfig && typeof cfg.channelsConfig === 'object' ? cfg.channelsConfig : null;
103
+ if (legacy) {
104
+ const mainName = cfg.mainChannel ?? 'main';
105
+ const preferred = legacy[mainName];
106
+ if (preferred && typeof preferred === 'object'
107
+ && (preferred.channelId || preferred.discordChannelId || preferred.telegramChatId)) {
108
+ return preferred;
109
+ }
110
+ for (const entry of Object.values(legacy)) {
111
+ if (entry && typeof entry === 'object'
112
+ && (entry.channelId || entry.discordChannelId || entry.telegramChatId)) {
113
+ return entry;
114
+ }
115
+ }
116
+ }
117
+ return cfg.channel && typeof cfg.channel === 'object' ? cfg.channel : {};
118
+ }
119
+
119
120
  function updateChannelsSection(build) {
120
121
  let next;
121
122
  updateSection('channels', (current) => {
122
- next = build(normalizeChannelsConfig(current));
123
- return normalizeChannelsConfig(next);
123
+ // Preserve any legacy channelsConfig/mainChannel that already live on disk
124
+ // for read-side compat, but never re-emit them from our writers: strip them
125
+ // out of the returned shape so writes converge on the single `channel`.
126
+ const normalized = normalizeChannelsConfig(current);
127
+ next = build(normalized);
128
+ const { channelsConfig: _lc, mainChannel: _lm, ...clean } = normalizeChannelsConfig(next);
129
+ return clean;
124
130
  });
125
131
  return next;
126
132
  }
@@ -136,19 +142,17 @@ function listEntryDirs(dir) {
136
142
  }
137
143
  }
138
144
 
139
- export function listChannels(config = {}) {
145
+ // Single-channel read: resolves `cfg.channel` (legacy channelsConfig fallback)
146
+ // into the flat shape the settings/TUI layer consumes.
147
+ export function getChannel(config = {}) {
140
148
  const cfg = normalizeChannelsConfig(config);
141
149
  const backend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
142
- return Object.entries(cfg.channelsConfig || {})
143
- .sort(([a], [b]) => a.localeCompare(b))
144
- .map(([name, value]) => ({
145
- name,
146
- channelId: channelIdForBackend(value, backend),
147
- discordChannelId: String(value?.discordChannelId || ''),
148
- telegramChatId: String(value?.telegramChatId || ''),
149
- mode: value?.mode || 'interactive',
150
- main: cfg.mainChannel === name,
151
- }));
150
+ const entry = resolveChannelEntry(cfg);
151
+ return {
152
+ channelId: channelIdForBackend(entry, backend),
153
+ discordChannelId: String(entry?.discordChannelId || ''),
154
+ telegramChatId: String(entry?.telegramChatId || ''),
155
+ };
152
156
  }
153
157
 
154
158
  export function saveDiscordToken(token) {
@@ -187,40 +191,24 @@ export function forgetWebhookAuthtoken() {
187
191
  return { ok: true };
188
192
  }
189
193
 
190
- export function saveChannel({ name, channelId, mode = 'interactive', main = false, backend = null } = {}) {
191
- const label = assertName(name, 'channel name');
194
+ // Single-channel write: persists `cfg.channel`, keeping the per-backend id
195
+ // fields (discordChannelId/telegramChatId) so a backend switch retains both
196
+ // ids. `backend` selects which per-backend field the id updates; when omitted
197
+ // the id lands on the active backend's field.
198
+ export function setChannel({ channelId, backend = null } = {}) {
192
199
  const id = String(channelId || '').trim();
193
200
  if (!id) throw new Error('channelId is required');
194
- const nextMode = mode === 'broadcast' ? 'broadcast' : 'interactive';
195
201
  const targetBackend = backend === 'telegram' || backend === 'discord' ? backend : null;
196
202
  return updateChannelsSection((cfg) => {
197
203
  const activeBackend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
198
- const current = seedBackendChannelIds(cfg.channelsConfig?.[label] || {}, activeBackend);
199
- const nextEntry = { ...current, mode: nextMode };
200
- if (targetBackend === 'telegram') nextEntry.telegramChatId = id;
201
- else if (targetBackend === 'discord') nextEntry.discordChannelId = id;
202
- else nextEntry.channelId = id;
203
- nextEntry.channelId = targetBackend
204
- ? (channelIdForBackend(nextEntry, activeBackend) || (targetBackend === activeBackend ? id : ''))
205
- : id;
206
- return {
207
- ...cfg,
208
- mainChannel: main || !cfg.mainChannel ? label : cfg.mainChannel,
209
- channelsConfig: {
210
- ...(cfg.channelsConfig || {}),
211
- [label]: nextEntry,
212
- },
213
- };
214
- });
215
- }
216
-
217
- export function deleteChannel(name) {
218
- const label = assertName(name, 'channel name');
219
- return updateChannelsSection((cfg) => {
220
- const channelsConfig = { ...(cfg.channelsConfig || {}) };
221
- delete channelsConfig[label];
222
- const nextMain = cfg.mainChannel === label ? Object.keys(channelsConfig)[0] || 'main' : cfg.mainChannel;
223
- return { ...cfg, channelsConfig, mainChannel: nextMain };
204
+ const writeBackend = targetBackend || activeBackend;
205
+ const current = seedBackendChannelIds(resolveChannelEntry(cfg), activeBackend);
206
+ const nextEntry = { ...current };
207
+ if (writeBackend === 'telegram') nextEntry.telegramChatId = id;
208
+ else nextEntry.discordChannelId = id;
209
+ // `channelId` mirrors the active backend's id for legacy readers.
210
+ nextEntry.channelId = channelIdForBackend(nextEntry, activeBackend);
211
+ return { ...cfg, channel: nextEntry };
224
212
  });
225
213
  }
226
214
 
@@ -230,7 +218,6 @@ export function setWebhookConfig(patch = {}) {
230
218
  webhook: {
231
219
  ...(cfg.webhook || {}),
232
220
  ...(Object.prototype.hasOwnProperty.call(patch, 'enabled') ? { enabled: patch.enabled === true } : {}),
233
- ...(Object.prototype.hasOwnProperty.call(patch, 'respectQuiet') ? { respectQuiet: patch.respectQuiet === true } : {}),
234
221
  ...(patch.port ? { port: Number(patch.port) || 3333 } : {}),
235
222
  ...(patch.domain ? { domain: String(patch.domain).trim() } : {}),
236
223
  ...(patch.ngrokDomain ? { ngrokDomain: String(patch.ngrokDomain).trim() } : {}),
@@ -245,14 +232,11 @@ export function setBackend(name) {
245
232
  }
246
233
  updateChannelsSection((cfg) => {
247
234
  const activeBackend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
248
- const channelsConfig = Object.fromEntries(Object.entries(cfg.channelsConfig || {}).map(([key, entry]) => [
249
- key,
250
- (() => {
251
- const seeded = seedBackendChannelIds(entry, activeBackend);
252
- return { ...seeded, channelId: channelIdForBackend(seeded, value) };
253
- })(),
254
- ]));
255
- return { ...cfg, channelsConfig, backend: value };
235
+ const seeded = seedBackendChannelIds(resolveChannelEntry(cfg), activeBackend);
236
+ // `channelId` now mirrors the newly-selected backend's id; both per-backend
237
+ // id fields are retained so switching back keeps the other id.
238
+ const channel = { ...seeded, channelId: channelIdForBackend(seeded, value) };
239
+ return { ...cfg, channel, backend: value };
256
240
  });
257
241
  return { ok: true, backend: value };
258
242
  }
@@ -274,8 +258,15 @@ export function listSchedules() {
274
258
  if (Object.prototype.hasOwnProperty.call(config, 'enabled')) {
275
259
  config.enabled = config.enabled !== 'false' && config.enabled !== false;
276
260
  }
261
+ // Dir name is canonical identity; frontmatter `name` is display-only and
262
+ // must never override the slug used for routing/deletion. `description`
263
+ // defaults to '' for old files with no such key (backward compat).
264
+ const description = String(config.description || '');
265
+ delete config.name;
266
+ delete config.description;
277
267
  return {
278
268
  name,
269
+ description,
279
270
  ...config,
280
271
  instructions: body,
281
272
  route: config.channel ? `channel:${config.channel}` : 'session',
@@ -285,6 +276,7 @@ export function listSchedules() {
285
276
 
286
277
  export function saveSchedule({
287
278
  name,
279
+ description = '',
288
280
  time,
289
281
  timezone,
290
282
  days,
@@ -302,7 +294,9 @@ export function saveSchedule({
302
294
  const dir = join(schedulesDir(), id);
303
295
  if (existsSync(dir) && overwrite !== true) throw new Error(`schedule "${id}" already exists`);
304
296
  mkdirSync(dir, { recursive: true });
305
- const config = { time: cron };
297
+ // name (== dir slug) and description lead the frontmatter per the SKILL.md
298
+ // convention; operational keys follow.
299
+ const config = { name: id, description: String(description || '').trim(), time: cron };
306
300
  if (timezone) config.timezone = String(timezone).trim();
307
301
  if (days && days !== 'daily') config.days = String(days).trim();
308
302
  if (channel) config.channel = String(channel).trim();
@@ -324,7 +318,13 @@ export function setScheduleEnabled(name, enabled) {
324
318
  const mdPath = join(dir, 'SCHEDULE.md');
325
319
  if (!existsSync(mdPath)) throw new Error(`schedule "${id}" does not exist`);
326
320
  const { frontmatter, body } = readMarkdownDocument(readText(mdPath, ''));
327
- writeTextAtomic(mdPath, serializeFrontmatterDoc({ ...frontmatter, enabled: enabled !== false }, body));
321
+ // Re-spread with name/description first so the round-trip keeps SKILL.md
322
+ // key order; name stays the dir slug regardless of stale frontmatter.
323
+ const { name: _staleName, description = '', ...rest } = frontmatter;
324
+ writeTextAtomic(mdPath, serializeFrontmatterDoc(
325
+ { name: id, description, ...rest, enabled: enabled !== false },
326
+ body,
327
+ ));
328
328
  return { name: id, enabled: enabled !== false };
329
329
  }
330
330
 
@@ -336,12 +336,19 @@ export function listWebhooks() {
336
336
  if (Object.prototype.hasOwnProperty.call(config, 'enabled')) {
337
337
  config.enabled = config.enabled !== 'false' && config.enabled !== false;
338
338
  }
339
+ // Dir name is canonical identity/URL routing key; frontmatter `name` is
340
+ // display-only and must not override the slug. `description` defaults to
341
+ // '' for old files with no such key (backward compat).
342
+ const description = String(config.description || '');
343
+ delete config.name;
344
+ delete config.description;
339
345
  // Secret lives in a side file (<name>/secret), never in frontmatter, so
340
346
  // an arbitrary user-supplied secret (quotes/colon/newline) round-trips
341
347
  // losslessly and setWebhookEnabled cannot corrupt it.
342
348
  const hasSecret = Boolean(readText(join(dir, 'secret'), '').trim());
343
349
  return {
344
350
  name,
351
+ description,
345
352
  ...config,
346
353
  secretSet: hasSecret,
347
354
  secret: undefined,
@@ -353,6 +360,7 @@ export function listWebhooks() {
353
360
 
354
361
  export function saveWebhook({
355
362
  name,
363
+ description = '',
356
364
  parser = 'github',
357
365
  secret,
358
366
  channel,
@@ -373,7 +381,9 @@ export function saveWebhook({
373
381
  if (existsSync(dir) && overwrite !== true) throw new Error(`webhook "${id}" already exists`);
374
382
  mkdirSync(dir, { recursive: true });
375
383
  const secretValue = String(secret || randomBytes(24).toString('hex')).trim();
376
- const config = { parser: nextParser };
384
+ // name (== dir slug) and description lead the frontmatter per the SKILL.md
385
+ // convention; operational keys (parser/channel/model/enabled) follow.
386
+ const config = { name: id, description: String(description || '').trim(), parser: nextParser };
377
387
  if (channel) config.channel = String(channel).trim();
378
388
  if (model) config.model = String(model).trim();
379
389
  if (enabled === false) config.enabled = false;
@@ -397,7 +407,13 @@ export function setWebhookEnabled(name, enabled) {
397
407
  const mdPath = join(dir, 'WEBHOOK.md');
398
408
  if (!existsSync(mdPath)) throw new Error(`webhook "${id}" does not exist`);
399
409
  const { frontmatter, body } = readMarkdownDocument(readText(mdPath, ''));
400
- writeTextAtomic(mdPath, serializeFrontmatterDoc({ ...frontmatter, enabled: enabled !== false }, body));
410
+ // Re-spread with name/description first so the round-trip keeps SKILL.md
411
+ // key order; name stays the dir slug regardless of stale frontmatter.
412
+ const { name: _staleName, description = '', ...rest } = frontmatter;
413
+ writeTextAtomic(mdPath, serializeFrontmatterDoc(
414
+ { name: id, description, ...rest, enabled: enabled !== false },
415
+ body,
416
+ ));
401
417
  return { name: id, enabled: enabled !== false };
402
418
  }
403
419
 
@@ -429,7 +445,7 @@ export function channelSetup(config = null) {
429
445
  stored: hasStoredSecret(SECRET_ACCOUNTS.webhookAuth),
430
446
  status: webhookAuth ? 'Set' : 'Off',
431
447
  },
432
- channels: listChannels(cfg),
448
+ channel: getChannel(cfg),
433
449
  schedules: listSchedules(),
434
450
  webhooks: listWebhooks(),
435
451
  };
@@ -440,11 +456,7 @@ export function renderChannelStatus(config = null) {
440
456
  const lines = [];
441
457
  lines.push(`discord ${setup.discord.status}${setup.discord.problem ? ` (${setup.discord.problem})` : ''}`);
442
458
  lines.push(`webhook ${setup.webhook.enabled === false ? 'disabled' : 'enabled'} · auth ${setup.webhook.status} · port ${setup.webhook.port || 3333}`);
443
- lines.push('channels');
444
- if (setup.channels.length === 0) lines.push(' (none)');
445
- for (const ch of setup.channels) {
446
- lines.push(` ${ch.name}${ch.main ? ' *' : ''} ${ch.channelId || '(unset)'} ${ch.mode}`);
447
- }
459
+ lines.push(`channel ${setup.channel.channelId || '(unset)'}`);
448
460
  lines.push('schedules');
449
461
  if (setup.schedules.length === 0) lines.push(' (none)');
450
462
  for (const item of setup.schedules) {
@@ -9,15 +9,12 @@ import { appendBuffered } from '../runtime/shared/buffered-appender.mjs';
9
9
  const CHANNEL_TOOLS = new Set([
10
10
  'reply',
11
11
  'fetch',
12
- 'react',
13
- 'edit_message',
14
- 'download_attachment',
15
- 'schedule_status',
16
- 'trigger_schedule',
17
- 'schedule_control',
12
+ // activate_channel_bridge/reload_config are NOT model-facing tools (no
13
+ // TOOL_DEFS entry) but stay in the allow-set: mixdog-session-runtime.mjs
14
+ // calls channels.execute() with these names directly as internal
15
+ // Lead-only runtime plumbing (bridge-claim on start, config hot-reload).
18
16
  'activate_channel_bridge',
19
17
  'reload_config',
20
- 'inject_command',
21
18
  ]);
22
19
 
23
20
  const WORKER_PRELOAD = fileURLToPath(new URL('./channel-worker-preload.cjs', import.meta.url));
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, resolve } from 'node:path';
2
+ import { existsSync } from 'node:fs';
2
3
  import { homedir } from 'node:os';
3
4
  import { makeAgentDispatch } from '../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
4
5
  import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
@@ -38,8 +39,28 @@ const EXPLORE_RESULT_CACHE_TTL_MS = (() => {
38
39
  const raw = Number(process.env.MIXDOG_EXPLORE_RESULT_CACHE_TTL_MS);
39
40
  return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 5 * 60_000;
40
41
  })();
42
+ // Hard ceiling for one explorer compute. The dispatch-level watchdog is the
43
+ // primary abort; this race is the last line of defence so a wedged compute can
44
+ // never hang the awaiting tool call (and its cache key) forever.
45
+ export const EXPLORE_COMPUTE_HARD_TIMEOUT_MS = (() => {
46
+ const raw = Number(process.env.MIXDOG_EXPLORE_HARD_TIMEOUT_MS);
47
+ return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 10 * 60_000;
48
+ })();
41
49
  const EXPLORE_RESULT_CACHE = new Map(); // key -> { ts, value?, promise? }
42
50
 
51
+ function withExploreHardTimeout(promise) {
52
+ if (!(EXPLORE_COMPUTE_HARD_TIMEOUT_MS > 0)) return promise;
53
+ let timer = null;
54
+ const timeout = new Promise((_resolve, reject) => {
55
+ timer = setTimeout(
56
+ () => reject(new Error(`explorer timed out after ${EXPLORE_COMPUTE_HARD_TIMEOUT_MS}ms`)),
57
+ EXPLORE_COMPUTE_HARD_TIMEOUT_MS,
58
+ );
59
+ if (typeof timer.unref === 'function') timer.unref();
60
+ });
61
+ return Promise.race([promise, timeout]).finally(() => { if (timer) clearTimeout(timer); });
62
+ }
63
+
43
64
  function escapeXml(str) {
44
65
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
45
66
  }
@@ -49,7 +70,7 @@ function escapeXml(str) {
49
70
  // reminder. The full no-verdict contract lives at system level
50
71
  // (rules/agent/30-explorer.md).
51
72
  export function buildExplorerPrompt(query) {
52
- return `<query>${escapeXml(query)}</query>\nReminder: output only anchor lines formatted as path:line — symbol/name — short reason, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
73
+ return `<query>${escapeXml(query)}</query>\nReminder: budget 4 tool turns — batch every candidate lookup per turn, never one tool at a time. The moment you have credible anchors, STOP and answer; spend remaining turns only while you have zero anchors. Output only anchor lines formatted as path:line — symbol/name — short reason, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
53
74
  }
54
75
 
55
76
  export function normalizeExploreQueries(rawQuery) {
@@ -80,11 +101,11 @@ function resolveExploreCwd(input, callerCwd) {
80
101
  return base;
81
102
  }
82
103
 
83
- function settledExplorerResult(result) {
104
+ function settledExplorerResult(result, cwd = null) {
84
105
  if (result?.status !== 'fulfilled') {
85
106
  return { ok: false, text: `[explorer error] ${result?.reason?.message || String(result?.reason)}` };
86
107
  }
87
- const text = cleanExplorerText(typeof result.value === 'string' ? result.value : responseText(result.value));
108
+ const text = cleanExplorerText(typeof result.value === 'string' ? result.value : responseText(result.value), cwd);
88
109
  if (!text) return { ok: false, text: '[explorer error] empty response' };
89
110
  return { ok: true, text };
90
111
  }
@@ -106,10 +127,10 @@ function fatalExploreError(settled) {
106
127
  return (settled || []).find((r) => r?.status === 'rejected' && isFatalExploreError(r.reason))?.reason || null;
107
128
  }
108
129
 
109
- function mergeSettled(settled, queries) {
130
+ function mergeSettled(settled, queries, cwd = null) {
110
131
  const single = queries.length === 1;
111
132
  if (single) {
112
- const { text: body } = settledExplorerResult(settled[0]);
133
+ const { text: body } = settledExplorerResult(settled[0], cwd);
113
134
  return body.length > EXPLORE_OUTPUT_CHAR_CAP
114
135
  ? body.slice(0, EXPLORE_OUTPUT_CHAR_CAP) + EXPLORE_TRUNCATION_MARKER
115
136
  : body;
@@ -120,7 +141,7 @@ function mergeSettled(settled, queries) {
120
141
  let truncated = false;
121
142
  for (let i = 0; i < settled.length; i++) {
122
143
  const header = `Q${i + 1}: ${String(queries[i] ?? '').replace(/\s+/g, ' ').slice(0, 60)}`;
123
- const { text: body } = settledExplorerResult(settled[i]);
144
+ const { text: body } = settledExplorerResult(settled[i], cwd);
124
145
  const piece = `${header}\n${body}`;
125
146
  const addLen = (parts.length === 0 ? 0 : sep.length) + piece.length;
126
147
  if (total + addLen > EXPLORE_OUTPUT_CHAR_CAP) {
@@ -208,11 +229,19 @@ function getCachedExploreResult(key) {
208
229
  async function runExploreCached(key, compute) {
209
230
  const cached = getCachedExploreResult(key);
210
231
  if (cached !== null) return cached;
211
- if (!exploreResultCacheEnabled()) return await compute();
212
- const pending = EXPLORE_RESULT_CACHE.get(key)?.promise;
213
- if (pending) return await pending;
232
+ if (!exploreResultCacheEnabled()) return await withExploreHardTimeout(compute());
233
+ const entry = EXPLORE_RESULT_CACHE.get(key);
234
+ if (entry?.promise) {
235
+ // Pending in-flight dedup — but never trust a pending entry forever: a
236
+ // compute that outlived the hard timeout is wedged (its own race should
237
+ // have rejected); drop the poisoned key and recompute fresh.
238
+ if (!(EXPLORE_COMPUTE_HARD_TIMEOUT_MS > 0) || Date.now() - entry.ts <= EXPLORE_COMPUTE_HARD_TIMEOUT_MS) {
239
+ return await entry.promise;
240
+ }
241
+ EXPLORE_RESULT_CACHE.delete(key);
242
+ }
214
243
  const promise = Promise.resolve()
215
- .then(() => compute())
244
+ .then(() => withExploreHardTimeout(compute()))
216
245
  .then((value) => {
217
246
  const text = typeof value === 'string' ? value : responseText(value);
218
247
  const cleaned = cleanExplorerText(text);
@@ -247,6 +276,7 @@ function responseText(response) {
247
276
  }
248
277
 
249
278
  const ANCHOR_LINE_RE = /(?:^|\s)(?:[A-Za-z]:[\\/][^:\n]+|\.{0,2}[\\/][^:\n]+|src[\\/][^:\n]+|[\w.-]+[\\/][^:\n]+):\d+\b/;
279
+ const ANCHOR_PATH_CAPTURE_RE = /(?:^|\s)((?:[A-Za-z]:[\\/][^:\n]+|\.{0,2}[\\/][^:\n]+|src[\\/][^:\n]+|[\w.-]+[\\/][^:\n]+)):\d+\b/;
250
280
  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;
251
281
 
252
282
  function normalizeExplorerLine(line) {
@@ -260,7 +290,16 @@ function normalizeExplorerLine(line) {
260
290
  .trim();
261
291
  }
262
292
 
263
- function cleanExplorerText(text) {
293
+ // Anchor existence filter: a locator answer citing a file that does not exist
294
+ // is a hallucination — drop the line. Only enforced when cwd is known.
295
+ function anchorLineExists(line, cwd) {
296
+ const m = line.match(ANCHOR_PATH_CAPTURE_RE);
297
+ if (!m) return true;
298
+ const p = m[1].trim();
299
+ try { return existsSync(isAbsolute(p) ? p : resolve(cwd, p)); } catch { return true; }
300
+ }
301
+
302
+ function cleanExplorerText(text, cwd = null) {
264
303
  const raw = String(text || '').trim();
265
304
  if (!raw) return '';
266
305
  const anchors = [];
@@ -270,7 +309,12 @@ function cleanExplorerText(text) {
270
309
  if (/^EXPLORATION_FAILED\b/i.test(line)) return 'EXPLORATION_FAILED';
271
310
  if (ANCHOR_LINE_RE.test(line)) anchors.push(line);
272
311
  }
273
- if (anchors.length) return anchors.join('\n');
312
+ if (anchors.length) {
313
+ const real = cwd ? anchors.filter((l) => anchorLineExists(l, cwd)) : anchors;
314
+ // All anchors pointed at nonexistent paths: fail harmlessly instead of
315
+ // forwarding hallucinated locations.
316
+ return real.length ? real.join('\n') : 'EXPLORATION_FAILED';
317
+ }
274
318
  if (FAILED_RE.test(raw)) return 'EXPLORATION_FAILED';
275
319
  return raw;
276
320
  }
@@ -311,10 +355,16 @@ async function runExploreSync(args = {}, ctx = {}) {
311
355
  const presetName = (route && typeof route === 'object')
312
356
  ? `${clean(route.provider)}/${clean(route.model)}`
313
357
  : (route || '');
358
+ // Explorer self-manages its turn budget: the 4-turn budget lives ONLY in
359
+ // explorer-scoped surfaces (rules/agent/30-explorer.md + per-query prompt),
360
+ // never in shared loop policy. The cap here is a runaway guard, unchanged
361
+ // from the original value; B-round live tests showed hard targets
362
+ // legitimately need ~10.
314
363
  const llm = makeAgentDispatch({
315
364
  agent: 'explorer',
316
365
  cwd: resolvedCwd,
317
366
  brief: true,
367
+ maxLoopIterations: 12,
318
368
  parentSessionId: ctx.callerSessionId || null,
319
369
  clientHostPid: ctx.clientHostPid || null,
320
370
  config,
@@ -328,8 +378,8 @@ async function runExploreSync(args = {}, ctx = {}) {
328
378
  const fatal = fatalExploreError(settled);
329
379
  if (fatal) return fail(presentErrorText(fatal, { surface: 'explore' }));
330
380
 
331
- const merged = mergeSettled(settled, working);
332
- const allFailed = settled.every((r) => !settledExplorerResult(r).ok);
381
+ const merged = mergeSettled(settled, working, resolvedCwd);
382
+ const allFailed = settled.every((r) => !settledExplorerResult(r, resolvedCwd).ok);
333
383
  const out = capNotice + merged;
334
384
  return allFailed ? fail(out) : ok(out);
335
385
  }