mixdog 0.9.94 → 0.9.96

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 (259) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +109 -0
  5. package/package.json +26 -13
  6. package/scripts/.tmp-coverage-report.mjs +96 -0
  7. package/scripts/build-tui.mjs +17 -1
  8. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  9. package/scripts/lib/isolated-root-cleanup.mjs +19 -0
  10. package/scripts/run-suite.mjs +100 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/lib/rules-builder.cjs +4 -3
  13. package/src/output-styles/detailed.md +13 -15
  14. package/src/output-styles/extreme-minimal.md +7 -11
  15. package/src/output-styles/minimal.md +4 -7
  16. package/src/output-styles/simple.md +11 -13
  17. package/src/rules/agent/30-explorer.md +7 -4
  18. package/src/rules/lead/01-general.md +1 -2
  19. package/src/rules/lead/lead-brief.md +4 -5
  20. package/src/rules/lead/lead-tool.md +3 -2
  21. package/src/rules/shared/01-tool.md +20 -35
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  24. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  26. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +13 -2
  27. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
  29. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +29 -14
  31. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  33. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
  34. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
  39. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  40. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  41. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  42. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
  44. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  46. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +29 -16
  47. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  49. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
  50. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
  52. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +13 -14
  53. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
  54. package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
  55. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +82 -22
  56. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  57. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  58. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +40 -36
  59. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
  61. package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
  62. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -33
  63. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
  64. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
  65. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  66. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
  67. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  68. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  69. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
  71. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -4
  72. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +168 -83
  75. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
  79. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +34 -18
  80. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  81. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  82. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +89 -11
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
  84. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +6 -39
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -6
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -4
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +56 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +124 -19
  90. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  91. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  92. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +7 -16
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
  100. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
  102. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
  103. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +24 -10
  104. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +20 -43
  106. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +76 -3
  107. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +13 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -5
  109. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +38 -13
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
  113. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +8 -6
  114. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +138 -25
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
  117. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  119. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
  120. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  121. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -2
  123. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
  124. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +177 -16
  125. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +21 -1
  126. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +217 -25
  127. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
  128. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +14 -15
  129. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +60 -8
  130. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  131. package/src/runtime/channels/backends/discord.mjs +5 -14
  132. package/src/runtime/channels/backends/telegram.mjs +0 -5
  133. package/src/runtime/channels/lib/config.mjs +2 -2
  134. package/src/runtime/channels/lib/inbound-handler.mjs +1 -2
  135. package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
  136. package/src/runtime/channels/lib/scheduler.mjs +3 -3
  137. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  138. package/src/runtime/media/renditions.mjs +21 -2
  139. package/src/runtime/memory/index.mjs +5 -38
  140. package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
  141. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  142. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  143. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
  144. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  145. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  146. package/src/runtime/memory/lib/http-router.mjs +8 -0
  147. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  148. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  149. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  150. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  151. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  152. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  153. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  154. package/src/runtime/memory/tool-defs.mjs +7 -11
  155. package/src/runtime/search/tool-defs.mjs +2 -2
  156. package/src/runtime/shared/agent-route-config.mjs +119 -0
  157. package/src/runtime/shared/atomic-file.mjs +133 -4
  158. package/src/runtime/shared/background-tasks.mjs +6 -3
  159. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  160. package/src/runtime/shared/child-guardian.mjs +204 -108
  161. package/src/runtime/shared/child-spawn-gate.mjs +60 -31
  162. package/src/runtime/shared/config.mjs +130 -44
  163. package/src/runtime/shared/resource-admission.mjs +146 -58
  164. package/src/runtime/shared/stream-progress.mjs +6 -0
  165. package/src/runtime/shared/tool-status.mjs +10 -1
  166. package/src/runtime/shared/tool-surface.mjs +2 -7
  167. package/src/runtime/shared/turn-snapshot.mjs +411 -21
  168. package/src/runtime/shared/turn-worktree-snapshot.mjs +552 -0
  169. package/src/session-runtime/config-helpers.mjs +39 -37
  170. package/src/session-runtime/context-status.mjs +9 -3
  171. package/src/session-runtime/lifecycle-api.mjs +35 -5
  172. package/src/session-runtime/mcp-glue.mjs +11 -6
  173. package/src/session-runtime/model-route-api.mjs +7 -5
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +94 -43
  176. package/src/session-runtime/runtime-core.mjs +57 -19
  177. package/src/session-runtime/runtime-tunables.mjs +4 -0
  178. package/src/session-runtime/self-update.mjs +33 -4
  179. package/src/session-runtime/session-lifecycle.mjs +11 -2
  180. package/src/session-runtime/session-text.mjs +2 -1
  181. package/src/session-runtime/session-turn-api.mjs +139 -23
  182. package/src/session-runtime/settings-api.mjs +5 -17
  183. package/src/session-runtime/tool-defs.mjs +3 -3
  184. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  185. package/src/session-runtime/workflow.mjs +25 -28
  186. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  187. package/src/standalone/agent-tool/helpers.mjs +2 -2
  188. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  189. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  190. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  191. package/src/standalone/agent-tool.mjs +4 -4
  192. package/src/standalone/backend-daemon.mjs +631 -0
  193. package/src/standalone/channel-admin.mjs +12 -12
  194. package/src/standalone/channel-daemon-client.mjs +17 -1
  195. package/src/standalone/channel-daemon-transport.mjs +231 -9
  196. package/src/standalone/channel-worker.mjs +3 -2
  197. package/src/standalone/engine-daemon-client.mjs +1069 -0
  198. package/src/standalone/engine-daemon-protocol.mjs +32 -0
  199. package/src/standalone/engine-daemon-service.mjs +1108 -0
  200. package/src/standalone/engine-daemon-transport.mjs +794 -0
  201. package/src/standalone/explore-tool.mjs +187 -70
  202. package/src/standalone/fair-call-scheduler.mjs +264 -0
  203. package/src/standalone/session-protocol.mjs +188 -0
  204. package/src/tui/App.jsx +92 -64
  205. package/src/tui/app/app-format.mjs +4 -2
  206. package/src/tui/app/app-view.jsx +10 -2
  207. package/src/tui/app/channel-pickers.mjs +7 -6
  208. package/src/tui/app/core-memory-picker.mjs +20 -20
  209. package/src/tui/app/doctor.mjs +5 -11
  210. package/src/tui/app/extension-pickers.mjs +20 -18
  211. package/src/tui/app/maintenance-pickers.mjs +27 -27
  212. package/src/tui/app/message-selector.mjs +103 -0
  213. package/src/tui/app/onboarding-steps.mjs +24 -20
  214. package/src/tui/app/project-picker.mjs +80 -56
  215. package/src/tui/app/prompt-submit.mjs +59 -43
  216. package/src/tui/app/resume-picker.mjs +2 -2
  217. package/src/tui/app/route-pickers.mjs +16 -9
  218. package/src/tui/app/settings-picker.mjs +60 -65
  219. package/src/tui/app/slash-dispatch.mjs +32 -28
  220. package/src/tui/app/transcript-window.mjs +57 -0
  221. package/src/tui/app/usage-context-panels.mjs +22 -7
  222. package/src/tui/app/use-mouse-input.mjs +25 -3
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +39 -17
  227. package/src/tui/app/use-transcript-scroll.mjs +39 -10
  228. package/src/tui/app/use-transcript-window.mjs +62 -14
  229. package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
  230. package/src/tui/components/PromptInput.jsx +81 -38
  231. package/src/tui/components/Spinner.jsx +89 -95
  232. package/src/tui/components/TextEntryPanel.jsx +14 -0
  233. package/src/tui/components/ToolExecution.jsx +2 -2
  234. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  235. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  236. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  237. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  238. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  239. package/src/tui/dist/index.mjs +1694 -10633
  240. package/src/tui/engine/agent-job-feed.mjs +2 -2
  241. package/src/tui/engine/live-share.mjs +110 -3
  242. package/src/tui/engine/session-api-ext.mjs +24 -5
  243. package/src/tui/engine/session-api.mjs +165 -55
  244. package/src/tui/engine/session-flow.mjs +50 -5
  245. package/src/tui/engine/tool-card-results.mjs +10 -2
  246. package/src/tui/engine/tool-result-text.mjs +10 -0
  247. package/src/tui/engine/turn.mjs +11 -64
  248. package/src/tui/engine-local-session.mjs +1124 -0
  249. package/src/tui/engine.mjs +16 -1065
  250. package/src/tui/index.jsx +41 -4
  251. package/src/tui/markdown/stream-fence.mjs +1 -1
  252. package/src/tui/spinner-meta.mjs +80 -0
  253. package/src/tui/spinner-verbs.mjs +83 -0
  254. package/src/ui/statusline-segments.mjs +43 -10
  255. package/src/ui/statusline.mjs +10 -1
  256. package/scripts/tmp-cdp-errors.mjs +0 -41
  257. package/scripts/tmp-cdp-inspect.mjs +0 -41
  258. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
  259. package/src/standalone/channel-daemon.mjs +0 -226
@@ -282,7 +282,7 @@ function summarizePromptCacheTools(tools) {
282
282
 
283
283
  /**
284
284
  * Build a stable, prefix-scoped prompt_cache_key for OpenAI-style key-prefix
285
- * providers. OpenAI OAuth follows Codex's thread-scoped key by default:
285
+ * providers. OpenAI OAuth uses a thread-scoped key by default:
286
286
  * prompt_cache_key is the session/thread identity, clamped to the backend's
287
287
  * 64-character limit. Other OpenAI-style providers keep the older
288
288
  * namespace+prefix-hash key shape, but no longer get a shard suffix unless an
@@ -400,7 +400,7 @@ function assignPromptCacheLaneSlot(provider, opts, shards, seed, { auto = false
400
400
 
401
401
  /**
402
402
  * Resolve an optional cache-lane slot for OpenAI-style prompt cache sharding.
403
- * Codex does not shard prompt_cache_key by default, so every provider now gets
403
+ * prompt_cache_key is not sharded by default, so every provider now gets
404
404
  * one un-suffixed key unless an env/config override opts into shards.
405
405
  */
406
406
  export function resolveProviderPromptCacheLane(provider, opts = {}, config = {}) {
@@ -1,16 +1,10 @@
1
- import { isKnownProvider } from '../../../../standalone/provider-admin.mjs';
1
+ import { configuredAgentRouteCandidates } from '../../../shared/agent-route-config.mjs';
2
2
  import { loadConfig } from '../config.mjs';
3
3
  import { getHiddenAgent } from '../internal-agents.mjs';
4
4
 
5
- const DEFAULT_AGENT_ROUTE_PROVIDER = 'anthropic-oauth';
6
-
7
- function normalizeMaintenanceCandidate(candidate, config) {
5
+ function normalizeMaintenanceCandidate(candidate) {
8
6
  if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return candidate || null;
9
- const configuredProvider = String(config?.defaultProvider || '').trim();
10
- const fallbackProvider = isKnownProvider(configuredProvider)
11
- ? configuredProvider
12
- : DEFAULT_AGENT_ROUTE_PROVIDER;
13
- const provider = String(candidate.provider || fallbackProvider).trim();
7
+ const provider = String(candidate.provider || '').trim();
14
8
  const model = String(candidate.model || '').trim();
15
9
  if (!provider || !model) return null;
16
10
  return {
@@ -34,20 +28,15 @@ export function resolveMaintenanceRoute({ preset, optsPreset, agent, config: cfg
34
28
  if (hidden) {
35
29
  try {
36
30
  const config = cfgIn || loadConfig({ secrets: false });
37
- const maint = config?.maintenance || {};
38
31
  const key = hidden.maintKey || hidden.slot;
39
32
  const role = key === 'explore' ? 'explore' : (key === 'memory' ? 'maintainer' : '');
40
- const workflowSlot = key === 'explore' ? 'explorer' : (key === 'memory' ? 'memory' : '');
41
- if (!role) return maint[key] ?? null;
33
+ if (!role) return config?.maintenance?.[key] ?? null;
42
34
  const candidates = [
43
- role ? config?.agents?.[role] : null,
44
- key === 'memory' ? config?.agents?.maintenance : null,
45
- workflowSlot ? config?.workflowRoutes?.[workflowSlot] : null,
46
- maint[key],
35
+ ...configuredAgentRouteCandidates(config, role),
47
36
  role ? config?.default : null,
48
37
  ];
49
38
  for (const candidate of candidates) {
50
- const route = normalizeMaintenanceCandidate(candidate, config);
39
+ const route = normalizeMaintenanceCandidate(candidate);
51
40
  if (route) return route;
52
41
  }
53
42
  return null;
@@ -31,7 +31,16 @@ function compactRecoveredErrorMessage(value) {
31
31
  : message;
32
32
  }
33
33
 
34
- function traceAgentLoop({ sessionId, iteration, sendMs, messageCount, bodyBytesEst, agent = null }) {
34
+ function traceAgentLoop({
35
+ sessionId,
36
+ iteration,
37
+ sendMs,
38
+ preSendMs,
39
+ toolResumeMs,
40
+ messageCount,
41
+ bodyBytesEst,
42
+ agent = null,
43
+ }) {
35
44
  // Two emit modes, no behavior change either way:
36
45
  // VERBOSE=1 → full loop row incl. body_bytes_est (payload serialized).
37
46
  // TIMING=1 → lightweight send-latency attribution for high-fanout
@@ -45,6 +54,8 @@ function traceAgentLoop({ sessionId, iteration, sendMs, messageCount, bodyBytesE
45
54
  kind: 'loop',
46
55
  agent: agent || null,
47
56
  send_ms: sendMs,
57
+ pre_send_ms: preSendMs ?? null,
58
+ tool_resume_ms: toolResumeMs ?? null,
48
59
  message_count: messageCount,
49
60
  body_bytes_est: bodyBytesEst ?? null,
50
61
  });
@@ -278,7 +289,7 @@ export function parseGrepCoverage(resultText, toolName, toolArgs, resultKind) {
278
289
  return out.length ? out : null;
279
290
  }
280
291
 
281
- // Codex-style patch failures all arrive as "apply_patch … failed" prose, but
292
+ // Patch failures all arrive as "apply_patch … failed" prose, but
282
293
  // a malformed envelope, a rejected hunk, a preflight veto and a size/lock
283
294
  // guard need different operator responses. Returning null means "nothing
284
295
  // patch-specific here" and lets the generic rules (path/enoent, schema/args,
@@ -368,6 +368,10 @@ function _resolveToolFailurePath() {
368
368
  if (process.env.MIXDOG_TOOL_FAILURE_LOG_DISABLE === '1') return null;
369
369
  if (_toolFailurePath) return _toolFailurePath;
370
370
  const explicit = process.env.MIXDOG_TOOL_FAILURE_LOG_PATH;
371
+ // The repo's own `node --test` suites drive intentional tool failures
372
+ // (patch ordering, arg guards, ...). Without an explicit path those rows
373
+ // land in the user's real failure log and read as production incidents.
374
+ if (!explicit && process.env.NODE_TEST_CONTEXT) return null;
371
375
  // Ship-mode default: skip diagnostic tool-failure log file IO unless
372
376
  // dev/debug, MIXDOG_DIAGNOSTICS, or an explicit path opts back in.
373
377
  if (!explicit && !isDiagnosticIOEnabled()) return null;
@@ -158,6 +158,34 @@ function traceAgentSse({ sessionId, sseParseMs, ttftMs, provider, model, transpo
158
158
  });
159
159
  }
160
160
 
161
+ // Per-turn preflight timing (submit → provider request). The runtime also
162
+ // emits this as a `mixdog:turn-timing` process event for the daemon log, but
163
+ // that sink is process-local and invisible to trace tooling — the row below is
164
+ // what session-bench reads, so TTFT stage regressions stay measurable offline.
165
+ function traceTurnTiming({
166
+ sessionId, status, requestId, ttftMs, endToEndTtftMs,
167
+ queueMs, routeMs, preflightMs, mcpMs, providerMs,
168
+ }) {
169
+ const ms = (value) => (Number.isFinite(Number(value)) ? Math.round(Number(value)) : null);
170
+ const payload = {
171
+ status: status || 'unknown',
172
+ request_id: requestId || null,
173
+ ttft_ms: ms(ttftMs),
174
+ end_to_end_ttft_ms: ms(endToEndTtftMs),
175
+ queue_ms: ms(queueMs),
176
+ route_ms: ms(routeMs),
177
+ preflight_ms: ms(preflightMs),
178
+ mcp_ms: ms(mcpMs),
179
+ provider_ms: ms(providerMs),
180
+ };
181
+ appendAgentTrace({
182
+ sessionId,
183
+ kind: 'turn_timing',
184
+ ...payload,
185
+ payload,
186
+ });
187
+ }
188
+
161
189
  function extractThinkingTokens(rawUsage) {
162
190
  if (!rawUsage || typeof rawUsage !== 'object') return null;
163
191
  const direct = Number(rawUsage.thinking_tokens ?? rawUsage.thinkingTokens);
@@ -294,6 +322,7 @@ export {
294
322
  traceAgentToolFailure,
295
323
  traceAgentCompact,
296
324
  traceAgentUsage,
325
+ traceTurnTiming,
297
326
  resolveTraceUsageInput,
298
327
  grokCacheChainTraceFields,
299
328
  traceAgentCompress,
@@ -1,5 +1,9 @@
1
1
  import { resolvePluginData } from '../../shared/plugin-paths.mjs';
2
2
  import { readSection, updateSection, updateSectionAsync, getAgentApiKey, AGENT_PROVIDER_ENV } from '../../shared/config.mjs';
3
+ import {
4
+ agentRouteStorageNeedsMigration,
5
+ canonicalizeAgentRouteStorage,
6
+ } from '../../shared/agent-route-config.mjs';
3
7
  import { OPENAI_COMPAT_PRESETS } from './providers/openai-compat-presets.mjs';
4
8
  import {
5
9
  hasAnthropicOAuthCredentials,
@@ -7,20 +11,6 @@ import {
7
11
  hasGrokOAuthCredentials,
8
12
  } from './providers/oauth-credential-probes.mjs';
9
13
 
10
- // Keep config loading free of standalone provider-admin and its provider
11
- // runtimes. These identifiers mirror that module's static catalog, assembled
12
- // solely from the lightweight config/provider constants already imported here.
13
- const CONFIG_PROVIDER_IDS = new Set([
14
- ...Object.keys(AGENT_PROVIDER_ENV),
15
- ...Object.keys(OPENAI_COMPAT_PRESETS),
16
- 'openai-oauth',
17
- 'anthropic-oauth',
18
- 'grok-oauth',
19
- ]);
20
- function isConfiguredProviderId(provider) {
21
- return CONFIG_PROVIDER_IDS.has(String(provider || '').trim());
22
- }
23
-
24
14
  // Thin wrapper around resolvePluginData so callers in this orchestrator tree
25
15
  // can import a single helper without reaching into shared/.
26
16
  export function getPluginData() {
@@ -33,19 +23,13 @@ export function getPluginData() {
33
23
  // llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
34
24
  //
35
25
  // Explore and Maintainer start without a route so they dynamically inherit the
36
- // Main route. Explicit `maintenance.explore` / `maintenance.memory` choices
37
- // remain supported. The memory cycles (chunker / re-scorer / core reviewer)
38
- // share the optional `memory` preset knob — the cycle agents stay separate
39
- // (cycle1/2/3-agent, distinct slots and invokedBy) but resolve their model from
40
- // `maint.memory` via the `maintKey` override on their hidden-role entries.
26
+ // Main route. Their explicit routes live canonically in `agents.explore` and
27
+ // `agents.maintainer`; load-time migration still accepts the older workflow /
28
+ // maintenance aliases.
41
29
  // scheduler/webhook still let a per-entry config.json model win first (the
42
30
  // caller passes it explicitly via opts.preset); the haiku default below only
43
31
  // applies when an entry omits its own model.
44
- // Slots surfaced as tunable rows in the Maintenance Setup panel. This is the
45
- // UI / allow-list view (GET cleanup + POST validation) and is intentionally a
46
- // SUBSET of DEFAULT_MAINTENANCE: scheduler/webhook carry a per-entry model and
47
- // are not shown as shared rows, but still inherit the haiku default above when
48
- // an entry omits its own model.
32
+ // Legacy route slots accepted only at config ingress for migration.
49
33
  const MAINTENANCE_SLOTS = Object.freeze(['explore', 'memory']);
50
34
 
51
35
  // --- User profile (statusline /profile) -------------------------------------
@@ -213,6 +197,13 @@ function normalizeSearchRoute(route) {
213
197
  return null;
214
198
  const provider = normalizeAgentProviderId(route.provider);
215
199
  const model = String(route.model || '').trim();
200
+ if (!provider && model) {
201
+ return {
202
+ provider: 'default',
203
+ model: 'default',
204
+ ...(String(route.toolType || '').trim() ? { toolType: String(route.toolType).trim() } : {}),
205
+ };
206
+ }
216
207
  if (!provider || !model)
217
208
  return null;
218
209
  const out = { provider, model };
@@ -230,17 +221,11 @@ function normalizeSearchRoute(route) {
230
221
  // Normalize stored maintenance slots to the direct {provider, model} route
231
222
  // shape (provider/model/effort/fast only). Non-route values are dropped so the
232
223
  // DEFAULT_MAINTENANCE route fills the slot.
233
- function normalizeMaintenanceRoutes(rawMaint, defaultProvider) {
224
+ function normalizeMaintenanceRoutes(rawMaint) {
234
225
  const out = {};
235
- const configuredProvider = String(defaultProvider || '').trim();
236
- const fallbackProvider = isConfiguredProviderId(configuredProvider)
237
- ? configuredProvider
238
- : 'anthropic-oauth';
239
226
  for (const [slot, value] of Object.entries(rawMaint || {})) {
240
227
  if (value && typeof value === 'object' && !Array.isArray(value)) {
241
- const provider = normalizeAgentProviderId(
242
- value.provider || ((slot === 'explore' || slot === 'memory') ? fallbackProvider : ''),
243
- );
228
+ const provider = normalizeAgentProviderId(value.provider);
244
229
  const model = String(value.model || '').trim();
245
230
  if (provider && model) {
246
231
  const route = { provider, model };
@@ -281,6 +266,174 @@ function normalizeRecapConfig(rawRecap) {
281
266
  return recap;
282
267
  }
283
268
 
269
+ function configObject(value) {
270
+ return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
271
+ }
272
+
273
+ function nonEmptyConfigObject(value) {
274
+ return Object.keys(value).length > 0 ? value : undefined;
275
+ }
276
+
277
+ function normalizeStoredCompactionType(value) {
278
+ const raw = String(value ?? '').trim().toLowerCase().replace(/_/g, '-');
279
+ if (['1', 'type1', 'type-1', 'semantic', 'summary', 'default'].includes(raw)) return 'semantic';
280
+ if (['2', 'type2', 'type-2', 'recall', 'recall-fast', 'recall-fasttrack',
281
+ 'recall-fast-track', 'fasttrack', 'fast-track'].includes(raw)) return 'recall-fasttrack';
282
+ return 'recall-fasttrack';
283
+ }
284
+
285
+ function canonicalizeAutoClearStorage(value) {
286
+ const raw = configObject(value);
287
+ const next = { ...raw };
288
+ const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
289
+ const providerSource = configObject(raw.providerIdleMs ?? raw.providerDefaults ?? raw.providers);
290
+ const providerIdleMs = {};
291
+ for (const [key, candidate] of Object.entries(providerSource)) {
292
+ const provider = String(key || '').trim().toLowerCase();
293
+ const duration = Number(candidate);
294
+ if (!provider || !Number.isFinite(duration) || duration <= 0) continue;
295
+ providerIdleMs[provider] = Math.max(60_000, Math.round(duration));
296
+ }
297
+ delete next.thresholdMs;
298
+ delete next.idleMillis;
299
+ delete next.providerDefaults;
300
+ delete next.providers;
301
+ delete next.custom;
302
+ if (Number.isFinite(idleMs) && idleMs > 0) next.idleMs = Math.max(60_000, Math.round(idleMs));
303
+ else delete next.idleMs;
304
+ if (Object.keys(providerIdleMs).length) next.providerIdleMs = providerIdleMs;
305
+ else delete next.providerIdleMs;
306
+ if (Object.prototype.hasOwnProperty.call(raw, 'enabled')) next.enabled = raw.enabled !== false;
307
+ if (Object.prototype.hasOwnProperty.call(raw, 'minContextPercent')) {
308
+ const percent = Number(raw.minContextPercent);
309
+ if (Number.isFinite(percent)) next.minContextPercent = Math.min(100, Math.max(0, Math.round(percent)));
310
+ else delete next.minContextPercent;
311
+ }
312
+ return nonEmptyConfigObject(next);
313
+ }
314
+
315
+ function canonicalizeCompactionStorage(value) {
316
+ const raw = configObject(value);
317
+ const next = { ...raw };
318
+ const hasType = ['type', 'compactType', 'compact_type']
319
+ .some((key) => Object.prototype.hasOwnProperty.call(raw, key));
320
+ if (hasType) next.type = normalizeStoredCompactionType(raw.type ?? raw.compactType ?? raw.compact_type);
321
+ if (Object.prototype.hasOwnProperty.call(raw, 'auto')
322
+ || Object.prototype.hasOwnProperty.call(raw, 'enabled')) {
323
+ next.auto = raw.auto !== false && raw.enabled !== false;
324
+ }
325
+ delete next.compactType;
326
+ delete next.compact_type;
327
+ delete next.enabled;
328
+ return nonEmptyConfigObject(next);
329
+ }
330
+
331
+ function canonicalizeShellStorage(value) {
332
+ const raw = configObject(value);
333
+ const next = { ...raw };
334
+ const command = String(raw.command ?? raw.path ?? raw.executable ?? raw.shell ?? '').trim();
335
+ delete next.path;
336
+ delete next.executable;
337
+ delete next.shell;
338
+ if (command) next.command = command;
339
+ else delete next.command;
340
+ return nonEmptyConfigObject(next);
341
+ }
342
+
343
+ function canonicalizeModulesStorage(value) {
344
+ const modules = configObject(value);
345
+ delete modules.memory;
346
+ return nonEmptyConfigObject(modules);
347
+ }
348
+
349
+ function migrateLegacyModelSettings(raw = {}) {
350
+ const modelSettings = raw.modelSettings && typeof raw.modelSettings === 'object'
351
+ ? { ...raw.modelSettings }
352
+ : {};
353
+ const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
354
+ ? raw.fastModels
355
+ : {};
356
+ for (const [key, enabled] of Object.entries(legacyFastModels)) {
357
+ if (enabled !== true) continue;
358
+ const current = modelSettings[key];
359
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
360
+ modelSettings[key] = { fast: true };
361
+ } else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
362
+ modelSettings[key] = { ...current, fast: true };
363
+ }
364
+ }
365
+ return modelSettings;
366
+ }
367
+
368
+ function canonicalizeLegacyAgentStorage(value = {}) {
369
+ const next = canonicalizeAgentRouteStorage(value);
370
+ next.presets = Array.isArray(next.presets)
371
+ ? next.presets.map((preset) => normalizePreset(preset)).filter(Boolean)
372
+ : [];
373
+ next.modelSettings = migrateLegacyModelSettings(value);
374
+ const autoClear = canonicalizeAutoClearStorage(next.autoClear);
375
+ if (autoClear) next.autoClear = autoClear;
376
+ else delete next.autoClear;
377
+ const compaction = canonicalizeCompactionStorage(next.compaction);
378
+ if (compaction) next.compaction = compaction;
379
+ else delete next.compaction;
380
+ const shell = canonicalizeShellStorage(next.shell);
381
+ if (shell) next.shell = shell;
382
+ else delete next.shell;
383
+ if (Object.prototype.hasOwnProperty.call(next, 'profile')) {
384
+ next.profile = normalizeProfileConfig(next.profile);
385
+ }
386
+ if (Object.prototype.hasOwnProperty.call(next, 'skills')) {
387
+ const skills = normalizeSkillsConfig(next.skills);
388
+ if (skills.disabled.length) next.skills = skills;
389
+ else delete next.skills;
390
+ }
391
+ if (!next.searchRoute && value?.search && typeof value.search === 'object') {
392
+ next.searchRoute = value.search;
393
+ }
394
+ next.searchRoute = normalizeSearchRoute(next.searchRoute);
395
+ const modules = next.modules && typeof next.modules === 'object' && !Array.isArray(next.modules)
396
+ ? { ...next.modules }
397
+ : {};
398
+ if (Object.prototype.hasOwnProperty.call(modules, 'memory')) {
399
+ const legacyMemory = modules.memory;
400
+ if (!(next.recap && typeof next.recap === 'object' && Object.prototype.hasOwnProperty.call(next.recap, 'enabled'))) {
401
+ const enabled = legacyMemory && typeof legacyMemory === 'object'
402
+ ? legacyMemory.enabled !== false
403
+ : legacyMemory !== false;
404
+ next.recap = { ...(next.recap || {}), enabled };
405
+ }
406
+ delete modules.memory;
407
+ }
408
+ if (Object.keys(modules).length) next.modules = modules;
409
+ else delete next.modules;
410
+ delete next.fastModels;
411
+ delete next.agentMaintenance;
412
+ delete next.runtime;
413
+ delete next.search;
414
+ delete next.capabilities;
415
+ delete next.defaultProvider;
416
+ delete next.guide;
417
+ return next;
418
+ }
419
+
420
+ function agentConfigStorageNeedsMigration(value = {}) {
421
+ const modules = value?.modules;
422
+ const presets = Array.isArray(value?.presets) ? value.presets : [];
423
+ const canonical = canonicalizeLegacyAgentStorage(value);
424
+ const normalizedFields = ['autoClear', 'compaction', 'shell', 'profile', 'skills', 'modules', 'guide'];
425
+ return agentRouteStorageNeedsMigration(value)
426
+ || Object.prototype.hasOwnProperty.call(value || {}, 'fastModels')
427
+ || Object.prototype.hasOwnProperty.call(value || {}, 'agentMaintenance')
428
+ || Object.prototype.hasOwnProperty.call(value || {}, 'runtime')
429
+ || Object.prototype.hasOwnProperty.call(value || {}, 'search')
430
+ || Object.prototype.hasOwnProperty.call(value || {}, 'capabilities')
431
+ || Object.prototype.hasOwnProperty.call(value || {}, 'defaultProvider')
432
+ || presets.some((preset) => !normalizePreset(preset))
433
+ || (modules && typeof modules === 'object' && Object.prototype.hasOwnProperty.call(modules, 'memory'))
434
+ || normalizedFields.some((key) => JSON.stringify(value?.[key]) !== JSON.stringify(canonical?.[key]));
435
+ }
436
+
284
437
  export function loadConfig(options = {}) {
285
438
  const includeSecrets = options.secrets !== false;
286
439
  const sectionRaw = readSection('agent');
@@ -290,6 +443,8 @@ export function loadConfig(options = {}) {
290
443
  if (raw.agent && raw.agent.providers) {
291
444
  raw = raw.agent;
292
445
  }
446
+ const storageNeedsMigration = agentConfigStorageNeedsMigration(raw);
447
+ raw = canonicalizeLegacyAgentStorage(raw);
293
448
  const defaults = buildDefaultConfig({ detectCredentials: includeSecrets });
294
449
  // Deep-merge provider subkeys: unknown per-provider values are
295
450
  // preserved through save/load so future fields round-trip
@@ -371,27 +526,13 @@ export function loadConfig(options = {}) {
371
526
  // without overriding an explicit modelSettings.fast value. Keep
372
527
  // the compatibility read here, but never expose/persist the
373
528
  // legacy container again.
374
- const modelSettings = raw.modelSettings && typeof raw.modelSettings === 'object'
375
- ? { ...raw.modelSettings }
376
- : {};
377
- const legacyFastModels = raw.fastModels && typeof raw.fastModels === 'object'
378
- ? raw.fastModels
379
- : {};
380
- for (const [key, enabled] of Object.entries(legacyFastModels)) {
381
- if (enabled !== true) continue;
382
- const current = modelSettings[key];
383
- if (!current || typeof current !== 'object' || Array.isArray(current)) {
384
- modelSettings[key] = { fast: true };
385
- } else if (!Object.prototype.hasOwnProperty.call(current, 'fast')) {
386
- modelSettings[key] = { ...current, fast: true };
387
- }
388
- }
529
+ const modelSettings = migrateLegacyModelSettings(raw);
389
530
  const workflowRoutes = raw.workflowRoutes && typeof raw.workflowRoutes === 'object' ? { ...raw.workflowRoutes } : {};
390
531
  delete workflowRoutes.search;
391
532
  // Normalize maintenance slots to routes, then overlay onto the
392
533
  // route-shaped defaults.
393
- const normalizedMaint = normalizeMaintenanceRoutes(rawMaint, raw.defaultProvider);
394
- return {
534
+ const normalizedMaint = normalizeMaintenanceRoutes(rawMaint);
535
+ const loaded = canonicalizeAgentRouteStorage({
395
536
  providers: mergedProviders,
396
537
  mcpServers,
397
538
  mcpProjectOverrides: raw.mcpProjectOverrides && typeof raw.mcpProjectOverrides === 'object' && !Array.isArray(raw.mcpProjectOverrides)
@@ -418,7 +559,40 @@ export function loadConfig(options = {}) {
418
559
  update: raw.update && typeof raw.update === 'object' ? { ...raw.update } : {},
419
560
  recap: recapConfig,
420
561
  modules: raw.modules && typeof raw.modules === 'object' ? { ...raw.modules } : {},
421
- };
562
+ });
563
+ if (storageNeedsMigration) {
564
+ try {
565
+ // `readSection('agent')` sees cross-section migrations from
566
+ // canonicalizeUnifiedConfig (root autoClear/compaction/
567
+ // shell and memory.user.title), while the in-lock raw
568
+ // section does not. Seed only those missing/partial values
569
+ // from the canonical read snapshot; the current in-lock
570
+ // section still wins for concurrent edits.
571
+ persistAgentConfig((current) => {
572
+ const merged = { ...current };
573
+ for (const key of ['autoClear', 'compaction', 'shell', 'recap']) {
574
+ if (!Object.prototype.hasOwnProperty.call(current, key)
575
+ && Object.prototype.hasOwnProperty.call(raw, key)) {
576
+ merged[key] = raw[key];
577
+ }
578
+ }
579
+ if (Object.prototype.hasOwnProperty.call(raw, 'profile')) {
580
+ const currentProfile = normalizeProfileConfig(current.profile);
581
+ const migratedProfile = normalizeProfileConfig(raw.profile);
582
+ merged.profile = {
583
+ ...currentProfile,
584
+ ...(!currentProfile.title && migratedProfile.title
585
+ ? { title: migratedProfile.title }
586
+ : {}),
587
+ };
588
+ }
589
+ return canonicalizeLegacyAgentStorage(merged);
590
+ });
591
+ } catch (err) {
592
+ process.stderr.write(`[config] persist canonical agent config failed: ${err?.message}\n`);
593
+ }
594
+ }
595
+ return loaded;
422
596
  }
423
597
  catch { /* fall through */ }
424
598
  }
@@ -430,7 +604,6 @@ export function loadConfig(options = {}) {
430
604
  presets: DEFAULT_PRESETS.map(p => ({ ...p })),
431
605
  default: null,
432
606
  maintenance: { ...DEFAULT_MAINTENANCE },
433
- workflowRoutes: {},
434
607
  searchRoute: null,
435
608
  modelSettings: {},
436
609
  onboarding: {},
@@ -491,6 +664,7 @@ export async function patchSkillsDisabledAsync(disabledNames) {
491
664
  }
492
665
 
493
666
  function buildAgentSaveBuilder(config, appliedDirty) {
667
+ const canonicalRoutes = canonicalizeLegacyAgentStorage(config);
494
668
  // Strip ephemeral defaults from providers but preserve any unknown
495
669
  // per-provider subkey so future schema additions round-trip through the
496
670
  // setup UI without changes here. apiKey is intentionally omitted —
@@ -514,13 +688,15 @@ function buildAgentSaveBuilder(config, appliedDirty) {
514
688
  persistedProviders[name] = slim;
515
689
  }
516
690
  }
517
- const workflowRoutes = config.workflowRoutes && typeof config.workflowRoutes === 'object'
518
- ? { ...config.workflowRoutes }
519
- : {};
520
- delete workflowRoutes.search;
521
- const presets = Array.isArray(config.presets)
522
- ? config.presets.filter(p => p?.id !== 'workflow-search')
691
+ const presets = Array.isArray(canonicalRoutes.presets)
692
+ ? canonicalRoutes.presets.filter(p => p?.id !== 'workflow-search')
523
693
  : [];
694
+ const profile = normalizeProfileConfig(config.profile);
695
+ const skills = normalizeSkillsConfig(config.skills);
696
+ const autoClear = canonicalizeAutoClearStorage(config.autoClear);
697
+ const compaction = canonicalizeCompactionStorage(config.compaction);
698
+ const shell = canonicalizeShellStorage(config.shell);
699
+ const modules = canonicalizeModulesStorage(config.modules);
524
700
  // Build the replacement from `existingRaw` — the section read INSIDE the
525
701
  // file lock — not a snapshot taken before it, so unmanaged keys written by
526
702
  // a concurrent instance survive the save (lost-update guard).
@@ -559,27 +735,25 @@ function buildAgentSaveBuilder(config, appliedDirty) {
559
735
  }
560
736
  const next = {
561
737
  ...existingRaw,
562
- guide: config.guide || existingRaw.guide || undefined,
563
738
  providers: persistedProviders,
564
739
  mcpServers,
565
740
  mcpProjectOverrides,
566
741
  presets,
567
742
  default: config.default || null,
568
- maintenance: config.maintenance || {},
569
- workflowRoutes,
743
+ maintenance: canonicalRoutes.maintenance,
570
744
  searchRoute: normalizeSearchRoute(config.searchRoute),
571
745
  modelSettings: config.modelSettings || {},
572
746
  onboarding: config.onboarding || {},
573
- agents: config.agents || {},
747
+ agents: canonicalRoutes.agents,
574
748
  workflow: config.workflow || { active: 'default' },
575
- profile: normalizeProfileConfig(config.profile),
576
- skills: normalizeSkillsConfig(config.skills),
577
- autoClear: config.autoClear || {},
578
- compaction: config.compaction || {},
579
- shell: config.shell || {},
749
+ profile,
750
+ skills: skills.disabled.length ? skills : undefined,
751
+ autoClear,
752
+ compaction,
753
+ shell,
580
754
  update: config.update || {},
581
755
  recap: config.recap || {},
582
- modules: config.modules || existingRaw.modules || {},
756
+ modules,
583
757
  };
584
758
  // These keys were previously round-tripped despite having no runtime
585
759
  // consumer. Explicitly remove them from the in-lock baseline so a
@@ -587,6 +761,11 @@ function buildAgentSaveBuilder(config, appliedDirty) {
587
761
  delete next.fastModels;
588
762
  delete next.agentMaintenance;
589
763
  delete next.runtime;
764
+ delete next.workflowRoutes;
765
+ delete next.search;
766
+ delete next.capabilities;
767
+ delete next.defaultProvider;
768
+ delete next.guide;
590
769
  return next;
591
770
  };
592
771
  }