mixdog 1.3.5 → 1.4.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 (341) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +2 -2
  3. package/package.json +2 -11
  4. package/scripts/daemon-stub.mjs +1 -1
  5. package/scripts/gate-local.mjs +4 -2
  6. package/scripts/native-release-status.mjs +2 -2
  7. package/scripts/recall-history-cases.json +1 -1
  8. package/scripts/suite-health.mjs +10 -1
  9. package/scripts/test.mjs +7 -11
  10. package/src/agents/reviewer/AGENT.md +7 -11
  11. package/src/agents/reviewer/agent.json +1 -1
  12. package/src/defaults/shipped-history.json +17 -1
  13. package/src/defaults/skills/browser-use/SKILL.md +59 -11
  14. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  15. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  16. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  17. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  18. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  19. package/src/defaults/skills/setup/SKILL.md +22 -0
  20. package/src/defaults/skills/setup/references/actions.md +39 -5
  21. package/src/defaults/skills/setup/references/surfaces.md +11 -2
  22. package/src/defaults/skills/visualize/SKILL.md +2 -1
  23. package/src/lib/rules-builder.cjs +10 -8
  24. package/src/rules/agent/AGENT.md +6 -6
  25. package/src/rules/lead/LEAD.md +18 -20
  26. package/src/rules/shared/05-tool-calls.md +5 -6
  27. package/src/rules/shared/10-tool-workflow.md +2 -0
  28. package/src/rules/shared/60-verification.md +7 -5
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/config.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  35. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  36. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
  39. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
  40. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
  41. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  42. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  45. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
  46. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  47. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  48. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  49. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
  50. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  52. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
  53. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  54. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  55. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  56. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  57. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  58. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  64. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  65. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  68. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  69. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  72. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  76. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  77. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
  78. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  79. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
  82. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
  83. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  84. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  86. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
  88. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  90. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  92. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
  93. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  94. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  97. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  99. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  101. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  102. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  103. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  104. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  105. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  106. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  107. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  109. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  110. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  112. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  113. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  114. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  115. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  116. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  117. package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
  118. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  120. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  121. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  122. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  123. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  124. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  125. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  126. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  127. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
  128. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  129. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  130. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  131. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  132. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  133. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  135. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
  136. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  138. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  141. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  143. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  144. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  145. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  146. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  147. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  148. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  149. package/src/runtime/browser-bridge/client.mjs +73 -8
  150. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  151. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  152. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  153. package/src/runtime/channels/lib/config.mjs +6 -61
  154. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  155. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  156. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  157. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  158. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  159. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  160. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  161. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  162. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  163. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  164. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  165. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  166. package/src/runtime/channels/lib/scheduler.mjs +33 -147
  167. package/src/runtime/channels/lib/schedules-db.mjs +34 -11
  168. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  169. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
  170. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  171. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  172. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  173. package/src/runtime/channels/lib/worker-main.mjs +10 -34
  174. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  175. package/src/runtime/computer-bridge/actions.mjs +9 -1
  176. package/src/runtime/computer-bridge/client.mjs +180 -8
  177. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  178. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  179. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  180. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  181. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  182. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  183. package/src/runtime/connectors/google-mcp.mjs +10 -12
  184. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  185. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  186. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  187. package/src/runtime/session-search/session-search-db.mjs +73 -9
  188. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  189. package/src/runtime/shared/abort-race.mjs +89 -0
  190. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  191. package/src/runtime/shared/agent-kind.mjs +4 -3
  192. package/src/runtime/shared/bounded-download.mjs +8 -3
  193. package/src/runtime/shared/config.mjs +2 -13
  194. package/src/runtime/shared/connector-accounts.mjs +20 -10
  195. package/src/runtime/shared/connector-catalog.d.mts +29 -0
  196. package/src/runtime/shared/connector-catalog.mjs +150 -0
  197. package/src/runtime/shared/connector-triggers.mjs +22 -19
  198. package/src/runtime/shared/defaults-separation.mjs +2 -14
  199. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  200. package/src/runtime/shared/err-text.mjs +24 -15
  201. package/src/runtime/shared/file-permissions.d.mts +0 -8
  202. package/src/runtime/shared/file-permissions.mjs +0 -11
  203. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  204. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  205. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  206. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  207. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  208. package/src/runtime/shared/mixy-presence.mjs +22 -0
  209. package/src/runtime/shared/orchestration.d.mts +2 -0
  210. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  211. package/src/runtime/shared/profile-config.cjs +1 -1
  212. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  213. package/src/runtime/shared/prompt-origin.mjs +21 -0
  214. package/src/runtime/shared/provider-accounts.mjs +7 -1
  215. package/src/runtime/shared/provider-display-name.mjs +29 -0
  216. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  217. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  218. package/src/runtime/shared/routine-trigger.mjs +10 -0
  219. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  220. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  221. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  222. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  223. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  224. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  225. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  226. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  227. package/src/runtime/terminal-bridge/client.mjs +10 -1
  228. package/src/runtime/visualize-bridge/client.mjs +27 -8
  229. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  230. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  231. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  232. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  233. package/src/session-runtime/agent-home-delivery.mjs +448 -185
  234. package/src/session-runtime/agent-task-sessions.mjs +329 -180
  235. package/src/session-runtime/boot/apis.mjs +9 -0
  236. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  237. package/src/session-runtime/boot/providers.mjs +3 -0
  238. package/src/session-runtime/boot/tools.mjs +6 -1
  239. package/src/session-runtime/config-lifecycle.mjs +6 -0
  240. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  241. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  242. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  243. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  244. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  245. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  246. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  247. package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
  248. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  249. package/src/session-runtime/mixy-heartbeat.mjs +153 -33
  250. package/src/session-runtime/notification-bus.mjs +31 -117
  251. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  252. package/src/session-runtime/notification-listeners.mjs +76 -0
  253. package/src/session-runtime/owned-automation-run.mjs +2 -1
  254. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  255. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  256. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  257. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  258. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  259. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  260. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  261. package/src/session-runtime/routine-owner.mjs +40 -9
  262. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  263. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  264. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  265. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  266. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  267. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  268. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  269. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  270. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  271. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  272. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  273. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  274. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  275. package/src/session-runtime/services/channel-admin.mjs +7 -13
  276. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  277. package/src/session-runtime/services/seeds.mjs +0 -28
  278. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  279. package/src/session-runtime/session-create/session-options.mjs +1 -0
  280. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  281. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  282. package/src/session-runtime/settings-system-api.mjs +13 -1
  283. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  284. package/src/session-runtime/setup-tool/executor.mjs +145 -22
  285. package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
  286. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  287. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
  288. package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
  289. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  290. package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
  291. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  292. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  293. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  294. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  295. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  296. package/src/standalone/channel-transport.mjs +1 -1
  297. package/src/standalone/daemon-channels-loader.mjs +10 -8
  298. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  299. package/src/standalone/daemon-session-control/request.mjs +51 -0
  300. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  301. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  302. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  303. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  304. package/src/standalone/daemon-session-control.mjs +124 -188
  305. package/src/standalone/daemon.mjs +26 -3
  306. package/src/standalone/session-client.mjs +7 -3
  307. package/src/standalone/session-protocol.mjs +5 -0
  308. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  309. package/src/standalone/session-service/quota-resume.mjs +3 -0
  310. package/src/standalone/session-service/retention/busy.mjs +65 -1
  311. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  312. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  313. package/src/standalone/session-tool-output.mjs +15 -7
  314. package/src/tui/app/model-options.mjs +3 -28
  315. package/src/tui/dist/index.mjs +58 -64
  316. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  317. package/src/tui/session/initial-state.mjs +3 -0
  318. package/src/tui/session/notice-surface.mjs +7 -1
  319. package/src/tui/session/queue-helpers.mjs +3 -42
  320. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  321. package/src/tui/session/session-api/routes-write.mjs +5 -1
  322. package/src/tui/session/session-api/routes.mjs +1 -0
  323. package/src/tui/session/session-api/settings.mjs +17 -1
  324. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  325. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  326. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  327. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  328. package/src/tui/session/session-flow/drain.mjs +118 -149
  329. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  330. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  331. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  332. package/src/tui/session/tool-approval-request.mjs +39 -0
  333. package/src/tui/session/tool-approval-settle.mjs +62 -0
  334. package/src/tui/session/tool-approval.mjs +69 -115
  335. package/src/tui/session/tui-steering-persist.mjs +8 -1
  336. package/src/tui/session/turn-stream.mjs +19 -2
  337. package/src/tui/session/turn.mjs +1 -0
  338. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  339. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  340. package/scripts/code-graph-description-contract.mjs +0 -188
  341. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -5,6 +5,7 @@ import http2 from 'node:http2';
5
5
  import { MAX_CONNECT_FRAME_BYTES, createCursorByteQueue } from './cursor-wire-guards.mjs';
6
6
  import { cursorClientVersion, warmCursorClientVersion } from './cursor-client-version.mjs';
7
7
  import { retryAfterMsFromError } from './retry-classifier.mjs';
8
+ import { providerTimeoutError } from '../stall-policy.mjs';
8
9
 
9
10
  export { warmCursorClientVersion };
10
11
 
@@ -153,7 +154,9 @@ export function openCursorStream({ accessToken, path = RUN_PATH, url = API_URL }
153
154
  if (closed || session.closed || session.destroyed) return;
154
155
  try {
155
156
  session.ping(() => {});
156
- } catch {}
157
+ } catch {
158
+ // the session may have closed since the check above
159
+ }
157
160
  }, H2_PING_INTERVAL_MS);
158
161
  ping.unref?.();
159
162
 
@@ -175,20 +178,28 @@ export function openCursorStream({ accessToken, path = RUN_PATH, url = API_URL }
175
178
  closeError = error || (status >= 400 ? httpStatusError(status, responseHeaders) : null);
176
179
  try {
177
180
  request.close();
178
- } catch {}
181
+ } catch {
182
+ // the request stream may already be closed
183
+ }
179
184
  try {
180
185
  session.close();
181
- } catch {}
186
+ } catch {
187
+ // the session may already be closed
188
+ }
182
189
  closeHandler?.(closeError);
183
190
  };
184
191
  const close = (error = null) => {
185
192
  if (closed) return;
186
193
  try {
187
194
  request.close(http2.constants.NGHTTP2_CANCEL);
188
- } catch {}
195
+ } catch {
196
+ // the request stream may already be closed
197
+ }
189
198
  try {
190
199
  session.destroy();
191
- } catch {}
200
+ } catch {
201
+ // the session may already be destroyed
202
+ }
192
203
  finish(error);
193
204
  };
194
205
 
@@ -243,17 +254,21 @@ export async function callCursorUnary({ accessToken, path, body, url = API_URL,
243
254
  let status = 0;
244
255
  let responseHeaders = null;
245
256
  let settled = false;
246
- const timeout = setTimeout(() => finish(new Error('Cursor request timed out')), timeoutMs);
257
+ const timeout = setTimeout(() => finish(providerTimeoutError('Cursor request', timeoutMs)), timeoutMs);
247
258
  const finish = (error = null) => {
248
259
  if (settled) return;
249
260
  settled = true;
250
261
  clearTimeout(timeout);
251
262
  try {
252
263
  request.close();
253
- } catch {}
264
+ } catch {
265
+ // the request stream may already be closed
266
+ }
254
267
  try {
255
268
  session.close();
256
- } catch {}
269
+ } catch {
270
+ // the session may already be closed
271
+ }
257
272
  if (error) reject(error);
258
273
  else if (status >= 400) reject(httpStatusError(status, responseHeaders));
259
274
  else resolve(Buffer.concat(chunks));
@@ -322,7 +337,8 @@ export function parseEndStream(bytes) {
322
337
  if (!payload?.error) return null;
323
338
  const connectCode = String(payload.error.code || 'error').toLowerCase();
324
339
  const detail = String(payload.error.message || 'request failed');
325
- const tooLarge = connectCode === 'resource_exhausted' && !THROTTLE_CUE_RE.test(detail) && REQUEST_TOO_LARGE_RE.test(detail);
340
+ const tooLarge =
341
+ connectCode === 'resource_exhausted' && !THROTTLE_CUE_RE.test(detail) && REQUEST_TOO_LARGE_RE.test(detail);
326
342
  const retryAfter =
327
343
  payload.error.retryAfter ??
328
344
  payload.error.retry_after ??
@@ -132,7 +132,9 @@ async function* responseSseEvents(response, signal) {
132
132
  if (signal) {
133
133
  try {
134
134
  signal.removeEventListener('abort', onAbort);
135
- } catch {}
135
+ } catch {
136
+ // the abort listener may already be gone
137
+ }
136
138
  }
137
139
  if (aborted) await reader.cancel().catch(() => {});
138
140
  }
@@ -480,16 +482,10 @@ function selectParameterizedCursorVariant(group, requested, sendOpts = {}) {
480
482
  availableVariants[0] ||
481
483
  null;
482
484
  const parameters = { ...(defaultVariant?.parameters || {}), ...aliasParameters };
485
+ const defaultContext = parameters.context;
483
486
  const routeParameters =
484
487
  sendOpts.modelParameters && typeof sendOpts.modelParameters === 'object' ? sendOpts.modelParameters : {};
485
- // The window picked the mode above: a stored `context` no variant of that
486
- // mode offers (a 256k left beside a raised 500K window) is a stale value and
487
- // yields to the mode's own; one the mode offers stays the user's choice.
488
- const staleContext =
489
- Object.hasOwn(routeParameters, 'context') &&
490
- !availableVariants.some((variant) => String(variant.parameters?.context ?? '') === String(routeParameters.context));
491
488
  for (const definition of group.definitions) {
492
- if (staleContext && definition.id === 'context') continue;
493
489
  if (Object.hasOwn(routeParameters, definition.id)) {
494
490
  parameters[definition.id] = String(routeParameters[definition.id]);
495
491
  }
@@ -508,9 +504,18 @@ function selectParameterizedCursorVariant(group, requested, sendOpts = {}) {
508
504
  ) {
509
505
  parameters.fast = sendOpts.fast === true ? 'true' : 'false';
510
506
  }
511
- const selected = availableVariants.find((variant) =>
512
- Object.entries(parameters).every(([id, value]) => String(variant.parameters?.[id] ?? '') === String(value))
513
- );
507
+ const matches = (variant) =>
508
+ Object.entries(parameters).every(([id, value]) => String(variant.parameters?.[id] ?? '') === String(value));
509
+ let selected = availableVariants.find(matches);
510
+ // Keep a supported explicit context. A persisted context can exist in this
511
+ // mode but conflict with its requested effort/fast/other options. In that
512
+ // case try only the mode's default context, preserving every other choice
513
+ // and still rejecting the request unless the full combination is offered.
514
+ if (!selected && Object.hasOwn(routeParameters, 'context')) {
515
+ if (defaultContext === undefined) delete parameters.context;
516
+ else parameters.context = defaultContext;
517
+ selected = availableVariants.find(matches);
518
+ }
514
519
  if (!selected && availableVariants.length) {
515
520
  throw new Error(`Cursor model ${group.id} does not offer the selected parameter combination`);
516
521
  }
@@ -649,7 +654,9 @@ class CursorProviderBase {
649
654
  let emittedToolCall = false;
650
655
  try {
651
656
  sendOpts.onStageChange?.('requesting');
652
- } catch {}
657
+ } catch {
658
+ // the stage observer is advisory
659
+ }
653
660
  try {
654
661
  const response = await runtime.handleChatCompletion(body, accessToken, {
655
662
  onStageChange: sendOpts.onStageChange,
@@ -664,7 +671,9 @@ class CursorProviderBase {
664
671
  });
665
672
  try {
666
673
  sendOpts.onStageChange?.('streaming');
667
- } catch {}
674
+ } catch {
675
+ // the stage observer is advisory
676
+ }
668
677
  return await consumeCompatChatCompletionStream(responseSseEvents(response, signal), {
669
678
  signal,
670
679
  label: this.name,
@@ -67,10 +67,10 @@ export function sameAccountIdentity(a, b) {
67
67
  return Boolean(left.email && right.email && left.email.toLowerCase() === right.email.toLowerCase());
68
68
  }
69
69
 
70
- // expires_at may arrive as a unix number (seconds or milliseconds) or an
71
- // ISO-8601 string. Normalize to epoch milliseconds; 0 means unknown.
70
+ // expires_at may arrive as a unix number (seconds or milliseconds), a numeric
71
+ // string, or an ISO-8601 string. Normalize to epoch milliseconds; 0 means unknown.
72
72
  export function normalizeExpiresAtMs(value) {
73
- if (typeof value === 'string') {
73
+ if (typeof value === 'string' && !Number.isFinite(Number(value.trim() || NaN))) {
74
74
  const ms = Date.parse(value);
75
75
  return Number.isFinite(ms) ? ms : 0;
76
76
  }
@@ -79,6 +79,20 @@ export function normalizeExpiresAtMs(value) {
79
79
  return n < 1e12 ? n * 1000 : n;
80
80
  }
81
81
 
82
+ /**
83
+ * Whether a stored `{ access_token, refresh_token?, expires_at }` document is a
84
+ * sign-in: it can refresh, or its access token is known to be unexpired. A copy
85
+ * made without a refresh token (an isolated dev instance, so it cannot rotate
86
+ * the original's) coasts on the access token until then; one with no known
87
+ * expiry and no refresh token is incomplete, not a sign-in.
88
+ */
89
+ export function isSignedInTokenDocument(raw) {
90
+ if (!raw?.access_token) return false;
91
+ if (raw.refresh_token) return true;
92
+ const expiresAt = normalizeExpiresAtMs(raw.expires_at ?? raw.expiresAt) || expiryFromAccessToken(raw.access_token);
93
+ return expiresAt > Date.now();
94
+ }
95
+
82
96
  /**
83
97
  * Status record for a stored OAuth credential that has an access token.
84
98
  * `expiresAt` is epoch ms (0 = unknown); `detail` labels the credential source.
@@ -274,7 +274,7 @@ function* walkContentParts(content) {
274
274
  * beforehand by preparePdfTextForProvider, so one reaching here is a bug.
275
275
  * Null when the part is not a file at all.
276
276
  */
277
- export function textFormOfFilePart(part) {
277
+ function textFormOfFilePart(part) {
278
278
  if (part?.type !== 'file' && part?.type !== 'document') return null;
279
279
  try {
280
280
  const file = fileInfo(part);
@@ -840,6 +840,56 @@ function partCarriesLiveMedia(part) {
840
840
  return Array.isArray(part.content) && contentCarriesLiveMedia(part.content);
841
841
  }
842
842
 
843
+ /** An image part in any spelling the providers use: inline, URL, file id or
844
+ * URI, Gemini inline data. */
845
+ function isImagePart(part) {
846
+ return Boolean(
847
+ part.type === 'image' ||
848
+ part.type === 'image_url' ||
849
+ part.type === 'input_image' ||
850
+ imageUrlFromPart(part) ||
851
+ imageFileIdFromPart(part) ||
852
+ imageFileUriFromPart(part) ||
853
+ geminiInlineInfo(part)
854
+ );
855
+ }
856
+
857
+ function mapPartText(part, mapText) {
858
+ if (typeof part === 'string') return mapText(part);
859
+ if (!part || typeof part !== 'object') return part;
860
+ // Media and attachment references are encodings, never text.
861
+ if (isAttachmentReference(part) || isImagePart(part) || fileInfo(part)) return part;
862
+ for (const key of ['text', 'content']) {
863
+ if (typeof part[key] === 'string') {
864
+ const next = mapText(part[key]);
865
+ return next === part[key] ? part : { ...part, [key]: next };
866
+ }
867
+ }
868
+ if (Array.isArray(part.content)) {
869
+ const nextContent = mapContentText(part.content, mapText);
870
+ if (nextContent !== part.content) return { ...part, content: nextContent };
871
+ }
872
+ return part;
873
+ }
874
+
875
+ /** `content` with `mapText` applied to its text and nothing else: a string,
876
+ * the text a text part or a part's string content carries (textFromPart),
877
+ * and nested part content; media parts and attachment references stay as
878
+ * they are. The same value when nothing changed. */
879
+ export function mapContentText(content, mapText) {
880
+ if (typeof content === 'string') return mapText(content);
881
+ const parts = contentParts(content);
882
+ if (!parts) return content;
883
+ let changed = false;
884
+ const out = parts.map((part) => {
885
+ const next = mapPartText(part, mapText);
886
+ if (next !== part) changed = true;
887
+ return next;
888
+ });
889
+ if (!changed) return content;
890
+ return Array.isArray(content) ? out : { ...content, content: out };
891
+ }
892
+
843
893
  function sanitizePartForStoredHistory(part) {
844
894
  if (typeof part === 'string') return part;
845
895
  if (!part || typeof part !== 'object') return part;
@@ -849,15 +899,7 @@ function sanitizePartForStoredHistory(part) {
849
899
  // reloading the session keeps the provider prompt cache intact.
850
900
  const storedImage = part.type === 'image' ? storedImagePart(part) : null;
851
901
  if (storedImage) return storedImage;
852
- if (
853
- part.type === 'image' ||
854
- part.type === 'image_url' ||
855
- part.type === 'input_image' ||
856
- imageUrlFromPart(part) ||
857
- imageFileIdFromPart(part) ||
858
- imageFileUriFromPart(part) ||
859
- geminiInlineInfo(part)
860
- ) {
902
+ if (isImagePart(part)) {
861
903
  return { type: 'text', text: storedHistoryImagePlaceholder(part) };
862
904
  }
863
905
  const file = fileInfo(part);
@@ -5,7 +5,14 @@ export const MODEL_PROXY_OPERATIONS: Readonly<{
5
5
  resolve: 'modelProxyResolve';
6
6
  heartbeat: 'modelProxyHeartbeat';
7
7
  }>;
8
- export type ModelProxyMethod = 'send' | 'listModels' | 'refreshModels' | 'isAvailable' | 'resolveWireRoute' | 'providerConfig';
8
+ export type ModelProxyMethod =
9
+ | 'send'
10
+ | 'listModels'
11
+ | 'refreshModels'
12
+ | 'isAvailable'
13
+ | 'quotaUsable'
14
+ | 'resolveWireRoute'
15
+ | 'providerConfig';
9
16
  export const MODEL_PROXY_METHODS: ReadonlyArray<ModelProxyMethod>;
10
17
  export const MODEL_PROXY_HEARTBEAT_MS: number;
11
18
  export const MODEL_PROXY_LEASE_MS: number;
@@ -34,6 +34,7 @@ export const MODEL_PROXY_METHODS = Object.freeze([
34
34
  'listModels',
35
35
  'refreshModels',
36
36
  'isAvailable',
37
+ 'quotaUsable',
37
38
  'resolveWireRoute',
38
39
  'providerConfig',
39
40
  ]);
@@ -8,6 +8,7 @@
8
8
  // so a new process (or an offline one) starts from the newest version this
9
9
  // machine has seen rather than from a floor that only moves with a release.
10
10
 
11
+ import { runAbortable } from '../../../shared/abort-race.mjs';
11
12
  import { readLastKnownVersion, rememberLastKnownVersion } from './client-version-store.mjs';
12
13
 
13
14
  const SEMVER = /^\d+\.\d+\.\d+$/;
@@ -41,11 +42,18 @@ export async function parseNpmLatest(res) {
41
42
  return SEMVER.test(v) ? v : null;
42
43
  }
43
44
 
44
- /** One GET of `url` parsed to a version; null when offline, slow, or unrecognised. */
45
+ /** One GET of `url` parsed to a version; null when offline, slow, or unrecognised.
46
+ * The deadline settles the lookup itself instead of relying on the request
47
+ * and its body read to honour the abort: a send awaits this lookup, and its
48
+ * timer is unref'd, so once the abort tears the socket down nothing else is
49
+ * left to wake a read that missed it. */
45
50
  export async function fetchRemoteVersion(url, parse, timeoutMs = TIMEOUT_MS) {
51
+ const signal = AbortSignal.timeout(timeoutMs);
46
52
  try {
47
- const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
48
- return res.ok ? (await parse(res)) || null : null;
53
+ return await runAbortable(signal, async () => {
54
+ const res = await fetch(url, { signal });
55
+ return res.ok ? (await parse(res)) || null : null;
56
+ });
49
57
  } catch {
50
58
  return null;
51
59
  }
@@ -5,6 +5,7 @@ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
5
5
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
6
6
  import { cursorTokenExpiry } from './cursor-auth.mjs';
7
7
  import { describeDevinOAuthCredentials } from './devin-auth.mjs';
8
+ import { isSignedInTokenDocument } from './lib/oauth-token-utils.mjs';
8
9
  import { isValidMuseCredentialDocument } from './muse-code-oauth-tokens.mjs';
9
10
  import { isValidOpenCodeGoCredentialDocument } from './opencode-go-oauth-tokens.mjs';
10
11
  import { developerOptionEnabled, developerOptionForProvider } from '../../../shared/developer-options.mjs';
@@ -152,7 +153,7 @@ function openAIOAuthState() {
152
153
  join(resolvePluginData(), 'openai-oauth.json'),
153
154
  ];
154
155
  return memoProbe('openai-oauth', paths, () =>
155
- resolveProbeState(paths, (docs) => docs.some((raw) => !!(raw?.access_token && raw?.refresh_token)))
156
+ resolveProbeState(paths, (docs) => docs.some((raw) => isSignedInTokenDocument(raw)))
156
157
  );
157
158
  }
158
159
 
@@ -18,12 +18,15 @@ import { getPluginData } from '../config.mjs';
18
18
  import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
19
19
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
20
20
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
21
+ import { TIMEOUT_PHASE } from '../../../shared/provider-timeout-phase.mjs';
21
22
  import { modelProxyClientMode, serverModelCredentialsDisabledError } from '../../../shared/model-proxy-mode.mjs';
22
23
  import {
23
24
  accountIdentityFields,
24
25
  decodeJwtPayload,
25
26
  expiryFromAccessToken,
27
+ isSignedInTokenDocument,
26
28
  normalizeAccountIdentity,
29
+ normalizeExpiresAtMs,
27
30
  oauthCredentialStatus,
28
31
  } from './lib/oauth-token-utils.mjs';
29
32
  import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
@@ -60,11 +63,8 @@ function getRefreshLockPath() {
60
63
  return `${getOwnTokenPath()}.refresh.lock`;
61
64
  }
62
65
 
63
- function _normalizeExpiresAt(value) {
64
- const n = Number(value || 0);
65
- if (!Number.isFinite(n) || n <= 0) return 0;
66
- return n < 1e12 ? n * 1000 : n;
67
- }
66
+ // One normalizer for the store and the sign-in predicate (isSignedInTokenDocument).
67
+ const _normalizeExpiresAt = normalizeExpiresAtMs;
68
68
 
69
69
  export function extractAccountId(token) {
70
70
  return decodeJwtPayload(token)?.['https://api.openai.com/auth']?.chatgpt_account_id;
@@ -117,7 +117,7 @@ export function loadTokens() {
117
117
  try {
118
118
  const stat = statSync(ownPath);
119
119
  const own = JSON.parse(readFileSync(ownPath, 'utf-8'));
120
- if (own.access_token && own.refresh_token) {
120
+ if (isSignedInTokenDocument(own)) {
121
121
  const identity = identityFromTokens(own.id_token, own.access_token);
122
122
  return {
123
123
  ...own,
@@ -144,8 +144,7 @@ export function saveTokens(tokens) {
144
144
  // same loader the runtime uses (loadTokens), no parallel hard-coded path probe.
145
145
  export function hasOpenAIOAuthCredentials() {
146
146
  try {
147
- const tokens = loadTokens();
148
- return !!(tokens?.access_token && tokens?.refresh_token);
147
+ return !!loadTokens()?.access_token;
149
148
  } catch {
150
149
  return false;
151
150
  }
@@ -256,7 +255,9 @@ async function exchangeRefreshToken(refreshToken, previousIdentity = null) {
256
255
  return tokens;
257
256
  } catch (err) {
258
257
  if (err?.name === 'AbortError')
259
- throw new Error(`OpenAI OAuth token refresh timed out after ${TOKEN_REFRESH_TIMEOUT_MS}ms`);
258
+ throw Object.assign(new Error(`OpenAI OAuth token refresh timed out after ${TOKEN_REFRESH_TIMEOUT_MS}ms`), {
259
+ timeoutPhase: TIMEOUT_PHASE.AUTH,
260
+ });
260
261
  throw err;
261
262
  } finally {
262
263
  clearTimeout(timeout);
@@ -7,13 +7,17 @@ import { createStreamSafetyStamps, resolveStallRetryBudget } from '../retry-clas
7
7
  import { _codexWsCompatibilityHeaders } from '../openai-codex-metadata.mjs';
8
8
  import { createWsSendAttempts } from '../openai-ws-send-attempts.mjs';
9
9
  import { createWsSendSpan } from '../openai-ws-send-span.mjs';
10
+ import { emitProviderRetryStage } from '../retry-executor.mjs';
11
+ import { retryReasonClass } from '../provider-error-kind.mjs';
10
12
 
11
13
  /** Stage callbacks are observers; a throwing one never fails the send. */
12
14
  export function notifyStage(onStageChange, stage, detail) {
13
15
  try {
14
16
  if (detail === undefined) onStageChange?.(stage);
15
17
  else onStageChange?.(stage, detail);
16
- } catch {}
18
+ } catch {
19
+ // the stage observer is advisory
20
+ }
17
21
  }
18
22
 
19
23
  export function createWsSendContext(opts) {
@@ -81,10 +85,24 @@ export function createWsSendContext(opts) {
81
85
  // Single caller-visible recovery path for both handshake/acquire retries
82
86
  // and retryable stream failures. The session/TUI stage bridge renders this
83
87
  // as non-terminal reconnect progress; transport code must not also print it
84
- // to stderr.
85
- const emitReconnectProgress = ({ attempt, max, classifier }) => {
88
+ // to stderr. A retry that knows its failure carries the reason and the wait
89
+ // (retryAt), so a long server-advised wait counts down with its cause
90
+ // instead of sitting silently at "n/max".
91
+ const emitReconnectProgress = ({ attempt, max, classifier, error = null, waitMs = 0 }) => {
86
92
  const retryAttempt = Number(attempt) || 1;
87
93
  const retryMax = Number(max) || 1;
94
+ if (error) {
95
+ // The reason shown is the fixed class, never the server's own text.
96
+ emitProviderRetryStage(onStageChange, {
97
+ attempt: retryAttempt,
98
+ maxAttempts: retryMax,
99
+ lastErr: error,
100
+ delayMs: waitMs,
101
+ classifier,
102
+ reasonClass: retryReasonClass(error),
103
+ });
104
+ return;
105
+ }
88
106
  notifyStage(onStageChange, 'reconnecting', {
89
107
  attempt: retryAttempt,
90
108
  max: retryMax,
@@ -105,6 +123,7 @@ export function createWsSendContext(opts) {
105
123
  retry429,
106
124
  stallRetryBudget,
107
125
  trace: { poolKey, traceProvider, useModel },
126
+ traceFn: _agentTraceFn,
108
127
  auth,
109
128
  body,
110
129
  });
@@ -3,7 +3,34 @@
3
3
  * WS send, plus the surface/backoff primitives both failure resolvers use.
4
4
  */
5
5
  import { performance } from 'node:perf_hooks';
6
+ import { appendAgentTrace } from '../../agent-trace.mjs';
6
7
  import { _sleepWithAbort, MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT, midstreamBackoffFor, tag } from './policy.mjs';
8
+ import { retryReasonClass, safeRetryErrorCode } from '../provider-error-kind.mjs';
9
+
10
+ /** One row per WS retry: the server's typed verdict (status, close code, an
11
+ * identifier-shaped code, the reason class) and the wait it bought, so a
12
+ * long advised wait is diagnosable after the fact. Never the server's free
13
+ * text: it can echo request content. Diagnostics only; never throws. */
14
+ function traceRetry(traceFn, trace, err, { attempt, max, classifier, waitMs }) {
15
+ try {
16
+ const payload = {
17
+ provider: trace?.traceProvider || null,
18
+ model: trace?.useModel || null,
19
+ transport: 'websocket',
20
+ attempt,
21
+ max,
22
+ classifier: classifier || null,
23
+ reason: retryReasonClass(err),
24
+ wait_ms: Math.round(Number(waitMs) || 0),
25
+ http_status: Number(err?.httpStatus || err?.status || 0) || null,
26
+ code: safeRetryErrorCode(err),
27
+ ws_close_code: Number(err?.wsCloseCode || 0) || null,
28
+ };
29
+ traceFn({ sessionId: trace?.poolKey || null, kind: 'ws_retry', ...payload, payload });
30
+ } catch {
31
+ // the retry trace sink is advisory
32
+ }
33
+ }
7
34
 
8
35
  export function createAttemptContext(deps) {
9
36
  const { sendSpan, externalSignal, sleepFn, safetyStamps } = deps;
@@ -31,10 +58,13 @@ export function createAttemptContext(deps) {
31
58
  };
32
59
  // Server advice (Retry-After, or a typed rate limit's "try again in")
33
60
  // replaces the local curve, like the reference client's retry deadline.
34
- const backoff = async (retryNumber, err = null) => {
61
+ // Resolved before the retry is announced so the reconnect stage can name
62
+ // the wait it is about to sit through.
63
+ const retryWaitMs = (retryNumber, err = null) => midstreamBackoffFor(retryNumber, err);
64
+ const backoff = async (waitMs) => {
35
65
  const sleepStart = performance.now();
36
66
  try {
37
- await _sleepWithAbort(midstreamBackoffFor(retryNumber, err), externalSignal, sleepFn);
67
+ await _sleepWithAbort(waitMs, externalSignal, sleepFn);
38
68
  } catch (sleepErr) {
39
69
  sendSpan.retryBackoffMs += performance.now() - sleepStart;
40
70
  sendSpan.emit('error');
@@ -53,14 +83,21 @@ export function createAttemptContext(deps) {
53
83
  }
54
84
  return first;
55
85
  };
56
- /** Stash the error a retry is about to leave behind and announce the retry. */
57
- const scheduleRetry = (err, { attemptIndex, classifier, retryLimit, remember }) => {
86
+ /** Stash the error a retry is about to leave behind and announce the retry
87
+ * with its cause and the wait before the next attempt. */
88
+ const scheduleRetry = (err, { attemptIndex, classifier, retryLimit, remember, waitMs = 0 }) => {
58
89
  if (remember) {
59
90
  state.firstAttemptError = err;
60
91
  state.firstAttemptClassifier = classifier;
61
92
  }
62
93
  tag(err, { midstreamClassifier: classifier });
63
- deps.emitReconnectProgress({ attempt: attemptIndex + 1, max: retryLimit, classifier });
94
+ deps.emitReconnectProgress({ attempt: attemptIndex + 1, max: retryLimit, classifier, error: err, waitMs });
95
+ traceRetry(deps.traceFn || appendAgentTrace, deps.trace, err, {
96
+ attempt: attemptIndex + 1,
97
+ max: retryLimit,
98
+ classifier,
99
+ waitMs,
100
+ });
64
101
  };
65
102
 
66
103
  return {
@@ -69,6 +106,7 @@ export function createAttemptContext(deps) {
69
106
  maxMidstreamRetries: MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT,
70
107
  stampAll,
71
108
  surface,
109
+ retryWaitMs,
72
110
  backoff,
73
111
  surfaceFirstAttempt,
74
112
  scheduleRetry,
@@ -81,13 +81,15 @@ export async function resolveHandshakeFailure(
81
81
  const retryable =
82
82
  classifier && status !== 401 && status !== 426 && err?.unsafeToRetry !== true && !externalSignal?.aborted;
83
83
  if (retryable && attemptIndex < ctx.maxMidstreamRetries) {
84
+ const waitMs = ctx.retryWaitMs(attemptIndex + 1, err);
84
85
  ctx.scheduleRetry(err, {
85
86
  attemptIndex,
86
87
  classifier,
87
88
  retryLimit: ctx.maxMidstreamRetries,
88
89
  remember: !state.firstAttemptError,
90
+ waitMs,
89
91
  });
90
- await ctx.backoff(attemptIndex + 1, err);
92
+ await ctx.backoff(waitMs);
91
93
  return true;
92
94
  }
93
95
  // A later auth/upgrade decision must win over an earlier transient
@@ -77,7 +77,9 @@ export async function resolveStreamFailure(ctx, err, { attemptIndex, entry, midS
77
77
  process.stderr.write(
78
78
  `[openai-oauth] stall retry budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first stall) — surfacing provider-terminal failure\n`
79
79
  );
80
- } catch {}
80
+ } catch {
81
+ // diagnostic write only; the provider-terminal failure is thrown next
82
+ }
81
83
  throw ctx.surface(markProviderRecoveryExhausted(err, { owner: 'openai-oauth-ws-stall-budget' }));
82
84
  }
83
85
  const retryLimit = classifier ? midstreamRetryLimit(classifier) : 0;
@@ -89,8 +91,9 @@ export async function resolveStreamFailure(ctx, err, { attemptIndex, entry, midS
89
91
  state.carryForwardCache = null;
90
92
  tag(entry, { lastResponseId: null });
91
93
  }
92
- ctx.scheduleRetry(err, { attemptIndex, classifier, retryLimit, remember: true });
93
- await ctx.backoff(attemptIndex + 1, err);
94
+ const waitMs = ctx.retryWaitMs(attemptIndex + 1, err);
95
+ ctx.scheduleRetry(err, { attemptIndex, classifier, retryLimit, remember: true, waitMs });
96
+ await ctx.backoff(waitMs);
94
97
  return true;
95
98
  }
96
99
  // Not retryable, OR we've already exhausted the retry budget. Do not