mixdog 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (493) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/gate-scheduler.mjs +2 -2
  7. package/scripts/lib/isolated-test-env.mjs +16 -0
  8. package/scripts/lib/run-node-tests.mjs +5 -14
  9. package/scripts/provider-pingpong-live.mjs +9 -9
  10. package/src/agents/advisor/AGENT.md +4 -4
  11. package/src/agents/advisor/agent.json +1 -1
  12. package/src/agents/reviewer/AGENT.md +5 -4
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/defaults/shipped-history.json +17 -0
  15. package/src/defaults/skills/browser-use/SKILL.md +7 -1
  16. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  17. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  18. package/src/defaults/skills/setup/references/actions.md +11 -4
  19. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  20. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  21. package/src/defaults/skills/visualize/SKILL.md +3 -2
  22. package/src/lib/rules-builder.cjs +56 -7
  23. package/src/output-styles/common.md +22 -6
  24. package/src/output-styles/detailed.md +7 -5
  25. package/src/output-styles/minimal.md +7 -3
  26. package/src/output-styles/simple.md +2 -3
  27. package/src/rules/agent/AGENT.md +7 -1
  28. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  29. package/src/rules/agent-mode/MIXY.md +21 -0
  30. package/src/rules/agent-mode/ROUTINE.md +6 -0
  31. package/src/rules/lead/LEAD.md +9 -4
  32. package/src/rules/shared/10-tool-workflow.md +2 -1
  33. package/src/rules/shared/60-verification.md +4 -5
  34. package/src/rules/shared/70-delivery.md +3 -0
  35. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  37. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  38. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  39. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  40. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +5 -3
  41. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  42. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  43. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +58 -64
  44. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  45. package/src/runtime/agent/orchestrator/mcp/client.mjs +122 -15
  46. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  47. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  48. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  49. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  50. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  51. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  52. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  53. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  55. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +41 -19
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  59. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  62. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +11 -5
  64. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  65. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  66. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  67. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  69. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  70. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  72. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  84. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  86. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  88. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  92. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  94. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  95. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  96. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  97. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  98. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  99. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  100. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +98 -4
  101. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  102. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  103. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +15 -8
  104. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  105. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  106. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  107. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  108. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  109. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  110. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  111. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  112. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  113. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  114. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  115. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  116. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  117. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  118. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  119. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  120. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  121. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  122. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  123. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  124. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  125. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  126. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  127. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  128. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  129. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  130. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  131. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  132. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  133. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  134. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  135. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  136. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +82 -33
  137. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +11 -4
  138. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  139. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  140. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  141. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  142. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +6 -1
  143. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  144. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  145. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  146. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  147. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  148. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  149. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  150. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  151. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  152. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  153. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  154. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  155. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  156. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  157. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  158. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  159. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  160. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  161. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  162. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  163. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  164. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  165. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  166. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  167. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  168. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  169. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  170. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  171. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  172. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  173. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  174. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  175. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
  176. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +325 -0
  177. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +84 -21
  178. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  179. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  180. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  181. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  182. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  183. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  184. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  185. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  186. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  187. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  188. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  189. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  190. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  191. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  192. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  193. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  194. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  195. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  196. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  197. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  198. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  199. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  200. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  201. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  202. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  203. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  204. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  205. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  206. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  207. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  208. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  209. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  210. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  211. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  212. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  213. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  214. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  215. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  216. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  217. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  218. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  219. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  220. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  221. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +123 -32
  222. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  223. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  224. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  225. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  226. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  227. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  228. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  229. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  230. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  231. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  232. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +64 -2
  233. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +10 -2
  234. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  235. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +8 -0
  236. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -1
  237. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  238. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +104 -55
  239. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  240. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  241. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  242. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  243. package/src/runtime/attachments/office-extract.mjs +11 -9
  244. package/src/runtime/browser-bridge/client.mjs +24 -0
  245. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  246. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  247. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  248. package/src/runtime/channels/lib/webhook.mjs +0 -1
  249. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  250. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  251. package/src/runtime/computer-bridge/client.mjs +28 -1
  252. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  253. package/src/runtime/connectors/google-mcp.mjs +446 -0
  254. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  255. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  256. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  257. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  258. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  259. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  260. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  261. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  262. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  263. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  264. package/src/runtime/memory/lib/memory.mjs +7 -1
  265. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  266. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  267. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  268. package/src/runtime/memory/lib/tool-call-handler.mjs +9 -5
  269. package/src/runtime/memory/tool-defs.mjs +5 -0
  270. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  271. package/src/runtime/office/core/tabular.mjs +1 -1
  272. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  273. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  274. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  275. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  276. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  277. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  278. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  279. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  280. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  281. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  282. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  283. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  284. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  285. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  286. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  287. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  288. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  289. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  290. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  291. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  292. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  293. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  294. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  295. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  296. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  297. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  298. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  299. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  300. package/src/runtime/shared/agent-avatar.mjs +320 -0
  301. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  302. package/src/runtime/shared/agent-route-config.mjs +16 -0
  303. package/src/runtime/shared/atomic-file.mjs +36 -14
  304. package/src/runtime/shared/background-tasks.mjs +60 -20
  305. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  306. package/src/runtime/shared/config.mjs +8 -11
  307. package/src/runtime/shared/connector-accounts.mjs +15 -0
  308. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  309. package/src/runtime/shared/connector-catalog.mjs +230 -0
  310. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  311. package/src/runtime/shared/connector-triggers.mjs +43 -0
  312. package/src/runtime/shared/file-lock.mjs +7 -1
  313. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  314. package/src/runtime/shared/file-permissions.d.mts +19 -4
  315. package/src/runtime/shared/file-permissions.mjs +310 -4
  316. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  317. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  318. package/src/runtime/shared/held-request-host.d.mts +12 -0
  319. package/src/runtime/shared/held-request-host.mjs +71 -0
  320. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  321. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  322. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  323. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  324. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  325. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  326. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  327. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  328. package/src/runtime/shared/notify-trace.mjs +8 -1
  329. package/src/runtime/shared/plain-preview.d.mts +1 -0
  330. package/src/runtime/shared/plain-preview.mjs +28 -0
  331. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  332. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  333. package/src/runtime/shared/process-runner.mjs +38 -0
  334. package/src/runtime/shared/provider-accounts.mjs +2 -1
  335. package/src/runtime/shared/provider-api-key.mjs +0 -1
  336. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  337. package/src/runtime/shared/resource-admission.mjs +60 -18
  338. package/src/runtime/shared/routine-limits.d.mts +11 -0
  339. package/src/runtime/shared/routine-limits.mjs +21 -0
  340. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  341. package/src/runtime/shared/routine-trigger.mjs +44 -0
  342. package/src/runtime/shared/run-child.mjs +84 -0
  343. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  344. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  345. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  346. package/src/runtime/shared/singleton-owner.mjs +126 -42
  347. package/src/runtime/shared/slow-span.mjs +4 -12
  348. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  349. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  350. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  351. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  352. package/src/runtime/shared/turn-changes.mjs +0 -8
  353. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  354. package/src/runtime/shared/user-data-guard.mjs +131 -34
  355. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  356. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  357. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  358. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  359. package/src/runtime/tidy/runners/biome.mjs +3 -1
  360. package/src/runtime/tidy/shadow.mjs +2 -1
  361. package/src/runtime/web-search/index.mjs +5 -7
  362. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  363. package/src/runtime/web-search/tool-defs.mjs +6 -1
  364. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  365. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  366. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  367. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  368. package/src/session-runtime/boot/apis.mjs +50 -18
  369. package/src/session-runtime/boot/facade.mjs +5 -8
  370. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  371. package/src/session-runtime/boot/tools.mjs +3 -0
  372. package/src/session-runtime/channel-config-api.mjs +53 -5
  373. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  374. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  375. package/src/session-runtime/held-requests.mjs +228 -0
  376. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  377. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  378. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  379. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  380. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  381. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  382. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  383. package/src/session-runtime/lifecycle/session-switch/resume.mjs +11 -4
  384. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  385. package/src/session-runtime/lifecycle/shared.mjs +25 -2
  386. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  387. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  388. package/src/session-runtime/lifecycle-api.mjs +3 -1
  389. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  390. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  391. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  392. package/src/session-runtime/model-route/set-route.mjs +14 -3
  393. package/src/session-runtime/owned-automation-run.mjs +229 -0
  394. package/src/session-runtime/provider-auth/login.mjs +10 -6
  395. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  396. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  397. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  398. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  399. package/src/session-runtime/provider-models.mjs +76 -10
  400. package/src/session-runtime/provider-readiness.mjs +4 -0
  401. package/src/session-runtime/resource-api.mjs +5 -0
  402. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  403. package/src/session-runtime/resource-skills-api.mjs +12 -0
  404. package/src/session-runtime/room-member-gate.mjs +48 -0
  405. package/src/session-runtime/routine-home-turn.mjs +156 -0
  406. package/src/session-runtime/routine-owner.mjs +48 -0
  407. package/src/session-runtime/schedule-session-run.mjs +30 -64
  408. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  409. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  410. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  411. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  412. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  413. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  414. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  415. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  416. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  417. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  418. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  419. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  420. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  421. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  422. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  423. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  424. package/src/session-runtime/services/agent-tool.mjs +20 -0
  425. package/src/session-runtime/services/channel-admin.mjs +93 -24
  426. package/src/session-runtime/services/memory-access.mjs +37 -0
  427. package/src/session-runtime/services/provider-admin.mjs +166 -59
  428. package/src/session-runtime/services/schedule-access.mjs +91 -0
  429. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  430. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  431. package/src/session-runtime/session-create/session-options.mjs +34 -7
  432. package/src/session-runtime/session-hooks.mjs +16 -8
  433. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  434. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  435. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  436. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  437. package/src/session-runtime/skills-api.mjs +16 -1
  438. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  439. package/src/session-runtime/turn/session-ops.mjs +10 -3
  440. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +11 -2
  441. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  442. package/src/session-runtime/usage-stats-api.mjs +6 -1
  443. package/src/session-runtime/webhook-session-run.mjs +49 -67
  444. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  445. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  446. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  447. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  448. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  449. package/src/standalone/daemon-agent-control.mjs +1 -8
  450. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  451. package/src/standalone/daemon-owner.mjs +117 -25
  452. package/src/standalone/daemon-session-control.mjs +11 -2
  453. package/src/standalone/daemon.mjs +61 -17
  454. package/src/standalone/session-call-cache.mjs +6 -0
  455. package/src/standalone/session-protocol.mjs +28 -1
  456. package/src/standalone/session-service/retention/busy.mjs +7 -2
  457. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  458. package/src/standalone/session-service.mjs +19 -0
  459. package/src/standalone/session-transport/routes.mjs +7 -6
  460. package/src/tui/app/model-options.mjs +39 -2
  461. package/src/tui/app/model-picker.mjs +16 -2
  462. package/src/tui/app/provider-setup-picker.mjs +1 -2
  463. package/src/tui/app/slash-commands.mjs +20 -0
  464. package/src/tui/dist/index.mjs +487 -128
  465. package/src/tui/session/context-state.mjs +6 -1
  466. package/src/tui/session/notice-surface.mjs +35 -4
  467. package/src/tui/session/notice-text.mjs +0 -1
  468. package/src/tui/session/queue-helpers.mjs +2 -1
  469. package/src/tui/session/session-api/extensions.mjs +4 -0
  470. package/src/tui/session/session-api/intake/submission.mjs +12 -13
  471. package/src/tui/session/session-api/integrations.mjs +9 -5
  472. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  473. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  474. package/src/tui/session/session-api/routes.mjs +48 -3
  475. package/src/tui/session/session-flow/drain.mjs +20 -3
  476. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  477. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  478. package/src/tui/session/session-flow/steering.mjs +104 -24
  479. package/src/tui/session/session-flow.mjs +2 -0
  480. package/src/tui/session/tool-approval.mjs +26 -5
  481. package/src/tui/session/transcript-intake.mjs +6 -2
  482. package/src/tui/session/transcript-spill.mjs +5 -1
  483. package/src/tui/session/tui-steering-persist.mjs +65 -42
  484. package/src/tui/session/turn-live-call.mjs +56 -0
  485. package/src/tui/session/turn-stream.mjs +33 -0
  486. package/src/tui/session/turn.mjs +50 -7
  487. package/src/ui/statusline.mjs +1 -6
  488. package/src/ui/streaming-markdown-heal.mjs +24 -7
  489. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  490. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  491. package/src/workflows/default/WORKFLOW.md +9 -13
  492. package/src/session-runtime/automation-agents.mjs +0 -94
  493. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -25,6 +25,7 @@ import {
25
25
  scrubOAuthSecrets,
26
26
  } from './lib/oauth-token-utils.mjs';
27
27
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
28
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
28
29
 
29
30
  const SIGN_IN_EXPIRED_PREFIX = 'Anthropic sign-in expired. Sign in again from Providers.';
30
31
 
@@ -66,8 +67,6 @@ const OAUTH_CALLBACK_PATH = '/callback';
66
67
  const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
67
68
  const OAUTH_MANUAL_REDIRECT_URI =
68
69
  process.env.ANTHROPIC_OAUTH_MANUAL_REDIRECT_URI || 'https://platform.claude.com/oauth/code/callback';
69
- const OAUTH_SUCCESS_REDIRECT_URL =
70
- process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_URL || 'https://platform.claude.com/oauth/code/success?app=claude-code';
71
70
  const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
72
71
  const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
73
72
 
@@ -110,7 +109,7 @@ function _loadCredentialsFile(path) {
110
109
  }
111
110
 
112
111
  // The token endpoint names the signed-in account next to the tokens.
113
- export function identityFromTokenResponse(json) {
112
+ function identityFromTokenResponse(json) {
114
113
  return normalizeAccountIdentity({
115
114
  id: json?.account?.uuid,
116
115
  email: json?.account?.email_address,
@@ -210,6 +209,7 @@ export function describeAnthropicOAuthCredentials() {
210
209
  return {
211
210
  ...oauthCredentialStatus({ hasRefresh, expiresAt, detail, refreshSkewMs: TOKEN_REFRESH_SKEW_MS }),
212
211
  ...accountIdentityFields(creds.identity),
212
+ plan: creds.subscriptionType || null,
213
213
  };
214
214
  } catch (err) {
215
215
  return {
@@ -570,9 +570,11 @@ function _parseOAuthCodeInput(input) {
570
570
  return parseOAuthCodeInput(input, { allowHashState: true });
571
571
  }
572
572
 
573
- async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
573
+ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri, signal = null }) {
574
+ signal?.throwIfAborted();
574
575
  const cleanCode = String(code || '').trim();
575
576
  if (!cleanCode) throw new Error('[anthropic-oauth] authorization code is required');
577
+ const timeoutSignal = AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS);
576
578
  const tokenRes = await fetch(TOKEN_URL, {
577
579
  method: 'POST',
578
580
  headers: tokenEndpointHeaders(),
@@ -585,7 +587,7 @@ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
585
587
  state,
586
588
  }),
587
589
  redirect: 'error',
588
- signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
590
+ signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
589
591
  dispatcher: getLlmDispatcher(),
590
592
  });
591
593
  if (!tokenRes.ok) {
@@ -605,6 +607,8 @@ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
605
607
  const expiresAt = expiresAtFromTokenResponse(json);
606
608
  const scopes = _oauthParseScopeField(json?.scope);
607
609
  const identity = identityFromTokenResponse(json);
610
+ // A cancelled login never stores the account it was exchanging.
611
+ signal?.throwIfAborted();
608
612
  const credPath = _oauthCredentialsWritePath();
609
613
  const raw = _updateCredentialsFile(credPath, (current) => {
610
614
  const base = current && typeof current === 'object' ? current : {};
@@ -664,14 +668,33 @@ export async function beginOAuthLogin({ openBrowserFn = null, openBrowser = true
664
668
  }
665
669
  };
666
670
 
671
+ const controller = new AbortController();
667
672
  let server = null;
668
673
  let timeout = null;
669
674
  let finish = null;
675
+ let exchangePromise = null;
676
+ // Browser and manual callbacks share one exchange: an authorization code is
677
+ // spent once, and a duplicate joins the first instead of failing the login.
678
+ const acceptCode = (code, redirectUri) => {
679
+ if (exchangePromise) return exchangePromise;
680
+ controller.signal.throwIfAborted();
681
+ // The browser deadline ends when a code arrives; the exchange has its own.
682
+ if (timeout) clearTimeout(timeout);
683
+ timeout = null;
684
+ exchangePromise = exchangeAuthorizationCode({ pkce, code, state, redirectUri, signal: controller.signal });
685
+ exchangePromise.then(
686
+ (tokens) => finish(tokens),
687
+ (error) => finish(null, error instanceof Error ? error : new Error(String(error)))
688
+ );
689
+ return exchangePromise;
690
+ };
670
691
  const waitForCallback = new Promise((resolve, reject) => {
671
692
  let settled = false;
672
693
  finish = (value, error = null) => {
673
694
  if (settled) return;
674
695
  settled = true;
696
+ // Work arriving after the login settled (or was cancelled) is rejected.
697
+ controller.abort();
675
698
  if (timeout) clearTimeout(timeout);
676
699
  try {
677
700
  server?.close();
@@ -691,20 +714,20 @@ export async function beginOAuthLogin({ openBrowserFn = null, openBrowser = true
691
714
  const code = u.searchParams.get('code');
692
715
  if (!code || u.searchParams.get('state') !== state) {
693
716
  // Reject this request only; the valid callback may still arrive.
694
- res.writeHead(400);
695
- res.end('Invalid');
717
+ res.writeHead(400, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
718
+ res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
696
719
  return;
697
720
  }
721
+ // The login settles (and its server closes) before this answer is sent,
722
+ // so the connection must not stay open to the closed login's handler.
698
723
  try {
699
- const tokens = await exchangeAuthorizationCode({ pkce, code, state, redirectUri: OAUTH_REDIRECT_URI });
700
- res.writeHead(302, { Location: OAUTH_SUCCESS_REDIRECT_URL });
701
- res.end();
702
- finish(tokens);
703
- } catch (err) {
704
- const error = err instanceof Error ? err : new Error(String(err));
705
- res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
706
- res.end(`Claude login failed: ${error.message}`);
707
- finish(null, error);
724
+ await acceptCode(code, OAUTH_REDIRECT_URI);
725
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
726
+ res.end(oauthSuccessHtml('Claude'));
727
+ } catch {
728
+ // The shared exchange reports its error through waitForCallback.
729
+ res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
730
+ res.end(oauthErrorHtml('Claude could not complete the token exchange.'));
708
731
  }
709
732
  });
710
733
  timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
@@ -730,10 +753,9 @@ export async function beginOAuthLogin({ openBrowserFn = null, openBrowser = true
730
753
  completeCode: async (input) => {
731
754
  const parsed = _parseOAuthCodeInput(input);
732
755
  if (parsed.state && parsed.state !== state) throw new Error('[anthropic-oauth] OAuth state mismatch');
756
+ if (!String(parsed.code || '').trim()) throw new Error('[anthropic-oauth] authorization code is required');
733
757
  const redirectUri = parsed.redirectUri || (parsed.state ? OAUTH_MANUAL_REDIRECT_URI : OAUTH_REDIRECT_URI);
734
- const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code, state, redirectUri });
735
- finish?.(tokens);
736
- return tokens;
758
+ return await acceptCode(parsed.code, redirectUri);
737
759
  },
738
760
  cancel: () => {
739
761
  finish?.(null);
@@ -448,7 +448,8 @@ export class AnthropicOAuthProvider {
448
448
  // a hard 400. Pairing here closes the gap regardless of caller.
449
449
  messages = sanitizeToolPairs(messages);
450
450
  const opts = sendOpts || {};
451
- const { onStageChange, onStreamDelta, onToolCall, onTextDelta, onTextReset } = streamCallbacks(opts);
451
+ const { onStageChange, onStreamDelta, onToolCall, onTextDelta, onTextReset, onStreamItemStart } =
452
+ streamCallbacks(opts);
452
453
  const externalSignal = opts.signal || null;
453
454
  // Test seam: lets the retry harness drive stream outcomes without a
454
455
  // live OAuth session.
@@ -610,7 +611,7 @@ export class AnthropicOAuthProvider {
610
611
  midState,
611
612
  onTextDelta,
612
613
  knownToolNames,
613
- { relayProgressUpdates: body.thinking?.display === 'updates' }
614
+ { relayProgressUpdates: body.thinking?.display === 'updates', onStreamItemStart }
614
615
  );
615
616
  try {
616
617
  controller?.abort?.('Anthropic SSE complete');
@@ -8,6 +8,7 @@ import {
8
8
  NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES,
9
9
  } from '../../lib/anthropic-native-blocks.mjs';
10
10
  import { parseAnthropicFallbackBlock } from '../../anthropic-server-fallback.mjs';
11
+ import { reportStreamItemStart } from '../../lib/send-callbacks.mjs';
11
12
 
12
13
  // Detach the provider payload from the parser's event object without altering
13
14
  // any field (opaque server-tool result payloads must round-trip byte-exact).
@@ -19,7 +20,7 @@ function cloneNativeBlock(block) {
19
20
  }
20
21
  }
21
22
 
22
- export function createContentBlockStart({ turn, blocks, state, progress, seedText }) {
23
+ export function createContentBlockStart({ turn, blocks, state, progress, seedText, onStreamItemStart }) {
23
24
  const seedThinkingBlock = (index, block) => {
24
25
  if (block.type === 'redacted_thinking') {
25
26
  // Opaque redacted payload: real reasoning content, exposed as a block.
@@ -59,6 +60,8 @@ export function createContentBlockStart({ turn, blocks, state, progress, seedTex
59
60
  name: block.name || '',
60
61
  inputJson: '',
61
62
  });
63
+ // The call opened; its input still streams before onToolCall.
64
+ reportStreamItemStart(onStreamItemStart, { kind: 'tool' });
62
65
  }
63
66
  if (block?.type === 'text') {
64
67
  // Seed the ordered text slot. An empty block still needs its slot (it
@@ -19,6 +19,7 @@ export function createTurnEvents({
19
19
  onToolCall,
20
20
  relayProgressUpdates,
21
21
  labels,
22
+ onStreamItemStart,
22
23
  }) {
23
24
  const { onMessageStart, onMessageDelta, onMessageStop } = createMessageEvents({ turn, blocks, state });
24
25
  const onContentBlockDelta = createContentBlockDelta({
@@ -38,6 +39,7 @@ export function createTurnEvents({
38
39
  state,
39
40
  progress,
40
41
  seedText: onContentBlockDelta.seedText,
42
+ onStreamItemStart,
41
43
  }),
42
44
  onContentBlockDelta,
43
45
  onContentBlockStop: createContentBlockStop({ turn, blocks, state, leak, progress, onToolCall, labels }),
@@ -24,6 +24,7 @@ import { OAUTH_STREAM_LABELS } from './lib/anthropic-stream-labels.mjs';
24
24
  * @param {Iterable<string>} [deps.knownToolNames]
25
25
  * @param {boolean} [deps.relayProgressUpdates] request used thinking.display "updates"
26
26
  * @param {{ display: string, tag: string }} [deps.labels] provider name for errors and stderr lines
27
+ * @param {(detail: object) => void} [deps.onStreamItemStart] a client tool_use block opened
27
28
  */
28
29
  export function createAnthropicSseTurn({
29
30
  state,
@@ -33,6 +34,7 @@ export function createAnthropicSseTurn({
33
34
  knownToolNames,
34
35
  relayProgressUpdates = false,
35
36
  labels = OAUTH_STREAM_LABELS,
37
+ onStreamItemStart = null,
36
38
  }) {
37
39
  const progress = (kind) => {
38
40
  try {
@@ -64,6 +66,7 @@ export function createAnthropicSseTurn({
64
66
  onToolCall,
65
67
  relayProgressUpdates,
66
68
  labels,
69
+ onStreamItemStart,
67
70
  });
68
71
  const outcome = createTurnOutcome({ turn, blocks, state, labels });
69
72
 
@@ -186,7 +186,7 @@ export async function parseSSEStream(
186
186
  state,
187
187
  onTextDelta,
188
188
  knownToolNames,
189
- { relayProgressUpdates = false, labels = OAUTH_STREAM_LABELS } = {}
189
+ { relayProgressUpdates = false, labels = OAUTH_STREAM_LABELS, onStreamItemStart = null } = {}
190
190
  ) {
191
191
  // Anthropic/Claude parity: every received SSE byte proves transport
192
192
  // activity, including comment and named ping keepalives. Content kinds
@@ -208,6 +208,7 @@ export async function parseSSEStream(
208
208
  knownToolNames,
209
209
  relayProgressUpdates,
210
210
  labels,
211
+ onStreamItemStart,
211
212
  });
212
213
  const watchdogs = createAnthropicSseWatchdogs({
213
214
  state,
@@ -162,7 +162,8 @@ export class AnthropicProvider {
162
162
  });
163
163
  this.fastModeBetaHeaderLatched = fastModeLatched;
164
164
 
165
- const { onStageChange, onStreamDelta, onToolCall, onTextDelta, onTextReset } = streamCallbacks(opts);
165
+ const { onStageChange, onStreamDelta, onToolCall, onTextDelta, onTextReset, onStreamItemStart } =
166
+ streamCallbacks(opts);
166
167
 
167
168
  // No absolute wall-clock cap on streaming generation: a stream still
168
169
  // emitting SSE deltas must not be killed by a fixed total-lifetime timer.
@@ -288,6 +289,7 @@ export class AnthropicProvider {
288
289
  {
289
290
  relayProgressUpdates: params.thinking?.display === 'updates',
290
291
  labels: { display: 'Anthropic', tag: this.name },
292
+ onStreamItemStart,
291
293
  }
292
294
  );
293
295
  try {
@@ -36,8 +36,7 @@ import {
36
36
 
37
37
  const ONBOARD_INTERVAL_MS = 1_000;
38
38
  const CALLBACK_FAILURE_HTML = oauthErrorHtml(
39
- 'Antigravity sign-in was not completed.',
40
- 'Return to Mixdog for the error details and any account verification link.'
39
+ 'Antigravity sign-in was not completed. Return to Mixdog for the error details and any account verification link.'
41
40
  );
42
41
 
43
42
  function extractProjectId(payload) {
@@ -219,7 +218,12 @@ async function provisionProject(context, tierId) {
219
218
  }
220
219
 
221
220
  /** Resolve account eligibility and return the project from a fresh status load. */
222
- export async function discoverProject(
221
+ export async function discoverProject(accessToken, options = {}) {
222
+ return (await discoverAccount(accessToken, options)).projectId;
223
+ }
224
+
225
+ /** Project plus the account's tier id (paid tier first), from the same status load. */
226
+ export async function discoverAccount(
223
227
  accessToken,
224
228
  { fetchFn = fetch, onProgress = null, signal = null, email = '' } = {}
225
229
  ) {
@@ -236,7 +240,8 @@ export async function discoverProject(
236
240
  const refreshed = await loadProjectState(context);
237
241
  const projectId = extractProjectId(refreshed);
238
242
  if (!projectId) throw new Error('[antigravity-oauth] loadCodeAssist did not return a cloudaicompanionProject');
239
- return projectId;
243
+ const tierId = refreshed.paidTier?.id || refreshed.currentTier?.id;
244
+ return { projectId, tierId: typeof tierId === 'string' ? tierId : '' };
240
245
  }
241
246
 
242
247
  export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetch, onProgress = null, signal = null }) {
@@ -268,12 +273,13 @@ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetc
268
273
  throw new Error('[antigravity-oauth] token exchange response missing access_token or refresh_token');
269
274
  }
270
275
  const { email, userId } = await fetchAccountInfo(json.access_token, { fetchFn, signal });
271
- const projectId = await discoverProject(json.access_token, { fetchFn, onProgress, signal, email });
276
+ const { projectId, tierId } = await discoverAccount(json.access_token, { fetchFn, onProgress, signal, email });
272
277
  const tokens = {
273
278
  access_token: json.access_token,
274
279
  refresh_token: json.refresh_token,
275
280
  expires_at: typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0,
276
281
  project_id: projectId,
282
+ tier_id: tierId,
277
283
  email,
278
284
  user_id: userId,
279
285
  };
@@ -204,6 +204,7 @@ export function loadTokens() {
204
204
  refresh_token: String(raw.refresh_token),
205
205
  expires_at: _normalizeExpiresAt(raw.expires_at ?? raw.expiresAt),
206
206
  project_id: String(raw.project_id || raw.projectId || ''),
207
+ tier_id: String(raw.tier_id || ''),
207
208
  email: String(raw.email || ''),
208
209
  user_id: String(raw.user_id || raw.userId || ''),
209
210
  source: 'own',
@@ -222,6 +223,7 @@ export function saveTokens(tokens) {
222
223
  refresh_token: tokens.refresh_token,
223
224
  expires_at: tokens.expires_at || 0,
224
225
  project_id: tokens.project_id || tokens.projectId || '',
226
+ tier_id: tokens.tier_id || '',
225
227
  email: tokens.email || '',
226
228
  user_id: tokens.user_id || tokens.userId || '',
227
229
  },
@@ -274,6 +276,7 @@ export function describeAntigravityOAuthCredentials() {
274
276
  ...accountIdentityFields({ id: tokens.user_id, email: tokens.email }),
275
277
  email: tokens.email || '',
276
278
  projectId: tokens.project_id || '',
279
+ plan: tokens.tier_id || null,
277
280
  };
278
281
  } catch {
279
282
  return credentialsNotSet();
@@ -340,6 +343,7 @@ export function refreshTokens({ fetchFn = fetch } = {}) {
340
343
  refresh_token: json.refresh_token || current.refresh_token,
341
344
  expires_at: typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0,
342
345
  project_id: current.project_id,
346
+ tier_id: current.tier_id,
343
347
  email: current.email,
344
348
  user_id: current.user_id,
345
349
  };
@@ -0,0 +1,163 @@
1
+ // Bounded, single-flight, cancellable provider catalog work, shared by the
2
+ // registry's catalog refreshes and the session runtime's shared catalog.
3
+ //
4
+ // - Deadline: a caller waits at most `timeoutMs` and then gets an ETIMEDOUT
5
+ // error entry instead of the provider's answer, so one hung provider cannot
6
+ // hold a catalog read or a startup/forced refresh.
7
+ // - Cancellation: on that deadline the operation's AbortSignal fires; a
8
+ // provider whose listModels honors `{ signal }` stops its work there.
9
+ // - Single flight: per provider at most one operation is pending in this
10
+ // process. A caller that finds one of its own kind joins it (with its own
11
+ // deadline); one that finds the other kind (a refresh vs a model list)
12
+ // waits for it inside its deadline before starting its own. An operation
13
+ // leaves the table only when the underlying work settles, not when a
14
+ // caller gives up, so repeated timed-out reads never pile up work; once an
15
+ // operation overran one deadline, later callers fail fast on it instead of
16
+ // each waiting out another deadline, until it settles.
17
+ // - Generations: one auth/account generation spans refreshes and lists alike
18
+ // (bumpProviderCatalogGeneration). Work of the other kind pending from an
19
+ // older generation, or from an older provider instance, never blocks or
20
+ // fails a caller of the newer one: that caller starts afresh. Same-kind
21
+ // work is never joined across generations, instances or a caller-supplied
22
+ // `epoch` (the catalog revision a list belongs to) either, unless it
23
+ // stalled (below).
24
+ // - Stalled work: an operation that overran its deadline (its signal fired)
25
+ // and still has not settled ignores cancellation. Until it settles, that
26
+ // provider name is not asked again for the same kind, not by a newer
27
+ // generation, epoch or provider instance: callers get an ETIMEDOUT entry
28
+ // marked `stalled` at once. Otherwise every account change, catalog
29
+ // revision or registry rebuild would leave one more never-ending call
30
+ // behind in the provider. Superseded calls can only overlap inside one
31
+ // deadline window, before any of them stalls.
32
+ import { clearProviderCatalogFailures, recordProviderCatalogOutcome } from './catalog-failures.mjs';
33
+
34
+ export const PROVIDER_CATALOG_TIMEOUT_MS = 4_000;
35
+
36
+ /** name -> the latest op started for that provider (either kind). */
37
+ const pendingOps = new Map();
38
+ /** `${kind}\0${name}` -> Set of the unsettled ops of that kind, of any
39
+ * generation, epoch or provider instance; emptied sets are removed. */
40
+ const runningOps = new Map();
41
+ /** `${kind}\0${name}` -> sequence number of the latest op started of that kind. */
42
+ const latestOpSeq = new Map();
43
+ let opSeq = 0;
44
+ let generation = 0;
45
+
46
+ const runKey = (name, kind) => `${kind}\0${name}`;
47
+
48
+ /** Opens a new auth/account generation for every provider's catalog work. */
49
+ export function bumpProviderCatalogGeneration() {
50
+ generation += 1;
51
+ // Failures of the previous account say nothing about the new one.
52
+ clearProviderCatalogFailures();
53
+ }
54
+
55
+ /** Provider names with catalog work still pending (diagnostics and tests). */
56
+ export function pendingProviderCatalogOps() {
57
+ return [...pendingOps.keys()];
58
+ }
59
+
60
+ /** `{ name, kind }` of every operation that ignored its deadline's abort and
61
+ * is still unsettled (diagnostics and tests). */
62
+ export function stalledProviderCatalogOps() {
63
+ return [...runningOps.values()]
64
+ .flatMap((ops) => [...ops])
65
+ .filter((op) => op.timedOut)
66
+ .map(({ name, kind }) => ({ name, kind }));
67
+ }
68
+
69
+ const isStalled = (name, kind) => [...(runningOps.get(runKey(name, kind)) || [])].some((op) => op.timedOut);
70
+
71
+ function startOp(name, provider, kind, run, epoch, opGeneration) {
72
+ const controller = new AbortController();
73
+ const settled = Promise.resolve()
74
+ .then(() => run(controller.signal))
75
+ .then(
76
+ (value) => ({ value }),
77
+ (error) => ({ error })
78
+ );
79
+ const seq = ++opSeq;
80
+ const op = { name, provider, kind, epoch, generation: opGeneration, seq, controller, settled, done: false };
81
+ const key = runKey(name, kind);
82
+ latestOpSeq.set(key, seq);
83
+ const running = runningOps.get(key) || new Set();
84
+ running.add(op);
85
+ runningOps.set(key, running);
86
+ pendingOps.set(name, op);
87
+ void settled.then(() => {
88
+ op.done = true;
89
+ if (pendingOps.get(name) === op) pendingOps.delete(name);
90
+ running.delete(op);
91
+ if (running.size === 0 && runningOps.get(key) === running) runningOps.delete(key);
92
+ });
93
+ return op;
94
+ }
95
+
96
+ function timeoutError(message) {
97
+ const error = new Error(message);
98
+ error.code = 'ETIMEDOUT';
99
+ return error;
100
+ }
101
+
102
+ /** Resolves `{ value }`, `{ error }`, or `{ error, timedOut: true }`; never rejects.
103
+ * The outcome is also kept as the provider's latest catalog failure (or
104
+ * recovery) for views that explain an empty list (catalog-failures.mjs),
105
+ * but only while it is current: a caller of a superseded generation, or one
106
+ * answered by an op that a newer op of its kind replaced, publishes nothing. */
107
+ export async function boundedProviderCatalogOp(op) {
108
+ const callerGeneration = generation;
109
+ const answered = { op: null };
110
+ const outcome = await runBoundedOp(op, callerGeneration, answered);
111
+ const currentOp = !answered.op || latestOpSeq.get(runKey(op.name, op.kind)) === answered.op.seq;
112
+ if (callerGeneration === generation && currentOp) recordProviderCatalogOutcome(op.name, outcome.error);
113
+ return outcome;
114
+ }
115
+
116
+ async function runBoundedOp(
117
+ { name, provider, kind, run, timeoutMs = PROVIDER_CATALOG_TIMEOUT_MS, epoch = 0 },
118
+ callerGeneration,
119
+ answered
120
+ ) {
121
+ let timer = null;
122
+ let watched = null;
123
+ const deadline = new Promise((resolve) => {
124
+ timer = setTimeout(() => {
125
+ const error = timeoutError(`provider "${name}" catalog ${kind} did not finish within ${timeoutMs}ms`);
126
+ if (watched) {
127
+ // Stalled until it settles: an op that honors the abort settles (and
128
+ // leaves the table) a few microtasks later, one that ignores it stays.
129
+ watched.timedOut = true;
130
+ watched.controller.abort(error);
131
+ }
132
+ resolve({ error, timedOut: true });
133
+ }, timeoutMs);
134
+ });
135
+ try {
136
+ for (;;) {
137
+ let op = pendingOps.get(name);
138
+ const superseded =
139
+ !op || op.provider !== provider || op.generation < callerGeneration || (op.kind === kind && op.epoch !== epoch);
140
+ if (superseded) {
141
+ if (isStalled(name, kind)) {
142
+ const error = timeoutError(`provider "${name}" catalog ${kind} ignored its deadline and is still pending`);
143
+ return { error, timedOut: true, stalled: true };
144
+ }
145
+ op = startOp(name, provider, kind, run, epoch, callerGeneration);
146
+ } else if (op.timedOut) {
147
+ // Already overran a deadline and still not settled: fail fast instead
148
+ // of making every later caller wait out another deadline for it.
149
+ const error = timeoutError(`provider "${name}" catalog ${op.kind} is still pending after its deadline`);
150
+ return { error, timedOut: true };
151
+ }
152
+ watched = op;
153
+ if (op.kind === kind) {
154
+ answered.op = op;
155
+ return await Promise.race([op.settled, deadline]);
156
+ }
157
+ const outcome = await Promise.race([op.settled.then(() => null), deadline]);
158
+ if (outcome) return outcome;
159
+ }
160
+ } finally {
161
+ clearTimeout(timer);
162
+ }
163
+ }
@@ -0,0 +1,34 @@
1
+ // The latest catalog failure of each provider, kept so a view that lists a
2
+ // provider's models can say WHY the list is empty (expired key, spent credits,
3
+ // timeout) instead of showing a silent 0/0. Cleared by the provider's next
4
+ // answer and by an account change.
5
+ import { safeErrorDetails } from '../../../shared/error-presentation.mjs';
6
+ import { providerErrorKind } from './provider-error-kind.mjs';
7
+
8
+ const MESSAGE_LIMIT = 300;
9
+ const failures = new Map();
10
+
11
+ /** Records a provider's catalog failure (`error`) or its recovery (no error). */
12
+ export function recordProviderCatalogOutcome(name, error) {
13
+ if (!error) {
14
+ failures.delete(name);
15
+ return;
16
+ }
17
+ const reason = typeof error.providerError?.message === 'string' ? error.providerError.message : '';
18
+ const text = safeErrorDetails(reason ? `${error.message}: ${reason}` : error.message);
19
+ failures.set(name, {
20
+ // Plain text, never a JSON envelope: describeError reads the kind below.
21
+ message: text.replace(/\s+/g, ' ').slice(0, MESSAGE_LIMIT),
22
+ errorKind: providerErrorKind(error),
23
+ at: Date.now(),
24
+ });
25
+ }
26
+
27
+ /** `{ message, errorKind, at }` of the provider's latest failure, or null. */
28
+ export function providerCatalogFailure(name) {
29
+ return failures.get(name) ?? null;
30
+ }
31
+
32
+ export function clearProviderCatalogFailures() {
33
+ failures.clear();
34
+ }
@@ -4,7 +4,7 @@ import { createRemoteVersionSource } from './npm-cli-version.mjs';
4
4
  // documented version endpoint; the official installer script embeds the
5
5
  // current build (…/lab/<build>/…), parsed best-effort. Any format change or
6
6
  // failure falls back to the floor.
7
- export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.16-e3cb207';
7
+ export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.19-6bbf2c4';
8
8
  const BUILD_PATTERN = /downloads\.cursor\.com\/lab\/(\d{4}\.\d{2}\.\d{2}-[0-9a-f]{6,40})\//;
9
9
  export const CURSOR_INSTALL_URL = 'https://cursor.com/install';
10
10
 
@@ -4,7 +4,7 @@ import { createServer } from 'node:http';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
6
6
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
7
- import { OAUTH_PAGE_CONTENT_TYPE, oauthSuccessHtml } from './lib/oauth-page.mjs';
7
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
8
8
  import { emailFromJwts } from './lib/oauth-token-utils.mjs';
9
9
  import {
10
10
  CLIENT_ID,
@@ -26,7 +26,8 @@ export function generatePKCE() {
26
26
  return createOAuthPkce();
27
27
  }
28
28
 
29
- export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
29
+ export async function exchangeAuthorizationCode({ discovery, pkce, code, signal = null }) {
30
+ signal?.throwIfAborted();
30
31
  const cleanCode = String(code || '').trim();
31
32
  if (!cleanCode) throw new Error('[grok-oauth] authorization code is required');
32
33
  const tokenRes = await fetch(discovery.token_endpoint, {
@@ -47,7 +48,9 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
47
48
  // Secret-bearing (authorization code + verifier): refuse
48
49
  // redirects so they can't be replayed to an untrusted host.
49
50
  redirect: 'error',
50
- signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
51
+ signal: signal
52
+ ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
53
+ : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
51
54
  dispatcher: getLlmDispatcher(),
52
55
  });
53
56
  if (!tokenRes.ok) {
@@ -72,6 +75,8 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
72
75
  ? { email: emailFromJwts(json.id_token, json.access_token) }
73
76
  : {}),
74
77
  };
78
+ // A cancelled login never stores the account it was exchanging.
79
+ signal?.throwIfAborted();
75
80
  saveTokens(tokens);
76
81
  return tokens;
77
82
  }
@@ -96,11 +101,30 @@ export async function beginOAuthLogin({ openBrowser = true } = {}) {
96
101
  let server = null;
97
102
  let timeout = null;
98
103
  let finish = null;
104
+ let exchangePromise = null;
105
+ const controller = new AbortController();
106
+ // Browser and manual callbacks share one exchange: an authorization code is
107
+ // single-use, and a duplicate must not settle the login before the first.
108
+ const acceptCode = (code) => {
109
+ if (exchangePromise) return exchangePromise;
110
+ controller.signal.throwIfAborted();
111
+ // The browser deadline ends when a code arrives; the exchange has its own.
112
+ if (timeout) clearTimeout(timeout);
113
+ timeout = null;
114
+ exchangePromise = exchangeAuthorizationCode({ discovery, pkce, code, signal: controller.signal });
115
+ exchangePromise.then(
116
+ (tokens) => finish(tokens),
117
+ (err) => finish(null, err instanceof Error ? err : new Error(String(err)))
118
+ );
119
+ return exchangePromise;
120
+ };
99
121
  const waitForCallback = new Promise((resolve, reject) => {
100
122
  let settled = false;
101
123
  finish = (value, error = null) => {
102
124
  if (settled) return;
103
125
  settled = true;
126
+ // Work arriving after the login settled (or was cancelled) is rejected.
127
+ controller.abort();
104
128
  if (timeout) clearTimeout(timeout);
105
129
  try {
106
130
  server?.close();
@@ -120,17 +144,18 @@ export async function beginOAuthLogin({ openBrowser = true } = {}) {
120
144
  const code = u.searchParams.get('code');
121
145
  if (!code || u.searchParams.get('state') !== state) {
122
146
  // Reject this request only; the valid callback may still arrive.
123
- res.writeHead(400);
124
- res.end('Invalid');
147
+ res.writeHead(400, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
148
+ res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
125
149
  return;
126
150
  }
127
- res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
128
- res.end(oauthSuccessHtml('Grok'));
129
151
  try {
130
- const tokens = await exchangeAuthorizationCode({ discovery, pkce, code });
131
- finish(tokens);
132
- } catch (err) {
133
- finish(null, err instanceof Error ? err : new Error(String(err)));
152
+ await acceptCode(code);
153
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
154
+ res.end(oauthSuccessHtml('Grok'));
155
+ } catch {
156
+ // The shared exchange reports its error through waitForCallback.
157
+ res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
158
+ res.end(oauthErrorHtml('Grok could not complete the token exchange.'));
134
159
  }
135
160
  });
136
161
  timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
@@ -161,9 +186,8 @@ export async function beginOAuthLogin({ openBrowser = true } = {}) {
161
186
  completeCode: async (input) => {
162
187
  const parsed = parseOAuthCodeInput(input, { allowHashState: true });
163
188
  if (parsed.state && parsed.state !== state) throw new Error('[grok-oauth] OAuth state mismatch');
164
- const tokens = await exchangeAuthorizationCode({ discovery, pkce, code: parsed.code });
165
- finish?.(tokens);
166
- return tokens;
189
+ if (!parsed.code) throw new Error('[grok-oauth] authorization code is required');
190
+ return await acceptCode(parsed.code);
167
191
  },
168
192
  cancel: () => {
169
193
  finish?.(null);