mixdog 1.0.12 → 1.1.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 (256) hide show
  1. package/README.md +75 -181
  2. package/package.json +4 -2
  3. package/scripts/build-shipped-history.mjs +131 -0
  4. package/scripts/lib/run-node-tests.mjs +235 -13
  5. package/scripts/lib/test-failure-records.mjs +5 -0
  6. package/scripts/lib/test-failure-summary.mjs +9 -3
  7. package/scripts/lib/test-spawn-ledger.mjs +19 -0
  8. package/scripts/prune-embedding-runtime.mjs +9 -5
  9. package/scripts/test-direct.mjs +16 -1
  10. package/src/agents/advisor/AGENT.md +38 -0
  11. package/src/agents/advisor/agent.json +6 -0
  12. package/src/agents/reviewer/AGENT.md +5 -2
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/defaults/shipped-history.json +381 -0
  15. package/src/defaults/skills/goal-management/SKILL.md +11 -5
  16. package/src/defaults/skills/setup/SKILL.md +5 -8
  17. package/src/defaults/skills/setup/references/actions.md +61 -31
  18. package/src/defaults/skills/setup/references/surfaces.md +12 -10
  19. package/src/lib/rules-builder.cjs +1 -1
  20. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  22. package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
  23. package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
  24. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/config.mjs +102 -21
  26. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
  27. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
  30. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
  32. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
  33. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
  34. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
  35. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
  36. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
  38. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
  43. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
  44. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
  47. package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
  48. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
  49. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
  50. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
  51. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
  52. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
  56. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
  58. package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
  63. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
  64. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
  65. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
  66. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
  67. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
  68. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  69. package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
  72. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
  73. package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
  77. package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
  85. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
  86. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
  87. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  88. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
  90. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
  91. package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
  92. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
  94. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
  95. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
  97. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
  99. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
  100. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
  101. package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
  104. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
  105. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
  106. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
  107. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
  108. package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
  109. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
  110. package/src/runtime/channels/lib/config.mjs +3 -8
  111. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
  112. package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
  113. package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
  114. package/src/runtime/computer-bridge/actions.mjs +2 -0
  115. package/src/runtime/effort-judge/judge-client.mjs +12 -3
  116. package/src/runtime/effort-judge/model-manifest.json +7 -7
  117. package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
  118. package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
  119. package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
  120. package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
  121. package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
  122. package/src/runtime/memory/lib/memory-embed.mjs +5 -0
  123. package/src/runtime/shared/agent-route-config.mjs +50 -2
  124. package/src/runtime/shared/bounded-file-read.mjs +166 -0
  125. package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
  126. package/src/runtime/shared/config.mjs +24 -4
  127. package/src/runtime/shared/defaults-separation.mjs +263 -0
  128. package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
  129. package/src/runtime/shared/llm/model-catalog.mjs +23 -0
  130. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
  131. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  132. package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
  133. package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
  134. package/src/runtime/shared/orchestration.mjs +14 -6
  135. package/src/runtime/shared/projects.mjs +65 -48
  136. package/src/runtime/shared/provider-accounts.mjs +47 -6
  137. package/src/runtime/shared/quota-resume-store.mjs +255 -0
  138. package/src/runtime/shared/shipped-definitions.mjs +275 -0
  139. package/src/runtime/shared/tool-card-model.golden.json +3 -3
  140. package/src/runtime/shared/tool-result-summary.mjs +86 -15
  141. package/src/runtime/shared/transcript-metadata.mjs +2 -0
  142. package/src/runtime/shared/turn-review-reply.mjs +2 -0
  143. package/src/runtime/shared/turn-snapshot.mjs +65 -6
  144. package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
  145. package/src/session-runtime/boot/apis.mjs +19 -0
  146. package/src/session-runtime/boot/config.mjs +4 -0
  147. package/src/session-runtime/boot/facade.mjs +4 -1
  148. package/src/session-runtime/boot/infrastructure.mjs +21 -2
  149. package/src/session-runtime/boot/session.mjs +3 -0
  150. package/src/session-runtime/channel-config-api.mjs +5 -0
  151. package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
  152. package/src/session-runtime/config-lifecycle.mjs +31 -6
  153. package/src/session-runtime/goal-facade-api.mjs +4 -0
  154. package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
  155. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
  156. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
  157. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
  158. package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
  159. package/src/session-runtime/media-api.mjs +32 -22
  160. package/src/session-runtime/model-recency.mjs +14 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +3 -5
  162. package/src/session-runtime/model-route/set-route.mjs +49 -21
  163. package/src/session-runtime/model-route-api.mjs +2 -1
  164. package/src/session-runtime/notification-bus.mjs +31 -6
  165. package/src/session-runtime/provider-auth/login.mjs +6 -0
  166. package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
  167. package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
  168. package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
  169. package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
  170. package/src/session-runtime/resource-api.mjs +8 -1
  171. package/src/session-runtime/runtime-core.mjs +4 -0
  172. package/src/session-runtime/runtime-facade.mjs +20 -1
  173. package/src/session-runtime/runtime-review-api.mjs +12 -1
  174. package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
  175. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
  176. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
  177. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
  178. package/src/session-runtime/services/channel-admin.mjs +87 -13
  179. package/src/session-runtime/services/provider-admin.mjs +256 -63
  180. package/src/session-runtime/services/seeds.mjs +73 -142
  181. package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
  182. package/src/session-runtime/services/usage-dashboard.mjs +7 -0
  183. package/src/session-runtime/session-address.mjs +125 -0
  184. package/src/session-runtime/session-create.mjs +57 -2
  185. package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
  186. package/src/session-runtime/settings-compaction-api.mjs +13 -0
  187. package/src/session-runtime/setup-tool/executor.mjs +92 -40
  188. package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
  189. package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
  190. package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
  191. package/src/session-runtime/tool-surface.mjs +16 -9
  192. package/src/session-runtime/turn/session-ops.mjs +3 -0
  193. package/src/session-runtime/turn/share-ops.mjs +4 -1
  194. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
  195. package/src/session-runtime/usage-stats-api.mjs +15 -0
  196. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
  197. package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
  198. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
  199. package/src/standalone/daemon-boot-coordinator.mjs +10 -0
  200. package/src/standalone/daemon-drain-policy.mjs +9 -0
  201. package/src/standalone/daemon-stored-session-views.mjs +7 -8
  202. package/src/standalone/daemon.mjs +21 -5
  203. package/src/standalone/local-session-runtime.mjs +11 -0
  204. package/src/standalone/session-client.mjs +52 -7
  205. package/src/standalone/session-protocol.mjs +8 -0
  206. package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
  207. package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
  208. package/src/standalone/session-service/entries/session-loads.mjs +9 -0
  209. package/src/standalone/session-service/projection/revisions.mjs +28 -4
  210. package/src/standalone/session-service/projection.mjs +1 -0
  211. package/src/standalone/session-service/quota-resume.mjs +140 -0
  212. package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
  213. package/src/standalone/session-service/session-calls.mjs +10 -2
  214. package/src/standalone/session-service/stored-reader.mjs +26 -1
  215. package/src/standalone/session-service.mjs +21 -0
  216. package/src/standalone/session-transport/routes.mjs +2 -0
  217. package/src/tui/app/doctor.mjs +7 -11
  218. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
  219. package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
  220. package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
  221. package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
  222. package/src/tui/components/ContextPanel.jsx +5 -1
  223. package/src/tui/components/Spinner.jsx +34 -319
  224. package/src/tui/components/SpinnerByline.jsx +256 -0
  225. package/src/tui/components/use-spinner-progress.mjs +113 -0
  226. package/src/tui/dist/index.mjs +15540 -14452
  227. package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
  228. package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
  229. package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
  230. package/src/tui/session/agent-job-feed.mjs +4 -0
  231. package/src/tui/session/context-state.mjs +13 -1
  232. package/src/tui/session/initial-state.mjs +6 -0
  233. package/src/tui/session/quota-resume.mjs +263 -0
  234. package/src/tui/session/session-api/intake/abort.mjs +39 -35
  235. package/src/tui/session/session-api/intake/submission.mjs +13 -21
  236. package/src/tui/session/session-api/intake.mjs +3 -3
  237. package/src/tui/session/session-api/integrations.mjs +5 -1
  238. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  239. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
  240. package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
  241. package/src/tui/session/session-api/lifecycle.mjs +71 -2
  242. package/src/tui/session/session-api/media.mjs +2 -2
  243. package/src/tui/session/session-api/quota-resume.mjs +21 -0
  244. package/src/tui/session/session-api/routes.mjs +2 -0
  245. package/src/tui/session/session-api/settings.mjs +1 -0
  246. package/src/tui/session/session-api.mjs +2 -0
  247. package/src/tui/session/session-flow/auto-clear.mjs +14 -6
  248. package/src/tui/session/session-flow/drain.mjs +52 -5
  249. package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
  250. package/src/tui/session/session-flow/queue.mjs +3 -0
  251. package/src/tui/session/session-flow/steering.mjs +11 -0
  252. package/src/tui/session/session-flow.mjs +1 -0
  253. package/src/tui/session/tool-approval.mjs +7 -2
  254. package/src/tui/session/transcript-intake.mjs +1 -0
  255. package/src/tui/session/turn.mjs +40 -3
  256. package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
@@ -36,8 +36,11 @@ An attached Desktop or TUI may navigate and return `opened:true`.
36
36
  - Settings → General: profile, Web Search exposure, display language, theme,
37
37
  side panels, and notifications.
38
38
  - Settings → Context: auto-compact and auto-clear.
39
- - Settings → Output style: output style selection.
40
- - Settings → Providers: API-key, OAuth, local providers, and usage sign-in.
39
+ - Settings → Output style: output style selection, plus the desktop-only
40
+ "Expand tool activity" choice (collapsed / commands and edits / everything),
41
+ saved per device; Ctrl+O toggles everything open for the current window.
42
+ - Settings → Providers: API-key, OAuth, custom provider registration, local
43
+ providers, and usage sign-in.
41
44
  - Extensions → Plugin → Git & GitHub: Git/GitHub CLI installation and connection.
42
45
  - Settings → Connection: web-app pairing and linked devices.
43
46
  - Settings → System: update, keep-awake, run in background after the window
@@ -53,11 +56,12 @@ the user to Desktop.
53
56
 
54
57
  ## Desktop-backed settings
55
58
 
56
- Setup can read and change Desktop appearance, keep-awake, run in background,
57
- usage pin, activity rail pins, Computer observe-only, Browser/Computer/voice installation and
58
- toggles, Projects, and linked-device revocation through the attached
59
- Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
60
- to the paired browser. No Desktop claimant means no confirmed change.
59
+ Setup can read and change Desktop appearance (including markdown default
60
+ view, link previews, and tool activity expansion), keep-awake, run in
61
+ background, turn-completion notifications, notification volume (Windows),
62
+ usage pin, activity rail pins, Computer observe-only, Browser/Computer
63
+ installation and toggles, Projects, and linked-device revocation through the
64
+ attached Desktop. A receipt identifies `scope:desktop-host`.
61
65
 
62
66
  Computer observe-only and background Memory cycles (`set_recap_enabled`)
63
67
  have no settings control in either app; setup is their only supported route. Usage pin is the pin toggle on the activity rail usage card.
@@ -74,12 +78,10 @@ The setup tool does not mutate:
74
78
  - web-app pairing credentials and OS permission dialogs;
75
79
  - TUI-local theme palettes (use `/theme`; Desktop themes are separate).
76
80
 
77
- Use `open` where a target exists, then let the user act. Never simulate a
78
- missing or denied operation by editing storage or switching surfaces.
81
+ Use `open` where a target exists, then let the user act.
79
82
 
80
83
  ## TUI notes
81
84
 
82
85
  The TUI settings hub exposes system shell in addition to common settings.
83
86
  Desktop-host actions need the Desktop app; opening the TUI is not a substitute.
84
- Use the schema and `status capabilities` as the supported API boundary.
85
87
 
@@ -366,7 +366,7 @@ function buildLeadRoleContent({ PLUGIN_ROOT, includeLeadBrief = true }) {
366
366
  const RULES_DIR = path.join(PLUGIN_ROOT, 'rules');
367
367
  const lead = readOptional(path.join(RULES_DIR, 'lead', 'LEAD.md'));
368
368
  if (!lead) return '';
369
- // Delegation-free workflows (Solo, headless) never expose the `agent` tool,
369
+ // Delegation-free sessions (headless, no delegatable agents) never expose the `agent` tool,
370
370
  // so Lead guidance marked `<!-- tools: agent -->` (briefing, completion
371
371
  // notifications) is dropped there and kept verbatim for delegating workflows.
372
372
  return omitToolRoutes(lead, includeLeadBrief ? [] : ['agent']);
@@ -30,7 +30,9 @@
30
30
  * Non-breakpoint providers:
31
31
  * - OpenAI (public): prompt_cache_key plus model-specific retention when
32
32
  * response storage is enabled (30m cache options or 24h retention).
33
- * - OpenAI OAuth: prompt_cache_key only (server in-memory 5-10min)
33
+ * - OpenAI OAuth: prompt_cache_key only; the Codex backend keeps the
34
+ * prompt cache at least 30 minutes on GPT-5.6+ models, refreshed on use
35
+ * (the openai-oauth auto-clear window matches it)
34
36
  * - Gemini: provider-managed explicit cachedContents with 5m default TTL, plus
35
37
  * implicit caching as a fallback when the prefix is below cache minimums.
36
38
  * - xAI: conversation routing for chat; Responses omits prompt_cache_key
@@ -76,6 +76,8 @@ export function prepareAgentSession({
76
76
  schemaAllowedTools,
77
77
  sessionId,
78
78
  mcpScopeId,
79
+ contextPercent,
80
+ selectedContextWindow,
79
81
  }) {
80
82
  const effectivePermission = resolveAgentSessionPermission(agent, permission);
81
83
  // Pass cwd through verbatim — null is the fixed agent sentinel meaning
@@ -112,6 +114,8 @@ export function prepareAgentSession({
112
114
  compaction: compaction || undefined,
113
115
  mcpScopeId: mcpScopeId || null,
114
116
  };
117
+ if (contextPercent) sessionOpts.contextPercent = contextPercent;
118
+ if (selectedContextWindow) sessionOpts.selectedContextWindow = selectedContextWindow;
115
119
  if (agentTag) sessionOpts.agentTag = agentTag;
116
120
  if (effectivePermission) sessionOpts.permission = effectivePermission;
117
121
  if (permissionMode) sessionOpts.permissionMode = permissionMode;
@@ -0,0 +1 @@
1
+ export function withDefaultPresets(stored: unknown): unknown[];
@@ -1,4 +1,6 @@
1
1
  // Preset normalization and identity are independent of config storage.
2
+ import { isDeepStrictEqual } from 'node:util';
3
+
2
4
  const ANTHROPIC_FAMILY_MODEL = Object.freeze({
3
5
  opus: 'claude-opus-4-8',
4
6
  sonnet: 'claude-sonnet-4-6',
@@ -64,6 +66,48 @@ export const DEFAULT_PRESETS = Object.freeze([
64
66
  }),
65
67
  ]);
66
68
 
69
+ const DEFAULT_PRESET_BY_ID = new Map(DEFAULT_PRESETS.map((preset) => [preset.id, preset]));
70
+
71
+ /** Effective presets: the stored entries in their stored order, then the
72
+ * shipped presets missing from storage in shipped order. */
73
+ export function withDefaultPresets(stored) {
74
+ const list = Array.isArray(stored) ? stored : [];
75
+ const present = new Set(list.map(presetKey));
76
+ return [...list, ...DEFAULT_PRESETS.filter((preset) => !present.has(preset.id)).map((preset) => ({ ...preset }))];
77
+ }
78
+
79
+ /** Stored form of `presets`: only entries that differ from the shipped default of the same id. */
80
+ export function presetsDelta(presets) {
81
+ return (Array.isArray(presets) ? presets : []).filter((preset) => {
82
+ const shipped = DEFAULT_PRESET_BY_ID.get(presetKey(preset));
83
+ return !shipped || !isDeepStrictEqual(normalizePreset(preset), { ...shipped });
84
+ });
85
+ }
86
+
87
+ /** Stored form of `maintenance`: only slots that differ from the shipped route. */
88
+ export function maintenanceDelta(maintenance) {
89
+ const source = maintenance && typeof maintenance === 'object' ? maintenance : {};
90
+ return Object.fromEntries(
91
+ Object.entries(source).filter(([slot, route]) => !isDeepStrictEqual(route, DEFAULT_MAINTENANCE[slot]))
92
+ );
93
+ }
94
+
95
+ /** Drop shipped presets and maintenance routes that equal their default; loadConfig restores them. */
96
+ export function stripStoredRouteDefaults(agent) {
97
+ const next = { ...agent };
98
+ if (Object.hasOwn(next, 'presets')) {
99
+ const presets = presetsDelta(next.presets);
100
+ if (presets.length) next.presets = presets;
101
+ else delete next.presets;
102
+ }
103
+ if (Object.hasOwn(next, 'maintenance')) {
104
+ const maintenance = maintenanceDelta(next.maintenance);
105
+ if (Object.keys(maintenance).length) next.maintenance = maintenance;
106
+ else delete next.maintenance;
107
+ }
108
+ return next;
109
+ }
110
+
67
111
  const AGENT_PROVIDER_ALIASES = Object.freeze({
68
112
  'openai-api': 'openai',
69
113
  'gemini-api': 'gemini',
@@ -50,9 +50,17 @@ export function mergeStoredProviders({ raw, defaults, includeSecrets }) {
50
50
  }
51
51
  }
52
52
  // Custom providers: the keychain account is the generated custom-<uuid> id.
53
+ // A keyless (auth 'none') custom provider never carries a key, not even a
54
+ // hand-edited plaintext one.
55
+ for (const [name, val] of Object.entries(mergedProviders)) {
56
+ if (val?.type === 'custom' && val.auth === 'none' && Object.hasOwn(val, 'apiKey')) {
57
+ const { apiKey: _dropped, ...rest } = val;
58
+ mergedProviders[name] = rest;
59
+ }
60
+ }
53
61
  if (includeSecrets) {
54
62
  for (const [name, val] of Object.entries(mergedProviders)) {
55
- if (val?.type !== 'custom' || !name.startsWith('custom-')) continue;
63
+ if (val?.type !== 'custom' || !name.startsWith('custom-') || val.auth === 'none') continue;
56
64
  const kc = getAgentApiKey(name);
57
65
  if (kc) mergedProviders[name] = { ...val, apiKey: kc, enabled: raw.providers?.[name]?.enabled !== false };
58
66
  }
@@ -4,9 +4,29 @@ import { mergeStoredProviders } from './config-providers-merge.mjs';
4
4
  import { normalizeExtensionScopes } from '../../shared/extension-scopes.mjs';
5
5
  import { applyConfigPatch, diffConfig } from '../../shared/config-patch.mjs';
6
6
  import { normalizeWorkflowSelection } from '../../shared/orchestration.mjs';
7
- import { DEFAULT_DISABLED_AGENT_IDS, canonicalizeAgentRouteStorage } from '../../shared/agent-route-config.mjs';
7
+ import {
8
+ DEFAULT_DISABLED_AGENT_ROSTER,
9
+ canonicalizeAgentRouteStorage,
10
+ disabledAgentDelta,
11
+ disabledAgentIds,
12
+ effectiveDisabledAgents,
13
+ withEffectiveDisabledAgents,
14
+ withStoredDisabledAgents,
15
+ } from '../../shared/agent-route-config.mjs';
16
+ import {
17
+ DEFAULTS_VERSION,
18
+ ensureDefaultsSeparated,
19
+ storedDefaultsVersion,
20
+ stripAgentSectionDefaults,
21
+ } from '../../shared/defaults-separation.mjs';
8
22
  import profileConfig from '../../shared/profile-config.cjs';
9
- import { DEFAULT_MAINTENANCE, DEFAULT_PRESETS, normalizePreset } from './config-presets.mjs';
23
+ import {
24
+ DEFAULT_MAINTENANCE,
25
+ DEFAULT_PRESETS,
26
+ normalizePreset,
27
+ stripStoredRouteDefaults,
28
+ withDefaultPresets,
29
+ } from './config-presets.mjs';
10
30
  import {
11
31
  agentConfigStorageNeedsMigration,
12
32
  canonicalizeAgentStorage,
@@ -100,11 +120,10 @@ export function buildDefaultConfig(options = {}) {
100
120
  providers['mixdog-local'] = { enabled: false };
101
121
  return {
102
122
  providers,
103
- // The Maintainer also starts off: background upkeep spends model calls, so
104
- // it runs only once the user turns it on and picks its model.
105
- disabledAgents: [...DEFAULT_DISABLED_AGENT_IDS, 'maintainer'],
123
+ // Effective roster; the file stores only the user's delta against it.
124
+ disabledAgents: [...DEFAULT_DISABLED_AGENT_ROSTER],
106
125
  workflow: { active: 'default' },
107
- orchestrationMode: 'none',
126
+ orchestrationMode: 'balanced',
108
127
  };
109
128
  }
110
129
 
@@ -170,9 +189,17 @@ function normalizeRecapConfig(rawRecap) {
170
189
 
171
190
  export function loadConfig(options = {}) {
172
191
  const includeSecrets = options.secrets !== false;
192
+ try {
193
+ ensureDefaultsSeparated({ stripRouteDefaults: separateStoredAgent });
194
+ } catch (err) {
195
+ process.stderr.write(`[config] defaults separation failed: ${err?.message}\n`);
196
+ }
173
197
  const sectionRaw = readSection('agent');
174
198
  if (hasKeys(sectionRaw)) {
175
199
  try {
200
+ // A file that could not be separated still holds the legacy form:
201
+ // before version 1 `disabledAgents` is the full list, not a delta.
202
+ const legacyDisabledAgents = storedDefaultsVersion() < 1;
176
203
  let raw = sectionRaw;
177
204
  const storageNeedsMigration = agentConfigStorageNeedsMigration(raw);
178
205
  raw = canonicalizeAgentStorage(raw);
@@ -206,7 +233,7 @@ export function loadConfig(options = {}) {
206
233
  const loaded = canonicalizeAgentRouteStorage({
207
234
  providers: mergedProviders,
208
235
  mcpServers,
209
- presets: normalizedPresets,
236
+ presets: withDefaultPresets(normalizedPresets),
210
237
  default: raw.default || null,
211
238
  maintenance: { ...DEFAULT_MAINTENANCE, ...normalizedMaint },
212
239
  workflowRoutes,
@@ -214,9 +241,10 @@ export function loadConfig(options = {}) {
214
241
  modelSettings,
215
242
  onboarding: raw.onboarding && typeof raw.onboarding === 'object' ? raw.onboarding : {},
216
243
  agents: raw.agents && typeof raw.agents === 'object' ? raw.agents : {},
217
- // Explicit "off" roster. canonicalizeAgentRouteStorage normalizes
218
- // and drops it when empty, so an all-enabled config stays clean.
219
- disabledAgents: Array.isArray(raw.disabledAgents) ? raw.disabledAgents : [],
244
+ // Effective "off" roster = code defaults + the stored user delta.
245
+ // canonicalizeAgentRouteStorage drops it when empty.
246
+ disabledAgents: legacyDisabledAgents ? disabledAgentIds(raw) : effectiveDisabledAgents(raw),
247
+ ...(raw.memoryTools && typeof raw.memoryTools === 'object' ? { memoryTools: { ...raw.memoryTools } } : {}),
220
248
  ...normalizeWorkflowSelection(raw),
221
249
  profile: normalizeProfileConfig(raw.profile),
222
250
  skills: normalizeSkillsConfig(raw.skills),
@@ -241,7 +269,7 @@ export function loadConfig(options = {}) {
241
269
  // Retired cross-section fields are no longer migrated by
242
270
  // the shared config layer. Normalize only the locked
243
271
  // current section; stale reads must not undo deletions.
244
- persistAgentConfig(canonicalizeAgentStorage);
272
+ persistAgentConfig((current) => storedAgentForm(canonicalizeAgentStorage(current)));
245
273
  } catch (err) {
246
274
  process.stderr.write(`[config] persist canonical agent config failed: ${err?.message}\n`);
247
275
  }
@@ -263,7 +291,7 @@ export function loadConfig(options = {}) {
263
291
  onboarding: {},
264
292
  agents: {},
265
293
  workflow: { active: 'default' },
266
- orchestrationMode: 'none',
294
+ orchestrationMode: 'balanced',
267
295
  profile: normalizeProfileConfig(null),
268
296
  skills: normalizeSkillsConfig(null),
269
297
  extensionScopes: normalizeExtensionScopes(null),
@@ -383,28 +411,81 @@ function buildAgentSaveBuilder(config) {
383
411
  // Developer options (developer-options.mjs); an empty set stays absent.
384
412
  if (Object.keys(developer).length) next.developer = developer;
385
413
  else if (Object.hasOwn(config, 'developer')) delete next.developer;
386
- if (canonicalRoutes.disabledAgents) {
387
- next.disabledAgents = canonicalRoutes.disabledAgents;
388
- } else {
389
- delete next.disabledAgents;
390
- }
414
+ // Only the user's delta against the default roster is stored.
415
+ delete next.disabledAgents;
416
+ delete next.enabledAgents;
417
+ // An unseparated file keeps the legacy full list until the pass succeeds.
418
+ if (storedDefaultsVersion() < 1) {
419
+ if (canonicalRoutes.disabledAgents?.length) next.disabledAgents = canonicalRoutes.disabledAgents;
420
+ } else Object.assign(next, disabledAgentDelta(canonicalRoutes.disabledAgents));
421
+ if (config.memoryTools && typeof config.memoryTools === 'object') next.memoryTools = { ...config.memoryTools };
422
+ else delete next.memoryTools;
391
423
  delete next.workflowRoutes;
392
- return removeRetiredAgentFields(next);
424
+ // Defaults live in code: values equal to the current default are not stored.
425
+ return storedAgentForm(removeRetiredAgentFields(next));
393
426
  };
394
427
  }
428
+
429
+ /** A numeric `default` is a preset index; store the preset id it points at (index into the effective list). */
430
+ function resolveNumericDefault(agent) {
431
+ const value = agent?.default;
432
+ // The loader reads a falsy default (including 0) as "none".
433
+ if (!value || !(typeof value === 'number' || /^\d+$/.test(String(value)))) return agent;
434
+ const stored = (Array.isArray(agent.presets) ? agent.presets : [])
435
+ .map((preset) => normalizePreset(preset))
436
+ .filter(Boolean)
437
+ .filter((preset) => preset.id !== 'workflow-search');
438
+ const { presets } = canonicalizeAgentRouteStorage({ presets: withDefaultPresets(stored) });
439
+ const preset = presets[Number(value)];
440
+ return preset ? { ...agent, default: preset.id } : agent;
441
+ }
442
+
443
+ /** The agent section as stored: defaults dropped. Route defaults go only once the file is separated. */
444
+ function storedAgentForm(agent) {
445
+ const stripped = stripAgentSectionDefaults(resolveNumericDefault(agent));
446
+ return storedDefaultsVersion() >= DEFAULTS_VERSION ? stripStoredRouteDefaults(stripped) : stripped;
447
+ }
448
+
449
+ /** Separation pass: normalize the compared sections first, then drop every default. */
450
+ function separateStoredAgent(agent) {
451
+ const full = canonicalizeAgentStorage(agent);
452
+ const next = { ...agent };
453
+ for (const key of ['autoClear', 'compaction', 'modules']) {
454
+ if (!Object.hasOwn(agent, key)) continue;
455
+ if (full[key] === undefined) delete next[key];
456
+ else next[key] = full[key];
457
+ }
458
+ return stripStoredRouteDefaults(stripAgentSectionDefaults(resolveNumericDefault(next)));
459
+ }
460
+
461
+ /** Patch the stored section; unseparated data keeps its form, a numeric default becomes an id first. */
462
+ function applyStoredPatch(current, changes) {
463
+ const base = resolveNumericDefault(current);
464
+ if (storedDefaultsVersion() >= 1) return applyConfigPatch(base, changes);
465
+ const delta = withStoredDisabledAgents({ ...base, disabledAgents: disabledAgentIds(base) });
466
+ return withEffectiveDisabledAgents(applyConfigPatch(delta, changes));
467
+ }
395
468
  // Diff the persisted schema, not runtime defaults, aliases, or secret overlays.
469
+ // Patches always speak the delta domain; an unseparated file's full list is
470
+ // converted here and back in applyStoredPatch.
471
+ function patchForm(agent) {
472
+ if (storedDefaultsVersion() >= 1) return agent;
473
+ const { disabledAgents, ...rest } = agent;
474
+ return { ...rest, ...disabledAgentDelta(disabledAgents || []) };
475
+ }
476
+
396
477
  export function createConfigPatch(before, after) {
397
- return diffConfig(buildAgentSaveBuilder(before)({}), buildAgentSaveBuilder(after)({}));
478
+ return diffConfig(patchForm(buildAgentSaveBuilder(before)({})), patchForm(buildAgentSaveBuilder(after)({})));
398
479
  }
399
480
 
400
481
  export function saveConfigPatch(changes) {
401
482
  if (!changes.length) return;
402
- persistAgentConfig((current) => applyConfigPatch(current, changes));
483
+ persistAgentConfig((current) => applyStoredPatch(current, changes));
403
484
  }
404
485
 
405
486
  export async function saveConfigPatchAsync(changes) {
406
487
  if (!changes.length) return;
407
- await persistAgentConfigAsync((current) => applyConfigPatch(current, changes));
488
+ await persistAgentConfigAsync((current) => applyStoredPatch(current, changes));
408
489
  }
409
490
 
410
491
  // Whole-section replacement is reserved for explicit full saves. Read/edit/save
@@ -6,10 +6,11 @@ import {
6
6
  changeProviderAccounts,
7
7
  blockProviderAccount,
8
8
  providerAccountExhausted,
9
+ providerAccountsResetAt,
9
10
  } from '../../../shared/provider-accounts.mjs';
10
11
  import { withProviderAccount, hasExplicitProviderAuthBinding } from '../../../shared/provider-auth-binding.mjs';
11
12
  import { retryAfterMsFromError, isExplicitUserAbortError, typedErrorCode } from './retry-classifier.mjs';
12
- import { fetchOAuthUsageSnapshot } from './oauth-usage.mjs';
13
+ import { fetchOAuthUsageSnapshot, readCachedOAuthUsageSnapshot } from './oauth-usage.mjs';
13
14
 
14
15
  // Only an explicit quota refusal advances the roster; a generic 429, auth
15
16
  // refusal, policy denial or transport failure retains its existing recovery.
@@ -49,7 +50,7 @@ export function resetAccountProbePacing() {
49
50
  async function probeRefusedAccounts(providerName, pool, model, account) {
50
51
  const now = Date.now();
51
52
  const due = pool.accounts
52
- .filter((row) => providerAccountExhausted(row, now, model))
53
+ .filter((row) => providerAccountExhausted(row, now, model, { useCredits: pool.useCredits === true }))
53
54
  .filter((row) => now - (lastProbeAt.get(`${providerName}:${row.id}`) || 0) >= PROBE_INTERVAL_MS)
54
55
  .sort((a, b) => (a.usage?.checkedAt || 0) - (b.usage?.checkedAt || 0));
55
56
  if (!due.length) return false;
@@ -64,12 +65,29 @@ async function probeRefusedAccounts(providerName, pool, model, account) {
64
65
  return true;
65
66
  }
66
67
 
67
- function accountsExhaustedError() {
68
- const error = new Error('All connected accounts have exhausted their quota. Check account usage and reset times.');
68
+ function accountsExhaustedError(useCredits) {
69
+ const error = new Error(
70
+ useCredits
71
+ ? 'All connected accounts have exhausted their quota. Check account usage and reset times.'
72
+ : 'The plan limit is reached and credit use is off. Turn on credit use or wait for the limit to reset.'
73
+ );
69
74
  error.code = 'provider_accounts_exhausted';
70
75
  return error;
71
76
  }
72
77
 
78
+ // Structured proof that the send ended because no account can serve `model`
79
+ // (not merely because a generic 429 or a stopped loop surfaced an error).
80
+ function quotaExhausted(error, providerName, model, rows, options = {}) {
81
+ if (error && typeof error === 'object') {
82
+ error.quotaExhausted = {
83
+ provider: providerName,
84
+ model: String(model || ''),
85
+ resetAt: providerAccountsResetAt(rows, Date.now(), model, options),
86
+ };
87
+ }
88
+ return error;
89
+ }
90
+
73
91
  // The send bound to one account: output callbacks are observed even when the
74
92
  // caller does not subscribe, and account-bound connection/delta state stays
75
93
  // apart without changing the visible Mixdog session, so no stale server state
@@ -114,18 +132,22 @@ function commitFallbackAccount(providerName, pool, row) {
114
132
  async function accountQuotaExhausted(providerName, row, model, error, account) {
115
133
  const status = Number(error?.status || error?.httpStatus || error?.response?.status || 0);
116
134
  const delay = retryAfterMsFromError(error);
117
- let exhausted = isAccountQuotaError(error) || (status === 429 && delay > QUOTA_WINDOW_RETRY_AFTER_MS);
118
- if (!exhausted && status === 429) {
135
+ const typed = isAccountQuotaError(error);
136
+ let exhausted = typed || (status === 429 && delay > QUOTA_WINDOW_RETRY_AFTER_MS);
137
+ // Proof of a spent quota: a typed quota refusal or an exhausted usage window
138
+ // (a long Retry-After alone only rotates the roster, it proves nothing).
139
+ let evidence = typed;
140
+ if (!typed && status === 429) {
119
141
  await fetchOAuthUsageSnapshot({ provider: providerName, accountId: row.id }, account(row.id), () => {}, {
120
142
  force: true,
121
143
  });
122
- exhausted = providerAccountExhausted(
123
- readProviderAccountPool(providerName).accounts.find((entry) => entry.id === row.id),
124
- Date.now(),
125
- model
126
- );
144
+ const pool = readProviderAccountPool(providerName);
145
+ const entry = pool.accounts.find((item) => item.id === row.id);
146
+ const options = { useCredits: pool.useCredits === true };
147
+ evidence = providerAccountExhausted({ ...entry, blockedUntil: 0 }, Date.now(), model, options);
148
+ exhausted = exhausted || evidence || providerAccountExhausted(entry, Date.now(), model, options);
127
149
  }
128
- return { exhausted, delay };
150
+ return { exhausted, delay, evidence };
129
151
  }
130
152
 
131
153
  // The pool's send: walks the eligible accounts until one answers, blocking
@@ -135,10 +157,43 @@ function poolSend(providerName, account) {
135
157
  const attempted = new Set();
136
158
  let lastError;
137
159
  let probed = false;
160
+ const evidenced = new Set();
161
+ // Mark `error` only when every account in `ids` (default: the roster) is
162
+ // exhausted with quota evidence: observed in this send, or a full window.
163
+ const markExhausted = (error, ids) => {
164
+ const pool = readProviderAccountPool(providerName);
165
+ // Auto switching off: the eligible set is the selected account alone.
166
+ const scope = ids || (pool.auto === false ? [pool.selectedId] : null);
167
+ const rows = scope ? pool.accounts.filter((entry) => scope.includes(entry.id)) : pool.accounts;
168
+ const opts = { useCredits: pool.useCredits === true };
169
+ const now = Date.now();
170
+ const all =
171
+ rows.length > 0 &&
172
+ rows.every((entry) =>
173
+ providerAccountExhausted(
174
+ evidenced.has(entry.id) || entry.blockedQuota === true ? entry : { ...entry, blockedUntil: 0 },
175
+ now,
176
+ model,
177
+ opts
178
+ )
179
+ );
180
+ return all ? quotaExhausted(error, providerName, model, rows, opts) : error;
181
+ };
138
182
  while (attempted.size < 20) {
139
183
  options.signal?.throwIfAborted();
140
184
  const pool = readProviderAccountPool(providerName);
141
- if (!pool.accounts.length) return account('default').send(messages, model, tools, options);
185
+ if (!pool.accounts.length) {
186
+ if (pool.useCredits !== true) {
187
+ const stored = readCachedOAuthUsageSnapshot({ provider: providerName, model: '' });
188
+ if (stored?.quotaWindows?.length) {
189
+ const row = { usage: { windows: stored.quotaWindows, checkedAt: stored.cachedAt } };
190
+ if (providerAccountExhausted(row, Date.now(), model)) {
191
+ throw quotaExhausted(accountsExhaustedError(false), providerName, model, [row]);
192
+ }
193
+ }
194
+ }
195
+ return account('default').send(messages, model, tools, options);
196
+ }
142
197
  const row = chooseProviderAccount(pool, attempted, Date.now(), model);
143
198
  if (!row) {
144
199
  // Once per send: a refusal built on a stale meter must not outlive the
@@ -148,8 +203,7 @@ function poolSend(providerName, account) {
148
203
  probed = true;
149
204
  if (await probeRefusedAccounts(providerName, pool, model, account)) continue;
150
205
  }
151
- if (lastError) throw lastError;
152
- throw accountsExhaustedError();
206
+ throw markExhausted(lastError || accountsExhaustedError(pool.useCredits === true));
153
207
  }
154
208
  attempted.add(row.id);
155
209
  const bound = boundAccountSend(providerName, row, messages, options);
@@ -187,28 +241,25 @@ function poolSend(providerName, account) {
187
241
  options.onStageChange?.('reconnecting', { message: 'Sign-in expired — switching to the next account' });
188
242
  continue;
189
243
  }
190
- const { exhausted, delay } = await accountQuotaExhausted(providerName, row, model, error, account);
244
+ const { exhausted, delay, evidence } = await accountQuotaExhausted(providerName, row, model, error, account);
191
245
  if (options.signal?.aborted || !exhausted) throw error;
246
+ if (evidence) evidenced.add(row.id);
192
247
  blockProviderAccount(
193
248
  providerName,
194
249
  row.id,
195
250
  Date.now() + (delay > 0 ? delay : 5 * 60_000),
196
251
  model,
197
- delay > 0 ? 'retry-after' : 'inferred'
252
+ delay > 0 ? 'retry-after' : 'inferred',
253
+ { quota: evidence === true }
198
254
  );
199
- if (
200
- pool.auto === false ||
201
- bound.emitted() ||
202
- error.unsafeToRetry ||
203
- error.liveTextEmitted ||
204
- error.emittedToolCall
205
- )
206
- throw error;
255
+ if (bound.emitted() || error.unsafeToRetry || error.liveTextEmitted || error.emittedToolCall) throw error;
256
+ // Auto switching off: the eligible set is the selected account alone.
257
+ if (pool.auto === false) throw markExhausted(error, [row.id]);
207
258
  lastError = error;
208
259
  options.onStageChange?.('reconnecting', { message: 'Quota exhausted — switching to the next account' });
209
260
  }
210
261
  }
211
- throw lastError || new Error('Provider account attempts exhausted.');
262
+ throw markExhausted(lastError || new Error('Provider account attempts exhausted.'));
212
263
  };
213
264
  }
214
265
 
@@ -14,6 +14,7 @@ import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropi
14
14
  import { applyAnthropicServerFallback } from './anthropic-server-fallback.mjs';
15
15
  import { fastModeAvailable } from './anthropic-fast-mode.mjs';
16
16
  import { applyAnthropicEffortToBody, shouldIncludeEffortBeta } from './anthropic-effort.mjs';
17
+ import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
17
18
  import {
18
19
  applyAnthropicCacheMarkers,
19
20
  clampAnthropicThinkingBudget as clampThinkingBudgetTokens,
@@ -44,7 +45,20 @@ import {
44
45
  * @param {boolean} deps.fastModeLatched the provider's sticky fast-mode beta latch
45
46
  * @returns {Promise<{ params: object, requestHeaders: object|null, knownToolNames: Set<string>, fastModeLatched: boolean }>}
46
47
  */
47
- export async function buildAnthropicApiRequest({ name, config, messages, useModel, tools, opts, fastModeLatched }) {
48
+ export async function buildAnthropicApiRequest({
49
+ name,
50
+ config,
51
+ messages,
52
+ useModel,
53
+ tools,
54
+ opts,
55
+ fastModeLatched,
56
+ modelInfo = null,
57
+ }) {
58
+ // A user-defined provider's reasoning comes from its own model row only:
59
+ // the global by-model-id capability index and the Claude-id ladders are
60
+ // first-party knowledge and must not leak onto (or from) a custom endpoint.
61
+ const isCustom = isCustomProviderName(name);
48
62
  const maxTokens = resolveMaxTokens(useModel);
49
63
  const effortProjection = projectEffortConfiguration(messages, name, useModel, {
50
64
  ...config,
@@ -81,8 +95,10 @@ export async function buildAnthropicApiRequest({ name, config, messages, useMode
81
95
  system: systemBlocks.length ? systemBlocks : undefined,
82
96
  messages: await prepareAnthropicImages(anthropicMessages, { signal: opts.signal }),
83
97
  };
98
+ // Server-side fallback and fast mode are decided from Claude model ids:
99
+ // first-party knowledge, never applied to a custom endpoint.
84
100
  applyAnthropicServerFallback(params, useModel, {
85
- enabled: config?.disableBetaHeaders !== true && opts.serverFallback !== false,
101
+ enabled: !isCustom && config?.disableBetaHeaders !== true && opts.serverFallback !== false,
86
102
  });
87
103
  if (requestTools.length) {
88
104
  // No cache_control on tools — the system BP covers tools via
@@ -105,18 +121,28 @@ export async function buildAnthropicApiRequest({ name, config, messages, useMode
105
121
  .map((t) => (t && typeof t.name === 'string' ? t.name : null))
106
122
  .filter(Boolean)
107
123
  );
108
- applyAnthropicEffortToBody(params, {
109
- model: useModel,
110
- opts: effortProjection ? { ...opts, effort: effortProjection.initialEffort } : opts,
111
- maxTokens,
112
- clampThinkingBudgetTokens,
113
- logTag: name,
114
- progressUpdates: config?.disableBetaHeaders !== true,
115
- });
124
+ if (isCustom) {
125
+ const { fields } = resolveCustomReasoning({
126
+ protocol: 'anthropic',
127
+ model: modelInfo,
128
+ effort: opts.effort ?? config?.reasoningEffort,
129
+ });
130
+ if (fields.thinking) params.thinking = fields.thinking;
131
+ if (fields.output_config) params.output_config = { ...(params.output_config || {}), ...fields.output_config };
132
+ } else {
133
+ applyAnthropicEffortToBody(params, {
134
+ model: useModel,
135
+ opts: effortProjection ? { ...opts, effort: effortProjection.initialEffort } : opts,
136
+ maxTokens,
137
+ clampThinkingBudgetTokens,
138
+ logTag: name,
139
+ progressUpdates: config?.disableBetaHeaders !== true,
140
+ });
141
+ }
116
142
  // Fast mode → speed: "fast" on models Anthropic marks as speed-capable.
117
143
  // Suppressed while the fast capacity pool is cooling down.
118
144
  let fastLatched = fastModeLatched;
119
- if (opts.fast === true && supportsAnthropicFastMode(useModel) && fastModeAvailable()) {
145
+ if (!isCustom && opts.fast === true && supportsAnthropicFastMode(useModel) && fastModeAvailable()) {
120
146
  params.speed = 'fast';
121
147
  fastLatched = true;
122
148
  }
@@ -133,19 +159,23 @@ export async function buildAnthropicApiRequest({ name, config, messages, useMode
133
159
  // too — otherwise opencode-go's anthropic-compatible routing
134
160
  // (disableBetaHeaders:true) would still send beta strings that a
135
161
  // third-party endpoint may reject.
162
+ // A custom endpoint gets no first-party base betas; only those its own
163
+ // request body needs (row-driven effort, deferred tools).
136
164
  let betaHeaders = null;
137
165
  if (!config?.disableBetaHeaders) {
138
166
  const betas = [
139
167
  buildAnthropicBetaHeaders({
168
+ ...(isCustom ? { base: '' } : {}),
140
169
  fastMode: fastLatched,
141
170
  toolSearch: hasDeferredTools,
142
- effort: shouldIncludeEffortBeta(useModel, opts),
171
+ // Custom: the beta follows the resolver's output, not the model id.
172
+ effort: isCustom ? Boolean(params.output_config?.effort) : shouldIncludeEffortBeta(useModel, opts),
143
173
  serverFallback: params.fallbacks === 'default',
144
174
  thinkingDisplayUpdates: params.thinking?.display === 'updates',
145
175
  }),
146
176
  ...(effortProjection ? [EFFORT_CONFIGURATION_BETA] : []),
147
- ];
148
- betaHeaders = { 'anthropic-beta': betas.join(',') };
177
+ ].filter(Boolean);
178
+ if (betas.length) betaHeaders = { 'anthropic-beta': betas.join(',') };
149
179
  }
150
180
  const requestHeaders =
151
181
  betaHeaders || opts.requestHeaders ? { ...(betaHeaders || {}), ...(opts.requestHeaders || {}) } : null;