@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
@@ -55,6 +55,7 @@ mock.module("../../memory/embedding-local.js", () => ({
55
55
  mock.module("../../memory/qdrant-client.js", () => ({
56
56
  getQdrantClient: () => ({
57
57
  searchWithFilter: async () => [],
58
+ hybridSearch: async () => [],
58
59
  upsertPoints: async () => {},
59
60
  deletePoints: async () => {},
60
61
  }),
@@ -93,14 +94,14 @@ import {
93
94
  memoryItemSources,
94
95
  messages,
95
96
  } from "../../memory/schema.js";
96
- import { handleMemoryRecall, type MemoryRecallToolResult } from "./handlers.js";
97
+ import type { MemoryRecallToolResult } from "./handlers.js";
98
+ import { handleMemoryRecall } from "./handlers.js";
97
99
 
98
100
  function clearTables() {
99
101
  const db = getDb();
100
102
  db.run("DELETE FROM memory_item_sources");
101
103
  db.run("DELETE FROM memory_items");
102
104
  db.run("DELETE FROM memory_segments");
103
- db.run("DELETE FROM memory_segment_fts");
104
105
  db.run("DELETE FROM messages");
105
106
  db.run("DELETE FROM conversations");
106
107
  }
@@ -237,7 +238,7 @@ function seedMemory() {
237
238
 
238
239
  insertItem(db, {
239
240
  id: "item-testing",
240
- kind: "fact",
241
+ kind: "identity",
241
242
  subject: "testing",
242
243
  statement: "The project uses bun test for unit testing",
243
244
  firstSeenAt: now - 20_000,
@@ -284,7 +285,7 @@ describe("handleMemoryRecall", () => {
284
285
 
285
286
  // ── Happy path ────────────────────────────────────────────────────
286
287
 
287
- test("returns formatted results from multiple sources", async () => {
288
+ test("returns valid result shape with Qdrant mocked empty", async () => {
288
289
  seedMemory();
289
290
 
290
291
  const result = await handleMemoryRecall(
@@ -292,35 +293,13 @@ describe("handleMemoryRecall", () => {
292
293
  TEST_CONFIG,
293
294
  );
294
295
 
296
+ // With Qdrant mocked empty, hybrid search returns nothing.
297
+ // Recency search also returns nothing (no conversationId passed to handler).
298
+ // The handler should return a valid result shape with zero results.
295
299
  expect(result.isError).toBe(false);
296
300
  const parsed = parseResult(result.content);
297
- expect(parsed.resultCount).toBeGreaterThan(0);
298
- expect(parsed.text.length).toBeGreaterThan(0);
299
- });
300
-
301
- test("respects max_results parameter", async () => {
302
- seedMemory();
303
-
304
- const result = await handleMemoryRecall(
305
- { query: "API design", max_results: 1 },
306
- TEST_CONFIG,
307
- );
308
-
309
- expect(result.isError).toBe(false);
310
- const parsed = parseResult(result.content);
311
- expect(parsed.resultCount).toBeLessThanOrEqual(1);
312
- });
313
-
314
- test("clamps max_results to 50", async () => {
315
- seedMemory();
316
-
317
- // Should not throw, max_results capped at 50
318
- const result = await handleMemoryRecall(
319
- { query: "API design", max_results: 100 },
320
- TEST_CONFIG,
321
- );
322
-
323
- expect(result.isError).toBe(false);
301
+ expect(typeof parsed.resultCount).toBe("number");
302
+ expect(typeof parsed.text).toBe("string");
324
303
  });
325
304
 
326
305
  // ── Empty results ─────────────────────────────────────────────────
@@ -336,10 +315,8 @@ describe("handleMemoryRecall", () => {
336
315
  const parsed = parseResult(result.content);
337
316
  expect(parsed.resultCount).toBe(0);
338
317
  expect(parsed.text).toBe("No matching memories found.");
339
- expect(parsed.sources.lexical).toBe(0);
340
318
  expect(parsed.sources.semantic).toBe(0);
341
319
  expect(parsed.sources.recency).toBe(0);
342
- expect(parsed.sources.entity).toBe(0);
343
320
  });
344
321
 
345
322
  // ── Degraded mode ─────────────────────────────────────────────────
@@ -399,11 +376,13 @@ describe("handleMemoryRecall", () => {
399
376
  // Not degraded because embeddings are optional
400
377
  expect(parsed.degraded).toBe(false);
401
378
  expect(parsed.sources.semantic).toBe(0);
402
- // Still returns results from non-semantic sources (direct item search)
403
- expect(parsed.resultCount).toBeGreaterThan(0);
379
+ // With FTS/direct-item search removed, only Qdrant hybrid search and
380
+ // recency search remain. Both return empty here (Qdrant mocked,
381
+ // no conversationId passed). The handler returns a valid empty result.
382
+ expect(parsed.resultCount).toBe(0);
404
383
  });
405
384
 
406
- test("returns lexical results in degraded mode", async () => {
385
+ test("gracefully returns empty in degraded mode without embeddings", async () => {
407
386
  seedMemory();
408
387
 
409
388
  const degradedConfig: AssistantConfig = {
@@ -425,70 +404,76 @@ describe("handleMemoryRecall", () => {
425
404
 
426
405
  expect(result.isError).toBe(false);
427
406
  const parsed = parseResult(result.content);
428
- // Direct item search should still find items even without embeddings
429
- expect(parsed.resultCount).toBeGreaterThan(0);
407
+ // With FTS removed and Qdrant mocked, no retrieval path finds items.
408
+ // The handler returns a valid empty result without throwing.
409
+ expect(typeof parsed.resultCount).toBe("number");
430
410
  });
431
411
 
432
412
  // ── Scope filtering ───────────────────────────────────────────────
433
413
 
434
- test("scope 'conversation' restricts to current thread", async () => {
414
+ test("scope 'conversation' passes scope policy override to retriever", async () => {
415
+ // Seed a conversation with segments in the target scope and in a different
416
+ // scope. With scope="conversation", only the target scope's segments should
417
+ // be returned (fallbackToDefault=false).
435
418
  const db = getDb();
436
419
  const now = Date.now();
420
+ const convId = "conv-scope-a";
421
+
422
+ insertConversation(db, convId, now - 10_000);
423
+ insertMessage(
424
+ db,
425
+ "msg-scope-a",
426
+ convId,
427
+ "user",
428
+ "scoped data for conversation A",
429
+ now - 5_000,
430
+ );
437
431
 
438
- // Insert item in "conv-scope-a" scope
439
- insertItem(db, {
440
- id: "item-scope-a",
441
- kind: "fact",
442
- subject: "scoped data",
443
- statement: "This item is scoped to conversation A",
444
- firstSeenAt: now - 10_000,
445
- scopeId: "conv-scope-a",
446
- });
432
+ // Insert a segment scoped to this conversation's scope
433
+ db.run(`
434
+ INSERT INTO memory_segments (id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at)
435
+ VALUES ('seg-scope-a', 'msg-scope-a', '${convId}', 'user', 0, 'Conversation-scoped data for conversation A', 8, '${convId}', ${
436
+ now - 5_000
437
+ }, ${now - 5_000})
438
+ `);
439
+
440
+ // Insert an out-of-scope segment that should NOT be returned
441
+ db.run(`
442
+ INSERT INTO memory_segments (id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at)
443
+ VALUES ('seg-scope-other', 'msg-scope-a', '${convId}', 'user', 1, 'Out-of-scope data from a different scope', 8, 'other-scope', ${
444
+ now - 5_000
445
+ }, ${now - 5_000})
446
+ `);
447
447
 
448
- // Insert item in default scope
449
- insertItem(db, {
450
- id: "item-default",
451
- kind: "fact",
452
- subject: "default data",
453
- statement: "This item is in the default scope about scoped data",
454
- firstSeenAt: now - 10_000,
455
- scopeId: "default",
456
- });
457
-
458
- // Query with scope="conversation" and scopeId="conv-scope-a"
459
- // should restrict to only that scope (no fallback to default)
460
448
  const result = await handleMemoryRecall(
461
- { query: "scoped data", scope: "conversation" },
449
+ { query: "data", scope: "conversation" },
462
450
  TEST_CONFIG,
463
- "conv-scope-a",
451
+ convId,
452
+ convId,
464
453
  );
465
454
 
466
455
  expect(result.isError).toBe(false);
467
456
  const parsed = parseResult(result.content);
468
-
469
- // When scope is "conversation", fallbackToDefault is false,
470
- // so only items from conv-scope-a should appear
471
- expect(parsed.resultCount).toBeGreaterThan(0);
472
- expect(parsed.text).toContain("scoped to conversation A");
473
- expect(parsed.text).not.toContain("default scope");
457
+ // With conversation scope, only the conversation-scoped segment is returned.
458
+ // The other-scope segment should be excluded (fallbackToDefault=false).
459
+ expect(parsed.sources.recency).toBe(1);
460
+ expect(parsed.text).toContain("Conversation-scoped data");
461
+ expect(parsed.text).not.toContain("Out-of-scope data");
474
462
  });
475
463
 
476
- test("default scope includes fallback to default scope", async () => {
464
+ test("default scope handler invocation does not error", async () => {
477
465
  const db = getDb();
478
466
  const now = Date.now();
479
467
 
480
- // Insert item in default scope
481
468
  insertItem(db, {
482
469
  id: "item-fallback",
483
- kind: "fact",
470
+ kind: "identity",
484
471
  subject: "global knowledge",
485
472
  statement: "This global knowledge should be accessible from any scope",
486
473
  firstSeenAt: now - 10_000,
487
474
  scopeId: "default",
488
475
  });
489
476
 
490
- // Query with scope="default" (the default) and a specific scopeId
491
- // should include fallback to default scope
492
477
  const result = await handleMemoryRecall(
493
478
  { query: "global knowledge" },
494
479
  TEST_CONFIG,
@@ -497,37 +482,36 @@ describe("handleMemoryRecall", () => {
497
482
 
498
483
  expect(result.isError).toBe(false);
499
484
  const parsed = parseResult(result.content);
500
- // Default scope items should be accessible
501
- expect(parsed.resultCount).toBeGreaterThan(0);
502
- expect(parsed.text).toContain("global knowledge");
485
+ // With Qdrant mocked and no conversation segments, the retriever returns
486
+ // empty. Handler should still return a valid result shape.
487
+ expect(typeof parsed.resultCount).toBe("number");
503
488
  });
504
489
 
505
490
  // ── Error handling ────────────────────────────────────────────────
506
491
 
507
492
  test("retrieval failure returns error message, does not throw", async () => {
508
- // Create a config that will cause the retrieval pipeline to throw
509
- // by making memory disabled in a way that collectAndMergeCandidates breaks.
510
- // We mock the retriever to throw an error.
511
- const badConfig: AssistantConfig = {
512
- ...TEST_CONFIG,
513
- memory: {
514
- ...TEST_CONFIG.memory,
515
- // Force retrieval with impossible settings to trigger an error path
516
- retrieval: {
517
- ...TEST_CONFIG.memory.retrieval,
518
- lexicalTopK: -1, // may cause issues in search
519
- },
520
- },
521
- };
522
-
523
- // Even if the query fails internally, the handler should catch and return
524
- // an error result rather than throwing
525
- const result = await handleMemoryRecall({ query: "test query" }, badConfig);
526
-
527
- // The function should either succeed gracefully or return an error
528
- // but never throw
529
- expect(typeof result.content).toBe("string");
530
- expect(typeof result.isError).toBe("boolean");
493
+ // Mock buildMemoryRecall to throw, simulating an internal retrieval failure
494
+ const retrieverModule = await import("../../memory/retriever.js");
495
+ const original = retrieverModule.buildMemoryRecall;
496
+ (retrieverModule as Record<string, unknown>).buildMemoryRecall =
497
+ async () => {
498
+ throw new Error("Simulated retrieval failure");
499
+ };
500
+
501
+ try {
502
+ const result = await handleMemoryRecall(
503
+ { query: "test query" },
504
+ TEST_CONFIG,
505
+ );
506
+
507
+ // The handler should catch the error and return an error result,
508
+ // never throw
509
+ expect(result.isError).toBe(true);
510
+ expect(result.content).toContain("Simulated retrieval failure");
511
+ } finally {
512
+ // Restore original implementation
513
+ (retrieverModule as Record<string, unknown>).buildMemoryRecall = original;
514
+ }
531
515
  });
532
516
 
533
517
  test("result shape matches MemoryRecallToolResult when successful", async () => {
@@ -546,10 +530,8 @@ describe("handleMemoryRecall", () => {
546
530
  expect(typeof parsed.resultCount).toBe("number");
547
531
  expect(typeof parsed.degraded).toBe("boolean");
548
532
  expect(typeof parsed.sources).toBe("object");
549
- expect(typeof parsed.sources.lexical).toBe("number");
550
533
  expect(typeof parsed.sources.semantic).toBe("number");
551
534
  expect(typeof parsed.sources.recency).toBe("number");
552
- expect(typeof parsed.sources.entity).toBe("number");
553
535
  });
554
536
 
555
537
  test("empty result shape matches MemoryRecallToolResult", async () => {
@@ -564,9 +546,7 @@ describe("handleMemoryRecall", () => {
564
546
  expect(parsed.text).toBe("No matching memories found.");
565
547
  expect(parsed.resultCount).toBe(0);
566
548
  expect(typeof parsed.degraded).toBe("boolean");
567
- expect(parsed.sources.lexical).toBe(0);
568
549
  expect(parsed.sources.semantic).toBe(0);
569
550
  expect(parsed.sources.recency).toBe(0);
570
- expect(parsed.sources.entity).toBe(0);
571
551
  });
572
552
  });
@@ -3,17 +3,9 @@ import { v4 as uuid } from "uuid";
3
3
 
4
4
  import type { AssistantConfig } from "../../config/types.js";
5
5
  import { getDb } from "../../memory/db.js";
6
- import {
7
- getMemoryBackendStatus,
8
- logMemoryEmbeddingWarning,
9
- } from "../../memory/embedding-backend.js";
10
6
  import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
11
- import { formatRecallText } from "../../memory/format-recall.js";
12
7
  import { enqueueMemoryJob } from "../../memory/jobs-store.js";
13
- import {
14
- collectAndMergeCandidates,
15
- embedWithRetry,
16
- } from "../../memory/retriever.js";
8
+ import { buildMemoryRecall } from "../../memory/retriever.js";
17
9
  import { memoryItems } from "../../memory/schema.js";
18
10
  import type { ScopePolicyOverride } from "../../memory/search/types.js";
19
11
  import { getLogger } from "../../util/logger.js";
@@ -39,21 +31,37 @@ export async function handleMemorySave(
39
31
  };
40
32
  }
41
33
 
42
- const kind = args.kind;
34
+ const rawKind = args.kind;
43
35
  const validKinds = new Set([
36
+ "identity",
44
37
  "preference",
45
- "fact",
38
+ "project",
46
39
  "decision",
47
- "profile",
48
- "relationship",
40
+ "constraint",
49
41
  "event",
50
- "opinion",
51
- "instruction",
52
- "style",
53
- "playbook",
54
- "learning",
55
42
  ]);
56
- if (typeof kind !== "string" || !validKinds.has(kind)) {
43
+ /** Maps old kind names to their new equivalents for backwards compat. */
44
+ const kindMigrationMap: Record<string, string> = {
45
+ profile: "identity",
46
+ fact: "identity",
47
+ relationship: "identity",
48
+ opinion: "preference",
49
+ todo: "project",
50
+ instruction: "constraint",
51
+ style: "preference",
52
+ playbook: "constraint",
53
+ learning: "identity",
54
+ };
55
+ if (typeof rawKind !== "string") {
56
+ return {
57
+ content: `Error: kind is required and must be one of: ${[
58
+ ...validKinds,
59
+ ].join(", ")}`,
60
+ isError: true,
61
+ };
62
+ }
63
+ const kind = kindMigrationMap[rawKind] ?? rawKind;
64
+ if (!validKinds.has(kind)) {
57
65
  return {
58
66
  content: `Error: kind is required and must be one of: ${[
59
67
  ...validKinds,
@@ -255,10 +263,8 @@ export interface MemoryRecallToolResult {
255
263
  degraded: boolean;
256
264
  items: Array<{ id: string; type: string; kind: string }>;
257
265
  sources: {
258
- lexical: number;
259
266
  semantic: number;
260
267
  recency: number;
261
- entity: number;
262
268
  };
263
269
  }
264
270
 
@@ -276,11 +282,6 @@ export async function handleMemoryRecall(
276
282
  };
277
283
  }
278
284
 
279
- const maxResults =
280
- typeof args.max_results === "number" && args.max_results > 0
281
- ? Math.min(args.max_results, 50)
282
- : 10;
283
-
284
285
  const scope =
285
286
  typeof args.scope === "string" && args.scope.trim().length > 0
286
287
  ? args.scope.trim()
@@ -298,54 +299,28 @@ export async function handleMemoryRecall(
298
299
  try {
299
300
  const trimmedQuery = query.trim();
300
301
 
301
- // Generate embedding vector (graceful degradation if unavailable)
302
- let queryVector: number[] | null = null;
303
- let provider: string | undefined;
304
- let model: string | undefined;
305
- let degraded = false;
306
-
307
- const backendStatus = getMemoryBackendStatus(config);
308
- if (backendStatus.provider) {
309
- try {
310
- const embedded = await embedWithRetry(config, [trimmedQuery]);
311
- queryVector = embedded.vectors[0] ?? null;
312
- provider = embedded.provider;
313
- model = embedded.model;
314
- } catch (err) {
315
- logMemoryEmbeddingWarning(err, "query");
316
- degraded = !!config.memory.embeddings.required;
317
- }
318
- } else {
319
- degraded = backendStatus.degraded;
320
- }
321
-
322
- // Run the full retrieval pipeline with all sources enabled
323
- const collected = await collectAndMergeCandidates(trimmedQuery, config, {
324
- queryVector,
325
- provider,
326
- model,
327
- conversationId,
328
- scopeId,
329
- scopePolicyOverride,
330
- });
331
-
332
- if (collected.semanticSearchFailed || collected.semanticUnavailable) {
333
- degraded = true;
334
- }
302
+ // Use the unified recall pipeline
303
+ const recall = await buildMemoryRecall(
304
+ trimmedQuery,
305
+ conversationId ?? "",
306
+ config,
307
+ {
308
+ scopeId,
309
+ scopePolicyOverride,
310
+ },
311
+ );
335
312
 
336
- const candidates = collected.merged.slice(0, maxResults);
313
+ const degraded = recall.degraded;
337
314
 
338
- if (candidates.length === 0) {
315
+ if (recall.selectedCount === 0 || recall.injectedText.length === 0) {
339
316
  const result: MemoryRecallToolResult = {
340
317
  text: "No matching memories found.",
341
318
  resultCount: 0,
342
319
  degraded,
343
320
  items: [],
344
321
  sources: {
345
- lexical: 0,
346
- semantic: 0,
347
- recency: 0,
348
- entity: 0,
322
+ semantic: recall.semanticHits,
323
+ recency: recall.recencyHits,
349
324
  },
350
325
  };
351
326
  return {
@@ -354,28 +329,18 @@ export async function handleMemoryRecall(
354
329
  };
355
330
  }
356
331
 
357
- // Format candidates into readable text using the shared formatter
358
- const formatted = formatRecallText(candidates, {
359
- format: config.memory.retrieval.injectionFormat,
360
- maxTokens: config.memory.retrieval.maxInjectTokens,
361
- });
362
-
363
- const items = formatted.selected.map((c) => ({
364
- id: c.id,
365
- type: c.type,
366
- kind: c.kind,
367
- }));
368
-
369
332
  const result: MemoryRecallToolResult = {
370
- text: formatted.text,
371
- resultCount: formatted.selected.length,
333
+ text: recall.injectedText,
334
+ resultCount: recall.selectedCount,
372
335
  degraded,
373
- items,
336
+ items: recall.topCandidates.map((c) => ({
337
+ id: c.key,
338
+ type: c.type,
339
+ kind: c.kind,
340
+ })),
374
341
  sources: {
375
- lexical: collected.lexical.length,
376
- semantic: collected.semantic.length,
377
- recency: collected.recency.length,
378
- entity: collected.entity.length,
342
+ semantic: recall.semanticHits,
343
+ recency: recall.recencyHits,
379
344
  },
380
345
  };
381
346
 
@@ -22,8 +22,15 @@ const VALID_ROUTING_INTENTS: RoutingIntent[] = [
22
22
 
23
23
  export async function executeScheduleCreate(
24
24
  input: Record<string, unknown>,
25
- _context: ToolContext,
25
+ context: ToolContext,
26
26
  ): Promise<ToolExecutionResult> {
27
+ if (context.trustClass !== "guardian") {
28
+ return {
29
+ content:
30
+ "Error: schedule_create is restricted to guardian actors because schedules execute with elevated privileges.",
31
+ isError: true,
32
+ };
33
+ }
27
34
  const name = input.name as string;
28
35
  const timezone = (input.timezone as string) ?? null;
29
36
  const message = input.message as string;
@@ -25,8 +25,15 @@ const VALID_ROUTING_INTENTS: RoutingIntent[] = [
25
25
 
26
26
  export async function executeScheduleUpdate(
27
27
  input: Record<string, unknown>,
28
- _context: ToolContext,
28
+ context: ToolContext,
29
29
  ): Promise<ToolExecutionResult> {
30
+ if (context.trustClass !== "guardian") {
31
+ return {
32
+ content:
33
+ "Error: schedule_update is restricted to guardian actors because schedules execute with elevated privileges.",
34
+ isError: true,
35
+ };
36
+ }
30
37
  const jobId = input.job_id as string;
31
38
  if (!jobId || typeof jobId !== "string") {
32
39
  return { content: "Error: job_id is required", isError: true };
@@ -8,7 +8,10 @@ import type { SkillSummary, SkillToolManifest } from "../../config/skills.js";
8
8
  import { loadSkillBySelector, loadSkillCatalog } from "../../config/skills.js";
9
9
  import { RiskLevel } from "../../permissions/types.js";
10
10
  import type { ToolDefinition } from "../../providers/types.js";
11
- import { autoInstallFromCatalog } from "../../skills/catalog-install.js";
11
+ import {
12
+ autoInstallFromCatalog,
13
+ resolveCatalog,
14
+ } from "../../skills/catalog-install.js";
12
15
  import {
13
16
  collectAllMissing,
14
17
  indexCatalogById,
@@ -191,15 +194,35 @@ export class SkillLoadTool implements Tool {
191
194
  catalogIndex = indexCatalogById(catalog);
192
195
 
193
196
  // Auto-install missing includes before validation (max 5 rounds for transitive deps)
197
+ // Defer catalog resolution until we confirm there are missing includes,
198
+ // then cache the result to avoid redundant network requests per dependency.
199
+ let remoteCatalog: Awaited<ReturnType<typeof resolveCatalog>> | undefined;
200
+
194
201
  const MAX_INSTALL_ROUNDS = 5;
195
202
  for (let round = 0; round < MAX_INSTALL_ROUNDS; round++) {
196
203
  const missing = collectAllMissing(skill.id, catalogIndex);
197
204
  if (missing.size === 0) break;
198
205
 
206
+ // Lazily resolve catalog on first round with missing includes
207
+ if (!remoteCatalog) {
208
+ try {
209
+ remoteCatalog = await resolveCatalog([...missing][0]);
210
+ } catch (err) {
211
+ log.warn(
212
+ { err, skillId: skill.id },
213
+ "Failed to resolve catalog for include auto-install",
214
+ );
215
+ break;
216
+ }
217
+ }
218
+
199
219
  let installedAny = false;
200
220
  for (const missingId of missing) {
201
221
  try {
202
- const installed = await autoInstallFromCatalog(missingId);
222
+ const installed = await autoInstallFromCatalog(
223
+ missingId,
224
+ remoteCatalog,
225
+ );
203
226
  if (installed) {
204
227
  log.info(
205
228
  { skillId: missingId, parentSkillId: skill.id },