@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -34,6 +34,8 @@ mock.module("../security/secure-keys.js", () => ({
34
34
  deleteSecureKeyAsync: mockDeleteSecureKeyAsync,
35
35
  setSecureKeyAsync: mockSetSecureKeyAsync,
36
36
  getSecureKey: (account: string) => secureKeyValues.get(account),
37
+ getSecureKeyAsync: (account: string) =>
38
+ Promise.resolve(secureKeyValues.get(account)),
37
39
  }));
38
40
 
39
41
  import { initializeDb, resetDb, resetTestTables } from "../memory/db.js";
@@ -179,6 +181,35 @@ describe("provider operations", () => {
179
181
  // createdAt should be preserved from the original insert
180
182
  expect(row!.createdAt).toBe(originalCreatedAt);
181
183
  });
184
+
185
+ test("persists pingUrl when provided", () => {
186
+ seedProviders([
187
+ {
188
+ providerKey: "github",
189
+ authUrl: "https://github.com/authorize",
190
+ tokenUrl: "https://github.com/token",
191
+ defaultScopes: ["repo"],
192
+ scopePolicy: {},
193
+ pingUrl: "https://api.github.com/user",
194
+ },
195
+ ]);
196
+ const row = getProvider("github");
197
+ expect(row!.pingUrl).toBe("https://api.github.com/user");
198
+ });
199
+
200
+ test("pingUrl defaults to null when omitted", () => {
201
+ seedProviders([
202
+ {
203
+ providerKey: "github",
204
+ authUrl: "https://github.com/authorize",
205
+ tokenUrl: "https://github.com/token",
206
+ defaultScopes: ["repo"],
207
+ scopePolicy: {},
208
+ },
209
+ ]);
210
+ const row = getProvider("github");
211
+ expect(row!.pingUrl).toBeNull();
212
+ });
182
213
  });
183
214
 
184
215
  describe("getProvider", () => {
@@ -279,13 +310,18 @@ describe("app operations", () => {
279
310
 
280
311
  test("stores clientSecret in secure storage on new app creation", async () => {
281
312
  seedTestProvider("github");
282
- const app = await upsertApp("github", "client-abc", "my-secret");
313
+ const app = await upsertApp("github", "client-abc", {
314
+ clientSecretValue: "my-secret",
315
+ });
283
316
 
284
317
  expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
285
318
  expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
286
319
  `oauth_app/${app.id}/client_secret`,
287
320
  "my-secret",
288
321
  );
322
+ expect(app.clientSecretCredentialPath).toBe(
323
+ `oauth_app/${app.id}/client_secret`,
324
+ );
289
325
  });
290
326
 
291
327
  test("stores clientSecret in secure storage when upserting an existing app", async () => {
@@ -293,11 +329,13 @@ describe("app operations", () => {
293
329
  const first = await upsertApp("github", "client-abc");
294
330
  mockSetSecureKeyAsync.mockClear();
295
331
 
296
- await upsertApp("github", "client-abc", "updated-secret");
332
+ await upsertApp("github", "client-abc", {
333
+ clientSecretValue: "updated-secret",
334
+ });
297
335
 
298
336
  expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
299
337
  expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
300
- `oauth_app/${first.id}/client_secret`,
338
+ first.clientSecretCredentialPath,
301
339
  "updated-secret",
302
340
  );
303
341
  });
@@ -307,9 +345,70 @@ describe("app operations", () => {
307
345
  mockSetSecureKeyAsync.mockResolvedValueOnce(false);
308
346
 
309
347
  await expect(
310
- upsertApp("github", "client-abc", "bad-secret"),
348
+ upsertApp("github", "client-abc", { clientSecretValue: "bad-secret" }),
311
349
  ).rejects.toThrow("Failed to store client_secret in secure storage");
312
350
  });
351
+
352
+ test("accepts clientSecretCredentialPath and verifies existence", async () => {
353
+ seedTestProvider("github");
354
+ secureKeyValues.set("custom/path", "stored-secret");
355
+
356
+ const app = await upsertApp("github", "client-abc", {
357
+ clientSecretCredentialPath: "custom/path",
358
+ });
359
+
360
+ expect(app.clientSecretCredentialPath).toBe("custom/path");
361
+ // Should not have called setSecureKeyAsync since we only provided a path
362
+ expect(mockSetSecureKeyAsync).not.toHaveBeenCalled();
363
+ });
364
+
365
+ test("throws when clientSecretCredentialPath points to nonexistent secret", async () => {
366
+ seedTestProvider("github");
367
+
368
+ await expect(
369
+ upsertApp("github", "client-abc", {
370
+ clientSecretCredentialPath: "nonexistent/path",
371
+ }),
372
+ ).rejects.toThrow("No secret found at credential path: nonexistent/path");
373
+ });
374
+
375
+ test("throws when both clientSecretValue and clientSecretCredentialPath are provided", async () => {
376
+ seedTestProvider("github");
377
+
378
+ await expect(
379
+ upsertApp("github", "client-abc", {
380
+ clientSecretValue: "my-secret",
381
+ clientSecretCredentialPath: "custom/path",
382
+ }),
383
+ ).rejects.toThrow(
384
+ "Cannot provide both clientSecretValue and clientSecretCredentialPath",
385
+ );
386
+ });
387
+
388
+ test("records default clientSecretCredentialPath when neither value nor path is provided", async () => {
389
+ seedTestProvider("github");
390
+ const app = await upsertApp("github", "client-abc");
391
+
392
+ expect(app.clientSecretCredentialPath).toBe(
393
+ `oauth_app/${app.id}/client_secret`,
394
+ );
395
+ });
396
+
397
+ test("updates clientSecretCredentialPath on existing row when path is provided", async () => {
398
+ seedTestProvider("github");
399
+ const first = await upsertApp("github", "client-abc");
400
+ expect(first.clientSecretCredentialPath).toBe(
401
+ `oauth_app/${first.id}/client_secret`,
402
+ );
403
+
404
+ secureKeyValues.set("new/custom/path", "stored-secret");
405
+ const updated = await upsertApp("github", "client-abc", {
406
+ clientSecretCredentialPath: "new/custom/path",
407
+ });
408
+
409
+ expect(updated.id).toBe(first.id);
410
+ expect(updated.clientSecretCredentialPath).toBe("new/custom/path");
411
+ });
313
412
  });
314
413
 
315
414
  describe("getApp", () => {
@@ -353,14 +452,29 @@ describe("app operations", () => {
353
452
  expect(getApp(app.id)).toBeUndefined();
354
453
  });
355
454
 
356
- test("cleans up client_secret from secure storage", async () => {
455
+ test("cleans up client_secret from secure storage using stored path", async () => {
357
456
  const app = await createTestApp("github", "client-1");
358
457
  mockDeleteSecureKeyAsync.mockClear();
359
458
 
360
459
  await deleteApp(app.id);
361
460
 
362
461
  expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
363
- `oauth_app/${app.id}/client_secret`,
462
+ app.clientSecretCredentialPath,
463
+ );
464
+ });
465
+
466
+ test("uses custom clientSecretCredentialPath when deleting", async () => {
467
+ seedTestProvider("github");
468
+ secureKeyValues.set("custom/secret/path", "the-secret");
469
+ const app = await upsertApp("github", "client-1", {
470
+ clientSecretCredentialPath: "custom/secret/path",
471
+ });
472
+ mockDeleteSecureKeyAsync.mockClear();
473
+
474
+ await deleteApp(app.id);
475
+
476
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
477
+ "custom/secret/path",
364
478
  );
365
479
  });
366
480
 
@@ -21,12 +21,14 @@ interface MockCallLog {
21
21
 
22
22
  let mockCollectionExists: boolean;
23
23
  let mockCollectionSize: number;
24
+ let mockUseNamedVectors: boolean;
24
25
  let mockSentinelPayload: Record<string, unknown> | null;
25
26
  let callLog: MockCallLog;
26
27
 
27
28
  function resetMockState() {
28
29
  mockCollectionExists = false;
29
30
  mockCollectionSize = 384;
31
+ mockUseNamedVectors = false;
30
32
  mockSentinelPayload = null;
31
33
  callLog = {
32
34
  collectionExists: 0,
@@ -51,7 +53,9 @@ mock.module("@qdrant/js-client-rest", () => ({
51
53
  return {
52
54
  config: {
53
55
  params: {
54
- vectors: { size: mockCollectionSize },
56
+ vectors: mockUseNamedVectors
57
+ ? { dense: { size: mockCollectionSize } }
58
+ : { size: mockCollectionSize },
55
59
  },
56
60
  },
57
61
  };
@@ -77,7 +81,12 @@ mock.module("@qdrant/js-client-rest", () => ({
77
81
  mockSentinelPayload &&
78
82
  opts.ids.includes("00000000-0000-0000-0000-000000000000")
79
83
  ) {
80
- return [{ id: "00000000-0000-0000-0000-000000000000", payload: mockSentinelPayload }];
84
+ return [
85
+ {
86
+ id: "00000000-0000-0000-0000-000000000000",
87
+ payload: mockSentinelPayload,
88
+ },
89
+ ];
81
90
  }
82
91
  return [];
83
92
  }
@@ -97,6 +106,7 @@ beforeEach(() => {
97
106
  describe("Qdrant collection migration", () => {
98
107
  test("deletes and recreates collection on dimension mismatch", async () => {
99
108
  mockCollectionExists = true;
109
+ mockUseNamedVectors = true;
100
110
  mockCollectionSize = 384; // Current collection has 384-dim vectors
101
111
 
102
112
  const client = new VellumQdrantClient({
@@ -108,14 +118,16 @@ describe("Qdrant collection migration", () => {
108
118
  embeddingModel: "gemini:gemini-embedding-2-preview",
109
119
  });
110
120
 
111
- await client.ensureCollection();
121
+ const result = await client.ensureCollection();
112
122
 
113
123
  expect(callLog.deleteCollection).toBe(1);
114
124
  expect(callLog.createCollection).toBe(1);
125
+ expect(result.migrated).toBe(true);
115
126
  });
116
127
 
117
128
  test("deletes and recreates collection on model-only mismatch", async () => {
118
129
  mockCollectionExists = true;
130
+ mockUseNamedVectors = true;
119
131
  mockCollectionSize = 768; // Same dimension
120
132
  mockSentinelPayload = {
121
133
  _meta: true,
@@ -131,16 +143,18 @@ describe("Qdrant collection migration", () => {
131
143
  embeddingModel: "gemini:gemini-embedding-2-preview", // New model
132
144
  });
133
145
 
134
- await client.ensureCollection();
146
+ const result = await client.ensureCollection();
135
147
 
136
148
  expect(callLog.deleteCollection).toBe(1);
137
149
  expect(callLog.createCollection).toBe(1);
138
150
  // Sentinel should be written for the new model
139
151
  expect(callLog.upsert).toBe(1);
152
+ expect(result.migrated).toBe(true);
140
153
  });
141
154
 
142
155
  test("leaves collection untouched when dimensions and model match", async () => {
143
156
  mockCollectionExists = true;
157
+ mockUseNamedVectors = true;
144
158
  mockCollectionSize = 768;
145
159
  mockSentinelPayload = {
146
160
  _meta: true,
@@ -156,14 +170,16 @@ describe("Qdrant collection migration", () => {
156
170
  embeddingModel: "gemini:gemini-embedding-2-preview",
157
171
  });
158
172
 
159
- await client.ensureCollection();
173
+ const result = await client.ensureCollection();
160
174
 
161
175
  expect(callLog.deleteCollection).toBe(0);
162
176
  expect(callLog.createCollection).toBe(0);
177
+ expect(result.migrated).toBe(false);
163
178
  });
164
179
 
165
180
  test("does not rebuild pre-existing collection without sentinel (graceful upgrade)", async () => {
166
181
  mockCollectionExists = true;
182
+ mockUseNamedVectors = true;
167
183
  mockCollectionSize = 768;
168
184
  mockSentinelPayload = null; // No sentinel — pre-existing collection
169
185
 
@@ -176,11 +192,12 @@ describe("Qdrant collection migration", () => {
176
192
  embeddingModel: "gemini:gemini-embedding-2-preview",
177
193
  });
178
194
 
179
- await client.ensureCollection();
195
+ const result = await client.ensureCollection();
180
196
 
181
197
  // No sentinel found → no model mismatch → collection kept
182
198
  expect(callLog.deleteCollection).toBe(0);
183
199
  expect(callLog.createCollection).toBe(0);
200
+ expect(result.migrated).toBe(false);
184
201
  });
185
202
 
186
203
  test("writes sentinel point when creating a new collection", async () => {
@@ -195,11 +212,37 @@ describe("Qdrant collection migration", () => {
195
212
  embeddingModel: "gemini:gemini-embedding-2-preview",
196
213
  });
197
214
 
198
- await client.ensureCollection();
215
+ const result = await client.ensureCollection();
199
216
 
200
217
  expect(callLog.createCollection).toBe(1);
201
218
  // Sentinel upsert should be called
202
219
  expect(callLog.upsert).toBe(1);
220
+ // Fresh collection, not a migration
221
+ expect(result.migrated).toBe(false);
222
+ });
223
+
224
+ test("deletes and recreates collection when migrating from unnamed to named vectors", async () => {
225
+ mockCollectionExists = true;
226
+ mockUseNamedVectors = false; // Legacy unnamed vectors
227
+ mockCollectionSize = 768;
228
+
229
+ const client = new VellumQdrantClient({
230
+ url: "http://localhost:6333",
231
+ collection: "memory",
232
+ vectorSize: 768, // Same dimension
233
+ onDisk: false,
234
+ quantization: "none",
235
+ embeddingModel: "gemini:gemini-embedding-2-preview",
236
+ });
237
+
238
+ const result = await client.ensureCollection();
239
+
240
+ // Unnamed vectors should trigger delete + recreate with named vectors
241
+ expect(callLog.deleteCollection).toBe(1);
242
+ expect(callLog.createCollection).toBe(1);
243
+ // Sentinel should be written for the new collection
244
+ expect(callLog.upsert).toBe(1);
245
+ expect(result.migrated).toBe(true);
203
246
  });
204
247
 
205
248
  test("does not write sentinel when embeddingModel is not provided", async () => {
@@ -214,10 +257,12 @@ describe("Qdrant collection migration", () => {
214
257
  // No embeddingModel
215
258
  });
216
259
 
217
- await client.ensureCollection();
260
+ const result = await client.ensureCollection();
218
261
 
219
262
  expect(callLog.createCollection).toBe(1);
220
263
  // No sentinel should be written
221
264
  expect(callLog.upsert).toBe(0);
265
+ // Fresh collection, not a migration
266
+ expect(result.migrated).toBe(false);
222
267
  });
223
268
  });
@@ -524,6 +524,5 @@ describe("computer-use registration split", () => {
524
524
  expect(registered.every((t) => t.name.startsWith("computer_use_"))).toBe(
525
525
  true,
526
526
  );
527
- expect(getTool("computer_use_request_control")).toBeUndefined();
528
527
  });
529
528
  });
@@ -4055,7 +4055,7 @@ describe("relay-server", () => {
4055
4055
  .filter((m) => m.type === "text");
4056
4056
  expect(
4057
4057
  textMessages.some((m) =>
4058
- (m.token ?? "").includes("said I can speak with you"),
4058
+ (m.token ?? "").includes("verified that you are Eve"),
4059
4059
  ),
4060
4060
  ).toBe(true);
4061
4061
 
@@ -4079,6 +4079,51 @@ describe("relay-server", () => {
4079
4079
  relay.destroy();
4080
4080
  });
4081
4081
 
4082
+ test("outbound invite prompt uses assistant introduction", async () => {
4083
+ ensureConversation("conv-outbound-invite-origin");
4084
+ ensureConversation("conv-outbound-invite");
4085
+ const session = createCallSession({
4086
+ conversationId: "conv-outbound-invite",
4087
+ provider: "twilio",
4088
+ fromNumber: "+15551111111",
4089
+ toNumber: "+15558887777",
4090
+ callMode: "invite",
4091
+ inviteFriendName: "Grace",
4092
+ inviteGuardianName: "Hank",
4093
+ initiatedFromConversationId: "conv-outbound-invite-origin",
4094
+ });
4095
+
4096
+ mockAssistantName = "Vellum";
4097
+
4098
+ const { ws, relay } = createMockWs(session.id);
4099
+
4100
+ await relay.handleMessage(
4101
+ JSON.stringify({
4102
+ type: "setup",
4103
+ callSid: "CA_outbound_invite",
4104
+ from: "+15551111111",
4105
+ to: "+15558887777",
4106
+ }),
4107
+ );
4108
+
4109
+ // Should be in verification-pending for invite redemption
4110
+ expect(relay.getConnectionState()).toBe("verification_pending");
4111
+
4112
+ // The prompt should use the outbound assistant introduction
4113
+ const textMessages = ws.sentMessages
4114
+ .map((raw) => JSON.parse(raw) as { type: string; token?: string })
4115
+ .filter((m) => m.type === "text");
4116
+ expect(
4117
+ textMessages.some(
4118
+ (m) =>
4119
+ (m.token ?? "").includes("this is Vellum") &&
4120
+ (m.token ?? "").includes("Hank's assistant"),
4121
+ ),
4122
+ ).toBe(true);
4123
+
4124
+ relay.destroy();
4125
+ });
4126
+
4082
4127
  // ── resolveGuardianLabel resolution priority ─────────────────────────
4083
4128
 
4084
4129
  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),
@@ -46,9 +46,14 @@ mock.module("../security/keychain-broker-client.js", () => ({
46
46
  return { found: false };
47
47
  },
48
48
  set: async (account: string, value: string) => {
49
- if (mockBrokerSetError) return false;
49
+ if (mockBrokerSetError)
50
+ return {
51
+ status: "rejected" as const,
52
+ code: "KEYCHAIN_ERROR",
53
+ message: "mock error",
54
+ };
50
55
  mockBrokerStore.set(account, value);
51
- return true;
56
+ return { status: "ok" as const };
52
57
  },
53
58
  del: async (account: string) => {
54
59
  if (mockBrokerDelError) return false;
@@ -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),
@@ -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