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
@@ -8,6 +8,15 @@ import { openInBrowser } from '../runtime/shared/open-url.mjs';
8
8
  // Settled flows stay readable long enough for a poller to observe the result.
9
9
  const SETTLED_FLOW_TTL_MS = 10 * 60_000;
10
10
  const NOT_COMPLETED = 'MCP OAuth sign-in was not completed';
11
+ export const MCP_OAUTH_LOGIN_TIMED_OUT = 'MCP OAuth sign-in timed out';
12
+ // A sign-in nobody finishes ends here, ahead of the OAuth layer's own 5-minute
13
+ // abort, so its failure is reported as the timeout it is.
14
+ const LOGIN_DEADLINE_MS = 5 * 60_000 - 10_000;
15
+ // The longest a status read waits for a change (local callers only).
16
+ const MAX_STATUS_WAIT_MS = 15_000;
17
+ // How a sign-in treats the account (mcp/oauth-account.mjs): a normal
18
+ // authorization, a repair of the same connection, or a transactional switch.
19
+ const MODES = new Set(['connect', 'reauth', 'switch']);
11
20
 
12
21
  const NOT_PENDING = 'MCP OAuth sign-in is no longer pending';
13
22
 
@@ -19,17 +28,36 @@ const NOT_PENDING = 'MCP OAuth sign-in is no longer pending';
19
28
  const flows = new Map();
20
29
 
21
30
  // A pending flow keeps its authorization URL readable: a client polling the
22
- // status (remote sign-in) still shows the page to open.
31
+ // status (remote sign-in) still shows the page to open. `step` names what a
32
+ // pending flow waits for: `browser` (the sign-in page), `connecting` (the
33
+ // callback arrived; tokens are exchanged), `tools` (signed in; the server
34
+ // connects and lists its tools).
23
35
  const view = (flow) => ({
24
36
  flowId: flow.flowId,
25
37
  state: flow.state,
38
+ ...(flow.state === 'pending' ? { step: flow.step } : {}),
26
39
  ...(flow.state === 'pending' && flow.url ? { url: flow.url } : {}),
27
40
  ...(flow.error ? { error: flow.error } : {}),
41
+ ...(flow.reason ? { reason: flow.reason } : {}),
28
42
  });
43
+ const versionOf = (flow) => `${flow.state}:${flow.step}`;
44
+ // Wake every status read waiting on this flow.
45
+ function changed(flow) {
46
+ const waiters = flow.waiters;
47
+ flow.waiters = [];
48
+ for (const wake of waiters) wake();
49
+ }
50
+ function setStep(flow, step) {
51
+ if (flow.state !== 'pending' || flow.step === step) return;
52
+ flow.step = step;
53
+ changed(flow);
54
+ }
29
55
  function settle(flow, state, error = null) {
30
56
  if (flow.state !== 'pending') return;
31
57
  flow.state = state;
32
58
  if (state === 'failed') flow.error = error || NOT_COMPLETED;
59
+ clearTimeout(flow.deadline);
60
+ changed(flow);
33
61
  setTimeout(() => flows.delete(flow.flowId), SETTLED_FLOW_TTL_MS).unref?.();
34
62
  }
35
63
  function flowOf(flowId) {
@@ -40,19 +68,23 @@ function flowOf(flowId) {
40
68
  async function cancelFlow(flow) {
41
69
  if (flow.state === 'pending') {
42
70
  flow.cancelled = true;
43
- flow.owner.cancel(flow.name);
71
+ flow.abort();
44
72
  await flow.done;
45
73
  }
46
74
  return view(flow);
47
75
  }
48
76
 
49
77
  /**
50
- * `connect(name, openBrowser)` reconnects the server interactively, calling
51
- * `openBrowser(url)` when a login starts, and resolves `{ connected, error }`.
78
+ * `connect(name, launch)` reconnects the server interactively, calling
79
+ * `launch(url)` when a login starts, and resolves `{ connected, error }`.
52
80
  * `cancel(name)` aborts that server's pending login. `completeCallback(name,
53
81
  * callbackUrl)` answers it with a pasted redirect URL (throws a fixed message
54
- * for an invalid one). `onComplete(name)` runs after a successful sign-in,
55
- * before the flow reports complete.
82
+ * for an invalid one). `switchAccount(name, launch, attempt)` runs an account
83
+ * switch instead (mode `switch`): it registers `attempt.abort` / `attempt.answer`
84
+ * and resolves `{ connected, error }` only after committing (or rolling back)
85
+ * the new sign-in. `launch` carries `accountMode` and the progress hooks
86
+ * `onCallback` / `onAuthorized` the OAuth layer calls. `onComplete(name)` runs
87
+ * after a successful sign-in, once the flow already reports complete.
56
88
  */
57
89
  export function createMcpOAuthLogins({
58
90
  connect,
@@ -60,8 +92,12 @@ export function createMcpOAuthLogins({
60
92
  completeCallback = () => {
61
93
  throw new Error(NOT_PENDING);
62
94
  },
95
+ switchAccount = async () => {
96
+ throw new Error('Switching the account is not supported here');
97
+ },
63
98
  onComplete = async () => {},
64
99
  openBrowser = openInBrowser,
100
+ loginTimeoutMs = LOGIN_DEADLINE_MS,
65
101
  }) {
66
102
  // name -> tail of that server's serialized begin/cancel/sign-out steps, so
67
103
  // replacing a pending flow is atomic and exactly one can be pending.
@@ -89,10 +125,11 @@ export function createMcpOAuthLogins({
89
125
  Promise.all([...flows.values()].filter((flow) => flow.owner === owner && flow.name === name).map(cancelFlow));
90
126
 
91
127
  async function begin(name, options = {}) {
92
- const { openBrowser: hostBrowser } = options || {};
128
+ const { openBrowser: hostBrowser, mode = 'connect' } = options || {};
93
129
  if (hostBrowser !== undefined && typeof hostBrowser !== 'boolean') {
94
130
  throw new TypeError('openBrowser must be a boolean');
95
131
  }
132
+ if (!MODES.has(mode)) throw new TypeError('mode must be connect, reauth or switch');
96
133
  let announceUrl;
97
134
  const urlReady = new Promise((resolve) => {
98
135
  announceUrl = resolve;
@@ -105,10 +142,17 @@ export function createMcpOAuthLogins({
105
142
  flowId: `mcp_oauth_${randomUUID()}`,
106
143
  name,
107
144
  owner,
145
+ mode,
108
146
  state: 'pending',
147
+ step: 'starting',
109
148
  error: null,
149
+ reason: null,
110
150
  cancelled: false,
151
+ timedOut: false,
111
152
  url: null,
153
+ waiters: [],
154
+ abort: () => owner.cancel(name),
155
+ answer: (callbackUrl) => owner.completeCallback(name, callbackUrl),
112
156
  };
113
157
  flows.set(flow.flowId, flow);
114
158
  const launch = async (url) => {
@@ -117,22 +161,51 @@ export function createMcpOAuthLogins({
117
161
  if (flow.cancelled) throw new Error('MCP OAuth login cancelled');
118
162
  flow.awaitingCallback = true;
119
163
  flow.url = url;
164
+ setStep(flow, 'browser');
120
165
  announceUrl(url);
121
166
  if (hostBrowser !== false) openBrowser(url);
167
+ // Nobody finished the sign-in page in time.
168
+ flow.deadline = setTimeout(() => {
169
+ if (flow.state !== 'pending' || flow.step !== 'browser') return;
170
+ flow.timedOut = flow.cancelled = true;
171
+ flow.abort();
172
+ }, loginTimeoutMs);
173
+ flow.deadline.unref?.();
122
174
  };
175
+ launch.accountMode = mode;
176
+ launch.onCallback = () => setStep(flow, 'connecting');
177
+ launch.onAuthorized = () => setStep(flow, 'tools');
123
178
  flow.done = (async () => {
124
179
  try {
125
- const outcome = await connect(name, launch);
126
- if (flow.cancelled) settle(flow, 'cancelled');
180
+ const outcome =
181
+ mode === 'switch'
182
+ ? await switchAccount(name, launch, {
183
+ set abort(fn) {
184
+ flow.abort = fn;
185
+ },
186
+ set answer(fn) {
187
+ flow.answer = fn;
188
+ },
189
+ })
190
+ : await connect(name, launch);
191
+ if (flow.timedOut) {
192
+ flow.reason = 'timeout';
193
+ settle(flow, 'failed', MCP_OAUTH_LOGIN_TIMED_OUT);
194
+ } else if (flow.cancelled) settle(flow, 'cancelled');
127
195
  else if (outcome?.connected) {
128
- // The sign-in itself succeeded; a failed follow-up never undoes it.
129
- await Promise.resolve()
196
+ settle(flow, 'complete');
197
+ // The other runtimes reconnect after the flow already reports
198
+ // complete: the caller never waits for every peer's reconnect, and
199
+ // a failed follow-up never undoes the sign-in.
200
+ void Promise.resolve()
130
201
  .then(() => onComplete(name))
131
202
  .catch(() => {});
132
- settle(flow, 'complete');
133
203
  } else settle(flow, 'failed', outcome?.error);
134
204
  } catch (error) {
135
- settle(flow, flow.cancelled ? 'cancelled' : 'failed', error?.message || String(error));
205
+ if (flow.timedOut) {
206
+ flow.reason = 'timeout';
207
+ settle(flow, 'failed', MCP_OAUTH_LOGIN_TIMED_OUT);
208
+ } else settle(flow, flow.cancelled ? 'cancelled' : 'failed', error?.message || String(error));
136
209
  }
137
210
  })();
138
211
  return flow;
@@ -147,7 +220,31 @@ export function createMcpOAuthLogins({
147
220
 
148
221
  return {
149
222
  begin,
150
- status: (flowId) => view(flowOf(flowId)),
223
+ /** The flow's view. With `{ since, waitMs }` (the `state:step` a caller
224
+ * last saw) the read resolves on the flow's next change, or after at most
225
+ * `waitMs` unchanged: the caller follows each step as it happens. */
226
+ status(flowId, options = {}) {
227
+ const flow = flowOf(flowId);
228
+ const { since, waitMs } = options || {};
229
+ if (since === undefined) return view(flow);
230
+ if (typeof since !== 'string') throw new TypeError('since must be a string');
231
+ if (!Number.isFinite(waitMs) || waitMs < 0) throw new TypeError('waitMs must be a non-negative number');
232
+ if (since !== versionOf(flow) || flow.state !== 'pending' || waitMs === 0) return view(flow);
233
+ return new Promise((resolve) => {
234
+ const wake = () => {
235
+ clearTimeout(timer);
236
+ resolve(view(flow));
237
+ };
238
+ const timer = setTimeout(
239
+ () => {
240
+ flow.waiters = flow.waiters.filter((waiter) => waiter !== wake);
241
+ resolve(view(flow));
242
+ },
243
+ Math.min(waitMs, MAX_STATUS_WAIT_MS)
244
+ );
245
+ flow.waiters.push(wake);
246
+ });
247
+ },
151
248
  cancel(flowId) {
152
249
  const flow = flowOf(flowId);
153
250
  return flow.owner.exclusive(flow.name, () => cancelFlow(flow));
@@ -158,7 +255,7 @@ export function createMcpOAuthLogins({
158
255
  async complete(flowId, callbackUrl) {
159
256
  const flow = flowOf(flowId);
160
257
  if (flow.state !== 'pending' || flow.cancelled || !flow.awaitingCallback) throw new Error(NOT_PENDING);
161
- flow.owner.completeCallback(flow.name, callbackUrl);
258
+ flow.answer(callbackUrl);
162
259
  await flow.done;
163
260
  return view(flow);
164
261
  },
@@ -35,7 +35,7 @@ export function createRoutePersistence(deps) {
35
35
  hasSecrets: getConfigHasSecrets(),
36
36
  });
37
37
  },
38
- persistAdoptedModelSettings(route, { keepMainModel } = {}) {
38
+ persistAdoptedModelSettings(route, { keepMainModel, onlyIfAbsent = false } = {}) {
39
39
  // saveModelSettings is in-memory only. persistLeadRoute debounce-writes
40
40
  // the adopted config (including modelSettings). If the lead preset cannot
41
41
  // be normalized, still debounce-persist so effort/fast are not memory-only.
@@ -43,7 +43,7 @@ export function createRoutePersistence(deps) {
43
43
  keepMainModel === true ||
44
44
  (keepMainModel !== false && tunesNonMainModel(getRoute(), route, resolveRoute(getConfig(), {})))
45
45
  ? null
46
- : persistLeadRoute(route);
46
+ : persistLeadRoute(route, { onlyIfAbsent });
47
47
  if (!leadRoute) saveConfigAndAdopt(getConfig());
48
48
  return leadRoute;
49
49
  },
@@ -10,7 +10,13 @@ export function createRouteTuning(deps, persist) {
10
10
  const { getConfig, getRoute, setRouteState, resolveRoute, lookupModelMeta, refreshRouteEffort } = deps;
11
11
 
12
12
  /** Save the live route's modelSettings, re-resolve through the lead preset
13
- * when one exists, refresh effort and tune the current session. */
13
+ * when one exists, refresh effort and tune the current session.
14
+ *
15
+ * This is not a Main-model write and needs no compare-and-set: it persists
16
+ * the LIVE route (read after every await), and only when that route's model
17
+ * is already the Main model (persistAdoptedModelSettings: tunesNonMainModel),
18
+ * so the lead preset's model is rewritten with itself and never changes. A
19
+ * live route without a model normalizes to nothing and writes no preset. */
14
20
  async function commitRouteTuning(fastCapable, modelMeta) {
15
21
  persist.saveRouteModelSettings(getRoute(), fastCapable);
16
22
  const leadRoute = persist.persistAdoptedModelSettings(getRoute());
@@ -140,15 +140,26 @@ function modelSwitchAnchorIndex(messages) {
140
140
  * (like an inheritance boundary): the transcript shows it live and after a
141
141
  * restore, and the model conversation gains no synthetic message. */
142
142
  export function recordModelSwitchBoundary(session, boundary) {
143
+ return recordTranscriptBoundary(
144
+ session,
145
+ 'modelSwitches',
146
+ boundary,
147
+ (entry) => entry?.at === boundary.at && entry?.modelId === boundary.modelId
148
+ );
149
+ }
150
+
151
+ /** A display-only boundary appended to `meta[field]` of the switch anchor
152
+ * message (model and workflow switches); `same` finds an already recorded one. */
153
+ export function recordTranscriptBoundary(session, field, boundary, same) {
143
154
  const messages = Array.isArray(session?.messages) ? session.messages : [];
144
155
  const index = modelSwitchAnchorIndex(messages);
145
156
  if (index < 0) return false;
146
157
  const anchor = messages[index];
147
- const switches = Array.isArray(anchor.meta?.modelSwitches) ? anchor.meta.modelSwitches : [];
148
- if (switches.some((entry) => entry?.at === boundary.at && entry?.modelId === boundary.modelId)) return true;
158
+ const switches = Array.isArray(anchor.meta?.[field]) ? anchor.meta[field] : [];
159
+ if (switches.some(same)) return true;
149
160
  // Replace the message object: the session save worker detects changed
150
161
  // messages by reference identity, so an in-place meta edit is never saved.
151
- messages[index] = { ...anchor, meta: { ...anchor.meta, modelSwitches: [...switches, boundary] } };
162
+ messages[index] = { ...anchor, meta: { ...anchor.meta, [field]: [...switches, boundary] } };
152
163
  return true;
153
164
  }
154
165
 
@@ -184,9 +195,23 @@ async function adoptRoute(deps, persist, next, options, lease) {
184
195
  scheduleStatuslineUsageRefresh,
185
196
  } = deps;
186
197
  const { selectedRoute, fastCapable, modelMeta } = await resolveSelectedRoute(deps, next);
198
+ // The Lead route a conditional fill wrote, until the file confirms it.
199
+ let filled = null;
187
200
  // Provider readiness may outlast the session the selection was made for.
188
201
  const adopted = lease.run(
189
202
  () => {
203
+ // Compare-and-set: a route that already has a model (picked while this
204
+ // selection was resolving) is never replaced. Checked synchronously with
205
+ // the adopt below, so nothing can set a route in between. A Lead route
206
+ // another process persisted may not have reached this runtime yet (the
207
+ // change notification is debounced): the config is re-read from disk
208
+ // right here, which adopts it and fills the empty route, and that route
209
+ // is the winner this selection answers with.
210
+ if (options?.onlyIfEmpty === true) {
211
+ if (clean(getRoute()?.model)) return false;
212
+ deps.reloadFullConfig?.();
213
+ if (clean(getRoute()?.model)) return false;
214
+ }
190
215
  if (options?.sessionOnly === true) {
191
216
  // An agent child's route belongs to that session alone: the shared
192
217
  // modelSettings bucket (saved effort/Fast per model) and the main
@@ -199,7 +224,9 @@ async function adoptRoute(deps, persist, next, options, lease) {
199
224
  // — the session surface's model pick, an heir — keeps it.
200
225
  const leadRoute = persist.persistAdoptedModelSettings(selectedRoute, {
201
226
  keepMainModel: options?.keepMainModel !== false,
227
+ onlyIfAbsent: options?.onlyIfEmpty === true,
202
228
  });
229
+ filled = leadRoute;
203
230
  setRouteState(resolveRoute(getConfig(), leadRoute ? { model: workflowPresetId('lead') } : selectedRoute));
204
231
  }
205
232
  return true;
@@ -207,9 +234,13 @@ async function adoptRoute(deps, persist, next, options, lease) {
207
234
  () => false
208
235
  );
209
236
  if (!adopted) return false;
237
+ // A conditional fill reaches the file only while no Lead route is stored
238
+ // there (judged under the config file lock). When another process wrote one
239
+ // after the re-read above, the file's route replaces the fill in the runtime.
240
+ const replaced = filled && (await deps.confirmLeadFill?.(filled)) === true;
210
241
  // The refresh completes against the route just adopted and only while the
211
242
  // selection still owns it.
212
- await refreshRouteEffort(modelMeta, getRoute(), null, lease);
243
+ await refreshRouteEffort(replaced ? null : modelMeta, getRoute(), null, lease);
213
244
  return lease.run(
214
245
  () => {
215
246
  refreshStatuslineUsageSnapshot(getRoute());
@@ -14,7 +14,7 @@ import {
14
14
  } from './native-web-search/tool-shape.mjs';
15
15
  import { createWebSearchRouteCandidates } from './native-web-search/routes.mjs';
16
16
  import { reportNativeWebSearchUsage } from './native-web-search-usage.mjs';
17
- import { canFallbackRoute } from '../runtime/shared/model-route-fallback.mjs';
17
+ import { canFallbackCapacityRoute } from '../runtime/shared/model-route-fallback.mjs';
18
18
 
19
19
  export function createNativeWebSearch(deps) {
20
20
  const {
@@ -103,7 +103,7 @@ export function createNativeWebSearch(deps) {
103
103
  try {
104
104
  return await sendCandidate(candidate, toolType, webSearchArgs, signal);
105
105
  } catch (err) {
106
- if (!canFallbackRoute(err, { signal })) throw err;
106
+ if (!canFallbackCapacityRoute(err, { signal })) throw err;
107
107
  errors.push(
108
108
  `${candidate.provider}${candidate.model ? `/${candidate.model}` : ''}/${toolType}: ${err?.message || String(err)}`
109
109
  );
@@ -1,4 +1,5 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
+ import { clean } from '../runtime/agent/orchestrator/runtime-core/session-text.mjs';
2
3
  import { flushPendingSessionConfigWrites } from './config-lifecycle.mjs';
3
4
 
4
5
  // Run only after the lifecycle has established that it needs a NEW session.
@@ -27,7 +28,11 @@ export function createNewSessionConfig({
27
28
  if (!initialConfig || typeof initialConfig !== 'object') {
28
29
  reloadFullConfig();
29
30
  if (!firstSession || !initialRouteExplicit) {
30
- rt.route = resolveRoute(rt.config, {});
31
+ const configured = resolveRoute(rt.config, {});
32
+ // With no Lead model configured the model picked for this runtime's
33
+ // still-empty conversation (a session-only choice never writes the
34
+ // Lead route) stands; the reload must not drop it before the first turn.
35
+ if (clean(configured?.model) || !clean(rt.route?.model)) rt.route = configured;
31
36
  }
32
37
  }
33
38
  invalidateOutputStyleStatusCache();
@@ -0,0 +1,229 @@
1
+ /**
2
+ * One run of an owned automation — a schedule or a webhook endpoint. The run
3
+ * itself is a turn in the owner bot's home thread (routine-home-turn.mjs);
4
+ * the bot's reply there is its result, so nothing is posted besides it. A run
5
+ * whose owner bot does not exist (routine-owner.mjs) never fires: it is
6
+ * skipped as paused, never run elsewhere. A second run of the same automation
7
+ * while one is in flight is skipped (and recorded).
8
+ *
9
+ * Unattended runs (timed fires, received webhooks; not the app's or a chat's
10
+ * "Run now") also back off after consecutive failures (ROUTINE_LIMITS).
11
+ * Runs have no concurrency cap or deadline (user decision).
12
+ *
13
+ * Routine results from before routines ran in the bot's thread may still sit
14
+ * on a routine's record (`result_delivery`): redeliverRoutine and the daemon's
15
+ * recovery queue them into the owner's thread (agent-home-delivery.mjs),
16
+ * never running the routine again.
17
+ */
18
+ import { announceRoutineRun, deliverScheduleResult } from './agent-home-delivery.mjs';
19
+ import { ROUTINE_LIMITS, routineBackoffRemaining } from '../runtime/shared/routine-limits.mjs';
20
+ import { ROUTINE_OWNER_PROBLEMS, routineOwnerProblem } from './routine-owner.mjs';
21
+
22
+ const DELIVERY_RETRY_MS = 60_000;
23
+ const errorText = (error) => error?.message || String(error);
24
+ const defaultLog = (line) => process.stderr.write(`mixdog routines: ${line}\n`);
25
+
26
+ // `${kind}:${name}` of every run in flight in this process: timed fires,
27
+ // received webhooks and the app's "Run now" all come through here.
28
+ const activeRuns = new Set();
29
+
30
+ // The PG record of each kind, imported on first use.
31
+ const RECORDS = {
32
+ schedule: () =>
33
+ import('../runtime/channels/lib/schedules-db.mjs').then((db) => ({
34
+ state: (name) => db.getSchedule(name),
35
+ list: () => db.listSchedules(),
36
+ recordDelivery: db.recordScheduleDelivery,
37
+ })),
38
+ webhook: () =>
39
+ import('../runtime/channels/lib/webhooks-db.mjs').then((db) => ({
40
+ state: (name) => db.loadEndpointConfig(name),
41
+ list: () => db.listEndpoints(),
42
+ recordDelivery: db.recordEndpointDelivery,
43
+ })),
44
+ };
45
+ /** `deps` over the kind's PG record for whatever `deps` leaves out. */
46
+ async function withRecord(kind, deps) {
47
+ if (deps.state && deps.list && deps.recordDelivery) return deps;
48
+ const record = await RECORDS[kind]();
49
+ return {
50
+ ...deps,
51
+ state: deps.state || record.state,
52
+ list: deps.list || record.list,
53
+ recordDelivery: deps.recordDelivery || record.recordDelivery,
54
+ };
55
+ }
56
+
57
+ let retryArmed = false;
58
+ /** One timer for the whole host: re-queue every record's undelivered result. */
59
+ function armDeliveryRetry() {
60
+ if (retryArmed) return;
61
+ retryArmed = true;
62
+ setTimeout(() => {
63
+ retryArmed = false;
64
+ recoverRoutineDeliveries().catch((error) => defaultLog(`delivery retry failed: ${errorText(error)}`));
65
+ }, DELIVERY_RETRY_MS).unref?.();
66
+ }
67
+
68
+ /** Show `run`'s live-only row in the owner's home; a failure only logs. */
69
+ async function announceRun(announce, row, run, log) {
70
+ try {
71
+ await announce(row, run);
72
+ } catch (error) {
73
+ log(`${run.kind} "${row.name}": run row not shown: ${errorText(error)}`);
74
+ }
75
+ }
76
+
77
+ /** Queue `item` for `row`'s owner; clear the record on success, mark it
78
+ * 'failed' (and arm the retry) when the queue refuses it. */
79
+ async function queueResult(row, item, entry, { deliver, recordDelivery, onDeliveryFailed, announce, log }) {
80
+ try {
81
+ await deliver(row, item);
82
+ } catch (error) {
83
+ const message = errorText(error);
84
+ log(`${item.kind} "${row.name}": result could not be queued (${message}); retrying later`);
85
+ await recordDelivery(row.name, item.runId, { ...entry, state: 'failed', error: message }).catch((recordError) =>
86
+ log(`${item.kind} "${row.name}": delivery failure not recorded: ${errorText(recordError)}`)
87
+ );
88
+ // The home's run row turns into "result not delivered" (retry: retryRoutineDelivery).
89
+ await announceRun(announce, row, { ...item, state: 'undelivered' }, log);
90
+ onDeliveryFailed();
91
+ return false;
92
+ }
93
+ // A clear that fails leaves the record 'pending': the next recovery re-queues
94
+ // it, which the queue's run-id key turns into a no-op.
95
+ await recordDelivery(row.name, item.runId, null).catch((error) =>
96
+ log(`${item.kind} "${row.name}": delivered result not cleared: ${errorText(error)}`)
97
+ );
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * `row` is `{ name, ownerAgentId }`; `run({ signal })` resolves `{ sessionId,
103
+ * result }` (`signal` is the caller's cancellation, if any). Resolves the
104
+ * outcome, or `{ skipped: true, reason: 'owner' | 'overlap' | 'backoff' }`
105
+ * (an owner skip also names `ownerProblem` and its `message`); a failed run
106
+ * rejects.
107
+ */
108
+ export async function runOwnedAutomation(kind, row, run, deps = {}) {
109
+ if (!row?.name) throw new Error(`${kind} run: a ${kind} row is required`);
110
+ const {
111
+ recordSkip = async () => {},
112
+ active = activeRuns,
113
+ limits = ROUTINE_LIMITS,
114
+ attended = false,
115
+ signal = null,
116
+ now = Date.now,
117
+ ownerProblem = routineOwnerProblem,
118
+ log = defaultLog,
119
+ } = deps;
120
+ const name = row.name;
121
+ const key = `${kind}:${name}`;
122
+ const skip = async (reason, extra = {}) => {
123
+ await Promise.resolve()
124
+ .then(() => recordSkip(name, new Date(now())))
125
+ .catch((error) => log(`${kind} "${name}": skip record failed: ${errorText(error)}`));
126
+ return { name, skipped: true, reason, sessionId: null, result: '', ...extra };
127
+ };
128
+ // A routine runs only in its own bot's thread: without that bot it waits.
129
+ const problem = await Promise.resolve()
130
+ .then(() => ownerProblem(row.ownerAgentId))
131
+ .catch(() => '');
132
+ if (problem) {
133
+ log(`${kind} "${name}": paused, ${ROUTINE_OWNER_PROBLEMS[problem]}`);
134
+ return skip('owner', { ownerProblem: problem, message: ROUTINE_OWNER_PROBLEMS[problem] });
135
+ }
136
+ if (active.has(key)) return skip('overlap');
137
+ active.add(key);
138
+ try {
139
+ if (!attended) {
140
+ const record = await withRecord(kind, deps);
141
+ const state = await Promise.resolve()
142
+ .then(() => record.state(name))
143
+ .catch(() => null);
144
+ const wait = routineBackoffRemaining(state, now(), limits);
145
+ if (wait > 0) {
146
+ log(`${kind} "${name}": skipped, backing off ${Math.ceil(wait / 1000)}s after failures in a row`);
147
+ return await skip('backoff');
148
+ }
149
+ }
150
+ return await run({ signal });
151
+ } finally {
152
+ active.delete(key);
153
+ }
154
+ }
155
+
156
+ /** Re-queue every result `row`'s record still holds, oldest first; resolves
157
+ * the errors of the ones the queue still refuses. Never reruns the routine. */
158
+ export async function redeliverRoutine(kind, row, deps = {}) {
159
+ const {
160
+ deliver = deliverScheduleResult,
161
+ onDeliveryFailed = armDeliveryRetry,
162
+ announce = announceRoutineRun,
163
+ log = defaultLog,
164
+ } = deps;
165
+ const record = await withRecord(kind, deps);
166
+ const entries = Object.entries(row?.resultDelivery || {}).sort(([, a], [, b]) =>
167
+ String(a?.at).localeCompare(String(b?.at))
168
+ );
169
+ const errors = [];
170
+ for (const [runId, entry] of entries) {
171
+ const result = entry?.result || {};
172
+ const item = {
173
+ runId,
174
+ kind,
175
+ ok: result.ok === true,
176
+ text: result.text || '',
177
+ runSessionId: result.runSessionId,
178
+ reason: result.reason,
179
+ paused: result.paused,
180
+ };
181
+ await queueResult(row, item, entry, {
182
+ ...record,
183
+ deliver,
184
+ announce,
185
+ log,
186
+ onDeliveryFailed: () => errors.push(runId),
187
+ });
188
+ }
189
+ if (errors.length) onDeliveryFailed();
190
+ return errors;
191
+ }
192
+
193
+ /** Daemon start (and the retry timer): re-queue every undelivered result of
194
+ * every schedule and webhook. Resolves how many runs still wait. Without a
195
+ * PostgreSQL cluster no routine exists, and none is started for this. */
196
+ export async function recoverRoutineDeliveries(depsByKind = null) {
197
+ if (!depsByKind) {
198
+ const [{ pgClusterExists }, { resolvePluginData }] = await Promise.all([
199
+ import('../runtime/memory/lib/pg/adapter.mjs'),
200
+ import('../runtime/shared/plugin-paths.mjs'),
201
+ ]);
202
+ if (!pgClusterExists(resolvePluginData())) return 0;
203
+ }
204
+ let waiting = 0;
205
+ for (const kind of ['schedule', 'webhook']) {
206
+ const deps = await withRecord(kind, depsByKind?.[kind] || {});
207
+ for (const row of await deps.list()) {
208
+ if (row?.resultDelivery && Object.keys(row.resultDelivery).length) {
209
+ waiting += (await redeliverRoutine(kind, row, deps)).length;
210
+ }
211
+ }
212
+ }
213
+ return waiting;
214
+ }
215
+
216
+ /** The app's "retry" on one routine: rejects with the error when still refused. */
217
+ export async function retryRoutineDelivery(kind, name, deps = {}) {
218
+ if (kind !== 'schedule' && kind !== 'webhook') throw new Error(`unknown routine kind "${kind}"`);
219
+ const record = await withRecord(kind, deps);
220
+ const row = await record.state(name);
221
+ if (!row) throw new Error(`${kind} "${name}" does not exist`);
222
+ const failed = await redeliverRoutine(kind, row, { ...deps, ...record });
223
+ if (failed.length) {
224
+ const latest = await record.state(name);
225
+ const error = latest?.resultDelivery?.[failed.at(-1)]?.error;
226
+ throw new Error(`"${name}": the result still could not be delivered${error ? ` (${error})` : ''}`);
227
+ }
228
+ return { name, delivered: Object.keys(row.resultDelivery || {}).length };
229
+ }
@@ -30,6 +30,11 @@ export function createResourceApi(deps) {
30
30
  if (!EXTENSION_SCOPE_KINDS.includes(scopeKind)) {
31
31
  throw new Error(`extension scope kind must be one of ${EXTENSION_SCOPE_KINDS.join(', ')}`);
32
32
  }
33
+ // A connector is on in every project unless a project switches it off
34
+ // (setProjectConnectorEnabled); "only in this project" no longer exists.
35
+ if (scopeKind === 'mcp') {
36
+ throw new Error('MCP servers are switched off per project in Project settings (setProjectConnectorEnabled)');
37
+ }
33
38
  let key = clean(name);
34
39
  if (!key) throw new Error('extension name is required');
35
40
  // Plugin scopes are keyed by registry id; a name must resolve to it.