@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,361 +0,0 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import {
5
- afterAll,
6
- beforeAll,
7
- beforeEach,
8
- describe,
9
- expect,
10
- mock,
11
- test,
12
- } from "bun:test";
13
-
14
- import { eq } from "drizzle-orm";
15
-
16
- const testDir = mkdtempSync(join(tmpdir(), "contradiction-checker-test-"));
17
-
18
- let nextRelationship = "ambiguous_contradiction";
19
- let nextExplanation = "Statements likely conflict but need confirmation.";
20
- let classifyCallCount = 0;
21
-
22
- const classifyRelationshipMock = mock(async () => {
23
- classifyCallCount += 1;
24
- return {
25
- content: [
26
- {
27
- type: "tool_use" as const,
28
- id: "test-tool-use-id",
29
- name: "classify_relationship",
30
- input: {
31
- relationship: nextRelationship,
32
- explanation: nextExplanation,
33
- },
34
- },
35
- ],
36
- model: "claude-haiku-4-5-20251001",
37
- stopReason: "tool_use",
38
- usage: { inputTokens: 0, outputTokens: 0 },
39
- };
40
- });
41
-
42
- mock.module("../providers/provider-send-message.js", () => ({
43
- getConfiguredProvider: () => ({
44
- sendMessage: classifyRelationshipMock,
45
- }),
46
- createTimeout: (ms: number) => {
47
- const controller = new AbortController();
48
- const timer = setTimeout(() => controller.abort(), ms);
49
- return {
50
- signal: controller.signal,
51
- cleanup: () => clearTimeout(timer),
52
- };
53
- },
54
- extractToolUse: (response: { content: Array<{ type: string }> }) => {
55
- return response.content.find(
56
- (b: { type: string }) => b.type === "tool_use",
57
- );
58
- },
59
- userMessage: (text: string) => ({
60
- role: "user",
61
- content: [{ type: "text", text }],
62
- }),
63
- }));
64
-
65
- mock.module("../util/platform.js", () => ({
66
- getDataDir: () => testDir,
67
- isMacOS: () => process.platform === "darwin",
68
- isLinux: () => process.platform === "linux",
69
- isWindows: () => process.platform === "win32",
70
- getPidPath: () => join(testDir, "test.pid"),
71
- getDbPath: () => join(testDir, "test.db"),
72
- getLogPath: () => join(testDir, "test.log"),
73
- ensureDataDir: () => {},
74
- }));
75
-
76
- mock.module("../util/logger.js", () => ({
77
- getLogger: () =>
78
- new Proxy({} as Record<string, unknown>, {
79
- get: () => () => {},
80
- }),
81
- }));
82
-
83
- let mockConflictableKinds: string[] = [
84
- "preference",
85
- "profile",
86
- "constraint",
87
- "instruction",
88
- "style",
89
- ];
90
-
91
- mock.module("../config/loader.js", () => ({
92
- getConfig: () => ({
93
- ui: {},
94
-
95
- apiKeys: { anthropic: "test-key" },
96
- memory: {
97
- conflicts: {
98
- conflictableKinds: mockConflictableKinds,
99
- },
100
- },
101
- }),
102
- }));
103
-
104
- import { checkContradictions } from "../memory/contradiction-checker.js";
105
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
106
- import { memoryItemConflicts, memoryItems } from "../memory/schema.js";
107
-
108
- beforeAll(() => {
109
- initializeDb();
110
- });
111
-
112
- beforeEach(() => {
113
- classifyCallCount = 0;
114
- mockConflictableKinds = [
115
- "preference",
116
- "profile",
117
- "constraint",
118
- "instruction",
119
- "style",
120
- ];
121
- const db = getDb();
122
- db.run("DELETE FROM memory_item_conflicts");
123
- db.run("DELETE FROM memory_item_sources");
124
- db.run("DELETE FROM memory_jobs");
125
- db.run("DELETE FROM memory_items");
126
- });
127
-
128
- afterAll(() => {
129
- resetDb();
130
- try {
131
- rmSync(testDir, { recursive: true, force: true });
132
- } catch {
133
- // best effort cleanup
134
- }
135
- });
136
-
137
- function insertMemoryItem(params: {
138
- id: string;
139
- statement: string;
140
- scopeId?: string;
141
- status?: "active" | "pending_clarification";
142
- kind?: string;
143
- }): void {
144
- const now = Date.now();
145
- const db = getDb();
146
- db.insert(memoryItems)
147
- .values({
148
- id: params.id,
149
- kind: params.kind ?? "preference",
150
- subject: "framework preference",
151
- statement: params.statement,
152
- status: params.status ?? "active",
153
- confidence: 0.8,
154
- importance: 0.7,
155
- fingerprint: `fp-${params.id}`,
156
- verificationState: "assistant_inferred",
157
- scopeId: params.scopeId ?? "default",
158
- firstSeenAt: now,
159
- lastSeenAt: now,
160
- })
161
- .run();
162
- }
163
-
164
- describe("checkContradictions", () => {
165
- test("marks candidate pending and writes one conflict row for ambiguous contradictions", async () => {
166
- nextRelationship = "ambiguous_contradiction";
167
- nextExplanation = "Seems contradictory; ask user to choose.";
168
-
169
- insertMemoryItem({
170
- id: "item-existing-ambiguous",
171
- statement: "User prefers React for frontend work.",
172
- scopeId: "workspace-a",
173
- });
174
- insertMemoryItem({
175
- id: "item-candidate-ambiguous",
176
- statement: "User prefers Vue for frontend work.",
177
- scopeId: "workspace-a",
178
- });
179
-
180
- await checkContradictions("item-candidate-ambiguous");
181
-
182
- const db = getDb();
183
- const candidate = db
184
- .select()
185
- .from(memoryItems)
186
- .where(eq(memoryItems.id, "item-candidate-ambiguous"))
187
- .get();
188
- const existing = db
189
- .select()
190
- .from(memoryItems)
191
- .where(eq(memoryItems.id, "item-existing-ambiguous"))
192
- .get();
193
- const conflicts = db.select().from(memoryItemConflicts).all();
194
-
195
- expect(classifyCallCount).toBe(1);
196
- expect(candidate?.status).toBe("pending_clarification");
197
- expect(existing?.invalidAt).toBeNull();
198
- expect(conflicts).toHaveLength(1);
199
- expect(conflicts[0].status).toBe("pending_clarification");
200
- expect(conflicts[0].existingItemId).toBe("item-existing-ambiguous");
201
- expect(conflicts[0].candidateItemId).toBe("item-candidate-ambiguous");
202
- expect(conflicts[0].relationship).toBe("ambiguous_contradiction");
203
- expect(conflicts[0].clarificationQuestion).toContain("Pending conflict:");
204
- expect(conflicts[0].clarificationQuestion).not.toContain(
205
- "I have conflicting notes",
206
- );
207
- expect(conflicts[0].clarificationQuestion).not.toContain(
208
- "Which one is correct?",
209
- );
210
- });
211
-
212
- test("keeps existing contradiction behavior and does not create conflict row", async () => {
213
- nextRelationship = "contradiction";
214
- nextExplanation = "The statements are directly incompatible.";
215
-
216
- insertMemoryItem({
217
- id: "item-existing-contradiction",
218
- statement: "User prefers dark mode.",
219
- });
220
- insertMemoryItem({
221
- id: "item-candidate-contradiction",
222
- statement: "User prefers light mode.",
223
- });
224
-
225
- await checkContradictions("item-candidate-contradiction");
226
-
227
- const db = getDb();
228
- const candidate = db
229
- .select()
230
- .from(memoryItems)
231
- .where(eq(memoryItems.id, "item-candidate-contradiction"))
232
- .get();
233
- const existing = db
234
- .select()
235
- .from(memoryItems)
236
- .where(eq(memoryItems.id, "item-existing-contradiction"))
237
- .get();
238
- const conflicts = db.select().from(memoryItemConflicts).all();
239
-
240
- expect(classifyCallCount).toBe(1);
241
- expect(candidate?.status).toBe("active");
242
- expect(typeof candidate?.validFrom).toBe("number");
243
- expect(typeof existing?.invalidAt).toBe("number");
244
- expect(conflicts).toHaveLength(0);
245
- });
246
-
247
- test("only evaluates contradiction candidates within the same scope", async () => {
248
- nextRelationship = "ambiguous_contradiction";
249
- nextExplanation = "Should not be used for this test.";
250
-
251
- insertMemoryItem({
252
- id: "item-existing-other-scope",
253
- statement: "Use Go for backend services.",
254
- scopeId: "workspace-b",
255
- });
256
- insertMemoryItem({
257
- id: "item-candidate-default-scope",
258
- statement: "Use Rust for backend services.",
259
- scopeId: "workspace-a",
260
- });
261
-
262
- await checkContradictions("item-candidate-default-scope");
263
-
264
- const db = getDb();
265
- const candidate = db
266
- .select()
267
- .from(memoryItems)
268
- .where(eq(memoryItems.id, "item-candidate-default-scope"))
269
- .get();
270
- const conflicts = db.select().from(memoryItemConflicts).all();
271
-
272
- expect(classifyCallCount).toBe(0);
273
- expect(candidate?.status).toBe("active");
274
- expect(conflicts).toHaveLength(0);
275
- });
276
-
277
- test("project kind ambiguous contradiction does not generate pending conflict with default config", async () => {
278
- nextRelationship = "ambiguous_contradiction";
279
- nextExplanation = "Project items may conflict but are not durable.";
280
-
281
- insertMemoryItem({
282
- id: "item-existing-project",
283
- statement: "The backend uses Node.js.",
284
- kind: "project",
285
- });
286
- insertMemoryItem({
287
- id: "item-candidate-project",
288
- statement: "The backend uses Deno.",
289
- kind: "project",
290
- });
291
-
292
- await checkContradictions("item-candidate-project");
293
-
294
- expect(classifyCallCount).toBe(0);
295
- const db = getDb();
296
- const conflicts = db.select().from(memoryItemConflicts).all();
297
- expect(conflicts).toHaveLength(0);
298
- });
299
-
300
- test("skips classification when item kind is not in conflictableKinds", async () => {
301
- mockConflictableKinds = ["instruction", "style"];
302
- nextRelationship = "ambiguous_contradiction";
303
-
304
- insertMemoryItem({
305
- id: "item-existing-ineligible",
306
- statement: "User prefers React for frontend work.",
307
- });
308
- insertMemoryItem({
309
- id: "item-candidate-ineligible",
310
- statement: "User prefers Vue for frontend work.",
311
- });
312
-
313
- await checkContradictions("item-candidate-ineligible");
314
-
315
- expect(classifyCallCount).toBe(0);
316
- const db = getDb();
317
- const conflicts = db.select().from(memoryItemConflicts).all();
318
- expect(conflicts).toHaveLength(0);
319
- });
320
-
321
- test("skips classification when candidate statement contains PR-tracking content", async () => {
322
- nextRelationship = "ambiguous_contradiction";
323
-
324
- insertMemoryItem({
325
- id: "item-existing-pr-tracking",
326
- statement: "Track PR #5526 for review.",
327
- });
328
- insertMemoryItem({
329
- id: "item-candidate-pr-tracking",
330
- statement: "Track PR #5525 for review.",
331
- });
332
-
333
- await checkContradictions("item-candidate-pr-tracking");
334
-
335
- expect(classifyCallCount).toBe(0);
336
- const db = getDb();
337
- const conflicts = db.select().from(memoryItemConflicts).all();
338
- expect(conflicts).toHaveLength(0);
339
- });
340
-
341
- test("durable preference contradiction still runs normal flow", async () => {
342
- nextRelationship = "ambiguous_contradiction";
343
- nextExplanation = "Both are valid preferences that conflict.";
344
-
345
- insertMemoryItem({
346
- id: "item-existing-durable",
347
- statement: "User prefers React for frontend work.",
348
- });
349
- insertMemoryItem({
350
- id: "item-candidate-durable",
351
- statement: "User prefers Vue for frontend work.",
352
- });
353
-
354
- await checkContradictions("item-candidate-durable");
355
-
356
- expect(classifyCallCount).toBe(1);
357
- const db = getDb();
358
- const conflicts = db.select().from(memoryItemConflicts).all();
359
- expect(conflicts).toHaveLength(1);
360
- });
361
- });
@@ -1,211 +0,0 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import {
5
- afterAll,
6
- beforeAll,
7
- beforeEach,
8
- describe,
9
- expect,
10
- mock,
11
- test,
12
- } from "bun:test";
13
-
14
- import { and, eq } from "drizzle-orm";
15
-
16
- const testDir = mkdtempSync(join(tmpdir(), "entity-extractor-test-"));
17
-
18
- mock.module("../util/platform.js", () => ({
19
- getDataDir: () => testDir,
20
- isMacOS: () => process.platform === "darwin",
21
- isLinux: () => process.platform === "linux",
22
- isWindows: () => process.platform === "win32",
23
- getPidPath: () => join(testDir, "test.pid"),
24
- getDbPath: () => join(testDir, "test.db"),
25
- getLogPath: () => join(testDir, "test.log"),
26
- ensureDataDir: () => {},
27
- }));
28
-
29
- mock.module("../util/logger.js", () => ({
30
- getLogger: () =>
31
- new Proxy({} as Record<string, unknown>, {
32
- get: () => () => {},
33
- }),
34
- }));
35
-
36
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
37
- import {
38
- resolveEntityName,
39
- upsertEntity,
40
- upsertEntityRelation,
41
- } from "../memory/entity-extractor.js";
42
- import { memoryEntities, memoryEntityRelations } from "../memory/schema.js";
43
-
44
- describe("entity extractor helpers", () => {
45
- beforeAll(() => {
46
- initializeDb();
47
- });
48
-
49
- beforeEach(() => {
50
- const db = getDb();
51
- db.run("DELETE FROM memory_item_entities");
52
- db.run("DELETE FROM memory_entity_relations");
53
- db.run("DELETE FROM memory_entities");
54
- db.run("DELETE FROM memory_checkpoints");
55
- });
56
-
57
- afterAll(() => {
58
- resetDb();
59
- try {
60
- rmSync(testDir, { recursive: true, force: true });
61
- } catch {
62
- // best effort cleanup
63
- }
64
- });
65
-
66
- test("upsertEntity reuses existing row via alias matching", () => {
67
- const db = getDb();
68
-
69
- const firstId = upsertEntity({
70
- name: "VS Code",
71
- type: "tool",
72
- aliases: ["vscode"],
73
- });
74
- const secondId = upsertEntity({
75
- name: "Visual Studio Code",
76
- type: "tool",
77
- aliases: ["VS Code", "vscode"],
78
- });
79
-
80
- expect(secondId).toBe(firstId);
81
- expect(resolveEntityName("vscode")).toBe(firstId);
82
- expect(resolveEntityName("VS Code")).toBe(firstId);
83
-
84
- const stored = db
85
- .select()
86
- .from(memoryEntities)
87
- .where(eq(memoryEntities.id, firstId))
88
- .get();
89
-
90
- expect(stored).toBeDefined();
91
- expect(stored!.mentionCount).toBe(2);
92
- const aliases = stored!.aliases
93
- ? (JSON.parse(stored!.aliases) as string[])
94
- : [];
95
- expect(aliases).toContain("vscode");
96
- });
97
-
98
- test("upsertEntityRelation merges duplicate edges by uniqueness key", () => {
99
- const db = getDb();
100
- const sourceEntityId = upsertEntity({
101
- name: "Project Atlas",
102
- type: "project",
103
- aliases: ["atlas"],
104
- });
105
- const targetEntityId = upsertEntity({
106
- name: "Qdrant",
107
- type: "tool",
108
- aliases: [],
109
- });
110
-
111
- upsertEntityRelation({
112
- sourceEntityId,
113
- targetEntityId,
114
- relation: "uses",
115
- evidence: "Project Atlas uses Qdrant for memory search",
116
- seenAt: 1_700_000_000_000,
117
- });
118
- upsertEntityRelation({
119
- sourceEntityId,
120
- targetEntityId,
121
- relation: "uses",
122
- evidence: null,
123
- seenAt: 1_700_000_100_000,
124
- });
125
- upsertEntityRelation({
126
- sourceEntityId,
127
- targetEntityId,
128
- relation: "uses",
129
- evidence: "Atlas still depends on Qdrant",
130
- seenAt: 1_700_000_200_000,
131
- });
132
-
133
- const relationRows = db
134
- .select()
135
- .from(memoryEntityRelations)
136
- .where(
137
- and(
138
- eq(memoryEntityRelations.sourceEntityId, sourceEntityId),
139
- eq(memoryEntityRelations.targetEntityId, targetEntityId),
140
- eq(memoryEntityRelations.relation, "uses"),
141
- ),
142
- )
143
- .all();
144
-
145
- expect(relationRows.length).toBe(1);
146
- expect(relationRows[0].firstSeenAt).toBe(1_700_000_000_000);
147
- expect(relationRows[0].lastSeenAt).toBe(1_700_000_200_000);
148
- expect(relationRows[0].evidence).toBe("Atlas still depends on Qdrant");
149
- });
150
-
151
- test("resolveEntityName prefers exact canonical name over alias match", () => {
152
- const db = getDb();
153
- const now = Date.now();
154
-
155
- // Insert two distinct entities directly to avoid upsertEntity dedupe.
156
- const aliasEntityId = crypto.randomUUID();
157
- db.insert(memoryEntities)
158
- .values({
159
- id: aliasEntityId,
160
- name: "React Native",
161
- type: "tool",
162
- aliases: JSON.stringify(["React", "RN"]),
163
- description: null,
164
- firstSeenAt: now,
165
- lastSeenAt: now,
166
- mentionCount: 1,
167
- })
168
- .run();
169
-
170
- const canonicalEntityId = crypto.randomUUID();
171
- db.insert(memoryEntities)
172
- .values({
173
- id: canonicalEntityId,
174
- name: "React",
175
- type: "tool",
176
- aliases: JSON.stringify(["ReactJS"]),
177
- description: null,
178
- firstSeenAt: now,
179
- lastSeenAt: now,
180
- mentionCount: 1,
181
- })
182
- .run();
183
-
184
- // resolveEntityName("React") should return the entity whose canonical
185
- // name is "React", not the one that merely lists it as an alias.
186
- expect(resolveEntityName("React")).toBe(canonicalEntityId);
187
- expect(resolveEntityName("react")).toBe(canonicalEntityId);
188
- // Alias-only lookups still work
189
- expect(resolveEntityName("RN")).toBe(aliasEntityId);
190
- expect(resolveEntityName("ReactJS")).toBe(canonicalEntityId);
191
- });
192
-
193
- test("upsertEntityRelation drops self-edges", () => {
194
- const db = getDb();
195
- const entityId = upsertEntity({
196
- name: "Sidd",
197
- type: "person",
198
- aliases: [],
199
- });
200
-
201
- upsertEntityRelation({
202
- sourceEntityId: entityId,
203
- targetEntityId: entityId,
204
- relation: "collaborates_with",
205
- evidence: "self edge should not be stored",
206
- });
207
-
208
- const relationRows = db.select().from(memoryEntityRelations).all();
209
- expect(relationRows.length).toBe(0);
210
- });
211
- });