mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -3,14 +3,23 @@ import { homedir } from 'node:os';
3
3
  import { basename, dirname, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { performance } from 'node:perf_hooks';
6
- import { ensureProjectMixdogMd, ensureStandaloneEnvironment } from './standalone/seeds.mjs';
7
- import { createStandaloneBridge } from './standalone/bridge-tool.mjs';
6
+ import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
7
+ import { createStandaloneAgent } from './standalone/agent-tool.mjs';
8
+ import { isAgentOwner } from './runtime/agent/orchestrator/agent-owner.mjs';
8
9
  import { EXPLORE_TOOL, runExplore } from './standalone/explore-tool.mjs';
9
10
  import { createStandaloneChannelWorker } from './standalone/channel-worker.mjs';
11
+ import { createStandaloneMemoryRuntime } from './standalone/memory-runtime-proxy.mjs';
10
12
  import { createStandaloneHookBus } from './standalone/hook-bus.mjs';
11
13
  import { writeLastSessionCwd } from './runtime/shared/user-cwd.mjs';
12
14
  import { cancelBackgroundTasks } from './runtime/shared/background-tasks.mjs';
13
- import { createWorkspaceRouter, formatWorkspaceSessionContext } from './runtime/shared/workspace-router.mjs';
15
+ import {
16
+ modelVisibleToolCompletionMessage,
17
+ shouldPersistModelVisibleToolCompletion,
18
+ } from './runtime/shared/tool-execution-contract.mjs';
19
+ import {
20
+ normalizeAgentPermissionOrNone,
21
+ readMarkdownDocument,
22
+ } from './runtime/shared/markdown-frontmatter.mjs';
14
23
  import { setConfiguredShell } from './runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs';
15
24
  import {
16
25
  PROVIDER_STATUS_TOOL,
@@ -25,6 +34,12 @@ import {
25
34
  setLocalProvider,
26
35
  } from './standalone/provider-admin.mjs';
27
36
  import { createUsageDashboard } from './standalone/usage-dashboard.mjs';
37
+ import { fetchOAuthUsageSnapshot } from './runtime/agent/orchestrator/providers/oauth-usage.mjs';
38
+ import { getModelMetadataSync } from './runtime/agent/orchestrator/providers/model-catalog.mjs';
39
+ import {
40
+ isResponsesFreeformTool,
41
+ toResponsesCustomTool,
42
+ } from './runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
28
43
  import {
29
44
  channelSetup,
30
45
  deleteChannel,
@@ -52,9 +67,76 @@ import {
52
67
  import {
53
68
  estimateMessagesTokens,
54
69
  estimateRequestReserveTokens,
70
+ estimateTranscriptContextUsage,
55
71
  estimateToolSchemaTokens,
56
72
  } from './runtime/agent/orchestrator/session/context-utils.mjs';
57
73
 
74
+ // Default compaction buffer rules — kept in lockstep with the worker runtime
75
+ // (loop.mjs resolveCompactBufferRatio / manager.mjs compactBufferRatioForSession
76
+ // and compact.mjs compactionBufferTokensForBoundary). By default the trigger is
77
+ // the effective compact boundary itself; explicit buffer settings can lower it.
78
+ const CONTEXT_DEFAULT_BUFFER_RATIO = 0;
79
+ const CONTEXT_MAX_BUFFER_RATIO = 0.25;
80
+ const CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO = 0.1;
81
+ function resolveContextBufferRatio(cfg = {}) {
82
+ // Percent-named inputs (bufferPercent/bufferPct/*_BUFFER_PERCENT): a value of
83
+ // 1 means 1% (→0.01). Ratio-named inputs (bufferRatio/bufferFraction): 0.01
84
+ // means 1%, while a value > 1 is treated as a legacy percent (10 → 10%).
85
+ const candidates = [
86
+ [cfg.bufferPercent, true],
87
+ [cfg.bufferPct, true],
88
+ [cfg.bufferRatio, false],
89
+ [cfg.bufferFraction, false],
90
+ [process.env.MIXDOG_AGENT_COMPACT_BUFFER_PERCENT, true],
91
+ [process.env.MIXDOG_AGENT_COMPACT_BUFFER_RATIO, false],
92
+ ];
93
+ for (const [raw, isPercent] of candidates) {
94
+ const n = Number(raw);
95
+ if (!Number.isFinite(n) || n <= 0) continue;
96
+ return isPercent ? Math.min(1, n / 100) : (n > 1 ? n / 100 : n);
97
+ }
98
+ return CONTEXT_DEFAULT_BUFFER_RATIO;
99
+ }
100
+ function isLegacyDefaultContextBufferTelemetry(cfg = {}, boundaryTokens = 0) {
101
+ const boundary = Number(boundaryTokens);
102
+ if (!Number.isFinite(boundary) || boundary <= 0) return false;
103
+ if (Number(process.env.MIXDOG_AGENT_COMPACT_BUFFER_TOKENS) > 0) return false;
104
+ for (const envName of ['MIXDOG_AGENT_COMPACT_BUFFER_PERCENT', 'MIXDOG_AGENT_COMPACT_BUFFER_RATIO']) {
105
+ const n = Number(process.env[envName]);
106
+ if (Number.isFinite(n) && n > 0) return false;
107
+ }
108
+ for (const key of ['bufferPercent', 'bufferPct', 'bufferFraction']) {
109
+ const n = Number(cfg?.[key]);
110
+ if (Number.isFinite(n) && n > 0) return false;
111
+ }
112
+ const explicitTokens = Number(cfg?.bufferTokens ?? cfg?.buffer);
113
+ const ratio = Number(cfg?.bufferRatio);
114
+ if (!Number.isFinite(explicitTokens) || explicitTokens <= 0 || !Number.isFinite(ratio) || Math.abs(ratio - CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO) > 1e-9) {
115
+ return false;
116
+ }
117
+ const expectedTokens = Math.floor(boundary * CONTEXT_LEGACY_DEFAULT_BUFFER_RATIO);
118
+ const cfgBoundary = Number(cfg?.boundaryTokens);
119
+ const cfgTrigger = Number(cfg?.triggerTokens);
120
+ return Math.floor(explicitTokens) === expectedTokens
121
+ || (Number.isFinite(cfgBoundary) && Math.floor(cfgBoundary) === Math.floor(boundary)
122
+ && Number.isFinite(cfgTrigger) && cfgTrigger > 0
123
+ && Math.floor(explicitTokens) === Math.max(0, Math.floor(boundary - cfgTrigger)));
124
+ }
125
+ function contextDefaultBufferTokens(boundaryTokens, cfg = {}) {
126
+ const boundary = Number(boundaryTokens);
127
+ if (!Number.isFinite(boundary) || boundary <= 0) return 0;
128
+ const effectiveCfg = isLegacyDefaultContextBufferTelemetry(cfg, boundary)
129
+ ? { ...cfg, bufferTokens: null, buffer: null, bufferRatio: null }
130
+ : cfg;
131
+ const cap = Math.max(0, Math.floor(boundary * CONTEXT_MAX_BUFFER_RATIO));
132
+ const explicit = Number(effectiveCfg.bufferTokens ?? effectiveCfg.buffer);
133
+ if (Number.isFinite(explicit) && explicit > 0) {
134
+ return Math.min(Math.floor(explicit), cap);
135
+ }
136
+ const ratio = resolveContextBufferRatio(effectiveCfg);
137
+ return Math.max(0, Math.min(Math.floor(boundary * ratio), cap));
138
+ }
139
+
58
140
  function sessionMessageText(content) {
59
141
  if (content == null) return '';
60
142
  if (typeof content === 'string') return content;
@@ -113,9 +195,8 @@ function splitMarkdownSections(text) {
113
195
  function reminderSectionBucket(section) {
114
196
  const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
115
197
  if (heading.includes('core memory')) return 'memory';
116
- if (heading.includes('mixdog-project-context') || heading.includes('project-context')) return 'project';
117
198
  if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
118
- if (heading.includes('workspace') || heading === 'role' || heading.includes('role-identity') || heading.includes('task-brief')) return 'workspace';
199
+ if (heading.includes('workspace')) return 'workspace';
119
200
  if (heading.includes('environment')) return 'environment';
120
201
  return 'other';
121
202
  }
@@ -134,7 +215,6 @@ function summarizeContextMessages(messages) {
134
215
  assistant: { count: 0, tokens: 0 },
135
216
  toolResults: { count: 0, tokens: 0 },
136
217
  reminders: { count: 0, tokens: 0, otherTokens: 0 },
137
- project: { tokens: 0 },
138
218
  workflow: { tokens: 0 },
139
219
  memory: { tokens: 0 },
140
220
  workspace: { tokens: 0 },
@@ -252,6 +332,10 @@ function envFlag(name) {
252
332
  return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
253
333
  }
254
334
 
335
+ function envPresent(name) {
336
+ return process.env[name] !== undefined && process.env[name] !== '';
337
+ }
338
+
255
339
  function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
256
340
  const raw = process.env[name];
257
341
  if (raw === undefined || raw === '') return fallback;
@@ -292,7 +376,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
292
376
  const EFFORT_OPTIONS_BY_PROVIDER = {
293
377
  openai: ['none', 'low', 'medium', 'high', 'xhigh'],
294
378
  'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
295
- anthropic: ['low', 'medium', 'high', 'max'],
379
+ anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
296
380
  'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
297
381
  xai: ['none', 'low', 'medium', 'high'],
298
382
  'grok-oauth': ['none', 'low', 'medium', 'high'],
@@ -329,15 +413,15 @@ export const TOOL_SEARCH_TOOL = {
329
413
  destructiveHint: false,
330
414
  idempotentHint: true,
331
415
  openWorldHint: false,
332
- bridgeHidden: true,
416
+ agentHidden: true,
333
417
  },
334
- description: 'Search the current standalone tool surface and select deferred tools/skills for the task. Use before unfamiliar or currently inactive tools.',
418
+ description: 'Search deferred tools; confident queries load matches.',
335
419
  inputSchema: {
336
420
  type: 'object',
337
421
  properties: {
338
- query: { type: 'string', description: 'Optional search text, e.g. shell, bridge, memory, skill, mcp.' },
339
- select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Tool/skill names to activate, as comma-separated text or an array.' },
340
- limit: { type: 'number', description: 'Maximum matches to return.' },
422
+ query: { type: 'string', description: 'Search text; confident hits load. select:a,b forces exact names.' },
423
+ select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Force exact tool names.' },
424
+ limit: { type: 'number', description: 'Max matches.' },
341
425
  },
342
426
  additionalProperties: false,
343
427
  },
@@ -352,43 +436,66 @@ const CHANNEL_STATUS_TOOL = {
352
436
  destructiveHint: false,
353
437
  idempotentHint: true,
354
438
  openWorldHint: false,
355
- bridgeHidden: true,
439
+ agentHidden: true,
356
440
  },
357
- description: 'List standalone Discord/channel/schedule/webhook configuration status. Read-only and never returns secrets.',
441
+ description: 'List channel/schedule/webhook status. No secrets.',
358
442
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
359
443
  };
360
444
 
361
445
  const CWD_TOOL = {
362
446
  name: 'cwd',
363
- title: 'Current Working Directory',
447
+ title: 'Work Project',
364
448
  annotations: {
365
- title: 'Current Working Directory',
449
+ title: 'Work Project',
366
450
  readOnlyHint: false,
367
451
  destructiveHint: false,
368
452
  idempotentHint: false,
369
453
  openWorldHint: false,
370
- bridgeHidden: true,
454
+ agentHidden: true,
371
455
  },
372
- description: 'Show or set the standalone session working directory. Default get; action=set requires path.',
456
+ description: 'Show or set the session work project for tool execution.',
373
457
  inputSchema: {
374
458
  type: 'object',
375
459
  properties: {
376
- action: { type: 'string', enum: ['get', 'set'], description: 'Default get, or set when path is provided.' },
377
- path: { type: 'string', description: 'Directory path for action=set. Relative paths resolve from the current cwd.' },
460
+ action: { type: 'string', enum: ['get', 'set'], description: 'Default get.' },
461
+ path: { type: 'string', description: 'Project directory for set.' },
378
462
  },
379
463
  additionalProperties: false,
380
464
  },
381
465
  };
382
466
 
467
+ export const SKILL_TOOL = {
468
+ name: 'Skill',
469
+ title: 'Skill',
470
+ annotations: {
471
+ title: 'Skill',
472
+ readOnlyHint: true,
473
+ destructiveHint: false,
474
+ idempotentHint: true,
475
+ openWorldHint: false,
476
+ agentHidden: false,
477
+ },
478
+ description: 'Load a named SKILL.md into context.',
479
+ inputSchema: {
480
+ type: 'object',
481
+ properties: {
482
+ name: { type: 'string', description: 'Skill name.' },
483
+ },
484
+ required: ['name'],
485
+ additionalProperties: false,
486
+ },
487
+ };
488
+
383
489
  const MEASURED_TOOL_USAGE = Object.freeze({
384
490
  read: 710,
385
491
  code_graph: 520,
386
492
  grep: 500,
493
+ find: 480,
387
494
  glob: 460,
388
495
  list: 430,
389
496
  apply_patch: 400,
390
497
  explore: 360,
391
- bridge: 330,
498
+ agent: 330,
392
499
  shell: 81,
393
500
  cwd: 2,
394
501
  diagnostics: 2,
@@ -399,44 +506,54 @@ const MEASURED_TOOL_USAGE = Object.freeze({
399
506
  channel_status: 2,
400
507
  });
401
508
  const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
402
- const DEFERRED_ALWAYS_ACTIVE_TOOLS = new Set([
509
+ const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
510
+ const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
511
+ 'read',
512
+ 'code_graph',
513
+ 'grep',
514
+ 'find',
515
+ 'glob',
516
+ 'list',
517
+ 'explore',
518
+ 'apply_patch',
519
+ 'Skill',
403
520
  'tool_search',
404
- 'recall',
405
- 'search',
406
- 'web_fetch',
407
521
  ]);
408
- const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
409
- const DEFERRED_DEFAULT_FULL_LIMIT = 9;
410
522
  const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
411
523
  'read',
412
524
  'code_graph',
413
525
  'grep',
526
+ 'find',
414
527
  'glob',
415
528
  'list',
416
529
  'explore',
530
+ 'Skill',
417
531
  'tool_search',
418
532
  ]);
419
533
  const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
420
534
  'read',
421
535
  'code_graph',
422
536
  'grep',
537
+ 'find',
423
538
  'glob',
424
539
  'list',
425
540
  'shell',
426
541
  'task',
427
542
  'explore',
428
543
  'apply_patch',
429
- 'bridge',
544
+ 'agent',
430
545
  'recall',
431
546
  'search',
432
547
  'web_fetch',
433
548
  'cwd',
549
+ 'Skill',
434
550
  'tool_search',
435
551
  ]);
436
552
  const READONLY_TOOL_NAMES = new Set([
437
553
  'read',
438
554
  'list',
439
555
  'grep',
556
+ 'find',
440
557
  'glob',
441
558
  'code_graph',
442
559
  'search',
@@ -447,21 +564,22 @@ const READONLY_TOOL_NAMES = new Set([
447
564
  'channel_status',
448
565
  'schedule_status',
449
566
  'fetch',
567
+ 'Skill',
450
568
  ]);
451
- const BRIDGE_HIDDEN_WRAPPER_TOOLS = new Set(['explore', 'search']);
569
+ const AGENT_HIDDEN_WRAPPER_TOOLS = new Set(['search']);
452
570
 
453
571
  function applyStandaloneToolDefaults(tool) {
454
- if (!tool || !BRIDGE_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
572
+ if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
455
573
  return {
456
574
  ...tool,
457
575
  annotations: {
458
576
  ...(tool.annotations || {}),
459
- bridgeHidden: true,
577
+ agentHidden: true,
460
578
  },
461
579
  };
462
580
  }
463
581
  const DEFERRED_SELECT_ALIASES = {
464
- filesystem: ['read', 'list', 'grep', 'glob'],
582
+ filesystem: ['read', 'list', 'grep', 'find', 'glob'],
465
583
  search: ['search', 'web_fetch'],
466
584
  web: ['web_fetch', 'search'],
467
585
  memory: ['memory', 'recall'],
@@ -474,11 +592,68 @@ const DEFERRED_SELECT_ALIASES = {
474
592
  channel: ['channel_status'],
475
593
  explore: ['explore'],
476
594
  discovery: ['explore'],
477
- bridge: ['bridge'],
595
+ agent: ['agent'],
478
596
  graph: ['code_graph'],
479
597
  code: ['code_graph'],
480
598
  shell: ['shell', 'task'],
481
599
  };
600
+ const TOOL_SEARCH_SAFE_AUTO_ALIASES = new Set([
601
+ 'shell',
602
+ 'web',
603
+ 'search',
604
+ 'agent',
605
+ 'provider',
606
+ 'providers',
607
+ 'channel',
608
+ 'schedule',
609
+ 'memory',
610
+ ]);
611
+ const TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES = new Set([
612
+ 'status',
613
+ 'state',
614
+ 'info',
615
+ 'list',
616
+ 'show',
617
+ 'config',
618
+ ]);
619
+ const TOOL_SEARCH_STOP_WORDS = new Set([
620
+ 'a',
621
+ 'an',
622
+ 'and',
623
+ 'are',
624
+ 'as',
625
+ 'for',
626
+ 'from',
627
+ 'how',
628
+ 'i',
629
+ 'in',
630
+ 'is',
631
+ 'me',
632
+ 'need',
633
+ 'of',
634
+ 'on',
635
+ 'please',
636
+ 'the',
637
+ 'to',
638
+ 'tool',
639
+ 'tools',
640
+ 'use',
641
+ 'using',
642
+ 'with',
643
+ ]);
644
+ const TOOL_SEARCH_ROW_ALIASES = Object.freeze({
645
+ agent: ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer'],
646
+ channel_status: ['channel status', 'discord status', 'channel config'],
647
+ cwd: ['cwd', 'working directory', 'current directory', 'project root', 'folder'],
648
+ memory: ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status'],
649
+ provider_status: ['provider status', 'auth status', 'model status', 'oauth status', 'provider config'],
650
+ recall: ['recall', 'previous work', 'past work', 'prior context', 'history', 'resume context'],
651
+ schedule_status: ['schedule status', 'cron status'],
652
+ search: ['web search', 'internet search', 'current info', 'latest info', 'online search', 'docs search'],
653
+ shell: ['run command', 'execute command', 'terminal', 'powershell', 'bash', 'run tests', 'test command', 'build command', 'npm', 'node', 'git'],
654
+ task: ['background task', 'async task', 'wait task', 'cancel task', 'task status'],
655
+ web_fetch: ['fetch url', 'fetch page', 'open url', 'web page', 'read url', 'docs page'],
656
+ });
482
657
 
483
658
  function normalizeToolMode(mode) {
484
659
  const value = String(mode || '').trim().toLowerCase();
@@ -503,8 +678,21 @@ function effortOptionsFor(provider, model) {
503
678
  const declared = Array.isArray(model?.reasoningLevels)
504
679
  ? model.reasoningLevels.map(clean).filter(Boolean)
505
680
  : [];
506
- if (Array.isArray(model?.reasoningLevels)) return filterProvider(declared);
507
681
  const family = clean(model?.family).toLowerCase();
682
+ if (Array.isArray(model?.reasoningLevels)) {
683
+ if (declared.length) return filterProvider(declared);
684
+ if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
685
+ return filterProvider(EFFORT_BY_FAMILY[family]);
686
+ }
687
+ return [];
688
+ }
689
+ const reasoningOptionEffort = Array.isArray(model?.reasoningOptions)
690
+ ? model.reasoningOptions.find((option) => clean(option?.type).toLowerCase() === 'effort')
691
+ : null;
692
+ const reasoningOptionValues = Array.isArray(reasoningOptionEffort?.values)
693
+ ? reasoningOptionEffort.values.map(clean).filter(Boolean)
694
+ : [];
695
+ if (reasoningOptionValues.length) return filterProvider(reasoningOptionValues);
508
696
  if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
509
697
  return filterProvider(EFFORT_BY_FAMILY[family]);
510
698
  }
@@ -557,10 +745,23 @@ function toolSpecForMode(mode) {
557
745
  return mode === 'readonly' ? ['tools:readonly'] : 'full';
558
746
  }
559
747
 
748
+ function deferredSurfaceModeForLead(mode) {
749
+ return mode === 'readonly' ? 'readonly' : 'lead';
750
+ }
751
+
560
752
  function clean(value) {
561
753
  return String(value ?? '').trim();
562
754
  }
563
755
 
756
+ // A resolved model meta carries catalog-derived fields (contextWindow, pricing,
757
+ // capabilities, …). The lookupModelMeta() fallback for an unknown id is the
758
+ // bare shape `{ id, provider }`, so "more than id/provider" reliably tells a
759
+ // real catalog hit apart from that placeholder.
760
+ function modelMetaLooksResolved(meta) {
761
+ if (!meta || typeof meta !== 'object') return false;
762
+ return Object.keys(meta).some((key) => key !== 'id' && key !== 'provider');
763
+ }
764
+
564
765
  const OUTPUT_STYLE_ORDER = ['default', 'simple', 'extreme-simple'];
565
766
  const OUTPUT_STYLE_ALIASES = new Map([
566
767
  ['compact', 'default'],
@@ -793,6 +994,21 @@ function resolveRoute(config, { provider, model, effort, fast } = {}) {
793
994
  };
794
995
  }
795
996
 
997
+ function isLikelyRawModelId(value) {
998
+ const model = clean(value);
999
+ if (!model || model.length > 160) return false;
1000
+ if (/\s/.test(model)) return false;
1001
+ return /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/.test(model);
1002
+ }
1003
+
1004
+ function validateRequestedModelSelector(config, requested = {}) {
1005
+ const model = clean(requested.model);
1006
+ if (!model) return;
1007
+ if (findPreset(config, model)) return;
1008
+ if (isLikelyRawModelId(model)) return;
1009
+ throw new Error(`Invalid model selector "${model}". Use a preset or a model id; free-form text cannot be used as a model.`);
1010
+ }
1011
+
796
1012
  function ensureProviderEnabled(config, provider) {
797
1013
  const providers = { ...(config?.providers || {}) };
798
1014
  providers[provider] = { ...(providers[provider] || {}), enabled: true };
@@ -827,12 +1043,51 @@ function normalizeSystemShellCommand(value) {
827
1043
  function normalizeAutoClearConfig(value = {}) {
828
1044
  const raw = value && typeof value === 'object' ? value : {};
829
1045
  const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
1046
+ const compactType = clean(raw.compactType ?? raw.compact_type ?? raw.type);
1047
+ const normalizedCompactType = compactType ? normalizeCompactTypeSetting(compactType, 'semantic') : '';
830
1048
  return {
831
1049
  enabled: raw.enabled !== false,
832
1050
  idleMs: Number.isFinite(idleMs) && idleMs > 0 ? Math.max(60_000, Math.round(idleMs)) : AUTO_CLEAR_DEFAULT_IDLE_MS,
1051
+ ...(normalizedCompactType ? { compactType: normalizedCompactType } : {}),
1052
+ };
1053
+ }
1054
+
1055
+ function normalizeCompactTypeSetting(value, fallback = 'semantic') {
1056
+ const raw = clean(value).toLowerCase().replace(/_/g, '-');
1057
+ if (!raw) return fallback;
1058
+ if (raw === '1' || raw === 'type1' || raw === 'type-1' || raw === 'semantic' || raw === 'summary' || raw === 'default') return 'semantic';
1059
+ if (raw === '2' || raw === 'type2' || raw === 'type-2' || raw === 'recall' || raw === 'recall-fast' || raw === 'recall-fasttrack' || raw === 'recall-fast-track' || raw === 'fasttrack' || raw === 'fast-track') return 'recall-fasttrack';
1060
+ return fallback;
1061
+ }
1062
+
1063
+ function normalizeCompactionConfig(value = {}, { memoryEnabled = true } = {}) {
1064
+ const raw = value && typeof value === 'object' ? value : {};
1065
+ let compactType = normalizeCompactTypeSetting(raw.compactType ?? raw.compact_type ?? raw.type, 'semantic');
1066
+ if (compactType === 'recall-fasttrack' && memoryEnabled === false) compactType = 'semantic';
1067
+ return {
1068
+ ...raw,
1069
+ auto: raw.auto !== false && raw.enabled !== false,
1070
+ type: compactType,
1071
+ compactType,
833
1072
  };
834
1073
  }
835
1074
 
1075
+ function moduleEnabled(configLike, name, fallback = true) {
1076
+ const entry = configLike?.modules?.[name];
1077
+ if (entry && typeof entry === 'object' && entry.enabled === false) return false;
1078
+ return fallback !== false;
1079
+ }
1080
+
1081
+ function setModuleEnabledInConfig(configLike, name, enabled) {
1082
+ const next = { ...(configLike || {}) };
1083
+ next.modules = { ...(next.modules || {}) };
1084
+ next.modules[name] = {
1085
+ ...(next.modules[name] || {}),
1086
+ enabled: enabled !== false,
1087
+ };
1088
+ return next;
1089
+ }
1090
+
836
1091
  function formatDurationMs(ms) {
837
1092
  const value = Math.max(0, Number(ms) || 0);
838
1093
  if (value % 3_600_000 === 0) return `${value / 3_600_000}h`;
@@ -881,6 +1136,44 @@ function openAiDirectModelSupportsFast(model) {
881
1136
  || /^gpt-5\.4-mini(?:-\d{4}|$)/.test(id);
882
1137
  }
883
1138
 
1139
+ function openAiModelSupportsHostedWebSearch(model) {
1140
+ const id = clean(model?.id || model).toLowerCase();
1141
+ if (!id) return false;
1142
+ if (model?.supportsWebSearch === true) return true;
1143
+ const tools = [
1144
+ ...(Array.isArray(model?.supportedTools) ? model.supportedTools : []),
1145
+ ...(Array.isArray(model?.tools) ? model.tools : []),
1146
+ ...(Array.isArray(model?.capabilities?.tools) ? model.capabilities.tools : []),
1147
+ ].map((tool) => clean(tool?.type || tool?.name || tool).toLowerCase());
1148
+ if (tools.some((tool) => tool === 'web_search' || tool === 'web_search_preview')) return true;
1149
+ if (/codex|image|audio|tts|stt|embedding|rerank|moderation|search-preview/.test(id)) return false;
1150
+ return /^gpt-(5(?:\.|$|-)|4\.1(?:-|$)|4o(?:-|$)|4\.5(?:-|$))/.test(id)
1151
+ || /^o[34](?:-|$)/.test(id);
1152
+ }
1153
+
1154
+ function grokModelSupportsHostedWebSearch(model) {
1155
+ const id = clean(model?.id || model).toLowerCase();
1156
+ if (!id || /imagine|image|video|composer/.test(id)) return false;
1157
+ if (id === 'grok-build') return false;
1158
+ return /^grok-/.test(id);
1159
+ }
1160
+
1161
+ function geminiModelSupportsHostedWebSearch(model) {
1162
+ const id = clean(model?.id || model).toLowerCase();
1163
+ if (!id || /embedding|aqa|imagen|veo|tts|image|computer-use|customtools/.test(id)) return false;
1164
+ return /^gemini-(3(?:\.|-|$)|2\.5-|2\.0-flash)/.test(id);
1165
+ }
1166
+
1167
+ function anthropicModelSupportsHostedWebSearch(model) {
1168
+ const id = clean(model?.id || model).toLowerCase();
1169
+ if (!id) return false;
1170
+ const match = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:[-.](\d+))?/);
1171
+ if (!match) return false;
1172
+ const major = Number(match[2]) || 0;
1173
+ const minor = Number(match[3]) || 0;
1174
+ return major > 4 || (major === 4 && minor >= 0);
1175
+ }
1176
+
884
1177
  function anthropicModelMetaSupportsFast(model) {
885
1178
  const id = clean(model?.id || model).toLowerCase();
886
1179
  return /^claude-(opus|sonnet)/.test(id);
@@ -895,6 +1188,16 @@ function fastCapableFor(provider, model) {
895
1188
  return false;
896
1189
  }
897
1190
 
1191
+ function searchCapableFor(provider, model) {
1192
+ const p = normalizeSearchProviderId(provider);
1193
+ if (!isSearchCapableProvider(p)) return false;
1194
+ if (p === 'openai' || p === 'openai-oauth') return openAiModelSupportsHostedWebSearch(model);
1195
+ if (p === 'grok-oauth' || p === 'xai') return grokModelSupportsHostedWebSearch(model);
1196
+ if (p === 'gemini') return geminiModelSupportsHostedWebSearch(model);
1197
+ if (p === 'anthropic' || p === 'anthropic-oauth') return anthropicModelSupportsHostedWebSearch(model);
1198
+ return model?.supportsWebSearch === true;
1199
+ }
1200
+
898
1201
  function fastPreferenceFor(config, provider, model) {
899
1202
  const key = routeFastKey(provider, model);
900
1203
  if (!key) return false;
@@ -903,10 +1206,10 @@ function fastPreferenceFor(config, provider, model) {
903
1206
  return config?.fastModels?.[key] === true;
904
1207
  }
905
1208
 
906
- function saveModelSettings(cfgMod, route, { fastCapable = true } = {}) {
1209
+ function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfig = null } = {}) {
907
1210
  const key = routeFastKey(route?.provider, route?.model);
908
- if (!key) return cfgMod.loadConfig();
909
- const nextConfig = cfgMod.loadConfig();
1211
+ if (!key) return baseConfig || cfgMod.loadConfig();
1212
+ const nextConfig = baseConfig || cfgMod.loadConfig();
910
1213
  const modelSettings = { ...(nextConfig.modelSettings || {}) };
911
1214
  const nextSetting = { ...(modelSettings[key] || {}) };
912
1215
  if (hasOwn(route, 'effort') && route.effort) nextSetting.effort = route.effort;
@@ -948,17 +1251,80 @@ function routeForStatusline(route) {
948
1251
  return out;
949
1252
  }
950
1253
 
1254
+ function writeStatuslineRoute(statusRoutes, session, route) {
1255
+ if (!session?.id || !route) return;
1256
+ const clientHostPid = session?.clientHostPid || process.pid;
1257
+ statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route), { clientHostPid });
1258
+ }
1259
+
951
1260
  const ONBOARDING_VERSION = 1;
952
- const WORKFLOW_ROUTE_SLOTS = ['lead', 'bridge', 'explorer', 'memory'];
1261
+ const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
953
1262
  const FIXED_AGENT_SLOTS = Object.freeze([
954
1263
  { id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
955
- { id: 'web-researcher', label: 'Web Researcher', description: 'External current-info research' },
956
1264
  { id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
957
1265
  { id: 'worker', label: 'Worker', description: 'Scoped implementation' },
958
1266
  { id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
959
1267
  { id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
960
1268
  { id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
961
1269
  ]);
1270
+ const SEARCH_CAPABLE_PROVIDERS = new Set([
1271
+ 'openai-oauth',
1272
+ 'openai',
1273
+ 'grok-oauth',
1274
+ 'xai',
1275
+ 'gemini',
1276
+ 'anthropic',
1277
+ 'anthropic-oauth',
1278
+ ]);
1279
+ const SEARCH_DEFAULT_PROVIDER = 'default';
1280
+ const SEARCH_DEFAULT_MODEL = 'default';
1281
+ const SEARCH_PROVIDER_ALIASES = Object.freeze({
1282
+ 'openai-api': 'openai',
1283
+ 'xai-api': 'xai',
1284
+ 'gemini-api': 'gemini',
1285
+ 'anthropic-api': 'anthropic',
1286
+ });
1287
+ const QUICK_SEARCH_MODELS = Object.freeze({
1288
+ 'openai-oauth': [
1289
+ { id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
1290
+ { id: 'gpt-5.4', display: 'GPT-5.4', latest: true, contextWindow: 1000000 },
1291
+ { id: 'gpt-5', display: 'GPT-5', contextWindow: 400000 },
1292
+ { id: 'gpt-4.1', display: 'GPT-4.1', contextWindow: 1000000 },
1293
+ ],
1294
+ openai: [
1295
+ { id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
1296
+ { id: 'gpt-5.4', display: 'GPT-5.4', latest: true, contextWindow: 1000000 },
1297
+ { id: 'gpt-5', display: 'GPT-5', contextWindow: 400000 },
1298
+ { id: 'gpt-4.1', display: 'GPT-4.1', contextWindow: 1000000 },
1299
+ { id: 'gpt-4o', display: 'GPT-4o', contextWindow: 128000 },
1300
+ ],
1301
+ 'grok-oauth': [
1302
+ { id: 'grok-4.3', display: 'Grok 4.3', latest: true, contextWindow: 1000000 },
1303
+ { id: 'grok-4.20', display: 'Grok 4.20', contextWindow: 1000000 },
1304
+ { id: 'grok-4', display: 'Grok 4', contextWindow: 256000 },
1305
+ ],
1306
+ xai: [
1307
+ { id: 'grok-4.3', display: 'Grok 4.3', latest: true, contextWindow: 1000000 },
1308
+ { id: 'grok-4.20', display: 'Grok 4.20', contextWindow: 1000000 },
1309
+ { id: 'grok-4', display: 'Grok 4', contextWindow: 256000 },
1310
+ ],
1311
+ gemini: [
1312
+ { id: 'gemini-3-pro', display: 'Gemini 3 Pro', latest: true, contextWindow: 1000000 },
1313
+ { id: 'gemini-2.5-pro', display: 'Gemini 2.5 Pro', contextWindow: 1000000 },
1314
+ { id: 'gemini-2.5-flash', display: 'Gemini 2.5 Flash', contextWindow: 1000000 },
1315
+ { id: 'gemini-2.0-flash', display: 'Gemini 2.0 Flash', contextWindow: 1000000 },
1316
+ ],
1317
+ 'anthropic-oauth': [
1318
+ { id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
1319
+ { id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
1320
+ { id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
1321
+ ],
1322
+ anthropic: [
1323
+ { id: 'claude-opus-4-8', display: 'Claude Opus 4.8', latest: true, contextWindow: 1000000 },
1324
+ { id: 'claude-sonnet-4-6', display: 'Claude Sonnet 4.6', latest: true, contextWindow: 1000000 },
1325
+ { id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
1326
+ ],
1327
+ });
962
1328
  const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
963
1329
  const agentDefinitionCache = new Map();
964
1330
  const DEFAULT_WORKFLOW_ID = 'default';
@@ -1016,12 +1382,14 @@ function readWorkflowPackFromDir(dir, source = 'built-in') {
1016
1382
  const entry = clean(manifest.entry) || 'WORKFLOW.md';
1017
1383
  const body = readTextSafe(join(dir, entry));
1018
1384
  if (!body) return null;
1385
+ const agentsConfigured = Array.isArray(manifest.agents);
1019
1386
  return {
1020
1387
  id,
1021
1388
  name: clean(manifest.name) || id,
1022
1389
  description: clean(manifest.description),
1023
1390
  entry,
1024
- agents: Array.isArray(manifest.agents) ? manifest.agents.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent)).filter(Boolean) : [],
1391
+ agentsConfigured,
1392
+ agents: agentsConfigured ? manifest.agents.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent)).filter(Boolean) : [],
1025
1393
  body,
1026
1394
  source,
1027
1395
  };
@@ -1059,6 +1427,20 @@ function loadWorkflowPack(dataDir, id) {
1059
1427
  return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in');
1060
1428
  }
1061
1429
 
1430
+ function workflowSummary(pack) {
1431
+ const id = normalizeWorkflowId(pack?.id, DEFAULT_WORKFLOW_ID);
1432
+ return {
1433
+ id,
1434
+ name: clean(pack?.name) || (id === DEFAULT_WORKFLOW_ID ? 'Default' : id),
1435
+ description: clean(pack?.description),
1436
+ source: clean(pack?.source),
1437
+ };
1438
+ }
1439
+
1440
+ function activeWorkflowSummary(config, dataDir) {
1441
+ return workflowSummary(loadWorkflowPack(dataDir, activeWorkflowId(config)));
1442
+ }
1443
+
1062
1444
  function loadAgentDefinition(dataDir, id) {
1063
1445
  const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
1064
1446
  if (!agentId) return null;
@@ -1067,20 +1449,22 @@ function loadAgentDefinition(dataDir, id) {
1067
1449
  for (const dir of agentSourceDirs(dataDir, agentId)) {
1068
1450
  const manifest = readJsonSafe(join(dir, 'agent.json')) || {};
1069
1451
  const entry = clean(manifest.entry) || 'AGENT.md';
1070
- const body = readTextSafe(join(dir, entry));
1452
+ const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
1453
+ const body = doc.body;
1071
1454
  if (!body) continue;
1072
1455
  const definition = {
1073
1456
  id: agentId,
1074
1457
  name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
1075
1458
  description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
1459
+ permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
1460
+ frontmatter: doc.frontmatter,
1076
1461
  body,
1077
1462
  };
1078
1463
  agentDefinitionCache.set(cacheKey, definition);
1079
1464
  return definition;
1080
1465
  }
1081
- const legacyBody = readTextSafe(join(dataDir || STANDALONE_DATA_DIR, 'roles', `${agentId}.md`))
1082
- || readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`));
1083
- if (!legacyBody) {
1466
+ const legacyDoc = readMarkdownDocument(readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`)));
1467
+ if (!legacyDoc.body) {
1084
1468
  agentDefinitionCache.set(cacheKey, null);
1085
1469
  return null;
1086
1470
  }
@@ -1088,7 +1472,9 @@ function loadAgentDefinition(dataDir, id) {
1088
1472
  id: agentId,
1089
1473
  name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
1090
1474
  description: '',
1091
- body: legacyBody,
1475
+ permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
1476
+ frontmatter: legacyDoc.frontmatter,
1477
+ body: legacyDoc.body,
1092
1478
  };
1093
1479
  agentDefinitionCache.set(cacheKey, definition);
1094
1480
  return definition;
@@ -1103,7 +1489,7 @@ function workflowContextBlock(config, dataDir) {
1103
1489
  if (pack.description) lines.push(pack.description);
1104
1490
  lines.push(pack.body);
1105
1491
 
1106
- const agentIds = pack.agents.length ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
1492
+ const agentIds = pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
1107
1493
  const agentBlocks = agentIds
1108
1494
  .map((id) => loadAgentDefinition(dataDir, id))
1109
1495
  .filter(Boolean);
@@ -1122,6 +1508,11 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
1122
1508
  const provider = clean(routeLike?.provider) || clean(fallback.provider);
1123
1509
  const model = clean(routeLike?.model) || clean(fallback.model);
1124
1510
  if (!provider || !model) return null;
1511
+ // Defensive: a workflow/agent route must carry a real model id. Reject values
1512
+ // that are obviously free-form text (whitespace, prose) so a bad string can
1513
+ // never be persisted as a preset/workflow route. Normal model ids pass
1514
+ // isLikelyRawModelId unchanged.
1515
+ if (!isLikelyRawModelId(model)) return null;
1125
1516
  const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
1126
1517
  const fast = routeLike?.fast ?? fallback.fast;
1127
1518
  return {
@@ -1132,6 +1523,41 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
1132
1523
  };
1133
1524
  }
1134
1525
 
1526
+ function normalizeSearchProviderId(provider) {
1527
+ const id = clean(provider);
1528
+ return SEARCH_PROVIDER_ALIASES[id] || id;
1529
+ }
1530
+
1531
+ function isDefaultSearchRouteConfig(routeLike = {}) {
1532
+ return normalizeSearchProviderId(routeLike?.provider) === SEARCH_DEFAULT_PROVIDER
1533
+ && clean(routeLike?.model).toLowerCase() === SEARCH_DEFAULT_MODEL;
1534
+ }
1535
+
1536
+ function isSearchCapableProvider(provider) {
1537
+ return SEARCH_CAPABLE_PROVIDERS.has(normalizeSearchProviderId(provider));
1538
+ }
1539
+
1540
+ function normalizeSearchRouteConfig(routeLike, fallback = {}) {
1541
+ const provider = normalizeSearchProviderId(routeLike?.provider || fallback.provider);
1542
+ const model = clean(routeLike?.model || fallback.model);
1543
+ if (!provider || !model) return null;
1544
+ let effort = null;
1545
+ try {
1546
+ effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
1547
+ } catch {
1548
+ effort = null;
1549
+ }
1550
+ const fast = routeLike?.fast ?? fallback.fast;
1551
+ const toolType = clean(routeLike?.toolType || fallback.toolType);
1552
+ return {
1553
+ provider,
1554
+ model,
1555
+ ...(effort ? { effort } : {}),
1556
+ ...(fast === true ? { fast: true } : {}),
1557
+ ...(toolType ? { toolType } : {}),
1558
+ };
1559
+ }
1560
+
1135
1561
  function upsertWorkflowPreset(presets, slot, routeLike) {
1136
1562
  const route = normalizeWorkflowRoute(routeLike);
1137
1563
  if (!route) return presets;
@@ -1139,7 +1565,7 @@ function upsertWorkflowPreset(presets, slot, routeLike) {
1139
1565
  const preset = {
1140
1566
  id,
1141
1567
  name: workflowPresetName(slot),
1142
- type: 'bridge',
1568
+ type: 'agent',
1143
1569
  provider: route.provider,
1144
1570
  model: route.model,
1145
1571
  ...(route.effort ? { effort: route.effort } : {}),
@@ -1161,24 +1587,19 @@ function summarizeWorkflowRoutes(config) {
1161
1587
  return out;
1162
1588
  }
1163
1589
 
1164
- function legacyWorkflowRolePreset(dataDir, role) {
1165
- try {
1166
- const file = join(dataDir, 'user-workflow.json');
1167
- if (!existsSync(file)) return '';
1168
- const raw = JSON.parse(readFileSync(file, 'utf8'));
1169
- const found = (raw?.roles || []).find((item) => clean(item?.name) === role);
1170
- return clean(found?.preset);
1171
- } catch {
1172
- return '';
1590
+ function routeFromPreset(config, slotValue) {
1591
+ // Maintenance slots now store a direct {provider, model} route. Accept that
1592
+ // shape first; fall back to the legacy preset-NAME string lookup so configs
1593
+ // written before the route migration still resolve.
1594
+ if (slotValue && typeof slotValue === 'object' && !Array.isArray(slotValue)) {
1595
+ const direct = normalizeWorkflowRoute(slotValue);
1596
+ if (direct) return direct;
1173
1597
  }
1174
- }
1175
-
1176
- function routeFromPreset(config, presetName) {
1177
- const preset = findPreset(config, presetName);
1598
+ const preset = findPreset(config, slotValue);
1178
1599
  return preset ? normalizeWorkflowRoute(preset) : null;
1179
1600
  }
1180
1601
 
1181
- function agentRouteFromConfig(config, agentId, dataDir) {
1602
+ function agentRouteFromConfig(config, agentId, _dataDir) {
1182
1603
  const id = normalizeAgentId(agentId);
1183
1604
  if (!id) return null;
1184
1605
  const explicit = normalizeWorkflowRoute(config?.agents?.[id])
@@ -1194,7 +1615,7 @@ function agentRouteFromConfig(config, agentId, dataDir) {
1194
1615
  if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
1195
1616
  if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
1196
1617
 
1197
- return routeFromPreset(config, legacyWorkflowRolePreset(dataDir, id));
1618
+ return null;
1198
1619
  }
1199
1620
 
1200
1621
  function toolResponseText(result) {
@@ -1233,17 +1654,26 @@ function currentSessionRecallRows(session, query, { limit = 10 } = {}) {
1233
1654
 
1234
1655
  function parseToolSelection(value) {
1235
1656
  if (Array.isArray(value)) return value.map(clean).filter(Boolean);
1236
- return String(value || '')
1657
+ if (value && typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
1658
+ return [...value].map(clean).filter(Boolean);
1659
+ }
1660
+ return String(value || '').replace(/^select\s*:/i, '')
1237
1661
  .split(/[,\s]+/)
1238
1662
  .map(clean)
1239
1663
  .filter(Boolean);
1240
1664
  }
1241
1665
 
1666
+ function parseToolSearchQuerySelection(query) {
1667
+ const match = clean(query).match(/^select\s*:\s*(.+)$/i);
1668
+ return match ? parseToolSelection(match[1]) : [];
1669
+ }
1670
+
1242
1671
  function toolKind(tool) {
1243
1672
  const name = clean(tool?.name);
1244
1673
  if (name.startsWith('mcp__')) return 'mcp';
1245
- if (name.startsWith('skill_') || name === 'skills_list') return 'skill';
1246
- if (tool?.annotations?.bridgeHidden) return 'control';
1674
+ if (name.startsWith('skill:') || tool?.annotations?.mixdogKind === 'skill') return 'skill';
1675
+ if (name === 'Skill' || name.startsWith('skill_') || name === 'skills_list' || name === 'skill_view') return 'skill';
1676
+ if (tool?.annotations?.agentHidden) return 'control';
1247
1677
  if (['apply_patch', 'shell'].includes(name)) return 'mutation';
1248
1678
  return 'tool';
1249
1679
  }
@@ -1255,9 +1685,9 @@ function toolSchemaBucket(tool) {
1255
1685
  if (kind === 'skill') return 'skills';
1256
1686
  if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
1257
1687
  if (name === 'search' || name === 'web_fetch') return 'web';
1258
- if (['read', 'grep', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
1688
+ if (['read', 'grep', 'find', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
1259
1689
  if (['shell', 'apply_patch'].includes(name)) return 'mutation';
1260
- if (name === 'bridge' || name === 'delegate' || name.includes('bridge')) return 'agents';
1690
+ if (name === 'agent' || name === 'delegate') return 'agents';
1261
1691
  if (name.includes('channel') || name.includes('discord') || name.includes('webhook')) return 'channels';
1262
1692
  if (name.includes('provider') || name === 'tool_search' || name === 'cwd') return 'setup';
1263
1693
  return 'other';
@@ -1304,6 +1734,17 @@ function activeToolForSurface(tool) {
1304
1734
  return JSON.parse(JSON.stringify(tool));
1305
1735
  }
1306
1736
 
1737
+ function deferredProviderMode(provider) {
1738
+ const p = clean(provider).toLowerCase();
1739
+ if (p === 'gemini') return 'full';
1740
+ if (p === 'anthropic' || p === 'anthropic-oauth'
1741
+ || p === 'openai' || p === 'openai-oauth'
1742
+ || p === 'xai' || p === 'grok-oauth') {
1743
+ return 'native';
1744
+ }
1745
+ return 'legacy';
1746
+ }
1747
+
1307
1748
  function filterDisallowedTools(tools, disallowed = []) {
1308
1749
  if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
1309
1750
  const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
@@ -1324,24 +1765,14 @@ function sortedNamesByMeasuredUsage(names) {
1324
1765
  }
1325
1766
 
1326
1767
  export function defaultDeferredToolNames(catalog, mode) {
1768
+ const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
1327
1769
  if (mode === 'lead') {
1328
- const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
1329
1770
  return new Set(DEFERRED_DEFAULT_LEAD_TOOLS.filter((name) => available.has(name)));
1330
1771
  }
1331
1772
  if (mode === 'readonly') {
1332
- const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
1333
1773
  return new Set(DEFERRED_DEFAULT_READONLY_TOOLS.filter((name) => available.has(name)));
1334
1774
  }
1335
- const names = new Set(DEFERRED_ALWAYS_ACTIVE_TOOLS);
1336
- const limit = DEFERRED_DEFAULT_FULL_LIMIT;
1337
- for (const tool of sortedCatalogByMeasuredUsage(catalog)) {
1338
- const name = clean(tool?.name);
1339
- if (!name || names.has(name)) continue;
1340
- if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
1341
- names.add(name);
1342
- if (names.size >= DEFERRED_ALWAYS_ACTIVE_TOOLS.size + limit) break;
1343
- }
1344
- return names;
1775
+ return new Set(DEFERRED_DEFAULT_FULL_TOOLS.filter((name) => available.has(name)));
1345
1776
  }
1346
1777
 
1347
1778
  export function compactToolSearchDescription(value, max = 220) {
@@ -1360,25 +1791,180 @@ function toolRow(tool, activeNames = new Set()) {
1360
1791
  };
1361
1792
  }
1362
1793
 
1794
+ function providerSupportsResponsesCustomTools(provider) {
1795
+ const p = clean(provider).toLowerCase();
1796
+ if (!p) return true;
1797
+ return p === 'openai' || p === 'openai-oauth';
1798
+ }
1799
+
1800
+ function openAILoadableToolSpec(tool, provider = '') {
1801
+ if (providerSupportsResponsesCustomTools(provider) && isResponsesFreeformTool(tool)) return toResponsesCustomTool(tool);
1802
+ return {
1803
+ type: 'function',
1804
+ name: clean(tool?.name),
1805
+ description: clean(tool?.description),
1806
+ defer_loading: true,
1807
+ parameters: tool?.inputSchema && typeof tool.inputSchema === 'object'
1808
+ ? tool.inputSchema
1809
+ : { type: 'object', properties: {} },
1810
+ };
1811
+ }
1812
+
1813
+ function toolSearchNativePayload(catalog, names, provider = '') {
1814
+ const selected = new Set((names || []).map(clean).filter(Boolean));
1815
+ if (!selected.size) return null;
1816
+ const tools = [];
1817
+ const refs = [];
1818
+ for (const tool of catalog || []) {
1819
+ const name = clean(tool?.name);
1820
+ if (!name || !selected.has(name)) continue;
1821
+ refs.push(name);
1822
+ tools.push(openAILoadableToolSpec(tool, provider));
1823
+ }
1824
+ if (!refs.length) return null;
1825
+ return {
1826
+ toolReferences: refs,
1827
+ openaiTools: tools,
1828
+ summary: `Loaded deferred tools: ${refs.join(', ')}`,
1829
+ };
1830
+ }
1831
+
1363
1832
  function toolSearchTokens(value) {
1364
1833
  return (clean(value).toLowerCase().match(/[a-z0-9_.-]+/g) || [])
1365
1834
  .map((token) => token.replace(/[-.]+/g, '_'))
1366
1835
  .filter(Boolean);
1367
1836
  }
1368
1837
 
1838
+ function toolSearchMeaningfulTokens(value) {
1839
+ return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
1840
+ }
1841
+
1369
1842
  function toolSearchText(row) {
1370
1843
  const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
1371
1844
  return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
1372
1845
  }
1373
1846
 
1847
+ function toolSearchRowAliases(name) {
1848
+ return TOOL_SEARCH_ROW_ALIASES[clean(name)] || [];
1849
+ }
1850
+
1851
+ function toolSearchRank(row, query) {
1852
+ const raw = clean(query).toLowerCase();
1853
+ if (!raw) return { score: 0, reasons: [] };
1854
+ const name = clean(row?.name).toLowerCase();
1855
+ const prettyName = name.replace(/_/g, ' ');
1856
+ const haystack = toolSearchText(row);
1857
+ const aliases = toolSearchRowAliases(name);
1858
+ const aliasText = aliases.join(' ').toLowerCase();
1859
+ const queryTokens = toolSearchMeaningfulTokens(raw);
1860
+ const nameTokens = new Set(toolSearchTokens(`${name} ${prettyName}`));
1861
+ const aliasTokens = new Set(toolSearchTokens(aliasText));
1862
+ let score = 0;
1863
+ const reasons = [];
1864
+ if (raw === name || raw === prettyName) {
1865
+ score += 120;
1866
+ reasons.push('exact-name');
1867
+ }
1868
+ if (aliases.some((alias) => raw === alias || raw === alias.replace(/[_-]+/g, ' '))) {
1869
+ score += 100;
1870
+ reasons.push('exact-alias');
1871
+ }
1872
+ if (haystack.includes(raw)) {
1873
+ score += 34;
1874
+ reasons.push('phrase');
1875
+ }
1876
+ for (const alias of aliases) {
1877
+ const normalizedAlias = alias.toLowerCase();
1878
+ if (normalizedAlias && raw.includes(normalizedAlias)) {
1879
+ score += 58;
1880
+ reasons.push('alias-phrase');
1881
+ break;
1882
+ }
1883
+ }
1884
+ let matchedTokens = 0;
1885
+ for (const token of queryTokens) {
1886
+ if (nameTokens.has(token) || name.includes(token)) {
1887
+ score += 24;
1888
+ matchedTokens += 1;
1889
+ continue;
1890
+ }
1891
+ if (aliasTokens.has(token) || aliasText.includes(token)) {
1892
+ score += 18;
1893
+ matchedTokens += 1;
1894
+ continue;
1895
+ }
1896
+ if (haystack.includes(token)) {
1897
+ score += 7;
1898
+ matchedTokens += 1;
1899
+ }
1900
+ }
1901
+ if (queryTokens.length && matchedTokens === queryTokens.length) {
1902
+ score += Math.min(18, queryTokens.length * 6);
1903
+ reasons.push('all-tokens');
1904
+ }
1905
+ if (clean(row?.kind).toLowerCase() === raw) score += 10;
1906
+ if (score > 0) score += Math.min(6, Math.floor(measuredToolUsage(name) / 150));
1907
+ return { score, reasons };
1908
+ }
1909
+
1374
1910
  function toolSearchMatches(row, query) {
1375
1911
  const raw = clean(query).toLowerCase();
1376
1912
  if (!raw) return true;
1377
- const haystack = toolSearchText(row);
1378
- if (haystack.includes(raw)) return true;
1379
- const tokens = toolSearchTokens(raw);
1380
- if (tokens.length === 0) return haystack.includes(raw);
1381
- return tokens.some((token) => haystack.includes(token));
1913
+ return toolSearchRank(row, raw).score > 0;
1914
+ }
1915
+
1916
+ function rankedToolSearchRows(rows, query) {
1917
+ const raw = clean(query).toLowerCase();
1918
+ if (!raw) return rows;
1919
+ return rows
1920
+ .map((row) => {
1921
+ const ranked = toolSearchRank(row, raw);
1922
+ return { ...row, score: ranked.score, reasons: ranked.reasons };
1923
+ })
1924
+ .filter((row) => row.score > 0)
1925
+ .sort((a, b) => {
1926
+ if (b.score !== a.score) return b.score - a.score;
1927
+ if (a.active !== b.active) return a.active ? 1 : -1;
1928
+ if (b.usage !== a.usage) return b.usage - a.usage;
1929
+ return String(a.name).localeCompare(String(b.name));
1930
+ });
1931
+ }
1932
+
1933
+ function queryHasAnyPhrase(query, phrases) {
1934
+ const text = clean(query).toLowerCase().replace(/[_-]+/g, ' ');
1935
+ return phrases.some((phrase) => text.includes(phrase));
1936
+ }
1937
+
1938
+ function autoToolSelectionNames(query, rows) {
1939
+ const raw = clean(query).toLowerCase();
1940
+ if (!raw || TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES.has(raw)) return [];
1941
+ if (TOOL_SEARCH_SAFE_AUTO_ALIASES.has(raw) && DEFERRED_SELECT_ALIASES[raw]) {
1942
+ return DEFERRED_SELECT_ALIASES[raw];
1943
+ }
1944
+ if (queryHasAnyPhrase(raw, ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status', '기억 저장', '메모리 저장'])) {
1945
+ return ['memory'];
1946
+ }
1947
+ if (queryHasAnyPhrase(raw, ['run', 'execute', 'test', 'tests', 'build', 'terminal', 'command', 'powershell', 'bash', 'shell', 'npm', 'node', 'git', '실행', '테스트', '빌드', '터미널', '쉘'])) {
1948
+ return ['shell', 'task'];
1949
+ }
1950
+ if (queryHasAnyPhrase(raw, ['web', 'internet', 'online', 'current info', 'latest', 'news', 'browse', 'docs', 'documentation', '웹', '인터넷', '최신', '뉴스', '문서'])) {
1951
+ return ['search', 'web_fetch'];
1952
+ }
1953
+ if (queryHasAnyPhrase(raw, ['recall', 'remember', 'memory previous', 'previous', 'history', 'past', 'prior', 'earlier', 'resume', '이전', '기억', '히스토리'])) {
1954
+ return ['recall'];
1955
+ }
1956
+ if (queryHasAnyPhrase(raw, ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer', '에이전트', '워커', '병렬'])) {
1957
+ return ['agent'];
1958
+ }
1959
+ if (queryHasAnyPhrase(raw, ['working directory', 'project root', 'current directory', 'cwd'])) {
1960
+ return ['cwd'];
1961
+ }
1962
+ const ranked = rankedToolSearchRows(rows, raw);
1963
+ const top = ranked[0];
1964
+ if (!top) return [];
1965
+ const reasons = new Set(top.reasons || []);
1966
+ if (reasons.has('exact-name') || reasons.has('exact-alias')) return [top.name];
1967
+ return [];
1382
1968
  }
1383
1969
 
1384
1970
  function expandSelectionNames(names) {
@@ -1393,6 +1979,39 @@ function expandSelectionNames(names) {
1393
1979
  return [...new Set(out)];
1394
1980
  }
1395
1981
 
1982
+ function storedDeferredToolNames(session) {
1983
+ for (const source of [session?.deferredDiscoveredTools, session?.deferredSelectedTools]) {
1984
+ const names = parseToolSelection(source);
1985
+ if (names.length) return names;
1986
+ }
1987
+ return [];
1988
+ }
1989
+
1990
+ function canonicalDeferredToolNames(catalog, names) {
1991
+ const byName = new Map();
1992
+ for (const tool of catalog || []) {
1993
+ const name = clean(tool?.name);
1994
+ if (!name) continue;
1995
+ byName.set(name, name);
1996
+ byName.set(name.toLowerCase(), name);
1997
+ }
1998
+ const out = [];
1999
+ for (const raw of expandSelectionNames(names)) {
2000
+ const name = clean(raw);
2001
+ const canonical = byName.get(name) || byName.get(name.toLowerCase());
2002
+ if (canonical) out.push(canonical);
2003
+ }
2004
+ return sortedNamesByMeasuredUsage(new Set(out));
2005
+ }
2006
+
2007
+ function setDeferredToolState(session, names) {
2008
+ if (!session) return [];
2009
+ const selected = sortedNamesByMeasuredUsage(new Set(parseToolSelection(names)));
2010
+ session.deferredDiscoveredTools = selected;
2011
+ session.deferredSelectedTools = selected;
2012
+ return selected;
2013
+ }
2014
+
1396
2015
  function isReadonlySelectable(tool) {
1397
2016
  const name = clean(tool?.name);
1398
2017
  if (READONLY_TOOL_NAMES.has(name)) return true;
@@ -1402,8 +2021,9 @@ function isReadonlySelectable(tool) {
1402
2021
  return false;
1403
2022
  }
1404
2023
 
1405
- function applyDeferredToolSurface(session, mode, extraTools = []) {
2024
+ function applyDeferredToolSurface(session, mode, extraTools = [], options = {}) {
1406
2025
  if (!session || !Array.isArray(session.tools)) return session;
2026
+ const providerMode = deferredProviderMode(options.provider || session.provider);
1407
2027
  const byName = new Map();
1408
2028
  for (const tool of [...session.tools, ...(extraTools || [])]) {
1409
2029
  const name = clean(tool?.name);
@@ -1412,14 +2032,34 @@ function applyDeferredToolSurface(session, mode, extraTools = []) {
1412
2032
  }
1413
2033
  const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
1414
2034
  const defaultNames = defaultDeferredToolNames(catalog, mode);
2035
+ const storedNames = providerMode === 'native' ? [] : storedDeferredToolNames(session);
2036
+ let selectedNames = providerMode === 'full'
2037
+ ? sortedNamesByMeasuredUsage(catalog.map((tool) => clean(tool?.name)).filter(Boolean))
2038
+ : [];
2039
+ if (providerMode !== 'full') {
2040
+ selectedNames = storedNames.length ? canonicalDeferredToolNames(catalog, storedNames) : [];
2041
+ if (!selectedNames.length || providerMode === 'native') selectedNames = sortedNamesByMeasuredUsage(defaultNames);
2042
+ }
2043
+ const selected = new Set(selectedNames);
1415
2044
  session.deferredToolCatalog = catalog;
1416
2045
  session.deferredToolUsage = MEASURED_TOOL_USAGE;
1417
- session.deferredSelectedTools = sortedNamesByMeasuredUsage(defaultNames);
2046
+ session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
2047
+ session.deferredProviderMode = providerMode;
2048
+ session.deferredNativeTools = providerMode === 'native';
1418
2049
  session.tools.length = 0;
2050
+ const active = [];
1419
2051
  for (const tool of catalog) {
1420
- if (!defaultNames.has(clean(tool?.name))) continue;
2052
+ if (!selected.has(clean(tool?.name))) continue;
1421
2053
  if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
1422
2054
  session.tools.push(tool);
2055
+ active.push(clean(tool?.name));
2056
+ }
2057
+ if (providerMode === 'native') {
2058
+ const discovered = canonicalDeferredToolNames(catalog, session.deferredDiscoveredTools || []);
2059
+ session.deferredSelectedTools = active;
2060
+ session.deferredDiscoveredTools = discovered.filter((name) => !selected.has(name));
2061
+ } else {
2062
+ setDeferredToolState(session, active);
1423
2063
  }
1424
2064
  return session;
1425
2065
  }
@@ -1428,57 +2068,106 @@ function selectDeferredTools(session, names, mode) {
1428
2068
  const catalog = Array.isArray(session?.deferredToolCatalog)
1429
2069
  ? session.deferredToolCatalog
1430
2070
  : (Array.isArray(session?.tools) ? session.tools : []);
1431
- const active = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
1432
- const byName = new Map(catalog.map((tool) => [tool?.name, tool]).filter(([name]) => name));
2071
+ const active = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
2072
+ const native = session?.deferredProviderMode === 'native' || session?.deferredNativeTools === true;
2073
+ const discovered = new Set(Array.isArray(session?.deferredDiscoveredTools) ? session.deferredDiscoveredTools : []);
2074
+ const byName = new Map();
2075
+ for (const tool of catalog) {
2076
+ const name = clean(tool?.name);
2077
+ if (!name) continue;
2078
+ byName.set(name, tool);
2079
+ byName.set(name.toLowerCase(), tool);
2080
+ }
1433
2081
  const added = [];
1434
2082
  const already = [];
1435
2083
  const blocked = [];
1436
2084
  const missing = [];
1437
- for (const name of expandSelectionNames(names)) {
1438
- const tool = byName.get(name);
2085
+ for (const rawName of expandSelectionNames(names)) {
2086
+ const requestedName = clean(rawName);
2087
+ const tool = byName.get(requestedName) || byName.get(requestedName.toLowerCase());
2088
+ const name = clean(tool?.name);
1439
2089
  if (!tool) {
1440
- missing.push(name);
2090
+ missing.push(requestedName);
1441
2091
  continue;
1442
2092
  }
1443
2093
  if (mode === 'readonly' && !isReadonlySelectable(tool)) {
1444
2094
  blocked.push({ name, reason: 'readonly mode' });
1445
2095
  continue;
1446
2096
  }
1447
- if (active.has(name)) {
2097
+ if (active.has(name) || discovered.has(name)) {
1448
2098
  already.push(name);
1449
2099
  continue;
1450
2100
  }
1451
- session.tools.push(tool);
1452
- active.add(name);
2101
+ if (native) {
2102
+ discovered.add(name);
2103
+ } else {
2104
+ session.tools.push(tool);
2105
+ active.add(name);
2106
+ }
1453
2107
  added.push(name);
1454
2108
  }
1455
- session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
1456
- return { added, already, blocked, missing };
2109
+ if (native) {
2110
+ session.deferredDiscoveredTools = sortedNamesByMeasuredUsage(discovered);
2111
+ session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
2112
+ } else {
2113
+ setDeferredToolState(session, active);
2114
+ }
2115
+ return { added, already, blocked, missing, native };
1457
2116
  }
1458
2117
 
1459
2118
  function renderToolSearch(args = {}, session, mode = 'full') {
1460
2119
  const catalog = Array.isArray(session?.deferredToolCatalog)
1461
2120
  ? session.deferredToolCatalog
1462
2121
  : (Array.isArray(session?.tools) ? session.tools : []);
1463
- const activeNames = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
1464
- const query = clean(args.query).toLowerCase();
1465
- const selectedNames = parseToolSelection(args.select);
2122
+ const rawQuery = clean(args.query);
2123
+ const explicitSelectedNames = parseToolSelection(args.select);
2124
+ const querySelectedNames = explicitSelectedNames.length ? [] : parseToolSearchQuerySelection(rawQuery);
2125
+ const forcedSelectedNames = explicitSelectedNames.length ? explicitSelectedNames : querySelectedNames;
2126
+ const query = querySelectedNames.length ? '' : rawQuery.toLowerCase();
1466
2127
  const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
1467
- const selection = selectedNames.length ? selectDeferredTools(session, selectedNames, mode) : null;
1468
- const nextActiveNames = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
1469
- const rows = catalog.map((tool) => toolRow(tool, nextActiveNames)).filter((row) => row.name);
2128
+ const initialActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
2129
+ const initialRows = catalog.map((tool) => toolRow(tool, initialActiveNames)).filter((row) => row.name);
2130
+ const autoSelectedNames = (!forcedSelectedNames.length && query)
2131
+ ? autoToolSelectionNames(query, initialRows)
2132
+ : [];
2133
+ const selectedNames = forcedSelectedNames.length ? forcedSelectedNames : autoSelectedNames;
2134
+ const toolSelection = selectedNames.length ? selectDeferredTools(session, selectedNames, mode) : null;
2135
+ const selectionMode = forcedSelectedNames.length ? 'select' : (autoSelectedNames.length ? 'auto' : null);
2136
+ const nextActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
2137
+ const rows = [
2138
+ ...catalog.map((tool) => toolRow(tool, nextActiveNames)),
2139
+ ].filter((row) => row.name);
1470
2140
  const matches = query
1471
- ? rows.filter((row) => toolSearchMatches(row, query))
2141
+ ? rankedToolSearchRows(rows, query)
1472
2142
  : rows;
2143
+ const selected = toolSelection
2144
+ ? {
2145
+ mode: selectionMode,
2146
+ tools: toolSelection,
2147
+ }
2148
+ : null;
2149
+ const nativeToolSearch = toolSelection?.native
2150
+ ? toolSearchNativePayload(catalog, toolSelection.added, session?.provider)
2151
+ : null;
1473
2152
  return JSON.stringify({
1474
- selected: selection,
2153
+ selected,
2154
+ ...(nativeToolSearch ? { nativeToolSearch } : {}),
1475
2155
  totalMatches: matches.length,
1476
2156
  matches: matches.slice(0, limit),
1477
2157
  activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
1478
- note: 'standalone: tool_search adds deferred tools to the current session schema for the next model iteration.',
2158
+ discoveredTools: sortedNamesByMeasuredUsage(session?.deferredDiscoveredTools || []),
2159
+ note: 'query ranks matches and auto-loads high-confidence deferred tools; select exact tool names, or query select:a,b, to force load.',
1479
2160
  }, null, 2);
1480
2161
  }
1481
2162
 
2163
+ export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full') {
2164
+ return renderToolSearch(args, session, mode);
2165
+ }
2166
+
2167
+ export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
2168
+ return saveModelSettings(cfgMod, route, options);
2169
+ }
2170
+
1482
2171
  function resolveCwdPath(currentCwd, value) {
1483
2172
  const raw = clean(value);
1484
2173
  if (!raw) throw new Error('cwd: path is required for action=set');
@@ -1501,7 +2190,6 @@ export async function createMixdogSessionRuntime({
1501
2190
  rootDir: STANDALONE_ROOT,
1502
2191
  dataDir: STANDALONE_DATA_DIR,
1503
2192
  });
1504
- ensureProjectMixdogMd({ cwd });
1505
2193
  bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
1506
2194
 
1507
2195
  const importsStartedAt = performance.now();
@@ -1533,18 +2221,51 @@ export async function createMixdogSessionRuntime({
1533
2221
  profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
1534
2222
  ]);
1535
2223
  bootProfile('imports:ready', { ms: (performance.now() - importsStartedAt).toFixed(1) });
2224
+ const pluginDataDir = cfgMod.getPluginData();
2225
+ const memoryRuntime = createStandaloneMemoryRuntime({
2226
+ entry: join(STANDALONE_ROOT, MEMORY_RUNTIME.replace(/^\.\//, '')),
2227
+ dataDir: pluginDataDir,
2228
+ cwd,
2229
+ });
1536
2230
  let memoryModPromise = null;
1537
- let memoryInitPromise = null;
1538
2231
  let searchModPromise = null;
1539
2232
  let codeGraphModPromise = null;
2233
+ let outputStyleStatusCache = null;
2234
+ let outputStyleStatusCacheAt = 0;
2235
+ let outputStyleStatusCacheDir = '';
2236
+
2237
+ const memoryEnabled = () => moduleEnabled(config, 'memory', true);
2238
+ const channelsEnabled = () => moduleEnabled(config, 'channels', true);
2239
+ const getOutputStyleStatusCached = ({ fresh = false } = {}) => {
2240
+ const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
2241
+ const cacheDir = resolve(dataDir);
2242
+ const now = performance.now();
2243
+ if (
2244
+ !fresh
2245
+ && outputStyleStatusCache
2246
+ && outputStyleStatusCacheDir === cacheDir
2247
+ && now - outputStyleStatusCacheAt < 2500
2248
+ ) {
2249
+ return outputStyleStatusCache;
2250
+ }
2251
+ outputStyleStatusCache = outputStyleStatus(dataDir);
2252
+ outputStyleStatusCacheAt = now;
2253
+ outputStyleStatusCacheDir = cacheDir;
2254
+ return outputStyleStatusCache;
2255
+ };
2256
+ const invalidateOutputStyleStatusCache = () => {
2257
+ outputStyleStatusCache = null;
2258
+ outputStyleStatusCacheAt = 0;
2259
+ outputStyleStatusCacheDir = '';
2260
+ };
1540
2261
 
1541
2262
  async function getMemoryModule() {
2263
+ if (!memoryEnabled()) throw new Error('memory is disabled in settings');
1542
2264
  const startedAt = performance.now();
1543
- memoryModPromise ??= import(MEMORY_RUNTIME);
2265
+ memoryModPromise ??= Promise.resolve(memoryRuntime);
1544
2266
  const mod = await memoryModPromise;
1545
2267
  if (typeof mod?.init === 'function') {
1546
- memoryInitPromise ??= mod.init();
1547
- await memoryInitPromise;
2268
+ await mod.init();
1548
2269
  }
1549
2270
  bootProfile('memory-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
1550
2271
  return mod;
@@ -1558,6 +2279,211 @@ export async function createMixdogSessionRuntime({
1558
2279
  return mod;
1559
2280
  }
1560
2281
 
2282
+ function normalizeSearchAllowedDomain(site) {
2283
+ const raw = clean(site);
2284
+ if (!raw) return '';
2285
+ try {
2286
+ return new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`).hostname.toLowerCase();
2287
+ } catch {
2288
+ return raw.replace(/^https?:\/\//i, '').split('/')[0].toLowerCase();
2289
+ }
2290
+ }
2291
+
2292
+ function nativeSearchUserLocation(locale) {
2293
+ if (!locale || typeof locale !== 'object' || Array.isArray(locale)) return null;
2294
+ const location = { type: 'approximate' };
2295
+ for (const key of ['country', 'region', 'city', 'timezone']) {
2296
+ const value = clean(locale[key]);
2297
+ if (value) location[key] = value;
2298
+ }
2299
+ return Object.keys(location).length > 1 ? location : null;
2300
+ }
2301
+
2302
+ function nativeSearchTool(args = {}, toolType = 'web_search', providerId = '') {
2303
+ const providerName = normalizeSearchProviderId(providerId);
2304
+ const domain = normalizeSearchAllowedDomain(args.site);
2305
+ const type = clean(toolType) || 'web_search';
2306
+ const location = nativeSearchUserLocation(args.locale);
2307
+ if (providerName === 'gemini') {
2308
+ return { type: type || 'google_search' };
2309
+ }
2310
+ if (providerName === 'anthropic' || providerName === 'anthropic-oauth') {
2311
+ const tool = {
2312
+ type: 'web_search_20250305',
2313
+ name: 'web_search',
2314
+ max_uses: Math.max(1, Math.min(10, Number(args.maxResults) || 5)),
2315
+ };
2316
+ if (domain) tool.allowed_domains = [domain];
2317
+ if (location) tool.user_location = location;
2318
+ return tool;
2319
+ }
2320
+ if (providerName === 'grok-oauth' || providerName === 'xai') {
2321
+ const tool = { type };
2322
+ if (domain) tool.filters = { allowed_domains: [domain] };
2323
+ return tool;
2324
+ }
2325
+ const tool = {
2326
+ type,
2327
+ };
2328
+ if (type === 'web_search') {
2329
+ tool.search_context_size = clean(args.contextSize) || 'low';
2330
+ if (domain) tool.filters = { allowed_domains: [domain] };
2331
+ if (location) tool.user_location = location;
2332
+ }
2333
+ return tool;
2334
+ }
2335
+
2336
+ function nativeSearchToolTypes(routeLike = {}) {
2337
+ const envToolType = clean(process.env.MIXDOG_NATIVE_SEARCH_TOOL_TYPE);
2338
+ if (envToolType) return [envToolType];
2339
+ const configured = clean(routeLike.toolType);
2340
+ if (configured) return [configured];
2341
+ const providerName = normalizeSearchProviderId(routeLike.provider);
2342
+ if (providerName === 'gemini') return ['google_search'];
2343
+ if (providerName === 'anthropic' || providerName === 'anthropic-oauth') return ['web_search'];
2344
+ if (providerName === 'grok-oauth' || providerName === 'xai') return ['web_search'];
2345
+ return ['web_search', 'web_search_preview'];
2346
+ }
2347
+
2348
+ function currentMainSearchModelMeta() {
2349
+ if (!route?.provider || !route?.model) return null;
2350
+ return { ...route, id: route.model, display: route.model, name: route.model };
2351
+ }
2352
+
2353
+ function nativeSearchRoutes() {
2354
+ const cfg = ensureFullConfig();
2355
+ searchRoute = normalizeSearchRouteConfig(cfg.searchRoute) || normalizeSearchRouteConfig(searchRoute);
2356
+ if (!searchRoute) return [];
2357
+ if (isDefaultSearchRouteConfig(searchRoute)) {
2358
+ const mainModel = currentMainSearchModelMeta();
2359
+ if (!mainModel || !searchCapableFor(route.provider, mainModel)) return [];
2360
+ return [{
2361
+ key: `default\n${route.provider}\n${route.model}`,
2362
+ provider: normalizeSearchProviderId(route.provider),
2363
+ model: route.model,
2364
+ source: 'default-search-route',
2365
+ effort: route.effectiveEffort || route.effort || null,
2366
+ fast: route.fast === true,
2367
+ toolType: searchRoute.toolType || null,
2368
+ }];
2369
+ }
2370
+ const providerName = normalizeSearchProviderId(searchRoute.provider);
2371
+ if (!isSearchCapableProvider(providerName)) return [];
2372
+ return [{
2373
+ key: `${providerName}\n${searchRoute.model}`,
2374
+ provider: providerName,
2375
+ model: searchRoute.model,
2376
+ source: 'search-route',
2377
+ effort: searchRoute.effort || null,
2378
+ fast: searchRoute.fast === true,
2379
+ toolType: searchRoute.toolType || null,
2380
+ }];
2381
+ }
2382
+
2383
+ function nativeSearchMessages(searchArgs = {}) {
2384
+ const prompt = searchArgs.prompt || '';
2385
+ return [
2386
+ {
2387
+ role: 'system',
2388
+ content: [
2389
+ 'You are Mixdog native web search.',
2390
+ 'Use the hosted web_search tool for current or external facts.',
2391
+ 'Answer concisely, cite source URLs, and do not request local tools or file edits.',
2392
+ ].join('\n'),
2393
+ },
2394
+ { role: 'user', content: prompt },
2395
+ ];
2396
+ }
2397
+
2398
+ function flattenNativeSearchSources(result = {}) {
2399
+ const out = [];
2400
+ const add = (source, fallbackTitle = '') => {
2401
+ if (!source || typeof source !== 'object') return;
2402
+ const url = clean(source.url || source.uri || source.href || source.source_url);
2403
+ if (!url) return;
2404
+ out.push({
2405
+ title: clean(source.title || source.query || source.name || fallbackTitle || url),
2406
+ url,
2407
+ snippet: clean(source.snippet || source.text || source.description),
2408
+ source: source.source || 'native-web-search',
2409
+ provider: source.provider || 'native-web-search',
2410
+ });
2411
+ };
2412
+ for (const citation of Array.isArray(result.citations) ? result.citations : []) add(citation);
2413
+ for (const call of Array.isArray(result.webSearchCalls) ? result.webSearchCalls : []) {
2414
+ const action = call?.action || {};
2415
+ for (const source of Array.isArray(action.sources) ? action.sources : []) add(source, action.query || '');
2416
+ if (action.url) add({ url: action.url, title: action.query || '' });
2417
+ for (const url of Array.isArray(action.urls) ? action.urls : []) add({ url, title: action.query || '' });
2418
+ }
2419
+ const seen = new Set();
2420
+ return out.filter((item) => {
2421
+ const key = item.url || `${item.title}\n${item.snippet}`;
2422
+ if (!key || seen.has(key)) return false;
2423
+ seen.add(key);
2424
+ return true;
2425
+ });
2426
+ }
2427
+
2428
+ async function runNativeWebSearch(searchArgs = {}, { signal } = {}) {
2429
+ const candidates = nativeSearchRoutes();
2430
+ if (!candidates.length) {
2431
+ if (isDefaultSearchRouteConfig(searchRoute)) {
2432
+ throw new Error(`default search route requires the current main model to support native web search (${route?.provider || 'unknown'}/${route?.model || 'unknown'})`);
2433
+ }
2434
+ throw new Error('search route is not configured; open /search to choose a search provider/model');
2435
+ }
2436
+ const errors = [];
2437
+ for (const candidate of candidates) {
2438
+ for (const toolType of nativeSearchToolTypes(candidate)) {
2439
+ try {
2440
+ await ensureProvidersReady(ensureProviderEnabled(config, candidate.provider));
2441
+ const providerImpl = reg.getProvider(candidate.provider);
2442
+ if (!providerImpl || typeof providerImpl.send !== 'function') {
2443
+ throw new Error(`provider "${candidate.provider}" is not ready`);
2444
+ }
2445
+ const model = candidate.model;
2446
+ const searchTool = nativeSearchTool(searchArgs, toolType, candidate.provider);
2447
+ const startedAt = Date.now();
2448
+ const result = await providerImpl.send(
2449
+ nativeSearchMessages(searchArgs),
2450
+ model,
2451
+ undefined,
2452
+ {
2453
+ signal,
2454
+ role: 'web-search',
2455
+ sessionId: `${session?.id || 'search'}:native-search:${Date.now().toString(36)}`,
2456
+ sourceType: 'native-search',
2457
+ sourceName: 'search',
2458
+ nativeTools: [searchTool],
2459
+ nativeInclude: candidate.provider === 'openai' || candidate.provider === 'openai-oauth'
2460
+ ? ['web_search_call.action.sources']
2461
+ : [],
2462
+ toolChoice: candidate.provider === 'gemini' ? 'auto' : 'required',
2463
+ ...(candidate.effort ? { effort: candidate.effort } : {}),
2464
+ fast: candidate.fast === true,
2465
+ onStageChange: () => {},
2466
+ onStreamDelta: () => {},
2467
+ },
2468
+ );
2469
+ const sources = flattenNativeSearchSources(result);
2470
+ return {
2471
+ content: String(result?.content || '').trim(),
2472
+ provider: candidate.provider,
2473
+ model: result?.model || candidate.model || null,
2474
+ usage: result?.usage || null,
2475
+ citations: sources,
2476
+ webSearchCalls: result?.webSearchCalls || [],
2477
+ durationMs: Date.now() - startedAt,
2478
+ };
2479
+ } catch (err) {
2480
+ errors.push(`${candidate.provider}${candidate.model ? `/${candidate.model}` : ''}/${toolType}: ${err?.message || String(err)}`);
2481
+ }
2482
+ }
2483
+ }
2484
+ throw new Error(`native web search failed: ${errors.join(' | ')}`);
2485
+ }
2486
+
1561
2487
  async function getCodeGraphModule() {
1562
2488
  const startedAt = performance.now();
1563
2489
  codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
@@ -1578,8 +2504,7 @@ export async function createMixdogSessionRuntime({
1578
2504
  };
1579
2505
  nextConfig.default = workflowPresetId('lead');
1580
2506
 
1581
- cfgMod.saveConfig(nextConfig);
1582
- config = nextConfig;
2507
+ saveConfigAndAdopt(nextConfig);
1583
2508
  return leadRoute;
1584
2509
  }
1585
2510
 
@@ -1620,6 +2545,10 @@ export async function createMixdogSessionRuntime({
1620
2545
  async function loadCoreMemoryContext() {
1621
2546
  // Boot should not pay for memory/PG startup unless explicitly requested.
1622
2547
  // Recall and memory tools still initialize the memory service on first use.
2548
+ if (!memoryEnabled()) {
2549
+ bootProfile('core-memory:disabled');
2550
+ return '';
2551
+ }
1623
2552
  if (process.env.MIXDOG_BOOT_CORE_MEMORY !== '1') {
1624
2553
  bootProfile('core-memory:skipped');
1625
2554
  return '';
@@ -1652,32 +2581,100 @@ export async function createMixdogSessionRuntime({
1652
2581
  setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
1653
2582
  let configHasSecrets = false;
1654
2583
  let route = resolveRoute(config, { provider, model });
2584
+ let searchRoute = normalizeSearchRouteConfig(config.searchRoute);
1655
2585
  bootProfile('config:ready', { ms: (performance.now() - configStartedAt).toFixed(1) });
1656
2586
  let mode = normalizeToolMode(toolMode);
1657
2587
  let session = null;
1658
2588
  let sessionCreatePromise = null;
1659
2589
  let currentCwd = cwd;
1660
2590
  let sessionNeedsCwdRefresh = false;
1661
- const workspaceRouter = createWorkspaceRouter({ entryCwd: cwd });
1662
2591
  let closeRequested = false;
1663
2592
  let channelStartTimer = null;
2593
+ let providerSetupWarmupTimer = null;
1664
2594
  let providerWarmupTimer = null;
1665
2595
  let providerModelWarmupTimer = null;
2596
+ let codeGraphPrewarmTimer = null;
2597
+ let codeGraphPrewarmInFlight = false;
2598
+ let codeGraphPrewarmQueuedCwd = '';
2599
+ let statuslineUsageWarmupTimer = null;
2600
+ let statuslineUsageRefreshTimer = null;
1666
2601
  let activeTurnCount = 0;
1667
2602
  let firstTurnCompleted = false;
2603
+ function hookTranscriptPath(sessionId) {
2604
+ const id = clean(sessionId);
2605
+ if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) return null;
2606
+ const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
2607
+ return join(dataDir, 'sessions', `${id}.json`);
2608
+ }
2609
+ function hookEffortPayload() {
2610
+ const level = clean(route.effectiveEffort || route.effort);
2611
+ return level ? { level: level.toLowerCase() } : undefined;
2612
+ }
2613
+ function hookCommonPayload(extra = {}) {
2614
+ const sid = clean(extra.session_id || extra.sessionId || session?.id);
2615
+ return {
2616
+ ...(sid ? { session_id: sid, transcript_path: hookTranscriptPath(sid) } : {}),
2617
+ cwd: currentCwd,
2618
+ permission_mode: session?.permissionMode || 'default',
2619
+ ...(hookEffortPayload() ? { effort: hookEffortPayload() } : {}),
2620
+ ...extra,
2621
+ };
2622
+ }
1668
2623
  const sessionPrewarmDelayMs = envDelayMs('MIXDOG_SESSION_PREWARM_DELAY_MS', 50, { min: 0, max: 10_000 });
2624
+ const providerSetupWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_SETUP_WARMUP_DELAY_MS', 300, { min: 0, max: 60_000 });
1669
2625
  const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
1670
- const providerModelWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS', 15_000, { min: 0, max: 120_000 });
2626
+ // Background model-catalog prefetch delay. Kept short so the first `/model`
2627
+ // open finds a warm cache instead of paying a cold full network load. The
2628
+ // work is async + unref'd, so short-lived detached runtimes still exit
2629
+ // cleanly without waiting on it. Operators can raise it via env if a
2630
+ // detached runtime must avoid the /models round-trip entirely.
2631
+ const providerModelWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS', 2_000, { min: 0, max: 120_000 });
2632
+ const codeGraphPrewarmDelayMs = envDelayMs('MIXDOG_CODE_GRAPH_PREWARM_DELAY_MS', 250, { min: 0, max: 60_000 });
2633
+ const statuslineUsageWarmupDelayMs = envDelayMs('MIXDOG_STATUSLINE_USAGE_WARMUP_DELAY_MS', 800, { min: 0, max: 60_000 });
2634
+ // Idle keep-alive: re-fetch usage before the statusline's 10-min staleness cut
2635
+ // (LIVE_USAGE_SNAPSHOT_MAX_AGE_MS) so the usage segment does not disappear
2636
+ // while the session sits idle with no turns to trigger a refresh.
2637
+ const statuslineUsageRefreshDelayMs = envDelayMs('MIXDOG_STATUSLINE_USAGE_REFRESH_MS', 240_000, { min: 30_000, max: 540_000 });
1671
2638
  const channelStartDelayMs = envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 });
1672
2639
  const backgroundBusyRetryMs = envDelayMs('MIXDOG_BACKGROUND_BUSY_RETRY_MS', 1_000, { min: 50, max: 10_000 });
2640
+ const sessionPrewarmEnabled = !envFlag('MIXDOG_DISABLE_SESSION_PREWARM')
2641
+ && (envFlag('MIXDOG_ENABLE_SESSION_PREWARM') || envPresent('MIXDOG_SESSION_PREWARM_DELAY_MS'));
2642
+ const providerWarmupEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
2643
+ && (
2644
+ envFlag('MIXDOG_ENABLE_PROVIDER_WARMUP')
2645
+ || envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')
2646
+ || envPresent('MIXDOG_PROVIDER_WARMUP_DELAY_MS')
2647
+ || envPresent('MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS')
2648
+ );
2649
+ // Boot-time model-catalog prefetch is intentionally decoupled from the
2650
+ // heavier providerWarmupEnabled gate (which stays opt-in for provider
2651
+ // *init* side effects). Fetching the model list in the background after a
2652
+ // short delay is cheap, fire-and-forget, and unref'd, so it is ON by
2653
+ // default — otherwise the FIRST `/model` open always paid a cold full
2654
+ // network load. Operators can still disable it explicitly.
2655
+ const modelPrefetchEnabled = !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
2656
+ && !envFlag('MIXDOG_DISABLE_MODEL_PREFETCH');
2657
+ const codeGraphPrewarmEnabled = !envFlag('MIXDOG_DISABLE_CODE_GRAPH_PREWARM');
2658
+ // Lazy code-graph prewarm (default ON): do NOT prewarm at startup / on cwd
2659
+ // change — that fired ~250ms after the first frame and, in a large tree,
2660
+ // burned a worker (and felt like a freeze) before the user did anything.
2661
+ // Instead prewarm ONCE on the first real turn, when a code lookup is actually
2662
+ // imminent. Operators who want the old eager behavior can set
2663
+ // MIXDOG_CODE_GRAPH_PREWARM_EAGER=1.
2664
+ const codeGraphPrewarmLazy = codeGraphPrewarmEnabled && !envFlag('MIXDOG_CODE_GRAPH_PREWARM_EAGER');
2665
+ let codeGraphFirstTurnPrewarmDone = false;
1673
2666
  const modelMetaByRoute = new Map();
1674
2667
  const notificationListeners = new Set();
1675
2668
  let providerModelsCache = { models: null, at: 0 };
1676
2669
  let providerModelsPromise = null;
2670
+ let searchProviderModelsCache = { models: null, at: 0 };
2671
+ let searchProviderModelsPromise = null;
2672
+ let usageDashboardCache = { dashboard: null, at: 0 };
2673
+ let usageDashboardPromise = null;
1677
2674
  let providerSetupCache = { setup: null, at: 0 };
2675
+ let providerSetupQuickCache = { setup: null, at: 0 };
1678
2676
  let providerSetupPromise = null;
1679
2677
  let providerInitPromise = null;
1680
- const PROVIDER_SETUP_CACHE_TTL_MS = 10_000;
1681
2678
  let mcpFailures = [];
1682
2679
  let preSessionToolSurface = null;
1683
2680
  let contextStatusCacheKey = null;
@@ -1687,72 +2684,6 @@ export async function createMixdogSessionRuntime({
1687
2684
  hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
1688
2685
  bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
1689
2686
 
1690
- function contextContentLength(content) {
1691
- if (typeof content === 'string') return content.length;
1692
- if (!Array.isArray(content)) {
1693
- try { return JSON.stringify(content ?? '').length; } catch { return String(content ?? '').length; }
1694
- }
1695
- let length = 0;
1696
- for (const part of content) {
1697
- if (typeof part === 'string') length += part.length;
1698
- else if (typeof part?.text === 'string') length += part.text.length;
1699
- else {
1700
- try { length += JSON.stringify(part ?? '').length; } catch { length += String(part ?? '').length; }
1701
- }
1702
- }
1703
- return length;
1704
- }
1705
-
1706
- function sameContextStatusKey(a, b) {
1707
- if (!a || !b || a.length !== b.length) return false;
1708
- for (let i = 0; i < a.length; i++) {
1709
- if (!Object.is(a[i], b[i])) return false;
1710
- }
1711
- return true;
1712
- }
1713
-
1714
- function buildContextStatusCacheKey(messages, tools, bridgeMode) {
1715
- const lastMessage = messages[messages.length - 1] || null;
1716
- const compaction = session?.compaction || null;
1717
- return [
1718
- session?.id || null,
1719
- route.provider,
1720
- route.model,
1721
- currentCwd,
1722
- mode,
1723
- bridgeMode,
1724
- messages,
1725
- messages.length,
1726
- lastMessage,
1727
- lastMessage?.role || null,
1728
- lastMessage?.content || null,
1729
- contextContentLength(lastMessage?.content),
1730
- Array.isArray(lastMessage?.toolCalls) ? lastMessage.toolCalls.length : 0,
1731
- tools,
1732
- tools.length,
1733
- session?.contextWindow || null,
1734
- session?.rawContextWindow || null,
1735
- session?.effectiveContextWindowPercent || null,
1736
- session?.lastContextTokens || 0,
1737
- session?.lastContextTokensUpdatedAt || 0,
1738
- session?.lastContextTokensStaleAfterCompact === true,
1739
- session?.lastInputTokens || 0,
1740
- session?.lastOutputTokens || 0,
1741
- session?.lastCachedReadTokens || 0,
1742
- session?.lastCacheWriteTokens || 0,
1743
- session?.totalInputTokens || 0,
1744
- session?.totalOutputTokens || 0,
1745
- session?.totalCachedReadTokens || 0,
1746
- session?.totalCacheWriteTokens || 0,
1747
- session?.compactBoundaryTokens || 0,
1748
- compaction,
1749
- compaction?.lastChangedAt || 0,
1750
- compaction?.lastCompactAt || 0,
1751
- compaction?.boundaryTokens || 0,
1752
- compaction?.triggerTokens || 0,
1753
- ];
1754
- }
1755
-
1756
2687
  function mcpTransportLabel(cfg = {}) {
1757
2688
  if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
1758
2689
  if (cfg.transport === 'http' || cfg.url) return 'http';
@@ -1762,23 +2693,42 @@ export async function createMixdogSessionRuntime({
1762
2693
 
1763
2694
  function emitRuntimeNotification(content, meta = {}) {
1764
2695
  const text = String(content || '').trim();
1765
- if (!text) return;
2696
+ if (!text) return false;
1766
2697
  const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
2698
+ let handled = false;
1767
2699
  for (const listener of [...notificationListeners]) {
1768
- try { listener(event); } catch {}
2700
+ try {
2701
+ if (listener(event) === true) handled = true;
2702
+ } catch {}
1769
2703
  }
2704
+ return handled;
1770
2705
  }
1771
2706
 
1772
2707
  function notifyFnForSession(callerSessionId) {
1773
2708
  return (text, meta = {}) => {
1774
- const hadRuntimeListener = notificationListeners.size > 0;
1775
- emitRuntimeNotification(text, meta);
1776
- // TUI sessions keep their own Claude-Code-style command queue via
1777
- // onNotification. Headless/model-tool callers have no listener, so fall
1778
- // back to the session manager pending-message queue.
1779
- if (!hadRuntimeListener && callerSessionId && typeof mgr.enqueuePendingMessage === 'function') {
1780
- try { mgr.enqueuePendingMessage(callerSessionId, String(text || '')); } catch {}
2709
+ const handledByRuntimeListener = emitRuntimeNotification(text, meta);
2710
+ let enqueued = false;
2711
+ // TUI sessions consume raw execution notifications for UI/task cards via
2712
+ // onNotification, but those raw envelopes are internal-only in pending
2713
+ // drain. Always mirror terminal completions with a model-visible wrapper
2714
+ // while keeping the raw text for UI display.
2715
+ if (callerSessionId && typeof mgr.enqueuePendingMessage === 'function'
2716
+ && shouldPersistModelVisibleToolCompletion(text, meta)) {
2717
+ try {
2718
+ const visible = modelVisibleToolCompletionMessage(text, meta);
2719
+ if (visible) enqueued = mgr.enqueuePendingMessage(callerSessionId, visible) > 0;
2720
+ } catch {}
2721
+ }
2722
+ // Headless/API listeners may exist but not consume the event; preserve
2723
+ // the old fallback for non-terminal notifications only when unhandled.
2724
+ if (!enqueued && !handledByRuntimeListener && callerSessionId
2725
+ && typeof mgr.enqueuePendingMessage === 'function') {
2726
+ try {
2727
+ const visible = modelVisibleToolCompletionMessage(text, meta);
2728
+ if (visible) enqueued = mgr.enqueuePendingMessage(callerSessionId, visible) > 0;
2729
+ } catch {}
1781
2730
  }
2731
+ return enqueued || handledByRuntimeListener;
1782
2732
  };
1783
2733
  }
1784
2734
 
@@ -1840,40 +2790,92 @@ export async function createMixdogSessionRuntime({
1840
2790
  };
1841
2791
  }
1842
2792
 
2793
+ function scheduleCodeGraphPrewarm(delayMs = codeGraphPrewarmDelayMs, reason = 'cwd') {
2794
+ if (!codeGraphPrewarmEnabled) {
2795
+ bootProfile('code-graph:prewarm-skipped', { reason: 'disabled' });
2796
+ return;
2797
+ }
2798
+ if (closeRequested) return;
2799
+ codeGraphPrewarmQueuedCwd = currentCwd;
2800
+ if (codeGraphPrewarmTimer) return;
2801
+ codeGraphPrewarmTimer = setTimeout(() => {
2802
+ codeGraphPrewarmTimer = null;
2803
+ if (closeRequested) return;
2804
+ if (activeTurnCount > 0 || sessionCreatePromise) {
2805
+ bootProfile('code-graph:prewarm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
2806
+ scheduleCodeGraphPrewarm(backgroundBusyRetryMs, 'busy');
2807
+ return;
2808
+ }
2809
+ if (codeGraphPrewarmInFlight) {
2810
+ bootProfile('code-graph:prewarm-deferred', { reason: 'in-flight' });
2811
+ scheduleCodeGraphPrewarm(backgroundBusyRetryMs, 'in-flight');
2812
+ return;
2813
+ }
2814
+ const prewarmCwd = codeGraphPrewarmQueuedCwd || currentCwd;
2815
+ codeGraphPrewarmQueuedCwd = '';
2816
+ codeGraphPrewarmInFlight = true;
2817
+ const startedAt = performance.now();
2818
+ bootProfile('code-graph:prewarm:start', { cwd: prewarmCwd, reason });
2819
+ void getCodeGraphModule()
2820
+ .then((mod) => {
2821
+ if (typeof mod?.prewarmCodeGraphIfProject !== 'function') return false;
2822
+ return mod.prewarmCodeGraphIfProject(prewarmCwd);
2823
+ })
2824
+ .then((scheduled) => bootProfile(scheduled ? 'code-graph:prewarm:scheduled' : 'code-graph:prewarm:no-project', {
2825
+ cwd: prewarmCwd,
2826
+ ms: (performance.now() - startedAt).toFixed(1),
2827
+ }))
2828
+ .catch((error) => bootProfile('code-graph:prewarm:failed', {
2829
+ cwd: prewarmCwd,
2830
+ ms: (performance.now() - startedAt).toFixed(1),
2831
+ error: error?.message || String(error),
2832
+ }))
2833
+ .finally(() => {
2834
+ codeGraphPrewarmInFlight = false;
2835
+ if (codeGraphPrewarmQueuedCwd && !closeRequested) {
2836
+ scheduleCodeGraphPrewarm(backgroundBusyRetryMs, 'queued');
2837
+ }
2838
+ });
2839
+ }, delayMs);
2840
+ codeGraphPrewarmTimer.unref?.();
2841
+ }
2842
+
1843
2843
  function applyResolvedCwd(nextCwd, { markRefresh = true } = {}) {
1844
2844
  const resolved = resolve(nextCwd);
1845
2845
  const stat = statSync(resolved);
1846
2846
  if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
1847
2847
  const changed = resolve(currentCwd) !== resolved;
1848
2848
  currentCwd = resolved;
1849
- ensureProjectMixdogMd({ cwd: currentCwd });
1850
2849
  process.env.MIXDOG_SESSION_CWD = currentCwd;
1851
2850
  writeLastSessionCwd(currentCwd);
1852
2851
  if (session) session.cwd = currentCwd;
1853
- if (changed && markRefresh && session?.id) sessionNeedsCwdRefresh = true;
2852
+ // cwd changes NEVER recreate the session: a mid-conversation cwd switch must
2853
+ // preserve the full message history (and the BP1–BP3 prompt cache). We only
2854
+ // retarget the live session's cwd in place; tool execution already reads the
2855
+ // current cwd per turn. `cwd` is intentionally absent from the prompt
2856
+ // context (see composeSystemPrompt), so there is nothing prompt-side to
2857
+ // refresh either. `markRefresh`/`changed` are kept only for signature
2858
+ // compatibility with existing callers.
2859
+ void markRefresh;
2860
+ // Lazy mode: before the first turn (e.g. the initial project-selection
2861
+ // cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
2862
+ // we are avoiding. Once a turn has run, an in-session cwd switch DOES
2863
+ // prewarm the new dir, since a lookup there is now likely.
2864
+ if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
2865
+ bootProfile('code-graph:prewarm-lazy', { reason: 'cwd-deferred-to-first-turn' });
2866
+ } else {
2867
+ scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
2868
+ }
1854
2869
  return currentCwd;
1855
2870
  }
1856
2871
 
1857
2872
  async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
1858
- if (!session?.id || !sessionNeedsCwdRefresh) return session;
1859
- const previousId = session.id;
1860
- statusRoutes?.clearGatewaySessionRoute?.(previousId);
1861
- mgr.closeSession(previousId, reason);
1862
- session = null;
2873
+ // No-op: cwd changes are applied in place by applyResolvedCwd and never
2874
+ // tear down the session. Retained as a stable hook for ask()'s pre-turn
2875
+ // call so the surrounding turn flow is unchanged.
2876
+ void reason;
1863
2877
  sessionNeedsCwdRefresh = false;
1864
- return await createCurrentSession();
1865
- }
1866
-
1867
- function buildWorkspaceContext() {
1868
- try {
1869
- return formatWorkspaceSessionContext(workspaceRouter.snapshot(currentCwd));
1870
- } catch (error) {
1871
- return [
1872
- '# Workspace',
1873
- `current cwd: ${currentCwd}`,
1874
- `project candidates: unavailable (${error?.message || String(error)})`,
1875
- ].join('\n');
1876
- }
2878
+ return session;
1877
2879
  }
1878
2880
 
1879
2881
  function skillContent(name) {
@@ -1884,6 +2886,12 @@ export async function createMixdogSessionRuntime({
1884
2886
  return { name, content };
1885
2887
  }
1886
2888
 
2889
+ function skillToolContent(name) {
2890
+ const skill = skillContent(name);
2891
+ const escapedName = String(skill.name || '').replace(/[<>&]/g, (ch) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[ch]));
2892
+ return `<skill>\n<name>${escapedName}</name>\n${skill.content}\n</skill>`;
2893
+ }
2894
+
1887
2895
  function addProjectSkill(input = {}) {
1888
2896
  const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
1889
2897
  if (!name) throw new Error('skill name is required');
@@ -1996,30 +3004,25 @@ export async function createMixdogSessionRuntime({
1996
3004
  return { name, config: { command, args, cwd: resolvedCwd } };
1997
3005
  }
1998
3006
 
1999
- const persistedBridgeMode = (() => {
2000
- try { return (sharedCfgMod.readSection('agent') || {}).bridgeMode; } catch { return undefined; }
2001
- })();
2002
- const bridgeStartedAt = performance.now();
2003
- const bridge = createStandaloneBridge({
3007
+ const agentToolStartedAt = performance.now();
3008
+ const agentTool = createStandaloneAgent({
2004
3009
  cfgMod,
2005
3010
  reg,
2006
3011
  mgr,
2007
3012
  dataDir: cfgMod.getPluginData(),
2008
3013
  cwd,
2009
- defaultMode: persistedBridgeMode ?? 'async',
2010
3014
  });
2011
- bootProfile('bridge:ready', { ms: (performance.now() - bridgeStartedAt).toFixed(1) });
2012
- const bridgeStatusState = () => {
3015
+ bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
3016
+ const agentStatusState = () => {
2013
3017
  try {
2014
- const status = bridge.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
3018
+ const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
2015
3019
  return {
2016
- bridgeMode: bridge.getDefaultMode?.() || status.bridgeMode || 'async',
2017
- bridgeWorkers: Array.isArray(status.workers) ? status.workers : [],
2018
- bridgeJobs: Array.isArray(status.jobs) ? status.jobs : [],
2019
- bridgeScope: status.scope || null,
3020
+ agentWorkers: Array.isArray(status.workers) ? status.workers : [],
3021
+ agentJobs: Array.isArray(status.jobs) ? status.jobs : [],
3022
+ agentScope: status.scope || null,
2020
3023
  };
2021
3024
  } catch {
2022
- return { bridgeMode: bridge.getDefaultMode?.() || 'async', bridgeWorkers: [], bridgeJobs: [], bridgeScope: null };
3025
+ return { agentWorkers: [], agentJobs: [], agentScope: null };
2023
3026
  }
2024
3027
  };
2025
3028
  const channelsStartedAt = performance.now();
@@ -2028,18 +3031,28 @@ export async function createMixdogSessionRuntime({
2028
3031
  rootDir: STANDALONE_ROOT,
2029
3032
  dataDir: cfgMod.getPluginData(),
2030
3033
  cwd,
3034
+ onNotify: (msg) => {
3035
+ if (msg?.method !== 'notifications/claude/channel') return;
3036
+ const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
3037
+ const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
3038
+ if (meta.silent_to_agent === true || meta.silent_to_agent === 'true') return;
3039
+ const instruction = typeof meta.instruction === 'string' ? meta.instruction.trim() : '';
3040
+ const content = instruction || String(params.content || '').trim();
3041
+ emitRuntimeNotification(content, meta);
3042
+ },
2031
3043
  });
2032
3044
  bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
2033
3045
  const toolsStartedAt = performance.now();
2034
3046
  const standaloneTools = [
2035
3047
  TOOL_SEARCH_TOOL,
3048
+ SKILL_TOOL,
2036
3049
  CWD_TOOL,
2037
3050
  EXPLORE_TOOL,
2038
3051
  ...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
2039
3052
  ...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
2040
3053
  ...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
2041
3054
  ...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
2042
- ...bridge.tools,
3055
+ ...agentTool.tools,
2043
3056
  PROVIDER_STATUS_TOOL,
2044
3057
  CHANNEL_STATUS_TOOL,
2045
3058
  ].map(applyStandaloneToolDefaults);
@@ -2060,7 +3073,7 @@ export async function createMixdogSessionRuntime({
2060
3073
  : [];
2061
3074
  const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
2062
3075
  const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
2063
- applyDeferredToolSurface(surface, mode, standaloneTools);
3076
+ applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
2064
3077
  return surface;
2065
3078
  }
2066
3079
 
@@ -2075,7 +3088,11 @@ export async function createMixdogSessionRuntime({
2075
3088
  const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
2076
3089
  ? preSessionToolSurface.deferredSelectedTools
2077
3090
  : [];
2078
- if (selected.length) selectDeferredTools(session, selected, mode);
3091
+ const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
3092
+ ? preSessionToolSurface.deferredDiscoveredTools
3093
+ : [];
3094
+ const replay = [...new Set([...selected, ...discovered])];
3095
+ if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
2079
3096
  }
2080
3097
  internalTools.setInternalToolsProvider({
2081
3098
  tools: standaloneTools,
@@ -2091,35 +3108,8 @@ export async function createMixdogSessionRuntime({
2091
3108
  routingSessionId: callerSessionId,
2092
3109
  clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
2093
3110
  notifyFn: notifyFnForSession(callerSessionId),
2094
- agentSearch: name === 'search'
2095
- ? async (searchArgs) => {
2096
- const query = Array.isArray(searchArgs.keywords)
2097
- ? searchArgs.keywords.join('\n')
2098
- : String(searchArgs.keywords || searchArgs.query || '');
2099
- const prompt = searchArgs.prompt || [
2100
- 'Perform a concise web research task for Mixdog search.',
2101
- '',
2102
- `Query: ${query}`,
2103
- searchArgs.site ? `Site/domain restriction: ${searchArgs.site}` : null,
2104
- searchArgs.type ? `Search type: ${searchArgs.type}` : null,
2105
- searchArgs.locale ? `Locale: ${typeof searchArgs.locale === 'string' ? searchArgs.locale : JSON.stringify(searchArgs.locale)}` : null,
2106
- `Max results: ${Math.max(1, Math.min(20, Number(searchArgs.maxResults) || 10))}`,
2107
- '',
2108
- 'Return a short answer first, then cite useful results as title + URL + one-line snippet.',
2109
- 'Do not edit files.',
2110
- ].filter(Boolean).join('\n');
2111
- const rendered = await bridge.execute({
2112
- type: 'spawn',
2113
- agent: 'web-researcher',
2114
- tag: `search_${Date.now().toString(36)}`,
2115
- prompt,
2116
- cwd: callerCwd,
2117
- wait: true,
2118
- firstResponseTimeoutMs: Number.isFinite(Number(searchArgs.firstResponseTimeoutMs)) ? Number(searchArgs.firstResponseTimeoutMs) : 120_000,
2119
- idleTimeoutMs: Number.isFinite(Number(searchArgs.idleTimeoutMs)) ? Number(searchArgs.idleTimeoutMs) : 30 * 60_000,
2120
- }, { invocationSource: 'user-command', callerCwd });
2121
- return String(rendered || '').replace(/^bridge result[^\n]*\n/i, '').trim();
2122
- }
3111
+ nativeSearch: name === 'search'
3112
+ ? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
2123
3113
  : undefined,
2124
3114
  });
2125
3115
  }
@@ -2133,7 +3123,9 @@ export async function createMixdogSessionRuntime({
2133
3123
  if (!codeGraphMod?.executeCodeGraphTool) throw new Error('code_graph runtime is not available');
2134
3124
  return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
2135
3125
  }
2136
- if (name === 'tool_search') return renderToolSearch(args, session, mode);
3126
+ if (name === 'tool_search') {
3127
+ return renderToolSearch(args, activeToolSurface(), mode);
3128
+ }
2137
3129
  if (name === 'explore') {
2138
3130
  const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
2139
3131
  return await runExplore(args || {}, {
@@ -2153,9 +3145,12 @@ export async function createMixdogSessionRuntime({
2153
3145
  }
2154
3146
  return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
2155
3147
  }
2156
- if (name === 'bridge') {
3148
+ if (name === 'Skill') {
3149
+ return skillToolContent(args?.name);
3150
+ }
3151
+ if (name === 'agent') {
2157
3152
  const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
2158
- return await bridge.execute(args, {
3153
+ return await agentTool.execute(args, {
2159
3154
  callerCwd,
2160
3155
  invocationSource: 'model-tool',
2161
3156
  callerSessionId,
@@ -2164,9 +3159,12 @@ export async function createMixdogSessionRuntime({
2164
3159
  notifyFn: notifyFnForSession(callerSessionId),
2165
3160
  });
2166
3161
  }
2167
- if (name === 'provider_status') return renderProviderStatus(cfgMod.loadConfig());
3162
+ if (name === 'provider_status') return renderProviderStatus(displayConfig());
2168
3163
  if (name === 'channel_status') return renderChannelStatus();
2169
- if (channels.isChannelTool(name)) return await channels.execute(name, args || {});
3164
+ if (channels.isChannelTool(name)) {
3165
+ if (!channelsEnabled()) throw new Error('channels are disabled in settings');
3166
+ return await channels.execute(name, args || {});
3167
+ }
2170
3168
  throw new Error(`unknown standalone internal tool: ${name}`);
2171
3169
  },
2172
3170
  });
@@ -2185,16 +3183,81 @@ export async function createMixdogSessionRuntime({
2185
3183
  function invalidateProviderCaches() {
2186
3184
  providerModelsCache = { models: null, at: 0 };
2187
3185
  providerModelsPromise = null;
3186
+ searchProviderModelsCache = { models: null, at: 0 };
3187
+ searchProviderModelsPromise = null;
3188
+ usageDashboardCache = { dashboard: null, at: 0 };
3189
+ usageDashboardPromise = null;
2188
3190
  providerSetupCache = { setup: null, at: 0 };
3191
+ providerSetupQuickCache = { setup: null, at: 0 };
2189
3192
  providerSetupPromise = null;
2190
3193
  providerInitPromise = null;
2191
3194
  modelMetaByRoute.clear();
2192
3195
  }
2193
3196
 
3197
+ function adoptConfig(nextConfig, { hasSecrets = configHasSecrets } = {}) {
3198
+ config = nextConfig;
3199
+ configHasSecrets = hasSecrets === true;
3200
+ setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
3201
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
3202
+ return config;
3203
+ }
3204
+
3205
+ // Persisting mixdog-config.json is heavy: it takes a cross-process file
3206
+ // lock, does an atomic temp+rename, and on win32 shells out to icacls
3207
+ // (an external process, multiple times per write) to enforce an owner-only
3208
+ // ACL. Doing that synchronously on EVERY option toggle froze the TUI main
3209
+ // thread, so adjusting a picker felt laggy and unresponsive. We split the
3210
+ // two concerns: adopt the new config in-memory IMMEDIATELY (so getProfile/
3211
+ // getAutoClear/etc. and the UI read fresh values on the same tick), and
3212
+ // DEBOUNCE the actual disk write so a burst of toggles collapses into a
3213
+ // single persist after input settles. The security model is unchanged —
3214
+ // cfgMod.saveConfig still strips apiKey (keychain-only) and still applies
3215
+ // the owner-only ACL; we only changed WHEN it runs, never HOW.
3216
+ let pendingConfigToSave = null;
3217
+ let configSaveTimer = null;
3218
+ const CONFIG_SAVE_DEBOUNCE_MS = 150;
3219
+ function flushConfigSave() {
3220
+ if (configSaveTimer) {
3221
+ clearTimeout(configSaveTimer);
3222
+ configSaveTimer = null;
3223
+ }
3224
+ if (pendingConfigToSave === null) return;
3225
+ const snapshot = pendingConfigToSave;
3226
+ pendingConfigToSave = null;
3227
+ try {
3228
+ cfgMod.saveConfig(snapshot);
3229
+ } catch (err) {
3230
+ process.stderr.write(`[config] debounced saveConfig failed: ${err?.message || err}\n`);
3231
+ }
3232
+ }
3233
+ function saveConfigAndAdopt(nextConfig, { hasSecrets = configHasSecrets } = {}) {
3234
+ // In-memory adopt is synchronous and first so callers that read back the
3235
+ // value immediately (e.g. setProfile -> getProfile) see the new state.
3236
+ const adopted = adoptConfig(nextConfig, { hasSecrets });
3237
+ // Persist the adopted object; coalesce rapid successive changes into one
3238
+ // disk write after CONFIG_SAVE_DEBOUNCE_MS of quiet.
3239
+ pendingConfigToSave = config;
3240
+ if (configSaveTimer) clearTimeout(configSaveTimer);
3241
+ configSaveTimer = setTimeout(flushConfigSave, CONFIG_SAVE_DEBOUNCE_MS);
3242
+ configSaveTimer.unref?.();
3243
+ return adopted;
3244
+ }
3245
+
3246
+ function reloadFullConfig() {
3247
+ // A pending debounced write holds the only copy of the latest change.
3248
+ // Flush it before re-reading from disk so loadConfig() observes (and the
3249
+ // subsequent adopt preserves) that change instead of reverting to a stale
3250
+ // on-disk snapshot.
3251
+ flushConfigSave();
3252
+ return adoptConfig(cfgMod.loadConfig(), { hasSecrets: true });
3253
+ }
3254
+
2194
3255
  function ensureFullConfig() {
2195
3256
  if (configHasSecrets) return config;
2196
- config = cfgMod.loadConfig();
2197
- configHasSecrets = true;
3257
+ return reloadFullConfig();
3258
+ }
3259
+
3260
+ function displayConfig() {
2198
3261
  return config;
2199
3262
  }
2200
3263
 
@@ -2207,6 +3270,19 @@ export async function createMixdogSessionRuntime({
2207
3270
  return ensureFullConfig();
2208
3271
  }
2209
3272
 
3273
+ function refreshStatuslineUsageSnapshot(routeLike = {}) {
3274
+ const providerId = clean(routeLike.provider);
3275
+ const modelId = clean(routeLike.model);
3276
+ if (!providerId || !providerId.includes('oauth')) return;
3277
+ const providerObj = reg.getProvider(providerId);
3278
+ if (!providerObj) return;
3279
+ void fetchOAuthUsageSnapshot({ provider: providerId, model: modelId }, providerObj, (message) => {
3280
+ if (process.env.MIXDOG_STATUSLINE_TRACE) {
3281
+ try { process.stderr.write(`[statusline] ${message}\n`); } catch {}
3282
+ }
3283
+ }).catch(() => {});
3284
+ }
3285
+
2210
3286
  async function ensureProvidersReady(providerConfig = config.providers || {}) {
2211
3287
  if (providerInitPromise) return await providerInitPromise;
2212
3288
  providerInitPromise = reg.initProviders(providerConfig)
@@ -2216,13 +3292,23 @@ export async function createMixdogSessionRuntime({
2216
3292
  return await providerInitPromise;
2217
3293
  }
2218
3294
 
2219
- async function cachedProviderSetup({ force = false } = {}) {
2220
- const now = Date.now();
2221
- if (!force && providerSetupCache.setup && now - providerSetupCache.at < PROVIDER_SETUP_CACHE_TTL_MS) {
3295
+ async function cachedProviderSetup({ force = false, quick = false } = {}) {
3296
+ if (!force && providerSetupCache.setup) {
2222
3297
  return providerSetupCache.setup;
2223
3298
  }
2224
- if (!force && providerSetupPromise) return await providerSetupPromise;
2225
- providerSetupPromise = providerSetup(cfgMod.loadConfig())
3299
+ if (quick) {
3300
+ if (!force && providerSetupQuickCache.setup) {
3301
+ return providerSetupQuickCache.setup;
3302
+ }
3303
+ const setup = await providerSetup(displayConfig(), { detectLocal: false, checkSecrets: false });
3304
+ providerSetupQuickCache = { setup, at: Date.now() };
3305
+ if (!providerSetupPromise && !providerSetupWarmupTimer && !closeRequested) {
3306
+ scheduleProviderSetupWarmup(0);
3307
+ }
3308
+ return setup;
3309
+ }
3310
+ if (providerSetupPromise) return await providerSetupPromise;
3311
+ providerSetupPromise = providerSetup(displayConfig(), { detectLocal: true })
2226
3312
  .then((setup) => {
2227
3313
  providerSetupCache = { setup, at: Date.now() };
2228
3314
  return setup;
@@ -2370,9 +3456,10 @@ function parsedProviderModelVersion(id) {
2370
3456
  description: m.description || '',
2371
3457
  supportsVision: m.supportsVision === true,
2372
3458
  supportsFunctionCalling: m.supportsFunctionCalling === true,
3459
+ supportsWebSearch: searchCapableFor(name, m),
2373
3460
  supportsPromptCaching: m.supportsPromptCaching === true,
2374
3461
  supportsReasoning: m.supportsReasoning === true,
2375
- reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : [],
3462
+ reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : undefined,
2376
3463
  reasoningOptions: Array.isArray(m.reasoningOptions) ? m.reasoningOptions : [],
2377
3464
  reasoningContentField: m.reasoningContentField || null,
2378
3465
  mode: m.mode || null,
@@ -2394,40 +3481,265 @@ function parsedProviderModelVersion(id) {
2394
3481
  return sortProviderModels((rows || []).map(hydrateProviderModelRow));
2395
3482
  }
2396
3483
 
2397
- async function loadProviderModelsFresh() {
3484
+ function quickProviderModelRows() {
3485
+ const pickerConfig = displayConfig();
3486
+ const rows = [];
3487
+ const seen = new Set();
3488
+ const addRoute = (routeLike = {}) => {
3489
+ const provider = clean(routeLike.provider);
3490
+ const model = clean(routeLike.model);
3491
+ if (!provider || !model) return;
3492
+ const key = `${provider}:${model}`;
3493
+ if (seen.has(key)) return;
3494
+ seen.add(key);
3495
+ const row = providerModelCacheRow(provider, {
3496
+ id: model,
3497
+ name: routeLike.modelDisplay || routeLike.display || model,
3498
+ display: routeLike.modelDisplay || routeLike.display || model,
3499
+ latest: routeLike.latest === true,
3500
+ supportsReasoning: !!routeLike.effort,
3501
+ mode: 'chat',
3502
+ });
3503
+ rows.push(row);
3504
+ modelMetaByRoute.set(modelMetaKey(provider, model), row);
3505
+ };
3506
+
3507
+ addRoute(route);
3508
+ for (const preset of pickerConfig.presets || []) addRoute(preset);
3509
+ for (const workflowRoute of Object.values(pickerConfig.workflowRoutes || {})) addRoute(workflowRoute);
3510
+ for (const agentRoute of Object.values(pickerConfig.agents || {})) addRoute(agentRoute);
3511
+ return providerModelsFromCacheRows(rows);
3512
+ }
3513
+
3514
+ function addQuickSearchModel(rows, seen, provider, model) {
3515
+ const providerName = normalizeSearchProviderId(provider);
3516
+ const modelId = clean(model?.id || model);
3517
+ if (!providerName || !modelId || !isSearchCapableProvider(providerName)) return;
3518
+ const key = `${providerName}:${modelId}`;
3519
+ if (seen.has(key)) return;
3520
+ const row = providerModelCacheRow(providerName, {
3521
+ id: modelId,
3522
+ name: model?.name || model?.display || modelId,
3523
+ display: model?.display || model?.name || modelId,
3524
+ contextWindow: model?.contextWindow || null,
3525
+ outputTokens: model?.outputTokens || null,
3526
+ latest: model?.latest === true,
3527
+ supportsWebSearch: true,
3528
+ supportsFunctionCalling: model?.supportsFunctionCalling === true,
3529
+ supportsPromptCaching: model?.supportsPromptCaching === true,
3530
+ supportsReasoning: model?.supportsReasoning === true,
3531
+ reasoningLevels: Array.isArray(model?.reasoningLevels) ? model.reasoningLevels : undefined,
3532
+ reasoningOptions: Array.isArray(model?.reasoningOptions) ? model.reasoningOptions : [],
3533
+ mode: 'chat',
3534
+ });
3535
+ if (row.supportsWebSearch !== true) return;
3536
+ seen.add(key);
3537
+ rows.push({
3538
+ ...row,
3539
+ provider: providerName,
3540
+ searchCapable: true,
3541
+ searchToolType: row.searchToolType || 'web_search',
3542
+ });
3543
+ }
3544
+
3545
+ function addDefaultSearchModel(rows, seen = new Set()) {
3546
+ const mainModel = currentMainSearchModelMeta();
3547
+ if (!mainModel || !searchCapableFor(route.provider, mainModel)) return;
3548
+ const key = `${SEARCH_DEFAULT_PROVIDER}:${SEARCH_DEFAULT_MODEL}`;
3549
+ if (seen.has(key)) return;
3550
+ seen.add(key);
3551
+ rows.push({
3552
+ id: SEARCH_DEFAULT_MODEL,
3553
+ provider: SEARCH_DEFAULT_PROVIDER,
3554
+ display: 'Default',
3555
+ name: 'Default',
3556
+ description: `Use current main model: ${route.provider}/${route.model}`,
3557
+ supportsWebSearch: true,
3558
+ searchCapable: true,
3559
+ searchToolType: 'web_search',
3560
+ mode: 'chat',
3561
+ });
3562
+ }
3563
+
3564
+ function quickSearchProviderModelRows() {
3565
+ const pickerConfig = displayConfig();
3566
+ const rows = [];
3567
+ const seen = new Set();
3568
+ addDefaultSearchModel(rows, seen);
3569
+ for (const [name, providerConfig] of Object.entries(pickerConfig.providers || {})) {
3570
+ const providerName = normalizeSearchProviderId(name);
3571
+ if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
3572
+ for (const model of QUICK_SEARCH_MODELS[providerName] || []) {
3573
+ addQuickSearchModel(rows, seen, providerName, model);
3574
+ }
3575
+ }
3576
+ const configuredSearch = normalizeSearchRouteConfig(pickerConfig.searchRoute) || normalizeSearchRouteConfig(searchRoute);
3577
+ if (configuredSearch?.provider && configuredSearch?.model) {
3578
+ addQuickSearchModel(rows, seen, configuredSearch.provider, {
3579
+ id: configuredSearch.model,
3580
+ display: configuredSearch.model,
3581
+ });
3582
+ }
3583
+ const mainModel = currentMainSearchModelMeta();
3584
+ if (mainModel && searchCapableFor(route.provider, mainModel)) {
3585
+ addQuickSearchModel(rows, seen, route.provider, {
3586
+ id: route.model,
3587
+ display: route.model,
3588
+ });
3589
+ }
3590
+ return searchModelsFromRows(rows);
3591
+ }
3592
+
3593
+ function searchModelsFromRows(rows) {
3594
+ return sortProviderModels((rows || [])
3595
+ .filter((row) => row.supportsWebSearch === true)
3596
+ .map((row) => ({
3597
+ ...row,
3598
+ provider: normalizeSearchProviderId(row.provider),
3599
+ searchCapable: true,
3600
+ searchToolType: row.searchToolType || 'web_search',
3601
+ })));
3602
+ }
3603
+
3604
+ function searchRowsWithDefault(rows = []) {
3605
+ const out = [];
3606
+ const seen = new Set();
3607
+ addDefaultSearchModel(out, seen);
3608
+ for (const row of rows || []) {
3609
+ const providerName = normalizeSearchProviderId(row?.provider);
3610
+ const modelId = clean(row?.id || row?.model);
3611
+ if (providerName === SEARCH_DEFAULT_PROVIDER && modelId.toLowerCase() === SEARCH_DEFAULT_MODEL) continue;
3612
+ const key = `${providerName}:${modelId}`;
3613
+ if (!providerName || !modelId || seen.has(key)) continue;
3614
+ seen.add(key);
3615
+ out.push(row);
3616
+ }
3617
+ return out;
3618
+ }
3619
+
3620
+ async function collectSearchProviderModels({ force = false } = {}) {
3621
+ if (!force && Array.isArray(searchProviderModelsCache.models)) {
3622
+ return providerModelsFromCacheRows(searchRowsWithDefault(searchProviderModelsCache.models));
3623
+ }
3624
+ if (!force && Array.isArray(providerModelsCache.models)) {
3625
+ const rows = searchRowsWithDefault(searchModelsFromRows(providerModelsCache.models));
3626
+ searchProviderModelsCache = { models: rows, at: Date.now() };
3627
+ return providerModelsFromCacheRows(rows);
3628
+ }
3629
+ if (!force) {
3630
+ const rows = searchRowsWithDefault(quickSearchProviderModelRows());
3631
+ searchProviderModelsCache = { models: rows, at: Date.now() };
3632
+ return providerModelsFromCacheRows(rows);
3633
+ }
3634
+ if (!searchProviderModelsPromise) {
3635
+ searchProviderModelsPromise = loadSearchProviderModelsFresh()
3636
+ .then((models) => {
3637
+ searchProviderModelsCache = { models, at: Date.now() };
3638
+ return models;
3639
+ })
3640
+ .finally(() => {
3641
+ searchProviderModelsPromise = null;
3642
+ });
3643
+ }
3644
+ return providerModelsFromCacheRows(await searchProviderModelsPromise);
3645
+ }
3646
+
3647
+ function enabledSearchProviderConfig() {
2398
3648
  ensureFullConfig();
3649
+ const out = {};
3650
+ for (const [name, providerConfig] of Object.entries(config.providers || {})) {
3651
+ const providerName = normalizeSearchProviderId(name);
3652
+ if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
3653
+ out[providerName] = { ...providerConfig, enabled: true };
3654
+ }
3655
+ return out;
3656
+ }
3657
+
3658
+ async function loadSearchProviderModelsFresh() {
3659
+ const searchProviders = enabledSearchProviderConfig();
3660
+ const providerNames = Object.keys(searchProviders);
3661
+ if (!providerNames.length) return [];
2399
3662
  await ensureProvidersReady(config.providers || {});
2400
- const allProviders = reg.getAllProviders();
3663
+ const providerResults = await Promise.all(providerNames.map(async (name) => {
3664
+ const provider = reg.getProvider(name);
3665
+ if (typeof provider?.listModels !== 'function') return [];
3666
+ try {
3667
+ const models = await provider.listModels();
3668
+ if (!Array.isArray(models)) return [];
3669
+ const rows = [];
3670
+ for (const m of models) {
3671
+ if (!m?.id || !isSelectableLlmModel(m)) continue;
3672
+ const row = providerModelCacheRow(name, m);
3673
+ if (row.supportsWebSearch !== true) continue;
3674
+ rows.push({
3675
+ ...row,
3676
+ provider: normalizeSearchProviderId(row.provider),
3677
+ searchCapable: true,
3678
+ searchToolType: row.searchToolType || 'web_search',
3679
+ });
3680
+ modelMetaByRoute.set(modelMetaKey(name, m.id), row);
3681
+ }
3682
+ return rows;
3683
+ } catch {
3684
+ // Keep the picker responsive if one search-capable provider has a
3685
+ // transient catalog/auth failure.
3686
+ return [];
3687
+ }
3688
+ }));
2401
3689
  const results = [];
2402
3690
  const seen = new Set();
2403
- for (const [name, provider] of allProviders) {
2404
- if (typeof provider?.listModels !== 'function') continue;
3691
+ addDefaultSearchModel(results, seen);
3692
+ for (const row of providerResults.flat()) {
3693
+ const key = `${normalizeSearchProviderId(row.provider)}:${row.id}`;
3694
+ if (seen.has(key)) continue;
3695
+ seen.add(key);
3696
+ results.push(row);
3697
+ }
3698
+ return results;
3699
+ }
3700
+
3701
+ async function loadProviderModelsFresh() {
3702
+ ensureFullConfig();
3703
+ await ensureProvidersReady(config.providers || {});
3704
+ const allProviders = [...reg.getAllProviders()];
3705
+ const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
3706
+ if (typeof provider?.listModels !== 'function') return [];
2405
3707
  try {
2406
3708
  const models = await provider.listModels();
2407
- if (Array.isArray(models)) {
2408
- for (const m of models) {
2409
- if (!m?.id) continue;
2410
- if (!isSelectableLlmModel(m)) continue;
2411
- const key = `${name}:${m.id}`;
2412
- if (seen.has(key)) continue;
2413
- seen.add(key);
2414
- const row = providerModelCacheRow(name, m);
2415
- results.push(row);
2416
- modelMetaByRoute.set(modelMetaKey(name, m.id), row);
2417
- }
3709
+ if (!Array.isArray(models)) return [];
3710
+ const rows = [];
3711
+ for (const m of models) {
3712
+ if (!m?.id) continue;
3713
+ if (!isSelectableLlmModel(m)) continue;
3714
+ rows.push(providerModelCacheRow(name, m));
2418
3715
  }
3716
+ return rows;
2419
3717
  } catch {
2420
3718
  // Ignore per-provider catalog failures so one bad credential or
2421
3719
  // transient /models error does not hide other authenticated models.
3720
+ return [];
2422
3721
  }
3722
+ }));
3723
+ const results = [];
3724
+ const seen = new Set();
3725
+ for (const row of providerResults.flat()) {
3726
+ const key = `${row.provider}:${row.id}`;
3727
+ if (seen.has(key)) continue;
3728
+ seen.add(key);
3729
+ results.push(row);
3730
+ modelMetaByRoute.set(modelMetaKey(row.provider, row.id), row);
2423
3731
  }
2424
3732
  return results;
2425
3733
  }
2426
3734
 
2427
- async function collectProviderModels({ force = false } = {}) {
3735
+ async function collectProviderModels({ force = false, quick = false } = {}) {
2428
3736
  if (!force && Array.isArray(providerModelsCache.models)) {
2429
3737
  return providerModelsFromCacheRows(providerModelsCache.models);
2430
3738
  }
3739
+ if (!force && quick) {
3740
+ warmProviderModelCache();
3741
+ return quickProviderModelRows();
3742
+ }
2431
3743
  if (!providerModelsPromise) {
2432
3744
  providerModelsPromise = loadProviderModelsFresh()
2433
3745
  .then((models) => {
@@ -2465,7 +3777,7 @@ function parsedProviderModelVersion(id) {
2465
3777
  const models = await collectProviderModels();
2466
3778
  const picked = models[0] || null;
2467
3779
  if (!picked) {
2468
- throw new Error('No provider models available. Run /providers to authenticate, then /model to choose a model.');
3780
+ throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
2469
3781
  }
2470
3782
  route = {
2471
3783
  ...route,
@@ -2498,7 +3810,7 @@ function parsedProviderModelVersion(id) {
2498
3810
 
2499
3811
  function requireModelRoute() {
2500
3812
  if (routeHasModel()) return;
2501
- throw new Error('No model configured. Run /providers to authenticate, then /model to choose a model.');
3813
+ throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
2502
3814
  }
2503
3815
 
2504
3816
  async function recreateCurrentSessionIfReady() {
@@ -2537,8 +3849,8 @@ function parsedProviderModelVersion(id) {
2537
3849
  const coreMemoryContext = await loadCoreMemoryContext();
2538
3850
  if (closeRequested) throw new Error('runtime is closing');
2539
3851
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3852
+ const workflow = activeWorkflowSummary(config, dataDir);
2540
3853
  const workflowContext = workflowContextBlock(config, dataDir);
2541
- const workspaceContext = buildWorkspaceContext();
2542
3854
  const sessionOpts = {
2543
3855
  provider: route.provider,
2544
3856
  model: route.model,
@@ -2553,9 +3865,12 @@ function parsedProviderModelVersion(id) {
2553
3865
  disallowedTools: LEAD_DISALLOWED_TOOLS,
2554
3866
  cwd: currentCwd,
2555
3867
  coreMemoryContext,
3868
+ workflow,
2556
3869
  workflowContext,
2557
- workspaceContext,
2558
3870
  fast: route.fast === true,
3871
+ compaction: config.compaction && typeof config.compaction === 'object'
3872
+ ? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
3873
+ : undefined,
2559
3874
  };
2560
3875
  if (hasOwn(route, 'effort') || route.effectiveEffort) {
2561
3876
  sessionOpts.effort = route.effectiveEffort || null;
@@ -2563,15 +3878,55 @@ function parsedProviderModelVersion(id) {
2563
3878
  session = mgr.createSession(sessionOpts);
2564
3879
  sessionNeedsCwdRefresh = false;
2565
3880
  Object.defineProperty(session, 'beforeToolHook', {
2566
- value: (input) => hooks.beforeTool(input),
3881
+ value: (input) => hooks.beforeTool(hookCommonPayload({
3882
+ ...input,
3883
+ session_id: input?.sessionId || input?.session_id || session?.id,
3884
+ tool_name: input?.name || input?.tool_name,
3885
+ tool_input: input?.args || input?.tool_input,
3886
+ tool_use_id: input?.toolCallId || input?.tool_use_id,
3887
+ cwd: input?.cwd || currentCwd,
3888
+ })),
3889
+ enumerable: false,
3890
+ configurable: true,
3891
+ writable: true,
3892
+ });
3893
+ // PostToolUse: bridge runtime tool completions to the standard hook bus.
3894
+ // dispatch() returns a promise; the loop's afterToolHook caller already
3895
+ // try/catches, so a rejection cannot escape the tool loop.
3896
+ Object.defineProperty(session, 'afterToolHook', {
3897
+ value: (input) => hooks.dispatch('PostToolUse', hookCommonPayload({
3898
+ session_id: input?.sessionId || input?.session_id || session?.id,
3899
+ cwd: input?.cwd || currentCwd,
3900
+ tool_name: input?.name,
3901
+ tool_input: input?.args,
3902
+ tool_use_id: input?.toolCallId || input?.tool_use_id,
3903
+ tool_response: input?.result,
3904
+ })),
2567
3905
  enumerable: false,
2568
3906
  configurable: true,
2569
3907
  writable: true,
2570
3908
  });
2571
- applyDeferredToolSurface(session, mode, standaloneTools);
3909
+ applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
2572
3910
  applyPreSessionToolSelection();
2573
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
3911
+ writeStatuslineRoute(statusRoutes, session, route);
2574
3912
  hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
3913
+ // SessionStart: bridge to the standard Claude Code hook bus. Best-effort;
3914
+ // a hook error must never break session creation. additionalContext is
3915
+ // injected before the first user turn as a system-reminder context pair.
3916
+ try {
3917
+ const startSource = /resume/i.test(String(reason || ''))
3918
+ ? 'resume'
3919
+ : (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
3920
+ const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: session.id, source: startSource, model: route.model }));
3921
+ const startContext = Array.isArray(startDispatch?.additionalContext)
3922
+ ? startDispatch.additionalContext.join('\n\n')
3923
+ : String(startDispatch?.additionalContext || '');
3924
+ if (startContext.trim()) {
3925
+ session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
3926
+ session.messages.push({ role: 'assistant', content: '.' });
3927
+ session.updatedAt = Date.now();
3928
+ }
3929
+ } catch { /* best-effort: never break session create */ }
2575
3930
  bootProfile('session:create:ready', {
2576
3931
  ms: (performance.now() - startedAt).toFixed(1),
2577
3932
  reason,
@@ -2590,7 +3945,10 @@ function parsedProviderModelVersion(id) {
2590
3945
  }
2591
3946
 
2592
3947
  function scheduleLeadSessionPrewarm() {
2593
- if (envFlag('MIXDOG_DISABLE_SESSION_PREWARM')) return;
3948
+ if (!sessionPrewarmEnabled) {
3949
+ bootProfile('session:prewarm-skipped');
3950
+ return;
3951
+ }
2594
3952
  const timer = setTimeout(() => {
2595
3953
  if (closeRequested || session?.id || sessionCreatePromise || activeTurnCount > 0) return;
2596
3954
  void createCurrentSession('prewarm')
@@ -2601,7 +3959,7 @@ function parsedProviderModelVersion(id) {
2601
3959
  }
2602
3960
 
2603
3961
  function scheduleProviderWarmup(delayMs = providerWarmupDelayMs) {
2604
- if (envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')) {
3962
+ if (!providerWarmupEnabled) {
2605
3963
  bootProfile('providers:warm-skipped');
2606
3964
  return;
2607
3965
  }
@@ -2609,6 +3967,10 @@ function parsedProviderModelVersion(id) {
2609
3967
  providerWarmupTimer = setTimeout(() => {
2610
3968
  providerWarmupTimer = null;
2611
3969
  if (closeRequested) return;
3970
+ if (!firstTurnCompleted && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
3971
+ bootProfile('providers:warm-deferred', { reason: 'first-turn-pending' });
3972
+ return;
3973
+ }
2612
3974
  if (activeTurnCount > 0 || sessionCreatePromise) {
2613
3975
  bootProfile('providers:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
2614
3976
  scheduleProviderWarmup(backgroundBusyRetryMs);
@@ -2616,8 +3978,7 @@ function parsedProviderModelVersion(id) {
2616
3978
  }
2617
3979
  const providersStartedAt = performance.now();
2618
3980
  try {
2619
- config = cfgMod.loadConfig();
2620
- configHasSecrets = true;
3981
+ reloadFullConfig();
2621
3982
  } catch (error) {
2622
3983
  bootProfile('config:full-failed', { error: error?.message || String(error) });
2623
3984
  }
@@ -2625,28 +3986,27 @@ function parsedProviderModelVersion(id) {
2625
3986
  .then(() => {
2626
3987
  bootProfile('providers:init:ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
2627
3988
  if (closeRequested) return null;
2628
- if (activeTurnCount > 0 || sessionCreatePromise) {
2629
- bootProfile('providers:optional-warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
2630
- scheduleProviderWarmup(backgroundBusyRetryMs);
2631
- return null;
2632
- }
2633
- return cachedProviderSetup();
2634
- })
2635
- .then((setup) => {
2636
- if (!setup) return;
2637
- bootProfile('provider-setup:warm-ready', {
2638
- api: Array.isArray(setup?.api) ? setup.api.length : 0,
2639
- oauth: Array.isArray(setup?.oauth) ? setup.oauth.length : 0,
2640
- local: Array.isArray(setup?.local) ? setup.local.length : 0,
2641
- });
3989
+ return true;
2642
3990
  })
2643
3991
  .catch((error) => bootProfile('providers:warm-failed', { error: error?.message || String(error) }));
2644
3992
  }, delayMs);
2645
3993
  providerWarmupTimer.unref?.();
2646
3994
  }
2647
3995
 
3996
+ function scheduleProviderSetupWarmup(delayMs = providerSetupWarmupDelayMs) {
3997
+ if (providerSetupWarmupTimer || closeRequested) return;
3998
+ providerSetupWarmupTimer = setTimeout(() => {
3999
+ providerSetupWarmupTimer = null;
4000
+ if (closeRequested) return;
4001
+ void cachedProviderSetup()
4002
+ .then(() => bootProfile('provider-setup:warm-ready'))
4003
+ .catch((error) => bootProfile('provider-setup:warm-failed', { error: error?.message || String(error) }));
4004
+ }, delayMs);
4005
+ providerSetupWarmupTimer.unref?.();
4006
+ }
4007
+
2648
4008
  function scheduleProviderModelWarmup(delayMs = providerModelWarmupDelayMs) {
2649
- if (envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')) return;
4009
+ if (!modelPrefetchEnabled) return;
2650
4010
  if (providerModelWarmupTimer || closeRequested) return;
2651
4011
  providerModelWarmupTimer = setTimeout(() => {
2652
4012
  providerModelWarmupTimer = null;
@@ -2661,11 +4021,75 @@ function parsedProviderModelVersion(id) {
2661
4021
  providerModelWarmupTimer.unref?.();
2662
4022
  }
2663
4023
 
4024
+ function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
4025
+ const providerId = clean(route?.provider);
4026
+ if (!providerId || !providerId.includes('oauth')) {
4027
+ bootProfile('statusline-usage:warm-skipped', { provider: providerId || null });
4028
+ return;
4029
+ }
4030
+ if (statuslineUsageWarmupTimer || closeRequested) return;
4031
+ statuslineUsageWarmupTimer = setTimeout(async () => {
4032
+ statuslineUsageWarmupTimer = null;
4033
+ if (closeRequested) return;
4034
+ if (activeTurnCount > 0 || sessionCreatePromise) {
4035
+ bootProfile('statusline-usage:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
4036
+ scheduleStatuslineUsageWarmup(backgroundBusyRetryMs);
4037
+ return;
4038
+ }
4039
+ try {
4040
+ ensureConfigForRouteProvider();
4041
+ await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
4042
+ if (closeRequested) return;
4043
+ refreshStatuslineUsageSnapshot(route);
4044
+ bootProfile('statusline-usage:warm-ready', { provider: clean(route?.provider) });
4045
+ } catch (error) {
4046
+ bootProfile('statusline-usage:warm-failed', { error: error?.message || String(error) });
4047
+ } finally {
4048
+ scheduleStatuslineUsageRefresh();
4049
+ }
4050
+ }, delayMs);
4051
+ statuslineUsageWarmupTimer.unref?.();
4052
+ }
4053
+
4054
+ // Idle keep-alive loop: periodically re-fetch the OAuth usage snapshot so its
4055
+ // cachedAt stays "live-fresh" and the statusline usage segment does not vanish
4056
+ // after LIVE_USAGE_SNAPSHOT_MAX_AGE_MS while the session is idle. Turn-driven
4057
+ // refreshes (recordStandaloneStatusTelemetry) already cover active sessions.
4058
+ function scheduleStatuslineUsageRefresh(delayMs = statuslineUsageRefreshDelayMs) {
4059
+ const providerId = clean(route?.provider);
4060
+ if (!providerId || !providerId.includes('oauth')) return;
4061
+ if (statuslineUsageRefreshTimer || closeRequested) return;
4062
+ statuslineUsageRefreshTimer = setTimeout(async () => {
4063
+ statuslineUsageRefreshTimer = null;
4064
+ if (closeRequested) return;
4065
+ if (activeTurnCount > 0 || sessionCreatePromise) {
4066
+ // Active turns refresh usage on their own; just re-arm the idle loop.
4067
+ scheduleStatuslineUsageRefresh();
4068
+ return;
4069
+ }
4070
+ try {
4071
+ ensureConfigForRouteProvider();
4072
+ await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
4073
+ if (closeRequested) return;
4074
+ refreshStatuslineUsageSnapshot(route);
4075
+ } catch {
4076
+ // Usage display must never affect the session runtime.
4077
+ } finally {
4078
+ scheduleStatuslineUsageRefresh();
4079
+ }
4080
+ }, delayMs);
4081
+ statuslineUsageRefreshTimer.unref?.();
4082
+ }
4083
+
2664
4084
  function scheduleChannelStart(delayMs = channelStartDelayMs) {
2665
4085
  if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
2666
4086
  bootProfile('channels:start-skipped');
2667
4087
  return;
2668
4088
  }
4089
+ if (!channelsEnabled()) {
4090
+ bootProfile('channels:start-disabled');
4091
+ return;
4092
+ }
2669
4093
  if (channelStartTimer || closeRequested) return;
2670
4094
  bootProfile('channels:start-scheduled', { delayMs });
2671
4095
  channelStartTimer = setTimeout(() => {
@@ -2700,12 +4124,31 @@ function parsedProviderModelVersion(id) {
2700
4124
  });
2701
4125
  }
2702
4126
 
2703
- bootProfile('session-runtime:ready', { lazySession: true, prewarmSession: true });
4127
+ bootProfile('session-runtime:ready', {
4128
+ lazySession: true,
4129
+ prewarmSession: sessionPrewarmEnabled,
4130
+ providerWarmup: providerWarmupEnabled,
4131
+ codeGraphPrewarm: codeGraphPrewarmEnabled,
4132
+ });
2704
4133
  scheduleLeadSessionPrewarm();
4134
+ // Lazy mode (default): skip the startup prewarm entirely; the first turn
4135
+ // triggers it instead (see ask()). Eager mode keeps the old startup schedule.
4136
+ if (!codeGraphPrewarmLazy) {
4137
+ scheduleCodeGraphPrewarm(codeGraphPrewarmDelayMs, 'startup');
4138
+ } else {
4139
+ bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
4140
+ }
4141
+ scheduleProviderSetupWarmup();
2705
4142
  scheduleProviderWarmup();
4143
+ // Warm the provider model catalog in the background, but keep it on its own
4144
+ // delay so short-lived detached runtimes can exit before /models I/O starts.
4145
+ // Operators that want earlier catalog warming can lower
4146
+ // MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
4147
+ scheduleProviderModelWarmup();
4148
+ scheduleStatuslineUsageWarmup();
2706
4149
  scheduleChannelStart();
2707
4150
 
2708
- function contextStatusCacheKeyFor({ messages, tools, bridgeMode }) {
4151
+ function contextStatusCacheKeyFor({ messages, tools }) {
2709
4152
  const compaction = session?.compaction || {};
2710
4153
  const lastMessage = messages[messages.length - 1] || null;
2711
4154
  return {
@@ -2715,7 +4158,6 @@ function parsedProviderModelVersion(id) {
2715
4158
  model: route.model,
2716
4159
  cwd: currentCwd,
2717
4160
  mode,
2718
- bridgeMode,
2719
4161
  messages,
2720
4162
  messageCount: messages.length,
2721
4163
  lastMessage,
@@ -2788,23 +4230,25 @@ function parsedProviderModelVersion(id) {
2788
4230
  get toolMode() {
2789
4231
  return mode;
2790
4232
  },
2791
- get bridgeMode() {
2792
- return bridge.getDefaultMode();
2793
- },
2794
4233
  get autoClear() {
2795
4234
  return normalizeAutoClearConfig(config.autoClear);
2796
4235
  },
2797
4236
  get systemShell() {
2798
4237
  return normalizeSystemShellConfig(config.shell);
2799
4238
  },
4239
+ get searchRoute() {
4240
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
4241
+ return searchRoute;
4242
+ },
2800
4243
  get workflow() {
2801
4244
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
2802
- const pack = loadWorkflowPack(dataDir, activeWorkflowId(config));
2803
- return pack ? { id: pack.id, name: pack.name, description: pack.description, source: pack.source } : { id: DEFAULT_WORKFLOW_ID, name: 'Default' };
4245
+ if (session?.workflow && typeof session.workflow === 'object') {
4246
+ return workflowSummary(session.workflow);
4247
+ }
4248
+ return activeWorkflowSummary(config, dataDir);
2804
4249
  },
2805
4250
  get outputStyle() {
2806
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
2807
- return outputStyleStatus(dataDir).current;
4251
+ return getOutputStyleStatusCached().current;
2808
4252
  },
2809
4253
  get cwd() {
2810
4254
  return currentCwd;
@@ -2813,10 +4257,16 @@ function parsedProviderModelVersion(id) {
2813
4257
  return session;
2814
4258
  },
2815
4259
  contextStatus() {
2816
- const messages = Array.isArray(session?.messages) ? session.messages : [];
4260
+ // Prefer the in-flight working transcript while a turn is running so the
4261
+ // context gauge reflects LIVE growth (user turn + tool calls/results) as
4262
+ // it accumulates, instead of freezing at the pre-turn committed snapshot.
4263
+ // askSession() sets session.liveTurnMessages for the turn duration and
4264
+ // clears it on commit/cancel/error, after which we fall back to the
4265
+ // authoritative committed transcript.
4266
+ const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
4267
+ const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
2817
4268
  const tools = Array.isArray(session?.tools) ? session.tools : [];
2818
- const bridgeMode = bridge.getDefaultMode();
2819
- const cacheKey = contextStatusCacheKeyFor({ messages, tools, bridgeMode });
4269
+ const cacheKey = contextStatusCacheKeyFor({ messages, tools });
2820
4270
  if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
2821
4271
  return contextStatusCacheValue;
2822
4272
  }
@@ -2829,7 +4279,9 @@ function parsedProviderModelVersion(id) {
2829
4279
  const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
2830
4280
  const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
2831
4281
  const lastContextTokens = Number(session?.lastContextTokens || 0);
2832
- const estimatedContextTokens = messageSummary.estimatedTokens + requestReserveTokens;
4282
+ const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
4283
+ messageCount: messageSummary.count,
4284
+ });
2833
4285
  const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
2834
4286
  const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
2835
4287
  const lastUsageStale = !!lastContextTokens && (
@@ -2839,31 +4291,58 @@ function parsedProviderModelVersion(id) {
2839
4291
  );
2840
4292
  const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
2841
4293
  const displayWindow = compactBoundaryTokens || effectiveWindow;
2842
- const usedTokens = lastUsageStale
2843
- ? estimatedContextTokens
2844
- : Math.max(estimatedContextTokens, lastContextTokens || 0);
4294
+ // The transcript estimate is the single source of truth for the displayed
4295
+ // context footprint. Provider-reported input_tokens (lastContextTokens)
4296
+ // swing non-monotonically and are not window-bounded on some providers
4297
+ // (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
4298
+ // metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
4299
+ // the gauge numerator.
4300
+ const usedTokens = estimatedContextTokens;
2845
4301
  const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
2846
4302
  const autoCompactTokenLimit = Number(session?.autoCompactTokenLimit || 0);
2847
- const defaultCompactTriggerTokens = compactBoundaryTokens ? Math.max(1, compactBoundaryTokens) : 0;
2848
- const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit <= compactBoundaryTokens
4303
+ // The runtime fires auto-compaction at the effective boundary by default.
4304
+ // Compute the same trigger here so /context shows what will actually fire
4305
+ // before any compaction telemetry (session.compaction.triggerTokens) is
4306
+ // recorded. Explicit buffer settings can still lower the trigger.
4307
+ // An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
4308
+ // still takes precedence and is honored verbatim when it sits at or below
4309
+ // the boundary.
4310
+ const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
4311
+ const defaultCompactTriggerTokens = compactBoundaryTokens
4312
+ ? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
4313
+ : 0;
4314
+ // Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
4315
+ // real trigger; a persisted value == boundary is a legacy derived
4316
+ // full-window artifact and must fall through to the default trigger.
4317
+ // Likewise ignore persisted default-buffer telemetry so the displayed
4318
+ // trigger matches what the runtime now actually fires.
4319
+ const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
4320
+ const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
4321
+ && persistedTriggerTokens > 0
4322
+ && persistedTriggerTokens < compactBoundaryTokens;
4323
+ const usablePersistedTrigger = persistedTriggerTokens > 0
4324
+ && !legacyDefaultTriggerTelemetry
4325
+ && (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
4326
+ ? persistedTriggerTokens
4327
+ : 0;
4328
+ const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
2849
4329
  ? autoCompactTokenLimit
2850
- : Number(session?.compaction?.triggerTokens || defaultCompactTriggerTokens || 0);
2851
- const compactBufferTokens = Number(session?.compaction?.bufferTokens || (compactBoundaryTokens && compactTriggerTokens ? Math.max(0, compactBoundaryTokens - compactTriggerTokens) : 0));
4330
+ : (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
4331
+ const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
4332
+ ? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
4333
+ : defaultCompactBufferTokens);
2852
4334
  const value = {
2853
4335
  sessionId: session?.id || null,
2854
4336
  provider: route.provider,
2855
4337
  model: route.model,
2856
4338
  cwd: currentCwd,
2857
4339
  toolMode: mode,
2858
- bridgeMode,
2859
4340
  contextWindow: displayWindow || effectiveWindow || null,
2860
4341
  effectiveContextWindow: effectiveWindow || null,
2861
4342
  rawContextWindow: rawWindow || null,
2862
4343
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
2863
4344
  usedTokens,
2864
- usedSource: lastContextTokens && !lastUsageStale && lastContextTokens >= estimatedContextTokens
2865
- ? 'last_api_request'
2866
- : 'estimated',
4345
+ usedSource: 'estimated',
2867
4346
  currentEstimatedTokens: estimatedContextTokens,
2868
4347
  lastApiRequestTokens: lastContextTokens || 0,
2869
4348
  lastApiRequestStale: lastUsageStale,
@@ -2901,26 +4380,64 @@ function parsedProviderModelVersion(id) {
2901
4380
  return value;
2902
4381
  },
2903
4382
  listProviders() {
2904
- return renderProviderStatus(cfgMod.loadConfig());
4383
+ return renderProviderStatus(displayConfig());
2905
4384
  },
2906
4385
  async getProviderSetup() {
2907
4386
  return await cachedProviderSetup();
2908
4387
  },
2909
4388
  async getUsageDashboard(options = {}) {
2910
- const nextConfig = cfgMod.loadConfig();
2911
- return await createUsageDashboard(nextConfig, {
2912
- ...(options || {}),
2913
- setup: await cachedProviderSetup(),
2914
- getProvider: (providerId) => reg.getProvider(providerId),
2915
- log: (message) => {
2916
- if (process.env.MIXDOG_USAGE_TRACE) {
2917
- try { process.stderr.write(`[usage] ${message}\n`); } catch {}
2918
- }
2919
- },
2920
- });
4389
+ const forceSetup = options?.force === true || options?.refresh === true;
4390
+ if (!forceSetup && usageDashboardCache.dashboard) {
4391
+ const cached = {
4392
+ ...usageDashboardCache.dashboard,
4393
+ refresh: false,
4394
+ checking: false,
4395
+ cached: true,
4396
+ cachedAt: usageDashboardCache.at,
4397
+ };
4398
+ if (typeof options?.onUpdate === 'function') {
4399
+ try { options.onUpdate(cached); } catch {}
4400
+ }
4401
+ return cached;
4402
+ }
4403
+ if (!forceSetup && usageDashboardPromise) return await usageDashboardPromise;
4404
+ const quickSetup = options?.quickSetup !== false;
4405
+ const getProvider = (providerId) => reg.getProvider(providerId);
4406
+ const log = (message) => {
4407
+ if (process.env.MIXDOG_USAGE_TRACE) {
4408
+ try { process.stderr.write(`[usage] ${message}\n`); } catch {}
4409
+ }
4410
+ };
4411
+ if (quickSetup && typeof options?.onUpdate === 'function') {
4412
+ const previewConfig = displayConfig();
4413
+ const previewSetup = await cachedProviderSetup({ force: false, quick: true });
4414
+ await createUsageDashboard(previewConfig, {
4415
+ ...(options || {}),
4416
+ preview: true,
4417
+ setup: previewSetup,
4418
+ getProvider,
4419
+ log,
4420
+ });
4421
+ }
4422
+ const buildDashboard = async () => {
4423
+ const dashboard = await createUsageDashboard(displayConfig(), {
4424
+ ...(options || {}),
4425
+ setup: await cachedProviderSetup({ force: forceSetup, quick: false }),
4426
+ getProvider,
4427
+ log,
4428
+ });
4429
+ usageDashboardCache = { dashboard, at: Date.now() };
4430
+ return dashboard;
4431
+ };
4432
+ if (forceSetup) return await buildDashboard();
4433
+ usageDashboardPromise = buildDashboard()
4434
+ .finally(() => {
4435
+ usageDashboardPromise = null;
4436
+ });
4437
+ return await usageDashboardPromise;
2921
4438
  },
2922
4439
  getOnboardingStatus() {
2923
- const nextConfig = cfgMod.loadConfig();
4440
+ const nextConfig = displayConfig();
2924
4441
  return {
2925
4442
  completed: nextConfig?.onboarding?.completed === true,
2926
4443
  version: nextConfig?.onboarding?.version || 0,
@@ -2931,17 +4448,118 @@ function parsedProviderModelVersion(id) {
2931
4448
  getAutoClear() {
2932
4449
  return normalizeAutoClearConfig(config.autoClear);
2933
4450
  },
4451
+ // --- User profile (/profile statusline command) ---------------------
4452
+ // getProfile returns the normalized { title, language } plus the resolved
4453
+ // language catalog entry and the full language list for the picker UI.
4454
+ getProfile() {
4455
+ const profile = cfgMod.normalizeProfileConfig(config.profile);
4456
+ return {
4457
+ ...profile,
4458
+ languageEntry: cfgMod.profileLanguageEntry(profile.language),
4459
+ languages: cfgMod.PROFILE_LANGUAGES,
4460
+ };
4461
+ },
4462
+ // setProfile patches title and/or language and persists. Unknown language
4463
+ // ids normalize back to 'system'. Prompt-side injection is wired separately
4464
+ // (composeSystemPrompt) — this only owns the stored value.
4465
+ setProfile(input = {}) {
4466
+ const current = cfgMod.normalizeProfileConfig(config.profile);
4467
+ const next = { ...current };
4468
+ if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
4469
+ next.title = input.title ?? input.name ?? '';
4470
+ }
4471
+ if (hasOwn(input, 'language') || hasOwn(input, 'lang')) {
4472
+ next.language = input.language ?? input.lang ?? 'system';
4473
+ }
4474
+ const normalized = cfgMod.normalizeProfileConfig(next);
4475
+ saveConfigAndAdopt({ ...config, profile: normalized });
4476
+ return this.getProfile();
4477
+ },
4478
+ getCompactionSettings() {
4479
+ return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
4480
+ },
4481
+ setCompactionSettings(input = {}) {
4482
+ const current = normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
4483
+ const next = { ...current };
4484
+ if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
4485
+ if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
4486
+ if (hasOwn(input, 'type') || hasOwn(input, 'compactType') || hasOwn(input, 'compact_type')) {
4487
+ const requestedType = input.type ?? input.compactType ?? input.compact_type;
4488
+ const compactType = normalizeCompactTypeSetting(requestedType, current.compactType || current.type || 'semantic');
4489
+ if (compactType === 'recall-fasttrack' && !memoryEnabled()) {
4490
+ throw new Error('recall-fasttrack compact requires memory to be enabled');
4491
+ }
4492
+ next.type = compactType;
4493
+ next.compactType = compactType;
4494
+ }
4495
+ const nextConfig = { ...config };
4496
+ nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
4497
+ saveConfigAndAdopt(nextConfig);
4498
+ if (session) {
4499
+ session.compaction = {
4500
+ ...(session.compaction || {}),
4501
+ ...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
4502
+ };
4503
+ }
4504
+ invalidateContextStatusCache();
4505
+ return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
4506
+ },
4507
+ getMemorySettings() {
4508
+ return {
4509
+ enabled: memoryEnabled(),
4510
+ compactFastTrackAvailable: memoryEnabled(),
4511
+ };
4512
+ },
4513
+ async setMemoryEnabled(enabled) {
4514
+ const nextConfig = setModuleEnabledInConfig({ ...config }, 'memory', enabled !== false);
4515
+ if (enabled === false) {
4516
+ nextConfig.compaction = normalizeCompactionConfig(nextConfig.compaction, { memoryEnabled: false });
4517
+ }
4518
+ saveConfigAndAdopt(nextConfig);
4519
+ if (!memoryEnabled() && memoryModPromise) {
4520
+ await memoryModPromise.then((mod) => mod?.stop?.()).catch(() => {});
4521
+ memoryModPromise = null;
4522
+ }
4523
+ if (session && config.compaction) {
4524
+ session.compaction = {
4525
+ ...(session.compaction || {}),
4526
+ ...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
4527
+ };
4528
+ }
4529
+ invalidatePreSessionToolSurface();
4530
+ invalidateContextStatusCache();
4531
+ return this.getMemorySettings();
4532
+ },
4533
+ getChannelSettings(options = {}) {
4534
+ return {
4535
+ enabled: channelsEnabled(),
4536
+ ...(options?.includeStatus === false ? {} : { status: channels.status() }),
4537
+ };
4538
+ },
4539
+ async setChannelsEnabled(enabled) {
4540
+ const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
4541
+ saveConfigAndAdopt(nextConfig);
4542
+ if (!channelsEnabled()) {
4543
+ if (channelStartTimer) {
4544
+ clearTimeout(channelStartTimer);
4545
+ channelStartTimer = null;
4546
+ }
4547
+ await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
4548
+ } else {
4549
+ scheduleChannelStart(0);
4550
+ }
4551
+ invalidatePreSessionToolSurface();
4552
+ return this.getChannelSettings();
4553
+ },
2934
4554
  getSystemShell() {
2935
4555
  return normalizeSystemShellConfig(config.shell);
2936
4556
  },
2937
4557
  setSystemShell(input = {}) {
2938
4558
  const command = normalizeSystemShellCommand(typeof input === 'string' ? input : input?.command);
2939
- const nextConfig = cfgMod.loadConfig();
2940
- cfgMod.saveConfig({
2941
- ...nextConfig,
2942
- shell: command ? { ...(nextConfig.shell || {}), command } : {},
4559
+ saveConfigAndAdopt({
4560
+ ...config,
4561
+ shell: command ? { ...(config.shell || {}), command } : {},
2943
4562
  });
2944
- config = cfgMod.loadConfig();
2945
4563
  setConfiguredShell(command);
2946
4564
  return normalizeSystemShellConfig(config.shell);
2947
4565
  },
@@ -2960,9 +4578,7 @@ function parsedProviderModelVersion(id) {
2960
4578
  next.idleMs = idleMs;
2961
4579
  if (!hasOwn(input, 'enabled')) next.enabled = true;
2962
4580
  }
2963
- const nextConfig = cfgMod.loadConfig();
2964
- cfgMod.saveConfig({ ...nextConfig, autoClear: next });
2965
- config = cfgMod.loadConfig();
4581
+ saveConfigAndAdopt({ ...config, autoClear: next });
2966
4582
  return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
2967
4583
  },
2968
4584
  async completeOnboarding(payload = {}) {
@@ -2970,7 +4586,7 @@ function parsedProviderModelVersion(id) {
2970
4586
  const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
2971
4587
  ? payload.workflowRoutes
2972
4588
  : {};
2973
- const nextConfig = cfgMod.loadConfig();
4589
+ const nextConfig = { ...config };
2974
4590
  let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
2975
4591
  const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
2976
4592
 
@@ -2989,10 +4605,13 @@ function parsedProviderModelVersion(id) {
2989
4605
 
2990
4606
  nextConfig.presets = presets;
2991
4607
  nextConfig.workflowRoutes = workflowRoutes;
4608
+ // Maintenance slots store a direct {provider, model} route. Reuse the
4609
+ // onboarding workflow routes directly; only overwrite a slot when its
4610
+ // workflow route was actually provided, otherwise keep the existing slot.
2992
4611
  nextConfig.maintenance = {
2993
4612
  ...(nextConfig.maintenance || {}),
2994
- explore: workflowRoutes.explorer ? workflowPresetId('explorer') : (nextConfig.maintenance?.explore || 'haiku'),
2995
- memory: workflowRoutes.memory ? workflowPresetId('memory') : (nextConfig.maintenance?.memory || 'haiku'),
4613
+ ...(workflowRoutes.explorer ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
4614
+ ...(workflowRoutes.memory ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
2996
4615
  };
2997
4616
  nextConfig.onboarding = {
2998
4617
  ...(nextConfig.onboarding || {}),
@@ -3001,8 +4620,7 @@ function parsedProviderModelVersion(id) {
3001
4620
  completedAt: new Date().toISOString(),
3002
4621
  };
3003
4622
 
3004
- cfgMod.saveConfig(nextConfig);
3005
- config = cfgMod.loadConfig();
4623
+ saveConfigAndAdopt(nextConfig);
3006
4624
  if (defaultRoute) {
3007
4625
  route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
3008
4626
  if (session?.id) mgr.closeSession(session.id, 'cli-onboarding-complete');
@@ -3085,26 +4703,34 @@ function parsedProviderModelVersion(id) {
3085
4703
  const result = String(secret || '').trim()
3086
4704
  ? saveProviderApiKey(cfgMod, providerId, secret)
3087
4705
  : await loginOAuthProvider(cfgMod, providerId);
3088
- config = cfgMod.loadConfig();
4706
+ reloadFullConfig();
3089
4707
  invalidateProviderCaches();
3090
4708
  warmProviderModelCache();
3091
4709
  return result;
3092
4710
  },
3093
4711
  async loginOAuthProvider(providerId) {
3094
4712
  const result = await loginOAuthProvider(cfgMod, providerId);
3095
- config = cfgMod.loadConfig();
4713
+ reloadFullConfig();
3096
4714
  invalidateProviderCaches();
3097
4715
  warmProviderModelCache();
3098
4716
  return result;
3099
4717
  },
3100
4718
  async beginOAuthProviderLogin(providerId) {
3101
4719
  const result = await beginOAuthProviderLogin(cfgMod, providerId);
3102
- config = cfgMod.loadConfig();
4720
+ reloadFullConfig();
3103
4721
  return {
3104
4722
  ...result,
4723
+ waitForCallback: result.waitForCallback?.then((completed) => {
4724
+ reloadFullConfig();
4725
+ if (completed) {
4726
+ invalidateProviderCaches();
4727
+ warmProviderModelCache();
4728
+ }
4729
+ return completed;
4730
+ }),
3105
4731
  completeCode: async (code) => {
3106
4732
  const completed = await result.completeCode(code);
3107
- config = cfgMod.loadConfig();
4733
+ reloadFullConfig();
3108
4734
  invalidateProviderCaches();
3109
4735
  warmProviderModelCache();
3110
4736
  return completed;
@@ -3113,42 +4739,97 @@ function parsedProviderModelVersion(id) {
3113
4739
  },
3114
4740
  saveProviderApiKey(providerId, secret) {
3115
4741
  const result = saveProviderApiKey(cfgMod, providerId, secret);
3116
- config = cfgMod.loadConfig();
4742
+ reloadFullConfig();
3117
4743
  invalidateProviderCaches();
3118
4744
  warmProviderModelCache();
3119
4745
  return result;
3120
4746
  },
3121
4747
  saveOpenAIUsageSessionKey(secret) {
3122
4748
  const result = saveOpenAIUsageSessionKey(cfgMod, secret);
3123
- config = cfgMod.loadConfig();
4749
+ reloadFullConfig();
3124
4750
  invalidateProviderCaches();
3125
4751
  return result;
3126
4752
  },
3127
4753
  saveOpenCodeGoUsageAuth(opts) {
3128
4754
  const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
3129
- config = cfgMod.loadConfig();
4755
+ reloadFullConfig();
3130
4756
  invalidateProviderCaches();
3131
4757
  return result;
3132
4758
  },
3133
4759
  setLocalProvider(providerId, opts) {
3134
4760
  const result = setLocalProvider(cfgMod, providerId, opts);
3135
- config = cfgMod.loadConfig();
4761
+ reloadFullConfig();
3136
4762
  invalidateProviderCaches();
3137
4763
  warmProviderModelCache();
3138
4764
  return result;
3139
4765
  },
3140
4766
  forgetProviderAuth(providerId) {
3141
- const result = forgetProviderAuth(providerId);
3142
- config = cfgMod.loadConfig();
4767
+ const result = forgetProviderAuth(cfgMod, providerId);
4768
+ reloadFullConfig();
3143
4769
  invalidateProviderCaches();
3144
4770
  warmProviderModelCache();
3145
4771
  return result;
3146
4772
  },
3147
4773
  listPresets() {
3148
- return cfgMod.listPresets(cfgMod.loadConfig());
4774
+ return cfgMod.listPresets(displayConfig());
3149
4775
  },
3150
4776
  async listProviderModels(options = {}) {
3151
- return await collectProviderModels({ force: options.force === true || options.refresh === true });
4777
+ return await collectProviderModels({
4778
+ force: options.force === true || options.refresh === true,
4779
+ quick: options.quick === true,
4780
+ });
4781
+ },
4782
+ getSearchRoute() {
4783
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
4784
+ return searchRoute;
4785
+ },
4786
+ async listSearchModels(options = {}) {
4787
+ return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
4788
+ },
4789
+ async setSearchRoute(next) {
4790
+ let selectedRoute = normalizeSearchRouteConfig(next);
4791
+ if (!selectedRoute && next?.model && searchRoute?.provider) {
4792
+ selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
4793
+ }
4794
+ if (!selectedRoute) throw new Error('search route requires provider and model');
4795
+ if (isDefaultSearchRouteConfig(selectedRoute)) {
4796
+ ensureFullConfig();
4797
+ const routeToSave = normalizeSearchRouteConfig({
4798
+ provider: SEARCH_DEFAULT_PROVIDER,
4799
+ model: SEARCH_DEFAULT_MODEL,
4800
+ ...(selectedRoute.toolType ? { toolType: selectedRoute.toolType } : {}),
4801
+ });
4802
+ const nextConfig = { ...config };
4803
+ nextConfig.searchRoute = routeToSave;
4804
+ saveConfigAndAdopt(nextConfig);
4805
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute);
4806
+ invalidateProviderCaches();
4807
+ return searchRoute;
4808
+ }
4809
+ if (!isSearchCapableProvider(selectedRoute.provider)) {
4810
+ throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
4811
+ }
4812
+ ensureFullConfig();
4813
+ await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
4814
+ const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
4815
+ if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
4816
+ throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
4817
+ }
4818
+ const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
4819
+ const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
4820
+ selectedRoute = {
4821
+ ...selectedRoute,
4822
+ ...(effort ? { effort } : {}),
4823
+ fast: fastCapable ? selectedRoute.fast === true : false,
4824
+ };
4825
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
4826
+ const routeToSave = normalizeSearchRouteConfig(selectedRoute);
4827
+ const nextConfig = { ...config };
4828
+ nextConfig.searchRoute = routeToSave;
4829
+ saveConfigAndAdopt(nextConfig);
4830
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute);
4831
+ invalidateProviderCaches();
4832
+ return searchRoute;
3152
4833
  },
3153
4834
  listAgents() {
3154
4835
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
@@ -3160,7 +4841,7 @@ function parsedProviderModelVersion(id) {
3160
4841
  }));
3161
4842
  },
3162
4843
  listWorkflows() {
3163
- const currentConfig = cfgMod.loadConfig();
4844
+ const currentConfig = displayConfig();
3164
4845
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3165
4846
  const active = activeWorkflowId(currentConfig);
3166
4847
  return listWorkflowPacks(dataDir).map((workflow) => ({
@@ -3173,16 +4854,13 @@ function parsedProviderModelVersion(id) {
3173
4854
  }));
3174
4855
  },
3175
4856
  getOutputStyle() {
3176
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3177
- return outputStyleStatus(dataDir);
4857
+ return getOutputStyleStatusCached();
3178
4858
  },
3179
4859
  listOutputStyles() {
3180
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3181
- return outputStyleStatus(dataDir);
4860
+ return getOutputStyleStatusCached();
3182
4861
  },
3183
4862
  async setOutputStyle(value) {
3184
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3185
- const before = outputStyleStatus(dataDir);
4863
+ const before = getOutputStyleStatusCached({ fresh: true });
3186
4864
  const selected = findOutputStyle(value, before.styles);
3187
4865
  if (!selected) {
3188
4866
  const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
@@ -3198,6 +4876,7 @@ function parsedProviderModelVersion(id) {
3198
4876
  }
3199
4877
  return next;
3200
4878
  });
4879
+ invalidateOutputStyleStatusCache();
3201
4880
  const hasConversation = sessionHasConversationMessages(session);
3202
4881
  let appliedToCurrentSession = !hasConversation;
3203
4882
  if (session?.id && !hasConversation) {
@@ -3206,23 +4885,17 @@ function parsedProviderModelVersion(id) {
3206
4885
  await recreateCurrentSessionIfReady();
3207
4886
  }
3208
4887
  invalidateContextStatusCache();
3209
- return { ...outputStyleStatus(dataDir), appliedToCurrentSession };
4888
+ return { ...getOutputStyleStatusCached({ fresh: true }), appliedToCurrentSession };
3210
4889
  },
3211
4890
  async setWorkflow(workflowId) {
3212
4891
  const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
3213
4892
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3214
4893
  const pack = loadWorkflowPack(dataDir, id);
3215
4894
  if (!pack || pack.id !== id) throw new Error(`workflow "${workflowId}" not found`);
3216
- const nextConfig = cfgMod.loadConfig();
4895
+ const nextConfig = { ...config };
3217
4896
  nextConfig.workflow = { ...(nextConfig.workflow || {}), active: id };
3218
- cfgMod.saveConfig(nextConfig);
3219
- config = cfgMod.loadConfig();
3220
- if (session?.id) {
3221
- mgr.closeSession(session.id, 'cli-workflow-switch');
3222
- session = null;
3223
- }
3224
- await recreateCurrentSessionIfReady();
3225
- return { id: pack.id, name: pack.name, description: pack.description, source: pack.source };
4897
+ saveConfigAndAdopt(nextConfig);
4898
+ return workflowSummary(pack);
3226
4899
  },
3227
4900
  async setAgentRoute(agentId, next) {
3228
4901
  const id = normalizeAgentId(agentId);
@@ -3232,12 +4905,12 @@ function parsedProviderModelVersion(id) {
3232
4905
  const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
3233
4906
  const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
3234
4907
  selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
3235
- config = saveModelSettings(cfgMod, selectedRoute, { fastCapable });
4908
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
3236
4909
 
3237
4910
  const routeToSave = normalizeWorkflowRoute(selectedRoute);
3238
4911
  if (!routeToSave) throw new Error('agent route requires provider and model');
3239
4912
  const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
3240
- const nextConfig = cfgMod.loadConfig();
4913
+ const nextConfig = { ...config };
3241
4914
  nextConfig.agents = {
3242
4915
  ...(nextConfig.agents || {}),
3243
4916
  [id]: routeToSave,
@@ -3249,24 +4922,48 @@ function parsedProviderModelVersion(id) {
3249
4922
  [agent.workflowSlot]: routeToSave,
3250
4923
  };
3251
4924
  nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, agent.workflowSlot, routeToSave);
4925
+ // Maintenance slots store a direct {provider, model} route now, so
4926
+ // mirror the agent route straight in instead of a preset-id string.
3252
4927
  nextConfig.maintenance = {
3253
4928
  ...(nextConfig.maintenance || {}),
3254
- ...(id === 'explore' ? { explore: workflowPresetId('explorer') } : {}),
3255
- ...(id === 'maintainer' ? { memory: workflowPresetId('memory') } : {}),
4929
+ ...(id === 'explore' ? { explore: routeToSave } : {}),
4930
+ ...(id === 'maintainer' ? { memory: routeToSave } : {}),
3256
4931
  };
3257
4932
  }
3258
- cfgMod.saveConfig(nextConfig);
3259
- config = cfgMod.loadConfig();
4933
+ saveConfigAndAdopt(nextConfig);
3260
4934
  return routeToSave;
3261
4935
  },
3262
4936
  async ask(prompt, options = {}) {
3263
4937
  activeTurnCount += 1;
4938
+ // Lazy code-graph prewarm: kick off the build ONCE, on the first real
4939
+ // turn, so a likely code lookup hits a warm cache — without paying the
4940
+ // post-first-frame prewarm freeze on idle startup. The schedule is async
4941
+ // + unref'd (worker thread), so it never blocks this turn.
4942
+ if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
4943
+ codeGraphFirstTurnPrewarmDone = true;
4944
+ scheduleCodeGraphPrewarm(0, 'first-turn');
4945
+ }
3264
4946
  const startedAt = Date.now();
3265
4947
  try {
3266
4948
  await refreshSessionForCwdIfNeeded('cwd-change');
3267
4949
  if (!session?.id) await createCurrentSession('turn');
3268
4950
  hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
3269
- const turnContext = [options.context || ''].map((part) => String(part || '').trim()).filter(Boolean).join('\n\n');
4951
+ // UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
4952
+ // must not block the turn, but a genuine blocked===true MUST throw.
4953
+ let promptDispatch = null;
4954
+ try {
4955
+ promptDispatch = await hooks.dispatch('UserPromptSubmit', hookCommonPayload({ session_id: session.id, prompt }));
4956
+ } catch { /* hook failure never blocks the turn */ }
4957
+ if (promptDispatch?.blocked === true) {
4958
+ throw new Error(`prompt blocked by hook: ${promptDispatch.reason || ''}`);
4959
+ }
4960
+ const hookContext = Array.isArray(promptDispatch?.additionalContext)
4961
+ ? promptDispatch.additionalContext.join('\n\n')
4962
+ : String(promptDispatch?.additionalContext || '');
4963
+ const turnContext = [options.context || '', hookContext]
4964
+ .map((part) => String(part || '').trim())
4965
+ .filter(Boolean)
4966
+ .join('\n\n');
3270
4967
  const result = await mgr.askSession(
3271
4968
  session.id,
3272
4969
  prompt,
@@ -3289,16 +4986,25 @@ function parsedProviderModelVersion(id) {
3289
4986
  {
3290
4987
  onTextDelta: options.onTextDelta,
3291
4988
  onReasoningDelta: options.onReasoningDelta,
4989
+ onAssistantText: options.onAssistantText,
3292
4990
  onUsageDelta: options.onUsageDelta,
3293
4991
  onToolResult: options.onToolResult,
4992
+ onToolApproval: options.onToolApproval,
4993
+ onCompactEvent: options.onCompactEvent,
3294
4994
  onStageChange: options.onStageChange,
3295
4995
  onStreamDelta: options.onStreamDelta,
3296
4996
  drainSteering: options.drainSteering,
3297
4997
  onSteerMessage: options.onSteerMessage,
4998
+ notifyFn: notifyFnForSession(session.id),
3298
4999
  },
3299
5000
  );
3300
5001
  session = mgr.getSession(session.id) || session;
3301
5002
  hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
5003
+ // Stop: bridge to the standard hook bus. Best-effort; ignore result,
5004
+ // never throw.
5005
+ try {
5006
+ await hooks.dispatch('Stop', hookCommonPayload({ session_id: session.id }));
5007
+ } catch { /* best-effort: Stop hook must never break the turn */ }
3302
5008
  return { result, session };
3303
5009
  } catch (error) {
3304
5010
  hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
@@ -3307,27 +5013,31 @@ function parsedProviderModelVersion(id) {
3307
5013
  activeTurnCount = Math.max(0, activeTurnCount - 1);
3308
5014
  if (!firstTurnCompleted) {
3309
5015
  firstTurnCompleted = true;
5016
+ scheduleProviderWarmup();
3310
5017
  scheduleProviderModelWarmup();
3311
5018
  }
3312
5019
  }
3313
5020
  },
3314
5021
  async clear(options = {}) {
3315
5022
  if (!session?.id) return false;
3316
- const cleared = await mgr.clearSessionMessages(session.id);
5023
+ const cleared = await mgr.clearSessionMessages(session.id, options);
3317
5024
  if (!cleared) return false;
3318
5025
  session = typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session);
3319
- if (options.recoverBridge === true) {
3320
- try { bridge.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
5026
+ if (options.recoverAgent === true) {
5027
+ try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
3321
5028
  }
3322
5029
  invalidateContextStatusCache();
3323
5030
  return true;
3324
5031
  },
3325
5032
  async compact(options = {}) {
3326
5033
  if (!session?.id) return null;
5034
+ if (activeTurnCount > 0) {
5035
+ return { changed: false, reason: 'compact skipped: turn in progress' };
5036
+ }
3327
5037
  const result = await mgr.compactSessionMessages(session.id);
3328
5038
  session = mgr.getSession(session.id) || session;
3329
- if (options.recoverBridge === true) {
3330
- try { bridge.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
5039
+ if (options.recoverAgent === true) {
5040
+ try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
3331
5041
  }
3332
5042
  invalidateContextStatusCache();
3333
5043
  return result;
@@ -3339,25 +5049,15 @@ function parsedProviderModelVersion(id) {
3339
5049
  await recreateCurrentSessionIfReady();
3340
5050
  return mode;
3341
5051
  },
3342
- setBridgeMode(nextMode) {
3343
- const applied = bridge.setDefaultMode(nextMode);
3344
- try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
3345
- return applied;
3346
- },
3347
- toggleBridgeMode() {
3348
- const applied = bridge.toggleDefaultMode();
3349
- try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
3350
- return applied;
3351
- },
3352
- bridgeStatus() {
3353
- return bridgeStatusState();
5052
+ agentStatus() {
5053
+ return agentStatusState();
3354
5054
  },
3355
5055
  get clientHostPid() {
3356
5056
  return session?.clientHostPid || process.pid;
3357
5057
  },
3358
- bridgeControl(args = {}) {
5058
+ agentControl(args = {}) {
3359
5059
  const callerSessionId = session?.id || null;
3360
- return bridge.execute(args, {
5060
+ return agentTool.execute(args, {
3361
5061
  callerCwd: currentCwd,
3362
5062
  invocationSource: 'user-command',
3363
5063
  callerSessionId,
@@ -3387,6 +5087,7 @@ function parsedProviderModelVersion(id) {
3387
5087
  activeCount: rows.filter((row) => row.active).length,
3388
5088
  tools,
3389
5089
  activeTools: sortedNamesByMeasuredUsage(activeNames),
5090
+ discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
3390
5091
  };
3391
5092
  },
3392
5093
  selectTools(names) {
@@ -3402,7 +5103,7 @@ function parsedProviderModelVersion(id) {
3402
5103
  return mcpStatus();
3403
5104
  },
3404
5105
  async reconnectMcp() {
3405
- config = cfgMod.loadConfig();
5106
+ reloadFullConfig();
3406
5107
  const status = await connectConfiguredMcp({ reset: true });
3407
5108
  invalidatePreSessionToolSurface();
3408
5109
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect');
@@ -3411,13 +5112,12 @@ function parsedProviderModelVersion(id) {
3411
5112
  },
3412
5113
  async addMcpServer(input = {}) {
3413
5114
  const { name, config: serverConfig } = normalizeMcpServerInput(input);
3414
- const nextConfig = cfgMod.loadConfig();
5115
+ const nextConfig = { ...config };
3415
5116
  nextConfig.mcpServers = {
3416
5117
  ...(nextConfig.mcpServers || {}),
3417
5118
  [name]: serverConfig,
3418
5119
  };
3419
- cfgMod.saveConfig(nextConfig);
3420
- config = cfgMod.loadConfig();
5120
+ saveConfigAndAdopt(nextConfig);
3421
5121
  const status = await connectConfiguredMcp({ reset: true });
3422
5122
  invalidatePreSessionToolSurface();
3423
5123
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add');
@@ -3427,7 +5127,7 @@ function parsedProviderModelVersion(id) {
3427
5127
  async removeMcpServer(name) {
3428
5128
  const serverName = clean(name);
3429
5129
  if (!serverName) throw new Error('MCP server name is required');
3430
- const nextConfig = cfgMod.loadConfig();
5130
+ const nextConfig = { ...config };
3431
5131
  const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
3432
5132
  ? { ...nextConfig.mcpServers }
3433
5133
  : {};
@@ -3435,8 +5135,7 @@ function parsedProviderModelVersion(id) {
3435
5135
  throw new Error(`MCP server not configured: ${serverName}`);
3436
5136
  }
3437
5137
  delete current[serverName];
3438
- cfgMod.saveConfig({ ...nextConfig, mcpServers: current });
3439
- config = cfgMod.loadConfig();
5138
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
3440
5139
  const status = await connectConfiguredMcp({ reset: true });
3441
5140
  invalidatePreSessionToolSurface();
3442
5141
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove');
@@ -3446,7 +5145,7 @@ function parsedProviderModelVersion(id) {
3446
5145
  async setMcpServerEnabled(name, enabled) {
3447
5146
  const serverName = clean(name);
3448
5147
  if (!serverName) throw new Error('MCP server name is required');
3449
- const nextConfig = cfgMod.loadConfig();
5148
+ const nextConfig = { ...config };
3450
5149
  const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
3451
5150
  ? { ...nextConfig.mcpServers }
3452
5151
  : {};
@@ -3454,8 +5153,7 @@ function parsedProviderModelVersion(id) {
3454
5153
  throw new Error(`MCP server not configured: ${serverName}`);
3455
5154
  }
3456
5155
  current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
3457
- cfgMod.saveConfig({ ...nextConfig, mcpServers: current });
3458
- config = cfgMod.loadConfig();
5156
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
3459
5157
  const status = await connectConfiguredMcp({ reset: true });
3460
5158
  invalidatePreSessionToolSurface();
3461
5159
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-toggle');
@@ -3506,13 +5204,12 @@ function parsedProviderModelVersion(id) {
3506
5204
  const key = clean(plugin.id || plugin.name || plugin);
3507
5205
  const dataDir = cfgMod.getPluginData?.();
3508
5206
  const removed = registryRemovePlugin(key, { dataDir });
3509
- const nextConfig = cfgMod.loadConfig();
5207
+ const nextConfig = { ...config };
3510
5208
  const serverName = pluginMcpServerName(plugin);
3511
5209
  if (nextConfig.mcpServers && Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
3512
5210
  const current = { ...nextConfig.mcpServers };
3513
5211
  delete current[serverName];
3514
- cfgMod.saveConfig({ ...nextConfig, mcpServers: current });
3515
- config = cfgMod.loadConfig();
5212
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
3516
5213
  await connectConfiguredMcp({ reset: true });
3517
5214
  invalidatePreSessionToolSurface();
3518
5215
  }
@@ -3527,7 +5224,7 @@ function parsedProviderModelVersion(id) {
3527
5224
  const scriptPath = join(root, script);
3528
5225
  if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
3529
5226
  const serverName = pluginMcpServerName(plugin);
3530
- const nextConfig = cfgMod.loadConfig();
5227
+ const nextConfig = { ...config };
3531
5228
  nextConfig.mcpServers = {
3532
5229
  ...(nextConfig.mcpServers || {}),
3533
5230
  [serverName]: {
@@ -3540,8 +5237,7 @@ function parsedProviderModelVersion(id) {
3540
5237
  },
3541
5238
  },
3542
5239
  };
3543
- cfgMod.saveConfig(nextConfig);
3544
- config = cfgMod.loadConfig();
5240
+ saveConfigAndAdopt(nextConfig);
3545
5241
  const status = await connectConfiguredMcp({ reset: true });
3546
5242
  invalidatePreSessionToolSurface();
3547
5243
  if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable');
@@ -3609,8 +5305,11 @@ function parsedProviderModelVersion(id) {
3609
5305
  }
3610
5306
  return result;
3611
5307
  },
3612
- async setRoute(next) {
5308
+ async setRoute(next, options = {}) {
5309
+ const applyToCurrentSession = options?.applyToCurrentSession !== false;
3613
5310
  const requested = { ...(next || {}) };
5311
+ validateRequestedModelSelector(config, requested);
5312
+ const providerExplicitlyRequested = clean(next?.provider) !== '';
3614
5313
  if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(route, 'effort')) {
3615
5314
  requested.effort = route.effort;
3616
5315
  }
@@ -3620,14 +5319,34 @@ function parsedProviderModelVersion(id) {
3620
5319
  let selectedRoute = resolveRoute(config, requested);
3621
5320
  await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
3622
5321
  const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
5322
+ // Guard (A안 / strict reject): a free-text string must never become the
5323
+ // lead model. When the caller did not pin a provider, the route did not
5324
+ // match a preset, and the model is unknown to both the live provider
5325
+ // catalog (modelMeta is still the {id,provider} placeholder) and the
5326
+ // offline catalog, refuse BEFORE any config write so a partial save
5327
+ // (saveModelSettings/adoptConfig/persistLeadRoute) can never land.
5328
+ if (!providerExplicitlyRequested
5329
+ && !selectedRoute.preset
5330
+ && !modelMetaLooksResolved(modelMeta)
5331
+ && !getModelMetadataSync(selectedRoute.model, selectedRoute.provider)) {
5332
+ throw new Error(`unknown model: ${selectedRoute.provider}/${selectedRoute.model}`);
5333
+ }
3623
5334
  const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
3624
5335
  selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
3625
- config = saveModelSettings(cfgMod, selectedRoute, { fastCapable });
5336
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
5337
+ if (!applyToCurrentSession) {
5338
+ persistLeadRoute(selectedRoute);
5339
+ refreshStatuslineUsageSnapshot(route);
5340
+ scheduleStatuslineUsageRefresh();
5341
+ return selectedRoute;
5342
+ }
3626
5343
  const leadRoute = persistLeadRoute(selectedRoute);
3627
5344
  route = resolveRoute(config, leadRoute
3628
5345
  ? { model: workflowPresetId('lead') }
3629
5346
  : selectedRoute);
3630
5347
  await refreshRouteEffort(modelMeta);
5348
+ refreshStatuslineUsageSnapshot(route);
5349
+ scheduleStatuslineUsageRefresh();
3631
5350
  if (session) {
3632
5351
  const updated = mgr.updateSessionRoute?.(session.id, {
3633
5352
  provider: route.provider,
@@ -3642,7 +5361,7 @@ function parsedProviderModelVersion(id) {
3642
5361
  session.fast = route.fast === true;
3643
5362
  session.effort = route.effectiveEffort || null;
3644
5363
  }
3645
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5364
+ writeStatuslineRoute(statusRoutes, session, route);
3646
5365
  invalidateContextStatusCache();
3647
5366
  }
3648
5367
  return route;
@@ -3656,14 +5375,14 @@ function parsedProviderModelVersion(id) {
3656
5375
  throw new Error(`fast mode is not available for ${route.provider}/${route.model}`);
3657
5376
  }
3658
5377
  route = resolveRoute(config, { provider: route.provider, model: route.model, effort: route.effort, fast: fastCapable ? enabled : false });
3659
- config = saveModelSettings(cfgMod, route, { fastCapable });
5378
+ adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
3660
5379
  const leadRoute = persistLeadRoute(route);
3661
5380
  if (leadRoute) route = resolveRoute(config, { model: workflowPresetId('lead') });
3662
5381
  await refreshRouteEffort(modelMeta);
3663
5382
  if (session) {
3664
5383
  session.fast = route.fast === true;
3665
5384
  session.effort = route.effectiveEffort || null;
3666
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5385
+ writeStatuslineRoute(statusRoutes, session, route);
3667
5386
  invalidateContextStatusCache();
3668
5387
  }
3669
5388
  return route.fast === true;
@@ -3676,15 +5395,17 @@ function parsedProviderModelVersion(id) {
3676
5395
  async setEffort(value) {
3677
5396
  const normalized = normalizeEffortInput(value);
3678
5397
  route = { ...route, effort: normalized };
3679
- config = saveModelSettings(cfgMod, route, { fastCapable: route.fastCapable !== false });
5398
+ const modelMeta = await lookupModelMeta(route.provider, route.model);
5399
+ const fastCapable = fastCapableFor(route.provider, modelMeta);
5400
+ adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
3680
5401
  const leadRoute = persistLeadRoute(route);
3681
5402
  if (leadRoute) {
3682
5403
  route = resolveRoute(config, { model: workflowPresetId('lead') });
3683
5404
  }
3684
- await refreshRouteEffort();
5405
+ await refreshRouteEffort(modelMeta);
3685
5406
  if (session) {
3686
5407
  session.effort = route.effectiveEffort || null;
3687
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5408
+ writeStatuslineRoute(statusRoutes, session, route);
3688
5409
  invalidateContextStatusCache();
3689
5410
  }
3690
5411
  return route;
@@ -3692,10 +5413,18 @@ function parsedProviderModelVersion(id) {
3692
5413
  async close(reason = 'cli-exit', options = {}) {
3693
5414
  const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
3694
5415
  closeRequested = true;
5416
+ // Persist any change that is still sitting in the debounce window so a
5417
+ // toggle made right before exit is not lost. Synchronous + best-effort:
5418
+ // teardown must continue even if the final write fails.
5419
+ try { flushConfigSave(); } catch {}
3695
5420
  if (channelStartTimer) {
3696
5421
  clearTimeout(channelStartTimer);
3697
5422
  channelStartTimer = null;
3698
5423
  }
5424
+ if (providerSetupWarmupTimer) {
5425
+ clearTimeout(providerSetupWarmupTimer);
5426
+ providerSetupWarmupTimer = null;
5427
+ }
3699
5428
  if (providerWarmupTimer) {
3700
5429
  clearTimeout(providerWarmupTimer);
3701
5430
  providerWarmupTimer = null;
@@ -3704,9 +5433,21 @@ function parsedProviderModelVersion(id) {
3704
5433
  clearTimeout(providerModelWarmupTimer);
3705
5434
  providerModelWarmupTimer = null;
3706
5435
  }
5436
+ if (codeGraphPrewarmTimer) {
5437
+ clearTimeout(codeGraphPrewarmTimer);
5438
+ codeGraphPrewarmTimer = null;
5439
+ }
5440
+ if (statuslineUsageWarmupTimer) {
5441
+ clearTimeout(statuslineUsageWarmupTimer);
5442
+ statuslineUsageWarmupTimer = null;
5443
+ }
5444
+ if (statuslineUsageRefreshTimer) {
5445
+ clearTimeout(statuslineUsageRefreshTimer);
5446
+ statuslineUsageRefreshTimer = null;
5447
+ }
3707
5448
  try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
3708
5449
  const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
3709
- try { bridge.closeAll(reason); } catch {}
5450
+ try { agentTool.closeAll(reason); } catch {}
3710
5451
  let mcpStop = null;
3711
5452
  try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
3712
5453
  const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
@@ -3715,14 +5456,35 @@ function parsedProviderModelVersion(id) {
3715
5456
  .catch(() => {})
3716
5457
  : null;
3717
5458
  const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
5459
+ const memoryStop = memoryModPromise
5460
+ ? memoryModPromise
5461
+ .then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
5462
+ .catch(() => {})
5463
+ .finally(() => {
5464
+ memoryModPromise = null;
5465
+ })
5466
+ : null;
3718
5467
  let ok = false;
3719
5468
  if (session?.id) {
3720
5469
  statusRoutes?.clearGatewaySessionRoute?.(session.id);
3721
5470
  ok = mgr.closeSession(session.id, reason);
3722
5471
  session = null;
3723
5472
  }
5473
+ const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
5474
+ ? import('./runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
5475
+ .then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
5476
+ .catch(() => {})
5477
+ : null;
5478
+ const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
5479
+ ? import('./runtime/agent/orchestrator/tools/bash-session.mjs')
5480
+ .then((mod) => mod?.shutdownBashSessions?.(reason))
5481
+ .catch(() => {})
5482
+ : null;
3724
5483
  if (detach) {
3725
5484
  try { await withTeardownDeadline(channelStop, 300, false); } catch {}
5485
+ try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
5486
+ try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
5487
+ try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
3726
5488
  for (const stop of [mcpStop, openaiWsStop, patchStop]) {
3727
5489
  Promise.resolve(stop).catch(() => {});
3728
5490
  }
@@ -3733,6 +5495,9 @@ function parsedProviderModelVersion(id) {
3733
5495
  withTeardownDeadline(mcpStop, 1500, false),
3734
5496
  withTeardownDeadline(openaiWsStop, 1500, false),
3735
5497
  withTeardownDeadline(patchStop, 1500, false),
5498
+ withTeardownDeadline(memoryStop, 5500, false),
5499
+ withTeardownDeadline(shellJobsStop, 1500, false),
5500
+ withTeardownDeadline(bashSessionsStop, 1500, false),
3736
5501
  ]);
3737
5502
  return ok;
3738
5503
  },
@@ -3750,7 +5515,7 @@ function parsedProviderModelVersion(id) {
3750
5515
  const leadish = role === 'lead'
3751
5516
  || sourceType === 'lead'
3752
5517
  || (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
3753
- || (!sourceType && !sourceName && owner !== 'bridge');
5518
+ || (!sourceType && !sourceName && !isAgentOwner(owner));
3754
5519
  if (!leadish) return null;
3755
5520
  let preview = cleanSessionPreview(s.preview || '');
3756
5521
  let messageCount = Math.max(0, Number(s.messageCount) || 0);
@@ -3796,11 +5561,11 @@ function parsedProviderModelVersion(id) {
3796
5561
  await refreshRouteEffort();
3797
5562
  session.effort = route.effectiveEffort || null;
3798
5563
  session.cwd = currentCwd;
3799
- applyDeferredToolSurface(session, mode, standaloneTools);
5564
+ applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
3800
5565
  invalidatePreSessionToolSurface();
3801
5566
  invalidateContextStatusCache();
3802
5567
  sessionNeedsCwdRefresh = false;
3803
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5568
+ writeStatuslineRoute(statusRoutes, session, route);
3804
5569
  return {
3805
5570
  id: resumed.id,
3806
5571
  messages: resumed.messages || [],