mixdog 1.2.6 → 1.3.1

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 (294) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +71 -9
  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/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +14 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +178 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent/AGENT.md +4 -0
  18. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  19. package/src/rules/agent-mode/MIXY.md +28 -0
  20. package/src/rules/agent-mode/ROUTINE.md +6 -0
  21. package/src/rules/lead/LEAD.md +7 -3
  22. package/src/rules/shared/70-delivery.md +3 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  24. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  25. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
  26. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  27. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
  28. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  29. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  31. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  34. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  35. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  36. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  37. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  44. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  46. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  52. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  53. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  61. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
  63. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
  64. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  66. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  67. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  68. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  69. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  70. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
  71. package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
  72. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  73. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  75. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
  76. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  77. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  78. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  79. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  80. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  81. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  82. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  83. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  84. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
  85. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  86. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  87. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  88. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  89. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
  90. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  91. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  92. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  93. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  94. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  96. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  97. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  98. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  99. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  100. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
  101. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
  102. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  103. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  105. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  106. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
  108. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  109. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
  110. package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
  111. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  112. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  114. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  115. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  116. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
  118. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  119. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  121. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  123. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  124. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  125. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  127. package/src/runtime/attachments/store.mjs +6 -0
  128. package/src/runtime/browser-bridge/client.mjs +24 -0
  129. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  130. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  131. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  132. package/src/runtime/channels/lib/webhook.mjs +0 -1
  133. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  134. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  135. package/src/runtime/computer-bridge/client.mjs +28 -1
  136. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  137. package/src/runtime/connectors/google-mcp.mjs +446 -0
  138. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  139. package/src/runtime/effort-judge/model-manifest.json +7 -7
  140. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  141. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  142. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  143. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  144. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  145. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  146. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  147. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  148. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  149. package/src/runtime/memory/lib/memory.mjs +7 -1
  150. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  151. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  152. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  153. package/src/runtime/memory/tool-defs.mjs +5 -0
  154. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  155. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  156. package/src/runtime/shared/agent-avatar.mjs +320 -0
  157. package/src/runtime/shared/agent-kind.mjs +152 -0
  158. package/src/runtime/shared/agent-route-config.d.mts +4 -0
  159. package/src/runtime/shared/agent-route-config.mjs +30 -0
  160. package/src/runtime/shared/agent-store.mjs +132 -0
  161. package/src/runtime/shared/atomic-file.mjs +14 -1
  162. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  163. package/src/runtime/shared/config-patch.mjs +20 -1
  164. package/src/runtime/shared/config.mjs +8 -1
  165. package/src/runtime/shared/connector-accounts.mjs +15 -0
  166. package/src/runtime/shared/connector-catalog.d.mts +51 -0
  167. package/src/runtime/shared/connector-catalog.mjs +228 -0
  168. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  169. package/src/runtime/shared/connector-triggers.mjs +45 -0
  170. package/src/runtime/shared/err-text.mjs +3 -1
  171. package/src/runtime/shared/error-presentation.mjs +91 -6
  172. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  173. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  174. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  175. package/src/runtime/shared/held-request-host.d.mts +12 -0
  176. package/src/runtime/shared/held-request-host.mjs +71 -0
  177. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  178. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  179. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  180. package/src/runtime/shared/plain-preview.d.mts +1 -0
  181. package/src/runtime/shared/plain-preview.mjs +33 -0
  182. package/src/runtime/shared/routine-limits.d.mts +11 -0
  183. package/src/runtime/shared/routine-limits.mjs +21 -0
  184. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  185. package/src/runtime/shared/routine-trigger.mjs +44 -0
  186. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  187. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  188. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  189. package/src/runtime/shared/slow-span.mjs +4 -12
  190. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  191. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  192. package/src/runtime/shared/turn-changes.mjs +0 -8
  193. package/src/runtime/shared/usage-readout-totals.mjs +58 -0
  194. package/src/runtime/shared/user-data-guard.mjs +119 -38
  195. package/src/session-runtime/agent-home-delivery.mjs +826 -0
  196. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  197. package/src/session-runtime/agent-room-delivery.mjs +133 -0
  198. package/src/session-runtime/agent-task-sessions.mjs +460 -0
  199. package/src/session-runtime/boot/apis.mjs +63 -18
  200. package/src/session-runtime/boot/facade.mjs +5 -8
  201. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  202. package/src/session-runtime/boot/providers.mjs +14 -4
  203. package/src/session-runtime/boot/tools.mjs +3 -0
  204. package/src/session-runtime/channel-config-api.mjs +53 -5
  205. package/src/session-runtime/config-lifecycle.mjs +43 -2
  206. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  207. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  208. package/src/session-runtime/handoff-attachments.mjs +593 -0
  209. package/src/session-runtime/held-requests.mjs +228 -0
  210. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  211. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  212. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  213. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
  214. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  215. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  216. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  217. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  218. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  219. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  220. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  221. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  222. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  223. package/src/session-runtime/model-route/set-route.mjs +35 -4
  224. package/src/session-runtime/native-web-search.mjs +2 -2
  225. package/src/session-runtime/new-session-config.mjs +6 -1
  226. package/src/session-runtime/owned-automation-run.mjs +229 -0
  227. package/src/session-runtime/resource-api.mjs +5 -0
  228. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  229. package/src/session-runtime/resource-skills-api.mjs +12 -0
  230. package/src/session-runtime/room-member-gate.mjs +48 -0
  231. package/src/session-runtime/route-resolution.mjs +5 -11
  232. package/src/session-runtime/routine-home-turn.mjs +156 -0
  233. package/src/session-runtime/routine-owner.mjs +48 -0
  234. package/src/session-runtime/schedule-session-run.mjs +30 -64
  235. package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
  236. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  237. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  238. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  239. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  240. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  241. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  242. package/src/session-runtime/services/channel-admin.mjs +93 -24
  243. package/src/session-runtime/services/memory-access.mjs +37 -0
  244. package/src/session-runtime/services/provider-admin.mjs +74 -39
  245. package/src/session-runtime/services/schedule-access.mjs +91 -0
  246. package/src/session-runtime/session-create/session-options.mjs +34 -7
  247. package/src/session-runtime/session-hooks.mjs +16 -8
  248. package/src/session-runtime/setup-tool/executor.mjs +79 -6
  249. package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
  250. package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
  251. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  252. package/src/session-runtime/skills-api.mjs +16 -1
  253. package/src/session-runtime/tool-policy-refresh.mjs +249 -29
  254. package/src/session-runtime/turn/session-ops.mjs +8 -1
  255. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  256. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
  257. package/src/session-runtime/webhook-session-run.mjs +49 -67
  258. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  259. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
  260. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  261. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  262. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  263. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  264. package/src/standalone/daemon-agent-control.mjs +1 -8
  265. package/src/standalone/daemon-session-control.mjs +21 -4
  266. package/src/standalone/daemon.mjs +31 -5
  267. package/src/standalone/session-protocol.mjs +27 -0
  268. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  269. package/src/standalone/session-service/agent-tree.mjs +4 -3
  270. package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
  271. package/src/standalone/session-service.mjs +19 -0
  272. package/src/tui/app/slash-commands.mjs +20 -0
  273. package/src/tui/dist/index.mjs +234 -95
  274. package/src/tui/session/context-state.mjs +6 -1
  275. package/src/tui/session/notice-surface.mjs +34 -4
  276. package/src/tui/session/queue-helpers.mjs +2 -1
  277. package/src/tui/session/session-api/extensions.mjs +4 -0
  278. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  279. package/src/tui/session/session-api/integrations.mjs +8 -0
  280. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  281. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  282. package/src/tui/session/session-api/routes.mjs +61 -5
  283. package/src/tui/session/session-flow/drain.mjs +12 -3
  284. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  285. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  286. package/src/tui/session/session-flow/steering.mjs +58 -24
  287. package/src/tui/session/tool-approval.mjs +26 -5
  288. package/src/tui/session/transcript-intake.mjs +8 -2
  289. package/src/tui/session/transcript-spill.mjs +5 -1
  290. package/src/tui/session/tui-steering-persist.mjs +65 -42
  291. package/src/tui/session/turn-live-call.mjs +64 -0
  292. package/src/tui/session/turn.mjs +34 -8
  293. package/src/session-runtime/automation-agents.mjs +0 -94
  294. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -0,0 +1,195 @@
1
+ // session/human-gate/requests.mjs
2
+ // The data model of a human-in-the-loop prompt. A request has one `kind`:
3
+ // approval — allow or deny one tool call;
4
+ // outbound — a call that posts, sends, pays or deletes outside (its action
5
+ // kind, target, account, the exact payload text, which fields
6
+ // the card may edit, and the mail/payment/delete detail lines);
7
+ // secret — the user types a value into a target field; the request names
8
+ // the target and the field label, never a value;
9
+ // takeover — the user does a step themselves and says "done";
10
+ // permission — the user picks one of the offered scopes.
11
+ // A response is one of HUMAN_RESPONSES plus a `remember` scope; a policy lists
12
+ // the scopes it allows and can leave out `always`. Nothing here ever carries a
13
+ // secret value: a decision that names one is stripped to its verdict.
14
+
15
+ export const HUMAN_REQUEST_KINDS = Object.freeze(['approval', 'outbound', 'secret', 'takeover', 'permission']);
16
+ export const HUMAN_RESPONSES = Object.freeze(['approve', 'deny', 'edit-and-approve', 'provide-secret', 'skip', 'done']);
17
+ export const REMEMBER_SCOPES = Object.freeze(['once', 'agent', 'always']);
18
+ export const SECRET_REQUEST_TIMEOUT_MS = 120_000;
19
+
20
+ const RESPONSES_BY_KIND = Object.freeze({
21
+ approval: ['approve', 'deny'],
22
+ outbound: ['approve', 'edit-and-approve', 'deny'],
23
+ secret: ['provide-secret', 'skip'],
24
+ takeover: ['done', 'deny'],
25
+ permission: ['approve', 'deny'],
26
+ });
27
+ const GRANTING_RESPONSES = new Set(['approve', 'edit-and-approve', 'provide-secret', 'done']);
28
+ const TEXT_LIMIT = 200;
29
+ /** An outbound card's extra lines: mail headers, the payment, the delete, the
30
+ * trash card's message list (gate.mjs describeEmails) and its remainder. */
31
+ const DETAIL_FIELDS = [
32
+ 'to',
33
+ 'cc',
34
+ 'subject',
35
+ 'merchant',
36
+ 'amount',
37
+ 'currency',
38
+ 'recurrence',
39
+ 'count',
40
+ 'size',
41
+ 'messages',
42
+ 'more',
43
+ ];
44
+ const DETAIL_LIMITS = { messages: 1_000 };
45
+ const PAYLOAD_LIMIT = 40_000;
46
+
47
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
48
+ const text = (value, limit = TEXT_LIMIT) => (typeof value === 'string' ? value.trim().slice(0, limit) : '');
49
+
50
+ /** The human kind of a request; anything else (ask_user's 'question', a
51
+ * hook's bare ask) is an ordinary approval for responses. */
52
+ export function humanRequestKind(request) {
53
+ return HUMAN_REQUEST_KINDS.includes(request?.kind) ? request.kind : 'approval';
54
+ }
55
+
56
+ /** A login takeover's sign-in page: https, or http on loopback only (a local
57
+ * site); never embedded credentials. `{ url, origin }`, or null. */
58
+ export function loginPageUrl(value) {
59
+ if (typeof value !== 'string' || !value.trim() || value.length > 2048) return null;
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(value.trim());
63
+ } catch {
64
+ return null;
65
+ }
66
+ const host = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
67
+ const loopback =
68
+ host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host);
69
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback)) return null;
70
+ if (parsed.username || parsed.password) return null;
71
+ return { url: parsed.href, origin: parsed.origin };
72
+ }
73
+
74
+ function rememberScopes(value) {
75
+ const listed = Array.isArray(value) ? value.filter((scope) => REMEMBER_SCOPES.includes(scope)) : [];
76
+ return ['once', ...new Set(listed.filter((scope) => scope !== 'once'))];
77
+ }
78
+
79
+ /** The kind-specific fields of a request, whitelisted. Spread onto the
80
+ * presented request; `args` is replaced for a secret so no value rides it. */
81
+ export function humanRequestFields(input = {}) {
82
+ const kind = input?.kind;
83
+ if (!HUMAN_REQUEST_KINDS.includes(kind)) return {};
84
+ const fields = { remember: rememberScopes(input.remember) };
85
+ // The agent the request acts for (its name on the card and the remembered grant).
86
+ if (text(input.agentId)) fields.agentId = text(input.agentId);
87
+ if (kind === 'outbound' && isRecord(input.outbound)) {
88
+ const outbound = input.outbound;
89
+ const details = isRecord(outbound.details) ? outbound.details : {};
90
+ fields.outbound = {
91
+ category: text(outbound.category),
92
+ target: text(outbound.target),
93
+ account: text(outbound.account),
94
+ payload: typeof outbound.payload === 'string' ? outbound.payload.slice(0, PAYLOAD_LIMIT) : '',
95
+ editable: Array.isArray(outbound.editable) ? outbound.editable.filter((name) => typeof name === 'string') : [],
96
+ details: Object.fromEntries(
97
+ DETAIL_FIELDS.filter((name) => text(details[name])).map((name) => [
98
+ name,
99
+ text(details[name], DETAIL_LIMITS[name]),
100
+ ])
101
+ ),
102
+ };
103
+ }
104
+ if (kind === 'secret') {
105
+ const secret = isRecord(input.secret) ? input.secret : {};
106
+ fields.secret = {
107
+ requestId: text(secret.requestId),
108
+ surface: secret.surface === 'computer' ? 'computer' : 'browser',
109
+ target: text(secret.target),
110
+ field: text(secret.field),
111
+ };
112
+ fields.args = { surface: fields.secret.surface, target: fields.secret.target, field: fields.secret.field };
113
+ fields.remember = ['once'];
114
+ }
115
+ if (kind === 'takeover') {
116
+ const takeover = isRecord(input.takeover) ? input.takeover : {};
117
+ const surface = takeover.surface === 'computer' ? 'computer' : 'browser';
118
+ // The window it is about (ask-user-tool-defs.mjs handoffIdentity): a
119
+ // browser page ID, or a computer window id and app, so the card can bring
120
+ // exactly that window forward on the user's click.
121
+ const token = (value, limit) => (/^[\w.:#@-]+$/.test(text(value, limit)) ? text(value, limit) : '');
122
+ const identity =
123
+ surface === 'browser'
124
+ ? { ...(token(takeover.tab, 64) ? { tab: token(takeover.tab, 64) } : {}) }
125
+ : {
126
+ ...(token(takeover.windowId, 128) ? { windowId: token(takeover.windowId, 128) } : {}),
127
+ ...(text(takeover.app, 120) ? { app: text(takeover.app, 120) } : {}),
128
+ };
129
+ // A login takeover (purpose 'login') opens the desktop's sign-in window on
130
+ // its validated page instead of handing over the side panel.
131
+ const login = surface === 'browser' && takeover.purpose === 'login' ? loginPageUrl(takeover.url) : null;
132
+ fields.takeover = {
133
+ surface,
134
+ instructions: text(takeover.instructions, 2_000),
135
+ ...(text(takeover.target) ? { target: text(takeover.target) } : {}),
136
+ ...identity,
137
+ ...(login ? { purpose: 'login', url: login.url, origin: login.origin } : {}),
138
+ };
139
+ fields.remember = ['once'];
140
+ }
141
+ if (kind === 'permission') {
142
+ const scopes = Array.isArray(input.permission?.scopes) ? input.permission.scopes : [];
143
+ fields.permission = {
144
+ scopes: scopes
145
+ .filter((scope) => isRecord(scope) && text(scope.id))
146
+ .map((scope) => ({ id: text(scope.id), label: text(scope.label) || text(scope.id) })),
147
+ };
148
+ }
149
+ return fields;
150
+ }
151
+
152
+ /** Only `editable` string fields of `editedArgs`; {} when nothing applies. */
153
+ function editableArgs(editedArgs, editable) {
154
+ if (!isRecord(editedArgs)) return {};
155
+ const out = {};
156
+ for (const name of editable || []) {
157
+ if (typeof editedArgs[name] === 'string') out[name] = editedArgs[name].slice(0, PAYLOAD_LIMIT);
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /**
163
+ * The canonical response to `request` from a decision: `approved` (the card's
164
+ * polarity) plus optional `{ response, editedArgs, remember, scope }`. An
165
+ * unknown or contradicting response falls back to the polarity's default;
166
+ * edits survive only on an outbound request and only for its editable fields;
167
+ * `remember` only when the request allows it and the call was granted.
168
+ */
169
+ export function normalizeHumanResponse(request, approved, extra = {}) {
170
+ const kind = humanRequestKind(request);
171
+ const allowed = RESPONSES_BY_KIND[kind];
172
+ const polarity = approved === true;
173
+ const fallback = polarity ? allowed[0] : allowed[allowed.length - 1];
174
+ const asked = isRecord(extra) && allowed.includes(extra.response) ? extra.response : '';
175
+ let response = asked && GRANTING_RESPONSES.has(asked) === polarity ? asked : fallback;
176
+ const result = { approved: polarity, response };
177
+ if (response === 'edit-and-approve') {
178
+ const edits = editableArgs(extra.editedArgs, request?.outbound?.editable);
179
+ if (Object.keys(edits).length) result.editedArgs = edits;
180
+ else response = result.response = 'approve';
181
+ }
182
+ const scopes = Array.isArray(request?.remember) ? request.remember : ['once'];
183
+ result.remember = polarity && scopes.includes(extra?.remember) ? extra.remember : 'once';
184
+ if (kind === 'permission' && polarity) {
185
+ const ids = (request?.permission?.scopes || []).map((scope) => scope.id);
186
+ if (ids.includes(extra?.scope)) result.scope = extra.scope;
187
+ }
188
+ return result;
189
+ }
190
+
191
+ /** `args` with the card's edited fields in place of the model's. */
192
+ export function applyEditedArgs(args, editedArgs, editable) {
193
+ const edits = editableArgs(editedArgs, editable);
194
+ return Object.keys(edits).length ? { ...(isRecord(args) ? args : {}), ...edits } : args;
195
+ }
@@ -0,0 +1,167 @@
1
+ // session/human-gate/store.mjs
2
+ // Per-agent permissions and the decision audit, both under the plugin data dir.
3
+ // A grant is keyed by agent + action kind (outbound-policy.mjs ACTION_KINDS)
4
+ // and says `allow` (never ask: "다시 묻지 않기") or `block` (deny without a
5
+ // card); no grant means ask. `agent` scope names the agent, `always` stands for
6
+ // every agent ('*'), and an agent's own grant wins over an every-agent one;
7
+ // `once` is never stored. Payments and deletes are never allowed. Grants are
8
+ // listed and revoked by id. The audit is one JSONL line per decision with
9
+ // agent, kind, action, tool, target, decision, deciding device and time —
10
+ // never a value, an argument or a payload — read back newest first.
11
+ import { createHash } from 'node:crypto';
12
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
13
+ import { dirname, join } from 'node:path';
14
+ import { parseJsonText, updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
15
+ import { ACTION_KINDS, isRememberableAction } from './outbound-policy.mjs';
16
+
17
+ const ANY_AGENT = '*';
18
+ const FIELD_LIMIT = 200;
19
+ const AUDIT_KEYS = ['agentId', 'kind', 'action', 'tool', 'target', 'decision', 'device'];
20
+ export const PERMISSION_MODES = Object.freeze(['ask', 'allow', 'block']);
21
+
22
+ const field = (value) => String(value ?? '').slice(0, FIELD_LIMIT);
23
+ const grantId = (agentId, action) =>
24
+ `grant_${createHash('sha256').update(`${agentId}\n${action}`).digest('hex').slice(0, 24)}`;
25
+ const isGrant = (row) => ACTION_KINDS.includes(row?.action) && (row.mode === 'allow' || row.mode === 'block');
26
+
27
+ export function createHumanGateStore(directory) {
28
+ const grantsFile = join(directory, 'grants.json');
29
+ const auditFile = join(directory, 'audit.jsonl');
30
+ const grantsOf = (doc) => (Array.isArray(doc?.grants) ? doc.grants.filter(isGrant) : []);
31
+ async function readGrants() {
32
+ try {
33
+ return grantsOf(parseJsonText(await readFile(grantsFile, 'utf8')));
34
+ } catch (error) {
35
+ if (error?.code === 'ENOENT') return [];
36
+ throw error;
37
+ }
38
+ }
39
+ async function updateGrants(mutate) {
40
+ await mkdir(directory, { recursive: true });
41
+ let result;
42
+ await updateJsonAtomic(grantsFile, (doc) => {
43
+ const out = mutate(grantsOf(doc));
44
+ if (!out) return undefined;
45
+ result = out.result;
46
+ return { grants: out.grants };
47
+ });
48
+ return result;
49
+ }
50
+ /** Set `agentId`'s mode for `action`; `ask` removes its grant. Returns the
51
+ * grant, or null for `ask` and for an `allow` that may not exist. */
52
+ async function setMode({ agentId, action, mode, device = '' }) {
53
+ if (!ACTION_KINDS.includes(action) || !PERMISSION_MODES.includes(mode)) return null;
54
+ if (mode === 'allow' && !isRememberableAction(action)) return null;
55
+ const owner = field(agentId);
56
+ const id = grantId(owner, action);
57
+ const grant = mode === 'ask' ? null : { id, agentId: owner, action, mode, device: field(device), at: Date.now() };
58
+ await updateGrants((grants) => ({
59
+ grants: [...grants.filter((row) => row.id !== id), ...(grant ? [grant] : [])],
60
+ result: grant,
61
+ }));
62
+ return grant;
63
+ }
64
+ /** `allow`, `block` or `ask` for `agentId` + `action`; the agent's own grant wins. */
65
+ async function modeFor({ agentId, action }) {
66
+ const grants = (await readGrants()).filter((grant) => grant.action === action);
67
+ const own =
68
+ grants.find((grant) => grant.agentId === agentId) || grants.find((grant) => grant.agentId === ANY_AGENT);
69
+ if (own?.mode === 'allow' && !isRememberableAction(action)) return 'ask';
70
+ return own?.mode || 'ask';
71
+ }
72
+ return {
73
+ setMode,
74
+ modeFor,
75
+ /** Stores an `allow` for `agent`/`always` scope; `once` stores nothing. Returns the grant or null. */
76
+ async grant({ agentId, action, scope, device = '' }) {
77
+ if (scope !== 'agent' && scope !== 'always') return null;
78
+ return setMode({ agentId: scope === 'always' ? ANY_AGENT : agentId, action, mode: 'allow', device });
79
+ },
80
+ /** Every action kind with `agentId`'s mode, for the profile's permission table. */
81
+ async permissions(agentId) {
82
+ const out = [];
83
+ for (const action of ACTION_KINDS) {
84
+ out.push({ action, mode: await modeFor({ agentId, action }), rememberable: isRememberableAction(action) });
85
+ }
86
+ return out;
87
+ },
88
+ /** Grants that apply to `agentId` (its own and every-agent ones); all when omitted. */
89
+ async listGrants({ agentId } = {}) {
90
+ const grants = await readGrants();
91
+ return agentId ? grants.filter((grant) => grant.agentId === agentId || grant.agentId === ANY_AGENT) : grants;
92
+ },
93
+ async revokeGrant(id) {
94
+ return (
95
+ (await updateGrants((grants) => {
96
+ const rest = grants.filter((grant) => grant.id !== id);
97
+ return rest.length === grants.length ? null : { grants: rest, result: true };
98
+ })) === true
99
+ );
100
+ },
101
+ async recordAudit(entry) {
102
+ const line = { at: Number.isFinite(entry?.at) ? entry.at : Date.now() };
103
+ for (const key of AUDIT_KEYS) line[key] = field(entry?.[key]);
104
+ await mkdir(dirname(auditFile), { recursive: true });
105
+ await appendFile(auditFile, `${JSON.stringify(line)}\n`, 'utf8');
106
+ return line;
107
+ },
108
+ /** Newest first; `agentId` filters to one agent. */
109
+ async readAudit({ agentId = '', limit = 100 } = {}) {
110
+ let raw = '';
111
+ try {
112
+ raw = await readFile(auditFile, 'utf8');
113
+ } catch (error) {
114
+ if (error?.code === 'ENOENT') return [];
115
+ throw error;
116
+ }
117
+ const rows = [];
118
+ for (const line of raw.split('\n').reverse()) {
119
+ if (!line) continue;
120
+ try {
121
+ const row = JSON.parse(line);
122
+ if (!agentId || row.agentId === agentId) rows.push(row);
123
+ } catch {}
124
+ if (rows.length >= limit) break;
125
+ }
126
+ return rows;
127
+ },
128
+ };
129
+ }
130
+
131
+ let processStore = null;
132
+ /** The process store under the plugin data dir; built on first use. */
133
+ export async function humanGateStore() {
134
+ if (!processStore) {
135
+ const { getPluginData } = await import('../../config.mjs');
136
+ processStore = createHumanGateStore(join(getPluginData(), 'human-gate'));
137
+ }
138
+ return processStore;
139
+ }
140
+
141
+ function requiredAgentId(agentId) {
142
+ if (typeof agentId !== 'string' || !agentId.trim() || agentId.length > FIELD_LIMIT || agentId === ANY_AGENT) {
143
+ throw new Error('agent id is invalid');
144
+ }
145
+ return agentId;
146
+ }
147
+
148
+ /** The profile's "권한" table for `agentId` (capability getAgentPermissions). */
149
+ export async function agentPermissions(agentId) {
150
+ return (await humanGateStore()).permissions(requiredAgentId(agentId));
151
+ }
152
+
153
+ /** Set one row of that table (capability setAgentPermission); returns the table. */
154
+ export async function setAgentPermission(agentId, action, mode) {
155
+ const agent = requiredAgentId(agentId);
156
+ if (!ACTION_KINDS.includes(action)) throw new Error(`action must be one of ${ACTION_KINDS.join(', ')}`);
157
+ if (!PERMISSION_MODES.includes(mode)) throw new Error(`mode must be one of ${PERMISSION_MODES.join(', ')}`);
158
+ if (mode === 'allow' && !isRememberableAction(action)) throw new Error(`${action} always asks; it cannot be allowed`);
159
+ const store = await humanGateStore();
160
+ await store.setMode({ agentId: agent, action, mode });
161
+ return store.permissions(agent);
162
+ }
163
+
164
+ /** Replace the process store (tests); null restores the default. */
165
+ export function setHumanGateStore(store) {
166
+ processStore = store || null;
167
+ }
@@ -17,6 +17,7 @@ import { HANDOFF_TIMEOUT_MAX_MS } from '../compact/constants.mjs';
17
17
  import { envFlag, envPositiveInt } from '../../../../shared/env.mjs';
18
18
  import { positiveInt } from '../../../../shared/numbers.mjs';
19
19
  import { isAgentOwner } from '../../agent-owner.mjs';
20
+ import { isAgentModeSession } from '../agent-mode.mjs';
20
21
  import { providerInputExcludesCache, providerNativePdf } from '../../providers/registry.mjs';
21
22
  import { contentMediaBytes, contentMediaCount } from '../../providers/media-normalization.mjs';
22
23
  import { splitProtectedContext } from '../compact/messages.mjs';
@@ -133,7 +134,8 @@ function effectiveContextWindowPercent(sessionRef, cfg) {
133
134
  export function resolveWorkerCompactPolicy(sessionRef, tools) {
134
135
  if (!sessionRef) return null;
135
136
  const cfg = sessionRef.compaction || {};
136
- const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
137
+ // Agent mode always compacts on its own: the user is never asked to.
138
+ const auto = (cfg.auto !== false || isAgentModeSession(sessionRef)) && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
137
139
  if (!auto) return { auto: false };
138
140
  const { contextWindow, boundaryTokens } = compactBoundaryFor(sessionRef, cfg);
139
141
  if (!boundaryTokens) return null;
@@ -8,6 +8,7 @@ import { resolveLiveToolCwd } from './tool-exec.mjs';
8
8
  import { createSteeringDrain } from './steering.mjs';
9
9
  import { createNoToolTurnResolver } from './no-tool-turn.mjs';
10
10
  import { classifyTerminationReason } from './termination.mjs';
11
+ import { routeUsageSegments } from './usage.mjs';
11
12
 
12
13
  // Consecutive identical-AND-failing tool calls (same name+args, error result)
13
14
  // tolerated across iterations before the loop refuses to re-execute and steers
@@ -153,5 +154,13 @@ export function finishLoop(state) {
153
154
  providerStateUpdated: state.providerStateUpdated,
154
155
  terminationReason: classifyTerminationReason(response, { sessionAgent: state.sessionAgent }),
155
156
  providerContinuations: state.noToolTurn.providerContinuations,
157
+ ...(state.routeUsageHops?.length
158
+ ? {
159
+ usageSegments: routeUsageSegments(state.routeUsageHops, state.lastUsage, {
160
+ provider: state.sessionRef?.provider,
161
+ model: state.sessionRef?.model,
162
+ }),
163
+ }
164
+ : {}),
156
165
  };
157
166
  }
@@ -16,6 +16,7 @@ import { isOffReadonlySurface } from './deferred-call-through.mjs';
16
16
  import { BRIDGE_CALL_TOOL_NAME, bridgeErrorText, isToolBridgeEnabled } from '../../runtime-core/tool-bridge.mjs';
17
17
  import { sessionAllowlistExcludes, sessionDisallows } from '../../runtime-core/deferred-tool-availability.mjs';
18
18
  import { providerToolExclusionReason } from './provider-tool-exclusions.mjs';
19
+ import { agentMcpToolPermission, projectMcpToolDisabled } from '../../mcp/agent-connectors.mjs';
19
20
 
20
21
  const WORKER_DENIED_TOOLS = new Set(['agent', 'ask_user', 'session']);
21
22
  const BRIDGE_DISCOVERY_TOOL_NAMES = new Set(['load_tool']);
@@ -122,6 +123,15 @@ export function preDispatchDenyForSession(sessionRef, call) {
122
123
  if (sessionDisallows(sessionRef, schemaName)) {
123
124
  return `Error: tool "${name}" is not available in this session.`;
124
125
  }
126
+ // An agent's connectors: a tool of a server it does not mount, or one set
127
+ // to `disabled`, stays refused though the process-wide registry knows it.
128
+ if (agentMcpToolPermission(sessionRef?.agent, schemaName) === 'disabled') {
129
+ return `Error: tool "${name}" is not available to this agent.`;
130
+ }
131
+ // The project the session runs in may switch a connector off as well.
132
+ if (projectMcpToolDisabled(sessionRef, schemaName)) {
133
+ return `Error: tool "${name}" is switched off in this project.`;
134
+ }
125
135
  if (isOffReadonlySurface(sessionRef, schemaName)) {
126
136
  return `Error: tool "${name}" is not available in this read-only session.`;
127
137
  }
@@ -1,4 +1,7 @@
1
- import { canFallbackRoute } from '../../../../shared/model-route-fallback.mjs';
1
+ import { routeFallbackVerdict } from '../../../../shared/model-route-fallback.mjs';
2
+ import { providerErrorKind } from '../../providers/provider-error-kind.mjs';
3
+ import { displayModelName } from '../../../../shared/model-display.mjs';
4
+ import { presentErrorText } from '../../../../shared/err-text.mjs';
2
5
  import { getProvider, initProviders } from '../../providers/registry.mjs';
3
6
  import { loadConfig } from '../../config.mjs';
4
7
  import { ensureProviderEnabled } from '../../runtime-core/config-helpers.mjs';
@@ -7,19 +10,107 @@ import { pinSessionWireRoute } from '../../providers/wire-route.mjs';
7
10
  import { buildRequestSendOptions } from '../manager/ask-turn-loop-options.mjs';
8
11
  import { prepareTurnEffortConfiguration } from '../../providers/effort-configuration.mjs';
9
12
  import { resolveSessionContextMeta } from '../manager/context-meta.mjs';
13
+ import { stripProviderReplayMessages } from '../manager/provider-replay.mjs';
14
+
15
+ // The short reason a notice gives for leaving a model, by provider kind.
16
+ const HOP_REASONS = {
17
+ quota_exhausted: 'usage limit reached',
18
+ entitlement: 'not included in the plan',
19
+ billing: 'out of credits',
20
+ rate_limited: 'rate-limited',
21
+ overloaded: 'unavailable',
22
+ transient: 'unavailable',
23
+ service_tier_unavailable: 'unavailable',
24
+ auth: 'sign-in failed',
25
+ permission: 'access denied',
26
+ invalid_request: 'model not available',
27
+ context_overflow: 'context too long',
28
+ };
29
+
30
+ const routeLabel = (route) => displayModelName(route.model, route.provider) || `${route.provider}/${route.model}`;
31
+
32
+ /** The muted transcript line for a hop: `<from> <reason> — switched to <to>.` */
33
+ export function routeFallbackNoticeText(from, to, error) {
34
+ const reason = HOP_REASONS[providerErrorKind(error).kind] || 'unavailable';
35
+ return `${routeLabel(from)} ${reason} — switched to ${routeLabel(to)}.`;
36
+ }
37
+
38
+ function routeFailure(route, error) {
39
+ return {
40
+ provider: route.provider,
41
+ model: route.model,
42
+ kind: providerErrorKind(error).kind,
43
+ error: presentErrorText(error),
44
+ };
45
+ }
46
+
47
+ // The chain ended after at least one hop: the final error keeps its own kind
48
+ // and names every model that was tried and why each failed. `skipped` is the
49
+ // next route when the chain stopped before it (a context overflow whose next
50
+ // window is no larger), so the text says why instead of "every model failed".
51
+ function annotateEndedChain(session, error, skipped = null) {
52
+ const tried = session?.routeFallbackFailures;
53
+ if (!tried?.length || !error || typeof error !== 'object' || error.routeFallbackAttempts) return;
54
+ const attempts = [...tried, routeFailure(session, error)];
55
+ try {
56
+ error.routeFallbackAttempts = attempts;
57
+ const lines = attempts.map((attempt) => `${routeLabel(attempt)}: ${attempt.error}`).join('; ');
58
+ const head = skipped
59
+ ? `Fallback chain stopped before ${routeLabel(skipped)} (its context window is not larger) — tried`
60
+ : 'Every model in the fallback chain failed —';
61
+ error.message = `${error.message}\n${head} ${lines}`;
62
+ if (skipped) error.routeFallbackStoppedBefore = { provider: skipped.provider, model: skipped.model };
63
+ } catch {
64
+ /* frozen errors keep their own message */
65
+ }
66
+ }
67
+
68
+ // A context overflow moves only to a model whose window is larger.
69
+ async function nextContextGrows(state, next) {
70
+ const config = ensureProviderEnabled(loadConfig(), next.provider);
71
+ await initProviders(config.providers, { signal: state.signal });
72
+ state.throwIfAborted();
73
+ const provider = getProvider(next.provider);
74
+ if (!provider) return false;
75
+ const current = resolveSessionContextMeta(state.provider, state.sessionRef.model, state.sessionRef).rawContextWindow;
76
+ const window = resolveSessionContextMeta(provider, next.model, next).rawContextWindow;
77
+ return Number(window) > Number(current || state.sessionRef.selectedContextWindow || 0);
78
+ }
10
79
 
11
80
  export async function advanceAgentRoute(state, error) {
12
81
  const session = state.sessionRef;
13
- const index = session?.routeFallbackIndex || 0;
14
- const next = session?.routeFallbacks?.[index];
15
- const committed = state.toolCallsTotal > 0 || state.response != null;
16
- if (!session?.agent || !next || !canFallbackRoute(error, { signal: state.signal, committed })) return false;
82
+ if (!session?.agent || !session.routeFallbacks?.length) return false;
83
+ const index = session.routeFallbackIndex || 0;
84
+ const next = session.routeFallbacks[index];
85
+ // Only THIS request must have produced nothing (its stream outcome says so):
86
+ // earlier requests of the turn may have run tools. Their calls and results
87
+ // are already in the live turn, which the next model continues from, so no
88
+ // completed tool runs again.
89
+ const verdict = routeFallbackVerdict(error, { signal: state.signal });
90
+ const hops = verdict === 'hop' || (verdict === 'context' && next && (await nextContextGrows(state, next)));
91
+ if (!next || !hops) {
92
+ if (verdict !== 'stay') annotateEndedChain(session, error, next || null);
93
+ return false;
94
+ }
17
95
  state.throwIfAborted();
18
96
  const config = ensureProviderEnabled(loadConfig(), next.provider);
19
97
  await initProviders(config.providers, { signal: state.signal });
20
98
  state.throwIfAborted();
21
99
  const provider = getProvider(next.provider);
22
100
  if (!provider) throw new Error(`Provider "${next.provider}" not available`);
101
+ const failed = routeFailure(session, error);
102
+ // The left model's share of the turn ends here: the running total at the
103
+ // switch bounds it, so each model's tokens are booked at its own rates.
104
+ state.routeUsageHops = [
105
+ ...(state.routeUsageHops || []),
106
+ { provider: session.provider, model: session.model, usage: state.lastUsage ? { ...state.lastUsage } : null },
107
+ ];
108
+ const notice = routeFallbackNoticeText(session, next, error);
109
+ // Saved with the new route: a crash before the live turn is checkpointed
110
+ // again recovers that turn without the previous provider's native state.
111
+ if (session.activeTurnCheckpoint && typeof session.activeTurnCheckpoint === 'object') {
112
+ session.activeTurnCheckpoint = { ...session.activeTurnCheckpoint, routeChanged: true };
113
+ }
23
114
  const oldOptions = buildRequestSendOptions({
24
115
  sessionId: state.sessionId,
25
116
  session,
@@ -41,9 +132,12 @@ export async function advanceAgentRoute(state, error) {
41
132
  );
42
133
  if (!updated) throw new Error('agent session closed during model fallback');
43
134
  updated.routeFallbackIndex = index + 1;
135
+ updated.routeFallbackFailures = [...(session.routeFallbackFailures || []), failed];
44
136
  await pinSessionWireRoute(updated, provider);
45
137
  state.throwIfAborted();
46
- state.messages.splice(0, state.messages.length, ...updated.messages);
138
+ // The live turn (prompt, assistant tool calls, tool results) carries on;
139
+ // only the previous provider's native replay state is dropped from it.
140
+ state.messages.splice(0, state.messages.length, ...stripProviderReplayMessages(state.messages));
47
141
  updated.liveTurnMessages = state.messages;
48
142
  state.sessionRef = updated;
49
143
  state.provider = provider;
@@ -67,6 +161,14 @@ export async function advanceAgentRoute(state, error) {
67
161
  signal: state.signal,
68
162
  })
69
163
  );
164
+ try {
165
+ process.stderr.write(
166
+ `[route-fallback] ${failed.provider}/${failed.model} (${failed.kind}) -> ${updated.provider}/${updated.model}\n`
167
+ );
168
+ state.opts.onStageChange?.('notice', { tone: 'info', message: notice });
169
+ } catch {
170
+ /* display-only */
171
+ }
70
172
  state.opts.onStageChange?.('connecting', {
71
173
  provider: updated.provider,
72
174
  model: updated.model,
@@ -181,6 +181,10 @@ export async function sendProviderRequest(state, round) {
181
181
  tools: round.sendTools,
182
182
  opts,
183
183
  });
184
+ // The request's time for the speed figures, the wait for the first token
185
+ // included: the span the usage ledger recorded as `duration_ms` (stamped on
186
+ // the response); wall time only for a response no ledger row timed.
187
+ const requestStartedAt = Date.now();
184
188
  let result;
185
189
  try {
186
190
  result = await runWithProviderRequestToolsScope(round.requestToolScope, () =>
@@ -211,12 +215,16 @@ export async function sendProviderRequest(state, round) {
211
215
  }
212
216
  if (result.action === 'proceed') sideQuestion?.settle(result.response);
213
217
  else sideQuestion?.discard();
218
+ const sendEndedAt = Date.now();
214
219
  return {
215
220
  result,
216
221
  providerMessages,
217
222
  prefixGuardCandidate,
218
223
  sendStartedAt,
219
- sendEndedAt: Date.now(),
224
+ sendEndedAt,
225
+ requestMs: Number.isFinite(result.response?.usageDurationMs)
226
+ ? Math.max(0, result.response.usageDurationMs)
227
+ : Math.max(0, sendEndedAt - requestStartedAt),
220
228
  preSendMs,
221
229
  toolResumeMs,
222
230
  };
@@ -429,6 +437,7 @@ export function settleSendResult(state, round, sent) {
429
437
  usage: reportedUsage,
430
438
  sendTools: round.sendTools,
431
439
  toolCalls: response.toolCalls?.length || 0,
440
+ requestMs: sent.requestMs,
432
441
  })
433
442
  )
434
443
  );