mixdog 1.4.1 → 1.4.2

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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -6,6 +6,72 @@ import { identityFromTokenResponse } from './oauth-account.mjs';
6
6
  import { answerOAuthCallback } from './oauth-callback.mjs';
7
7
  import { anySignal, loginFailed, reauthenticationRequired, scopeNotGranted } from './oauth-shared.mjs';
8
8
 
9
+ // Fetch for the SDK: transport-URL normalized, no redirects, bounded by the
10
+ // login `signal`. The token endpoint's answer is read for who the tokens
11
+ // belong to (oauth-account.mjs): the SDK keeps only the token fields.
12
+ function makeSafeFetch({ fetchImpl, flow, signal }) {
13
+ return async (input, init = {}) => {
14
+ const response = await fetchImpl(normalizeMcpTransportUrl(input), {
15
+ ...init,
16
+ redirect: 'error',
17
+ signal: anySignal(signal, init.signal),
18
+ });
19
+ if (response.ok && /(^|&)grant_type=/.test(String(init.body ?? ''))) {
20
+ flow.identity = identityFromTokenResponse(
21
+ await response
22
+ .clone()
23
+ .json()
24
+ .catch(() => null)
25
+ );
26
+ }
27
+ return response;
28
+ };
29
+ }
30
+
31
+ // The caller's progress hooks (mcp-oauth-login.mjs): the callback arrived,
32
+ // then the tokens are stored.
33
+ function reportProgress(browser, hook) {
34
+ try {
35
+ browser?.[hook]?.();
36
+ } catch {
37
+ /* progress reporting never fails the login */
38
+ }
39
+ }
40
+
41
+ // The callback promise of one login, and the browser page held open for the
42
+ // loopback callback until the code exchange and scope check end: `settle()` is
43
+ // success, `settle({ code })` a failure. Bounded by `signal`, like the
44
+ // exchange itself. `setFlushing` receives each answered page's flush.
45
+ function awaitCallback({ flow, signal, stopListening, setFlushing }) {
46
+ let abort;
47
+ let respond;
48
+ const settle = (failure) => {
49
+ const answer = respond;
50
+ respond = undefined;
51
+ if (!answer) return undefined;
52
+ const flushed = answer(failure);
53
+ setFlushing(flushed);
54
+ return flushed;
55
+ };
56
+ const callback = new Promise((resolve, reject) => {
57
+ const finish = (code, page) => {
58
+ if (page) respond = page;
59
+ return code ? resolve(code) : reject(loginFailed());
60
+ };
61
+ flow.finishCallback = finish;
62
+ // Timeout, cancel or abort: the held page gets the generic failure now
63
+ // (the exchange or a credential write may be stalled past the deadline),
64
+ // and the listener closes once it is flushed.
65
+ abort = () => {
66
+ finish();
67
+ settle({});
68
+ stopListening();
69
+ };
70
+ signal.addEventListener('abort', abort, { once: true });
71
+ });
72
+ return { callback, settle, abort };
73
+ }
74
+
9
75
  /**
10
76
  * The browser login of one connection over its credential `cache` and login
11
77
  * `flow` state: the loopback callback listener and the SDK authorization
@@ -65,61 +131,19 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
65
131
  // the token fields, so the token endpoint's answer is read here.
66
132
  flow.identity = undefined;
67
133
  flow.signal = signal;
68
- const safeFetch = async (input, init = {}) => {
69
- const response = await fetchImpl(normalizeMcpTransportUrl(input), {
70
- ...init,
71
- redirect: 'error',
72
- signal: anySignal(signal, init.signal),
73
- });
74
- if (response.ok && /(^|&)grant_type=/.test(String(init.body ?? ''))) {
75
- flow.identity = identityFromTokenResponse(
76
- await response
77
- .clone()
78
- .json()
79
- .catch(() => null)
80
- );
81
- }
82
- return response;
83
- };
84
- // The caller's progress hooks (mcp-oauth-login.mjs): the callback arrived,
85
- // then the tokens are stored.
86
- const progress = (hook) => {
87
- try {
88
- browser?.[hook]?.();
89
- } catch {
90
- /* progress reporting never fails the login */
91
- }
92
- };
93
- let abort;
94
- // The browser page held open for the loopback callback until the code
95
- // exchange and scope check end: `settle()` is success, `settle({ code })`
96
- // a failure. Bounded by `signal`, like the exchange itself.
97
- let respond;
98
- const settle = (failure) => {
99
- const answer = respond;
100
- respond = undefined;
101
- if (!answer) return undefined;
102
- flushing = answer(failure);
103
- return flushing;
104
- };
105
- const callback = new Promise((resolve, reject) => {
106
- const finish = (code, page) => {
107
- if (page) respond = page;
108
- return code ? resolve(code) : reject(loginFailed());
109
- };
110
- flow.finishCallback = finish;
111
- // Timeout, cancel or abort: the held page gets the generic failure now
112
- // (the exchange or a credential write may be stalled past the deadline),
113
- // and the listener closes once it is flushed.
114
- abort = () => {
115
- finish();
116
- settle({});
117
- stopListening();
118
- };
119
- signal.addEventListener('abort', abort, { once: true });
134
+ const safeFetch = makeSafeFetch({ fetchImpl, flow, signal });
135
+ const progress = (hook) => reportProgress(browser, hook);
136
+ const { callback, settle, abort } = awaitCallback({
137
+ flow,
138
+ signal,
139
+ stopListening,
140
+ setFlushing: (page) => {
141
+ flushing = page;
142
+ },
120
143
  });
121
144
  // A discovery/registration error can precede waiting for the callback.
122
145
  callback.catch(() => {});
146
+ let codeReceived = false;
123
147
  try {
124
148
  signal.throwIfAborted();
125
149
  const { auth } = await import('@modelcontextprotocol/sdk/client/auth.js');
@@ -141,6 +165,7 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
141
165
  });
142
166
  if (result === 'REDIRECT') {
143
167
  const code = await callback;
168
+ codeReceived = true;
144
169
  signal.throwIfAborted();
145
170
  progress('onCallback');
146
171
  const exchanged = await cache.withLock(async () => {
@@ -157,6 +182,12 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
157
182
  await settle();
158
183
  } catch (error) {
159
184
  await settle({ code: error?.code });
185
+ // An authorization server that no longer knows our client_id answers the
186
+ // browser with an HTTP error, never a redirect or a token-endpoint
187
+ // invalid_client, so the SDK cannot see it: register again next time.
188
+ if (interactive && flow.browserOpened && !codeReceived && !provider.preRegistered) {
189
+ await provider.invalidateCredentials('client').catch(() => {});
190
+ }
160
191
  // SDK errors may contain authorization server response bodies/secrets.
161
192
  if (error?.code === 'reauthenticationRequired' || !interactive) throw reauthenticationRequired();
162
193
  // Our own error names scopes only (no token data): the one to pass on.
@@ -50,28 +50,33 @@ export function isMcpOAuthScopeNotGranted(error) {
50
50
  return false;
51
51
  }
52
52
 
53
- // One keychain entry per exact server URL. Windows stores each account as a
54
- // file name, so the account is a hash of the URL and nothing else.
55
- export function mcpOAuthAccount(url) {
56
- return `mcp-oauth.${createHash('sha256').update(url).digest('hex')}`;
53
+ // One keychain entry per credential key: the exact server URL, or a connector
54
+ // account's own key (client-config.mjs resolveMcpOAuthCredentialKey). Windows
55
+ // stores each account as a file name, so the account is a hash of the key and
56
+ // nothing else.
57
+ export function mcpOAuthAccount(key) {
58
+ return `mcp-oauth.${createHash('sha256').update(key).digest('hex')}`;
57
59
  }
58
60
 
59
- /** Whether OAuth tokens are stored for one server URL, read from the
60
- * non-secret revision marker without touching the keychain. */
61
- export function hasStoredMcpOAuthTokens(url) {
61
+ /** What one sign-in is stored under: `credentialKey`, else the normalized URL. */
62
+ const credentialIdOf = (url, credentialKey) => credentialKey || normalizeMcpTransportUrl(url);
63
+
64
+ /** Whether OAuth tokens are stored for one server URL (or `credentialKey`),
65
+ * read from the non-secret revision marker without touching the keychain. */
66
+ export function hasStoredMcpOAuthTokens(url, credentialKey) {
62
67
  try {
63
- const marker = readFileSync(credentialFile(mcpOAuthAccount(normalizeMcpTransportUrl(url)), 'rev'), 'utf8');
68
+ const marker = readFileSync(credentialFile(mcpOAuthAccount(credentialIdOf(url, credentialKey)), 'rev'), 'utf8');
64
69
  return marker.endsWith(TOKENS_FLAG);
65
70
  } catch {
66
71
  return false;
67
72
  }
68
73
  }
69
74
 
70
- /** Who the stored sign-in of one MCP server URL belongs to, without a keychain
71
- * read: `{ account, workspace, forcedChoice }` (empty strings when the server
72
- * named nobody), or null when signed out. */
73
- export function storedMcpOAuthIdentity(url) {
74
- return readIdentityFile(mcpOAuthAccount(normalizeMcpTransportUrl(url)));
75
+ /** Who the stored sign-in of one MCP server URL (or `credentialKey`) belongs
76
+ * to, without a keychain read: `{ account, workspace, forcedChoice }` (empty
77
+ * strings when the server named nobody), or null when signed out. */
78
+ export function storedMcpOAuthIdentity(url, credentialKey) {
79
+ return readIdentityFile(mcpOAuthAccount(credentialIdOf(url, credentialKey)));
75
80
  }
76
81
 
77
82
  // RFC 7009 revocation of the stored grant, only where the authorization server
@@ -114,9 +119,15 @@ async function revokeStoredGrant(saved, fetchImpl) {
114
119
  * explicit reconnect to log in again. With `revoke`, the grant is first
115
120
  * revoked at the authorization server where it documents a revocation
116
121
  * endpoint. Resolves `{ revoked }`. */
117
- export async function clearMcpOAuth({ url, credentialStore = keychain, revoke = false, fetchImpl = fetch }) {
118
- url = normalizeMcpTransportUrl(url);
119
- const account = mcpOAuthAccount(url);
122
+ export async function clearMcpOAuth({
123
+ url,
124
+ credentialKey,
125
+ credentialStore = keychain,
126
+ revoke = false,
127
+ fetchImpl = fetch,
128
+ }) {
129
+ const id = credentialIdOf(url, credentialKey);
130
+ const account = mcpOAuthAccount(id);
120
131
  let revoked = false;
121
132
  await withCredentialLock(account, async () => {
122
133
  const saved = await readStoredCredentials(credentialStore, account);
@@ -125,7 +136,7 @@ export async function clearMcpOAuth({ url, credentialStore = keychain, revoke =
125
136
  if (saved != null) await credentialStore.deleteSecretAsync(account);
126
137
  await writeIdentityFile(account, null);
127
138
  });
128
- scopes.get(credentialStore)?.get(url)?.oauth.clear();
139
+ scopes.get(credentialStore)?.get(id)?.oauth.clear();
129
140
  return { revoked };
130
141
  }
131
142
 
@@ -151,6 +162,7 @@ async function storeEntry(account, url, value, credentialStore) {
151
162
  */
152
163
  export function startMcpOAuthSwitch({
153
164
  url,
165
+ credentialKey,
154
166
  launch,
155
167
  serverName,
156
168
  credentialStore = keychain,
@@ -209,7 +221,7 @@ export function startMcpOAuthSwitch({
209
221
  timeoutMs,
210
222
  });
211
223
  const done = (async () => {
212
- const saved = await readStoredCredentials(credentialStore, mcpOAuthAccount(url));
224
+ const saved = await readStoredCredentials(credentialStore, mcpOAuthAccount(credentialIdOf(url, credentialKey)));
213
225
  if (saved) {
214
226
  const value = JSON.parse(saved);
215
227
  if (value.url === url) {
@@ -244,9 +256,9 @@ export function startMcpOAuthSwitch({
244
256
  /** Store the sign-in a switch returned as the URL's sign-in; resolves a
245
257
  * `rollback()` that restores the previous one (or none). Live connections
246
258
  * adopt either through the revision marker on their next request. */
247
- export async function commitMcpOAuthSwitch({ url, signIn, credentialStore = keychain }) {
259
+ export async function commitMcpOAuthSwitch({ url, credentialKey, signIn, credentialStore = keychain }) {
248
260
  url = normalizeMcpTransportUrl(url);
249
- const account = mcpOAuthAccount(url);
261
+ const account = mcpOAuthAccount(credentialIdOf(url, credentialKey));
250
262
  let previous = null;
251
263
  await withCredentialLock(account, async () => {
252
264
  previous = await readStoredCredentials(credentialStore, account);
@@ -262,9 +274,11 @@ export async function commitMcpOAuthSwitch({ url, signIn, credentialStore = keyc
262
274
  }
263
275
 
264
276
  // The keychain is the only durable store. Exact URLs, including query strings,
265
- // partition clients and tokens even when servers share an authorization issuer.
277
+ // partition clients and tokens even when servers share an authorization issuer;
278
+ // a `credentialKey` (a connector account's own) partitions them at one URL.
266
279
  export function createMcpOAuth({
267
280
  url,
281
+ credentialKey,
268
282
  credentialStore = keychain,
269
283
  interactive = false,
270
284
  isTransportEndpoint = () => false,
@@ -272,15 +286,16 @@ export function createMcpOAuth({
272
286
  ...options
273
287
  }) {
274
288
  url = normalizeMcpTransportUrl(url);
289
+ const id = credentialIdOf(url, credentialKey);
275
290
  let entries = scopes.get(credentialStore);
276
291
  if (!entries) {
277
292
  entries = new Map();
278
293
  scopes.set(credentialStore, entries);
279
294
  }
280
- let entry = entries.get(url);
295
+ let entry = entries.get(id);
281
296
  if (!entry) {
282
- entry = { oauth: createSharedOAuth({ url, credentialStore, ...options }), users: 0 };
283
- entries.set(url, entry);
297
+ entry = { oauth: createSharedOAuth({ url, account: mcpOAuthAccount(id), credentialStore, ...options }), users: 0 };
298
+ entries.set(id, entry);
284
299
  }
285
300
  entry.users++;
286
301
  const lifetime = new AbortController();
@@ -346,7 +361,7 @@ export function createMcpOAuth({
346
361
  lifetime.abort();
347
362
  if (--entry.users === 0) {
348
363
  entry.oauth.close();
349
- entries.delete(url);
364
+ entries.delete(id);
350
365
  }
351
366
  },
352
367
  };
@@ -354,6 +369,7 @@ export function createMcpOAuth({
354
369
 
355
370
  function createSharedOAuth({
356
371
  url,
372
+ account,
357
373
  credentialStore,
358
374
  openBrowser = openInBrowser,
359
375
  fetchImpl = fetch,
@@ -361,7 +377,7 @@ function createSharedOAuth({
361
377
  oauthClient = null,
362
378
  }) {
363
379
  const lifetime = new AbortController();
364
- const cache = createCredentialCache({ url, credentialStore, account: mcpOAuthAccount(url) });
380
+ const cache = createCredentialCache({ url, credentialStore, account });
365
381
  const { creds } = cache;
366
382
  const flow = {
367
383
  listener: undefined,
@@ -13,6 +13,7 @@ import {
13
13
  providerAccountWithBlocks,
14
14
  providerAccountBlockedUntil,
15
15
  staleQuotaWindows,
16
+ isCustomKeyProvider,
16
17
  } from '../../../shared/provider-accounts.mjs';
17
18
  import { withProviderAccount, hasExplicitProviderAuthBinding } from '../../../shared/provider-auth-binding.mjs';
18
19
  import { isExplicitUserAbortError, markProviderRecoveryExhausted } from './retry-classifier.mjs';
@@ -161,8 +162,11 @@ function quotaExhausted(error, providerName, model, rows, options = {}) {
161
162
  // caller does not subscribe, and account-bound connection/delta state stays
162
163
  // apart without changing the visible Mixdog session, so no stale server state
163
164
  // crosses an account switch. `emitted()` reports whether output reached the
164
- // caller.
165
- function boundAccountSend(providerName, row, messages, options) {
165
+ // caller. `servedBy` names the account a history message or provider state
166
+ // came from; only that account's replay and state are passed on.
167
+ const replayAccount = (entry) => entry?.providerReplay?.accountId ?? entry?.providerAccountId;
168
+
169
+ function boundAccountSend(providerName, row, messages, options, servedBy = replayAccount) {
166
170
  let emitted = false;
167
171
  const opts = { ...options };
168
172
  for (const key of ['onStreamDelta', 'onTextDelta', 'onToolCall', 'onThinkingDelta']) {
@@ -178,9 +182,9 @@ function boundAccountSend(providerName, row, messages, options) {
178
182
  // The spend still belongs to the visible session (usage-accounting.mjs).
179
183
  opts.usageSessionId = options.usageSessionId || options.sessionId;
180
184
  }
181
- opts.providerState = options.providerState?.providerAccountId === row.id ? options.providerState : undefined;
185
+ opts.providerState = servedBy(options.providerState) === row.id ? options.providerState : undefined;
182
186
  const history = messages.map((message) => {
183
- if (message.providerReplay?.accountId === row.id) return message;
187
+ if (servedBy(message) === row.id) return message;
184
188
  const { providerReplay, reasoning, thinkingBlocks, ...plain } = message;
185
189
  return plain;
186
190
  });
@@ -204,7 +208,7 @@ async function commitFallbackAccount(providerName, pool, row, model, refusalScop
204
208
  // the binding survives a restart and eviction from this map, which only
205
209
  // spares the history walk. Bounded, oldest binding first out.
206
210
  const SESSION_ACCOUNT_LIMIT = 1000;
207
- const ROUTING_METADATA_KEY = 'accountRouting';
211
+ export const ROUTING_METADATA_KEY = 'accountRouting';
208
212
  const sessionAccounts = new Map();
209
213
 
210
214
  // Forget every in-memory binding, as a restart does (tests).
@@ -520,7 +524,7 @@ function rotationReason(evidence, accountBlocked) {
520
524
 
521
525
  // The pool's send: walks the eligible accounts until one answers, blocking
522
526
  // the ones whose quota window is exhausted along the way.
523
- function poolSend(providerName, account) {
527
+ function poolSend(providerName, account, servedBy) {
524
528
  return async function send(messages, model, tools, options = {}) {
525
529
  const request = { providerName, model, account, options };
526
530
  // This send's record of the roster: the accounts it tried, those it found
@@ -541,7 +545,8 @@ function poolSend(providerName, account) {
541
545
  options.signal?.throwIfAborted();
542
546
  const pool = readProviderAccountPool(providerName);
543
547
  if (!pool.accounts.length) {
544
- throwIfPlanQuotaSpent(providerName, pool, model);
548
+ // A custom provider's key has no plan meter to consult.
549
+ if (!isCustomKeyProvider(providerName)) throwIfPlanQuotaSpent(providerName, pool, model);
545
550
  return account('default').send(messages, model, tools, options);
546
551
  }
547
552
  const policy = providerAccountPolicy(pool);
@@ -559,7 +564,7 @@ function poolSend(providerName, account) {
559
564
  }
560
565
  ledger.attempted.add(row.id);
561
566
  sends += 1;
562
- const bound = boundAccountSend(providerName, row, messages, options);
567
+ const bound = boundAccountSend(providerName, row, messages, options, servedBy);
563
568
  try {
564
569
  const result = await account(row.id).send(bound.history, model, tools, bound.opts);
565
570
  quotaStrikes.delete(`${providerName}:${row.id}`);
@@ -660,3 +665,33 @@ export function createAccountPoolProvider(providerName, create) {
660
665
  }
661
666
  );
662
667
  }
668
+
669
+ // Every key of a custom provider reaches one endpoint, so its answers carry
670
+ // no key-bound server state but what an answer's routing names: history from
671
+ // before the key pool (or from no pooled send) is the primary key's.
672
+ function customKeyServedBy(providerName) {
673
+ return (entry) => {
674
+ const routing = entry?.providerMetadata?.[ROUTING_METADATA_KEY];
675
+ if (routing?.provider === providerName && routing.accountId) return routing.accountId;
676
+ return replayAccount(entry) || 'default';
677
+ };
678
+ }
679
+
680
+ /**
681
+ * A custom provider's API keys as one roster (provider-accounts.mjs
682
+ * `setCustomProviderKeyIds`): `primary` is the `default` account and answers
683
+ * everything but `send`; `keyEngine(id)` is the engine sending with
684
+ * additional key `id`. Without additional keys the roster is empty and every
685
+ * send is the primary's own, unchanged.
686
+ */
687
+ export function createCustomKeyPoolProvider(providerName, primary, keyEngine) {
688
+ const engines = new Map([['default', primary]]);
689
+ const account = (id) => {
690
+ if (!engines.has(id)) engines.set(id, keyEngine(id));
691
+ return engines.get(id);
692
+ };
693
+ const send = poolSend(providerName, account, customKeyServedBy(providerName));
694
+ return new Proxy(primary, {
695
+ get: (target, key, receiver) => (key === 'send' ? send : Reflect.get(target, key, receiver)),
696
+ });
697
+ }
@@ -189,8 +189,7 @@ export async function buildAnthropicApiRequest({
189
189
  // Diagnostic: per-block prefix hashes of exactly what is sent.
190
190
  recordAnthropicPrefix(anthropicPrefixSessionId(opts), anthropicPrefixFingerprint(params));
191
191
 
192
- const requestHeaders = requestHeadersFor(params, {
193
- config,
192
+ const betas = requestBetasFor(params, {
194
193
  opts,
195
194
  isCustom,
196
195
  fastLatched,
@@ -198,8 +197,10 @@ export async function buildAnthropicApiRequest({
198
197
  capabilityModel,
199
198
  effortProjection,
200
199
  });
200
+ const requestHeaders = requestHeadersFor(betas, { config, opts });
201
201
 
202
- return { params, requestHeaders, knownToolNames, fastModeLatched: fastLatched };
202
+ // `betas`: the selected beta names, whether or not this route sends them as a header.
203
+ return { params, requestHeaders, betas, knownToolNames, fastModeLatched: fastLatched };
203
204
  }
204
205
 
205
206
  /** The turn's thinking/effort fields on `params` (in place): a custom
@@ -247,35 +248,36 @@ function resolveRequestToolChoice(params, opts, config) {
247
248
  else delete params.tool_choice;
248
249
  }
249
250
 
250
- /** The per-request headers of the final body `params`: the anthropic-beta
251
- * set it needs, then the caller's `opts.requestHeaders`; null when none. */
252
- function requestHeadersFor(
253
- params,
254
- { config, opts, isCustom, fastLatched, hasDeferredTools, capabilityModel, effortProjection }
255
- ) {
251
+ /** The anthropic-beta names the final body `params` needs. A custom endpoint
252
+ * gets no first-party base betas; only those its own request body needs
253
+ * (row-driven effort, deferred tools). */
254
+ function requestBetasFor(params, { opts, isCustom, fastLatched, hasDeferredTools, capabilityModel, effortProjection }) {
255
+ return [
256
+ buildAnthropicBetaHeaders({
257
+ ...(isCustom ? { base: '' } : {}),
258
+ fastMode: fastLatched,
259
+ toolSearch: hasDeferredTools,
260
+ // Custom: the beta follows the resolver's output, not the model id.
261
+ effort: isCustom ? Boolean(params.output_config?.effort) : shouldIncludeEffortBeta(capabilityModel, opts),
262
+ serverFallback: params.fallbacks === 'default',
263
+ thinkingDisplayUpdates: params.thinking?.display === 'updates',
264
+ turnScopedSystem: hasAnthropicTurnScopedMessages(params.messages),
265
+ }),
266
+ ...(effortProjection ? [EFFORT_CONFIGURATION_BETA] : []),
267
+ ]
268
+ .join(',')
269
+ .split(',')
270
+ .filter(Boolean);
271
+ }
272
+
273
+ /** The per-request headers: the anthropic-beta header of `betas`, then the
274
+ * caller's `opts.requestHeaders`; null when none. */
275
+ function requestHeadersFor(betas, { config, opts }) {
256
276
  // Per-call headers override the client defaultHeaders, so the
257
277
  // constructor-level disableBetaHeaders opt-out must be honoured here
258
278
  // too — otherwise opencode-go's anthropic-compatible routing
259
279
  // (disableBetaHeaders:true) would still send beta strings that a
260
280
  // third-party endpoint may reject.
261
- // A custom endpoint gets no first-party base betas; only those its own
262
- // request body needs (row-driven effort, deferred tools).
263
- let betaHeaders = null;
264
- if (!config?.disableBetaHeaders) {
265
- const betas = [
266
- buildAnthropicBetaHeaders({
267
- ...(isCustom ? { base: '' } : {}),
268
- fastMode: fastLatched,
269
- toolSearch: hasDeferredTools,
270
- // Custom: the beta follows the resolver's output, not the model id.
271
- effort: isCustom ? Boolean(params.output_config?.effort) : shouldIncludeEffortBeta(capabilityModel, opts),
272
- serverFallback: params.fallbacks === 'default',
273
- thinkingDisplayUpdates: params.thinking?.display === 'updates',
274
- turnScopedSystem: hasAnthropicTurnScopedMessages(params.messages),
275
- }),
276
- ...(effortProjection ? [EFFORT_CONFIGURATION_BETA] : []),
277
- ].filter(Boolean);
278
- if (betas.length) betaHeaders = { 'anthropic-beta': betas.join(',') };
279
- }
281
+ const betaHeaders = !config?.disableBetaHeaders && betas.length ? { 'anthropic-beta': betas.join(',') } : null;
280
282
  return betaHeaders || opts.requestHeaders ? { ...(betaHeaders || {}), ...(opts.requestHeaders || {}) } : null;
281
283
  }
@@ -10,7 +10,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
10
10
  // source release just to advance the user-agent version.
11
11
  // Claude Opus 5.5 requires 2.1.280 or newer. The release workflow raises this
12
12
  // to the published CLI version (scripts/sync-client-version-floors.mjs).
13
- export const DEFAULT_CLI_VERSION = '2.1.311';
13
+ export const DEFAULT_CLI_VERSION = '2.1.312';
14
14
  export const CLAUDE_CLI_NPM_PACKAGE = '@anthropic-ai/claude-code';
15
15
 
16
16
  const CACHE_SCHEMA_VERSION = 1;
@@ -22,7 +22,12 @@ import {
22
22
  expiryFromAccessToken,
23
23
  isDefinitiveOAuthFailure,
24
24
  normalizeAccountIdentity,
25
+ oauthBodySummary,
25
26
  oauthCredentialStatus,
27
+ oauthFailureReason,
28
+ oauthPublicError,
29
+ oauthRequestError,
30
+ oauthResponseJson,
26
31
  scrubOAuthSecrets,
27
32
  } from './lib/oauth-token-utils.mjs';
28
33
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
@@ -271,7 +276,7 @@ export function _scrubTokens(text, secretValues = []) {
271
276
  }
272
277
 
273
278
  function _tokenEndpointError(operation, status, text, secretValues = []) {
274
- const safeDetail = _scrubTokens(text, secretValues).replace(/\s+/g, ' ').trim().slice(0, 500);
279
+ const safeDetail = oauthBodySummary(text, secretValues);
275
280
  const detail = safeDetail ? `: ${safeDetail}` : '';
276
281
  const compatibility =
277
282
  status === 403
@@ -282,7 +287,7 @@ function _tokenEndpointError(operation, status, text, secretValues = []) {
282
287
  ' update Mixdog or set MIXDOG_CLI_VERSION to the installed official Claude Code version;' +
283
288
  ' then retry sign-in via /providers without an intercepting network layer if applicable.'
284
289
  : '';
285
- return new Error(`${operation} ${status}${detail}.${compatibility}`);
290
+ return oauthPublicError(new Error(`${operation} ${status}${detail}.${compatibility}`));
286
291
  }
287
292
 
288
293
  export function isAnthropicOAuthRefreshDisabled() {
@@ -578,21 +583,26 @@ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri, signa
578
583
  const cleanCode = String(code || '').trim();
579
584
  if (!cleanCode) throw new Error('[anthropic-oauth] authorization code is required');
580
585
  const timeoutSignal = AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS);
581
- const tokenRes = await fetch(TOKEN_URL, {
582
- method: 'POST',
583
- headers: tokenEndpointHeaders(),
584
- body: JSON.stringify({
585
- grant_type: 'authorization_code',
586
- code: cleanCode,
587
- redirect_uri: redirectUri,
588
- client_id: CLAUDE_CODE_CLIENT_ID,
589
- code_verifier: pkce.verifier,
590
- state,
591
- }),
592
- redirect: 'error',
593
- signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
594
- dispatcher: getLlmDispatcher(),
595
- });
586
+ let tokenRes;
587
+ try {
588
+ tokenRes = await fetch(TOKEN_URL, {
589
+ method: 'POST',
590
+ headers: tokenEndpointHeaders(),
591
+ body: JSON.stringify({
592
+ grant_type: 'authorization_code',
593
+ code: cleanCode,
594
+ redirect_uri: redirectUri,
595
+ client_id: CLAUDE_CODE_CLIENT_ID,
596
+ code_verifier: pkce.verifier,
597
+ state,
598
+ }),
599
+ redirect: 'error',
600
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
601
+ dispatcher: getLlmDispatcher(),
602
+ });
603
+ } catch (err) {
604
+ throw oauthRequestError('[anthropic-oauth] token exchange', err);
605
+ }
596
606
  if (!tokenRes.ok) {
597
607
  const text = await tokenRes.text().catch(() => '');
598
608
  throw _tokenEndpointError('[anthropic-oauth] token exchange', tokenRes.status, text, [
@@ -601,7 +611,7 @@ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri, signa
601
611
  state,
602
612
  ]);
603
613
  }
604
- const json = await tokenRes.json();
614
+ const json = await oauthResponseJson(tokenRes, '[anthropic-oauth] token exchange');
605
615
  const accessToken = json?.access_token || json?.accessToken;
606
616
  const refreshToken = json?.refresh_token || json?.refreshToken;
607
617
  if (!accessToken || !refreshToken) {
@@ -727,10 +737,10 @@ export async function beginOAuthLogin({ openBrowserFn = null, openBrowser = true
727
737
  await acceptCode(code, OAUTH_REDIRECT_URI);
728
738
  res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
729
739
  res.end(oauthSuccessHtml('Claude'));
730
- } catch {
731
- // The shared exchange reports its error through waitForCallback.
740
+ } catch (err) {
741
+ // The shared exchange also reports its error through waitForCallback.
732
742
  res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
733
- res.end(oauthErrorHtml('Claude could not complete the token exchange.'));
743
+ res.end(oauthErrorHtml(oauthFailureReason('Claude could not complete the token exchange.', err)));
734
744
  }
735
745
  });
736
746
  timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
@@ -266,7 +266,9 @@ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetc
266
266
  });
267
267
  if (!res.ok) {
268
268
  const text = await res.text().catch(() => '');
269
- throw new Error(`[antigravity-oauth] token exchange ${res.status}: ${_scrubTokens(text).slice(0, 300)}`);
269
+ // The authorization code is redacted by value: `code` alone is not a secret field.
270
+ const detail = _scrubTokens(text, [cleanCode, verifier]).slice(0, 300);
271
+ throw new Error(`[antigravity-oauth] token exchange ${res.status}: ${detail}`);
270
272
  }
271
273
  const json = await res.json();
272
274
  if (!json?.access_token || !json?.refresh_token) {
@@ -231,8 +231,8 @@ export function saveTokens(tokens) {
231
231
  );
232
232
  }
233
233
 
234
- export function _scrubTokens(text) {
235
- return scrubOAuthSecrets(text);
234
+ export function _scrubTokens(text, secretValues = []) {
235
+ return scrubOAuthSecrets(text, secretValues);
236
236
  }
237
237
 
238
238
  export function hasAntigravityOAuthCredentials() {
@@ -20,7 +20,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
20
20
  // version (gpt-6.1-sol requires >= 0.159.0, measured 2026-10-09), so the
21
21
  // release workflow raises this to the published CLI version on every release
22
22
  // (scripts/sync-client-version-floors.mjs).
23
- export const CODEX_CLIENT_VERSION_FLOOR = '0.164.0';
23
+ export const CODEX_CLIENT_VERSION_FLOOR = '0.165.0';
24
24
  export const CODEX_CLI_NPM_PACKAGE = '@openai/codex';
25
25
  const live = createNpmVersionSource(CODEX_CLI_NPM_PACKAGE, { persistKey: 'codex-cli' });
26
26
 
@@ -361,7 +361,7 @@ export class CommandCodeOAuthProvider {
361
361
  if (Array.isArray(body?.data)) list = body.data;
362
362
  else if (Array.isArray(body)) list = body;
363
363
  const items = list.filter((item) => modelWire(item));
364
- publish(items.map(rosterRow));
364
+ await publish(items.map(rosterRow));
365
365
  if (this.tokens?.apiKey === apiKey) {
366
366
  // Rows accumulate for one key: a later roster that misses a model
367
367
  // leaves the endpoints it was last listed with.