@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
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Memory lifecycle E2E regression test.
3
+ *
4
+ * Verifies the new memory pipeline end-to-end:
5
+ * - 6-kind enum items (identity, preference, project, decision, constraint, event)
6
+ * - Supersession chains (supersedes/supersededBy fields)
7
+ * - Hybrid search retrieval
8
+ * - Two-layer XML injection format (<memory_context> with sections)
9
+ * - Stripping removes <memory_context> tags
10
+ * - No conflict gate references
11
+ */
1
12
  import { mkdtempSync, rmSync } from "node:fs";
2
13
  import { tmpdir } from "node:os";
3
14
  import { join } from "node:path";
@@ -11,8 +22,6 @@ import {
11
22
  test,
12
23
  } from "bun:test";
13
24
 
14
- import { eq } from "drizzle-orm";
15
-
16
25
  import { DEFAULT_CONFIG } from "../config/defaults.js";
17
26
 
18
27
  const testDir = mkdtempSync(join(tmpdir(), "memory-lifecycle-e2e-"));
@@ -38,56 +47,13 @@ mock.module("../util/logger.js", () => ({
38
47
  mock.module("../memory/qdrant-client.js", () => ({
39
48
  getQdrantClient: () => ({
40
49
  searchWithFilter: async () => [],
50
+ hybridSearch: async () => [],
41
51
  upsertPoints: async () => {},
42
52
  deletePoints: async () => {},
43
53
  }),
44
54
  initQdrantClient: () => {},
45
55
  }));
46
56
 
47
- // Mock clarification resolver to prevent real Anthropic API calls when
48
- // ANTHROPIC_API_KEY is set. Without this, resolveConflictClarification can
49
- // resolve conflicts via LLM before the test asserts on pending state.
50
- mock.module("../memory/clarification-resolver.js", () => ({
51
- resolveConflictClarification: async (input: { userMessage: string }) => {
52
- const msg = input.userMessage.toLowerCase();
53
- // "Use the new renderer going forward" → keep candidate
54
- if (
55
- msg.includes("new") ||
56
- msg.includes("replace") ||
57
- msg.includes("instead")
58
- ) {
59
- return {
60
- resolution: "keep_candidate" as const,
61
- strategy: "heuristic" as const,
62
- resolvedStatement: null,
63
- explanation:
64
- "User response explicitly points to candidate/new statement.",
65
- };
66
- }
67
- // "Keep the old runtime one" → keep existing
68
- if (
69
- msg.includes("old") ||
70
- msg.includes("existing") ||
71
- msg.includes("still")
72
- ) {
73
- return {
74
- resolution: "keep_existing" as const,
75
- strategy: "heuristic" as const,
76
- resolvedStatement: null,
77
- explanation:
78
- "User response explicitly points to existing/old statement.",
79
- };
80
- }
81
- // Default: still_unclear (e.g. "Need react roadmap update today")
82
- return {
83
- resolution: "still_unclear" as const,
84
- strategy: "heuristic" as const,
85
- resolvedStatement: null,
86
- explanation: "No clear directional cue found in user message.",
87
- };
88
- },
89
- }));
90
-
91
57
  const TEST_CONFIG = {
92
58
  ...DEFAULT_CONFIG,
93
59
  memory: {
@@ -103,43 +69,7 @@ const TEST_CONFIG = {
103
69
  },
104
70
  retrieval: {
105
71
  ...DEFAULT_CONFIG.memory.retrieval,
106
- lexicalTopK: 40,
107
- semanticTopK: 0,
108
72
  maxInjectTokens: 900,
109
- dynamicBudget: {
110
- ...DEFAULT_CONFIG.memory.retrieval.dynamicBudget,
111
- enabled: true,
112
- minInjectTokens: 180,
113
- maxInjectTokens: 360,
114
- targetHeadroomTokens: 700,
115
- },
116
- reranking: {
117
- ...DEFAULT_CONFIG.memory.retrieval.reranking,
118
- enabled: false,
119
- },
120
- },
121
- entity: {
122
- ...DEFAULT_CONFIG.memory.entity,
123
- relationRetrieval: {
124
- ...DEFAULT_CONFIG.memory.entity.relationRetrieval,
125
- enabled: true,
126
- maxSeedEntities: 4,
127
- maxNeighborEntities: 6,
128
- maxEdges: 8,
129
- neighborScoreMultiplier: 0.65,
130
- },
131
- },
132
- conflicts: {
133
- ...DEFAULT_CONFIG.memory.conflicts,
134
- enabled: true,
135
- gateMode: "soft" as const,
136
- relevanceThreshold: 0.2,
137
- resolverLlmTimeoutMs: 250,
138
- },
139
- profile: {
140
- ...DEFAULT_CONFIG.memory.profile,
141
- enabled: true,
142
- maxInjectTokens: 300,
143
73
  },
144
74
  },
145
75
  };
@@ -152,41 +82,22 @@ mock.module("../config/loader.js", () => ({
152
82
  invalidateConfigCache: () => {},
153
83
  }));
154
84
 
155
- import { ConflictGate } from "../daemon/session-conflict-gate.js";
156
- import {
157
- injectDynamicProfileIntoUserMessage,
158
- stripDynamicProfileMessages,
159
- } from "../daemon/session-dynamic-profile.js";
160
- import {
161
- createOrUpdatePendingConflict,
162
- getConflictById,
163
- } from "../memory/conflict-store.js";
164
85
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
165
- import { enqueueResolvePendingConflictsForMessageJob } from "../memory/jobs-store.js";
166
86
  import {
167
87
  resetCleanupScheduleThrottle,
168
88
  resetStaleSweepThrottle,
169
- runMemoryJobsOnce,
170
89
  } from "../memory/jobs-worker.js";
171
- import { buildMemoryRecall } from "../memory/retriever.js";
90
+ import {
91
+ buildMemoryRecall,
92
+ injectMemoryRecallAsSeparateMessage,
93
+ stripMemoryRecallMessages,
94
+ } from "../memory/retriever.js";
172
95
  import {
173
96
  conversations,
174
- memoryEntities,
175
- memoryEntityRelations,
176
- memoryItemConflicts,
177
- memoryItemEntities,
178
97
  memoryItems,
179
98
  memoryItemSources,
180
99
  messages,
181
100
  } from "../memory/schema.js";
182
- import type { Message } from "../providers/types.js";
183
-
184
- function messageText(message: Message): string {
185
- return message.content
186
- .filter((block) => block.type === "text")
187
- .map((block) => (block as { type: "text"; text: string }).text)
188
- .join("\n");
189
- }
190
101
 
191
102
  describe("Memory lifecycle E2E regression", () => {
192
103
  beforeAll(() => {
@@ -195,15 +106,9 @@ describe("Memory lifecycle E2E regression", () => {
195
106
 
196
107
  beforeEach(() => {
197
108
  const db = getDb();
198
- db.run("DELETE FROM memory_item_conflicts");
199
- db.run("DELETE FROM memory_item_entities");
200
- db.run("DELETE FROM memory_entity_relations");
201
- db.run("DELETE FROM memory_entities");
202
109
  db.run("DELETE FROM memory_item_sources");
203
110
  db.run("DELETE FROM memory_embeddings");
204
- db.run("DELETE FROM memory_summaries");
205
111
  db.run("DELETE FROM memory_items");
206
- db.run("DELETE FROM memory_segment_fts");
207
112
  db.run("DELETE FROM memory_segments");
208
113
  db.run("DELETE FROM messages");
209
114
  db.run("DELETE FROM conversations");
@@ -222,7 +127,7 @@ describe("Memory lifecycle E2E regression", () => {
222
127
  }
223
128
  });
224
129
 
225
- test("relation expansion, soft gate, background resolution, and profile hygiene remain consistent", async () => {
130
+ test("extraction produces items with 6-kind enum and supersession chains form correctly", async () => {
226
131
  const db = getDb();
227
132
  const now = 1_701_100_000_000;
228
133
  const conversationId = "conv-memory-lifecycle";
@@ -256,315 +161,267 @@ describe("Memory lifecycle E2E regression", () => {
256
161
  ]),
257
162
  createdAt: now + 10,
258
163
  },
259
- {
260
- id: "msg-lifecycle-background",
261
- conversationId,
262
- role: "user",
263
- content: JSON.stringify([
264
- { type: "text", text: "Keep the old runtime one." },
265
- ]),
266
- createdAt: now + 500,
267
- },
268
164
  ])
269
165
  .run();
270
166
 
271
- db.insert(memoryItems)
272
- .values([
273
- {
274
- id: "item-atlas-direct",
275
- kind: "preference",
276
- subject: "atlas rollout",
277
- statement: "Project Atlas prefers blue-green rollouts.",
167
+ // Seed items using the 6-kind enum
168
+ const kinds = [
169
+ "identity",
170
+ "preference",
171
+ "project",
172
+ "decision",
173
+ "constraint",
174
+ "event",
175
+ ] as const;
176
+ for (let i = 0; i < kinds.length; i++) {
177
+ db.insert(memoryItems)
178
+ .values({
179
+ id: `item-kind-${kinds[i]}`,
180
+ kind: kinds[i],
181
+ subject: `${kinds[i]} test`,
182
+ statement: `This is a ${kinds[i]} item for testing.`,
278
183
  status: "active",
279
- confidence: 0.95,
280
- importance: 0.9,
281
- fingerprint: "fp-item-atlas-direct",
282
- verificationState: "assistant_inferred",
283
- scopeId: "default",
284
- firstSeenAt: now + 10,
285
- lastSeenAt: now + 10,
286
- validFrom: now + 10,
287
- invalidAt: null,
288
- },
289
- {
290
- id: "item-k8s-relation",
291
- kind: "fact",
292
- subject: "autoscaling",
293
- statement: "Scale API pods at 70% CPU with Kubernetes HPA.",
294
- status: "active",
295
- confidence: 0.82,
296
- importance: 0.7,
297
- fingerprint: "fp-item-k8s-relation",
184
+ confidence: 0.9,
185
+ importance: 0.8,
186
+ fingerprint: `fp-item-kind-${kinds[i]}`,
298
187
  verificationState: "assistant_inferred",
299
188
  scopeId: "default",
300
- firstSeenAt: now + 12,
301
- lastSeenAt: now + 12,
302
- validFrom: now + 12,
303
- invalidAt: null,
304
- },
305
- ])
306
- .run();
307
-
308
- db.insert(memoryItemSources)
309
- .values([
310
- {
311
- memoryItemId: "item-atlas-direct",
189
+ firstSeenAt: now + i,
190
+ lastSeenAt: now + i,
191
+ })
192
+ .run();
193
+
194
+ db.insert(memoryItemSources)
195
+ .values({
196
+ memoryItemId: `item-kind-${kinds[i]}`,
312
197
  messageId: "msg-lifecycle-seed",
313
- evidence: "Atlas rollout policy note",
314
- createdAt: now + 10,
315
- },
316
- {
317
- memoryItemId: "item-k8s-relation",
318
- messageId: "msg-lifecycle-seed",
319
- evidence: "Kubernetes autoscaling note",
320
- createdAt: now + 12,
321
- },
322
- ])
323
- .run();
198
+ evidence: `${kinds[i]} evidence`,
199
+ createdAt: now + i,
200
+ })
201
+ .run();
202
+ }
324
203
 
325
- db.insert(memoryEntities)
326
- .values([
327
- {
328
- id: "entity-atlas-lifecycle",
329
- name: "Project Atlas",
330
- type: "project",
331
- aliases: JSON.stringify(["atlas"]),
332
- description: null,
333
- firstSeenAt: now,
334
- lastSeenAt: now + 500,
335
- mentionCount: 4,
336
- },
337
- {
338
- id: "entity-kubernetes-lifecycle",
339
- name: "Kubernetes",
340
- type: "tool",
341
- aliases: JSON.stringify(["k8s"]),
342
- description: null,
343
- firstSeenAt: now,
344
- lastSeenAt: now + 500,
345
- mentionCount: 3,
346
- },
347
- ])
204
+ // Create a supersession chain: old decision superseded by new decision
205
+ db.insert(memoryItems)
206
+ .values({
207
+ id: "item-old-decision",
208
+ kind: "decision",
209
+ subject: "deploy strategy",
210
+ statement: "Deploy manually every Friday.",
211
+ status: "superseded",
212
+ confidence: 0.7,
213
+ importance: 0.6,
214
+ fingerprint: "fp-old-decision",
215
+ verificationState: "assistant_inferred",
216
+ scopeId: "default",
217
+ firstSeenAt: now - 10_000,
218
+ lastSeenAt: now - 10_000,
219
+ supersededBy: "item-kind-decision",
220
+ })
348
221
  .run();
349
222
 
350
- db.insert(memoryEntityRelations)
223
+ // Update the new decision to reference the old one
224
+ db.run(
225
+ `UPDATE memory_items SET supersedes = 'item-old-decision' WHERE id = 'item-kind-decision'`,
226
+ );
227
+
228
+ // Verify supersession chain is stored correctly
229
+ const oldDecision = db
230
+ .select()
231
+ .from(memoryItems)
232
+ .all()
233
+ .find((i) => i.id === "item-old-decision");
234
+ const newDecision = db
235
+ .select()
236
+ .from(memoryItems)
237
+ .all()
238
+ .find((i) => i.id === "item-kind-decision");
239
+
240
+ expect(oldDecision).toBeDefined();
241
+ expect(oldDecision!.status).toBe("superseded");
242
+ expect(oldDecision!.supersededBy).toBe("item-kind-decision");
243
+
244
+ expect(newDecision).toBeDefined();
245
+ expect(newDecision!.status).toBe("active");
246
+ expect(newDecision!.supersedes).toBe("item-old-decision");
247
+ });
248
+
249
+ test("recall completes and recency search retrieves relevant items", async () => {
250
+ const db = getDb();
251
+ const now = 1_701_100_000_000;
252
+ const conversationId = "conv-recall-lifecycle";
253
+
254
+ db.insert(conversations)
351
255
  .values({
352
- id: "rel-atlas-kubernetes-lifecycle",
353
- sourceEntityId: "entity-atlas-lifecycle",
354
- targetEntityId: "entity-kubernetes-lifecycle",
355
- relation: "uses",
356
- evidence: "Project Atlas runs on Kubernetes",
357
- firstSeenAt: now + 20,
358
- lastSeenAt: now + 20,
256
+ id: conversationId,
257
+ title: null,
258
+ createdAt: now,
259
+ updatedAt: now,
260
+ totalInputTokens: 0,
261
+ totalOutputTokens: 0,
262
+ totalEstimatedCost: 0,
263
+ contextSummary: null,
264
+ contextCompactedMessageCount: 0,
265
+ contextCompactedAt: null,
359
266
  })
360
267
  .run();
361
268
 
362
- db.insert(memoryItemEntities)
363
- .values([
364
- {
365
- memoryItemId: "item-atlas-direct",
366
- entityId: "entity-atlas-lifecycle",
367
- },
368
- {
369
- memoryItemId: "item-k8s-relation",
370
- entityId: "entity-kubernetes-lifecycle",
371
- },
372
- ])
269
+ db.insert(messages)
270
+ .values({
271
+ id: "msg-recall-seed",
272
+ conversationId,
273
+ role: "user",
274
+ content: JSON.stringify([
275
+ {
276
+ type: "text",
277
+ text: "Atlas deployment notes mention Kubernetes infrastructure.",
278
+ },
279
+ ]),
280
+ createdAt: now + 10,
281
+ })
373
282
  .run();
374
283
 
284
+ // Insert a segment that recency search can find
285
+ db.run(`
286
+ INSERT INTO memory_segments (
287
+ id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at
288
+ ) VALUES (
289
+ 'seg-recall-seed', 'msg-recall-seed', '${conversationId}', 'user', 0,
290
+ 'Atlas deployment notes mention Kubernetes infrastructure.', 10, 'default',
291
+ ${now + 10}, ${now + 10}
292
+ )
293
+ `);
294
+
375
295
  const recall = await buildMemoryRecall(
376
296
  "atlas deployment guidance",
377
297
  conversationId,
378
298
  TEST_CONFIG,
379
299
  );
380
- expect(recall.injectedText).toContain("blue-green rollouts");
381
- expect(recall.injectedText).toContain("70% CPU");
382
- expect(recall.relationSeedEntityCount).toBeGreaterThan(0);
383
- expect(recall.relationTraversedEdgeCount).toBeGreaterThan(0);
384
- expect(recall.relationNeighborEntityCount).toBeGreaterThan(0);
385
- expect(recall.relationExpandedItemCount).toBeGreaterThan(0);
386
300
 
387
- db.insert(memoryItems)
388
- .values([
389
- {
390
- id: "item-ui-existing",
391
- kind: "preference",
392
- subject: "ui renderer",
393
- statement: "React renderer is the default for Atlas dashboard.",
394
- status: "active",
395
- confidence: 0.88,
396
- importance: 0.8,
397
- fingerprint: "fp-item-ui-existing",
398
- verificationState: "user_reported",
399
- scopeId: "default",
400
- firstSeenAt: now + 30,
401
- lastSeenAt: now + 30,
402
- validFrom: now + 30,
403
- invalidAt: null,
404
- },
405
- {
406
- id: "item-ui-candidate",
407
- kind: "preference",
408
- subject: "ui renderer",
409
- statement: "Svelte renderer is the default for Atlas dashboard.",
410
- status: "pending_clarification",
411
- confidence: 0.84,
412
- importance: 0.8,
413
- fingerprint: "fp-item-ui-candidate",
414
- verificationState: "user_reported",
415
- scopeId: "default",
416
- firstSeenAt: now + 31,
417
- lastSeenAt: now + 31,
418
- validFrom: now + 31,
419
- invalidAt: null,
420
- },
421
- ])
301
+ // Recency search finds segments but their finalScore (semantic*0.7 +
302
+ // recency*0.2 + confidence*0.1) is too low to pass tier classification
303
+ // (threshold > 0.6) because semantic=0 with Qdrant mocked empty.
304
+ // Verify recency search ran successfully.
305
+ expect(recall.recencyHits).toBeGreaterThan(0);
306
+ // Candidates exist but don't pass tier classification, so injectedText is empty
307
+ expect(recall.enabled).toBe(true);
308
+ });
309
+
310
+ test("two-layer XML injection format uses <memory_context> tags", async () => {
311
+ const db = getDb();
312
+ const now = 1_701_100_000_000;
313
+ const conversationId = "conv-injection-format";
314
+
315
+ db.insert(conversations)
316
+ .values({
317
+ id: conversationId,
318
+ title: null,
319
+ createdAt: now,
320
+ updatedAt: now,
321
+ totalInputTokens: 0,
322
+ totalOutputTokens: 0,
323
+ totalEstimatedCost: 0,
324
+ contextSummary: null,
325
+ contextCompactedMessageCount: 0,
326
+ contextCompactedAt: null,
327
+ })
422
328
  .run();
423
329
 
424
- const gatedConflict = createOrUpdatePendingConflict({
425
- scopeId: "default",
426
- existingItemId: "item-ui-existing",
427
- candidateItemId: "item-ui-candidate",
428
- relationship: "ambiguous_contradiction",
429
- clarificationQuestion: "Should we keep React or move to Svelte?",
430
- });
431
-
432
- const conflictGate = new ConflictGate();
433
-
434
- // First evaluation: conflict remains pending; evaluate returns void (no
435
- // user-facing output — conflict handling is fully internal)
436
- const firstResult = await conflictGate.evaluate(
437
- "Need react roadmap update today",
438
- TEST_CONFIG.memory.conflicts,
439
- );
440
- expect(firstResult).toBeUndefined();
330
+ db.insert(messages)
331
+ .values({
332
+ id: "msg-injection-seed",
333
+ conversationId,
334
+ role: "user",
335
+ content: JSON.stringify([
336
+ {
337
+ type: "text",
338
+ text: "My preferred timezone is America/Los_Angeles.",
339
+ },
340
+ ]),
341
+ createdAt: now + 10,
342
+ })
343
+ .run();
441
344
 
442
- const pendingAfterFirstGate = getConflictById(gatedConflict.id);
443
- expect(pendingAfterFirstGate?.status).toBe("pending_clarification");
345
+ db.run(`
346
+ INSERT INTO memory_segments (
347
+ id, message_id, conversation_id, role, segment_index, text, token_estimate, scope_id, created_at, updated_at
348
+ ) VALUES (
349
+ 'seg-injection-seed', 'msg-injection-seed', '${conversationId}', 'user', 0,
350
+ 'My preferred timezone is America/Los_Angeles.', 10, 'default',
351
+ ${now + 10}, ${now + 10}
352
+ )
353
+ `);
444
354
 
445
- // Second evaluation: clarification-like reply resolves the conflict
446
- // internally; still no user-facing prompt is produced
447
- const secondResult = await conflictGate.evaluate(
448
- "Use the new renderer going forward.",
449
- TEST_CONFIG.memory.conflicts,
355
+ const recall = await buildMemoryRecall(
356
+ "timezone",
357
+ conversationId,
358
+ TEST_CONFIG,
450
359
  );
451
- expect(secondResult).toBeUndefined();
452
360
 
453
- const resolvedAfterSecondGate = getConflictById(gatedConflict.id);
454
- expect(resolvedAfterSecondGate?.status).toBe("resolved_keep_candidate");
361
+ // The recency-only promotion path (Step 6 in retriever) ensures the
362
+ // seeded segment reaches tier 2 and is injected even without semantic
363
+ // search. Verify structure of the two-layer XML format.
364
+ expect(recall.recencyHits).toBeGreaterThan(0);
365
+ expect(recall.enabled).toBe(true);
366
+ expect(recall.injectedText.length).toBeGreaterThan(0);
367
+ expect(recall.injectedTokens).toBeGreaterThan(0);
368
+ expect(recall.injectedText).toContain("<memory_context>");
369
+ expect(recall.injectedText).toContain("</memory_context>");
370
+ });
455
371
 
456
- const uiExisting = db
457
- .select()
458
- .from(memoryItems)
459
- .where(eq(memoryItems.id, "item-ui-existing"))
460
- .get();
461
- const uiCandidate = db
462
- .select()
463
- .from(memoryItems)
464
- .where(eq(memoryItems.id, "item-ui-candidate"))
465
- .get();
466
- expect(uiExisting?.status).toBe("superseded");
467
- expect(uiCandidate?.status).toBe("active");
372
+ test("stripping removes <memory_context> tags from injected recall", () => {
373
+ const memoryRecallText =
374
+ "<memory_context>\n\n<relevant_context>\nuser prefers concise answers\n</relevant_context>\n\n</memory_context>";
375
+ const originalMessages = [
376
+ {
377
+ role: "user" as const,
378
+ content: [{ type: "text", text: "Actual user request" }],
379
+ },
380
+ ];
381
+ const injected = injectMemoryRecallAsSeparateMessage(
382
+ originalMessages,
383
+ memoryRecallText,
384
+ );
468
385
 
469
- db.insert(memoryItems)
470
- .values([
471
- {
472
- id: "item-runtime-existing",
473
- kind: "preference",
474
- subject: "runtime",
475
- statement: "Node.js 20 remains the default runtime.",
476
- status: "active",
477
- confidence: 0.83,
478
- importance: 0.7,
479
- fingerprint: "fp-item-runtime-existing",
480
- verificationState: "user_reported",
481
- scopeId: "default",
482
- firstSeenAt: now + 200,
483
- lastSeenAt: now + 200,
484
- validFrom: now + 200,
485
- invalidAt: null,
486
- },
487
- {
488
- id: "item-runtime-candidate",
489
- kind: "preference",
490
- subject: "runtime",
491
- statement: "Bun becomes the default runtime.",
492
- status: "pending_clarification",
493
- confidence: 0.81,
494
- importance: 0.7,
495
- fingerprint: "fp-item-runtime-candidate",
496
- verificationState: "user_reported",
497
- scopeId: "default",
498
- firstSeenAt: now + 201,
499
- lastSeenAt: now + 201,
500
- validFrom: now + 201,
501
- invalidAt: null,
502
- },
503
- ])
504
- .run();
386
+ expect(injected).toHaveLength(3);
387
+ expect(injected[0].role).toBe("user");
388
+ expect(injected[0].content[0].text).toBe(memoryRecallText);
389
+ expect(injected[1].role as string).toBe("assistant");
390
+ expect(injected[2].role).toBe("user");
391
+ expect(injected[2].content[0].text).toBe("Actual user request");
505
392
 
506
- const backgroundConflict = createOrUpdatePendingConflict({
507
- scopeId: "default",
508
- existingItemId: "item-runtime-existing",
509
- candidateItemId: "item-runtime-candidate",
510
- relationship: "ambiguous_contradiction",
511
- });
393
+ const cleaned = stripMemoryRecallMessages(injected, memoryRecallText);
394
+ expect(cleaned).toHaveLength(1);
395
+ expect(cleaned[0].content[0].text).toBe("Actual user request");
396
+ });
512
397
 
513
- db.update(memoryItemConflicts)
514
- .set({ createdAt: now + 300, updatedAt: now + 300 })
515
- .where(eq(memoryItemConflicts.id, backgroundConflict.id))
516
- .run();
398
+ test("empty retrieval returns no injection", async () => {
399
+ const db = getDb();
400
+ const now = 1_701_100_000_000;
401
+ const conversationId = "conv-empty-lifecycle";
517
402
 
518
- enqueueResolvePendingConflictsForMessageJob(
519
- "msg-lifecycle-background",
520
- "default",
521
- );
522
- const processedJobs = await runMemoryJobsOnce();
523
- expect(processedJobs).toBe(1);
403
+ db.insert(conversations)
404
+ .values({
405
+ id: conversationId,
406
+ title: null,
407
+ createdAt: now,
408
+ updatedAt: now,
409
+ totalInputTokens: 0,
410
+ totalOutputTokens: 0,
411
+ totalEstimatedCost: 0,
412
+ contextSummary: null,
413
+ contextCompactedMessageCount: 0,
414
+ contextCompactedAt: null,
415
+ })
416
+ .run();
524
417
 
525
- const backgroundResolvedConflict = getConflictById(backgroundConflict.id);
526
- expect(backgroundResolvedConflict?.status).toBe("resolved_keep_existing");
527
- expect(backgroundResolvedConflict?.resolutionNote).toContain(
528
- "Background message resolver",
418
+ const recall = await buildMemoryRecall(
419
+ "completely unrelated xyzzy topic",
420
+ conversationId,
421
+ TEST_CONFIG,
529
422
  );
530
423
 
531
- const runtimeExisting = db
532
- .select()
533
- .from(memoryItems)
534
- .where(eq(memoryItems.id, "item-runtime-existing"))
535
- .get();
536
- const runtimeCandidate = db
537
- .select()
538
- .from(memoryItems)
539
- .where(eq(memoryItems.id, "item-runtime-candidate"))
540
- .get();
541
- expect(runtimeExisting?.status).toBe("active");
542
- expect(runtimeCandidate?.status).toBe("superseded");
543
-
544
- const profileText =
545
- "<dynamic-user-profile>\n- timezone: America/Los_Angeles\n- prefers concise answers\n</dynamic-user-profile>";
546
- const baseUserMessage: Message = {
547
- role: "user",
548
- content: [{ type: "text", text: "Plan next sprint milestones." }],
549
- };
550
-
551
- const injectedProfileMessage = injectDynamicProfileIntoUserMessage(
552
- baseUserMessage,
553
- profileText,
554
- );
555
- const runtimeUserText = messageText(injectedProfileMessage);
556
- expect(runtimeUserText).toContain("<dynamic-profile-context>");
557
- expect(runtimeUserText).toContain("<dynamic-user-profile>");
558
- expect(runtimeUserText).toContain("</dynamic-profile-context>");
559
-
560
- const strippedMessages = stripDynamicProfileMessages(
561
- [injectedProfileMessage],
562
- profileText,
563
- );
564
- expect(strippedMessages).toHaveLength(1);
565
- expect(messageText(strippedMessages[0] as Message).trim()).toBe(
566
- "Plan next sprint milestones.",
567
- );
568
- expect(messageText(baseUserMessage)).toBe("Plan next sprint milestones.");
424
+ expect(recall.injectedText).toBe("");
425
+ expect(recall.injectedTokens).toBe(0);
569
426
  });
570
427
  });