@vellumai/assistant 0.11.3 → 0.11.4-dev.202608190019.b94dbf2

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 (596) hide show
  1. package/AGENTS.md +9 -3
  2. package/ARCHITECTURE.md +32 -10
  3. package/docs/architecture/memory.md +43 -5
  4. package/docs/architecture/security.md +96 -152
  5. package/docs/architecture/turn-actor.md +70 -0
  6. package/docs/browser-use-architecture-phase2.md +128 -56
  7. package/docs/flux-turn-detection-spike.md +248 -0
  8. package/docs/guardian-request-flow.md +35 -0
  9. package/docs/stt-provider-onboarding.md +3 -1
  10. package/knip.json +3 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  24. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  25. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  26. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  28. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  29. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  30. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  31. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  32. package/openapi.yaml +289 -113
  33. package/package.json +1 -1
  34. package/scripts/voice-ttft-spike.ts +3 -3
  35. package/scripts/write-plugin-api-shim.ts +10 -0
  36. package/src/__tests__/app-compiler.test.ts +38 -3
  37. package/src/__tests__/app-control-flow.test.ts +1 -0
  38. package/src/__tests__/approval-routes-http.test.ts +2 -2
  39. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  40. package/src/__tests__/attachments-store.test.ts +21 -12
  41. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  42. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  43. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  44. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  45. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  46. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  47. package/src/__tests__/channel-inbound-disk-pressure.test.ts +4 -4
  48. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  49. package/src/__tests__/checker.test.ts +20 -314
  50. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  51. package/src/__tests__/compaction-events.test.ts +8 -10
  52. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  53. package/src/__tests__/config-schema.test.ts +25 -10
  54. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  55. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  56. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  57. package/src/__tests__/conversation-agent-loop.test.ts +32 -21
  58. package/src/__tests__/conversation-attachments.test.ts +0 -1
  59. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  61. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  63. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  64. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  65. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  66. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  67. package/src/__tests__/conversation-queue.test.ts +154 -4
  68. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  69. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  71. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +187 -102
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  75. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  82. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  83. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  85. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  86. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  87. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  88. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  89. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  90. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  91. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  92. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  93. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  94. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  95. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  96. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  97. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  98. package/src/__tests__/document-sync-tags.test.ts +0 -75
  99. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  100. package/src/__tests__/edit-propagation.test.ts +107 -11
  101. package/src/__tests__/file-ops-service.test.ts +163 -30
  102. package/src/__tests__/filesystem-tools.test.ts +23 -24
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  104. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  105. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  106. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  107. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  108. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  109. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  110. package/src/__tests__/injector-chain.test.ts +63 -41
  111. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  112. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  113. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  114. package/src/__tests__/llm-schema.test.ts +5 -2
  115. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  116. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  117. package/src/__tests__/media-generate-image.test.ts +131 -21
  118. package/src/__tests__/memory-retrieval-hook.test.ts +100 -7
  119. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  120. package/src/__tests__/mtime-cache.test.ts +3 -1
  121. package/src/__tests__/non-member-access-request.test.ts +0 -20
  122. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  123. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  124. package/src/__tests__/plugin-api-store-credential.test.ts +261 -0
  125. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  126. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  127. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  128. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  129. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  130. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -4
  131. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  132. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  133. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  134. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  135. package/src/__tests__/qdrant-manager.test.ts +14 -1
  136. package/src/__tests__/reaction-persistence.test.ts +200 -7
  137. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  138. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  139. package/src/__tests__/run-conversation-turn-persistence.test.ts +460 -105
  140. package/src/__tests__/run-due-schedules.test.ts +21 -0
  141. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  142. package/src/__tests__/schedule-routes.test.ts +23 -0
  143. package/src/__tests__/schedule-store.test.ts +17 -0
  144. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  145. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  146. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  147. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  148. package/src/__tests__/skills.test.ts +32 -0
  149. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  150. package/src/__tests__/starter-task-flow.test.ts +5 -4
  151. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  152. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  153. package/src/__tests__/subagent-spawn-and-await.test.ts +18 -17
  154. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  155. package/src/__tests__/subagent-tools.test.ts +81 -101
  156. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  157. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  158. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  159. package/src/__tests__/tool-executor.test.ts +68 -39
  160. package/src/__tests__/turn-events-store.test.ts +43 -0
  161. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  162. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  163. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  164. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  165. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  166. package/src/__tests__/user-plugin-loader.test.ts +3 -1
  167. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  168. package/src/__tests__/visible-app-context.test.ts +16 -9
  169. package/src/__tests__/voice-config-update.test.ts +40 -0
  170. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  171. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  172. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  173. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  174. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  175. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  176. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  177. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  178. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  179. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  180. package/src/acp/__tests__/auth-required.test.ts +161 -0
  181. package/src/acp/acp-claude-oauth.ts +19 -2
  182. package/src/acp/agent-process.test.ts +100 -0
  183. package/src/acp/agent-process.ts +29 -26
  184. package/src/acp/auth-required.ts +102 -0
  185. package/src/acp/session-manager.test.ts +119 -0
  186. package/src/acp/session-manager.ts +76 -3
  187. package/src/api/events/acp-auth-required.ts +55 -0
  188. package/src/api/events/host-file.ts +2 -2
  189. package/src/api/index.ts +7 -0
  190. package/src/api/surfaces.ts +12 -3
  191. package/src/apps/app-store.ts +3 -0
  192. package/src/bundler/package-resolver.ts +2 -30
  193. package/src/calls/__tests__/voice-session-bridge.test.ts +194 -11
  194. package/src/calls/__tests__/voice-triage-escalate.test.ts +102 -0
  195. package/src/calls/call-controller.ts +19 -3
  196. package/src/calls/call-setup-flow.ts +0 -1
  197. package/src/calls/media-stream-stt-session.ts +15 -0
  198. package/src/calls/voice-session-bridge.ts +115 -41
  199. package/src/calls/voice-triage-escalate.ts +105 -2
  200. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  201. package/src/channels/config.ts +13 -0
  202. package/src/channels/plugin-channel-declarations.ts +108 -0
  203. package/src/channels/types.ts +30 -0
  204. package/src/cli/AGENTS.md +5 -2
  205. package/src/cli/bundled-modules.ts +29 -0
  206. package/src/cli/commands/credentials.help.ts +2 -2
  207. package/src/cli/commands/db/repair.ts +4 -8
  208. package/src/cli/commands/domain.ts +6 -3
  209. package/src/cli/commands/email.ts +6 -3
  210. package/src/cli/commands/inference-providers.ts +1 -1
  211. package/src/cli/commands/keys.ts +8 -3
  212. package/src/cli/commands/mcp.help.ts +13 -4
  213. package/src/cli/commands/mcp.ts +9 -0
  214. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  215. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  216. package/src/cli/commands/memory/index.help.ts +84 -28
  217. package/src/cli/commands/memory/index.ts +2 -0
  218. package/src/cli/commands/memory/memory-v2.ts +58 -54
  219. package/src/cli/commands/memory/memory-v3.ts +64 -0
  220. package/src/cli/commands/memory/memory-validate.ts +18 -0
  221. package/src/cli/commands/plugins.ts +85 -36
  222. package/src/cli/commands/schedules.ts +35 -1
  223. package/src/cli/commands/stt.help.ts +27 -2
  224. package/src/cli/commands/trust.ts +3 -14
  225. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  226. package/src/cli/lib/bundled-marketplace.json +14 -1
  227. package/src/cli/lib/upgrade-plugin.ts +42 -0
  228. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  229. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  230. package/src/config/__tests__/default-provider.test.ts +6 -1
  231. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  232. package/src/config/assistant-feature-flags.ts +36 -15
  233. package/src/config/balanced-model-experiment.ts +35 -0
  234. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  235. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  236. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  237. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  238. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  239. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  240. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  241. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  242. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  243. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  244. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  245. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  246. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  247. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  248. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  249. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  250. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  251. package/src/config/call-site-defaults.ts +11 -5
  252. package/src/config/default-profile-catalog.ts +179 -16
  253. package/src/config/default-profile-names.ts +4 -1
  254. package/src/config/default-provider-resolution.ts +4 -0
  255. package/src/config/feature-flag-registry.json +12 -12
  256. package/src/config/llm-context-resolution.ts +11 -3
  257. package/src/config/llm-resolver.ts +28 -1
  258. package/src/config/profile-materialization.ts +70 -22
  259. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  260. package/src/config/schemas/call-site-catalog.ts +4 -4
  261. package/src/config/schemas/live-voice.ts +57 -23
  262. package/src/config/schemas/llm.ts +59 -32
  263. package/src/config/schemas/mcp.ts +23 -0
  264. package/src/config/schemas/plugin-updates.ts +6 -2
  265. package/src/config/schemas/stt.ts +1 -0
  266. package/src/config/skills.ts +9 -2
  267. package/src/context/outbound-sanitize.ts +96 -1
  268. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  269. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  270. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  271. package/src/daemon/conversation-agent-loop.ts +31 -19
  272. package/src/daemon/conversation-messaging.ts +5 -1
  273. package/src/daemon/conversation-notifiers.ts +20 -10
  274. package/src/daemon/conversation-process.ts +20 -6
  275. package/src/daemon/conversation-runtime-assembly.ts +12 -6
  276. package/src/daemon/conversation-store.ts +14 -4
  277. package/src/daemon/conversation-surfaces.ts +75 -26
  278. package/src/daemon/conversation-tool-setup.ts +3 -6
  279. package/src/daemon/conversation.ts +154 -51
  280. package/src/daemon/doordash-steps.ts +2 -2
  281. package/src/daemon/lifecycle.ts +14 -1
  282. package/src/daemon/mcp-reload-service.ts +36 -6
  283. package/src/daemon/process-message.ts +13 -26
  284. package/src/daemon/providers-setup.ts +6 -3
  285. package/src/daemon/trust-context-types.ts +29 -0
  286. package/src/daemon/trust-context.ts +12 -7
  287. package/src/daemon/wake-conversation-ops.ts +3 -2
  288. package/src/daemon/windows-compiled-entry.ts +4 -0
  289. package/src/documents/document-store.ts +143 -240
  290. package/src/hooks/hook-loader.ts +3 -3
  291. package/src/hooks/registry.ts +103 -45
  292. package/src/hooks/types.ts +5 -0
  293. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  294. package/src/inbound/oauth-callback-url.ts +61 -0
  295. package/src/ipc/gateway-client.test.ts +1 -1
  296. package/src/ipc/gateway-client.ts +14 -21
  297. package/src/ipc/gateway-flag-listener.ts +17 -3
  298. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  299. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  300. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +1050 -0
  301. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  302. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  303. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  304. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  305. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  306. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  307. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  308. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  309. package/src/live-voice/live-voice-archive.ts +2 -0
  310. package/src/live-voice/live-voice-manager.ts +16 -3
  311. package/src/live-voice/live-voice-metrics.ts +57 -32
  312. package/src/live-voice/live-voice-photo.ts +1 -2
  313. package/src/live-voice/live-voice-session.ts +598 -323
  314. package/src/live-voice/progress-narration.ts +277 -0
  315. package/src/live-voice/protocol.ts +21 -1
  316. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  317. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  318. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  319. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  320. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  321. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  322. package/src/mcp/client.ts +16 -11
  323. package/src/mcp/effective-config.ts +113 -0
  324. package/src/mcp/manager.ts +11 -6
  325. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  326. package/src/mcp/mcp-oauth-provider.ts +205 -240
  327. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  328. package/src/monitoring/control.ts +1 -0
  329. package/src/monitoring/db-integrity-sample.ts +4 -5
  330. package/src/monitoring/plugin-auto-update.ts +128 -24
  331. package/src/notifications/AGENTS.md +2 -0
  332. package/src/notifications/approval-card-data.ts +33 -0
  333. package/src/notifications/signal.ts +1 -0
  334. package/src/permissions/AGENTS.md +16 -0
  335. package/src/permissions/checker.test.ts +75 -122
  336. package/src/permissions/checker.ts +44 -560
  337. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  338. package/src/permissions/confirmation-guardian-request.ts +2 -2
  339. package/src/permissions/prompter.ts +1 -5
  340. package/src/permissions/question-guardian-request.test.ts +14 -6
  341. package/src/permissions/question-guardian-request.ts +1 -2
  342. package/src/persistence/attachments-store.ts +8 -1
  343. package/src/persistence/bookmark-crud.ts +3 -7
  344. package/src/persistence/conversation-attention-store.ts +16 -45
  345. package/src/persistence/conversation-crud.ts +33 -4
  346. package/src/persistence/conversation-lineage.ts +9 -0
  347. package/src/persistence/conversation-queries.ts +182 -52
  348. package/src/persistence/delivery-crud.ts +150 -36
  349. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  350. package/src/persistence/external-conversation-store.ts +32 -4
  351. package/src/persistence/llm-request-log-store.ts +4 -10
  352. package/src/persistence/llm-usage-store.ts +8 -3
  353. package/src/persistence/message-reads.test.ts +197 -0
  354. package/src/persistence/message-reads.ts +211 -0
  355. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  356. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  357. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  358. package/src/persistence/real-user-turn-filter.ts +27 -3
  359. package/src/persistence/schema/documents.ts +4 -4
  360. package/src/persistence/steps.ts +9 -0
  361. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  362. package/src/plugin-api/constants.ts +8 -0
  363. package/src/plugin-api/conversation-turn.ts +195 -5
  364. package/src/plugin-api/index.ts +38 -6
  365. package/src/plugin-api/resolve-credential.ts +3 -2
  366. package/src/plugin-api/store-credential.ts +146 -0
  367. package/src/plugin-api/vision-support.test.ts +1 -1
  368. package/src/plugin-api/webhook-url.ts +13 -11
  369. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  370. package/src/plugins/defaults/main.ts +21 -7
  371. package/src/plugins/defaults/memory/AGENTS.md +15 -3
  372. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  373. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  374. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  375. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  376. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  377. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  378. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  379. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  380. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  381. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  382. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  383. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  384. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +17 -24
  385. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  386. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  387. package/src/plugins/defaults/memory/indexer.ts +3 -1
  388. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  389. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  390. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  391. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  392. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  393. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +147 -0
  394. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  395. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  396. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  397. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  398. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  399. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  400. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +195 -58
  401. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +93 -24
  402. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  403. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  404. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  405. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  406. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  407. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  408. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  409. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -33
  410. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  411. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  412. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  413. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  414. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  415. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  416. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  417. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  418. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  419. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  420. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  421. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  422. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  423. package/src/plugins/defaults/memory/worker.ts +6 -3
  424. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  425. package/src/plugins/external-plugin-loader.ts +47 -0
  426. package/src/plugins/mcp-servers.ts +361 -0
  427. package/src/plugins/mtime-cache.ts +37 -46
  428. package/src/plugins/pipeline.ts +14 -7
  429. package/src/plugins/plugin-execution-context.ts +35 -11
  430. package/src/plugins/worker-plugin-surface.ts +33 -0
  431. package/src/prompts/templates/system-sections.ts +0 -7
  432. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  433. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  434. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  435. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  436. package/src/providers/__tests__/retry-callsite.test.ts +25 -2
  437. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  438. package/src/providers/anthropic/client.ts +332 -241
  439. package/src/providers/call-site-routing.ts +30 -3
  440. package/src/providers/connection-resolution.ts +194 -11
  441. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +4 -0
  442. package/src/providers/inference/adapter-factory.ts +3 -0
  443. package/src/providers/inference/auth.ts +6 -0
  444. package/src/providers/inference/connection-availability.ts +24 -2
  445. package/src/providers/inference/connections.ts +2 -0
  446. package/src/providers/model-catalog.ts +3 -3
  447. package/src/providers/model-intents.ts +28 -8
  448. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +163 -40
  449. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  450. package/src/providers/openai/chat-completions-provider.ts +133 -36
  451. package/src/providers/openai/codex-models.ts +2 -1
  452. package/src/providers/provider-send-message.ts +32 -3
  453. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  454. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +625 -0
  455. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  456. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  457. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  458. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +679 -0
  459. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  460. package/src/providers/speech-to-text/resolve.ts +25 -2
  461. package/src/routes/control.ts +1 -0
  462. package/src/routes/route-host-client.ts +1 -0
  463. package/src/routes/route-host-protocol.ts +7 -0
  464. package/src/routes/worker.ts +48 -15
  465. package/src/runtime/AGENTS.md +16 -17
  466. package/src/runtime/access-request-helper.ts +9 -12
  467. package/src/runtime/agent-wake.ts +18 -15
  468. package/src/runtime/effective-capabilities.test.ts +43 -0
  469. package/src/runtime/effective-capabilities.ts +27 -0
  470. package/src/runtime/guardian-reply-router.ts +6 -2
  471. package/src/runtime/pre-first-message-gate.ts +4 -0
  472. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  473. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +522 -2
  474. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  475. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  476. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  477. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  478. package/src/runtime/routes/__tests__/plugins-routes.test.ts +132 -35
  479. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  480. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  481. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  482. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  483. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +133 -1
  484. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  485. package/src/runtime/routes/channel-route-shared.ts +0 -6
  486. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  487. package/src/runtime/routes/conversation-list-routes.ts +181 -23
  488. package/src/runtime/routes/conversation-management-routes.ts +17 -12
  489. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  490. package/src/runtime/routes/conversation-routes.ts +43 -29
  491. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  492. package/src/runtime/routes/credential-routes.ts +26 -82
  493. package/src/runtime/routes/default-provider-routes.ts +15 -0
  494. package/src/runtime/routes/documents-routes.ts +3 -222
  495. package/src/runtime/routes/inbound-message-handler.ts +21 -45
  496. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  497. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  498. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  499. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  500. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  501. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -79
  502. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  503. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -7
  504. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -8
  505. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -79
  506. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  507. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  508. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  509. package/src/runtime/routes/log-export-routes.ts +3 -0
  510. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  511. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  512. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  513. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  514. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  515. package/src/runtime/routes/plugins-routes.ts +38 -8
  516. package/src/runtime/routes/schedule-routes.ts +94 -7
  517. package/src/runtime/routes/settings-routes.ts +6 -6
  518. package/src/runtime/routes/stt-routes.ts +31 -25
  519. package/src/runtime/routes/surface-action-routes.ts +20 -19
  520. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  521. package/src/runtime/routes/user-route-dispatcher.ts +69 -15
  522. package/src/runtime/routes/user-route-import.ts +108 -0
  523. package/src/runtime/routes/user-route-resolution.ts +18 -1
  524. package/src/runtime/routes/workspace-routes.ts +0 -9
  525. package/src/runtime/routes/workspace-utils.ts +3 -13
  526. package/src/runtime/services/conversation-serializer.ts +7 -2
  527. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  528. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  529. package/src/schedule/plugin-schedule-availability.ts +58 -0
  530. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  531. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  532. package/src/schedule/schedule-store.ts +5 -1
  533. package/src/schedule/scheduler.ts +9 -4
  534. package/src/schedule/worker-control.ts +1 -0
  535. package/src/schedule/worker.ts +6 -0
  536. package/src/security/oauth2.ts +6 -22
  537. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  538. package/src/stt/__tests__/types.test.ts +94 -0
  539. package/src/stt/daemon-batch-transcriber.ts +10 -0
  540. package/src/stt/stt-stream-session.ts +8 -4
  541. package/src/stt/types.ts +103 -0
  542. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  543. package/src/subagent/consult-context.ts +11 -11
  544. package/src/subagent/consult-prompt.ts +26 -35
  545. package/src/subagent/manager.ts +21 -40
  546. package/src/subagent/notify.ts +7 -1
  547. package/src/subagent/types.ts +15 -12
  548. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  549. package/src/tools/acp/spawn.test.ts +97 -0
  550. package/src/tools/acp/spawn.ts +38 -4
  551. package/src/tools/credentials/ref-parse.ts +35 -0
  552. package/src/tools/credentials/resolve.ts +4 -10
  553. package/src/tools/credentials/store.ts +168 -0
  554. package/src/tools/document/document-tool.ts +12 -3
  555. package/src/tools/executor.ts +53 -15
  556. package/src/tools/filesystem/read.ts +27 -10
  557. package/src/tools/host-filesystem/read.ts +27 -15
  558. package/src/tools/network/url-safety.ts +9 -16
  559. package/src/tools/permission-checker.ts +43 -52
  560. package/src/tools/registry.ts +2 -1
  561. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  562. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  563. package/src/tools/shared/filesystem/types.ts +5 -5
  564. package/src/tools/skills/scaffold-managed.ts +25 -7
  565. package/src/tools/subagent/spawn.ts +28 -88
  566. package/src/tools/tool-types.ts +26 -13
  567. package/src/tools/types.ts +6 -6
  568. package/src/tools/ui-surface/surface-shape-docs.ts +12 -1
  569. package/src/tools/workflows/run-workflow.ts +1 -2
  570. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  571. package/src/tts/reasoning-tag-filter.ts +89 -0
  572. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  573. package/src/util/logger.ts +16 -0
  574. package/src/util/think-tag-stream.ts +95 -0
  575. package/src/util/worker-process.ts +37 -4
  576. package/src/windows-compiled-cli.ts +32 -0
  577. package/src/windows-compiled-entry.ts +4 -0
  578. package/src/windows-compiled-logger.ts +6 -0
  579. package/src/windows-compiled-worker-entry.ts +29 -0
  580. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  581. package/src/workspace/custom-profile-ensure.ts +4 -24
  582. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  583. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  584. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  585. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  586. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  587. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  588. package/src/workspace/migrations/registry.ts +10 -0
  589. package/src/workspace/provider-commit-message-generator.ts +7 -5
  590. package/src/__tests__/document-workspace-file.test.ts +0 -467
  591. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  592. package/src/live-voice/front-decision.ts +0 -476
  593. package/src/permissions/ipc-risk-types.ts +0 -143
  594. package/src/permissions/risk-types.ts +0 -76
  595. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  596. package/src/subagent/consult-transcript.ts +0 -90
@@ -1,102 +1,182 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
1
+ /**
2
+ * `runConversationTurn` against the real conversation store, the real agent
3
+ * loop and the real database. The only thing stood in for is the provider's
4
+ * HTTP boundary — `resolveProviderFromConnection` hands back a scripted mock
5
+ * — so everything these tests read back (the `conversations` row, the user
6
+ * message and the metadata stamped on it, the channel binding) is what
7
+ * production wrote, not a stub mirroring what production is believed to do.
8
+ */
9
+ import {
10
+ afterEach,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ mock,
15
+ spyOn,
16
+ test,
17
+ } from "bun:test";
2
18
 
19
+ import type { AssistantEvent } from "../api/index.js";
20
+ import { clearAllActiveConversations } from "../daemon/conversation-store.js";
21
+ import {
22
+ conversationMetadataSyncTag,
23
+ SYNC_TAGS,
24
+ } from "../daemon/message-types/sync.js";
3
25
  import {
4
26
  createConversation,
5
- ensureConversationExists,
6
27
  getConversation,
28
+ getMessages,
29
+ parseMessageMetadata,
7
30
  } from "../persistence/conversation-crud.js";
31
+ import { getConversationByKey } from "../persistence/conversation-key-store.js";
8
32
  import {
9
33
  isEchoSuppressedUserMessage,
10
34
  isReplyPushIneligibleUserMessage,
11
35
  } from "../persistence/conversation-types.js";
12
36
  import { getDb } from "../persistence/db-connection.js";
13
37
  import { initializeDb } from "../persistence/db-init.js";
38
+ import { buildScopedConversationKey } from "../persistence/delivery-crud.js";
39
+ import { getBindingByConversation } from "../persistence/external-conversation-store.js";
40
+ import { createConnection } from "../providers/inference/connections.js";
41
+ import * as providerRegistry from "../providers/registry.js";
42
+ import type { Provider, ProviderResponse } from "../providers/types.js";
43
+ import { createMockProvider, textResponse } from "./helpers/mock-provider.js";
44
+ import { setConfig } from "./helpers/set-config.js";
45
+ import { waitFor } from "./helpers/wait-for.js";
14
46
 
15
47
  await initializeDb();
16
48
 
17
- // Capture the list-invalidation calls `runConversationTurn` fires when it
18
- // creates a brand-new conversation row, without pulling in the real sync
19
- // publisher (which reaches for live SSE subscribers).
20
- const listChangedCalls: Array<{ kind: string; conversationId: string }> = [];
21
- mock.module("../runtime/sync/resource-sync-events.js", () => ({
22
- publishConversationListAndMetadataChanged: (
23
- kind: string,
24
- conversationId: string,
25
- ) => {
26
- listChangedCalls.push({ kind, conversationId });
27
- },
28
- }));
29
-
30
- // Stub the heavy machinery: the in-memory conversation build (provider wiring,
31
- // system prompt, history hydration) and the SSE event fan-out. The agent turn
32
- // itself is a no-op — this test only asserts that the `conversations` row is
33
- // persisted before the turn runs. The real `getOrCreateConversation` now
34
- // creates the DB row before hydrating, so the mock mirrors that by calling
35
- // `ensureConversationExists`. The persistence module is intentionally NOT
36
- // mocked so the real `ensureConversationExists` runs against the real DB.
37
- let lastProcessMessageConversationId: string | undefined;
38
- let lastProcessMessageOptions: Record<string, unknown> | undefined;
39
- let lastEnqueueOptions: Record<string, unknown> | undefined;
40
- let conversationIsProcessing = false;
41
- mock.module("../daemon/conversation-store.js", () => ({
42
- getOrCreateConversation: async (
43
- conversationId: string,
44
- options?: { conversationType?: string },
45
- ) => {
46
- if (!getConversation(conversationId)) {
47
- if (options?.conversationType) {
48
- createConversation({
49
- id: conversationId,
50
- conversationType: options.conversationType as
51
- | "standard"
52
- | "background",
53
- });
54
- } else {
55
- ensureConversationExists(conversationId, "vellum");
56
- }
57
- }
58
- return {
59
- abortController: undefined,
60
- isProcessing: () => conversationIsProcessing,
61
- async processMessage(processOptions: Record<string, unknown>) {
62
- // The row must already exist by the time the turn persists its user
63
- // message — record the id so the FK precondition can be asserted.
64
- lastProcessMessageConversationId = conversationId;
65
- lastProcessMessageOptions = processOptions;
66
- return "user-message-id";
67
- },
68
- enqueueMessage: (enqueueOptions: Record<string, unknown>) => {
69
- lastEnqueueOptions = enqueueOptions;
70
- return { rejected: false };
71
- },
72
- };
49
+ // A conversation is built against the default provider, which resolves from
50
+ // config through a connection row. Both are seeded for real so the store takes
51
+ // the same path it takes in the daemon; only the provider the connection
52
+ // resolves to is scripted.
53
+ setConfig("llm", {
54
+ profiles: {
55
+ test: {
56
+ provider: "anthropic",
57
+ provider_connection: "test-conn",
58
+ model: "claude-opus-4-6",
59
+ },
73
60
  },
74
- }));
61
+ activeProfile: "test",
62
+ });
63
+ setConfig("memory", { enabled: false, v2: { enabled: false } });
64
+
65
+ createConnection(getDb(), {
66
+ name: "test-conn",
67
+ provider: "anthropic",
68
+ auth: { type: "api_key", credential: "credential/test/api_key" },
69
+ });
70
+
71
+ // The SSE fan-out is the one collaborator with nowhere to write in a test —
72
+ // every publish the turn makes (including the list invalidations asserted
73
+ // below, which reach clients through the real `resource-sync-events`) funnels
74
+ // through here.
75
+ const broadcasts: AssistantEvent[] = [];
76
+ mock.module(
77
+ "../runtime/assistant-event-hub.js",
78
+ (): Partial<typeof import("../runtime/assistant-event-hub.js")> => ({
79
+ broadcastMessage: (msg: AssistantEvent) => {
80
+ broadcasts.push(msg);
81
+ },
82
+ }),
83
+ );
75
84
 
76
- mock.module("../runtime/assistant-event-hub.js", () => ({
77
- broadcastMessage: () => {},
78
- }));
85
+ const { provider: scriptedProvider } = createMockProvider([
86
+ textResponse("scripted reply"),
87
+ ]);
79
88
 
80
- mock.module("../providers/media-resolve.js", () => ({
81
- resolveMediaSourceData: () => null,
82
- }));
89
+ let gate: Promise<void> | undefined;
90
+ let releaseGate: (() => void) | undefined;
91
+
92
+ /**
93
+ * Park the next turn inside the provider call until released, so a second turn
94
+ * arrives while the conversation is genuinely processing rather than while a
95
+ * stub reports that it is.
96
+ */
97
+ function holdTheTurnOpen(): void {
98
+ let resolveGate!: () => void;
99
+ gate = new Promise<void>((resolve) => {
100
+ resolveGate = resolve;
101
+ });
102
+ releaseGate = () => {
103
+ gate = undefined;
104
+ releaseGate = undefined;
105
+ resolveGate();
106
+ };
107
+ }
108
+
109
+ // A test that fails before releasing must not leave the next one parked on a
110
+ // gate it never opened.
111
+ afterEach(() => releaseGate?.());
112
+
113
+ const providerCalls: { tools?: { name: string }[] }[] = [];
114
+
115
+ const provider: Provider = {
116
+ name: "scripted",
117
+ async sendMessage(messages, options): Promise<ProviderResponse> {
118
+ providerCalls.push({ tools: options?.tools });
119
+ await gate;
120
+ return scriptedProvider.sendMessage(messages, options);
121
+ },
122
+ };
123
+
124
+ spyOn(providerRegistry, "resolveProviderFromConnection").mockResolvedValue(
125
+ provider,
126
+ );
83
127
 
84
- // Import under test AFTER the mocks are registered so its dynamic imports
85
- // resolve to the stubs above.
86
128
  const { runConversationTurn } =
87
129
  await import("../plugin-api/conversation-turn.js");
88
130
 
131
+ /**
132
+ * The list-level invalidations a turn published, as the tag sets clients
133
+ * consume. Only a shape-changing reason carries the list umbrella tag, so a
134
+ * reason that merely edits an existing row (a generated title) is excluded —
135
+ * these are the announcements that a conversation appeared or vanished.
136
+ */
137
+ function listInvalidations(): string[][] {
138
+ return broadcasts
139
+ .filter((msg) => msg.type === "sync_changed")
140
+ .map((msg) => msg.tags)
141
+ .filter((tags) => tags.includes(SYNC_TAGS.conversationsList));
142
+ }
143
+
144
+ /** The tag set `publishConversationListAndMetadataChanged` emits for a new row. */
145
+ function creationTags(conversationId: string): string[] {
146
+ return [
147
+ SYNC_TAGS.conversationsList,
148
+ conversationMetadataSyncTag(conversationId),
149
+ ];
150
+ }
151
+
152
+ /** User message rows of a conversation, oldest first. */
153
+ function userRows(conversationId: string) {
154
+ return getMessages(conversationId).filter((row) => row.role === "user");
155
+ }
156
+
157
+ /** The metadata a turn persisted on a message row, parsed as consumers read it. */
158
+ function metadataOf(
159
+ row: { metadata: string | null } | undefined,
160
+ ): Record<string, unknown> | undefined {
161
+ return parseMessageMetadata(row?.metadata ?? null);
162
+ }
163
+
164
+ function resetDb(): void {
165
+ // In-memory conversations outlive the rows they were hydrated from, and a
166
+ // reused id would otherwise hand the next test a Conversation whose
167
+ // `conversations` row this wipe just deleted.
168
+ clearAllActiveConversations();
169
+ const db = getDb();
170
+ db.run("DELETE FROM messages");
171
+ db.run("DELETE FROM external_conversation_bindings");
172
+ db.run("DELETE FROM conversation_keys");
173
+ db.run("DELETE FROM conversations");
174
+ broadcasts.length = 0;
175
+ providerCalls.length = 0;
176
+ }
177
+
89
178
  describe("runConversationTurn persistence", () => {
90
- beforeEach(() => {
91
- const db = getDb();
92
- db.run("DELETE FROM messages");
93
- db.run("DELETE FROM conversations");
94
- listChangedCalls.length = 0;
95
- lastProcessMessageConversationId = undefined;
96
- lastProcessMessageOptions = undefined;
97
- lastEnqueueOptions = undefined;
98
- conversationIsProcessing = false;
99
- });
179
+ beforeEach(resetDb);
100
180
 
101
181
  test("persists a conversations row for a freshly-minted conversation", async () => {
102
182
  const result = await runConversationTurn({
@@ -104,16 +184,20 @@ describe("runConversationTurn persistence", () => {
104
184
  });
105
185
 
106
186
  // The row exists on disk — not just as an in-memory Conversation object —
107
- // so the user-message persist inside the turn has its FK target.
108
- const row = getConversation(result.conversationId);
109
- expect(row?.id).toBe(result.conversationId);
110
- expect(lastProcessMessageConversationId).toBe(result.conversationId);
187
+ // and the user message the turn persisted found its FK target there.
188
+ expect(getConversation(result.conversationId)?.id).toBe(
189
+ result.conversationId,
190
+ );
191
+ expect(userRows(result.conversationId).map((row) => row.id)).toEqual([
192
+ result.userMessageId,
193
+ ]);
111
194
 
112
195
  // Siblings/sidebars are told about the new conversation, mirroring the
113
196
  // send-message route.
114
- expect(listChangedCalls).toEqual([
115
- { kind: "created", conversationId: result.conversationId },
116
- ]);
197
+ expect(listInvalidations()).toEqual([creationTags(result.conversationId)]);
198
+ expect(
199
+ broadcasts.filter((msg) => msg.type === "conversation_list_invalidated"),
200
+ ).toEqual([{ type: "conversation_list_invalidated", reason: "created" }]);
117
201
  });
118
202
 
119
203
  test("adopts a caller-supplied conversation id verbatim when no row exists", async () => {
@@ -126,12 +210,12 @@ describe("runConversationTurn persistence", () => {
126
210
 
127
211
  expect(result.conversationId).toBe(conversationId);
128
212
  expect(getConversation(conversationId)?.id).toBe(conversationId);
129
- expect(listChangedCalls).toEqual([{ kind: "created", conversationId }]);
213
+ expect(listInvalidations()).toEqual([creationTags(conversationId)]);
130
214
  });
131
215
 
132
216
  test("is a no-op for an already-persisted conversation row", async () => {
133
217
  const existing = createConversation({ title: "already here" });
134
- listChangedCalls.length = 0;
218
+ broadcasts.length = 0;
135
219
 
136
220
  const result = await runConversationTurn({
137
221
  conversationId: existing.id,
@@ -141,7 +225,7 @@ describe("runConversationTurn persistence", () => {
141
225
  expect(result.conversationId).toBe(existing.id);
142
226
  // Row is untouched and no duplicate "created" invalidation fires.
143
227
  expect(getConversation(existing.id)?.title).toBe("already here");
144
- expect(listChangedCalls).toEqual([]);
228
+ expect(listInvalidations()).toEqual([]);
145
229
  });
146
230
  });
147
231
 
@@ -151,28 +235,24 @@ describe("runConversationTurn persistence", () => {
151
235
  // eligibility predicate's verdict on the stamped metadata, so the marker and
152
236
  // the gate that reads it cannot drift apart.
153
237
  describe("runConversationTurn provenance", () => {
154
- beforeEach(() => {
155
- const db = getDb();
156
- db.run("DELETE FROM messages");
157
- db.run("DELETE FROM conversations");
158
- lastProcessMessageOptions = undefined;
159
- lastEnqueueOptions = undefined;
160
- conversationIsProcessing = false;
161
- });
238
+ beforeEach(resetDb);
162
239
 
163
240
  test("stamps the initiating row automated so its reply raises no push", async () => {
164
241
  const existing = createConversation({ title: "standard conversation" });
165
242
 
166
- await runConversationTurn({
243
+ const result = await runConversationTurn({
167
244
  conversationId: existing.id,
168
245
  content: [{ type: "text", text: "transcript excerpt" }],
169
246
  });
170
247
 
171
- const metadata = lastProcessMessageOptions?.metadata as Record<
172
- string,
173
- unknown
174
- >;
175
- expect(metadata).toEqual({ automated: true });
248
+ // The scripted reply came back, so the marker below was stamped by a turn
249
+ // that ran rather than by one that fell over into an error row.
250
+ expect(result.content).toMatchObject([
251
+ { type: "text", text: "scripted reply" },
252
+ ]);
253
+
254
+ const metadata = metadataOf(userRows(existing.id)[0]);
255
+ expect(metadata).toMatchObject({ automated: true });
176
256
  expect(isReplyPushIneligibleUserMessage(metadata)).toBe(true);
177
257
  // Not an echo-suppression marker: the row still renders in the transcript.
178
258
  expect(isEchoSuppressedUserMessage(metadata)).toBe(false);
@@ -180,16 +260,291 @@ describe("runConversationTurn provenance", () => {
180
260
 
181
261
  test("stamps the same marker on a turn queued behind a busy conversation", async () => {
182
262
  const existing = createConversation({ title: "busy conversation" });
183
- conversationIsProcessing = true;
263
+ holdTheTurnOpen();
264
+
265
+ const inFlight = runConversationTurn({
266
+ conversationId: existing.id,
267
+ content: [{ type: "text", text: "first excerpt" }],
268
+ });
269
+ await waitFor(() => userRows(existing.id).length === 1, {
270
+ timeoutMs: 5_000,
271
+ message: "the first turn never reached the provider",
272
+ });
184
273
 
185
274
  const result = await runConversationTurn({
186
275
  conversationId: existing.id,
187
276
  content: [{ type: "text", text: "transcript excerpt" }],
188
277
  });
189
-
190
278
  expect(result.queued).toBe(true);
191
- const metadata = lastEnqueueOptions?.metadata as Record<string, unknown>;
192
- expect(metadata).toEqual({ automated: true });
279
+
280
+ // The queue drains after the in-flight turn releases, and the marker has
281
+ // to survive that trip: it is stamped now but read off the persisted row
282
+ // much later.
283
+ releaseGate?.();
284
+ await inFlight;
285
+ await waitFor(() => userRows(existing.id).length === 2, {
286
+ timeoutMs: 5_000,
287
+ message: "the queued turn never drained",
288
+ });
289
+
290
+ const metadata = metadataOf(userRows(existing.id)[1]);
291
+ expect(metadata).toMatchObject({ automated: true });
193
292
  expect(isReplyPushIneligibleUserMessage(metadata)).toBe(true);
293
+ }, 20_000);
294
+ });
295
+
296
+ /**
297
+ * Addressing a turn by chat rather than by conversation id.
298
+ *
299
+ * The point of these is not that the turn runs (that is the same code path
300
+ * either way) but that the conversation it runs in is the one the rest of
301
+ * the assistant already addresses by those coordinates. A caller keeping its
302
+ * own chat-to-conversation map would pass every test above and still be
303
+ * invisible to conversation reset, to the deny lanes, and to the channel
304
+ * metadata the conversation list renders.
305
+ */
306
+ describe("runConversationTurn channel binding", () => {
307
+ const CHANNEL = {
308
+ sourceChannel: "plugin" as const,
309
+ externalChatId: "imessage:+12025550142",
310
+ displayName: "Ada",
311
+ };
312
+
313
+ beforeEach(resetDb);
314
+
315
+ test("resolves the chat to the conversation inbound would have used", async () => {
316
+ const result = await runConversationTurn({
317
+ channel: CHANNEL,
318
+ content: [{ type: "text", text: "hello" }],
319
+ });
320
+
321
+ // The public name for this conversation, which is what reset and the deny
322
+ // lanes address it by.
323
+ expect(
324
+ getConversationByKey(
325
+ buildScopedConversationKey(
326
+ CHANNEL.sourceChannel,
327
+ CHANNEL.externalChatId,
328
+ ),
329
+ )?.conversationId,
330
+ ).toBe(result.conversationId);
331
+ });
332
+
333
+ test("keeps the same chat in the same conversation across turns", async () => {
334
+ const first = await runConversationTurn({
335
+ channel: CHANNEL,
336
+ content: [{ type: "text", text: "hello" }],
337
+ });
338
+ const second = await runConversationTurn({
339
+ channel: CHANNEL,
340
+ content: [{ type: "text", text: "still me" }],
341
+ });
342
+
343
+ expect(second.conversationId).toBe(first.conversationId);
344
+ // Only the first turn minted a conversation.
345
+ expect(listInvalidations()).toEqual([creationTags(first.conversationId)]);
346
+ });
347
+
348
+ test("gives a different chat its own conversation", async () => {
349
+ const ada = await runConversationTurn({
350
+ channel: CHANNEL,
351
+ content: [{ type: "text", text: "hello" }],
352
+ });
353
+ const grace = await runConversationTurn({
354
+ channel: { ...CHANNEL, externalChatId: "imessage:+12025550188" },
355
+ content: [{ type: "text", text: "hello" }],
356
+ });
357
+
358
+ expect(grace.conversationId).not.toBe(ada.conversationId);
359
+ });
360
+
361
+ test("records the channel metadata the conversation list reads", async () => {
362
+ const result = await runConversationTurn({
363
+ channel: { ...CHANNEL, externalUserId: "+12025550142" },
364
+ content: [{ type: "text", text: "hello" }],
365
+ });
366
+
367
+ const binding = getBindingByConversation(result.conversationId);
368
+ expect(binding).toMatchObject({
369
+ sourceChannel: "plugin",
370
+ externalChatId: CHANNEL.externalChatId,
371
+ externalUserId: "+12025550142",
372
+ displayName: "Ada",
373
+ });
374
+ });
375
+
376
+ test("follows the vendor when a display name changes", async () => {
377
+ await runConversationTurn({
378
+ channel: CHANNEL,
379
+ content: [{ type: "text", text: "hello" }],
380
+ });
381
+ const result = await runConversationTurn({
382
+ channel: { ...CHANNEL, displayName: "Ada L." },
383
+ content: [{ type: "text", text: "renamed" }],
384
+ });
385
+
386
+ expect(getBindingByConversation(result.conversationId)?.displayName).toBe(
387
+ "Ada L.",
388
+ );
389
+ });
390
+
391
+ test("runs the turn on the channel it was addressed by", async () => {
392
+ // Runtime assembly reads the per-turn channel first and only then the
393
+ // conversation's origin, so a turn into a conversation whose origin was
394
+ // never recorded would otherwise run as `vellum`. That is the wrong
395
+ // channel for the message rows and for the permission cascade the tools
396
+ // are approved against.
397
+ const result = await runConversationTurn({
398
+ channel: CHANNEL,
399
+ content: [{ type: "text", text: "hello" }],
400
+ });
401
+
402
+ const rows = getMessages(result.conversationId);
403
+ expect(metadataOf(rows[0])).toMatchObject({
404
+ userMessageChannel: "plugin",
405
+ assistantMessageChannel: "plugin",
406
+ userMessageInterface: "plugin",
407
+ assistantMessageInterface: "plugin",
408
+ });
409
+ expect(rows[1].role).toBe("assistant");
410
+ expect(metadataOf(rows[1])).toMatchObject({
411
+ userMessageChannel: "plugin",
412
+ assistantMessageChannel: "plugin",
413
+ userMessageInterface: "plugin",
414
+ assistantMessageInterface: "plugin",
415
+ });
416
+ });
417
+
418
+ test("puts core tools on the wire for a plugin-channel turn", async () => {
419
+ // Plugin-driven iMessage turns are non-interactive, so host tools stay
420
+ // off, but sandbox tools (bash, web_search, ...) have to remain on the
421
+ // request. Missing them is what makes the model dump `to=bash code:`
422
+ // into the SMS as prose. The create path initializes the registry
423
+ // before constructing the Conversation that snapshots it.
424
+ await runConversationTurn({
425
+ channel: CHANNEL,
426
+ content: [{ type: "text", text: "hello" }],
427
+ });
428
+
429
+ const names = providerCalls[0]?.tools?.map((tool) => tool.name) ?? [];
430
+ expect(names.length).toBeGreaterThan(0);
431
+ expect(names).toContain("bash");
432
+ });
433
+
434
+ test("a queued turn carries its own channel rather than inheriting one", async () => {
435
+ // The drain happens after this call returns and reads the channel off the
436
+ // queued message, falling back to whichever turn was in flight. Here that
437
+ // fallback would be `vellum` — someone else's channel on a conversation
438
+ // shared with another channel.
439
+ const conversationId = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
440
+ holdTheTurnOpen();
441
+
442
+ const inFlight = runConversationTurn({
443
+ conversationId,
444
+ content: [{ type: "text", text: "native turn" }],
445
+ });
446
+ await waitFor(() => userRows(conversationId).length === 1, {
447
+ timeoutMs: 5_000,
448
+ message: "the first turn never reached the provider",
449
+ });
450
+
451
+ const result = await runConversationTurn({
452
+ conversationId,
453
+ channel: CHANNEL,
454
+ content: [{ type: "text", text: "hello" }],
455
+ });
456
+ expect(result.queued).toBe(true);
457
+
458
+ releaseGate?.();
459
+ await inFlight;
460
+ await waitFor(() => userRows(conversationId).length === 2, {
461
+ timeoutMs: 5_000,
462
+ message: "the queued turn never drained",
463
+ });
464
+
465
+ expect(metadataOf(userRows(conversationId)[1])).toMatchObject({
466
+ userMessageChannel: "plugin",
467
+ assistantMessageChannel: "plugin",
468
+ userMessageInterface: "plugin",
469
+ assistantMessageInterface: "plugin",
470
+ });
471
+ }, 20_000);
472
+
473
+ test("leaves the channel unset when the turn names none", async () => {
474
+ // Clearing rather than leaving whatever the last turn set: a stale
475
+ // context would report this turn on a channel it has nothing to do with.
476
+ await runConversationTurn({
477
+ channel: CHANNEL,
478
+ content: [{ type: "text", text: "hello" }],
479
+ });
480
+
481
+ const result = await runConversationTurn({
482
+ content: [{ type: "text", text: "hello" }],
483
+ });
484
+
485
+ const rows = getMessages(result.conversationId);
486
+ const userMetadata = metadataOf(rows[0]);
487
+ expect(userMetadata).toMatchObject({ automated: true });
488
+ expect(userMetadata?.userMessageChannel).toBeUndefined();
489
+ // The conversation carries no origin either, so the turn falls back to the
490
+ // native channel rather than to the previous turn's.
491
+ expect(metadataOf(rows[1])).toMatchObject({
492
+ userMessageChannel: "vellum",
493
+ assistantMessageChannel: "vellum",
494
+ });
495
+ }, 20_000);
496
+
497
+ test("keeps a known sender when a later turn omits it", async () => {
498
+ // A plugin that knows only the chat coordinates this time is silent about
499
+ // the sender, not asserting it has none. Erasing the stored name on that
500
+ // silence drops it from the conversation and session APIs.
501
+ await runConversationTurn({
502
+ channel: { ...CHANNEL, externalUserId: "+12025550142" },
503
+ content: [{ type: "text", text: "hello" }],
504
+ });
505
+
506
+ const result = await runConversationTurn({
507
+ channel: {
508
+ sourceChannel: CHANNEL.sourceChannel,
509
+ externalChatId: CHANNEL.externalChatId,
510
+ },
511
+ content: [{ type: "text", text: "still me" }],
512
+ });
513
+
514
+ expect(getBindingByConversation(result.conversationId)).toMatchObject({
515
+ externalUserId: "+12025550142",
516
+ displayName: "Ada",
517
+ });
518
+ });
519
+
520
+ test("clears a sender the caller explicitly says is gone", async () => {
521
+ await runConversationTurn({
522
+ channel: CHANNEL,
523
+ content: [{ type: "text", text: "hello" }],
524
+ });
525
+
526
+ const result = await runConversationTurn({
527
+ channel: { ...CHANNEL, displayName: null },
528
+ content: [{ type: "text", text: "anonymous now" }],
529
+ });
530
+
531
+ expect(
532
+ getBindingByConversation(result.conversationId)?.displayName,
533
+ ).toBeNull();
534
+ });
535
+
536
+ test("an explicit conversation id wins over the address", async () => {
537
+ // Two different conversations were named. Re-resolving would overrule the
538
+ // caller, so the explicit one is honoured and nothing is bound behind it.
539
+ const existing = createConversation({ title: "chosen" });
540
+
541
+ const result = await runConversationTurn({
542
+ conversationId: existing.id,
543
+ channel: CHANNEL,
544
+ content: [{ type: "text", text: "hello" }],
545
+ });
546
+
547
+ expect(result.conversationId).toBe(existing.id);
548
+ expect(getBindingByConversation(existing.id)).toBeNull();
194
549
  });
195
550
  });
@@ -57,6 +57,7 @@ mock.module("../persistence/lifecycle-quiesce.js", () => ({
57
57
 
58
58
  import { getDb } from "../persistence/db-connection.js";
59
59
  import { initializeDb } from "../persistence/db-init.js";
60
+ import { isPluginDirActivated } from "../plugins/mtime-cache.js";
60
61
  import {
61
62
  createSchedule,
62
63
  deferClaimedSchedule,
@@ -342,6 +343,26 @@ describe("fire-time source-availability gate", () => {
342
343
  expect(skipRunsFor(job.id)).toHaveLength(1);
343
344
  });
344
345
 
346
+ test("executes an armed sourced row even though no plugin is activated in this process", async () => {
347
+ const job = await seedDueSourcedScript();
348
+ // The tick runs in the schedule worker, which activates no plugins, so the
349
+ // activation ledger is empty here. The gate must therefore read the disk
350
+ // alone: reading activation would skip every plugin schedule. Arming is
351
+ // the daemon-side reconciler's call.
352
+ expect(isPluginDirActivated(pluginDir)).toBe(false);
353
+
354
+ const result = await runDueSchedulesOnce();
355
+
356
+ expect(result.completed).toBe(1);
357
+ expect(result.skipped).toBe(0);
358
+ expect(existsSync(marker)).toBe(true);
359
+ const runs = rawDb()
360
+ .query("SELECT status FROM cron_runs WHERE job_id = ?")
361
+ .all(job.id) as Array<{ status: string }>;
362
+ expect(runs).toHaveLength(1);
363
+ expect(runs[0].status).toBe("ok");
364
+ });
365
+
345
366
  test("does not execute a due sourced row while the feature flag is off", async () => {
346
367
  const job = await seedDueSourcedScript();
347
368
  setOverridesForTesting({ "plugin-schedules": false });