@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -189,6 +189,7 @@ import {
189
189
  RelayConnection,
190
190
  } from "../calls/relay-server.js";
191
191
  import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
192
+ import { upsertContact } from "../contacts/contact-store.js";
192
193
  import {
193
194
  createGuardianBinding,
194
195
  upsertContactChannel,
@@ -289,6 +290,11 @@ function resetTables() {
289
290
  ensuredConvIds = new Set();
290
291
  }
291
292
 
293
+ /** Create a throwaway contact and return its ID, for use as the invite's contactId. */
294
+ function createTargetContact(displayName = "Test Contact"): string {
295
+ return upsertContact({ displayName, role: "contact" }).id;
296
+ }
297
+
292
298
  function addTrustedVoiceContact(phoneNumber: string): void {
293
299
  upsertContactChannel({
294
300
  sourceChannel: "phone",
@@ -2053,6 +2059,7 @@ describe("relay-server", () => {
2053
2059
  const codeHash = hashVoiceCode(code);
2054
2060
  createInvite({
2055
2061
  sourceChannel: "phone",
2062
+ contactId: createTargetContact(),
2056
2063
  maxUses: 1,
2057
2064
  expectedExternalUserId: "+15558887777",
2058
2065
  voiceCodeHash: codeHash,
@@ -2126,6 +2133,7 @@ describe("relay-server", () => {
2126
2133
  const codeHash = hashVoiceCode(code);
2127
2134
  createInvite({
2128
2135
  sourceChannel: "phone",
2136
+ contactId: createTargetContact(),
2129
2137
  maxUses: 1,
2130
2138
  expectedExternalUserId: "+15558886666",
2131
2139
  voiceCodeHash: codeHash,
@@ -4013,6 +4021,7 @@ describe("relay-server", () => {
4013
4021
  const codeHash = hashVoiceCode(code);
4014
4022
  createInvite({
4015
4023
  sourceChannel: "phone",
4024
+ contactId: createTargetContact(),
4016
4025
  maxUses: 1,
4017
4026
  expectedExternalUserId: "+15557776666",
4018
4027
  voiceCodeHash: codeHash,
@@ -4055,7 +4064,7 @@ describe("relay-server", () => {
4055
4064
  .filter((m) => m.type === "text");
4056
4065
  expect(
4057
4066
  textMessages.some((m) =>
4058
- (m.token ?? "").includes("said I can speak with you"),
4067
+ (m.token ?? "").includes("verified that you are Eve"),
4059
4068
  ),
4060
4069
  ).toBe(true);
4061
4070
 
@@ -4079,6 +4088,51 @@ describe("relay-server", () => {
4079
4088
  relay.destroy();
4080
4089
  });
4081
4090
 
4091
+ test("outbound invite prompt uses assistant introduction", async () => {
4092
+ ensureConversation("conv-outbound-invite-origin");
4093
+ ensureConversation("conv-outbound-invite");
4094
+ const session = createCallSession({
4095
+ conversationId: "conv-outbound-invite",
4096
+ provider: "twilio",
4097
+ fromNumber: "+15551111111",
4098
+ toNumber: "+15558887777",
4099
+ callMode: "invite",
4100
+ inviteFriendName: "Grace",
4101
+ inviteGuardianName: "Hank",
4102
+ initiatedFromConversationId: "conv-outbound-invite-origin",
4103
+ });
4104
+
4105
+ mockAssistantName = "Vellum";
4106
+
4107
+ const { ws, relay } = createMockWs(session.id);
4108
+
4109
+ await relay.handleMessage(
4110
+ JSON.stringify({
4111
+ type: "setup",
4112
+ callSid: "CA_outbound_invite",
4113
+ from: "+15551111111",
4114
+ to: "+15558887777",
4115
+ }),
4116
+ );
4117
+
4118
+ // Should be in verification-pending for invite redemption
4119
+ expect(relay.getConnectionState()).toBe("verification_pending");
4120
+
4121
+ // The prompt should use the outbound assistant introduction
4122
+ const textMessages = ws.sentMessages
4123
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
4124
+ .filter((m) => m.type === "text");
4125
+ expect(
4126
+ textMessages.some(
4127
+ (m) =>
4128
+ (m.token ?? "").includes("this is Vellum") &&
4129
+ (m.token ?? "").includes("Hank's assistant"),
4130
+ ),
4131
+ ).toBe(true);
4132
+
4133
+ relay.destroy();
4134
+ });
4135
+
4082
4136
  // ── resolveGuardianLabel resolution priority ─────────────────────────
4083
4137
 
4084
4138
  test("guardian label: USER.md name takes precedence over Contact.displayName", async () => {
@@ -61,6 +61,11 @@ const ctx: ToolContext = {
61
61
  trustClass: "guardian",
62
62
  };
63
63
 
64
+ const trustedCtx: ToolContext = {
65
+ ...ctx,
66
+ trustClass: "trusted_contact",
67
+ };
68
+
64
69
  // ── schedule_create ─────────────────────────────────────────────────
65
70
 
66
71
  describe("schedule_create tool", () => {
@@ -169,6 +174,20 @@ describe("schedule_create tool", () => {
169
174
  expect(result.isError).toBe(true);
170
175
  expect(result.content).toContain("Invalid cron expression");
171
176
  });
177
+
178
+ test("rejects non-guardian actors", async () => {
179
+ const result = await executeScheduleCreate(
180
+ {
181
+ name: "Blocked schedule",
182
+ expression: "0 9 * * *",
183
+ message: "test",
184
+ },
185
+ trustedCtx,
186
+ );
187
+
188
+ expect(result.isError).toBe(true);
189
+ expect(result.content).toContain("restricted to guardian actors");
190
+ });
172
191
  });
173
192
 
174
193
  // ── schedule_create with fire_at (one-shot) ──────────────────────────
@@ -680,6 +699,19 @@ describe("schedule_update tool", () => {
680
699
  expect(result.isError).toBe(true);
681
700
  expect(result.content).toContain("Invalid cron expression");
682
701
  });
702
+
703
+ test("rejects non-guardian actors", async () => {
704
+ const result = await executeScheduleUpdate(
705
+ {
706
+ job_id: "nonexistent-id",
707
+ message: "injected",
708
+ },
709
+ trustedCtx,
710
+ );
711
+
712
+ expect(result.isError).toBe(true);
713
+ expect(result.content).toContain("restricted to guardian actors");
714
+ });
683
715
  });
684
716
 
685
717
  // ── schedule_update with mode and routing ────────────────────────────
@@ -84,7 +84,7 @@ describe("issueLeafCert", () => {
84
84
 
85
85
  expect(certA.cert).not.toBe(certB.cert);
86
86
  expect(certA.key).not.toBe(certB.key);
87
- });
87
+ }, 15_000);
88
88
  });
89
89
 
90
90
  describe("getCAPath", () => {
@@ -43,6 +43,7 @@ mock.module("../security/secure-keys.js", () => {
43
43
  };
44
44
  return {
45
45
  getSecureKey: (key: string) => storedKeys.get(key) ?? null,
46
+ getSecureKeyAsync: async (key: string) => storedKeys.get(key) ?? undefined,
46
47
  setSecureKey: syncSet,
47
48
  setSecureKeyAsync: async (key: string, value: string) =>
48
49
  syncSet(key, value),
@@ -0,0 +1,183 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { credentialKey } from "../security/credential-key.js";
4
+
5
+ let lastGeminiConstructorOpts: Record<string, unknown> | null = null;
6
+ let secureKeyStore: Record<string, string | undefined> = {};
7
+ const metadataUpserts: Array<{ service: string; field: string }> = [];
8
+ const metadataDeletes: Array<{ service: string; field: string }> = [];
9
+
10
+ const PLATFORM_BASE_URL = "https://platform.example.com";
11
+ const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
12
+ const MANAGED_PROVIDERS = [
13
+ "anthropic",
14
+ "openai",
15
+ "gemini",
16
+ "fireworks",
17
+ "openrouter",
18
+ ] as const;
19
+
20
+ const mockConfig = {
21
+ apiKeys: {},
22
+ provider: "anthropic",
23
+ model: "test-model",
24
+ };
25
+
26
+ mock.module("@google/genai", () => ({
27
+ GoogleGenAI: class MockGoogleGenAI {
28
+ constructor(opts: Record<string, unknown>) {
29
+ lastGeminiConstructorOpts = opts;
30
+ }
31
+ models = {
32
+ generateContentStream: async () => ({
33
+ [Symbol.asyncIterator]: async function* () {
34
+ /* no chunks */
35
+ },
36
+ }),
37
+ };
38
+ },
39
+ ApiError: class FakeApiError extends Error {
40
+ status: number;
41
+ constructor(status: number, message: string) {
42
+ super(message);
43
+ this.status = status;
44
+ this.name = "ApiError";
45
+ }
46
+ },
47
+ }));
48
+
49
+ mock.module("../config/env.js", () => ({
50
+ getPlatformBaseUrl: () => PLATFORM_BASE_URL,
51
+ }));
52
+
53
+ mock.module("../config/loader.js", () => ({
54
+ API_KEY_PROVIDERS: [
55
+ "anthropic",
56
+ "openai",
57
+ "gemini",
58
+ "fireworks",
59
+ "openrouter",
60
+ ],
61
+ getConfig: () => mockConfig,
62
+ invalidateConfigCache: () => {},
63
+ }));
64
+
65
+ mock.module("../logfire.js", () => ({
66
+ wrapWithLogfire: (provider: unknown) => provider,
67
+ }));
68
+
69
+ mock.module("../security/secure-keys.js", () => ({
70
+ getSecureKey: (key: string) => secureKeyStore[key],
71
+ getSecureKeyAsync: async (key: string) => secureKeyStore[key],
72
+ setSecureKeyAsync: async (key: string, value: string) => {
73
+ secureKeyStore[key] = value;
74
+ return true;
75
+ },
76
+ deleteSecureKeyAsync: async (key: string) => {
77
+ delete secureKeyStore[key];
78
+ return "deleted";
79
+ },
80
+ }));
81
+
82
+ mock.module("../tools/credentials/metadata-store.js", () => ({
83
+ assertMetadataWritable: () => {},
84
+ upsertCredentialMetadata: (service: string, field: string) => {
85
+ metadataUpserts.push({ service, field });
86
+ },
87
+ deleteCredentialMetadata: (service: string, field: string) => {
88
+ metadataDeletes.push({ service, field });
89
+ },
90
+ }));
91
+
92
+ mock.module("../util/logger.js", () => ({
93
+ getLogger: () =>
94
+ new Proxy({} as Record<string, unknown>, {
95
+ get: () => () => {},
96
+ }),
97
+ }));
98
+
99
+ import {
100
+ getProviderRoutingSource,
101
+ initializeProviders,
102
+ listProviders,
103
+ } from "../providers/registry.js";
104
+ import {
105
+ handleAddSecret,
106
+ handleDeleteSecret,
107
+ } from "../runtime/routes/secret-routes.js";
108
+
109
+ function makeAddCredentialRequest(name: string, value: string): Request {
110
+ return new Request("http://localhost/v1/secrets", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({
114
+ type: "credential",
115
+ name,
116
+ value,
117
+ }),
118
+ });
119
+ }
120
+
121
+ function makeDeleteCredentialRequest(name: string): Request {
122
+ return new Request("http://localhost/v1/secrets", {
123
+ method: "DELETE",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({
126
+ type: "credential",
127
+ name,
128
+ }),
129
+ });
130
+ }
131
+
132
+ describe("secret routes managed proxy registry sync", () => {
133
+ beforeEach(() => {
134
+ secureKeyStore = {};
135
+ metadataUpserts.length = 0;
136
+ metadataDeletes.length = 0;
137
+ lastGeminiConstructorOpts = null;
138
+ initializeProviders(mockConfig);
139
+ });
140
+
141
+ test("adding vellum:assistant_api_key bootstraps managed providers immediately", async () => {
142
+ expect(listProviders()).toEqual([]);
143
+
144
+ const res = await handleAddSecret(
145
+ makeAddCredentialRequest("vellum:assistant_api_key", "ast-managed-key"),
146
+ );
147
+
148
+ expect(res.status).toBe(201);
149
+ expect(secureKeyStore[ASSISTANT_API_KEY_PATH]).toBe("ast-managed-key");
150
+ expect(metadataUpserts).toEqual([
151
+ { service: "vellum", field: "assistant_api_key" },
152
+ ]);
153
+
154
+ const providers = listProviders();
155
+ expect(providers).toHaveLength(MANAGED_PROVIDERS.length);
156
+ for (const provider of MANAGED_PROVIDERS) {
157
+ expect(providers).toContain(provider);
158
+ expect(getProviderRoutingSource(provider)).toBe("managed-proxy");
159
+ }
160
+ expect(lastGeminiConstructorOpts).toBeDefined();
161
+ });
162
+
163
+ test("deleting vellum:assistant_api_key clears managed providers immediately", async () => {
164
+ secureKeyStore[ASSISTANT_API_KEY_PATH] = "ast-managed-key";
165
+ initializeProviders(mockConfig);
166
+
167
+ for (const provider of MANAGED_PROVIDERS) {
168
+ expect(listProviders()).toContain(provider);
169
+ expect(getProviderRoutingSource(provider)).toBe("managed-proxy");
170
+ }
171
+
172
+ const res = await handleDeleteSecret(
173
+ makeDeleteCredentialRequest("vellum:assistant_api_key"),
174
+ );
175
+
176
+ expect(res.status).toBe(200);
177
+ expect(secureKeyStore[ASSISTANT_API_KEY_PATH]).toBeUndefined();
178
+ expect(metadataDeletes).toEqual([
179
+ { service: "vellum", field: "assistant_api_key" },
180
+ ]);
181
+ expect(listProviders()).toEqual([]);
182
+ });
183
+ });
@@ -33,12 +33,14 @@ let mockBrokerStore: Map<string, string> = new Map();
33
33
  let mockBrokerGetError = false;
34
34
  let mockBrokerSetError = false;
35
35
  let mockBrokerDelError = false;
36
+ let mockBrokerGetCalled = false;
36
37
 
37
38
  mock.module("../security/keychain-broker-client.js", () => ({
38
39
  createBrokerClient: () => ({
39
40
  isAvailable: () => mockBrokerAvailable,
40
41
  ping: async () => (mockBrokerAvailable ? { pong: true } : null),
41
42
  get: async (account: string) => {
43
+ mockBrokerGetCalled = true;
42
44
  // null = broker error (fall back to encrypted store)
43
45
  if (mockBrokerGetError) return null;
44
46
  const value = mockBrokerStore.get(account);
@@ -46,9 +48,14 @@ mock.module("../security/keychain-broker-client.js", () => ({
46
48
  return { found: false };
47
49
  },
48
50
  set: async (account: string, value: string) => {
49
- if (mockBrokerSetError) return false;
51
+ if (mockBrokerSetError)
52
+ return {
53
+ status: "rejected" as const,
54
+ code: "KEYCHAIN_ERROR",
55
+ message: "mock error",
56
+ };
50
57
  mockBrokerStore.set(account, value);
51
- return true;
58
+ return { status: "ok" as const };
52
59
  },
53
60
  del: async (account: string) => {
54
61
  if (mockBrokerDelError) return false;
@@ -95,6 +102,7 @@ describe("secure-keys", () => {
95
102
  mockBrokerGetError = false;
96
103
  mockBrokerSetError = false;
97
104
  mockBrokerDelError = false;
105
+ mockBrokerGetCalled = false;
98
106
 
99
107
  if (existsSync(TEST_DIR)) {
100
108
  rmSync(TEST_DIR, { recursive: true });
@@ -202,20 +210,22 @@ describe("secure-keys", () => {
202
210
  // Async variants — broker available path
203
211
  // -----------------------------------------------------------------------
204
212
  describe("async variants with broker available", () => {
205
- test("getSecureKeyAsync returns broker value when available", async () => {
213
+ test("getSecureKeyAsync returns encrypted store value when both stores have key", async () => {
206
214
  mockBrokerAvailable = true;
207
215
  mockBrokerStore.set("api-key", "broker-value");
208
216
  setSecureKey("api-key", "encrypted-value");
209
- expect(await getSecureKeyAsync("api-key")).toBe("broker-value");
217
+ // Encrypted store is checked first — broker is never called
218
+ expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
219
+ expect(mockBrokerGetCalled).toBe(false);
210
220
  });
211
221
 
212
- test("getSecureKeyAsync falls back to encrypted store when broker reports not-found", async () => {
222
+ test("getSecureKeyAsync returns encrypted store value without calling broker", async () => {
213
223
  mockBrokerAvailable = true;
214
- // Broker has nothing for this key — returns { found: false }.
215
- // Keys may exist only in the encrypted store (written while broker
216
- // was unavailable or via sync setSecureKey), so we must fall back.
224
+ // Only encrypted store has the key — broker has nothing.
225
+ // Encrypted store is checked first, so broker.get() is never called.
217
226
  setSecureKey("api-key", "encrypted-value");
218
227
  expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
228
+ expect(mockBrokerGetCalled).toBe(false);
219
229
  });
220
230
 
221
231
  test("getSecureKeyAsync returns undefined when neither broker nor encrypted store has key", async () => {
@@ -224,11 +234,14 @@ describe("secure-keys", () => {
224
234
  expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
225
235
  });
226
236
 
227
- test("getSecureKeyAsync falls back to encrypted store on broker error", async () => {
237
+ test("getSecureKeyAsync returns encrypted store value even when broker would error", async () => {
228
238
  mockBrokerAvailable = true;
229
239
  mockBrokerGetError = true;
240
+ // Encrypted store hit short-circuits — broker is never called, so
241
+ // the broker error flag is irrelevant.
230
242
  setSecureKey("api-key", "encrypted-value");
231
243
  expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
244
+ expect(mockBrokerGetCalled).toBe(false);
232
245
  });
233
246
 
234
247
  test("setSecureKeyAsync writes to broker and encrypted store", async () => {
@@ -303,10 +316,56 @@ describe("secure-keys", () => {
303
316
  });
304
317
 
305
318
  // -----------------------------------------------------------------------
306
- // Stale-value prevention — broker-first reads after credential updates
319
+ // Encrypted-store-first read order
320
+ // -----------------------------------------------------------------------
321
+ describe("encrypted-store-first read order", () => {
322
+ test("returns value from encrypted store without calling broker", async () => {
323
+ mockBrokerAvailable = true;
324
+ setSecureKey("test-account", "test-secret");
325
+ mockBrokerStore.set("test-account", "broker-secret");
326
+
327
+ const result = await getSecureKeyAsync("test-account");
328
+ expect(result).toBe("test-secret");
329
+ // Broker should never have been called — encrypted store hit
330
+ // short-circuits the lookup.
331
+ expect(mockBrokerGetCalled).toBe(false);
332
+ });
333
+
334
+ test("falls back to broker when encrypted store returns undefined", async () => {
335
+ mockBrokerAvailable = true;
336
+ // Encrypted store has nothing for this key
337
+ mockBrokerStore.set("test-account", "broker-secret");
338
+
339
+ const result = await getSecureKeyAsync("test-account");
340
+ expect(result).toBe("broker-secret");
341
+ // Broker should have been called as fallback
342
+ expect(mockBrokerGetCalled).toBe(true);
343
+ });
344
+
345
+ test("returns undefined when neither store has the key", async () => {
346
+ mockBrokerAvailable = true;
347
+ // Neither encrypted store nor broker has the key
348
+
349
+ const result = await getSecureKeyAsync("test-account");
350
+ expect(result).toBeUndefined();
351
+ });
352
+
353
+ test("returns undefined without broker call when broker unavailable and encrypted store empty", async () => {
354
+ // Broker is unavailable (default state), encrypted store is empty
355
+ mockBrokerAvailable = false;
356
+
357
+ const result = await getSecureKeyAsync("test-account");
358
+ expect(result).toBeUndefined();
359
+ // Broker.get() should not have been called since broker is unavailable
360
+ expect(mockBrokerGetCalled).toBe(false);
361
+ });
362
+ });
363
+
364
+ // -----------------------------------------------------------------------
365
+ // Stale-value prevention — encrypted-store-first reads avoid stale broker data
307
366
  // -----------------------------------------------------------------------
308
367
  describe("stale-value prevention", () => {
309
- test("setSecureKeyAsync updates broker so broker-first read returns new value", async () => {
368
+ test("setSecureKeyAsync updates both stores so encrypted-store-first read returns new value", async () => {
310
369
  mockBrokerAvailable = true;
311
370
  // Simulate broker holding an old value
312
371
  mockBrokerStore.set("api-key", "old-broker-value");
@@ -316,7 +375,7 @@ describe("secure-keys", () => {
316
375
  const ok = await setSecureKeyAsync("api-key", "new-value");
317
376
  expect(ok).toBe(true);
318
377
 
319
- // Broker-first read should return the new value, not stale old value
378
+ // Encrypted-store-first read returns the new value
320
379
  const value = await getSecureKeyAsync("api-key");
321
380
  expect(value).toBe("new-value");
322
381
  // Both stores should agree
@@ -324,7 +383,7 @@ describe("secure-keys", () => {
324
383
  expect(getSecureKey("api-key")).toBe("new-value");
325
384
  });
326
385
 
327
- test("deleteSecureKeyAsync removes from broker so broker-first read falls through", async () => {
386
+ test("deleteSecureKeyAsync removes from both stores so read returns undefined", async () => {
328
387
  mockBrokerAvailable = true;
329
388
  mockBrokerStore.set("api-key", "old-broker-value");
330
389
  setSecureKey("api-key", "old-encrypted-value");
@@ -333,22 +392,23 @@ describe("secure-keys", () => {
333
392
  const result = await deleteSecureKeyAsync("api-key");
334
393
  expect(result).toBe("deleted");
335
394
 
336
- // Broker-first read should not find the key in either store
395
+ // Neither store has the key returns undefined
337
396
  const value = await getSecureKeyAsync("api-key");
338
397
  expect(value).toBeUndefined();
339
398
  });
340
399
 
341
- test("sync setSecureKey does NOT update broker stale read demonstrates the problem", async () => {
400
+ test("sync setSecureKey updates encrypted storeencrypted-store-first read returns fresh value", async () => {
342
401
  mockBrokerAvailable = true;
343
402
  mockBrokerStore.set("api-key", "old-broker-value");
344
403
 
345
404
  // Sync write only updates encrypted store, NOT broker
346
405
  setSecureKey("api-key", "new-encrypted-value");
347
406
 
348
- // Broker-first read still returns the stale broker value
407
+ // Encrypted-store-first read returns the fresh encrypted value,
408
+ // bypassing the stale broker entirely.
349
409
  const value = await getSecureKeyAsync("api-key");
350
- expect(value).toBe("old-broker-value");
351
- // This is the exact bug that async migration fixes
410
+ expect(value).toBe("new-encrypted-value");
411
+ expect(mockBrokerGetCalled).toBe(false);
352
412
  });
353
413
 
354
414
  test("setSecureKeyAsync failure leaves both stores unchanged", async () => {
@@ -118,6 +118,7 @@ function makeCompletingSession(): Session {
118
118
  setHostBashProxy: () => {},
119
119
  setHostFileProxy: () => {},
120
120
  setHostCuProxy: () => {},
121
+ addPreactivatedSkillId: () => {},
121
122
  hasAnyPendingConfirmation: () => false,
122
123
  hasPendingConfirmation: () => false,
123
124
  denyAllPendingConfirmations: () => {},
@@ -175,6 +176,7 @@ function makeHangingSession(): Session {
175
176
  setHostBashProxy: () => {},
176
177
  setHostFileProxy: () => {},
177
178
  setHostCuProxy: () => {},
179
+ addPreactivatedSkillId: () => {},
178
180
  hasAnyPendingConfirmation: () => false,
179
181
  hasPendingConfirmation: () => false,
180
182
  denyAllPendingConfirmations: () => {},
@@ -260,6 +262,7 @@ function makePendingApprovalSession(
260
262
  setHostBashProxy: () => {},
261
263
  setHostFileProxy: () => {},
262
264
  setHostCuProxy: () => {},
265
+ addPreactivatedSkillId: () => {},
263
266
  hasAnyPendingConfirmation: () => pending.size > 0,
264
267
  hasPendingConfirmation: (candidateRequestId: string) =>
265
268
  pending.has(candidateRequestId),
@@ -78,7 +78,7 @@ describe("renderHistoryContent", () => {
78
78
  );
79
79
  });
80
80
 
81
- test("renders image attachments in history output", () => {
81
+ test("skips image attachment placeholder text (images sent as separate attachments)", () => {
82
82
  const output = renderHistoryContent([
83
83
  {
84
84
  type: "image",
@@ -90,7 +90,7 @@ describe("renderHistoryContent", () => {
90
90
  },
91
91
  ]);
92
92
 
93
- expect(output.text).toContain("[Image attachment] image/png, 5 B");
93
+ expect(output.text).toBe("");
94
94
  });
95
95
 
96
96
  test("appends attachment lines after text content", () => {
@@ -75,18 +75,6 @@ mock.module("../security/secret-allowlist.js", () => ({
75
75
  resetAllowlist: () => {},
76
76
  }));
77
77
 
78
- mock.module("../memory/admin.js", () => ({
79
- getMemoryConflictAndCleanupStats: () => ({
80
- conflicts: { pending: 0, resolved: 0, oldestPendingAgeMs: null },
81
- cleanup: {
82
- resolvedBacklog: 0,
83
- supersededBacklog: 0,
84
- resolvedCompleted24h: 0,
85
- supersededCompleted24h: 0,
86
- },
87
- }),
88
- }));
89
-
90
78
  // Track all messages persisted to DB
91
79
  let persistedMessages: Array<{ role: string; content: string }> = [];
92
80
 
@@ -128,13 +116,12 @@ mock.module("../memory/retriever.js", () => ({
128
116
  enabled: false,
129
117
  degraded: false,
130
118
  injectedText: "",
131
- lexicalHits: 0,
119
+
132
120
  semanticHits: 0,
133
121
  recencyHits: 0,
134
122
  injectedTokens: 0,
135
123
  latencyMs: 0,
136
124
  }),
137
- injectMemoryRecallIntoUserMessage: (msg: Message) => msg,
138
125
  stripMemoryRecallMessages: (msgs: Message[]) => msgs,
139
126
  }));
140
127