mixdog 0.8.0 → 0.9.0

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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  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 +344 -0
  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 +1223 -95
  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 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. 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;
@@ -86,6 +168,39 @@ function messageContextText(message) {
86
168
  return text;
87
169
  }
88
170
 
171
+ function stripSystemReminder(text) {
172
+ return String(text || '')
173
+ .replace(/^\s*<system-reminder>\s*/i, '')
174
+ .replace(/\s*<\/system-reminder>\s*$/i, '')
175
+ .trim();
176
+ }
177
+
178
+ function splitMarkdownSections(text) {
179
+ const sections = [];
180
+ let current = [];
181
+ for (const line of String(text || '').split(/\r?\n/)) {
182
+ if (/^#\s+/.test(line) && current.length) {
183
+ const body = current.join('\n').trim();
184
+ if (body) sections.push(body);
185
+ current = [line];
186
+ } else {
187
+ current.push(line);
188
+ }
189
+ }
190
+ const tail = current.join('\n').trim();
191
+ if (tail) sections.push(tail);
192
+ return sections;
193
+ }
194
+
195
+ function reminderSectionBucket(section) {
196
+ const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
197
+ if (heading.includes('core memory')) return 'memory';
198
+ if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
199
+ if (heading.includes('workspace')) return 'workspace';
200
+ if (heading.includes('environment')) return 'environment';
201
+ return 'other';
202
+ }
203
+
89
204
  function summarizeContextMessages(messages) {
90
205
  const rows = {
91
206
  system: { count: 0, tokens: 0 },
@@ -94,15 +209,54 @@ function summarizeContextMessages(messages) {
94
209
  tool: { count: 0, tokens: 0 },
95
210
  other: { count: 0, tokens: 0 },
96
211
  };
212
+ const semantic = {
213
+ system: { count: 0, tokens: 0 },
214
+ chat: { count: 0, tokens: 0 },
215
+ assistant: { count: 0, tokens: 0 },
216
+ toolResults: { count: 0, tokens: 0 },
217
+ reminders: { count: 0, tokens: 0, otherTokens: 0 },
218
+ workflow: { tokens: 0 },
219
+ memory: { tokens: 0 },
220
+ workspace: { tokens: 0 },
221
+ environment: { tokens: 0 },
222
+ other: { tokens: 0 },
223
+ };
97
224
  let toolCallCount = 0;
98
225
  let toolCallTokens = 0;
99
226
  let toolResultCount = 0;
100
227
  let toolResultTokens = 0;
101
228
  for (const message of messages || []) {
102
229
  const role = rows[message?.role] ? message.role : 'other';
103
- const tokens = roughTokenCount(messageContextText(message)) + 4;
230
+ const text = messageContextText(message);
231
+ const tokens = roughTokenCount(text) + 4;
104
232
  rows[role].count += 1;
105
233
  rows[role].tokens += tokens;
234
+ if (role === 'system') {
235
+ semantic.system.count += 1;
236
+ semantic.system.tokens += tokens;
237
+ } else if (role === 'user') {
238
+ if (String(text || '').trim().startsWith('<system-reminder>')) {
239
+ semantic.reminders.count += 1;
240
+ semantic.reminders.tokens += tokens;
241
+ let sectionTokens = 0;
242
+ for (const section of splitMarkdownSections(stripSystemReminder(text))) {
243
+ const bucket = reminderSectionBucket(section);
244
+ const sectionTokenCount = roughTokenCount(section);
245
+ semantic[bucket].tokens += sectionTokenCount;
246
+ sectionTokens += sectionTokenCount;
247
+ }
248
+ semantic.reminders.otherTokens += Math.max(0, tokens - sectionTokens);
249
+ } else {
250
+ semantic.chat.count += 1;
251
+ semantic.chat.tokens += tokens;
252
+ }
253
+ } else if (role === 'assistant') {
254
+ semantic.assistant.count += 1;
255
+ semantic.assistant.tokens += tokens;
256
+ } else if (role === 'tool') {
257
+ semantic.toolResults.count += 1;
258
+ semantic.toolResults.tokens += tokens;
259
+ }
106
260
  if (message?.role === 'assistant' && Array.isArray(message.toolCalls) && message.toolCalls.length) {
107
261
  toolCallCount += message.toolCalls.length;
108
262
  try { toolCallTokens += roughTokenCount(JSON.stringify(message.toolCalls)); }
@@ -117,6 +271,7 @@ function summarizeContextMessages(messages) {
117
271
  count: Array.isArray(messages) ? messages.length : 0,
118
272
  estimatedTokens: Array.isArray(messages) ? estimateMessagesTokens(messages) : 0,
119
273
  roles: rows,
274
+ semantic,
120
275
  toolCallCount,
121
276
  toolCallTokens,
122
277
  toolResultCount,
@@ -177,6 +332,10 @@ function envFlag(name) {
177
332
  return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
178
333
  }
179
334
 
335
+ function envPresent(name) {
336
+ return process.env[name] !== undefined && process.env[name] !== '';
337
+ }
338
+
180
339
  function envDelayMs(name, fallback, { min = 0, max = 60_000 } = {}) {
181
340
  const raw = process.env[name];
182
341
  if (raw === undefined || raw === '') return fallback;
@@ -217,7 +376,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
217
376
  const EFFORT_OPTIONS_BY_PROVIDER = {
218
377
  openai: ['none', 'low', 'medium', 'high', 'xhigh'],
219
378
  'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
220
- anthropic: ['low', 'medium', 'high', 'max'],
379
+ anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
221
380
  'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
222
381
  xai: ['none', 'low', 'medium', 'high'],
223
382
  'grok-oauth': ['none', 'low', 'medium', 'high'],
@@ -254,14 +413,15 @@ export const TOOL_SEARCH_TOOL = {
254
413
  destructiveHint: false,
255
414
  idempotentHint: true,
256
415
  openWorldHint: false,
416
+ agentHidden: true,
257
417
  },
258
- 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.',
259
419
  inputSchema: {
260
420
  type: 'object',
261
421
  properties: {
262
- query: { type: 'string', description: 'Optional search text, e.g. edit, bridge, memory, skill, mcp.' },
263
- select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Tool/skill names to activate, as comma-separated text or an array.' },
264
- 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.' },
265
425
  },
266
426
  additionalProperties: false,
267
427
  },
@@ -276,30 +436,52 @@ const CHANNEL_STATUS_TOOL = {
276
436
  destructiveHint: false,
277
437
  idempotentHint: true,
278
438
  openWorldHint: false,
279
- bridgeHidden: true,
439
+ agentHidden: true,
280
440
  },
281
- description: 'List standalone Discord/channel/schedule/webhook configuration status. Read-only and never returns secrets.',
441
+ description: 'List channel/schedule/webhook status. No secrets.',
282
442
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
283
443
  };
284
444
 
285
445
  const CWD_TOOL = {
286
446
  name: 'cwd',
287
- title: 'Current Working Directory',
447
+ title: 'Work Project',
288
448
  annotations: {
289
- title: 'Current Working Directory',
449
+ title: 'Work Project',
290
450
  readOnlyHint: false,
291
451
  destructiveHint: false,
292
452
  idempotentHint: false,
293
453
  openWorldHint: false,
294
- bridgeHidden: true,
454
+ agentHidden: true,
455
+ },
456
+ description: 'Show or set the session work project for tool execution.',
457
+ inputSchema: {
458
+ type: 'object',
459
+ properties: {
460
+ action: { type: 'string', enum: ['get', 'set'], description: 'Default get.' },
461
+ path: { type: 'string', description: 'Project directory for set.' },
462
+ },
463
+ additionalProperties: false,
464
+ },
465
+ };
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,
295
477
  },
296
- description: 'Show or set the standalone session working directory. Default get; action=set requires path.',
478
+ description: 'Load a named SKILL.md into context.',
297
479
  inputSchema: {
298
480
  type: 'object',
299
481
  properties: {
300
- action: { type: 'string', enum: ['get', 'set'], description: 'Default get, or set when path is provided.' },
301
- path: { type: 'string', description: 'Directory path for action=set. Relative paths resolve from the current cwd.' },
482
+ name: { type: 'string', description: 'Skill name.' },
302
483
  },
484
+ required: ['name'],
303
485
  additionalProperties: false,
304
486
  },
305
487
  };
@@ -308,11 +490,12 @@ const MEASURED_TOOL_USAGE = Object.freeze({
308
490
  read: 710,
309
491
  code_graph: 520,
310
492
  grep: 500,
493
+ find: 480,
311
494
  glob: 460,
312
495
  list: 430,
313
496
  apply_patch: 400,
314
497
  explore: 360,
315
- bridge: 330,
498
+ agent: 330,
316
499
  shell: 81,
317
500
  cwd: 2,
318
501
  diagnostics: 2,
@@ -323,44 +506,54 @@ const MEASURED_TOOL_USAGE = Object.freeze({
323
506
  channel_status: 2,
324
507
  });
325
508
  const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
326
- 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',
327
520
  'tool_search',
328
- 'recall',
329
- 'search',
330
- 'web_fetch',
331
521
  ]);
332
- const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
333
- const DEFERRED_DEFAULT_FULL_LIMIT = 8;
334
522
  const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
335
523
  'read',
336
524
  'code_graph',
337
525
  'grep',
526
+ 'find',
338
527
  'glob',
339
528
  'list',
340
529
  'explore',
530
+ 'Skill',
341
531
  'tool_search',
342
532
  ]);
343
533
  const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
344
534
  'read',
345
535
  'code_graph',
346
536
  'grep',
537
+ 'find',
347
538
  'glob',
348
539
  'list',
349
540
  'shell',
350
541
  'task',
351
542
  'explore',
352
543
  'apply_patch',
353
- 'bridge',
544
+ 'agent',
354
545
  'recall',
355
546
  'search',
356
547
  'web_fetch',
357
548
  'cwd',
549
+ 'Skill',
358
550
  'tool_search',
359
551
  ]);
360
552
  const READONLY_TOOL_NAMES = new Set([
361
553
  'read',
362
554
  'list',
363
555
  'grep',
556
+ 'find',
364
557
  'glob',
365
558
  'code_graph',
366
559
  'search',
@@ -371,21 +564,22 @@ const READONLY_TOOL_NAMES = new Set([
371
564
  'channel_status',
372
565
  'schedule_status',
373
566
  'fetch',
567
+ 'Skill',
374
568
  ]);
375
- const BRIDGE_HIDDEN_WRAPPER_TOOLS = new Set(['explore', 'search']);
569
+ const AGENT_HIDDEN_WRAPPER_TOOLS = new Set(['search']);
376
570
 
377
571
  function applyStandaloneToolDefaults(tool) {
378
- if (!tool || !BRIDGE_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
572
+ if (!tool || !AGENT_HIDDEN_WRAPPER_TOOLS.has(tool.name)) return tool;
379
573
  return {
380
574
  ...tool,
381
575
  annotations: {
382
576
  ...(tool.annotations || {}),
383
- bridgeHidden: true,
577
+ agentHidden: true,
384
578
  },
385
579
  };
386
580
  }
387
581
  const DEFERRED_SELECT_ALIASES = {
388
- filesystem: ['read', 'list', 'grep', 'glob'],
582
+ filesystem: ['read', 'list', 'grep', 'find', 'glob'],
389
583
  search: ['search', 'web_fetch'],
390
584
  web: ['web_fetch', 'search'],
391
585
  memory: ['memory', 'recall'],
@@ -398,11 +592,68 @@ const DEFERRED_SELECT_ALIASES = {
398
592
  channel: ['channel_status'],
399
593
  explore: ['explore'],
400
594
  discovery: ['explore'],
401
- bridge: ['bridge'],
595
+ agent: ['agent'],
402
596
  graph: ['code_graph'],
403
597
  code: ['code_graph'],
404
598
  shell: ['shell', 'task'],
405
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
+ });
406
657
 
407
658
  function normalizeToolMode(mode) {
408
659
  const value = String(mode || '').trim().toLowerCase();
@@ -427,8 +678,21 @@ function effortOptionsFor(provider, model) {
427
678
  const declared = Array.isArray(model?.reasoningLevels)
428
679
  ? model.reasoningLevels.map(clean).filter(Boolean)
429
680
  : [];
430
- if (Array.isArray(model?.reasoningLevels)) return filterProvider(declared);
431
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);
432
696
  if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
433
697
  return filterProvider(EFFORT_BY_FAMILY[family]);
434
698
  }
@@ -481,10 +745,23 @@ function toolSpecForMode(mode) {
481
745
  return mode === 'readonly' ? ['tools:readonly'] : 'full';
482
746
  }
483
747
 
748
+ function deferredSurfaceModeForLead(mode) {
749
+ return mode === 'readonly' ? 'readonly' : 'lead';
750
+ }
751
+
484
752
  function clean(value) {
485
753
  return String(value ?? '').trim();
486
754
  }
487
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
+
488
765
  const OUTPUT_STYLE_ORDER = ['default', 'simple', 'extreme-simple'];
489
766
  const OUTPUT_STYLE_ALIASES = new Map([
490
767
  ['compact', 'default'],
@@ -717,6 +994,21 @@ function resolveRoute(config, { provider, model, effort, fast } = {}) {
717
994
  };
718
995
  }
719
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
+
720
1012
  function ensureProviderEnabled(config, provider) {
721
1013
  const providers = { ...(config?.providers || {}) };
722
1014
  providers[provider] = { ...(providers[provider] || {}), enabled: true };
@@ -751,12 +1043,51 @@ function normalizeSystemShellCommand(value) {
751
1043
  function normalizeAutoClearConfig(value = {}) {
752
1044
  const raw = value && typeof value === 'object' ? value : {};
753
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') : '';
754
1048
  return {
755
1049
  enabled: raw.enabled !== false,
756
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,
757
1072
  };
758
1073
  }
759
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
+
760
1091
  function formatDurationMs(ms) {
761
1092
  const value = Math.max(0, Number(ms) || 0);
762
1093
  if (value % 3_600_000 === 0) return `${value / 3_600_000}h`;
@@ -805,6 +1136,44 @@ function openAiDirectModelSupportsFast(model) {
805
1136
  || /^gpt-5\.4-mini(?:-\d{4}|$)/.test(id);
806
1137
  }
807
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
+
808
1177
  function anthropicModelMetaSupportsFast(model) {
809
1178
  const id = clean(model?.id || model).toLowerCase();
810
1179
  return /^claude-(opus|sonnet)/.test(id);
@@ -819,6 +1188,16 @@ function fastCapableFor(provider, model) {
819
1188
  return false;
820
1189
  }
821
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
+
822
1201
  function fastPreferenceFor(config, provider, model) {
823
1202
  const key = routeFastKey(provider, model);
824
1203
  if (!key) return false;
@@ -827,10 +1206,10 @@ function fastPreferenceFor(config, provider, model) {
827
1206
  return config?.fastModels?.[key] === true;
828
1207
  }
829
1208
 
830
- function saveModelSettings(cfgMod, route, { fastCapable = true } = {}) {
1209
+ function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfig = null } = {}) {
831
1210
  const key = routeFastKey(route?.provider, route?.model);
832
- if (!key) return cfgMod.loadConfig();
833
- const nextConfig = cfgMod.loadConfig();
1211
+ if (!key) return baseConfig || cfgMod.loadConfig();
1212
+ const nextConfig = baseConfig || cfgMod.loadConfig();
834
1213
  const modelSettings = { ...(nextConfig.modelSettings || {}) };
835
1214
  const nextSetting = { ...(modelSettings[key] || {}) };
836
1215
  if (hasOwn(route, 'effort') && route.effort) nextSetting.effort = route.effort;
@@ -872,17 +1251,80 @@ function routeForStatusline(route) {
872
1251
  return out;
873
1252
  }
874
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
+
875
1260
  const ONBOARDING_VERSION = 1;
876
- const WORKFLOW_ROUTE_SLOTS = ['lead', 'bridge', 'explorer', 'memory'];
1261
+ const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
877
1262
  const FIXED_AGENT_SLOTS = Object.freeze([
878
1263
  { id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
879
- { id: 'web-researcher', label: 'Web Researcher', description: 'External current-info research' },
880
1264
  { id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
881
1265
  { id: 'worker', label: 'Worker', description: 'Scoped implementation' },
882
1266
  { id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
883
1267
  { id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
884
1268
  { id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
885
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
+ });
886
1328
  const AGENT_ROLE_IDS = new Set(FIXED_AGENT_SLOTS.map((agent) => agent.id));
887
1329
  const agentDefinitionCache = new Map();
888
1330
  const DEFAULT_WORKFLOW_ID = 'default';
@@ -940,12 +1382,14 @@ function readWorkflowPackFromDir(dir, source = 'built-in') {
940
1382
  const entry = clean(manifest.entry) || 'WORKFLOW.md';
941
1383
  const body = readTextSafe(join(dir, entry));
942
1384
  if (!body) return null;
1385
+ const agentsConfigured = Array.isArray(manifest.agents);
943
1386
  return {
944
1387
  id,
945
1388
  name: clean(manifest.name) || id,
946
1389
  description: clean(manifest.description),
947
1390
  entry,
948
- 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) : [],
949
1393
  body,
950
1394
  source,
951
1395
  };
@@ -983,6 +1427,20 @@ function loadWorkflowPack(dataDir, id) {
983
1427
  return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in');
984
1428
  }
985
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
+
986
1444
  function loadAgentDefinition(dataDir, id) {
987
1445
  const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
988
1446
  if (!agentId) return null;
@@ -991,20 +1449,22 @@ function loadAgentDefinition(dataDir, id) {
991
1449
  for (const dir of agentSourceDirs(dataDir, agentId)) {
992
1450
  const manifest = readJsonSafe(join(dir, 'agent.json')) || {};
993
1451
  const entry = clean(manifest.entry) || 'AGENT.md';
994
- const body = readTextSafe(join(dir, entry));
1452
+ const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
1453
+ const body = doc.body;
995
1454
  if (!body) continue;
996
1455
  const definition = {
997
1456
  id: agentId,
998
1457
  name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
999
1458
  description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
1459
+ permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
1460
+ frontmatter: doc.frontmatter,
1000
1461
  body,
1001
1462
  };
1002
1463
  agentDefinitionCache.set(cacheKey, definition);
1003
1464
  return definition;
1004
1465
  }
1005
- const legacyBody = readTextSafe(join(dataDir || STANDALONE_DATA_DIR, 'roles', `${agentId}.md`))
1006
- || readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`));
1007
- if (!legacyBody) {
1466
+ const legacyDoc = readMarkdownDocument(readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`)));
1467
+ if (!legacyDoc.body) {
1008
1468
  agentDefinitionCache.set(cacheKey, null);
1009
1469
  return null;
1010
1470
  }
@@ -1012,7 +1472,9 @@ function loadAgentDefinition(dataDir, id) {
1012
1472
  id: agentId,
1013
1473
  name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
1014
1474
  description: '',
1015
- body: legacyBody,
1475
+ permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
1476
+ frontmatter: legacyDoc.frontmatter,
1477
+ body: legacyDoc.body,
1016
1478
  };
1017
1479
  agentDefinitionCache.set(cacheKey, definition);
1018
1480
  return definition;
@@ -1027,7 +1489,7 @@ function workflowContextBlock(config, dataDir) {
1027
1489
  if (pack.description) lines.push(pack.description);
1028
1490
  lines.push(pack.body);
1029
1491
 
1030
- 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);
1031
1493
  const agentBlocks = agentIds
1032
1494
  .map((id) => loadAgentDefinition(dataDir, id))
1033
1495
  .filter(Boolean);
@@ -1046,6 +1508,11 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
1046
1508
  const provider = clean(routeLike?.provider) || clean(fallback.provider);
1047
1509
  const model = clean(routeLike?.model) || clean(fallback.model);
1048
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;
1049
1516
  const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
1050
1517
  const fast = routeLike?.fast ?? fallback.fast;
1051
1518
  return {
@@ -1056,6 +1523,41 @@ function normalizeWorkflowRoute(routeLike, fallback = {}) {
1056
1523
  };
1057
1524
  }
1058
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
+
1059
1561
  function upsertWorkflowPreset(presets, slot, routeLike) {
1060
1562
  const route = normalizeWorkflowRoute(routeLike);
1061
1563
  if (!route) return presets;
@@ -1063,7 +1565,7 @@ function upsertWorkflowPreset(presets, slot, routeLike) {
1063
1565
  const preset = {
1064
1566
  id,
1065
1567
  name: workflowPresetName(slot),
1066
- type: 'bridge',
1568
+ type: 'agent',
1067
1569
  provider: route.provider,
1068
1570
  model: route.model,
1069
1571
  ...(route.effort ? { effort: route.effort } : {}),
@@ -1085,24 +1587,19 @@ function summarizeWorkflowRoutes(config) {
1085
1587
  return out;
1086
1588
  }
1087
1589
 
1088
- function legacyWorkflowRolePreset(dataDir, role) {
1089
- try {
1090
- const file = join(dataDir, 'user-workflow.json');
1091
- if (!existsSync(file)) return '';
1092
- const raw = JSON.parse(readFileSync(file, 'utf8'));
1093
- const found = (raw?.roles || []).find((item) => clean(item?.name) === role);
1094
- return clean(found?.preset);
1095
- } catch {
1096
- 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;
1097
1597
  }
1098
- }
1099
-
1100
- function routeFromPreset(config, presetName) {
1101
- const preset = findPreset(config, presetName);
1598
+ const preset = findPreset(config, slotValue);
1102
1599
  return preset ? normalizeWorkflowRoute(preset) : null;
1103
1600
  }
1104
1601
 
1105
- function agentRouteFromConfig(config, agentId, dataDir) {
1602
+ function agentRouteFromConfig(config, agentId, _dataDir) {
1106
1603
  const id = normalizeAgentId(agentId);
1107
1604
  if (!id) return null;
1108
1605
  const explicit = normalizeWorkflowRoute(config?.agents?.[id])
@@ -1118,7 +1615,7 @@ function agentRouteFromConfig(config, agentId, dataDir) {
1118
1615
  if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
1119
1616
  if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
1120
1617
 
1121
- return routeFromPreset(config, legacyWorkflowRolePreset(dataDir, id));
1618
+ return null;
1122
1619
  }
1123
1620
 
1124
1621
  function toolResponseText(result) {
@@ -1157,21 +1654,57 @@ function currentSessionRecallRows(session, query, { limit = 10 } = {}) {
1157
1654
 
1158
1655
  function parseToolSelection(value) {
1159
1656
  if (Array.isArray(value)) return value.map(clean).filter(Boolean);
1160
- 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, '')
1161
1661
  .split(/[,\s]+/)
1162
1662
  .map(clean)
1163
1663
  .filter(Boolean);
1164
1664
  }
1165
1665
 
1666
+ function parseToolSearchQuerySelection(query) {
1667
+ const match = clean(query).match(/^select\s*:\s*(.+)$/i);
1668
+ return match ? parseToolSelection(match[1]) : [];
1669
+ }
1670
+
1166
1671
  function toolKind(tool) {
1167
1672
  const name = clean(tool?.name);
1168
1673
  if (name.startsWith('mcp__')) return 'mcp';
1169
- if (name.startsWith('skill_') || name === 'skills_list') return 'skill';
1170
- if (tool?.annotations?.bridgeHidden) return 'control';
1171
- if (['edit', 'write', 'apply_patch', 'shell'].includes(name)) return 'write';
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';
1677
+ if (['apply_patch', 'shell'].includes(name)) return 'mutation';
1172
1678
  return 'tool';
1173
1679
  }
1174
1680
 
1681
+ function toolSchemaBucket(tool) {
1682
+ const name = clean(tool?.name);
1683
+ const kind = toolKind(tool);
1684
+ if (kind === 'mcp') return 'mcp';
1685
+ if (kind === 'skill') return 'skills';
1686
+ if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
1687
+ if (name === 'search' || name === 'web_fetch') return 'web';
1688
+ if (['read', 'grep', 'find', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
1689
+ if (['shell', 'apply_patch'].includes(name)) return 'mutation';
1690
+ if (name === 'agent' || name === 'delegate') return 'agents';
1691
+ if (name.includes('channel') || name.includes('discord') || name.includes('webhook')) return 'channels';
1692
+ if (name.includes('provider') || name === 'tool_search' || name === 'cwd') return 'setup';
1693
+ return 'other';
1694
+ }
1695
+
1696
+ function estimateToolSchemaBreakdown(tools) {
1697
+ const out = {};
1698
+ for (const tool of Array.isArray(tools) ? tools : []) {
1699
+ const bucket = toolSchemaBucket(tool);
1700
+ const row = out[bucket] || { count: 0, tokens: 0 };
1701
+ row.count += 1;
1702
+ row.tokens += estimateToolSchemaTokens([tool]);
1703
+ out[bucket] = row;
1704
+ }
1705
+ return out;
1706
+ }
1707
+
1175
1708
  function measuredToolUsage(name) {
1176
1709
  return MEASURED_TOOL_USAGE[clean(name)] || 0;
1177
1710
  }
@@ -1201,6 +1734,17 @@ function activeToolForSurface(tool) {
1201
1734
  return JSON.parse(JSON.stringify(tool));
1202
1735
  }
1203
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
+
1204
1748
  function filterDisallowedTools(tools, disallowed = []) {
1205
1749
  if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
1206
1750
  const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
@@ -1221,24 +1765,14 @@ function sortedNamesByMeasuredUsage(names) {
1221
1765
  }
1222
1766
 
1223
1767
  export function defaultDeferredToolNames(catalog, mode) {
1768
+ const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
1224
1769
  if (mode === 'lead') {
1225
- const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
1226
1770
  return new Set(DEFERRED_DEFAULT_LEAD_TOOLS.filter((name) => available.has(name)));
1227
1771
  }
1228
1772
  if (mode === 'readonly') {
1229
- const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
1230
1773
  return new Set(DEFERRED_DEFAULT_READONLY_TOOLS.filter((name) => available.has(name)));
1231
1774
  }
1232
- const names = new Set(DEFERRED_ALWAYS_ACTIVE_TOOLS);
1233
- const limit = DEFERRED_DEFAULT_FULL_LIMIT;
1234
- for (const tool of sortedCatalogByMeasuredUsage(catalog)) {
1235
- const name = clean(tool?.name);
1236
- if (!name || names.has(name)) continue;
1237
- if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
1238
- names.add(name);
1239
- if (names.size >= DEFERRED_ALWAYS_ACTIVE_TOOLS.size + limit) break;
1240
- }
1241
- return names;
1775
+ return new Set(DEFERRED_DEFAULT_FULL_TOOLS.filter((name) => available.has(name)));
1242
1776
  }
1243
1777
 
1244
1778
  export function compactToolSearchDescription(value, max = 220) {
@@ -1257,25 +1791,180 @@ function toolRow(tool, activeNames = new Set()) {
1257
1791
  };
1258
1792
  }
1259
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
+
1260
1832
  function toolSearchTokens(value) {
1261
1833
  return (clean(value).toLowerCase().match(/[a-z0-9_.-]+/g) || [])
1262
1834
  .map((token) => token.replace(/[-.]+/g, '_'))
1263
1835
  .filter(Boolean);
1264
1836
  }
1265
1837
 
1838
+ function toolSearchMeaningfulTokens(value) {
1839
+ return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
1840
+ }
1841
+
1266
1842
  function toolSearchText(row) {
1267
1843
  const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
1268
1844
  return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
1269
1845
  }
1270
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
+
1271
1910
  function toolSearchMatches(row, query) {
1272
1911
  const raw = clean(query).toLowerCase();
1273
1912
  if (!raw) return true;
1274
- const haystack = toolSearchText(row);
1275
- if (haystack.includes(raw)) return true;
1276
- const tokens = toolSearchTokens(raw);
1277
- if (tokens.length === 0) return haystack.includes(raw);
1278
- 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 [];
1279
1968
  }
1280
1969
 
1281
1970
  function expandSelectionNames(names) {
@@ -1290,6 +1979,39 @@ function expandSelectionNames(names) {
1290
1979
  return [...new Set(out)];
1291
1980
  }
1292
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
+
1293
2015
  function isReadonlySelectable(tool) {
1294
2016
  const name = clean(tool?.name);
1295
2017
  if (READONLY_TOOL_NAMES.has(name)) return true;
@@ -1299,8 +2021,9 @@ function isReadonlySelectable(tool) {
1299
2021
  return false;
1300
2022
  }
1301
2023
 
1302
- function applyDeferredToolSurface(session, mode, extraTools = []) {
2024
+ function applyDeferredToolSurface(session, mode, extraTools = [], options = {}) {
1303
2025
  if (!session || !Array.isArray(session.tools)) return session;
2026
+ const providerMode = deferredProviderMode(options.provider || session.provider);
1304
2027
  const byName = new Map();
1305
2028
  for (const tool of [...session.tools, ...(extraTools || [])]) {
1306
2029
  const name = clean(tool?.name);
@@ -1309,14 +2032,34 @@ function applyDeferredToolSurface(session, mode, extraTools = []) {
1309
2032
  }
1310
2033
  const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
1311
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);
1312
2044
  session.deferredToolCatalog = catalog;
1313
2045
  session.deferredToolUsage = MEASURED_TOOL_USAGE;
1314
- session.deferredSelectedTools = sortedNamesByMeasuredUsage(defaultNames);
2046
+ session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
2047
+ session.deferredProviderMode = providerMode;
2048
+ session.deferredNativeTools = providerMode === 'native';
1315
2049
  session.tools.length = 0;
2050
+ const active = [];
1316
2051
  for (const tool of catalog) {
1317
- if (!defaultNames.has(clean(tool?.name))) continue;
2052
+ if (!selected.has(clean(tool?.name))) continue;
1318
2053
  if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
1319
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);
1320
2063
  }
1321
2064
  return session;
1322
2065
  }
@@ -1325,57 +2068,106 @@ function selectDeferredTools(session, names, mode) {
1325
2068
  const catalog = Array.isArray(session?.deferredToolCatalog)
1326
2069
  ? session.deferredToolCatalog
1327
2070
  : (Array.isArray(session?.tools) ? session.tools : []);
1328
- const active = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
1329
- 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
+ }
1330
2081
  const added = [];
1331
2082
  const already = [];
1332
2083
  const blocked = [];
1333
2084
  const missing = [];
1334
- for (const name of expandSelectionNames(names)) {
1335
- 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);
1336
2089
  if (!tool) {
1337
- missing.push(name);
2090
+ missing.push(requestedName);
1338
2091
  continue;
1339
2092
  }
1340
2093
  if (mode === 'readonly' && !isReadonlySelectable(tool)) {
1341
2094
  blocked.push({ name, reason: 'readonly mode' });
1342
2095
  continue;
1343
2096
  }
1344
- if (active.has(name)) {
2097
+ if (active.has(name) || discovered.has(name)) {
1345
2098
  already.push(name);
1346
2099
  continue;
1347
2100
  }
1348
- session.tools.push(tool);
1349
- active.add(name);
2101
+ if (native) {
2102
+ discovered.add(name);
2103
+ } else {
2104
+ session.tools.push(tool);
2105
+ active.add(name);
2106
+ }
1350
2107
  added.push(name);
1351
2108
  }
1352
- session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
1353
- 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 };
1354
2116
  }
1355
2117
 
1356
2118
  function renderToolSearch(args = {}, session, mode = 'full') {
1357
2119
  const catalog = Array.isArray(session?.deferredToolCatalog)
1358
2120
  ? session.deferredToolCatalog
1359
2121
  : (Array.isArray(session?.tools) ? session.tools : []);
1360
- const activeNames = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
1361
- const query = clean(args.query).toLowerCase();
1362
- 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();
1363
2127
  const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
1364
- const selection = selectedNames.length ? selectDeferredTools(session, selectedNames, mode) : null;
1365
- const nextActiveNames = new Set((session?.tools || []).map((tool) => tool?.name).filter(Boolean));
1366
- 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);
1367
2140
  const matches = query
1368
- ? rows.filter((row) => toolSearchMatches(row, query))
2141
+ ? rankedToolSearchRows(rows, query)
1369
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;
1370
2152
  return JSON.stringify({
1371
- selected: selection,
2153
+ selected,
2154
+ ...(nativeToolSearch ? { nativeToolSearch } : {}),
1372
2155
  totalMatches: matches.length,
1373
2156
  matches: matches.slice(0, limit),
1374
2157
  activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
1375
- 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.',
1376
2160
  }, null, 2);
1377
2161
  }
1378
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
+
1379
2171
  function resolveCwdPath(currentCwd, value) {
1380
2172
  const raw = clean(value);
1381
2173
  if (!raw) throw new Error('cwd: path is required for action=set');
@@ -1398,7 +2190,6 @@ export async function createMixdogSessionRuntime({
1398
2190
  rootDir: STANDALONE_ROOT,
1399
2191
  dataDir: STANDALONE_DATA_DIR,
1400
2192
  });
1401
- ensureProjectMixdogMd({ cwd });
1402
2193
  bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
1403
2194
 
1404
2195
  const importsStartedAt = performance.now();
@@ -1430,18 +2221,51 @@ export async function createMixdogSessionRuntime({
1430
2221
  profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
1431
2222
  ]);
1432
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
+ });
1433
2230
  let memoryModPromise = null;
1434
- let memoryInitPromise = null;
1435
2231
  let searchModPromise = null;
1436
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
+ };
1437
2261
 
1438
2262
  async function getMemoryModule() {
2263
+ if (!memoryEnabled()) throw new Error('memory is disabled in settings');
1439
2264
  const startedAt = performance.now();
1440
- memoryModPromise ??= import(MEMORY_RUNTIME);
2265
+ memoryModPromise ??= Promise.resolve(memoryRuntime);
1441
2266
  const mod = await memoryModPromise;
1442
2267
  if (typeof mod?.init === 'function') {
1443
- memoryInitPromise ??= mod.init();
1444
- await memoryInitPromise;
2268
+ await mod.init();
1445
2269
  }
1446
2270
  bootProfile('memory-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
1447
2271
  return mod;
@@ -1455,6 +2279,211 @@ export async function createMixdogSessionRuntime({
1455
2279
  return mod;
1456
2280
  }
1457
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
+
1458
2487
  async function getCodeGraphModule() {
1459
2488
  const startedAt = performance.now();
1460
2489
  codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
@@ -1475,8 +2504,7 @@ export async function createMixdogSessionRuntime({
1475
2504
  };
1476
2505
  nextConfig.default = workflowPresetId('lead');
1477
2506
 
1478
- cfgMod.saveConfig(nextConfig);
1479
- config = nextConfig;
2507
+ saveConfigAndAdopt(nextConfig);
1480
2508
  return leadRoute;
1481
2509
  }
1482
2510
 
@@ -1517,6 +2545,10 @@ export async function createMixdogSessionRuntime({
1517
2545
  async function loadCoreMemoryContext() {
1518
2546
  // Boot should not pay for memory/PG startup unless explicitly requested.
1519
2547
  // Recall and memory tools still initialize the memory service on first use.
2548
+ if (!memoryEnabled()) {
2549
+ bootProfile('core-memory:disabled');
2550
+ return '';
2551
+ }
1520
2552
  if (process.env.MIXDOG_BOOT_CORE_MEMORY !== '1') {
1521
2553
  bootProfile('core-memory:skipped');
1522
2554
  return '';
@@ -1549,32 +2581,100 @@ export async function createMixdogSessionRuntime({
1549
2581
  setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
1550
2582
  let configHasSecrets = false;
1551
2583
  let route = resolveRoute(config, { provider, model });
2584
+ let searchRoute = normalizeSearchRouteConfig(config.searchRoute);
1552
2585
  bootProfile('config:ready', { ms: (performance.now() - configStartedAt).toFixed(1) });
1553
2586
  let mode = normalizeToolMode(toolMode);
1554
2587
  let session = null;
1555
2588
  let sessionCreatePromise = null;
1556
2589
  let currentCwd = cwd;
1557
2590
  let sessionNeedsCwdRefresh = false;
1558
- const workspaceRouter = createWorkspaceRouter({ entryCwd: cwd });
1559
2591
  let closeRequested = false;
1560
2592
  let channelStartTimer = null;
2593
+ let providerSetupWarmupTimer = null;
1561
2594
  let providerWarmupTimer = null;
1562
2595
  let providerModelWarmupTimer = null;
2596
+ let codeGraphPrewarmTimer = null;
2597
+ let codeGraphPrewarmInFlight = false;
2598
+ let codeGraphPrewarmQueuedCwd = '';
2599
+ let statuslineUsageWarmupTimer = null;
2600
+ let statuslineUsageRefreshTimer = null;
1563
2601
  let activeTurnCount = 0;
1564
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
+ }
1565
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 });
1566
2625
  const providerWarmupDelayMs = envDelayMs('MIXDOG_PROVIDER_WARMUP_DELAY_MS', 1_500, { min: 0, max: 60_000 });
1567
- 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 });
1568
2638
  const channelStartDelayMs = envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 });
1569
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;
1570
2666
  const modelMetaByRoute = new Map();
1571
2667
  const notificationListeners = new Set();
1572
2668
  let providerModelsCache = { models: null, at: 0 };
1573
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;
1574
2674
  let providerSetupCache = { setup: null, at: 0 };
2675
+ let providerSetupQuickCache = { setup: null, at: 0 };
1575
2676
  let providerSetupPromise = null;
1576
2677
  let providerInitPromise = null;
1577
- const PROVIDER_SETUP_CACHE_TTL_MS = 10_000;
1578
2678
  let mcpFailures = [];
1579
2679
  let preSessionToolSurface = null;
1580
2680
  let contextStatusCacheKey = null;
@@ -1584,72 +2684,6 @@ export async function createMixdogSessionRuntime({
1584
2684
  hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
1585
2685
  bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
1586
2686
 
1587
- function contextContentLength(content) {
1588
- if (typeof content === 'string') return content.length;
1589
- if (!Array.isArray(content)) {
1590
- try { return JSON.stringify(content ?? '').length; } catch { return String(content ?? '').length; }
1591
- }
1592
- let length = 0;
1593
- for (const part of content) {
1594
- if (typeof part === 'string') length += part.length;
1595
- else if (typeof part?.text === 'string') length += part.text.length;
1596
- else {
1597
- try { length += JSON.stringify(part ?? '').length; } catch { length += String(part ?? '').length; }
1598
- }
1599
- }
1600
- return length;
1601
- }
1602
-
1603
- function sameContextStatusKey(a, b) {
1604
- if (!a || !b || a.length !== b.length) return false;
1605
- for (let i = 0; i < a.length; i++) {
1606
- if (!Object.is(a[i], b[i])) return false;
1607
- }
1608
- return true;
1609
- }
1610
-
1611
- function buildContextStatusCacheKey(messages, tools, bridgeMode) {
1612
- const lastMessage = messages[messages.length - 1] || null;
1613
- const compaction = session?.compaction || null;
1614
- return [
1615
- session?.id || null,
1616
- route.provider,
1617
- route.model,
1618
- currentCwd,
1619
- mode,
1620
- bridgeMode,
1621
- messages,
1622
- messages.length,
1623
- lastMessage,
1624
- lastMessage?.role || null,
1625
- lastMessage?.content || null,
1626
- contextContentLength(lastMessage?.content),
1627
- Array.isArray(lastMessage?.toolCalls) ? lastMessage.toolCalls.length : 0,
1628
- tools,
1629
- tools.length,
1630
- session?.contextWindow || null,
1631
- session?.rawContextWindow || null,
1632
- session?.effectiveContextWindowPercent || null,
1633
- session?.lastContextTokens || 0,
1634
- session?.lastContextTokensUpdatedAt || 0,
1635
- session?.lastContextTokensStaleAfterCompact === true,
1636
- session?.lastInputTokens || 0,
1637
- session?.lastOutputTokens || 0,
1638
- session?.lastCachedReadTokens || 0,
1639
- session?.lastCacheWriteTokens || 0,
1640
- session?.totalInputTokens || 0,
1641
- session?.totalOutputTokens || 0,
1642
- session?.totalCachedReadTokens || 0,
1643
- session?.totalCacheWriteTokens || 0,
1644
- session?.compactBoundaryTokens || 0,
1645
- compaction,
1646
- compaction?.lastChangedAt || 0,
1647
- compaction?.lastCompactAt || 0,
1648
- compaction?.boundaryTokens || 0,
1649
- compaction?.triggerTokens || 0,
1650
- ];
1651
- }
1652
-
1653
2687
  function mcpTransportLabel(cfg = {}) {
1654
2688
  if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
1655
2689
  if (cfg.transport === 'http' || cfg.url) return 'http';
@@ -1659,23 +2693,42 @@ export async function createMixdogSessionRuntime({
1659
2693
 
1660
2694
  function emitRuntimeNotification(content, meta = {}) {
1661
2695
  const text = String(content || '').trim();
1662
- if (!text) return;
2696
+ if (!text) return false;
1663
2697
  const event = { content: text, meta: meta && typeof meta === 'object' ? meta : {} };
2698
+ let handled = false;
1664
2699
  for (const listener of [...notificationListeners]) {
1665
- try { listener(event); } catch {}
2700
+ try {
2701
+ if (listener(event) === true) handled = true;
2702
+ } catch {}
1666
2703
  }
2704
+ return handled;
1667
2705
  }
1668
2706
 
1669
2707
  function notifyFnForSession(callerSessionId) {
1670
2708
  return (text, meta = {}) => {
1671
- const hadRuntimeListener = notificationListeners.size > 0;
1672
- emitRuntimeNotification(text, meta);
1673
- // TUI sessions keep their own Claude-Code-style command queue via
1674
- // onNotification. Headless/model-tool callers have no listener, so fall
1675
- // back to the session manager pending-message queue.
1676
- if (!hadRuntimeListener && callerSessionId && typeof mgr.enqueuePendingMessage === 'function') {
1677
- 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 {}
1678
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 {}
2730
+ }
2731
+ return enqueued || handledByRuntimeListener;
1679
2732
  };
1680
2733
  }
1681
2734
 
@@ -1737,40 +2790,93 @@ export async function createMixdogSessionRuntime({
1737
2790
  };
1738
2791
  }
1739
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
+
1740
2843
  function applyResolvedCwd(nextCwd, { markRefresh = true } = {}) {
1741
2844
  const resolved = resolve(nextCwd);
1742
2845
  const stat = statSync(resolved);
1743
2846
  if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
1744
2847
  const changed = resolve(currentCwd) !== resolved;
1745
2848
  currentCwd = resolved;
1746
- ensureProjectMixdogMd({ cwd: currentCwd });
1747
2849
  process.env.MIXDOG_SESSION_CWD = currentCwd;
1748
2850
  writeLastSessionCwd(currentCwd);
1749
2851
  if (session) session.cwd = currentCwd;
1750
- 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 changed;
2860
+ void markRefresh;
2861
+ // Lazy mode: before the first turn (e.g. the initial project-selection
2862
+ // cwd set), do NOT prewarm — that is exactly the post-first-frame freeze
2863
+ // we are avoiding. Once a turn has run, an in-session cwd switch DOES
2864
+ // prewarm the new dir, since a lookup there is now likely.
2865
+ if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
2866
+ bootProfile('code-graph:prewarm-lazy', { reason: 'cwd-deferred-to-first-turn' });
2867
+ } else {
2868
+ scheduleCodeGraphPrewarm(changed ? 0 : codeGraphPrewarmDelayMs, changed ? 'cwd-change' : 'cwd');
2869
+ }
1751
2870
  return currentCwd;
1752
2871
  }
1753
2872
 
1754
2873
  async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
1755
- if (!session?.id || !sessionNeedsCwdRefresh) return session;
1756
- const previousId = session.id;
1757
- statusRoutes?.clearGatewaySessionRoute?.(previousId);
1758
- mgr.closeSession(previousId, reason);
1759
- session = null;
2874
+ // No-op: cwd changes are applied in place by applyResolvedCwd and never
2875
+ // tear down the session. Retained as a stable hook for ask()'s pre-turn
2876
+ // call so the surrounding turn flow is unchanged.
2877
+ void reason;
1760
2878
  sessionNeedsCwdRefresh = false;
1761
- return await createCurrentSession();
1762
- }
1763
-
1764
- function buildWorkspaceContext() {
1765
- try {
1766
- return formatWorkspaceSessionContext(workspaceRouter.snapshot(currentCwd));
1767
- } catch (error) {
1768
- return [
1769
- '# Workspace',
1770
- `current cwd: ${currentCwd}`,
1771
- `project candidates: unavailable (${error?.message || String(error)})`,
1772
- ].join('\n');
1773
- }
2879
+ return session;
1774
2880
  }
1775
2881
 
1776
2882
  function skillContent(name) {
@@ -1781,6 +2887,12 @@ export async function createMixdogSessionRuntime({
1781
2887
  return { name, content };
1782
2888
  }
1783
2889
 
2890
+ function skillToolContent(name) {
2891
+ const skill = skillContent(name);
2892
+ const escapedName = String(skill.name || '').replace(/[<>&]/g, (ch) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[ch]));
2893
+ return `<skill>\n<name>${escapedName}</name>\n${skill.content}\n</skill>`;
2894
+ }
2895
+
1784
2896
  function addProjectSkill(input = {}) {
1785
2897
  const name = clean(input.name).replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
1786
2898
  if (!name) throw new Error('skill name is required');
@@ -1893,30 +3005,25 @@ export async function createMixdogSessionRuntime({
1893
3005
  return { name, config: { command, args, cwd: resolvedCwd } };
1894
3006
  }
1895
3007
 
1896
- const persistedBridgeMode = (() => {
1897
- try { return (sharedCfgMod.readSection('agent') || {}).bridgeMode; } catch { return undefined; }
1898
- })();
1899
- const bridgeStartedAt = performance.now();
1900
- const bridge = createStandaloneBridge({
3008
+ const agentToolStartedAt = performance.now();
3009
+ const agentTool = createStandaloneAgent({
1901
3010
  cfgMod,
1902
3011
  reg,
1903
3012
  mgr,
1904
3013
  dataDir: cfgMod.getPluginData(),
1905
3014
  cwd,
1906
- defaultMode: persistedBridgeMode ?? 'async',
1907
3015
  });
1908
- bootProfile('bridge:ready', { ms: (performance.now() - bridgeStartedAt).toFixed(1) });
1909
- const bridgeStatusState = () => {
3016
+ bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
3017
+ const agentStatusState = () => {
1910
3018
  try {
1911
- const status = bridge.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
3019
+ const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
1912
3020
  return {
1913
- bridgeMode: bridge.getDefaultMode?.() || status.bridgeMode || 'async',
1914
- bridgeWorkers: Array.isArray(status.workers) ? status.workers : [],
1915
- bridgeJobs: Array.isArray(status.jobs) ? status.jobs : [],
1916
- bridgeScope: status.scope || null,
3021
+ agentWorkers: Array.isArray(status.workers) ? status.workers : [],
3022
+ agentJobs: Array.isArray(status.jobs) ? status.jobs : [],
3023
+ agentScope: status.scope || null,
1917
3024
  };
1918
3025
  } catch {
1919
- return { bridgeMode: bridge.getDefaultMode?.() || 'async', bridgeWorkers: [], bridgeJobs: [], bridgeScope: null };
3026
+ return { agentWorkers: [], agentJobs: [], agentScope: null };
1920
3027
  }
1921
3028
  };
1922
3029
  const channelsStartedAt = performance.now();
@@ -1925,18 +3032,28 @@ export async function createMixdogSessionRuntime({
1925
3032
  rootDir: STANDALONE_ROOT,
1926
3033
  dataDir: cfgMod.getPluginData(),
1927
3034
  cwd,
3035
+ onNotify: (msg) => {
3036
+ if (msg?.method !== 'notifications/claude/channel') return;
3037
+ const params = msg?.params && typeof msg.params === 'object' ? msg.params : {};
3038
+ const meta = params.meta && typeof params.meta === 'object' ? params.meta : {};
3039
+ if (meta.silent_to_agent === true || meta.silent_to_agent === 'true') return;
3040
+ const instruction = typeof meta.instruction === 'string' ? meta.instruction.trim() : '';
3041
+ const content = instruction || String(params.content || '').trim();
3042
+ emitRuntimeNotification(content, meta);
3043
+ },
1928
3044
  });
1929
3045
  bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
1930
3046
  const toolsStartedAt = performance.now();
1931
3047
  const standaloneTools = [
1932
3048
  TOOL_SEARCH_TOOL,
3049
+ SKILL_TOOL,
1933
3050
  CWD_TOOL,
1934
3051
  EXPLORE_TOOL,
1935
3052
  ...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
1936
3053
  ...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
1937
3054
  ...(channelToolDefs?.TOOL_DEFS || []).filter((tool) => channels.isChannelTool(tool?.name)),
1938
3055
  ...(codeGraphToolDefs?.CODE_GRAPH_TOOL_DEFS || []).filter((tool) => tool?.name === 'code_graph'),
1939
- ...bridge.tools,
3056
+ ...agentTool.tools,
1940
3057
  PROVIDER_STATUS_TOOL,
1941
3058
  CHANNEL_STATUS_TOOL,
1942
3059
  ].map(applyStandaloneToolDefaults);
@@ -1957,7 +3074,7 @@ export async function createMixdogSessionRuntime({
1957
3074
  : [];
1958
3075
  const tools = filterDisallowedTools(previewTools, LEAD_DISALLOWED_TOOLS);
1959
3076
  const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
1960
- applyDeferredToolSurface(surface, mode, standaloneTools);
3077
+ applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
1961
3078
  return surface;
1962
3079
  }
1963
3080
 
@@ -1972,7 +3089,11 @@ export async function createMixdogSessionRuntime({
1972
3089
  const selected = Array.isArray(preSessionToolSurface.deferredSelectedTools)
1973
3090
  ? preSessionToolSurface.deferredSelectedTools
1974
3091
  : [];
1975
- if (selected.length) selectDeferredTools(session, selected, mode);
3092
+ const discovered = Array.isArray(preSessionToolSurface.deferredDiscoveredTools)
3093
+ ? preSessionToolSurface.deferredDiscoveredTools
3094
+ : [];
3095
+ const replay = [...new Set([...selected, ...discovered])];
3096
+ if (replay.length) selectDeferredTools(session, replay, deferredSurfaceModeForLead(mode));
1976
3097
  }
1977
3098
  internalTools.setInternalToolsProvider({
1978
3099
  tools: standaloneTools,
@@ -1988,35 +3109,8 @@ export async function createMixdogSessionRuntime({
1988
3109
  routingSessionId: callerSessionId,
1989
3110
  clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
1990
3111
  notifyFn: notifyFnForSession(callerSessionId),
1991
- agentSearch: name === 'search'
1992
- ? async (searchArgs) => {
1993
- const query = Array.isArray(searchArgs.keywords)
1994
- ? searchArgs.keywords.join('\n')
1995
- : String(searchArgs.keywords || searchArgs.query || '');
1996
- const prompt = searchArgs.prompt || [
1997
- 'Perform a concise web research task for Mixdog search.',
1998
- '',
1999
- `Query: ${query}`,
2000
- searchArgs.site ? `Site/domain restriction: ${searchArgs.site}` : null,
2001
- searchArgs.type ? `Search type: ${searchArgs.type}` : null,
2002
- searchArgs.locale ? `Locale: ${typeof searchArgs.locale === 'string' ? searchArgs.locale : JSON.stringify(searchArgs.locale)}` : null,
2003
- `Max results: ${Math.max(1, Math.min(20, Number(searchArgs.maxResults) || 10))}`,
2004
- '',
2005
- 'Return a short answer first, then cite useful results as title + URL + one-line snippet.',
2006
- 'Do not edit files.',
2007
- ].filter(Boolean).join('\n');
2008
- const rendered = await bridge.execute({
2009
- type: 'spawn',
2010
- agent: 'web-researcher',
2011
- tag: `search_${Date.now().toString(36)}`,
2012
- prompt,
2013
- cwd: callerCwd,
2014
- wait: true,
2015
- firstResponseTimeoutMs: Number.isFinite(Number(searchArgs.firstResponseTimeoutMs)) ? Number(searchArgs.firstResponseTimeoutMs) : 120_000,
2016
- idleTimeoutMs: Number.isFinite(Number(searchArgs.idleTimeoutMs)) ? Number(searchArgs.idleTimeoutMs) : 30 * 60_000,
2017
- }, { invocationSource: 'user-command', callerCwd });
2018
- return String(rendered || '').replace(/^bridge result[^\n]*\n/i, '').trim();
2019
- }
3112
+ nativeSearch: name === 'search'
3113
+ ? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerCtx?.signal || session?.controller?.signal })
2020
3114
  : undefined,
2021
3115
  });
2022
3116
  }
@@ -2030,7 +3124,9 @@ export async function createMixdogSessionRuntime({
2030
3124
  if (!codeGraphMod?.executeCodeGraphTool) throw new Error('code_graph runtime is not available');
2031
3125
  return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
2032
3126
  }
2033
- if (name === 'tool_search') return renderToolSearch(args, session, mode);
3127
+ if (name === 'tool_search') {
3128
+ return renderToolSearch(args, activeToolSurface(), mode);
3129
+ }
2034
3130
  if (name === 'explore') {
2035
3131
  const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
2036
3132
  return await runExplore(args || {}, {
@@ -2050,9 +3146,12 @@ export async function createMixdogSessionRuntime({
2050
3146
  }
2051
3147
  return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
2052
3148
  }
2053
- if (name === 'bridge') {
3149
+ if (name === 'Skill') {
3150
+ return skillToolContent(args?.name);
3151
+ }
3152
+ if (name === 'agent') {
2054
3153
  const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
2055
- return await bridge.execute(args, {
3154
+ return await agentTool.execute(args, {
2056
3155
  callerCwd,
2057
3156
  invocationSource: 'model-tool',
2058
3157
  callerSessionId,
@@ -2061,9 +3160,12 @@ export async function createMixdogSessionRuntime({
2061
3160
  notifyFn: notifyFnForSession(callerSessionId),
2062
3161
  });
2063
3162
  }
2064
- if (name === 'provider_status') return renderProviderStatus(cfgMod.loadConfig());
3163
+ if (name === 'provider_status') return renderProviderStatus(displayConfig());
2065
3164
  if (name === 'channel_status') return renderChannelStatus();
2066
- if (channels.isChannelTool(name)) return await channels.execute(name, args || {});
3165
+ if (channels.isChannelTool(name)) {
3166
+ if (!channelsEnabled()) throw new Error('channels are disabled in settings');
3167
+ return await channels.execute(name, args || {});
3168
+ }
2067
3169
  throw new Error(`unknown standalone internal tool: ${name}`);
2068
3170
  },
2069
3171
  });
@@ -2082,16 +3184,81 @@ export async function createMixdogSessionRuntime({
2082
3184
  function invalidateProviderCaches() {
2083
3185
  providerModelsCache = { models: null, at: 0 };
2084
3186
  providerModelsPromise = null;
3187
+ searchProviderModelsCache = { models: null, at: 0 };
3188
+ searchProviderModelsPromise = null;
3189
+ usageDashboardCache = { dashboard: null, at: 0 };
3190
+ usageDashboardPromise = null;
2085
3191
  providerSetupCache = { setup: null, at: 0 };
3192
+ providerSetupQuickCache = { setup: null, at: 0 };
2086
3193
  providerSetupPromise = null;
2087
3194
  providerInitPromise = null;
2088
3195
  modelMetaByRoute.clear();
2089
3196
  }
2090
3197
 
3198
+ function adoptConfig(nextConfig, { hasSecrets = configHasSecrets } = {}) {
3199
+ config = nextConfig;
3200
+ configHasSecrets = hasSecrets === true;
3201
+ setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
3202
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
3203
+ return config;
3204
+ }
3205
+
3206
+ // Persisting mixdog-config.json is heavy: it takes a cross-process file
3207
+ // lock, does an atomic temp+rename, and on win32 shells out to icacls
3208
+ // (an external process, multiple times per write) to enforce an owner-only
3209
+ // ACL. Doing that synchronously on EVERY option toggle froze the TUI main
3210
+ // thread, so adjusting a picker felt laggy and unresponsive. We split the
3211
+ // two concerns: adopt the new config in-memory IMMEDIATELY (so getProfile/
3212
+ // getAutoClear/etc. and the UI read fresh values on the same tick), and
3213
+ // DEBOUNCE the actual disk write so a burst of toggles collapses into a
3214
+ // single persist after input settles. The security model is unchanged —
3215
+ // cfgMod.saveConfig still strips apiKey (keychain-only) and still applies
3216
+ // the owner-only ACL; we only changed WHEN it runs, never HOW.
3217
+ let pendingConfigToSave = null;
3218
+ let configSaveTimer = null;
3219
+ const CONFIG_SAVE_DEBOUNCE_MS = 150;
3220
+ function flushConfigSave() {
3221
+ if (configSaveTimer) {
3222
+ clearTimeout(configSaveTimer);
3223
+ configSaveTimer = null;
3224
+ }
3225
+ if (pendingConfigToSave === null) return;
3226
+ const snapshot = pendingConfigToSave;
3227
+ pendingConfigToSave = null;
3228
+ try {
3229
+ cfgMod.saveConfig(snapshot);
3230
+ } catch (err) {
3231
+ process.stderr.write(`[config] debounced saveConfig failed: ${err?.message || err}\n`);
3232
+ }
3233
+ }
3234
+ function saveConfigAndAdopt(nextConfig, { hasSecrets = configHasSecrets } = {}) {
3235
+ // In-memory adopt is synchronous and first so callers that read back the
3236
+ // value immediately (e.g. setProfile -> getProfile) see the new state.
3237
+ const adopted = adoptConfig(nextConfig, { hasSecrets });
3238
+ // Persist the adopted object; coalesce rapid successive changes into one
3239
+ // disk write after CONFIG_SAVE_DEBOUNCE_MS of quiet.
3240
+ pendingConfigToSave = config;
3241
+ if (configSaveTimer) clearTimeout(configSaveTimer);
3242
+ configSaveTimer = setTimeout(flushConfigSave, CONFIG_SAVE_DEBOUNCE_MS);
3243
+ configSaveTimer.unref?.();
3244
+ return adopted;
3245
+ }
3246
+
3247
+ function reloadFullConfig() {
3248
+ // A pending debounced write holds the only copy of the latest change.
3249
+ // Flush it before re-reading from disk so loadConfig() observes (and the
3250
+ // subsequent adopt preserves) that change instead of reverting to a stale
3251
+ // on-disk snapshot.
3252
+ flushConfigSave();
3253
+ return adoptConfig(cfgMod.loadConfig(), { hasSecrets: true });
3254
+ }
3255
+
2091
3256
  function ensureFullConfig() {
2092
3257
  if (configHasSecrets) return config;
2093
- config = cfgMod.loadConfig();
2094
- configHasSecrets = true;
3258
+ return reloadFullConfig();
3259
+ }
3260
+
3261
+ function displayConfig() {
2095
3262
  return config;
2096
3263
  }
2097
3264
 
@@ -2104,6 +3271,19 @@ export async function createMixdogSessionRuntime({
2104
3271
  return ensureFullConfig();
2105
3272
  }
2106
3273
 
3274
+ function refreshStatuslineUsageSnapshot(routeLike = {}) {
3275
+ const providerId = clean(routeLike.provider);
3276
+ const modelId = clean(routeLike.model);
3277
+ if (!providerId || !providerId.includes('oauth')) return;
3278
+ const providerObj = reg.getProvider(providerId);
3279
+ if (!providerObj) return;
3280
+ void fetchOAuthUsageSnapshot({ provider: providerId, model: modelId }, providerObj, (message) => {
3281
+ if (process.env.MIXDOG_STATUSLINE_TRACE) {
3282
+ try { process.stderr.write(`[statusline] ${message}\n`); } catch {}
3283
+ }
3284
+ }).catch(() => {});
3285
+ }
3286
+
2107
3287
  async function ensureProvidersReady(providerConfig = config.providers || {}) {
2108
3288
  if (providerInitPromise) return await providerInitPromise;
2109
3289
  providerInitPromise = reg.initProviders(providerConfig)
@@ -2113,13 +3293,23 @@ export async function createMixdogSessionRuntime({
2113
3293
  return await providerInitPromise;
2114
3294
  }
2115
3295
 
2116
- async function cachedProviderSetup({ force = false } = {}) {
2117
- const now = Date.now();
2118
- if (!force && providerSetupCache.setup && now - providerSetupCache.at < PROVIDER_SETUP_CACHE_TTL_MS) {
3296
+ async function cachedProviderSetup({ force = false, quick = false } = {}) {
3297
+ if (!force && providerSetupCache.setup) {
2119
3298
  return providerSetupCache.setup;
2120
3299
  }
2121
- if (!force && providerSetupPromise) return await providerSetupPromise;
2122
- providerSetupPromise = providerSetup(cfgMod.loadConfig())
3300
+ if (quick) {
3301
+ if (!force && providerSetupQuickCache.setup) {
3302
+ return providerSetupQuickCache.setup;
3303
+ }
3304
+ const setup = await providerSetup(displayConfig(), { detectLocal: false, checkSecrets: false });
3305
+ providerSetupQuickCache = { setup, at: Date.now() };
3306
+ if (!providerSetupPromise && !providerSetupWarmupTimer && !closeRequested) {
3307
+ scheduleProviderSetupWarmup(0);
3308
+ }
3309
+ return setup;
3310
+ }
3311
+ if (providerSetupPromise) return await providerSetupPromise;
3312
+ providerSetupPromise = providerSetup(displayConfig(), { detectLocal: true })
2123
3313
  .then((setup) => {
2124
3314
  providerSetupCache = { setup, at: Date.now() };
2125
3315
  return setup;
@@ -2267,9 +3457,10 @@ function parsedProviderModelVersion(id) {
2267
3457
  description: m.description || '',
2268
3458
  supportsVision: m.supportsVision === true,
2269
3459
  supportsFunctionCalling: m.supportsFunctionCalling === true,
3460
+ supportsWebSearch: searchCapableFor(name, m),
2270
3461
  supportsPromptCaching: m.supportsPromptCaching === true,
2271
3462
  supportsReasoning: m.supportsReasoning === true,
2272
- reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : [],
3463
+ reasoningLevels: Array.isArray(m.reasoningLevels) ? m.reasoningLevels : undefined,
2273
3464
  reasoningOptions: Array.isArray(m.reasoningOptions) ? m.reasoningOptions : [],
2274
3465
  reasoningContentField: m.reasoningContentField || null,
2275
3466
  mode: m.mode || null,
@@ -2291,40 +3482,265 @@ function parsedProviderModelVersion(id) {
2291
3482
  return sortProviderModels((rows || []).map(hydrateProviderModelRow));
2292
3483
  }
2293
3484
 
2294
- async function loadProviderModelsFresh() {
3485
+ function quickProviderModelRows() {
3486
+ const pickerConfig = displayConfig();
3487
+ const rows = [];
3488
+ const seen = new Set();
3489
+ const addRoute = (routeLike = {}) => {
3490
+ const provider = clean(routeLike.provider);
3491
+ const model = clean(routeLike.model);
3492
+ if (!provider || !model) return;
3493
+ const key = `${provider}:${model}`;
3494
+ if (seen.has(key)) return;
3495
+ seen.add(key);
3496
+ const row = providerModelCacheRow(provider, {
3497
+ id: model,
3498
+ name: routeLike.modelDisplay || routeLike.display || model,
3499
+ display: routeLike.modelDisplay || routeLike.display || model,
3500
+ latest: routeLike.latest === true,
3501
+ supportsReasoning: !!routeLike.effort,
3502
+ mode: 'chat',
3503
+ });
3504
+ rows.push(row);
3505
+ modelMetaByRoute.set(modelMetaKey(provider, model), row);
3506
+ };
3507
+
3508
+ addRoute(route);
3509
+ for (const preset of pickerConfig.presets || []) addRoute(preset);
3510
+ for (const workflowRoute of Object.values(pickerConfig.workflowRoutes || {})) addRoute(workflowRoute);
3511
+ for (const agentRoute of Object.values(pickerConfig.agents || {})) addRoute(agentRoute);
3512
+ return providerModelsFromCacheRows(rows);
3513
+ }
3514
+
3515
+ function addQuickSearchModel(rows, seen, provider, model) {
3516
+ const providerName = normalizeSearchProviderId(provider);
3517
+ const modelId = clean(model?.id || model);
3518
+ if (!providerName || !modelId || !isSearchCapableProvider(providerName)) return;
3519
+ const key = `${providerName}:${modelId}`;
3520
+ if (seen.has(key)) return;
3521
+ const row = providerModelCacheRow(providerName, {
3522
+ id: modelId,
3523
+ name: model?.name || model?.display || modelId,
3524
+ display: model?.display || model?.name || modelId,
3525
+ contextWindow: model?.contextWindow || null,
3526
+ outputTokens: model?.outputTokens || null,
3527
+ latest: model?.latest === true,
3528
+ supportsWebSearch: true,
3529
+ supportsFunctionCalling: model?.supportsFunctionCalling === true,
3530
+ supportsPromptCaching: model?.supportsPromptCaching === true,
3531
+ supportsReasoning: model?.supportsReasoning === true,
3532
+ reasoningLevels: Array.isArray(model?.reasoningLevels) ? model.reasoningLevels : undefined,
3533
+ reasoningOptions: Array.isArray(model?.reasoningOptions) ? model.reasoningOptions : [],
3534
+ mode: 'chat',
3535
+ });
3536
+ if (row.supportsWebSearch !== true) return;
3537
+ seen.add(key);
3538
+ rows.push({
3539
+ ...row,
3540
+ provider: providerName,
3541
+ searchCapable: true,
3542
+ searchToolType: row.searchToolType || 'web_search',
3543
+ });
3544
+ }
3545
+
3546
+ function addDefaultSearchModel(rows, seen = new Set()) {
3547
+ const mainModel = currentMainSearchModelMeta();
3548
+ if (!mainModel || !searchCapableFor(route.provider, mainModel)) return;
3549
+ const key = `${SEARCH_DEFAULT_PROVIDER}:${SEARCH_DEFAULT_MODEL}`;
3550
+ if (seen.has(key)) return;
3551
+ seen.add(key);
3552
+ rows.push({
3553
+ id: SEARCH_DEFAULT_MODEL,
3554
+ provider: SEARCH_DEFAULT_PROVIDER,
3555
+ display: 'Default',
3556
+ name: 'Default',
3557
+ description: `Use current main model: ${route.provider}/${route.model}`,
3558
+ supportsWebSearch: true,
3559
+ searchCapable: true,
3560
+ searchToolType: 'web_search',
3561
+ mode: 'chat',
3562
+ });
3563
+ }
3564
+
3565
+ function quickSearchProviderModelRows() {
3566
+ const pickerConfig = displayConfig();
3567
+ const rows = [];
3568
+ const seen = new Set();
3569
+ addDefaultSearchModel(rows, seen);
3570
+ for (const [name, providerConfig] of Object.entries(pickerConfig.providers || {})) {
3571
+ const providerName = normalizeSearchProviderId(name);
3572
+ if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
3573
+ for (const model of QUICK_SEARCH_MODELS[providerName] || []) {
3574
+ addQuickSearchModel(rows, seen, providerName, model);
3575
+ }
3576
+ }
3577
+ const configuredSearch = normalizeSearchRouteConfig(pickerConfig.searchRoute) || normalizeSearchRouteConfig(searchRoute);
3578
+ if (configuredSearch?.provider && configuredSearch?.model) {
3579
+ addQuickSearchModel(rows, seen, configuredSearch.provider, {
3580
+ id: configuredSearch.model,
3581
+ display: configuredSearch.model,
3582
+ });
3583
+ }
3584
+ const mainModel = currentMainSearchModelMeta();
3585
+ if (mainModel && searchCapableFor(route.provider, mainModel)) {
3586
+ addQuickSearchModel(rows, seen, route.provider, {
3587
+ id: route.model,
3588
+ display: route.model,
3589
+ });
3590
+ }
3591
+ return searchModelsFromRows(rows);
3592
+ }
3593
+
3594
+ function searchModelsFromRows(rows) {
3595
+ return sortProviderModels((rows || [])
3596
+ .filter((row) => row.supportsWebSearch === true)
3597
+ .map((row) => ({
3598
+ ...row,
3599
+ provider: normalizeSearchProviderId(row.provider),
3600
+ searchCapable: true,
3601
+ searchToolType: row.searchToolType || 'web_search',
3602
+ })));
3603
+ }
3604
+
3605
+ function searchRowsWithDefault(rows = []) {
3606
+ const out = [];
3607
+ const seen = new Set();
3608
+ addDefaultSearchModel(out, seen);
3609
+ for (const row of rows || []) {
3610
+ const providerName = normalizeSearchProviderId(row?.provider);
3611
+ const modelId = clean(row?.id || row?.model);
3612
+ if (providerName === SEARCH_DEFAULT_PROVIDER && modelId.toLowerCase() === SEARCH_DEFAULT_MODEL) continue;
3613
+ const key = `${providerName}:${modelId}`;
3614
+ if (!providerName || !modelId || seen.has(key)) continue;
3615
+ seen.add(key);
3616
+ out.push(row);
3617
+ }
3618
+ return out;
3619
+ }
3620
+
3621
+ async function collectSearchProviderModels({ force = false } = {}) {
3622
+ if (!force && Array.isArray(searchProviderModelsCache.models)) {
3623
+ return providerModelsFromCacheRows(searchRowsWithDefault(searchProviderModelsCache.models));
3624
+ }
3625
+ if (!force && Array.isArray(providerModelsCache.models)) {
3626
+ const rows = searchRowsWithDefault(searchModelsFromRows(providerModelsCache.models));
3627
+ searchProviderModelsCache = { models: rows, at: Date.now() };
3628
+ return providerModelsFromCacheRows(rows);
3629
+ }
3630
+ if (!force) {
3631
+ const rows = searchRowsWithDefault(quickSearchProviderModelRows());
3632
+ searchProviderModelsCache = { models: rows, at: Date.now() };
3633
+ return providerModelsFromCacheRows(rows);
3634
+ }
3635
+ if (!searchProviderModelsPromise) {
3636
+ searchProviderModelsPromise = loadSearchProviderModelsFresh()
3637
+ .then((models) => {
3638
+ searchProviderModelsCache = { models, at: Date.now() };
3639
+ return models;
3640
+ })
3641
+ .finally(() => {
3642
+ searchProviderModelsPromise = null;
3643
+ });
3644
+ }
3645
+ return providerModelsFromCacheRows(await searchProviderModelsPromise);
3646
+ }
3647
+
3648
+ function enabledSearchProviderConfig() {
2295
3649
  ensureFullConfig();
3650
+ const out = {};
3651
+ for (const [name, providerConfig] of Object.entries(config.providers || {})) {
3652
+ const providerName = normalizeSearchProviderId(name);
3653
+ if (!providerConfig?.enabled || !isSearchCapableProvider(providerName)) continue;
3654
+ out[providerName] = { ...providerConfig, enabled: true };
3655
+ }
3656
+ return out;
3657
+ }
3658
+
3659
+ async function loadSearchProviderModelsFresh() {
3660
+ const searchProviders = enabledSearchProviderConfig();
3661
+ const providerNames = Object.keys(searchProviders);
3662
+ if (!providerNames.length) return [];
2296
3663
  await ensureProvidersReady(config.providers || {});
2297
- const allProviders = reg.getAllProviders();
3664
+ const providerResults = await Promise.all(providerNames.map(async (name) => {
3665
+ const provider = reg.getProvider(name);
3666
+ if (typeof provider?.listModels !== 'function') return [];
3667
+ try {
3668
+ const models = await provider.listModels();
3669
+ if (!Array.isArray(models)) return [];
3670
+ const rows = [];
3671
+ for (const m of models) {
3672
+ if (!m?.id || !isSelectableLlmModel(m)) continue;
3673
+ const row = providerModelCacheRow(name, m);
3674
+ if (row.supportsWebSearch !== true) continue;
3675
+ rows.push({
3676
+ ...row,
3677
+ provider: normalizeSearchProviderId(row.provider),
3678
+ searchCapable: true,
3679
+ searchToolType: row.searchToolType || 'web_search',
3680
+ });
3681
+ modelMetaByRoute.set(modelMetaKey(name, m.id), row);
3682
+ }
3683
+ return rows;
3684
+ } catch {
3685
+ // Keep the picker responsive if one search-capable provider has a
3686
+ // transient catalog/auth failure.
3687
+ return [];
3688
+ }
3689
+ }));
2298
3690
  const results = [];
2299
3691
  const seen = new Set();
2300
- for (const [name, provider] of allProviders) {
2301
- if (typeof provider?.listModels !== 'function') continue;
3692
+ addDefaultSearchModel(results, seen);
3693
+ for (const row of providerResults.flat()) {
3694
+ const key = `${normalizeSearchProviderId(row.provider)}:${row.id}`;
3695
+ if (seen.has(key)) continue;
3696
+ seen.add(key);
3697
+ results.push(row);
3698
+ }
3699
+ return results;
3700
+ }
3701
+
3702
+ async function loadProviderModelsFresh() {
3703
+ ensureFullConfig();
3704
+ await ensureProvidersReady(config.providers || {});
3705
+ const allProviders = [...reg.getAllProviders()];
3706
+ const providerResults = await Promise.all(allProviders.map(async ([name, provider]) => {
3707
+ if (typeof provider?.listModels !== 'function') return [];
2302
3708
  try {
2303
3709
  const models = await provider.listModels();
2304
- if (Array.isArray(models)) {
2305
- for (const m of models) {
2306
- if (!m?.id) continue;
2307
- if (!isSelectableLlmModel(m)) continue;
2308
- const key = `${name}:${m.id}`;
2309
- if (seen.has(key)) continue;
2310
- seen.add(key);
2311
- const row = providerModelCacheRow(name, m);
2312
- results.push(row);
2313
- modelMetaByRoute.set(modelMetaKey(name, m.id), row);
2314
- }
3710
+ if (!Array.isArray(models)) return [];
3711
+ const rows = [];
3712
+ for (const m of models) {
3713
+ if (!m?.id) continue;
3714
+ if (!isSelectableLlmModel(m)) continue;
3715
+ rows.push(providerModelCacheRow(name, m));
2315
3716
  }
3717
+ return rows;
2316
3718
  } catch {
2317
3719
  // Ignore per-provider catalog failures so one bad credential or
2318
3720
  // transient /models error does not hide other authenticated models.
3721
+ return [];
2319
3722
  }
3723
+ }));
3724
+ const results = [];
3725
+ const seen = new Set();
3726
+ for (const row of providerResults.flat()) {
3727
+ const key = `${row.provider}:${row.id}`;
3728
+ if (seen.has(key)) continue;
3729
+ seen.add(key);
3730
+ results.push(row);
3731
+ modelMetaByRoute.set(modelMetaKey(row.provider, row.id), row);
2320
3732
  }
2321
3733
  return results;
2322
3734
  }
2323
3735
 
2324
- async function collectProviderModels({ force = false } = {}) {
3736
+ async function collectProviderModels({ force = false, quick = false } = {}) {
2325
3737
  if (!force && Array.isArray(providerModelsCache.models)) {
2326
3738
  return providerModelsFromCacheRows(providerModelsCache.models);
2327
3739
  }
3740
+ if (!force && quick) {
3741
+ warmProviderModelCache();
3742
+ return quickProviderModelRows();
3743
+ }
2328
3744
  if (!providerModelsPromise) {
2329
3745
  providerModelsPromise = loadProviderModelsFresh()
2330
3746
  .then((models) => {
@@ -2362,7 +3778,7 @@ function parsedProviderModelVersion(id) {
2362
3778
  const models = await collectProviderModels();
2363
3779
  const picked = models[0] || null;
2364
3780
  if (!picked) {
2365
- throw new Error('No provider models available. Run /providers to authenticate, then /model to choose a model.');
3781
+ throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
2366
3782
  }
2367
3783
  route = {
2368
3784
  ...route,
@@ -2395,7 +3811,7 @@ function parsedProviderModelVersion(id) {
2395
3811
 
2396
3812
  function requireModelRoute() {
2397
3813
  if (routeHasModel()) return;
2398
- throw new Error('No model configured. Run /providers to authenticate, then /model to choose a model.');
3814
+ throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
2399
3815
  }
2400
3816
 
2401
3817
  async function recreateCurrentSessionIfReady() {
@@ -2434,8 +3850,8 @@ function parsedProviderModelVersion(id) {
2434
3850
  const coreMemoryContext = await loadCoreMemoryContext();
2435
3851
  if (closeRequested) throw new Error('runtime is closing');
2436
3852
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3853
+ const workflow = activeWorkflowSummary(config, dataDir);
2437
3854
  const workflowContext = workflowContextBlock(config, dataDir);
2438
- const workspaceContext = buildWorkspaceContext();
2439
3855
  const sessionOpts = {
2440
3856
  provider: route.provider,
2441
3857
  model: route.model,
@@ -2450,9 +3866,12 @@ function parsedProviderModelVersion(id) {
2450
3866
  disallowedTools: LEAD_DISALLOWED_TOOLS,
2451
3867
  cwd: currentCwd,
2452
3868
  coreMemoryContext,
3869
+ workflow,
2453
3870
  workflowContext,
2454
- workspaceContext,
2455
3871
  fast: route.fast === true,
3872
+ compaction: config.compaction && typeof config.compaction === 'object'
3873
+ ? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
3874
+ : undefined,
2456
3875
  };
2457
3876
  if (hasOwn(route, 'effort') || route.effectiveEffort) {
2458
3877
  sessionOpts.effort = route.effectiveEffort || null;
@@ -2460,15 +3879,55 @@ function parsedProviderModelVersion(id) {
2460
3879
  session = mgr.createSession(sessionOpts);
2461
3880
  sessionNeedsCwdRefresh = false;
2462
3881
  Object.defineProperty(session, 'beforeToolHook', {
2463
- value: (input) => hooks.beforeTool(input),
3882
+ value: (input) => hooks.beforeTool(hookCommonPayload({
3883
+ ...input,
3884
+ session_id: input?.sessionId || input?.session_id || session?.id,
3885
+ tool_name: input?.name || input?.tool_name,
3886
+ tool_input: input?.args || input?.tool_input,
3887
+ tool_use_id: input?.toolCallId || input?.tool_use_id,
3888
+ cwd: input?.cwd || currentCwd,
3889
+ })),
3890
+ enumerable: false,
3891
+ configurable: true,
3892
+ writable: true,
3893
+ });
3894
+ // PostToolUse: bridge runtime tool completions to the standard hook bus.
3895
+ // dispatch() returns a promise; the loop's afterToolHook caller already
3896
+ // try/catches, so a rejection cannot escape the tool loop.
3897
+ Object.defineProperty(session, 'afterToolHook', {
3898
+ value: (input) => hooks.dispatch('PostToolUse', hookCommonPayload({
3899
+ session_id: input?.sessionId || input?.session_id || session?.id,
3900
+ cwd: input?.cwd || currentCwd,
3901
+ tool_name: input?.name,
3902
+ tool_input: input?.args,
3903
+ tool_use_id: input?.toolCallId || input?.tool_use_id,
3904
+ tool_response: input?.result,
3905
+ })),
2464
3906
  enumerable: false,
2465
3907
  configurable: true,
2466
3908
  writable: true,
2467
3909
  });
2468
- applyDeferredToolSurface(session, mode, standaloneTools);
3910
+ applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
2469
3911
  applyPreSessionToolSelection();
2470
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
3912
+ writeStatuslineRoute(statusRoutes, session, route);
2471
3913
  hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
3914
+ // SessionStart: bridge to the standard Claude Code hook bus. Best-effort;
3915
+ // a hook error must never break session creation. additionalContext is
3916
+ // injected before the first user turn as a system-reminder context pair.
3917
+ try {
3918
+ const startSource = /resume/i.test(String(reason || ''))
3919
+ ? 'resume'
3920
+ : (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
3921
+ const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: session.id, source: startSource, model: route.model }));
3922
+ const startContext = Array.isArray(startDispatch?.additionalContext)
3923
+ ? startDispatch.additionalContext.join('\n\n')
3924
+ : String(startDispatch?.additionalContext || '');
3925
+ if (startContext.trim()) {
3926
+ session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
3927
+ session.messages.push({ role: 'assistant', content: '.' });
3928
+ session.updatedAt = Date.now();
3929
+ }
3930
+ } catch { /* best-effort: never break session create */ }
2472
3931
  bootProfile('session:create:ready', {
2473
3932
  ms: (performance.now() - startedAt).toFixed(1),
2474
3933
  reason,
@@ -2487,7 +3946,10 @@ function parsedProviderModelVersion(id) {
2487
3946
  }
2488
3947
 
2489
3948
  function scheduleLeadSessionPrewarm() {
2490
- if (envFlag('MIXDOG_DISABLE_SESSION_PREWARM')) return;
3949
+ if (!sessionPrewarmEnabled) {
3950
+ bootProfile('session:prewarm-skipped');
3951
+ return;
3952
+ }
2491
3953
  const timer = setTimeout(() => {
2492
3954
  if (closeRequested || session?.id || sessionCreatePromise || activeTurnCount > 0) return;
2493
3955
  void createCurrentSession('prewarm')
@@ -2498,7 +3960,7 @@ function parsedProviderModelVersion(id) {
2498
3960
  }
2499
3961
 
2500
3962
  function scheduleProviderWarmup(delayMs = providerWarmupDelayMs) {
2501
- if (envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')) {
3963
+ if (!providerWarmupEnabled) {
2502
3964
  bootProfile('providers:warm-skipped');
2503
3965
  return;
2504
3966
  }
@@ -2506,6 +3968,10 @@ function parsedProviderModelVersion(id) {
2506
3968
  providerWarmupTimer = setTimeout(() => {
2507
3969
  providerWarmupTimer = null;
2508
3970
  if (closeRequested) return;
3971
+ if (!firstTurnCompleted && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
3972
+ bootProfile('providers:warm-deferred', { reason: 'first-turn-pending' });
3973
+ return;
3974
+ }
2509
3975
  if (activeTurnCount > 0 || sessionCreatePromise) {
2510
3976
  bootProfile('providers:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
2511
3977
  scheduleProviderWarmup(backgroundBusyRetryMs);
@@ -2513,8 +3979,7 @@ function parsedProviderModelVersion(id) {
2513
3979
  }
2514
3980
  const providersStartedAt = performance.now();
2515
3981
  try {
2516
- config = cfgMod.loadConfig();
2517
- configHasSecrets = true;
3982
+ reloadFullConfig();
2518
3983
  } catch (error) {
2519
3984
  bootProfile('config:full-failed', { error: error?.message || String(error) });
2520
3985
  }
@@ -2522,28 +3987,27 @@ function parsedProviderModelVersion(id) {
2522
3987
  .then(() => {
2523
3988
  bootProfile('providers:init:ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
2524
3989
  if (closeRequested) return null;
2525
- if (activeTurnCount > 0 || sessionCreatePromise) {
2526
- bootProfile('providers:optional-warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
2527
- scheduleProviderWarmup(backgroundBusyRetryMs);
2528
- return null;
2529
- }
2530
- return cachedProviderSetup();
2531
- })
2532
- .then((setup) => {
2533
- if (!setup) return;
2534
- bootProfile('provider-setup:warm-ready', {
2535
- api: Array.isArray(setup?.api) ? setup.api.length : 0,
2536
- oauth: Array.isArray(setup?.oauth) ? setup.oauth.length : 0,
2537
- local: Array.isArray(setup?.local) ? setup.local.length : 0,
2538
- });
3990
+ return true;
2539
3991
  })
2540
3992
  .catch((error) => bootProfile('providers:warm-failed', { error: error?.message || String(error) }));
2541
3993
  }, delayMs);
2542
3994
  providerWarmupTimer.unref?.();
2543
3995
  }
2544
3996
 
3997
+ function scheduleProviderSetupWarmup(delayMs = providerSetupWarmupDelayMs) {
3998
+ if (providerSetupWarmupTimer || closeRequested) return;
3999
+ providerSetupWarmupTimer = setTimeout(() => {
4000
+ providerSetupWarmupTimer = null;
4001
+ if (closeRequested) return;
4002
+ void cachedProviderSetup()
4003
+ .then(() => bootProfile('provider-setup:warm-ready'))
4004
+ .catch((error) => bootProfile('provider-setup:warm-failed', { error: error?.message || String(error) }));
4005
+ }, delayMs);
4006
+ providerSetupWarmupTimer.unref?.();
4007
+ }
4008
+
2545
4009
  function scheduleProviderModelWarmup(delayMs = providerModelWarmupDelayMs) {
2546
- if (envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')) return;
4010
+ if (!modelPrefetchEnabled) return;
2547
4011
  if (providerModelWarmupTimer || closeRequested) return;
2548
4012
  providerModelWarmupTimer = setTimeout(() => {
2549
4013
  providerModelWarmupTimer = null;
@@ -2558,11 +4022,75 @@ function parsedProviderModelVersion(id) {
2558
4022
  providerModelWarmupTimer.unref?.();
2559
4023
  }
2560
4024
 
4025
+ function scheduleStatuslineUsageWarmup(delayMs = statuslineUsageWarmupDelayMs) {
4026
+ const providerId = clean(route?.provider);
4027
+ if (!providerId || !providerId.includes('oauth')) {
4028
+ bootProfile('statusline-usage:warm-skipped', { provider: providerId || null });
4029
+ return;
4030
+ }
4031
+ if (statuslineUsageWarmupTimer || closeRequested) return;
4032
+ statuslineUsageWarmupTimer = setTimeout(async () => {
4033
+ statuslineUsageWarmupTimer = null;
4034
+ if (closeRequested) return;
4035
+ if (activeTurnCount > 0 || sessionCreatePromise) {
4036
+ bootProfile('statusline-usage:warm-deferred', { reason: activeTurnCount > 0 ? 'turn-active' : 'session-create' });
4037
+ scheduleStatuslineUsageWarmup(backgroundBusyRetryMs);
4038
+ return;
4039
+ }
4040
+ try {
4041
+ ensureConfigForRouteProvider();
4042
+ await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
4043
+ if (closeRequested) return;
4044
+ refreshStatuslineUsageSnapshot(route);
4045
+ bootProfile('statusline-usage:warm-ready', { provider: clean(route?.provider) });
4046
+ } catch (error) {
4047
+ bootProfile('statusline-usage:warm-failed', { error: error?.message || String(error) });
4048
+ } finally {
4049
+ scheduleStatuslineUsageRefresh();
4050
+ }
4051
+ }, delayMs);
4052
+ statuslineUsageWarmupTimer.unref?.();
4053
+ }
4054
+
4055
+ // Idle keep-alive loop: periodically re-fetch the OAuth usage snapshot so its
4056
+ // cachedAt stays "live-fresh" and the statusline usage segment does not vanish
4057
+ // after LIVE_USAGE_SNAPSHOT_MAX_AGE_MS while the session is idle. Turn-driven
4058
+ // refreshes (recordStandaloneStatusTelemetry) already cover active sessions.
4059
+ function scheduleStatuslineUsageRefresh(delayMs = statuslineUsageRefreshDelayMs) {
4060
+ const providerId = clean(route?.provider);
4061
+ if (!providerId || !providerId.includes('oauth')) return;
4062
+ if (statuslineUsageRefreshTimer || closeRequested) return;
4063
+ statuslineUsageRefreshTimer = setTimeout(async () => {
4064
+ statuslineUsageRefreshTimer = null;
4065
+ if (closeRequested) return;
4066
+ if (activeTurnCount > 0 || sessionCreatePromise) {
4067
+ // Active turns refresh usage on their own; just re-arm the idle loop.
4068
+ scheduleStatuslineUsageRefresh();
4069
+ return;
4070
+ }
4071
+ try {
4072
+ ensureConfigForRouteProvider();
4073
+ await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
4074
+ if (closeRequested) return;
4075
+ refreshStatuslineUsageSnapshot(route);
4076
+ } catch {
4077
+ // Usage display must never affect the session runtime.
4078
+ } finally {
4079
+ scheduleStatuslineUsageRefresh();
4080
+ }
4081
+ }, delayMs);
4082
+ statuslineUsageRefreshTimer.unref?.();
4083
+ }
4084
+
2561
4085
  function scheduleChannelStart(delayMs = channelStartDelayMs) {
2562
4086
  if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
2563
4087
  bootProfile('channels:start-skipped');
2564
4088
  return;
2565
4089
  }
4090
+ if (!channelsEnabled()) {
4091
+ bootProfile('channels:start-disabled');
4092
+ return;
4093
+ }
2566
4094
  if (channelStartTimer || closeRequested) return;
2567
4095
  bootProfile('channels:start-scheduled', { delayMs });
2568
4096
  channelStartTimer = setTimeout(() => {
@@ -2597,12 +4125,31 @@ function parsedProviderModelVersion(id) {
2597
4125
  });
2598
4126
  }
2599
4127
 
2600
- bootProfile('session-runtime:ready', { lazySession: true, prewarmSession: true });
4128
+ bootProfile('session-runtime:ready', {
4129
+ lazySession: true,
4130
+ prewarmSession: sessionPrewarmEnabled,
4131
+ providerWarmup: providerWarmupEnabled,
4132
+ codeGraphPrewarm: codeGraphPrewarmEnabled,
4133
+ });
2601
4134
  scheduleLeadSessionPrewarm();
4135
+ // Lazy mode (default): skip the startup prewarm entirely; the first turn
4136
+ // triggers it instead (see ask()). Eager mode keeps the old startup schedule.
4137
+ if (!codeGraphPrewarmLazy) {
4138
+ scheduleCodeGraphPrewarm(codeGraphPrewarmDelayMs, 'startup');
4139
+ } else {
4140
+ bootProfile('code-graph:prewarm-lazy', { reason: 'startup-deferred-to-first-turn' });
4141
+ }
4142
+ scheduleProviderSetupWarmup();
2602
4143
  scheduleProviderWarmup();
4144
+ // Warm the provider model catalog in the background, but keep it on its own
4145
+ // delay so short-lived detached runtimes can exit before /models I/O starts.
4146
+ // Operators that want earlier catalog warming can lower
4147
+ // MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS explicitly.
4148
+ scheduleProviderModelWarmup();
4149
+ scheduleStatuslineUsageWarmup();
2603
4150
  scheduleChannelStart();
2604
4151
 
2605
- function contextStatusCacheKeyFor({ messages, tools, bridgeMode }) {
4152
+ function contextStatusCacheKeyFor({ messages, tools }) {
2606
4153
  const compaction = session?.compaction || {};
2607
4154
  const lastMessage = messages[messages.length - 1] || null;
2608
4155
  return {
@@ -2612,7 +4159,6 @@ function parsedProviderModelVersion(id) {
2612
4159
  model: route.model,
2613
4160
  cwd: currentCwd,
2614
4161
  mode,
2615
- bridgeMode,
2616
4162
  messages,
2617
4163
  messageCount: messages.length,
2618
4164
  lastMessage,
@@ -2685,23 +4231,25 @@ function parsedProviderModelVersion(id) {
2685
4231
  get toolMode() {
2686
4232
  return mode;
2687
4233
  },
2688
- get bridgeMode() {
2689
- return bridge.getDefaultMode();
2690
- },
2691
4234
  get autoClear() {
2692
4235
  return normalizeAutoClearConfig(config.autoClear);
2693
4236
  },
2694
4237
  get systemShell() {
2695
4238
  return normalizeSystemShellConfig(config.shell);
2696
4239
  },
4240
+ get searchRoute() {
4241
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
4242
+ return searchRoute;
4243
+ },
2697
4244
  get workflow() {
2698
4245
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
2699
- const pack = loadWorkflowPack(dataDir, activeWorkflowId(config));
2700
- return pack ? { id: pack.id, name: pack.name, description: pack.description, source: pack.source } : { id: DEFAULT_WORKFLOW_ID, name: 'Default' };
4246
+ if (session?.workflow && typeof session.workflow === 'object') {
4247
+ return workflowSummary(session.workflow);
4248
+ }
4249
+ return activeWorkflowSummary(config, dataDir);
2701
4250
  },
2702
4251
  get outputStyle() {
2703
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
2704
- return outputStyleStatus(dataDir).current;
4252
+ return getOutputStyleStatusCached().current;
2705
4253
  },
2706
4254
  get cwd() {
2707
4255
  return currentCwd;
@@ -2710,22 +4258,31 @@ function parsedProviderModelVersion(id) {
2710
4258
  return session;
2711
4259
  },
2712
4260
  contextStatus() {
2713
- const messages = Array.isArray(session?.messages) ? session.messages : [];
4261
+ // Prefer the in-flight working transcript while a turn is running so the
4262
+ // context gauge reflects LIVE growth (user turn + tool calls/results) as
4263
+ // it accumulates, instead of freezing at the pre-turn committed snapshot.
4264
+ // askSession() sets session.liveTurnMessages for the turn duration and
4265
+ // clears it on commit/cancel/error, after which we fall back to the
4266
+ // authoritative committed transcript.
4267
+ const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
4268
+ const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
2714
4269
  const tools = Array.isArray(session?.tools) ? session.tools : [];
2715
- const bridgeMode = bridge.getDefaultMode();
2716
- const cacheKey = contextStatusCacheKeyFor({ messages, tools, bridgeMode });
4270
+ const cacheKey = contextStatusCacheKeyFor({ messages, tools });
2717
4271
  if (contextStatusCacheValue && sameContextStatusCacheKey(cacheKey, contextStatusCacheKey)) {
2718
4272
  return contextStatusCacheValue;
2719
4273
  }
2720
4274
 
2721
4275
  const messageSummary = summarizeContextMessages(messages);
2722
4276
  const toolSchemaTokens = estimateToolSchemaTokens(tools);
4277
+ const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
2723
4278
  const requestReserveTokens = estimateRequestReserveTokens(tools);
2724
4279
  const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
2725
4280
  const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
2726
4281
  const effectiveWindow = Number(session?.contextWindow || rawWindow || 0);
2727
4282
  const lastContextTokens = Number(session?.lastContextTokens || 0);
2728
- const estimatedContextTokens = messageSummary.estimatedTokens + requestReserveTokens;
4283
+ const estimatedContextTokens = estimateTranscriptContextUsage(messages, tools, {
4284
+ messageCount: messageSummary.count,
4285
+ });
2729
4286
  const compactAt = Number(session?.compaction?.lastChangedAt || session?.compaction?.lastCompactAt || 0);
2730
4287
  const usageAt = Number(session?.lastContextTokensUpdatedAt || 0);
2731
4288
  const lastUsageStale = !!lastContextTokens && (
@@ -2735,31 +4292,58 @@ function parsedProviderModelVersion(id) {
2735
4292
  );
2736
4293
  const compactBoundaryTokens = Number(session?.compactBoundaryTokens || session?.compaction?.boundaryTokens || 0);
2737
4294
  const displayWindow = compactBoundaryTokens || effectiveWindow;
2738
- const usedTokens = lastUsageStale
2739
- ? estimatedContextTokens
2740
- : Math.max(estimatedContextTokens, lastContextTokens || 0);
4295
+ // The transcript estimate is the single source of truth for the displayed
4296
+ // context footprint. Provider-reported input_tokens (lastContextTokens)
4297
+ // swing non-monotonically and are not window-bounded on some providers
4298
+ // (e.g. OpenAI gpt-5.5 Responses API), so they are kept only as secondary
4299
+ // metadata (lastApiRequestTokens / usage.lastContextTokens) and never feed
4300
+ // the gauge numerator.
4301
+ const usedTokens = estimatedContextTokens;
2741
4302
  const freeTokens = displayWindow ? Math.max(0, displayWindow - usedTokens) : 0;
2742
4303
  const autoCompactTokenLimit = Number(session?.autoCompactTokenLimit || 0);
2743
- const defaultCompactTriggerTokens = compactBoundaryTokens ? Math.max(1, compactBoundaryTokens) : 0;
2744
- const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit <= compactBoundaryTokens
4304
+ // The runtime fires auto-compaction at the effective boundary by default.
4305
+ // Compute the same trigger here so /context shows what will actually fire
4306
+ // before any compaction telemetry (session.compaction.triggerTokens) is
4307
+ // recorded. Explicit buffer settings can still lower the trigger.
4308
+ // An explicit autoCompactTokenLimit (provider/catalog/seed supplied)
4309
+ // still takes precedence and is honored verbatim when it sits at or below
4310
+ // the boundary.
4311
+ const defaultCompactBufferTokens = contextDefaultBufferTokens(compactBoundaryTokens, session?.compaction || {});
4312
+ const defaultCompactTriggerTokens = compactBoundaryTokens
4313
+ ? Math.max(1, compactBoundaryTokens - defaultCompactBufferTokens)
4314
+ : 0;
4315
+ // Only an explicit auto-compact limit STRICTLY BELOW the boundary is a
4316
+ // real trigger; a persisted value == boundary is a legacy derived
4317
+ // full-window artifact and must fall through to the default trigger.
4318
+ // Likewise ignore persisted default-buffer telemetry so the displayed
4319
+ // trigger matches what the runtime now actually fires.
4320
+ const persistedTriggerTokens = Number(session?.compaction?.triggerTokens || 0);
4321
+ const legacyDefaultTriggerTelemetry = isLegacyDefaultContextBufferTelemetry(session?.compaction || {}, compactBoundaryTokens)
4322
+ && persistedTriggerTokens > 0
4323
+ && persistedTriggerTokens < compactBoundaryTokens;
4324
+ const usablePersistedTrigger = persistedTriggerTokens > 0
4325
+ && !legacyDefaultTriggerTelemetry
4326
+ && (!compactBoundaryTokens || persistedTriggerTokens < compactBoundaryTokens)
4327
+ ? persistedTriggerTokens
4328
+ : 0;
4329
+ const compactTriggerTokens = autoCompactTokenLimit && compactBoundaryTokens && autoCompactTokenLimit < compactBoundaryTokens
2745
4330
  ? autoCompactTokenLimit
2746
- : Number(session?.compaction?.triggerTokens || defaultCompactTriggerTokens || 0);
2747
- const compactBufferTokens = Number(session?.compaction?.bufferTokens || (compactBoundaryTokens && compactTriggerTokens ? Math.max(0, compactBoundaryTokens - compactTriggerTokens) : 0));
4331
+ : (usablePersistedTrigger || defaultCompactTriggerTokens || 0);
4332
+ const compactBufferTokens = Number(compactBoundaryTokens && compactTriggerTokens
4333
+ ? Math.max(0, compactBoundaryTokens - compactTriggerTokens)
4334
+ : defaultCompactBufferTokens);
2748
4335
  const value = {
2749
4336
  sessionId: session?.id || null,
2750
4337
  provider: route.provider,
2751
4338
  model: route.model,
2752
4339
  cwd: currentCwd,
2753
4340
  toolMode: mode,
2754
- bridgeMode,
2755
4341
  contextWindow: displayWindow || effectiveWindow || null,
2756
4342
  effectiveContextWindow: effectiveWindow || null,
2757
4343
  rawContextWindow: rawWindow || null,
2758
4344
  effectiveContextWindowPercent: session?.effectiveContextWindowPercent || null,
2759
4345
  usedTokens,
2760
- usedSource: lastContextTokens && !lastUsageStale && lastContextTokens >= estimatedContextTokens
2761
- ? 'last_api_request'
2762
- : 'estimated',
4346
+ usedSource: 'estimated',
2763
4347
  currentEstimatedTokens: estimatedContextTokens,
2764
4348
  lastApiRequestTokens: lastContextTokens || 0,
2765
4349
  lastApiRequestStale: lastUsageStale,
@@ -2776,6 +4360,7 @@ function parsedProviderModelVersion(id) {
2776
4360
  messages: messageSummary,
2777
4361
  request: {
2778
4362
  toolSchemaTokens,
4363
+ toolSchemaBreakdown,
2779
4364
  requestOverheadTokens,
2780
4365
  reserveTokens: requestReserveTokens,
2781
4366
  },
@@ -2796,26 +4381,64 @@ function parsedProviderModelVersion(id) {
2796
4381
  return value;
2797
4382
  },
2798
4383
  listProviders() {
2799
- return renderProviderStatus(cfgMod.loadConfig());
4384
+ return renderProviderStatus(displayConfig());
2800
4385
  },
2801
4386
  async getProviderSetup() {
2802
4387
  return await cachedProviderSetup();
2803
4388
  },
2804
4389
  async getUsageDashboard(options = {}) {
2805
- const nextConfig = cfgMod.loadConfig();
2806
- return await createUsageDashboard(nextConfig, {
2807
- ...(options || {}),
2808
- setup: await cachedProviderSetup(),
2809
- getProvider: (providerId) => reg.getProvider(providerId),
2810
- log: (message) => {
2811
- if (process.env.MIXDOG_USAGE_TRACE) {
2812
- try { process.stderr.write(`[usage] ${message}\n`); } catch {}
2813
- }
2814
- },
2815
- });
4390
+ const forceSetup = options?.force === true || options?.refresh === true;
4391
+ if (!forceSetup && usageDashboardCache.dashboard) {
4392
+ const cached = {
4393
+ ...usageDashboardCache.dashboard,
4394
+ refresh: false,
4395
+ checking: false,
4396
+ cached: true,
4397
+ cachedAt: usageDashboardCache.at,
4398
+ };
4399
+ if (typeof options?.onUpdate === 'function') {
4400
+ try { options.onUpdate(cached); } catch {}
4401
+ }
4402
+ return cached;
4403
+ }
4404
+ if (!forceSetup && usageDashboardPromise) return await usageDashboardPromise;
4405
+ const quickSetup = options?.quickSetup !== false;
4406
+ const getProvider = (providerId) => reg.getProvider(providerId);
4407
+ const log = (message) => {
4408
+ if (process.env.MIXDOG_USAGE_TRACE) {
4409
+ try { process.stderr.write(`[usage] ${message}\n`); } catch {}
4410
+ }
4411
+ };
4412
+ if (quickSetup && typeof options?.onUpdate === 'function') {
4413
+ const previewConfig = displayConfig();
4414
+ const previewSetup = await cachedProviderSetup({ force: false, quick: true });
4415
+ await createUsageDashboard(previewConfig, {
4416
+ ...(options || {}),
4417
+ preview: true,
4418
+ setup: previewSetup,
4419
+ getProvider,
4420
+ log,
4421
+ });
4422
+ }
4423
+ const buildDashboard = async () => {
4424
+ const dashboard = await createUsageDashboard(displayConfig(), {
4425
+ ...(options || {}),
4426
+ setup: await cachedProviderSetup({ force: forceSetup, quick: false }),
4427
+ getProvider,
4428
+ log,
4429
+ });
4430
+ usageDashboardCache = { dashboard, at: Date.now() };
4431
+ return dashboard;
4432
+ };
4433
+ if (forceSetup) return await buildDashboard();
4434
+ usageDashboardPromise = buildDashboard()
4435
+ .finally(() => {
4436
+ usageDashboardPromise = null;
4437
+ });
4438
+ return await usageDashboardPromise;
2816
4439
  },
2817
4440
  getOnboardingStatus() {
2818
- const nextConfig = cfgMod.loadConfig();
4441
+ const nextConfig = displayConfig();
2819
4442
  return {
2820
4443
  completed: nextConfig?.onboarding?.completed === true,
2821
4444
  version: nextConfig?.onboarding?.version || 0,
@@ -2826,17 +4449,118 @@ function parsedProviderModelVersion(id) {
2826
4449
  getAutoClear() {
2827
4450
  return normalizeAutoClearConfig(config.autoClear);
2828
4451
  },
4452
+ // --- User profile (/profile statusline command) ---------------------
4453
+ // getProfile returns the normalized { title, language } plus the resolved
4454
+ // language catalog entry and the full language list for the picker UI.
4455
+ getProfile() {
4456
+ const profile = cfgMod.normalizeProfileConfig(config.profile);
4457
+ return {
4458
+ ...profile,
4459
+ languageEntry: cfgMod.profileLanguageEntry(profile.language),
4460
+ languages: cfgMod.PROFILE_LANGUAGES,
4461
+ };
4462
+ },
4463
+ // setProfile patches title and/or language and persists. Unknown language
4464
+ // ids normalize back to 'system'. Prompt-side injection is wired separately
4465
+ // (composeSystemPrompt) — this only owns the stored value.
4466
+ setProfile(input = {}) {
4467
+ const current = cfgMod.normalizeProfileConfig(config.profile);
4468
+ const next = { ...current };
4469
+ if (hasOwn(input, 'title') || hasOwn(input, 'name')) {
4470
+ next.title = input.title ?? input.name ?? '';
4471
+ }
4472
+ if (hasOwn(input, 'language') || hasOwn(input, 'lang')) {
4473
+ next.language = input.language ?? input.lang ?? 'system';
4474
+ }
4475
+ const normalized = cfgMod.normalizeProfileConfig(next);
4476
+ saveConfigAndAdopt({ ...config, profile: normalized });
4477
+ return this.getProfile();
4478
+ },
4479
+ getCompactionSettings() {
4480
+ return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
4481
+ },
4482
+ setCompactionSettings(input = {}) {
4483
+ const current = normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
4484
+ const next = { ...current };
4485
+ if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
4486
+ if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
4487
+ if (hasOwn(input, 'type') || hasOwn(input, 'compactType') || hasOwn(input, 'compact_type')) {
4488
+ const requestedType = input.type ?? input.compactType ?? input.compact_type;
4489
+ const compactType = normalizeCompactTypeSetting(requestedType, current.compactType || current.type || 'semantic');
4490
+ if (compactType === 'recall-fasttrack' && !memoryEnabled()) {
4491
+ throw new Error('recall-fasttrack compact requires memory to be enabled');
4492
+ }
4493
+ next.type = compactType;
4494
+ next.compactType = compactType;
4495
+ }
4496
+ const nextConfig = { ...config };
4497
+ nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
4498
+ saveConfigAndAdopt(nextConfig);
4499
+ if (session) {
4500
+ session.compaction = {
4501
+ ...(session.compaction || {}),
4502
+ ...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
4503
+ };
4504
+ }
4505
+ invalidateContextStatusCache();
4506
+ return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
4507
+ },
4508
+ getMemorySettings() {
4509
+ return {
4510
+ enabled: memoryEnabled(),
4511
+ compactFastTrackAvailable: memoryEnabled(),
4512
+ };
4513
+ },
4514
+ async setMemoryEnabled(enabled) {
4515
+ const nextConfig = setModuleEnabledInConfig({ ...config }, 'memory', enabled !== false);
4516
+ if (enabled === false) {
4517
+ nextConfig.compaction = normalizeCompactionConfig(nextConfig.compaction, { memoryEnabled: false });
4518
+ }
4519
+ saveConfigAndAdopt(nextConfig);
4520
+ if (!memoryEnabled() && memoryModPromise) {
4521
+ await memoryModPromise.then((mod) => mod?.stop?.()).catch(() => {});
4522
+ memoryModPromise = null;
4523
+ }
4524
+ if (session && config.compaction) {
4525
+ session.compaction = {
4526
+ ...(session.compaction || {}),
4527
+ ...normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() }),
4528
+ };
4529
+ }
4530
+ invalidatePreSessionToolSurface();
4531
+ invalidateContextStatusCache();
4532
+ return this.getMemorySettings();
4533
+ },
4534
+ getChannelSettings(options = {}) {
4535
+ return {
4536
+ enabled: channelsEnabled(),
4537
+ ...(options?.includeStatus === false ? {} : { status: channels.status() }),
4538
+ };
4539
+ },
4540
+ async setChannelsEnabled(enabled) {
4541
+ const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
4542
+ saveConfigAndAdopt(nextConfig);
4543
+ if (!channelsEnabled()) {
4544
+ if (channelStartTimer) {
4545
+ clearTimeout(channelStartTimer);
4546
+ channelStartTimer = null;
4547
+ }
4548
+ await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
4549
+ } else {
4550
+ scheduleChannelStart(0);
4551
+ }
4552
+ invalidatePreSessionToolSurface();
4553
+ return this.getChannelSettings();
4554
+ },
2829
4555
  getSystemShell() {
2830
4556
  return normalizeSystemShellConfig(config.shell);
2831
4557
  },
2832
4558
  setSystemShell(input = {}) {
2833
4559
  const command = normalizeSystemShellCommand(typeof input === 'string' ? input : input?.command);
2834
- const nextConfig = cfgMod.loadConfig();
2835
- cfgMod.saveConfig({
2836
- ...nextConfig,
2837
- shell: command ? { ...(nextConfig.shell || {}), command } : {},
4560
+ saveConfigAndAdopt({
4561
+ ...config,
4562
+ shell: command ? { ...(config.shell || {}), command } : {},
2838
4563
  });
2839
- config = cfgMod.loadConfig();
2840
4564
  setConfiguredShell(command);
2841
4565
  return normalizeSystemShellConfig(config.shell);
2842
4566
  },
@@ -2855,9 +4579,7 @@ function parsedProviderModelVersion(id) {
2855
4579
  next.idleMs = idleMs;
2856
4580
  if (!hasOwn(input, 'enabled')) next.enabled = true;
2857
4581
  }
2858
- const nextConfig = cfgMod.loadConfig();
2859
- cfgMod.saveConfig({ ...nextConfig, autoClear: next });
2860
- config = cfgMod.loadConfig();
4582
+ saveConfigAndAdopt({ ...config, autoClear: next });
2861
4583
  return { ...normalizeAutoClearConfig(config.autoClear), label: formatDurationMs(normalizeAutoClearConfig(config.autoClear).idleMs) };
2862
4584
  },
2863
4585
  async completeOnboarding(payload = {}) {
@@ -2865,7 +4587,7 @@ function parsedProviderModelVersion(id) {
2865
4587
  const workflowInput = payload.workflowRoutes && typeof payload.workflowRoutes === 'object'
2866
4588
  ? payload.workflowRoutes
2867
4589
  : {};
2868
- const nextConfig = cfgMod.loadConfig();
4590
+ const nextConfig = { ...config };
2869
4591
  let presets = Array.isArray(nextConfig.presets) ? nextConfig.presets.slice() : [];
2870
4592
  const workflowRoutes = { ...(nextConfig.workflowRoutes || {}) };
2871
4593
 
@@ -2884,10 +4606,13 @@ function parsedProviderModelVersion(id) {
2884
4606
 
2885
4607
  nextConfig.presets = presets;
2886
4608
  nextConfig.workflowRoutes = workflowRoutes;
4609
+ // Maintenance slots store a direct {provider, model} route. Reuse the
4610
+ // onboarding workflow routes directly; only overwrite a slot when its
4611
+ // workflow route was actually provided, otherwise keep the existing slot.
2887
4612
  nextConfig.maintenance = {
2888
4613
  ...(nextConfig.maintenance || {}),
2889
- explore: workflowRoutes.explorer ? workflowPresetId('explorer') : (nextConfig.maintenance?.explore || 'haiku'),
2890
- memory: workflowRoutes.memory ? workflowPresetId('memory') : (nextConfig.maintenance?.memory || 'haiku'),
4614
+ ...(workflowRoutes.explorer ? { explore: normalizeWorkflowRoute(workflowRoutes.explorer) } : {}),
4615
+ ...(workflowRoutes.memory ? { memory: normalizeWorkflowRoute(workflowRoutes.memory) } : {}),
2891
4616
  };
2892
4617
  nextConfig.onboarding = {
2893
4618
  ...(nextConfig.onboarding || {}),
@@ -2896,8 +4621,7 @@ function parsedProviderModelVersion(id) {
2896
4621
  completedAt: new Date().toISOString(),
2897
4622
  };
2898
4623
 
2899
- cfgMod.saveConfig(nextConfig);
2900
- config = cfgMod.loadConfig();
4624
+ saveConfigAndAdopt(nextConfig);
2901
4625
  if (defaultRoute) {
2902
4626
  route = resolveRoute(config, { provider: defaultRoute.provider, model: defaultRoute.model, effort: defaultRoute.effort });
2903
4627
  if (session?.id) mgr.closeSession(session.id, 'cli-onboarding-complete');
@@ -2980,26 +4704,34 @@ function parsedProviderModelVersion(id) {
2980
4704
  const result = String(secret || '').trim()
2981
4705
  ? saveProviderApiKey(cfgMod, providerId, secret)
2982
4706
  : await loginOAuthProvider(cfgMod, providerId);
2983
- config = cfgMod.loadConfig();
4707
+ reloadFullConfig();
2984
4708
  invalidateProviderCaches();
2985
4709
  warmProviderModelCache();
2986
4710
  return result;
2987
4711
  },
2988
4712
  async loginOAuthProvider(providerId) {
2989
4713
  const result = await loginOAuthProvider(cfgMod, providerId);
2990
- config = cfgMod.loadConfig();
4714
+ reloadFullConfig();
2991
4715
  invalidateProviderCaches();
2992
4716
  warmProviderModelCache();
2993
4717
  return result;
2994
4718
  },
2995
4719
  async beginOAuthProviderLogin(providerId) {
2996
4720
  const result = await beginOAuthProviderLogin(cfgMod, providerId);
2997
- config = cfgMod.loadConfig();
4721
+ reloadFullConfig();
2998
4722
  return {
2999
4723
  ...result,
4724
+ waitForCallback: result.waitForCallback?.then((completed) => {
4725
+ reloadFullConfig();
4726
+ if (completed) {
4727
+ invalidateProviderCaches();
4728
+ warmProviderModelCache();
4729
+ }
4730
+ return completed;
4731
+ }),
3000
4732
  completeCode: async (code) => {
3001
4733
  const completed = await result.completeCode(code);
3002
- config = cfgMod.loadConfig();
4734
+ reloadFullConfig();
3003
4735
  invalidateProviderCaches();
3004
4736
  warmProviderModelCache();
3005
4737
  return completed;
@@ -3008,42 +4740,97 @@ function parsedProviderModelVersion(id) {
3008
4740
  },
3009
4741
  saveProviderApiKey(providerId, secret) {
3010
4742
  const result = saveProviderApiKey(cfgMod, providerId, secret);
3011
- config = cfgMod.loadConfig();
4743
+ reloadFullConfig();
3012
4744
  invalidateProviderCaches();
3013
4745
  warmProviderModelCache();
3014
4746
  return result;
3015
4747
  },
3016
4748
  saveOpenAIUsageSessionKey(secret) {
3017
4749
  const result = saveOpenAIUsageSessionKey(cfgMod, secret);
3018
- config = cfgMod.loadConfig();
4750
+ reloadFullConfig();
3019
4751
  invalidateProviderCaches();
3020
4752
  return result;
3021
4753
  },
3022
4754
  saveOpenCodeGoUsageAuth(opts) {
3023
4755
  const result = saveOpenCodeGoUsageAuth(cfgMod, opts);
3024
- config = cfgMod.loadConfig();
4756
+ reloadFullConfig();
3025
4757
  invalidateProviderCaches();
3026
4758
  return result;
3027
4759
  },
3028
4760
  setLocalProvider(providerId, opts) {
3029
4761
  const result = setLocalProvider(cfgMod, providerId, opts);
3030
- config = cfgMod.loadConfig();
4762
+ reloadFullConfig();
3031
4763
  invalidateProviderCaches();
3032
4764
  warmProviderModelCache();
3033
4765
  return result;
3034
4766
  },
3035
4767
  forgetProviderAuth(providerId) {
3036
- const result = forgetProviderAuth(providerId);
3037
- config = cfgMod.loadConfig();
4768
+ const result = forgetProviderAuth(cfgMod, providerId);
4769
+ reloadFullConfig();
3038
4770
  invalidateProviderCaches();
3039
4771
  warmProviderModelCache();
3040
4772
  return result;
3041
4773
  },
3042
4774
  listPresets() {
3043
- return cfgMod.listPresets(cfgMod.loadConfig());
4775
+ return cfgMod.listPresets(displayConfig());
3044
4776
  },
3045
4777
  async listProviderModels(options = {}) {
3046
- return await collectProviderModels({ force: options.force === true || options.refresh === true });
4778
+ return await collectProviderModels({
4779
+ force: options.force === true || options.refresh === true,
4780
+ quick: options.quick === true,
4781
+ });
4782
+ },
4783
+ getSearchRoute() {
4784
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
4785
+ return searchRoute;
4786
+ },
4787
+ async listSearchModels(options = {}) {
4788
+ return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
4789
+ },
4790
+ async setSearchRoute(next) {
4791
+ let selectedRoute = normalizeSearchRouteConfig(next);
4792
+ if (!selectedRoute && next?.model && searchRoute?.provider) {
4793
+ selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
4794
+ }
4795
+ if (!selectedRoute) throw new Error('search route requires provider and model');
4796
+ if (isDefaultSearchRouteConfig(selectedRoute)) {
4797
+ ensureFullConfig();
4798
+ const routeToSave = normalizeSearchRouteConfig({
4799
+ provider: SEARCH_DEFAULT_PROVIDER,
4800
+ model: SEARCH_DEFAULT_MODEL,
4801
+ ...(selectedRoute.toolType ? { toolType: selectedRoute.toolType } : {}),
4802
+ });
4803
+ const nextConfig = { ...config };
4804
+ nextConfig.searchRoute = routeToSave;
4805
+ saveConfigAndAdopt(nextConfig);
4806
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute);
4807
+ invalidateProviderCaches();
4808
+ return searchRoute;
4809
+ }
4810
+ if (!isSearchCapableProvider(selectedRoute.provider)) {
4811
+ throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
4812
+ }
4813
+ ensureFullConfig();
4814
+ await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
4815
+ const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
4816
+ if (!searchCapableFor(selectedRoute.provider, modelMeta)) {
4817
+ throw new Error(`model "${selectedRoute.model}" is not marked as web-search capable`);
4818
+ }
4819
+ const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
4820
+ const effort = coerceEffortFor(selectedRoute.provider, modelMeta, selectedRoute.effort);
4821
+ selectedRoute = {
4822
+ ...selectedRoute,
4823
+ ...(effort ? { effort } : {}),
4824
+ fast: fastCapable ? selectedRoute.fast === true : false,
4825
+ };
4826
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
4827
+ const routeToSave = normalizeSearchRouteConfig(selectedRoute);
4828
+ const nextConfig = { ...config };
4829
+ nextConfig.searchRoute = routeToSave;
4830
+ saveConfigAndAdopt(nextConfig);
4831
+ searchRoute = normalizeSearchRouteConfig(config.searchRoute);
4832
+ invalidateProviderCaches();
4833
+ return searchRoute;
3047
4834
  },
3048
4835
  listAgents() {
3049
4836
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
@@ -3055,7 +4842,7 @@ function parsedProviderModelVersion(id) {
3055
4842
  }));
3056
4843
  },
3057
4844
  listWorkflows() {
3058
- const currentConfig = cfgMod.loadConfig();
4845
+ const currentConfig = displayConfig();
3059
4846
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3060
4847
  const active = activeWorkflowId(currentConfig);
3061
4848
  return listWorkflowPacks(dataDir).map((workflow) => ({
@@ -3068,16 +4855,13 @@ function parsedProviderModelVersion(id) {
3068
4855
  }));
3069
4856
  },
3070
4857
  getOutputStyle() {
3071
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3072
- return outputStyleStatus(dataDir);
4858
+ return getOutputStyleStatusCached();
3073
4859
  },
3074
4860
  listOutputStyles() {
3075
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3076
- return outputStyleStatus(dataDir);
4861
+ return getOutputStyleStatusCached();
3077
4862
  },
3078
4863
  async setOutputStyle(value) {
3079
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3080
- const before = outputStyleStatus(dataDir);
4864
+ const before = getOutputStyleStatusCached({ fresh: true });
3081
4865
  const selected = findOutputStyle(value, before.styles);
3082
4866
  if (!selected) {
3083
4867
  const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
@@ -3093,6 +4877,7 @@ function parsedProviderModelVersion(id) {
3093
4877
  }
3094
4878
  return next;
3095
4879
  });
4880
+ invalidateOutputStyleStatusCache();
3096
4881
  const hasConversation = sessionHasConversationMessages(session);
3097
4882
  let appliedToCurrentSession = !hasConversation;
3098
4883
  if (session?.id && !hasConversation) {
@@ -3101,23 +4886,17 @@ function parsedProviderModelVersion(id) {
3101
4886
  await recreateCurrentSessionIfReady();
3102
4887
  }
3103
4888
  invalidateContextStatusCache();
3104
- return { ...outputStyleStatus(dataDir), appliedToCurrentSession };
4889
+ return { ...getOutputStyleStatusCached({ fresh: true }), appliedToCurrentSession };
3105
4890
  },
3106
4891
  async setWorkflow(workflowId) {
3107
4892
  const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
3108
4893
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
3109
4894
  const pack = loadWorkflowPack(dataDir, id);
3110
4895
  if (!pack || pack.id !== id) throw new Error(`workflow "${workflowId}" not found`);
3111
- const nextConfig = cfgMod.loadConfig();
4896
+ const nextConfig = { ...config };
3112
4897
  nextConfig.workflow = { ...(nextConfig.workflow || {}), active: id };
3113
- cfgMod.saveConfig(nextConfig);
3114
- config = cfgMod.loadConfig();
3115
- if (session?.id) {
3116
- mgr.closeSession(session.id, 'cli-workflow-switch');
3117
- session = null;
3118
- }
3119
- await recreateCurrentSessionIfReady();
3120
- return { id: pack.id, name: pack.name, description: pack.description, source: pack.source };
4898
+ saveConfigAndAdopt(nextConfig);
4899
+ return workflowSummary(pack);
3121
4900
  },
3122
4901
  async setAgentRoute(agentId, next) {
3123
4902
  const id = normalizeAgentId(agentId);
@@ -3127,12 +4906,12 @@ function parsedProviderModelVersion(id) {
3127
4906
  const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
3128
4907
  const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
3129
4908
  selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
3130
- config = saveModelSettings(cfgMod, selectedRoute, { fastCapable });
4909
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
3131
4910
 
3132
4911
  const routeToSave = normalizeWorkflowRoute(selectedRoute);
3133
4912
  if (!routeToSave) throw new Error('agent route requires provider and model');
3134
4913
  const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
3135
- const nextConfig = cfgMod.loadConfig();
4914
+ const nextConfig = { ...config };
3136
4915
  nextConfig.agents = {
3137
4916
  ...(nextConfig.agents || {}),
3138
4917
  [id]: routeToSave,
@@ -3144,24 +4923,48 @@ function parsedProviderModelVersion(id) {
3144
4923
  [agent.workflowSlot]: routeToSave,
3145
4924
  };
3146
4925
  nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, agent.workflowSlot, routeToSave);
4926
+ // Maintenance slots store a direct {provider, model} route now, so
4927
+ // mirror the agent route straight in instead of a preset-id string.
3147
4928
  nextConfig.maintenance = {
3148
4929
  ...(nextConfig.maintenance || {}),
3149
- ...(id === 'explore' ? { explore: workflowPresetId('explorer') } : {}),
3150
- ...(id === 'maintainer' ? { memory: workflowPresetId('memory') } : {}),
4930
+ ...(id === 'explore' ? { explore: routeToSave } : {}),
4931
+ ...(id === 'maintainer' ? { memory: routeToSave } : {}),
3151
4932
  };
3152
4933
  }
3153
- cfgMod.saveConfig(nextConfig);
3154
- config = cfgMod.loadConfig();
4934
+ saveConfigAndAdopt(nextConfig);
3155
4935
  return routeToSave;
3156
4936
  },
3157
4937
  async ask(prompt, options = {}) {
3158
4938
  activeTurnCount += 1;
4939
+ // Lazy code-graph prewarm: kick off the build ONCE, on the first real
4940
+ // turn, so a likely code lookup hits a warm cache — without paying the
4941
+ // post-first-frame prewarm freeze on idle startup. The schedule is async
4942
+ // + unref'd (worker thread), so it never blocks this turn.
4943
+ if (codeGraphPrewarmLazy && !codeGraphFirstTurnPrewarmDone) {
4944
+ codeGraphFirstTurnPrewarmDone = true;
4945
+ scheduleCodeGraphPrewarm(0, 'first-turn');
4946
+ }
3159
4947
  const startedAt = Date.now();
3160
4948
  try {
3161
4949
  await refreshSessionForCwdIfNeeded('cwd-change');
3162
4950
  if (!session?.id) await createCurrentSession('turn');
3163
4951
  hooks.emit('turn:start', { sessionId: session.id, prompt, cwd: currentCwd });
3164
- const turnContext = [options.context || ''].map((part) => String(part || '').trim()).filter(Boolean).join('\n\n');
4952
+ // UserPromptSubmit: bridge to the standard hook bus. A hook FAILURE
4953
+ // must not block the turn, but a genuine blocked===true MUST throw.
4954
+ let promptDispatch = null;
4955
+ try {
4956
+ promptDispatch = await hooks.dispatch('UserPromptSubmit', hookCommonPayload({ session_id: session.id, prompt }));
4957
+ } catch { /* hook failure never blocks the turn */ }
4958
+ if (promptDispatch?.blocked === true) {
4959
+ throw new Error(`prompt blocked by hook: ${promptDispatch.reason || ''}`);
4960
+ }
4961
+ const hookContext = Array.isArray(promptDispatch?.additionalContext)
4962
+ ? promptDispatch.additionalContext.join('\n\n')
4963
+ : String(promptDispatch?.additionalContext || '');
4964
+ const turnContext = [options.context || '', hookContext]
4965
+ .map((part) => String(part || '').trim())
4966
+ .filter(Boolean)
4967
+ .join('\n\n');
3165
4968
  const result = await mgr.askSession(
3166
4969
  session.id,
3167
4970
  prompt,
@@ -3184,16 +4987,25 @@ function parsedProviderModelVersion(id) {
3184
4987
  {
3185
4988
  onTextDelta: options.onTextDelta,
3186
4989
  onReasoningDelta: options.onReasoningDelta,
4990
+ onAssistantText: options.onAssistantText,
3187
4991
  onUsageDelta: options.onUsageDelta,
3188
4992
  onToolResult: options.onToolResult,
4993
+ onToolApproval: options.onToolApproval,
4994
+ onCompactEvent: options.onCompactEvent,
3189
4995
  onStageChange: options.onStageChange,
3190
4996
  onStreamDelta: options.onStreamDelta,
3191
4997
  drainSteering: options.drainSteering,
3192
4998
  onSteerMessage: options.onSteerMessage,
4999
+ notifyFn: notifyFnForSession(session.id),
3193
5000
  },
3194
5001
  );
3195
5002
  session = mgr.getSession(session.id) || session;
3196
5003
  hooks.emit('turn:end', { sessionId: session.id, elapsedMs: Date.now() - startedAt });
5004
+ // Stop: bridge to the standard hook bus. Best-effort; ignore result,
5005
+ // never throw.
5006
+ try {
5007
+ await hooks.dispatch('Stop', hookCommonPayload({ session_id: session.id }));
5008
+ } catch { /* best-effort: Stop hook must never break the turn */ }
3197
5009
  return { result, session };
3198
5010
  } catch (error) {
3199
5011
  hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
@@ -3202,27 +5014,31 @@ function parsedProviderModelVersion(id) {
3202
5014
  activeTurnCount = Math.max(0, activeTurnCount - 1);
3203
5015
  if (!firstTurnCompleted) {
3204
5016
  firstTurnCompleted = true;
5017
+ scheduleProviderWarmup();
3205
5018
  scheduleProviderModelWarmup();
3206
5019
  }
3207
5020
  }
3208
5021
  },
3209
5022
  async clear(options = {}) {
3210
5023
  if (!session?.id) return false;
3211
- const cleared = await mgr.clearSessionMessages(session.id);
5024
+ const cleared = await mgr.clearSessionMessages(session.id, options);
3212
5025
  if (!cleared) return false;
3213
5026
  session = typeof cleared === 'object' ? cleared : (mgr.getSession(session.id) || session);
3214
- if (options.recoverBridge === true) {
3215
- try { bridge.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
5027
+ if (options.recoverAgent === true) {
5028
+ try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
3216
5029
  }
3217
5030
  invalidateContextStatusCache();
3218
5031
  return true;
3219
5032
  },
3220
5033
  async compact(options = {}) {
3221
5034
  if (!session?.id) return null;
5035
+ if (activeTurnCount > 0) {
5036
+ return { changed: false, reason: 'compact skipped: turn in progress' };
5037
+ }
3222
5038
  const result = await mgr.compactSessionMessages(session.id);
3223
5039
  session = mgr.getSession(session.id) || session;
3224
- if (options.recoverBridge === true) {
3225
- try { bridge.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
5040
+ if (options.recoverAgent === true) {
5041
+ try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
3226
5042
  }
3227
5043
  invalidateContextStatusCache();
3228
5044
  return result;
@@ -3234,25 +5050,15 @@ function parsedProviderModelVersion(id) {
3234
5050
  await recreateCurrentSessionIfReady();
3235
5051
  return mode;
3236
5052
  },
3237
- setBridgeMode(nextMode) {
3238
- const applied = bridge.setDefaultMode(nextMode);
3239
- try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
3240
- return applied;
3241
- },
3242
- toggleBridgeMode() {
3243
- const applied = bridge.toggleDefaultMode();
3244
- try { sharedCfgMod.updateSection('agent', (s) => ({ ...(s || {}), bridgeMode: applied })); } catch {}
3245
- return applied;
3246
- },
3247
- bridgeStatus() {
3248
- return bridgeStatusState();
5053
+ agentStatus() {
5054
+ return agentStatusState();
3249
5055
  },
3250
5056
  get clientHostPid() {
3251
5057
  return session?.clientHostPid || process.pid;
3252
5058
  },
3253
- bridgeControl(args = {}) {
5059
+ agentControl(args = {}) {
3254
5060
  const callerSessionId = session?.id || null;
3255
- return bridge.execute(args, {
5061
+ return agentTool.execute(args, {
3256
5062
  callerCwd: currentCwd,
3257
5063
  invocationSource: 'user-command',
3258
5064
  callerSessionId,
@@ -3282,6 +5088,7 @@ function parsedProviderModelVersion(id) {
3282
5088
  activeCount: rows.filter((row) => row.active).length,
3283
5089
  tools,
3284
5090
  activeTools: sortedNamesByMeasuredUsage(activeNames),
5091
+ discoveredTools: sortedNamesByMeasuredUsage(surface?.deferredDiscoveredTools || []),
3285
5092
  };
3286
5093
  },
3287
5094
  selectTools(names) {
@@ -3297,7 +5104,7 @@ function parsedProviderModelVersion(id) {
3297
5104
  return mcpStatus();
3298
5105
  },
3299
5106
  async reconnectMcp() {
3300
- config = cfgMod.loadConfig();
5107
+ reloadFullConfig();
3301
5108
  const status = await connectConfiguredMcp({ reset: true });
3302
5109
  invalidatePreSessionToolSurface();
3303
5110
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-reconnect');
@@ -3306,13 +5113,12 @@ function parsedProviderModelVersion(id) {
3306
5113
  },
3307
5114
  async addMcpServer(input = {}) {
3308
5115
  const { name, config: serverConfig } = normalizeMcpServerInput(input);
3309
- const nextConfig = cfgMod.loadConfig();
5116
+ const nextConfig = { ...config };
3310
5117
  nextConfig.mcpServers = {
3311
5118
  ...(nextConfig.mcpServers || {}),
3312
5119
  [name]: serverConfig,
3313
5120
  };
3314
- cfgMod.saveConfig(nextConfig);
3315
- config = cfgMod.loadConfig();
5121
+ saveConfigAndAdopt(nextConfig);
3316
5122
  const status = await connectConfiguredMcp({ reset: true });
3317
5123
  invalidatePreSessionToolSurface();
3318
5124
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-add');
@@ -3322,7 +5128,7 @@ function parsedProviderModelVersion(id) {
3322
5128
  async removeMcpServer(name) {
3323
5129
  const serverName = clean(name);
3324
5130
  if (!serverName) throw new Error('MCP server name is required');
3325
- const nextConfig = cfgMod.loadConfig();
5131
+ const nextConfig = { ...config };
3326
5132
  const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
3327
5133
  ? { ...nextConfig.mcpServers }
3328
5134
  : {};
@@ -3330,8 +5136,7 @@ function parsedProviderModelVersion(id) {
3330
5136
  throw new Error(`MCP server not configured: ${serverName}`);
3331
5137
  }
3332
5138
  delete current[serverName];
3333
- cfgMod.saveConfig({ ...nextConfig, mcpServers: current });
3334
- config = cfgMod.loadConfig();
5139
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
3335
5140
  const status = await connectConfiguredMcp({ reset: true });
3336
5141
  invalidatePreSessionToolSurface();
3337
5142
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-remove');
@@ -3341,7 +5146,7 @@ function parsedProviderModelVersion(id) {
3341
5146
  async setMcpServerEnabled(name, enabled) {
3342
5147
  const serverName = clean(name);
3343
5148
  if (!serverName) throw new Error('MCP server name is required');
3344
- const nextConfig = cfgMod.loadConfig();
5149
+ const nextConfig = { ...config };
3345
5150
  const current = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
3346
5151
  ? { ...nextConfig.mcpServers }
3347
5152
  : {};
@@ -3349,8 +5154,7 @@ function parsedProviderModelVersion(id) {
3349
5154
  throw new Error(`MCP server not configured: ${serverName}`);
3350
5155
  }
3351
5156
  current[serverName] = { ...(current[serverName] || {}), enabled: enabled !== false };
3352
- cfgMod.saveConfig({ ...nextConfig, mcpServers: current });
3353
- config = cfgMod.loadConfig();
5157
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
3354
5158
  const status = await connectConfiguredMcp({ reset: true });
3355
5159
  invalidatePreSessionToolSurface();
3356
5160
  if (session?.id) mgr.closeSession(session.id, 'cli-mcp-toggle');
@@ -3401,13 +5205,12 @@ function parsedProviderModelVersion(id) {
3401
5205
  const key = clean(plugin.id || plugin.name || plugin);
3402
5206
  const dataDir = cfgMod.getPluginData?.();
3403
5207
  const removed = registryRemovePlugin(key, { dataDir });
3404
- const nextConfig = cfgMod.loadConfig();
5208
+ const nextConfig = { ...config };
3405
5209
  const serverName = pluginMcpServerName(plugin);
3406
5210
  if (nextConfig.mcpServers && Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
3407
5211
  const current = { ...nextConfig.mcpServers };
3408
5212
  delete current[serverName];
3409
- cfgMod.saveConfig({ ...nextConfig, mcpServers: current });
3410
- config = cfgMod.loadConfig();
5213
+ saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
3411
5214
  await connectConfiguredMcp({ reset: true });
3412
5215
  invalidatePreSessionToolSurface();
3413
5216
  }
@@ -3422,7 +5225,7 @@ function parsedProviderModelVersion(id) {
3422
5225
  const scriptPath = join(root, script);
3423
5226
  if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
3424
5227
  const serverName = pluginMcpServerName(plugin);
3425
- const nextConfig = cfgMod.loadConfig();
5228
+ const nextConfig = { ...config };
3426
5229
  nextConfig.mcpServers = {
3427
5230
  ...(nextConfig.mcpServers || {}),
3428
5231
  [serverName]: {
@@ -3435,8 +5238,7 @@ function parsedProviderModelVersion(id) {
3435
5238
  },
3436
5239
  },
3437
5240
  };
3438
- cfgMod.saveConfig(nextConfig);
3439
- config = cfgMod.loadConfig();
5241
+ saveConfigAndAdopt(nextConfig);
3440
5242
  const status = await connectConfiguredMcp({ reset: true });
3441
5243
  invalidatePreSessionToolSurface();
3442
5244
  if (session?.id) mgr.closeSession(session.id, 'cli-plugin-mcp-enable');
@@ -3504,8 +5306,11 @@ function parsedProviderModelVersion(id) {
3504
5306
  }
3505
5307
  return result;
3506
5308
  },
3507
- async setRoute(next) {
5309
+ async setRoute(next, options = {}) {
5310
+ const applyToCurrentSession = options?.applyToCurrentSession !== false;
3508
5311
  const requested = { ...(next || {}) };
5312
+ validateRequestedModelSelector(config, requested);
5313
+ const providerExplicitlyRequested = clean(next?.provider) !== '';
3509
5314
  if (requested.effort === undefined && !requested.provider && !requested.model && hasOwn(route, 'effort')) {
3510
5315
  requested.effort = route.effort;
3511
5316
  }
@@ -3515,14 +5320,34 @@ function parsedProviderModelVersion(id) {
3515
5320
  let selectedRoute = resolveRoute(config, requested);
3516
5321
  await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
3517
5322
  const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
5323
+ // Guard (A안 / strict reject): a free-text string must never become the
5324
+ // lead model. When the caller did not pin a provider, the route did not
5325
+ // match a preset, and the model is unknown to both the live provider
5326
+ // catalog (modelMeta is still the {id,provider} placeholder) and the
5327
+ // offline catalog, refuse BEFORE any config write so a partial save
5328
+ // (saveModelSettings/adoptConfig/persistLeadRoute) can never land.
5329
+ if (!providerExplicitlyRequested
5330
+ && !selectedRoute.preset
5331
+ && !modelMetaLooksResolved(modelMeta)
5332
+ && !getModelMetadataSync(selectedRoute.model, selectedRoute.provider)) {
5333
+ throw new Error(`unknown model: ${selectedRoute.provider}/${selectedRoute.model}`);
5334
+ }
3518
5335
  const fastCapable = fastCapableFor(selectedRoute.provider, modelMeta);
3519
5336
  selectedRoute = { ...selectedRoute, fast: fastCapable ? selectedRoute.fast === true : false };
3520
- config = saveModelSettings(cfgMod, selectedRoute, { fastCapable });
5337
+ adoptConfig(saveModelSettings(cfgMod, selectedRoute, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
5338
+ if (!applyToCurrentSession) {
5339
+ persistLeadRoute(selectedRoute);
5340
+ refreshStatuslineUsageSnapshot(route);
5341
+ scheduleStatuslineUsageRefresh();
5342
+ return selectedRoute;
5343
+ }
3521
5344
  const leadRoute = persistLeadRoute(selectedRoute);
3522
5345
  route = resolveRoute(config, leadRoute
3523
5346
  ? { model: workflowPresetId('lead') }
3524
5347
  : selectedRoute);
3525
5348
  await refreshRouteEffort(modelMeta);
5349
+ refreshStatuslineUsageSnapshot(route);
5350
+ scheduleStatuslineUsageRefresh();
3526
5351
  if (session) {
3527
5352
  const updated = mgr.updateSessionRoute?.(session.id, {
3528
5353
  provider: route.provider,
@@ -3537,7 +5362,7 @@ function parsedProviderModelVersion(id) {
3537
5362
  session.fast = route.fast === true;
3538
5363
  session.effort = route.effectiveEffort || null;
3539
5364
  }
3540
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5365
+ writeStatuslineRoute(statusRoutes, session, route);
3541
5366
  invalidateContextStatusCache();
3542
5367
  }
3543
5368
  return route;
@@ -3551,14 +5376,14 @@ function parsedProviderModelVersion(id) {
3551
5376
  throw new Error(`fast mode is not available for ${route.provider}/${route.model}`);
3552
5377
  }
3553
5378
  route = resolveRoute(config, { provider: route.provider, model: route.model, effort: route.effort, fast: fastCapable ? enabled : false });
3554
- config = saveModelSettings(cfgMod, route, { fastCapable });
5379
+ adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
3555
5380
  const leadRoute = persistLeadRoute(route);
3556
5381
  if (leadRoute) route = resolveRoute(config, { model: workflowPresetId('lead') });
3557
5382
  await refreshRouteEffort(modelMeta);
3558
5383
  if (session) {
3559
5384
  session.fast = route.fast === true;
3560
5385
  session.effort = route.effectiveEffort || null;
3561
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5386
+ writeStatuslineRoute(statusRoutes, session, route);
3562
5387
  invalidateContextStatusCache();
3563
5388
  }
3564
5389
  return route.fast === true;
@@ -3571,15 +5396,17 @@ function parsedProviderModelVersion(id) {
3571
5396
  async setEffort(value) {
3572
5397
  const normalized = normalizeEffortInput(value);
3573
5398
  route = { ...route, effort: normalized };
3574
- config = saveModelSettings(cfgMod, route, { fastCapable: route.fastCapable !== false });
5399
+ const modelMeta = await lookupModelMeta(route.provider, route.model);
5400
+ const fastCapable = fastCapableFor(route.provider, modelMeta);
5401
+ adoptConfig(saveModelSettings(cfgMod, route, { fastCapable, baseConfig: config }), { hasSecrets: configHasSecrets });
3575
5402
  const leadRoute = persistLeadRoute(route);
3576
5403
  if (leadRoute) {
3577
5404
  route = resolveRoute(config, { model: workflowPresetId('lead') });
3578
5405
  }
3579
- await refreshRouteEffort();
5406
+ await refreshRouteEffort(modelMeta);
3580
5407
  if (session) {
3581
5408
  session.effort = route.effectiveEffort || null;
3582
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5409
+ writeStatuslineRoute(statusRoutes, session, route);
3583
5410
  invalidateContextStatusCache();
3584
5411
  }
3585
5412
  return route;
@@ -3587,10 +5414,18 @@ function parsedProviderModelVersion(id) {
3587
5414
  async close(reason = 'cli-exit', options = {}) {
3588
5415
  const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
3589
5416
  closeRequested = true;
5417
+ // Persist any change that is still sitting in the debounce window so a
5418
+ // toggle made right before exit is not lost. Synchronous + best-effort:
5419
+ // teardown must continue even if the final write fails.
5420
+ try { flushConfigSave(); } catch {}
3590
5421
  if (channelStartTimer) {
3591
5422
  clearTimeout(channelStartTimer);
3592
5423
  channelStartTimer = null;
3593
5424
  }
5425
+ if (providerSetupWarmupTimer) {
5426
+ clearTimeout(providerSetupWarmupTimer);
5427
+ providerSetupWarmupTimer = null;
5428
+ }
3594
5429
  if (providerWarmupTimer) {
3595
5430
  clearTimeout(providerWarmupTimer);
3596
5431
  providerWarmupTimer = null;
@@ -3599,9 +5434,21 @@ function parsedProviderModelVersion(id) {
3599
5434
  clearTimeout(providerModelWarmupTimer);
3600
5435
  providerModelWarmupTimer = null;
3601
5436
  }
5437
+ if (codeGraphPrewarmTimer) {
5438
+ clearTimeout(codeGraphPrewarmTimer);
5439
+ codeGraphPrewarmTimer = null;
5440
+ }
5441
+ if (statuslineUsageWarmupTimer) {
5442
+ clearTimeout(statuslineUsageWarmupTimer);
5443
+ statuslineUsageWarmupTimer = null;
5444
+ }
5445
+ if (statuslineUsageRefreshTimer) {
5446
+ clearTimeout(statuslineUsageRefreshTimer);
5447
+ statuslineUsageRefreshTimer = null;
5448
+ }
3602
5449
  try { cancelBackgroundTasks({ reason, notify: false }); } catch {}
3603
5450
  const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
3604
- try { bridge.closeAll(reason); } catch {}
5451
+ try { agentTool.closeAll(reason); } catch {}
3605
5452
  let mcpStop = null;
3606
5453
  try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
3607
5454
  const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
@@ -3610,14 +5457,35 @@ function parsedProviderModelVersion(id) {
3610
5457
  .catch(() => {})
3611
5458
  : null;
3612
5459
  const patchStop = closePatchRuntimeIfLoaded(detach ? { waitForExit: false } : undefined);
5460
+ const memoryStop = memoryModPromise
5461
+ ? memoryModPromise
5462
+ .then((mod) => (typeof mod?.stop === 'function' ? mod.stop() : null))
5463
+ .catch(() => {})
5464
+ .finally(() => {
5465
+ memoryModPromise = null;
5466
+ })
5467
+ : null;
3613
5468
  let ok = false;
3614
5469
  if (session?.id) {
3615
5470
  statusRoutes?.clearGatewaySessionRoute?.(session.id);
3616
5471
  ok = mgr.closeSession(session.id, reason);
3617
5472
  session = null;
3618
5473
  }
5474
+ const shellJobsStop = globalThis.__mixdogShellJobsRuntimeLoaded === true
5475
+ ? import('./runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs')
5476
+ .then((mod) => mod?.shutdownShellJobs?.(reason, { sync: !detach }))
5477
+ .catch(() => {})
5478
+ : null;
5479
+ const bashSessionsStop = globalThis.__mixdogBashSessionRuntimeLoaded === true
5480
+ ? import('./runtime/agent/orchestrator/tools/bash-session.mjs')
5481
+ .then((mod) => mod?.shutdownBashSessions?.(reason))
5482
+ .catch(() => {})
5483
+ : null;
3619
5484
  if (detach) {
3620
5485
  try { await withTeardownDeadline(channelStop, 300, false); } catch {}
5486
+ try { await withTeardownDeadline(shellJobsStop, 300, false); } catch {}
5487
+ try { await withTeardownDeadline(bashSessionsStop, 300, false); } catch {}
5488
+ try { await withTeardownDeadline(memoryStop, 1500, false); } catch {}
3621
5489
  for (const stop of [mcpStop, openaiWsStop, patchStop]) {
3622
5490
  Promise.resolve(stop).catch(() => {});
3623
5491
  }
@@ -3628,6 +5496,9 @@ function parsedProviderModelVersion(id) {
3628
5496
  withTeardownDeadline(mcpStop, 1500, false),
3629
5497
  withTeardownDeadline(openaiWsStop, 1500, false),
3630
5498
  withTeardownDeadline(patchStop, 1500, false),
5499
+ withTeardownDeadline(memoryStop, 5500, false),
5500
+ withTeardownDeadline(shellJobsStop, 1500, false),
5501
+ withTeardownDeadline(bashSessionsStop, 1500, false),
3631
5502
  ]);
3632
5503
  return ok;
3633
5504
  },
@@ -3645,23 +5516,27 @@ function parsedProviderModelVersion(id) {
3645
5516
  const leadish = role === 'lead'
3646
5517
  || sourceType === 'lead'
3647
5518
  || (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
3648
- || (!sourceType && !sourceName && owner !== 'bridge');
5519
+ || (!sourceType && !sourceName && !isAgentOwner(owner));
3649
5520
  if (!leadish) return null;
3650
- const msgs = s.messages || [];
3651
- const userPreviews = msgs
3652
- .filter(m => m && m.role === 'user')
3653
- .map(m => cleanSessionPreview(sessionMessageText(m.content)))
3654
- .filter(text => !isSessionPreviewNoise(text));
3655
- const preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
5521
+ let preview = cleanSessionPreview(s.preview || '');
5522
+ let messageCount = Math.max(0, Number(s.messageCount) || 0);
5523
+ if (!preview && Array.isArray(s.messages)) {
5524
+ const msgs = s.messages || [];
5525
+ const userPreviews = msgs
5526
+ .filter(m => m && m.role === 'user')
5527
+ .map(m => cleanSessionPreview(sessionMessageText(m.content)))
5528
+ .filter(text => !isSessionPreviewNoise(text));
5529
+ preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
5530
+ messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
5531
+ }
3656
5532
  if (!preview) return null;
3657
- const userAsst = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant'));
3658
5533
  return {
3659
5534
  id: s.id,
3660
5535
  updatedAt: s.updatedAt,
3661
5536
  cwd: s.cwd || '',
3662
5537
  model: s.model,
3663
5538
  provider: s.provider,
3664
- messageCount: userAsst.length,
5539
+ messageCount,
3665
5540
  preview,
3666
5541
  };
3667
5542
  }).filter(Boolean);
@@ -3687,11 +5562,11 @@ function parsedProviderModelVersion(id) {
3687
5562
  await refreshRouteEffort();
3688
5563
  session.effort = route.effectiveEffort || null;
3689
5564
  session.cwd = currentCwd;
3690
- applyDeferredToolSurface(session, mode, standaloneTools);
5565
+ applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
3691
5566
  invalidatePreSessionToolSurface();
3692
5567
  invalidateContextStatusCache();
3693
5568
  sessionNeedsCwdRefresh = false;
3694
- statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route));
5569
+ writeStatuslineRoute(statusRoutes, session, route);
3695
5570
  return {
3696
5571
  id: resumed.id,
3697
5572
  messages: resumed.messages || [],