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
@@ -1,7 +1,31 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { getPluginData } from '../config.mjs';
4
- import { updateJsonAtomicSync, writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
4
+ import { updateJsonAtomic, writeJsonAtomicAsync } from '../../../shared/atomic-file.mjs';
5
+
6
+ // Disk writes never block the event loop: they run on the async atomic-file
7
+ // primitives, one at a time per cache file, in the order they were requested
8
+ // (a later write is never overtaken by an earlier one finishing late). Every
9
+ // instance of one file shares these module-level tables, since several
10
+ // instances and several providers can address the same path.
11
+ const writeTails = new Map(); // path → tail of that file's write chain
12
+ // path → JSON text of the newest save() whose write has not finished. Sync
13
+ // readers see it, so a save is visible at once although its disk write is not.
14
+ const unsettledSaves = new Map();
15
+
16
+ function enqueueWrite(p, task) {
17
+ const run = (writeTails.get(p) ?? Promise.resolve()).then(task);
18
+ writeTails.set(p, run);
19
+ run.then(() => {
20
+ if (writeTails.get(p) === run) writeTails.delete(p);
21
+ });
22
+ return run;
23
+ }
24
+
25
+ /** Resolves once every cache write requested so far has settled (shutdown, tests). */
26
+ export async function flushModelCacheWrites() {
27
+ while (writeTails.size) await Promise.all([...writeTails.values()]);
28
+ }
5
29
 
6
30
  // Shared model-catalog disk-cache CRUD, parameterized over the small bits each
7
31
  // provider varies: cache file name, TTL, an optional schema-version gate, and an
@@ -15,9 +39,10 @@ export function makeModelCache({ fileName, ttlMs, version = null, onSave = null
15
39
  // allowStale skips the TTL only; the schema-version gate still applies.
16
40
  function loadEntrySync({ allowStale = false } = {}) {
17
41
  const p = path();
18
- if (!existsSync(p)) return null;
42
+ const unsettled = unsettledSaves.get(p);
43
+ if (unsettled === undefined && !existsSync(p)) return null;
19
44
  try {
20
- const raw = JSON.parse(readFileSync(p, 'utf-8'));
45
+ const raw = JSON.parse(unsettled ?? readFileSync(p, 'utf-8'));
21
46
  if (version != null && raw?.version !== version) return null;
22
47
  if (!raw?.fetchedAt || !Array.isArray(raw.models)) return null;
23
48
  if (!allowStale && Date.now() - raw.fetchedAt > ttlMs) return null;
@@ -31,56 +56,98 @@ export function makeModelCache({ fileName, ttlMs, version = null, onSave = null
31
56
  return loadEntrySync(options)?.models ?? null;
32
57
  }
33
58
 
59
+ // The in-memory state (sync readers, onSave mirror) updates at once; the
60
+ // returned promise settles, never rejecting, when the disk write has.
34
61
  function save(models, extra = {}) {
62
+ const p = path();
63
+ // The value as of this call: later mutation of `models` must not change
64
+ // what lands on disk (the old synchronous write captured it the same way).
65
+ let text;
66
+ let record;
67
+ try {
68
+ text = JSON.stringify({
69
+ ...extra,
70
+ ...(version != null ? { version } : {}),
71
+ fetchedAt: Date.now(),
72
+ models,
73
+ });
74
+ record = JSON.parse(text);
75
+ unsettledSaves.set(p, text);
76
+ } catch {
77
+ return Promise.resolve();
78
+ }
35
79
  try {
36
- writeJsonAtomicSync(
37
- path(),
38
- {
39
- ...extra,
40
- ...(version != null ? { version } : {}),
41
- fetchedAt: Date.now(),
42
- models,
43
- },
44
- { lock: true, fsyncDir: true }
45
- );
46
80
  if (onSave) onSave(models);
47
81
  } catch {
48
82
  /* best-effort */
49
83
  }
84
+ return enqueueWrite(p, async () => {
85
+ try {
86
+ await writeJsonAtomicAsync(p, record, { lock: true, fsyncDir: true });
87
+ } catch {
88
+ /* best-effort */
89
+ } finally {
90
+ if (unsettledSaves.get(p) === text) unsettledSaves.delete(p);
91
+ }
92
+ });
50
93
  }
51
94
 
52
95
  // A discovery request claims a generation when it starts. Its roster is
53
96
  // written only if no newer request started meanwhile — in this process
54
97
  // (generation) or another one (`requestedAt` checked under the file lock) —
55
98
  // and `isCurrent()` (the credential it was fetched with is still active) holds.
99
+ // The returned publish() resolves, never rejecting, to whether the roster was
100
+ // written; the cross-process check runs under the file lock, so the answer
101
+ // is only known once the queued write has run.
56
102
  let generation = 0;
57
103
  function beginSave(isCurrent) {
58
104
  const claimed = ++generation;
59
105
  const requestedAt = Date.now();
106
+ const stillWanted = () => claimed === generation && isCurrent();
60
107
  return (models, extra = {}) => {
61
- if (claimed !== generation || !isCurrent()) return false;
62
- let written = false;
108
+ if (!stillWanted()) return Promise.resolve(false);
109
+ const p = path();
110
+ // The roster as of this call, not as mutated while the write waits.
111
+ let snapshot;
63
112
  try {
64
- updateJsonAtomicSync(
65
- path(),
66
- (cur) => {
67
- if (Number(cur?.requestedAt) > requestedAt) return undefined;
68
- written = true;
69
- return {
70
- ...extra,
71
- ...(version != null ? { version } : {}),
72
- fetchedAt: Date.now(),
73
- requestedAt,
74
- models,
75
- };
76
- },
77
- { fsyncDir: true }
78
- );
113
+ snapshot = JSON.parse(JSON.stringify({ extra, models }));
79
114
  } catch {
80
- /* best-effort */
115
+ return Promise.resolve(false);
81
116
  }
82
- if (written && onSave) onSave(models);
83
- return written;
117
+ return enqueueWrite(p, async () => {
118
+ // A newer request may have started while this write waited its turn.
119
+ if (!stillWanted()) return false;
120
+ let written = false;
121
+ try {
122
+ await updateJsonAtomic(
123
+ p,
124
+ (cur) => {
125
+ if (Number(cur?.requestedAt) > requestedAt) return undefined;
126
+ // The lock wait is asynchronous: a newer request or a credential
127
+ // change may have landed since the checks above. Re-decide now,
128
+ // right before accepting the data.
129
+ if (!stillWanted()) return undefined;
130
+ written = true;
131
+ return {
132
+ ...snapshot.extra,
133
+ ...(version != null ? { version } : {}),
134
+ fetchedAt: Date.now(),
135
+ requestedAt,
136
+ models: snapshot.models,
137
+ };
138
+ },
139
+ { fsyncDir: true }
140
+ );
141
+ } catch {
142
+ /* best-effort */
143
+ }
144
+ try {
145
+ if (written && onSave) onSave(models);
146
+ } catch {
147
+ /* best-effort */
148
+ }
149
+ return written;
150
+ });
84
151
  };
85
152
  }
86
153
 
@@ -435,7 +435,7 @@ export class MuseCodeOAuthProvider {
435
435
  });
436
436
  const rows = discovered.map((row) => museRow(row.id, row));
437
437
  // A superseded or other-credential response never replaces the live roster either.
438
- if (publish(discovered)) inner._enrichedModels = rows;
438
+ if (await publish(discovered)) inner._enrichedModels = rows;
439
439
  return rows;
440
440
  }
441
441
 
@@ -0,0 +1,45 @@
1
+ // The authorization code a test login sends: redacted by value, since `code`
2
+ // alone is also an error/status field.
3
+ export const AUTH_CODE = 'SECRET-CODE';
4
+
5
+ // Every secret field a token endpoint might echo, each with a distinct value
6
+ // that a test can search for in pages, errors and logs.
7
+ export const SECRET_FIELDS = {
8
+ access_token: 'SECRET-ACCESS',
9
+ refresh_token: 'SECRET-REFRESH',
10
+ id_token: 'SECRET-ID',
11
+ client_secret: 'SECRET-CLIENT',
12
+ code: AUTH_CODE,
13
+ code_verifier: 'SECRET-VERIFIER',
14
+ code_challenge: 'SECRET-CHALLENGE',
15
+ device_code: 'SECRET-DEVICE',
16
+ accessToken: 'SECRET-CAMEL-ACCESS',
17
+ refreshToken: 'SECRET-CAMEL-REFRESH',
18
+ idToken: 'SECRET-CAMEL-ID',
19
+ clientSecret: 'SECRET-CAMEL-CLIENT',
20
+ token: 'SECRET-TOKEN',
21
+ };
22
+
23
+ export const SECRET_RE = /SECRET-/;
24
+
25
+ /** Valid JSON whose secret keys are written with escapes, nested in objects and arrays. */
26
+ export const ESCAPED_SECRET_BODY = String.raw`{"error":{"code":400,"details":[{"client\u005fsecret":"SECRET-ESC-CLIENT","Refresh\u005FToken":"SECRET-ESC-REFRESH"},["x",{"\u0069d_token":"SECRET-ESC-ID"}]],"meta":{"api\u005Fkey":"SECRET-ESC-KEY","acc\u0065ss\u005ftoken":"SECRET-ESC-ACCESS"}}}`;
27
+
28
+ /** Valid JSON whose secrets sit inside string values: form-encoded, a query string, nested JSON. */
29
+ export const STRING_SECRET_BODY = JSON.stringify({
30
+ error: 'invalid_request',
31
+ detail: 'access_token=SECRET-FORM-ACCESS&client_secret=SECRET-FORM-CLIENT',
32
+ error_uri: 'https://auth.example/cb?client_secret=SECRET-QUERY-CLIENT&state=ok',
33
+ nested: JSON.stringify({ refresh_token: 'SECRET-NESTED', inner: String.raw`{"client\u005fsecret":"SECRET-DEEP"}` }),
34
+ });
35
+
36
+ /** Token-endpoint failure bodies carrying every secret field (JSON, escaped JSON, strings, form-encoded). */
37
+ export const SECRET_BODIES = [
38
+ JSON.stringify({ error: 'invalid_grant', ...SECRET_FIELDS }),
39
+ ESCAPED_SECRET_BODY,
40
+ STRING_SECRET_BODY,
41
+ new URLSearchParams({ error: 'invalid_grant', ...SECRET_FIELDS }).toString(),
42
+ ];
43
+
44
+ /** A 200 body that is not JSON but carries tokens (the parser would quote it). */
45
+ export const MALFORMED_TOKEN_BODY = '{"access_token":"SECRET-ACCESS","refresh_token":"SECRET-REFRESH" oops';
@@ -12,6 +12,7 @@ import { createServer } from 'node:http';
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
14
14
  import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
15
+ import { oauthFailureReason, oauthHttpError, oauthRequestError, oauthResponseJson } from './lib/oauth-token-utils.mjs';
15
16
  import { openLoginUrl } from './lib/open-login-url.mjs';
16
17
  import { sleepWithAbort } from './retry-backoff.mjs';
17
18
 
@@ -28,14 +29,6 @@ const TOKEN_TIMEOUT_MS = 30_000;
28
29
  const DEVICE_LOGIN_TIMEOUT_MS = 15 * 60_000;
29
30
  const DEFAULT_DEVICE_INTERVAL_S = 5;
30
31
 
31
- function _scrubOAuthLoginBody(text) {
32
- return String(text || '')
33
- .replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
34
- .replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"')
35
- .replace(/"id_token"\s*:\s*"[^"]+"/g, '"id_token":"[REDACTED]"')
36
- .replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9._-]+/g, '[REDACTED]');
37
- }
38
-
39
32
  /**
40
33
  * @param {object} deps
41
34
  * @param {string} deps.clientId
@@ -72,26 +65,31 @@ export function createOpenAIOAuthLogin(deps) {
72
65
  signal?.throwIfAborted();
73
66
  const cleanCode = String(code || '').trim();
74
67
  if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
75
- const tokenRes = await fetch(tokenUrl, {
76
- method: 'POST',
77
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
78
- body: new URLSearchParams({
79
- grant_type: 'authorization_code',
80
- code: cleanCode,
81
- redirect_uri: redirectUri,
82
- client_id: clientId,
83
- code_verifier: pkce.verifier,
84
- }),
85
- redirect: 'error',
86
- signal: signal
87
- ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
88
- : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
89
- });
68
+ let tokenRes;
69
+ try {
70
+ tokenRes = await fetch(tokenUrl, {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
73
+ body: new URLSearchParams({
74
+ grant_type: 'authorization_code',
75
+ code: cleanCode,
76
+ redirect_uri: redirectUri,
77
+ client_id: clientId,
78
+ code_verifier: pkce.verifier,
79
+ }),
80
+ redirect: 'error',
81
+ signal: signal
82
+ ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
83
+ : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
84
+ });
85
+ } catch (err) {
86
+ throw oauthRequestError('[openai-oauth] token exchange', err);
87
+ }
90
88
  if (!tokenRes.ok) {
91
89
  const text = await tokenRes.text().catch(() => '');
92
- throw new Error(`[openai-oauth] token exchange ${tokenRes.status}: ${_scrubOAuthLoginBody(text).slice(0, 500)}`);
90
+ throw oauthHttpError('[openai-oauth] token exchange', tokenRes.status, text, [cleanCode, pkce.verifier]);
93
91
  }
94
- const json = await tokenRes.json();
92
+ const json = await oauthResponseJson(tokenRes, '[openai-oauth] token exchange');
95
93
  if (!json.access_token || !json.refresh_token) {
96
94
  throw new Error('[openai-oauth] token exchange response missing access_token or refresh_token');
97
95
  }
@@ -292,10 +290,10 @@ export function createOpenAIOAuthLogin(deps) {
292
290
  // written; do not leave a keep-alive connection bound to it.
293
291
  res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
294
292
  res.end(oauthSuccessHtml('OpenAI'));
295
- } catch {
296
- // The shared exchange reports its error through waitForCallback.
293
+ } catch (err) {
294
+ // The shared exchange also reports its error through waitForCallback.
297
295
  res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
298
- res.end(oauthErrorHtml('OpenAI could not complete the token exchange.'));
296
+ res.end(oauthErrorHtml(oauthFailureReason('OpenAI could not complete the token exchange.', err)));
299
297
  }
300
298
  });
301
299
  timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
@@ -40,7 +40,9 @@ export function createWsEventSwitch({
40
40
  // progress only once the model has produced output; before that the silent
41
41
  // thinking follows it.
42
42
  function onItemAdded() {
43
- const kind = response.toolInFlight ? 'tool' : watchdogs.semanticSeen() ? 'semantic' : null;
43
+ let kind = null;
44
+ if (response.toolInFlight) kind = 'tool';
45
+ else if (watchdogs.semanticSeen()) kind = 'semantic';
44
46
  if (kind) watchdogs.bumpSemantic();
45
47
  progress(kind ?? 'transport');
46
48
  }
@@ -93,6 +93,7 @@ export function createWsStreamWatchdogs({
93
93
  // synthesized completion. Attach the streamed partial state so
94
94
  // the loop's partial/tool recovery can finalize it explicitly.
95
95
  err.streamStalled = true;
96
+ err.code = 'ESTREAMSTALL';
96
97
  attachPartialState(err);
97
98
  fail(err, 'inter_chunk_timeout');
98
99
  }, interChunkMs);
@@ -228,7 +228,7 @@ export class OpenCodeGoOAuthProvider {
228
228
  });
229
229
  const response = await consoleGetJson(CONFIG_URL, tokens, { fetchFn: this.config.fetchImpl, refresh: true });
230
230
  const remote = goConfigFromResponse(response, listsGoModels(response) ? null : await loadModelsDevGoModels());
231
- publish(remote.models, {
231
+ await publish(remote.models, {
232
232
  baseURL: remote.baseURL,
233
233
  headers: remote.headers,
234
234
  accountId: tokens.account_id,
@@ -170,7 +170,9 @@ export class OpenCodeGoProvider {
170
170
  const { OpenCodeGoGeminiEngine } = await import('./opencode-go-gemini.mjs');
171
171
  this.gemini = new OpenCodeGoGeminiEngine({
172
172
  ...this.config,
173
- baseURL: this.openai.baseURL,
173
+ // The OAuth org config names the chat base `.../go/openai/v1`; the
174
+ // generateContent routes live under `.../go/v1` and 404 on it.
175
+ baseURL: String(this.openai.baseURL).replace(/\/openai\/v1$/i, '/v1'),
174
176
  extraHeaders: {
175
177
  ...GO_GATEWAY_PRESET.extraHeaders,
176
178
  ...(this.config.extraHeaders || {}),
@@ -3,7 +3,9 @@ import { oauthCredentialProbeState } from './oauth-credential-probes.mjs';
3
3
  import { providerAvailable } from './provider-availability.mjs';
4
4
  import { refreshCatalog as refreshMetadataCatalog } from './model-catalog.mjs';
5
5
  import { wrapProviderAdmission } from './admission-scheduler.mjs';
6
- import { createAccountPoolProvider } from './account-pool.mjs';
6
+ import { createAccountPoolProvider, createCustomKeyPoolProvider } from './account-pool.mjs';
7
+ import { customKeyEngine } from './custom-provider-keys.mjs';
8
+ import { customProviderKeyIds } from '../../../shared/provider-accounts.mjs';
7
9
  import { isInclusiveProvider } from '../../../shared/llm/cost.mjs';
8
10
  import { noEnabledProvidersError } from './registry-errors.mjs';
9
11
  import { providerTakesNativePdf } from '../../../attachments/store.mjs';
@@ -182,6 +184,7 @@ async function loadProviderExport(cacheKey, spec, exportName, signal = null) {
182
184
  const PROVIDER_MODULES = {
183
185
  anthropic: ['./anthropic.mjs', 'AnthropicProvider'],
184
186
  gemini: ['./gemini.mjs', 'GeminiProvider'],
187
+ 'vertex-ai': ['./vertex-ai.mjs', 'VertexAIProvider'],
185
188
  'openai-oauth': ['./openai-oauth.mjs', 'OpenAIOAuthProvider'],
186
189
  'anthropic-oauth': ['./anthropic-oauth.mjs', 'AnthropicOAuthProvider'],
187
190
  'grok-oauth': ['./grok-oauth.mjs', 'GrokOAuthProvider'],
@@ -227,13 +230,39 @@ async function instantiateProvider(name, Ctor, cfg, proxyChannel = null) {
227
230
  const { createModelProxyProvider } = await import('./model-proxy-provider.mjs');
228
231
  return wrapProviderAdmission(createModelProxyProvider(name, cfg, proxyChannel ?? getModelProxyChannel()), name);
229
232
  }
230
- if (isCustomProviderConfig(name, cfg)) return wrapProviderAdmission(await Ctor(name, cfg), name);
233
+ if (isCustomProviderConfig(name, cfg)) {
234
+ const { [CUSTOM_KEY_IDS]: keyIds, ...settings } = cfg;
235
+ const primary = wrapProviderAdmission(await Ctor(name, settings), name);
236
+ // Only a provider with additional keys sends through the key roster.
237
+ if (!keyIds?.length) return primary;
238
+ return createCustomKeyPoolProvider(name, primary, (id) => customKeyEngine(primary, name, id));
239
+ }
231
240
  if (Object.hasOwn(OPENAI_COMPAT_PRESETS, name) && !Object.hasOwn(PROVIDER_MODULES, name)) {
232
241
  return wrapProviderAdmission(new Ctor(name, cfg), name);
233
242
  }
234
243
  return createAccountPoolProvider(name, () => wrapProviderAdmission(new Ctor(cfg), name));
235
244
  }
236
245
 
246
+ // A custom provider's additional keys live in its key roster, not the config.
247
+ // Their ids are read once per init and join its entry (and so its reuse
248
+ // signature): a save that changes them rebuilds the provider, which is wrapped
249
+ // in the key pool only while it has some. A single-key provider's entry and
250
+ // sends stay as they were, with no roster read per send.
251
+ const CUSTOM_KEY_IDS = 'customKeyIds';
252
+
253
+ function withCustomKeyIds(config) {
254
+ if (!config || typeof config !== 'object') return config;
255
+ let out = config;
256
+ for (const [name, cfg] of Object.entries(config)) {
257
+ if (!isCustomProviderConfig(name, cfg) || cfg.auth === 'none') continue;
258
+ const ids = customProviderKeyIds(name);
259
+ if (!ids.length) continue;
260
+ if (out === config) out = { ...config };
261
+ out[name] = { ...cfg, [CUSTOM_KEY_IDS]: ids };
262
+ }
263
+ return out;
264
+ }
265
+
237
266
  let _initProvidersCalls = 0;
238
267
  /** Test seam: how many times initProviders was called in this process. */
239
268
  export function _initProvidersCallCountForTest() {
@@ -249,6 +278,8 @@ export async function initProviders(config, { signal = null } = {}) {
249
278
  if (proxyChannel) {
250
279
  const { loadModelProxyProviderConfig } = await import('./model-proxy-provider.mjs');
251
280
  config = await awaitWithAbort(loadModelProxyProviderConfig({ channel: proxyChannel }), signal);
281
+ } else {
282
+ config = withCustomKeyIds(config);
252
283
  }
253
284
  const sig = reuseSignature(config, proxyChannel);
254
285
  // Coalesce: an identical config is already mid-init — attach to it.