@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,508 +0,0 @@
1
- import { eq } from "drizzle-orm";
2
-
3
- import { getConfig } from "../config/loader.js";
4
- import {
5
- createTimeout,
6
- extractToolUse,
7
- getConfiguredProvider,
8
- userMessage,
9
- } from "../providers/provider-send-message.js";
10
- import { getLogger } from "../util/logger.js";
11
- import { truncate } from "../util/truncate.js";
12
- import { areStatementsCoherent } from "./conflict-intent.js";
13
- import {
14
- isConflictKindEligible,
15
- isStatementConflictEligible,
16
- } from "./conflict-policy.js";
17
- import { createOrUpdatePendingConflict } from "./conflict-store.js";
18
- import { getDb, getSqlite, rawAll } from "./db.js";
19
- import { enqueueMemoryJob } from "./jobs-store.js";
20
- import { memoryItems } from "./schema.js";
21
- import { clampUnitInterval } from "./validation.js";
22
-
23
- const log = getLogger("memory-contradiction-checker");
24
-
25
- const CONTRADICTION_LLM_TIMEOUT_MS = 15_000;
26
-
27
- type Relationship =
28
- | "contradiction"
29
- | "update"
30
- | "complement"
31
- | "ambiguous_contradiction";
32
-
33
- interface ClassifyResult {
34
- relationship: Relationship;
35
- explanation: string;
36
- }
37
-
38
- const CONTRADICTION_SYSTEM_PROMPT = `You are a memory consistency checker. Given two statements about the same subject, determine their relationship.
39
-
40
- Classify the relationship as one of:
41
- - "contradiction": The new statement directly contradicts the old statement. They cannot both be true at the same time. Example: "User prefers dark mode" vs "User prefers light mode".
42
- - "update": The new statement provides updated or more specific information that supersedes the old statement, but does not contradict it. Example: "User works at Acme" vs "User works at Acme as a senior engineer".
43
- - "complement": The statements are compatible and provide different, non-overlapping information. Both can coexist. Example: "User likes TypeScript" vs "User prefers functional programming".
44
- - "ambiguous_contradiction": The statements appear to conflict, but there is not enough confidence to invalidate either statement without user clarification.
45
-
46
- Be conservative: only classify as "contradiction" when the statements are genuinely incompatible. Prefer "complement" when in doubt.`;
47
-
48
- /**
49
- * Check a newly extracted memory item against existing items for contradictions.
50
- * Searches for existing active items with similar subject/statement, then uses
51
- * LLM to classify the relationship and handle accordingly.
52
- */
53
- export async function checkContradictions(newItemId: string): Promise<void> {
54
- const db = getDb();
55
- const newItem = db
56
- .select()
57
- .from(memoryItems)
58
- .where(eq(memoryItems.id, newItemId))
59
- .get();
60
-
61
- if (!newItem || newItem.status !== "active") {
62
- log.debug(
63
- { newItemId },
64
- "Skipping contradiction check — item not found or not active",
65
- );
66
- return;
67
- }
68
-
69
- // Find existing active items with similar kind + subject
70
- const candidates = findSimilarItems(newItem);
71
- if (candidates.length === 0) {
72
- log.debug(
73
- { newItemId, subject: newItem.subject },
74
- "No similar items found for contradiction check",
75
- );
76
- return;
77
- }
78
-
79
- const provider = getConfiguredProvider();
80
- if (!provider) {
81
- log.debug("Configured provider unavailable for contradiction checking");
82
- return;
83
- }
84
-
85
- const config = getConfig();
86
-
87
- if (!isConflictKindEligible(newItem.kind, config.memory.conflicts)) {
88
- log.debug(
89
- { newItemId, kind: newItem.kind },
90
- "Skipping contradiction check — kind not eligible for conflicts",
91
- );
92
- return;
93
- }
94
-
95
- // Skip if the new item's statement is transient/non-durable
96
- if (
97
- !isStatementConflictEligible(
98
- newItem.kind,
99
- newItem.statement,
100
- config.memory.conflicts,
101
- )
102
- ) {
103
- log.debug(
104
- { newItemId, kind: newItem.kind },
105
- "Skipping contradiction check — statement is transient or non-durable",
106
- );
107
- return;
108
- }
109
-
110
- for (const existing of candidates) {
111
- // Skip candidate if its statement is transient/non-durable
112
- if (
113
- !isStatementConflictEligible(
114
- existing.kind,
115
- existing.statement,
116
- config.memory.conflicts,
117
- )
118
- ) {
119
- log.debug(
120
- { existingId: existing.id },
121
- "Skipping candidate — statement is transient or non-durable",
122
- );
123
- continue;
124
- }
125
-
126
- // Skip pairs with zero topical overlap — they are not real contradictions
127
- if (!areStatementsCoherent(existing.statement, newItem.statement)) {
128
- log.debug(
129
- { existingId: existing.id, newId: newItem.id },
130
- "Skipping candidate — zero statement overlap (incoherent pair)",
131
- );
132
- continue;
133
- }
134
-
135
- try {
136
- const result = await classifyRelationship(existing, newItem);
137
- const mutated = handleRelationship(result, existing, newItem);
138
- // Only stop when the new item itself was actually invalidated (update case)
139
- // or gated (ambiguous_contradiction). For contradiction, the old item is
140
- // invalidated but the new item remains active and should continue to be
141
- // checked against remaining candidates. Skip the break when the transaction
142
- // detected stale data and performed no mutation.
143
- if (
144
- mutated &&
145
- (result.relationship === "update" ||
146
- result.relationship === "ambiguous_contradiction")
147
- )
148
- break;
149
- } catch (err) {
150
- const message = err instanceof Error ? err.message : String(err);
151
- log.warn(
152
- { err: message, newItemId, existingId: existing.id },
153
- "Contradiction classification failed for pair",
154
- );
155
- }
156
- }
157
- }
158
-
159
- interface MemoryItemRow {
160
- id: string;
161
- kind: string;
162
- subject: string;
163
- statement: string;
164
- status: string;
165
- confidence: number;
166
- importance: number | null;
167
- scopeId: string;
168
- lastSeenAt: number;
169
- }
170
-
171
- /**
172
- * Find existing active items that are similar to the given item.
173
- * Uses LIKE queries on subject and keyword overlap on statement.
174
- */
175
- function findSimilarItems(item: MemoryItemRow): MemoryItemRow[] {
176
- // Extract significant words from subject for LIKE matching
177
- const subjectWords = item.subject
178
- .toLowerCase()
179
- .split(/[^a-z0-9_.-]+/g)
180
- .filter((w) => w.length >= 3);
181
-
182
- // Extract significant words from statement for additional matching
183
- const statementWords = item.statement
184
- .toLowerCase()
185
- .split(/[^a-z0-9_.-]+/g)
186
- .filter((w) => w.length >= 3);
187
-
188
- if (subjectWords.length === 0 && statementWords.length === 0) return [];
189
-
190
- // Build LIKE clauses for subject similarity
191
- const likeClauses: string[] = [];
192
- for (const word of subjectWords) {
193
- const escaped = escapeSqlLike(word);
194
- likeClauses.push(`LOWER(subject) LIKE '%${escaped}%'`);
195
- }
196
-
197
- // Also match on statement keywords (top 5 longest words for specificity)
198
- const topStatementWords = statementWords
199
- .sort((a, b) => b.length - a.length)
200
- .slice(0, 5);
201
- for (const word of topStatementWords) {
202
- const escaped = escapeSqlLike(word);
203
- likeClauses.push(`LOWER(statement) LIKE '%${escaped}%'`);
204
- }
205
-
206
- if (likeClauses.length === 0) return [];
207
-
208
- const sqlQuery = `
209
- SELECT id, kind, subject, statement, status, confidence, importance, scope_id, last_seen_at
210
- FROM memory_items
211
- WHERE status = 'active'
212
- AND invalid_at IS NULL
213
- AND kind = ?
214
- AND id <> ?
215
- AND scope_id = ?
216
- AND (${likeClauses.join(" OR ")})
217
- ORDER BY last_seen_at DESC
218
- LIMIT 10
219
- `;
220
-
221
- try {
222
- interface SimilarItemRow {
223
- id: string;
224
- kind: string;
225
- subject: string;
226
- statement: string;
227
- status: string;
228
- confidence: number;
229
- importance: number | null;
230
- scope_id: string;
231
- last_seen_at: number;
232
- }
233
- const rows = rawAll<SimilarItemRow>(
234
- sqlQuery,
235
- item.kind,
236
- item.id,
237
- item.scopeId,
238
- );
239
-
240
- return rows.map((row) => ({
241
- id: row.id,
242
- kind: row.kind,
243
- subject: row.subject,
244
- statement: row.statement,
245
- status: row.status,
246
- confidence: row.confidence,
247
- importance: row.importance,
248
- scopeId: row.scope_id,
249
- lastSeenAt: row.last_seen_at,
250
- }));
251
- } catch (err) {
252
- log.warn({ err }, "Failed to search for similar memory items");
253
- return [];
254
- }
255
- }
256
-
257
- /**
258
- * Use LLM to classify the relationship between two memory items.
259
- */
260
- async function classifyRelationship(
261
- existingItem: MemoryItemRow,
262
- newItem: MemoryItemRow,
263
- ): Promise<ClassifyResult> {
264
- const provider = getConfiguredProvider()!;
265
-
266
- const userContent = [
267
- `Subject: ${newItem.subject}`,
268
- "",
269
- `Old statement: ${existingItem.statement}`,
270
- `New statement: ${newItem.statement}`,
271
- ].join("\n");
272
-
273
- const { signal, cleanup } = createTimeout(CONTRADICTION_LLM_TIMEOUT_MS);
274
- try {
275
- const response = await provider.sendMessage(
276
- [userMessage(userContent)],
277
- [
278
- {
279
- name: "classify_relationship",
280
- description:
281
- "Classify the relationship between two memory statements",
282
- input_schema: {
283
- type: "object" as const,
284
- properties: {
285
- relationship: {
286
- type: "string",
287
- enum: [
288
- "contradiction",
289
- "update",
290
- "complement",
291
- "ambiguous_contradiction",
292
- ],
293
- description:
294
- "The relationship between the old and new statements",
295
- },
296
- explanation: {
297
- type: "string",
298
- description:
299
- "Brief explanation of why this relationship was chosen",
300
- },
301
- },
302
- required: ["relationship", "explanation"],
303
- },
304
- },
305
- ],
306
- CONTRADICTION_SYSTEM_PROMPT,
307
- {
308
- config: {
309
- modelIntent: "latency-optimized",
310
- max_tokens: 256,
311
- tool_choice: { type: "tool" as const, name: "classify_relationship" },
312
- },
313
- signal,
314
- },
315
- );
316
- cleanup();
317
-
318
- const toolBlock = extractToolUse(response);
319
- if (!toolBlock) {
320
- throw new Error("No tool_use block in contradiction check response");
321
- }
322
-
323
- const input = toolBlock.input as {
324
- relationship?: string;
325
- explanation?: string;
326
- };
327
- const relationship = input.relationship as Relationship;
328
- if (
329
- ![
330
- "contradiction",
331
- "update",
332
- "complement",
333
- "ambiguous_contradiction",
334
- ].includes(relationship)
335
- ) {
336
- throw new Error(`Invalid relationship type: ${relationship}`);
337
- }
338
-
339
- return {
340
- relationship,
341
- explanation: truncate(String(input.explanation ?? ""), 500, ""),
342
- };
343
- } finally {
344
- cleanup();
345
- }
346
- }
347
-
348
- /**
349
- * Handle the classified relationship between an existing and new memory item.
350
- *
351
- * Wrapped in a SQLite transaction so that multi-row mutations (e.g. invalidating
352
- * the old item AND setting validFrom on the new one) are atomic. The transaction
353
- * also re-verifies both items are still active before mutating, preventing a
354
- * TOCTOU race when multiple workers process contradictions concurrently.
355
- */
356
- function handleRelationship(
357
- result: ClassifyResult,
358
- existingItem: MemoryItemRow,
359
- newItem: MemoryItemRow,
360
- ): boolean {
361
- if (result.relationship === "complement") {
362
- log.debug(
363
- {
364
- existingId: existingItem.id,
365
- newId: newItem.id,
366
- explanation: result.explanation,
367
- },
368
- "Complement detected — keeping both items",
369
- );
370
- return false;
371
- }
372
-
373
- return getSqlite()
374
- .transaction(() => {
375
- const db = getDb();
376
- const now = Date.now();
377
-
378
- // Re-check both items inside the transaction to guard against concurrent mutations
379
- const freshExisting = db
380
- .select()
381
- .from(memoryItems)
382
- .where(eq(memoryItems.id, existingItem.id))
383
- .get();
384
- const freshNew = db
385
- .select()
386
- .from(memoryItems)
387
- .where(eq(memoryItems.id, newItem.id))
388
- .get();
389
-
390
- if (
391
- !freshExisting ||
392
- freshExisting.status !== "active" ||
393
- freshExisting.invalidAt != null
394
- ) {
395
- log.debug(
396
- { existingId: existingItem.id },
397
- "Existing item no longer active — skipping",
398
- );
399
- return false;
400
- }
401
- if (
402
- !freshNew ||
403
- (freshNew.status !== "active" &&
404
- result.relationship !== "ambiguous_contradiction") ||
405
- freshNew.invalidAt != null
406
- ) {
407
- log.debug(
408
- { newId: newItem.id },
409
- "New item no longer active — skipping",
410
- );
411
- return false;
412
- }
413
-
414
- switch (result.relationship) {
415
- case "contradiction": {
416
- log.info(
417
- {
418
- existingId: existingItem.id,
419
- newId: newItem.id,
420
- explanation: result.explanation,
421
- },
422
- "Contradiction detected — invalidating old item",
423
- );
424
- db.update(memoryItems)
425
- .set({ invalidAt: now })
426
- .where(eq(memoryItems.id, existingItem.id))
427
- .run();
428
- db.update(memoryItems)
429
- .set({ validFrom: now })
430
- .where(eq(memoryItems.id, newItem.id))
431
- .run();
432
- return true;
433
- }
434
- case "update": {
435
- log.debug(
436
- {
437
- existingId: existingItem.id,
438
- newId: newItem.id,
439
- explanation: result.explanation,
440
- },
441
- "Update detected — merging into existing item",
442
- );
443
- db.update(memoryItems)
444
- .set({
445
- statement: newItem.statement,
446
- lastSeenAt: Math.max(
447
- freshExisting.lastSeenAt,
448
- freshNew!.lastSeenAt,
449
- ),
450
- confidence: clampUnitInterval(
451
- Math.max(freshExisting.confidence, freshNew!.confidence),
452
- ),
453
- })
454
- .where(eq(memoryItems.id, existingItem.id))
455
- .run();
456
- enqueueMemoryJob("embed_item", { itemId: existingItem.id });
457
- db.update(memoryItems)
458
- .set({ invalidAt: now })
459
- .where(eq(memoryItems.id, newItem.id))
460
- .run();
461
- return true;
462
- }
463
- case "ambiguous_contradiction": {
464
- log.info(
465
- {
466
- existingId: existingItem.id,
467
- newId: newItem.id,
468
- explanation: result.explanation,
469
- },
470
- "Ambiguous contradiction detected — gating candidate pending clarification",
471
- );
472
- db.update(memoryItems)
473
- .set({ status: "pending_clarification" })
474
- .where(eq(memoryItems.id, newItem.id))
475
- .run();
476
- createOrUpdatePendingConflict({
477
- scopeId: newItem.scopeId,
478
- existingItemId: existingItem.id,
479
- candidateItemId: newItem.id,
480
- relationship: "ambiguous_contradiction",
481
- clarificationQuestion: buildClarificationQuestion(
482
- existingItem.statement,
483
- newItem.statement,
484
- ),
485
- });
486
- return true;
487
- }
488
- default:
489
- return false;
490
- }
491
- })
492
- .immediate();
493
- }
494
-
495
- function escapeSqlLike(s: string): string {
496
- return s.replace(/'/g, "''").replace(/%/g, "").replace(/_/g, "");
497
- }
498
-
499
- function buildClarificationQuestion(
500
- existingStatement: string,
501
- candidateStatement: string,
502
- ): string {
503
- const normalize = (input: string): string =>
504
- truncate(input.replace(/\s+/g, " ").trim(), 180, "");
505
- return `Pending conflict: "${normalize(
506
- existingStatement,
507
- )}" vs "${normalize(candidateStatement)}"`;
508
- }