@vellumai/assistant 0.11.1 → 0.11.2-staging.2

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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -11,13 +11,17 @@ let mockManagedProxyCtx = {
11
11
  };
12
12
  let mockAssistantId = "";
13
13
  let mockSecureKeys: Record<string, string | null | undefined> = {};
14
+ // Re-pointable so the probe-deadline tests can hang context resolution.
15
+ let mockResolveManagedProxyContext: () => Promise<
16
+ typeof mockManagedProxyCtx
17
+ > = async () => mockManagedProxyCtx;
14
18
 
15
19
  // ---------------------------------------------------------------------------
16
20
  // Module mocks
17
21
  // ---------------------------------------------------------------------------
18
22
 
19
23
  mock.module("../providers/platform-proxy/context.js", () => ({
20
- resolveManagedProxyContext: async () => mockManagedProxyCtx,
24
+ resolveManagedProxyContext: () => mockResolveManagedProxyContext(),
21
25
  }));
22
26
 
23
27
  mock.module("../config/env.js", () => ({
@@ -38,7 +42,11 @@ mock.module("../security/credential-key.js", () => ({
38
42
  // Import under test (after mocks)
39
43
  // ---------------------------------------------------------------------------
40
44
 
41
- import { VellumPlatformClient } from "./client.js";
45
+ import {
46
+ _resetConfiguredProbeCacheForTests,
47
+ isPlatformClientConfigured,
48
+ VellumPlatformClient,
49
+ } from "./client.js";
42
50
 
43
51
  // ---------------------------------------------------------------------------
44
52
  // Tests
@@ -56,6 +64,8 @@ describe("VellumPlatformClient", () => {
56
64
  };
57
65
  mockAssistantId = "asst-123";
58
66
  mockSecureKeys = {};
67
+ mockResolveManagedProxyContext = async () => mockManagedProxyCtx;
68
+ _resetConfiguredProbeCacheForTests();
59
69
  });
60
70
 
61
71
  afterEach(() => {
@@ -119,6 +129,166 @@ describe("VellumPlatformClient", () => {
119
129
  });
120
130
  });
121
131
 
132
+ describe("isPlatformClientConfigured()", () => {
133
+ test("true when base url, api key, and assistant id are all present", async () => {
134
+ expect(await isPlatformClientConfigured()).toBe(true);
135
+ });
136
+
137
+ test("false when the platform assistant id is missing", async () => {
138
+ mockAssistantId = "";
139
+
140
+ expect(await isPlatformClientConfigured()).toBe(false);
141
+ });
142
+
143
+ test("false when auth prerequisites are missing", async () => {
144
+ mockManagedProxyCtx = {
145
+ enabled: false,
146
+ platformBaseUrl: "",
147
+ assistantApiKey: "",
148
+ };
149
+
150
+ expect(await isPlatformClientConfigured()).toBe(false);
151
+ });
152
+
153
+ test("hung resolution answers false at the deadline, then serves the late result from cache", async () => {
154
+ let releaseHang = () => {};
155
+ const hang = new Promise<void>((resolve) => {
156
+ releaseHang = resolve;
157
+ });
158
+ mockResolveManagedProxyContext = async () => {
159
+ await hang;
160
+ return mockManagedProxyCtx;
161
+ };
162
+
163
+ // Resolution is hung and no result has ever settled -- the deadline
164
+ // must answer false instead of stalling the caller.
165
+ expect(await isPlatformClientConfigured()).toBe(false);
166
+
167
+ // Release the abandoned resolution; it settles true in the background
168
+ // and refreshes the cache.
169
+ releaseHang();
170
+ await Bun.sleep(10);
171
+
172
+ // Resolution hangs forever this time, but the cache answers true.
173
+ mockResolveManagedProxyContext = () => new Promise(() => {});
174
+ expect(await isPlatformClientConfigured()).toBe(true);
175
+ });
176
+
177
+ test("concurrent calls share a single in-flight resolution", async () => {
178
+ let resolutionCount = 0;
179
+ mockResolveManagedProxyContext = async () => {
180
+ resolutionCount += 1;
181
+ await Bun.sleep(5);
182
+ return mockManagedProxyCtx;
183
+ };
184
+
185
+ const [first, second] = await Promise.all([
186
+ isPlatformClientConfigured(),
187
+ isPlatformClientConfigured(),
188
+ ]);
189
+
190
+ expect(first).toBe(true);
191
+ expect(second).toBe(true);
192
+ expect(resolutionCount).toBe(1);
193
+ });
194
+
195
+ test("a settled flight clears, so the next call starts a fresh resolution", async () => {
196
+ let resolutionCount = 0;
197
+ mockResolveManagedProxyContext = async () => {
198
+ resolutionCount += 1;
199
+ return mockManagedProxyCtx;
200
+ };
201
+
202
+ expect(await isPlatformClientConfigured()).toBe(true);
203
+ expect(await isPlatformClientConfigured()).toBe(true);
204
+ expect(resolutionCount).toBe(2);
205
+ });
206
+
207
+ test("a caller that finds an abandoned flight rotates in a fresh resolution", async () => {
208
+ let resolutionCount = 0;
209
+ // The first resolution hangs forever; only the rotated-in second one
210
+ // settles.
211
+ const pendingHangs = [new Promise<void>(() => {})];
212
+ mockResolveManagedProxyContext = async () => {
213
+ resolutionCount += 1;
214
+ const hang = pendingHangs.shift();
215
+ if (hang) {
216
+ await hang;
217
+ }
218
+ return mockManagedProxyCtx;
219
+ };
220
+
221
+ // The first caller abandons the hung flight at the deadline.
222
+ expect(await isPlatformClientConfigured()).toBe(false);
223
+
224
+ // The hung flight is marked abandoned, so the next caller rotates in a
225
+ // fresh resolution instead of joining it; the fresh flight settles
226
+ // immediately and answers live.
227
+ expect(await isPlatformClientConfigured()).toBe(true);
228
+ expect(resolutionCount).toBe(2);
229
+ });
230
+
231
+ test("a later caller joins a flight nobody has abandoned", async () => {
232
+ let resolutionCount = 0;
233
+ let release = () => {};
234
+ // Gating the resolution keeps it in flight until the second caller has
235
+ // joined, so the assertion does not race a settle.
236
+ const gate = new Promise<void>((resolve) => {
237
+ release = resolve;
238
+ });
239
+ mockResolveManagedProxyContext = async () => {
240
+ resolutionCount += 1;
241
+ await gate;
242
+ return mockManagedProxyCtx;
243
+ };
244
+
245
+ const first = isPlatformClientConfigured();
246
+ await Bun.sleep(1);
247
+ const second = isPlatformClientConfigured();
248
+ release();
249
+
250
+ expect(await first).toBe(true);
251
+ expect(await second).toBe(true);
252
+ expect(resolutionCount).toBe(1);
253
+ });
254
+
255
+ test("a rotated-out flight's late settle cannot overwrite the newer flight's cached result", async () => {
256
+ let releaseFirstHang = () => {};
257
+ const firstHang = new Promise<void>((resolve) => {
258
+ releaseFirstHang = resolve;
259
+ });
260
+ const pendingHangs = [firstHang];
261
+ mockResolveManagedProxyContext = async () => {
262
+ const hang = pendingHangs.shift();
263
+ if (hang) {
264
+ await hang;
265
+ }
266
+ return mockManagedProxyCtx;
267
+ };
268
+
269
+ // First caller abandons the hung flight; second caller rotates in a
270
+ // fresh flight that settles true and writes the cache.
271
+ expect(await isPlatformClientConfigured()).toBe(false);
272
+ expect(await isPlatformClientConfigured()).toBe(true);
273
+
274
+ // The rotated-out flight now settles resolving an unconfigured
275
+ // context. Generation fencing must keep it from clobbering the newer
276
+ // flight's cached `true`.
277
+ mockManagedProxyCtx = {
278
+ enabled: false,
279
+ platformBaseUrl: "",
280
+ assistantApiKey: "",
281
+ };
282
+ releaseFirstHang();
283
+ await Bun.sleep(10);
284
+
285
+ // Resolution hangs forever, so this answer can only come from the
286
+ // cache, which must still hold the newer flight's `true`.
287
+ mockResolveManagedProxyContext = () => new Promise(() => {});
288
+ expect(await isPlatformClientConfigured()).toBe(true);
289
+ });
290
+ });
291
+
122
292
  describe("fetch()", () => {
123
293
  test("prepends base URL to path", async () => {
124
294
  globalThis.fetch = mock(async (url: string | URL | Request) => {
@@ -34,6 +34,174 @@ export interface OwnerConsent {
34
34
  shareDiagnosticsAcceptedVersion: string;
35
35
  }
36
36
 
37
+ interface PlatformClientConfig {
38
+ baseUrl: string;
39
+ apiKey: string;
40
+ assistantId: string;
41
+ }
42
+
43
+ /**
44
+ * Resolve the platform client's prerequisites.
45
+ *
46
+ * First tries the in-memory managed proxy context (available when the daemon
47
+ * has rehydrated env overrides). Falls back to reading platform credentials
48
+ * directly from the credential store so that standalone CLI invocations work
49
+ * without the daemon having run its rehydration step.
50
+ *
51
+ * Returns `null` when auth prerequisites are missing (not logged in, no API
52
+ * key). The assistant ID is resolved but not required.
53
+ */
54
+ async function resolvePlatformClientConfig(): Promise<PlatformClientConfig | null> {
55
+ if (!arePlatformFeaturesEnabled()) {
56
+ log.debug("platform features disabled -- returning null");
57
+ return null;
58
+ }
59
+
60
+ const ctx = await resolveManagedProxyContext();
61
+
62
+ let baseUrl = ctx.enabled ? ctx.platformBaseUrl : "";
63
+ let apiKey = ctx.enabled ? ctx.assistantApiKey : "";
64
+ let assistantId = getPlatformAssistantId();
65
+
66
+ // Fall back to credential store for values not yet rehydrated (standalone CLI).
67
+ if (!baseUrl) {
68
+ baseUrl =
69
+ (await getSecureKeyAsync(credentialKey("vellum", "platform_base_url"))) ??
70
+ "";
71
+ }
72
+ if (!apiKey) {
73
+ apiKey =
74
+ (await getSecureKeyAsync(credentialKey("vellum", "assistant_api_key"))) ??
75
+ "";
76
+ }
77
+ if (!assistantId) {
78
+ assistantId =
79
+ (
80
+ await getSecureKeyAsync(
81
+ credentialKey("vellum", "platform_assistant_id"),
82
+ )
83
+ )?.trim() ?? "";
84
+ }
85
+
86
+ if (!baseUrl || !apiKey) {
87
+ const level = _missingPrereqsWarned ? "debug" : "warn";
88
+ _missingPrereqsWarned = true;
89
+ log[level](
90
+ {
91
+ hasBaseUrl: !!baseUrl,
92
+ hasApiKey: !!apiKey,
93
+ hasAssistantId: !!assistantId,
94
+ managedProxyEnabled: ctx.enabled,
95
+ },
96
+ "Platform client prerequisites missing -- returning null",
97
+ );
98
+ return null;
99
+ }
100
+
101
+ return { baseUrl, apiKey, assistantId };
102
+ }
103
+
104
+ // Bounds the configured probe: resolution can hit up to three credential-store
105
+ // reads, and urgent-notification dispatch awaits this probe, so a slow
106
+ // credential backend must answer from cache instead of stalling the banner.
107
+ const CONFIGURED_PROBE_DEADLINE_MS = 500;
108
+
109
+ let lastKnownConfigured: boolean | null = null;
110
+ // Single-flight slot with rotation: concurrent probes share one resolution,
111
+ // while a flight some caller already gave up on is replaced so a hung
112
+ // credential backend cannot pin the slot (and the cache) stale until a 45s
113
+ // credential timeout finally settles it. Generation fencing keeps an
114
+ // out-of-order settle from a rotated-out flight from overwriting the result a
115
+ // newer flight already wrote.
116
+ interface ConfiguredProbeFlight {
117
+ promise: Promise<boolean>;
118
+ generation: number;
119
+ // Set once a caller's deadline elapsed on this flight. Rotation keys off
120
+ // this observed give-up rather than a wall-clock age, so the decision never
121
+ // lands on the deadline boundary, where drift between the timer's clock and
122
+ // Date.now() would decide whether a caller rejoins the flight it just
123
+ // abandoned.
124
+ abandoned: boolean;
125
+ }
126
+ let inFlightConfiguredProbe: ConfiguredProbeFlight | null = null;
127
+ let probeGeneration = 0;
128
+ let lastWrittenGeneration = 0;
129
+
130
+ export function _resetConfiguredProbeCacheForTests(): void {
131
+ lastKnownConfigured = null;
132
+ inFlightConfiguredProbe = null;
133
+ // Outstanding flights all carry generations at or below the current
134
+ // counter; raising the written floor to it blocks their settles from
135
+ // resurrecting the cache after a reset.
136
+ lastWrittenGeneration = probeGeneration;
137
+ }
138
+
139
+ function startOrJoinConfiguredProbe(): ConfiguredProbeFlight {
140
+ const existing = inFlightConfiguredProbe;
141
+ if (existing !== null && !existing.abandoned) {
142
+ return existing;
143
+ }
144
+ probeGeneration += 1;
145
+ const generation = probeGeneration;
146
+ const promise = resolvePlatformClientConfig()
147
+ .then((config) => config !== null && config.assistantId.length > 0)
148
+ .catch((err: unknown) => {
149
+ log.debug(
150
+ { err },
151
+ "Configured probe failed -- treating as not configured",
152
+ );
153
+ return false;
154
+ })
155
+ .then((value) => {
156
+ if (generation > lastWrittenGeneration) {
157
+ lastKnownConfigured = value;
158
+ lastWrittenGeneration = generation;
159
+ }
160
+ if (inFlightConfiguredProbe?.generation === generation) {
161
+ inFlightConfiguredProbe = null;
162
+ }
163
+ return value;
164
+ });
165
+ const flight: ConfiguredProbeFlight = {
166
+ promise,
167
+ generation,
168
+ abandoned: false,
169
+ };
170
+ inFlightConfiguredProbe = flight;
171
+ return flight;
172
+ }
173
+
174
+ /**
175
+ * Whether the platform client can actually dispatch: auth prerequisites plus
176
+ * a nonempty platform assistant id (`PlatformPushAdapter.send()` fails fast
177
+ * without one). Constructs no client and makes no network requests.
178
+ *
179
+ * Bounded by {@link CONFIGURED_PROBE_DEADLINE_MS}: when config resolution is
180
+ * slower than the deadline, returns the last settled result (or `false` when
181
+ * none exists yet) and marks the flight abandoned. Concurrent calls share one
182
+ * in-flight resolution; the next call after an abandonment starts a fresh one,
183
+ * so a hung backend never pins the cache stale, and generation fencing keeps a
184
+ * rotated-out flight's late settle from overwriting a newer result.
185
+ */
186
+ export async function isPlatformClientConfigured(): Promise<boolean> {
187
+ const flight = startOrJoinConfiguredProbe();
188
+
189
+ let deadlineTimer: ReturnType<typeof setTimeout> | undefined;
190
+ const deadline = new Promise<"deadline">((resolve) => {
191
+ deadlineTimer = setTimeout(
192
+ () => resolve("deadline"),
193
+ CONFIGURED_PROBE_DEADLINE_MS,
194
+ );
195
+ });
196
+ const raced = await Promise.race([flight.promise, deadline]);
197
+ clearTimeout(deadlineTimer);
198
+ if (raced === "deadline") {
199
+ flight.abandoned = true;
200
+ return lastKnownConfigured ?? false;
201
+ }
202
+ return raced;
203
+ }
204
+
37
205
  export class VellumPlatformClient {
38
206
  private readonly platformBaseUrl: string;
39
207
  private readonly apiKey: string;
@@ -50,67 +218,22 @@ export class VellumPlatformClient {
50
218
  }
51
219
 
52
220
  /**
53
- * Create a platform client by resolving managed proxy context.
54
- *
55
- * First tries the in-memory managed proxy context (available when the daemon
56
- * has rehydrated env overrides). Falls back to reading platform credentials
57
- * directly from the credential store so that standalone CLI invocations work
58
- * without the daemon having run its rehydration step.
221
+ * Create a platform client from {@link resolvePlatformClientConfig}.
59
222
  *
60
223
  * Returns `null` when auth prerequisites are missing (not logged in, no API
61
- * key). The assistant ID is resolved but not required callers that need it
62
- * should check `platformAssistantId` themselves.
224
+ * key). The assistant ID is resolved but not required -- callers that need
225
+ * it should check `platformAssistantId` themselves.
63
226
  */
64
227
  static async create(): Promise<VellumPlatformClient | null> {
65
- if (!arePlatformFeaturesEnabled()) {
66
- log.debug("platform features disabled — returning null");
67
- return null;
68
- }
69
-
70
- const ctx = await resolveManagedProxyContext();
71
-
72
- let baseUrl = ctx.enabled ? ctx.platformBaseUrl : "";
73
- let apiKey = ctx.enabled ? ctx.assistantApiKey : "";
74
- let assistantId = getPlatformAssistantId();
75
-
76
- // Fall back to credential store for values not yet rehydrated (standalone CLI).
77
- if (!baseUrl) {
78
- baseUrl =
79
- (await getSecureKeyAsync(
80
- credentialKey("vellum", "platform_base_url"),
81
- )) ?? "";
82
- }
83
- if (!apiKey) {
84
- apiKey =
85
- (await getSecureKeyAsync(
86
- credentialKey("vellum", "assistant_api_key"),
87
- )) ?? "";
88
- }
89
- if (!assistantId) {
90
- assistantId =
91
- (
92
- await getSecureKeyAsync(
93
- credentialKey("vellum", "platform_assistant_id"),
94
- )
95
- )?.trim() ?? "";
96
- }
97
-
98
- if (!baseUrl || !apiKey) {
99
- const level = _missingPrereqsWarned ? "debug" : "warn";
100
- _missingPrereqsWarned = true;
101
- log[level](
102
- {
103
- hasBaseUrl: !!baseUrl,
104
- hasApiKey: !!apiKey,
105
- hasAssistantId: !!assistantId,
106
- managedProxyEnabled: ctx.enabled,
107
- },
108
- "Platform client prerequisites missing — returning null",
109
- );
228
+ const config = await resolvePlatformClientConfig();
229
+ if (!config) {
110
230
  return null;
111
231
  }
112
-
113
- return new VellumPlatformClient(baseUrl, apiKey, assistantId);
232
+ return new VellumPlatformClient(
233
+ config.baseUrl,
234
+ config.apiKey,
235
+ config.assistantId,
236
+ );
114
237
  }
115
238
 
116
239
  /**
@@ -67,6 +67,17 @@ export interface RunConversationTurnResult {
67
67
  queued?: boolean;
68
68
  }
69
69
 
70
+ /**
71
+ * Metadata stamped on the row that opens a plugin-driven turn. A plugin drives
72
+ * the turn on its own schedule, so the row is machine-initiated even when it
73
+ * lands in an ordinary standard conversation the user also types into: the
74
+ * `automated` marker is what keeps the reply out of the `chat.assistant_reply`
75
+ * push (see `isReplyPushIneligibleUserMessage`) and out of the memory
76
+ * extraction pass, alongside every other machine-authored prompt. It is not an
77
+ * echo-suppression marker, so the row still renders in the transcript.
78
+ */
79
+ const PLUGIN_TURN_MESSAGE_METADATA = { automated: true } as const;
80
+
70
81
  // ---------------------------------------------------------------------------
71
82
  // Content conversion
72
83
  // ---------------------------------------------------------------------------
@@ -210,6 +221,7 @@ export async function runConversationTurn(
210
221
  onEvent,
211
222
  requestId,
212
223
  isInteractive: false,
224
+ metadata: { ...PLUGIN_TURN_MESSAGE_METADATA },
213
225
  });
214
226
  if (enqueueResult.rejected) {
215
227
  throw new Error(
@@ -229,6 +241,7 @@ export async function runConversationTurn(
229
241
  attachments,
230
242
  onEvent,
231
243
  isInteractive: false,
244
+ metadata: { ...PLUGIN_TURN_MESSAGE_METADATA },
232
245
  ...(options.callSite ? { callSite: options.callSite } : {}),
233
246
  });
234
247
 
@@ -67,6 +67,7 @@ describe("getModelProfiles", () => {
67
67
  "balanced",
68
68
  "quality-optimized",
69
69
  "cost-optimized",
70
+ "latency-optimized",
70
71
  ]);
71
72
  });
72
73
 
@@ -85,6 +86,7 @@ describe("getModelProfiles", () => {
85
86
  "balanced",
86
87
  "cost-optimized",
87
88
  "disabled",
89
+ "latency-optimized",
88
90
  "quality-optimized",
89
91
  ]);
90
92
  const disabled = result.find((p) => p.key === "disabled");
@@ -122,6 +124,7 @@ describe("getModelProfiles", () => {
122
124
  "mix",
123
125
  "balanced",
124
126
  "cost-optimized",
127
+ "latency-optimized",
125
128
  "quality-optimized",
126
129
  ]);
127
130
  });
@@ -131,6 +134,7 @@ describe("getModelProfiles", () => {
131
134
  expect(result.map((p) => p.key).sort()).toEqual([
132
135
  "balanced",
133
136
  "cost-optimized",
137
+ "latency-optimized",
134
138
  "quality-optimized",
135
139
  ]);
136
140
  for (const profile of result) {
@@ -150,7 +154,7 @@ describe("getModelProfiles", () => {
150
154
  "Best results with the most capable model",
151
155
  );
152
156
  expect(result.find((p) => p.key === "cost-optimized")?.description).toBe(
153
- "Fastest responses at lower cost",
157
+ "Cheapest responses, for high-volume work",
154
158
  );
155
159
  });
156
160
 
@@ -162,7 +166,7 @@ describe("getModelProfiles", () => {
162
166
  // Tier descriptions are intent-only (no model names - LUM-2881), so the
163
167
  // managed and BYOK columns read the same for this tier.
164
168
  expect(result.find((p) => p.key === "cost-optimized")?.description).toBe(
165
- "Fastest responses at lower cost",
169
+ "Cheapest responses, for high-volume work",
166
170
  );
167
171
  });
168
172
 
@@ -261,4 +261,38 @@ describe("bookmark-crud", () => {
261
261
  /Message ghost not found/,
262
262
  );
263
263
  });
264
+
265
+ test("listBookmarks drives the join from message_bookmarks even with planner stats that predate the table", () => {
266
+ const sqlite = new Database(":memory:");
267
+ sqlite.exec("PRAGMA foreign_keys = ON");
268
+ const captured: string[] = [];
269
+ const db = drizzle(sqlite, {
270
+ schema,
271
+ logger: { logQuery: (query: string) => captured.push(query) },
272
+ });
273
+ const raw = getSqliteFrom(db);
274
+ bootstrapMessageTables(raw);
275
+ migrateMessageBookmarks(db);
276
+ for (let i = 0; i < 30; i++) {
277
+ seedConversationAndMessage(raw, {
278
+ conversationId: `conv-${i}`,
279
+ messageId: `msg-${i}`,
280
+ });
281
+ }
282
+ // ANALYZE writes no sqlite_stat1 rows for an empty table, leaving
283
+ // message_bookmarks without statistics while messages gets them. This is
284
+ // the production state that made the planner scan messages.
285
+ raw.exec("ANALYZE");
286
+
287
+ listBookmarks(db);
288
+ const sql = captured.at(-1);
289
+ expect(sql).toBeDefined();
290
+ const plan = raw.query(`EXPLAIN QUERY PLAN ${sql}`).all() as Array<{
291
+ detail: string;
292
+ }>;
293
+ expect(plan[0]?.detail ?? "").toMatch(/^SCAN message_bookmarks/);
294
+ expect(plan.some((row) => row.detail.includes("SCAN messages"))).toBe(
295
+ false,
296
+ );
297
+ });
264
298
  });
@@ -169,6 +169,18 @@ describe("buildExcerpt", () => {
169
169
  expect(excerpt.startsWith("…")).toBe(true);
170
170
  });
171
171
 
172
+ test("keeps the match near the front of the display excerpt", () => {
173
+ const prefix = "p".repeat(200);
174
+ const excerpt = buildExcerpt(
175
+ JSON.stringify([{ type: "text", text: `${prefix} alpha trailing text` }]),
176
+ "alpha",
177
+ );
178
+
179
+ const alphaIndex = excerpt.indexOf("alpha");
180
+ expect(alphaIndex).toBeGreaterThan(0);
181
+ expect(alphaIndex).toBeLessThanOrEqual(32);
182
+ });
183
+
172
184
  test("whole-query matches also respect tokenizer boundaries", () => {
173
185
  const filler = "party planning notes ".repeat(10);
174
186
  const excerpt = buildExcerpt(
@@ -337,6 +349,16 @@ describe("buildRecallEvidenceExcerpt", () => {
337
349
  );
338
350
  });
339
351
 
352
+ test("keeps the wide leading window for recall evidence", () => {
353
+ const prefix = "p".repeat(200);
354
+ const excerpt = buildRecallEvidenceExcerpt(
355
+ JSON.stringify([{ type: "text", text: `${prefix} alpha trailing text` }]),
356
+ "alpha",
357
+ );
358
+
359
+ expect(excerpt.indexOf("alpha")).toBeGreaterThan(50);
360
+ });
361
+
340
362
  test("keeps the raw fallback for messages with no legible text", () => {
341
363
  const raw = JSON.stringify([
342
364
  { type: "tool_use", id: "toolu_01", name: "bash", input: {} },
@@ -38,6 +38,15 @@ mock.module("../../../../persistence/conversation-crud.js", () => ({
38
38
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
39
39
  }));
40
40
 
41
+ // Stub the user-activity gate probe open. Its real logic is covered by
42
+ // memory-retrospective-accounting.test.ts and
43
+ // memory-retrospective-enqueue.test.ts; against this file's empty stub db it
44
+ // would report no activity and close the gate, masking the memory.enabled
45
+ // gate under test.
46
+ mock.module("../memory-retrospective-accounting.js", () => ({
47
+ hasQualifyingUserMessageAfter: () => true,
48
+ }));
49
+
41
50
  // Stub the qdrant breaker so `enqueueMemoryJob` doesn't trip on it.
42
51
  mock.module(
43
52
  "../../../../persistence/embeddings/qdrant-circuit-breaker.js",
@@ -94,6 +103,8 @@ const stubDb = makeStubDb();
94
103
  mock.module("../../../../persistence/db-connection.js", () => ({
95
104
  getDb: () => stubDb,
96
105
  getMemoryDb: () => stubDb,
106
+ // Truthy handle: consulted only as memory-db's availability gate.
107
+ getMemorySqlite: () => ({}),
97
108
  }));
98
109
 
99
110
  // Now load the real modules under test.