@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,165 +0,0 @@
1
- import { getLogger } from "../util/logger.js";
2
- import { rawGet, rawRun } from "./db.js";
3
-
4
- const log = getLogger("fts-reconciler");
5
-
6
- export interface FtsReconciliationResult {
7
- table: string;
8
- baseCount: number;
9
- ftsCount: number;
10
- missingInserted: number;
11
- orphansRemoved: number;
12
- staleRefreshed: number;
13
- }
14
-
15
- /**
16
- * Reconcile a single FTS index against its base table. Detects missing entries
17
- * (rows in the base table with no corresponding FTS row) and orphaned entries
18
- * (FTS rows whose base table row no longer exists), then repairs both.
19
- *
20
- * This is lighter than a full rebuild — it only touches the delta rather than
21
- * wiping and re-inserting the entire index.
22
- */
23
- function reconcileTable(opts: {
24
- ftsTable: string;
25
- ftsIdColumn: string;
26
- ftsContentColumn: string;
27
- baseTable: string;
28
- baseIdColumn: string;
29
- baseContentColumn: string;
30
- }): FtsReconciliationResult {
31
- const {
32
- ftsTable,
33
- ftsIdColumn,
34
- ftsContentColumn,
35
- baseTable,
36
- baseIdColumn,
37
- baseContentColumn,
38
- } = opts;
39
-
40
- const baseCount = (
41
- rawGet<{ c: number }>(`SELECT COUNT(*) AS c FROM ${baseTable}`) ?? { c: 0 }
42
- ).c;
43
- const ftsCount = (
44
- rawGet<{ c: number }>(`SELECT COUNT(*) AS c FROM ${ftsTable}`) ?? { c: 0 }
45
- ).c;
46
-
47
- // Find base table rows missing from the FTS index
48
- const missingInserted = rawRun(/*sql*/ `
49
- INSERT INTO ${ftsTable}(${ftsIdColumn}, ${ftsContentColumn})
50
- SELECT b.${baseIdColumn}, b.${baseContentColumn}
51
- FROM ${baseTable} b
52
- LEFT JOIN ${ftsTable} f ON f.${ftsIdColumn} = b.${baseIdColumn}
53
- WHERE f.${ftsIdColumn} IS NULL
54
- `);
55
-
56
- // Find FTS rows whose base table row no longer exists
57
- const orphansRemoved = rawRun(/*sql*/ `
58
- DELETE FROM ${ftsTable}
59
- WHERE ${ftsIdColumn} IN (
60
- SELECT f.${ftsIdColumn}
61
- FROM ${ftsTable} f
62
- LEFT JOIN ${baseTable} b ON b.${baseIdColumn} = f.${ftsIdColumn}
63
- WHERE b.${baseIdColumn} IS NULL
64
- )
65
- `);
66
-
67
- // Refresh FTS rows whose content is stale (base row was updated but the
68
- // update trigger didn't fire or was missing). Delete-then-insert is the
69
- // standard FTS5 update pattern.
70
- const staleDeleted = rawRun(/*sql*/ `
71
- DELETE FROM ${ftsTable}
72
- WHERE ${ftsIdColumn} IN (
73
- SELECT f.${ftsIdColumn}
74
- FROM ${ftsTable} f
75
- JOIN ${baseTable} b ON b.${baseIdColumn} = f.${ftsIdColumn}
76
- WHERE b.${baseContentColumn} IS NOT f.${ftsContentColumn}
77
- )
78
- `);
79
- if (staleDeleted > 0) {
80
- rawRun(/*sql*/ `
81
- INSERT INTO ${ftsTable}(${ftsIdColumn}, ${ftsContentColumn})
82
- SELECT b.${baseIdColumn}, b.${baseContentColumn}
83
- FROM ${baseTable} b
84
- LEFT JOIN ${ftsTable} f ON f.${ftsIdColumn} = b.${baseIdColumn}
85
- WHERE f.${ftsIdColumn} IS NULL
86
- `);
87
- }
88
-
89
- return {
90
- table: ftsTable,
91
- baseCount,
92
- ftsCount,
93
- missingInserted,
94
- orphansRemoved,
95
- staleRefreshed: staleDeleted,
96
- };
97
- }
98
-
99
- /**
100
- * Reconcile all FTS indexes. Returns results for each table so callers can
101
- * inspect what was repaired.
102
- */
103
- export function reconcileFtsIndexes(): FtsReconciliationResult[] {
104
- const results: FtsReconciliationResult[] = [];
105
- const errors: unknown[] = [];
106
-
107
- // memory_segment_fts tracks memory_segments
108
- try {
109
- const memResult = reconcileTable({
110
- ftsTable: "memory_segment_fts",
111
- ftsIdColumn: "segment_id",
112
- ftsContentColumn: "text",
113
- baseTable: "memory_segments",
114
- baseIdColumn: "id",
115
- baseContentColumn: "text",
116
- });
117
- results.push(memResult);
118
- if (
119
- memResult.missingInserted > 0 ||
120
- memResult.orphansRemoved > 0 ||
121
- memResult.staleRefreshed > 0
122
- ) {
123
- log.info(memResult, "Reconciled memory_segment_fts");
124
- } else {
125
- log.debug(memResult, "memory_segment_fts is in sync");
126
- }
127
- } catch (err) {
128
- log.error({ err }, "Failed to reconcile memory_segment_fts");
129
- errors.push(err);
130
- }
131
-
132
- // messages_fts tracks messages
133
- try {
134
- const msgResult = reconcileTable({
135
- ftsTable: "messages_fts",
136
- ftsIdColumn: "message_id",
137
- ftsContentColumn: "content",
138
- baseTable: "messages",
139
- baseIdColumn: "id",
140
- baseContentColumn: "content",
141
- });
142
- results.push(msgResult);
143
- if (
144
- msgResult.missingInserted > 0 ||
145
- msgResult.orphansRemoved > 0 ||
146
- msgResult.staleRefreshed > 0
147
- ) {
148
- log.info(msgResult, "Reconciled messages_fts");
149
- } else {
150
- log.debug(msgResult, "messages_fts is in sync");
151
- }
152
- } catch (err) {
153
- log.error({ err }, "Failed to reconcile messages_fts");
154
- errors.push(err);
155
- }
156
-
157
- if (errors.length > 0) {
158
- throw new AggregateError(
159
- errors,
160
- `FTS reconciliation failed for ${errors.length} table(s)`,
161
- );
162
- }
163
-
164
- return results;
165
- }
@@ -1,200 +0,0 @@
1
- import { and, asc, eq, inArray, lt, ne } from "drizzle-orm";
2
-
3
- import type { AssistantConfig } from "../../config/types.js";
4
- import { getLogger } from "../../util/logger.js";
5
- import { resolveConflictClarification } from "../clarification-resolver.js";
6
- import {
7
- areStatementsCoherent,
8
- computeConflictRelevance,
9
- looksLikeClarificationReply,
10
- shouldAttemptConflictResolution,
11
- } from "../conflict-intent.js";
12
- import {
13
- isConflictKindPairEligible,
14
- isConflictUserEvidenced,
15
- isStatementConflictEligible,
16
- } from "../conflict-policy.js";
17
- import {
18
- applyConflictResolution,
19
- listPendingConflictDetails,
20
- resolveConflict,
21
- } from "../conflict-store.js";
22
- import { getDb } from "../db.js";
23
- import { asPositiveMs, asString } from "../job-utils.js";
24
- import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
25
- import { extractTextFromStoredMessageContent } from "../message-content.js";
26
- import { memoryItemConflicts, messages } from "../schema.js";
27
-
28
- const log = getLogger("memory-jobs-worker");
29
-
30
- const CLEANUP_BATCH_LIMIT = 250;
31
-
32
- export async function resolvePendingConflictsForMessageJob(
33
- job: MemoryJob,
34
- config: AssistantConfig,
35
- ): Promise<void> {
36
- if (!config.memory.conflicts.enabled) return;
37
- const messageId = asString(job.payload.messageId);
38
- if (!messageId) return;
39
- const scopeId = asString(job.payload.scopeId) ?? "default";
40
- const db = getDb();
41
- const message = db
42
- .select({
43
- id: messages.id,
44
- role: messages.role,
45
- content: messages.content,
46
- createdAt: messages.createdAt,
47
- })
48
- .from(messages)
49
- .where(eq(messages.id, messageId))
50
- .get();
51
- if (!message || message.role !== "user") return;
52
-
53
- const userMessage = extractTextFromStoredMessageContent(
54
- message.content,
55
- ).trim();
56
- if (userMessage.length === 0) return;
57
- const clarificationReply = looksLikeClarificationReply(userMessage);
58
- if (!clarificationReply) return;
59
-
60
- const pending = listPendingConflictDetails(scopeId, 25);
61
-
62
- // Dismiss non-actionable conflicts (kind or statement policy)
63
- const conflictableKinds = config.memory.conflicts.conflictableKinds;
64
- for (const conflict of pending) {
65
- const kindEligible = isConflictKindPairEligible(
66
- conflict.existingKind,
67
- conflict.candidateKind,
68
- { conflictableKinds },
69
- );
70
- if (
71
- !kindEligible ||
72
- !isStatementConflictEligible(
73
- conflict.existingKind,
74
- conflict.existingStatement,
75
- { conflictableKinds },
76
- ) ||
77
- !isStatementConflictEligible(
78
- conflict.candidateKind,
79
- conflict.candidateStatement,
80
- { conflictableKinds },
81
- )
82
- ) {
83
- resolveConflict(conflict.id, {
84
- status: "dismissed",
85
- resolutionNote: "Dismissed by conflict policy (transient/non-durable).",
86
- });
87
- } else if (
88
- !isConflictUserEvidenced(
89
- conflict.existingVerificationState,
90
- conflict.candidateVerificationState,
91
- )
92
- ) {
93
- resolveConflict(conflict.id, {
94
- status: "dismissed",
95
- resolutionNote:
96
- "Dismissed by conflict policy (no user-evidenced provenance).",
97
- });
98
- } else if (
99
- !areStatementsCoherent(
100
- conflict.existingStatement,
101
- conflict.candidateStatement,
102
- )
103
- ) {
104
- resolveConflict(conflict.id, {
105
- status: "dismissed",
106
- resolutionNote:
107
- "Dismissed by conflict policy (incoherent — zero statement overlap).",
108
- });
109
- }
110
- }
111
-
112
- // Re-fetch after dismissal
113
- const actionablePending = listPendingConflictDetails(scopeId, 25);
114
- const eligible = actionablePending.filter(
115
- (conflict) => conflict.createdAt <= message.createdAt,
116
- );
117
- if (eligible.length === 0) return;
118
- const candidates = eligible.filter((conflict) => {
119
- const relevance = computeConflictRelevance(userMessage, conflict);
120
- return shouldAttemptConflictResolution({
121
- clarificationReply,
122
- relevance,
123
- });
124
- });
125
- if (candidates.length === 0) return;
126
-
127
- let resolvedCount = 0;
128
- for (const conflict of candidates) {
129
- const resolution = await resolveConflictClarification(
130
- {
131
- existingStatement: conflict.existingStatement,
132
- candidateStatement: conflict.candidateStatement,
133
- userMessage,
134
- },
135
- { timeoutMs: config.memory.conflicts.resolverLlmTimeoutMs },
136
- );
137
- if (resolution.resolution === "still_unclear") continue;
138
- const resolved = applyConflictResolution({
139
- conflictId: conflict.id,
140
- resolution: resolution.resolution,
141
- mergedStatement:
142
- resolution.resolution === "merge" ? resolution.resolvedStatement : null,
143
- resolutionNote: `Background message resolver (${resolution.strategy}): ${resolution.explanation}`,
144
- });
145
- if (resolved) resolvedCount += 1;
146
- }
147
-
148
- log.debug(
149
- {
150
- messageId,
151
- scopeId,
152
- pendingConflicts: pending.length,
153
- eligibleConflicts: eligible.length,
154
- candidateConflicts: candidates.length,
155
- resolvedConflicts: resolvedCount,
156
- },
157
- "Processed pending conflict resolution job",
158
- );
159
- }
160
-
161
- export function cleanupResolvedConflictsJob(
162
- job: MemoryJob,
163
- config: AssistantConfig,
164
- ): void {
165
- const db = getDb();
166
- const retentionMs =
167
- asPositiveMs(job.payload.retentionMs) ??
168
- config.memory.cleanup.resolvedConflictRetentionMs;
169
- const cutoff = Date.now() - retentionMs;
170
- const stale = db
171
- .select({ id: memoryItemConflicts.id })
172
- .from(memoryItemConflicts)
173
- .where(
174
- and(
175
- ne(memoryItemConflicts.status, "pending_clarification"),
176
- lt(memoryItemConflicts.resolvedAt, cutoff),
177
- ),
178
- )
179
- .orderBy(asc(memoryItemConflicts.resolvedAt), asc(memoryItemConflicts.id))
180
- .limit(CLEANUP_BATCH_LIMIT)
181
- .all();
182
- if (stale.length === 0) return;
183
-
184
- const ids = stale.map((row) => row.id);
185
- db.delete(memoryItemConflicts)
186
- .where(inArray(memoryItemConflicts.id, ids))
187
- .run();
188
- if (stale.length === CLEANUP_BATCH_LIMIT) {
189
- enqueueMemoryJob("cleanup_resolved_conflicts", { retentionMs });
190
- }
191
-
192
- log.debug(
193
- {
194
- removedConflicts: stale.length,
195
- retentionMs,
196
- cutoff,
197
- },
198
- "Cleaned up resolved memory conflicts",
199
- );
200
- }
@@ -1,195 +0,0 @@
1
- import { and, desc, eq, inArray, isNull } from "drizzle-orm";
2
-
3
- import { getConfig } from "../config/loader.js";
4
- import { estimateTextTokens } from "../context/token-estimator.js";
5
- import { getDb } from "./db.js";
6
- import { memoryItems } from "./schema.js";
7
-
8
- const PROFILE_KIND_ALLOWLIST = [
9
- "profile",
10
- "preference",
11
- "constraint",
12
- "instruction",
13
- "style",
14
- ] as const;
15
-
16
- const TRUST_RANK: Record<string, number> = {
17
- user_confirmed: 3,
18
- user_reported: 2,
19
- assistant_inferred: 1,
20
- };
21
-
22
- export interface CompileProfileOptions {
23
- scopeId?: string;
24
- maxInjectTokensOverride?: number;
25
- /** When true and scopeId is not 'default', query both the given scope and 'default'. */
26
- includeDefaultFallback?: boolean;
27
- }
28
-
29
- export interface CompiledProfile {
30
- text: string;
31
- sourceCount: number;
32
- selectedCount: number;
33
- budgetTokens: number;
34
- tokenEstimate: number;
35
- }
36
-
37
- interface ProfileCandidate {
38
- kind: string;
39
- subject: string;
40
- statement: string;
41
- verificationState: string;
42
- confidence: number;
43
- importance: number | null;
44
- lastSeenAt: number;
45
- firstSeenAt: number;
46
- scopeId: string;
47
- }
48
-
49
- export function compileDynamicProfile(
50
- options?: CompileProfileOptions,
51
- ): CompiledProfile {
52
- const config = getConfig();
53
- const profileConfig = config.memory.profile;
54
- const scopeId = options?.scopeId ?? "default";
55
- const budgetTokens = Math.max(
56
- 0,
57
- Math.floor(
58
- options?.maxInjectTokensOverride ?? profileConfig.maxInjectTokens,
59
- ),
60
- );
61
- if (!profileConfig.enabled || budgetTokens <= 0) {
62
- return {
63
- text: "",
64
- sourceCount: 0,
65
- selectedCount: 0,
66
- budgetTokens,
67
- tokenEstimate: 0,
68
- };
69
- }
70
-
71
- const db = getDb();
72
- const shouldFallback =
73
- options?.includeDefaultFallback === true && scopeId !== "default";
74
- const scopeFilter = shouldFallback
75
- ? inArray(memoryItems.scopeId, [scopeId, "default"])
76
- : eq(memoryItems.scopeId, scopeId);
77
- const rows = db
78
- .select({
79
- kind: memoryItems.kind,
80
- subject: memoryItems.subject,
81
- statement: memoryItems.statement,
82
- verificationState: memoryItems.verificationState,
83
- confidence: memoryItems.confidence,
84
- importance: memoryItems.importance,
85
- lastSeenAt: memoryItems.lastSeenAt,
86
- firstSeenAt: memoryItems.firstSeenAt,
87
- scopeId: memoryItems.scopeId,
88
- })
89
- .from(memoryItems)
90
- .where(
91
- and(
92
- scopeFilter,
93
- eq(memoryItems.status, "active"),
94
- isNull(memoryItems.invalidAt),
95
- inArray(memoryItems.kind, [...PROFILE_KIND_ALLOWLIST]),
96
- ),
97
- )
98
- .orderBy(desc(memoryItems.lastSeenAt))
99
- .all();
100
-
101
- const nowMs = Date.now();
102
- const trusted = rows
103
- .filter((row) => TRUST_RANK[row.verificationState] !== undefined)
104
- .sort((a, b) =>
105
- compareProfileCandidates(
106
- a,
107
- b,
108
- nowMs,
109
- shouldFallback ? scopeId : undefined,
110
- ),
111
- );
112
-
113
- const selectedLines: string[] = [];
114
- const seenKeys = new Set<string>();
115
- for (const candidate of trusted) {
116
- const subject = normalizeWhitespace(candidate.subject, 80);
117
- const statement = normalizeWhitespace(candidate.statement, 220);
118
- if (!subject || !statement) continue;
119
- const dedupeKey = `${candidate.kind}|${subject.toLowerCase()}`;
120
- if (seenKeys.has(dedupeKey)) continue;
121
-
122
- const line = `- ${subject}: ${statement}`;
123
- const tentative = renderProfileText([...selectedLines, line]);
124
- if (estimateTextTokens(tentative) > budgetTokens) continue;
125
- seenKeys.add(dedupeKey);
126
- selectedLines.push(line);
127
- }
128
-
129
- const text = renderProfileText(selectedLines);
130
- const tokenEstimate = text ? estimateTextTokens(text) : 0;
131
- return {
132
- text,
133
- sourceCount: trusted.length,
134
- selectedCount: selectedLines.length,
135
- budgetTokens,
136
- tokenEstimate,
137
- };
138
- }
139
-
140
- /** Half-life for recency decay — items seen this many ms ago score ~0.5. (30 days) */
141
- const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
142
-
143
- function recencyScore(lastSeenAt: number, nowMs: number): number {
144
- const ageMs = Math.max(0, nowMs - lastSeenAt);
145
- return Math.pow(0.5, ageMs / RECENCY_HALF_LIFE_MS);
146
- }
147
-
148
- function candidateRankScore(c: ProfileCandidate, nowMs: number): number {
149
- const importance = c.importance ?? 0.5;
150
- const recency = recencyScore(c.lastSeenAt, nowMs);
151
- // Use weighted additive formula: importance dominates, recency is a minor boost
152
- // This preserves high-importance items even when they haven't been seen recently
153
- return importance * 0.7 + recency * 0.3;
154
- }
155
-
156
- function compareProfileCandidates(
157
- left: ProfileCandidate,
158
- right: ProfileCandidate,
159
- nowMs: number,
160
- /** When set, entries matching this scope sort before 'default' entries. */
161
- preferredScopeId?: string,
162
- ): number {
163
- // Scoped entries beat default fallback entries so local overrides win deduplication
164
- if (preferredScopeId !== undefined) {
165
- const leftIsPreferred = left.scopeId === preferredScopeId;
166
- const rightIsPreferred = right.scopeId === preferredScopeId;
167
- if (leftIsPreferred && !rightIsPreferred) return -1;
168
- if (!leftIsPreferred && rightIsPreferred) return 1;
169
- }
170
-
171
- const trustDelta =
172
- (TRUST_RANK[right.verificationState] ?? 0) -
173
- (TRUST_RANK[left.verificationState] ?? 0);
174
- if (trustDelta !== 0) return trustDelta;
175
-
176
- const scoreDelta =
177
- candidateRankScore(right, nowMs) - candidateRankScore(left, nowMs);
178
- if (scoreDelta !== 0) return scoreDelta;
179
-
180
- const confidenceDelta = right.confidence - left.confidence;
181
- if (confidenceDelta !== 0) return confidenceDelta;
182
-
183
- return right.firstSeenAt - left.firstSeenAt;
184
- }
185
-
186
- function normalizeWhitespace(input: string, maxLength: number): string {
187
- return input.replace(/\s+/g, " ").trim().slice(0, maxLength);
188
- }
189
-
190
- function renderProfileText(lines: string[]): string {
191
- if (lines.length === 0) return "";
192
- return ["<dynamic-user-profile>", ...lines, "</dynamic-user-profile>"].join(
193
- "\n",
194
- );
195
- }
@@ -1,117 +0,0 @@
1
- import { createHash } from "crypto";
2
-
3
- import type {
4
- MemoryRecallOptions,
5
- MemoryRecallResult,
6
- } from "./search/types.js";
7
-
8
- /**
9
- * In-memory cache for memory recall results.
10
- *
11
- * The full retrieval pipeline (FTS5 + Qdrant + entity graph + RRF merge) is
12
- * expensive. When the same query is issued multiple turns in a row (common
13
- * when the conversation context hasn't changed), we can serve the cached
14
- * result instantly.
15
- *
16
- * Invalidation: a monotonic version counter is bumped whenever new memory
17
- * is indexed (segments, items, embeddings). Cache entries are only valid
18
- * when their version matches the current global version.
19
- */
20
-
21
- interface CacheEntry {
22
- version: number;
23
- createdAt: number;
24
- result: MemoryRecallResult;
25
- }
26
-
27
- const MAX_ENTRIES = 32;
28
- const TTL_MS = 60_000; // 60 seconds
29
-
30
- let _version = 0;
31
- const _cache = new Map<string, CacheEntry>();
32
-
33
- /** Bump the global memory version, invalidating all cached recall results. */
34
- export function bumpMemoryVersion(): void {
35
- _version++;
36
- }
37
-
38
- /** Return the current memory version (for snapshot-based staleness checks). */
39
- export function getMemoryVersion(): number {
40
- return _version;
41
- }
42
-
43
- /** Build a deterministic cache key from the recall inputs. */
44
- function buildCacheKey(
45
- query: string,
46
- conversationId: string,
47
- options?: MemoryRecallOptions,
48
- configFingerprint?: string,
49
- ): string {
50
- const parts = [
51
- query,
52
- conversationId,
53
- options?.scopeId ?? "",
54
- options?.scopePolicyOverride
55
- ? `${options.scopePolicyOverride.scopeId}:${options.scopePolicyOverride.fallbackToDefault}`
56
- : "",
57
- options?.excludeMessageIds
58
- ? [...options.excludeMessageIds].sort().join(",")
59
- : "",
60
- options?.maxInjectTokensOverride != null
61
- ? String(options.maxInjectTokensOverride)
62
- : "",
63
- configFingerprint ?? "",
64
- ];
65
- return createHash("sha256").update(parts.join("\0")).digest("hex");
66
- }
67
-
68
- /** Look up a cached recall result. Returns undefined on miss or stale entry. */
69
- export function getCachedRecall(
70
- query: string,
71
- conversationId: string,
72
- options?: MemoryRecallOptions,
73
- configFingerprint?: string,
74
- ): MemoryRecallResult | undefined {
75
- const key = buildCacheKey(query, conversationId, options, configFingerprint);
76
- const entry = _cache.get(key);
77
- if (!entry) return undefined;
78
- if (entry.version !== _version || Date.now() - entry.createdAt > TTL_MS) {
79
- _cache.delete(key);
80
- return undefined;
81
- }
82
- // Move to end of Map iteration order so it's treated as most-recently-used
83
- _cache.delete(key);
84
- _cache.set(key, entry);
85
- return entry.result;
86
- }
87
-
88
- /**
89
- * Store a recall result in the cache. Evicts least-recently-used entries when full.
90
- *
91
- * When `snapshotVersion` is provided, the entry is only stored if the
92
- * snapshot still matches the current global version — this prevents a
93
- * stale result from being cached under a version that was bumped while
94
- * the retrieval pipeline was in flight.
95
- */
96
- export function setCachedRecall(
97
- query: string,
98
- conversationId: string,
99
- options: MemoryRecallOptions | undefined,
100
- result: MemoryRecallResult,
101
- snapshotVersion?: number,
102
- configFingerprint?: string,
103
- ): void {
104
- // If a snapshot version was provided, only cache when it still matches
105
- // the current version — otherwise the result may be stale.
106
- if (snapshotVersion !== undefined && snapshotVersion !== _version) return;
107
-
108
- const key = buildCacheKey(query, conversationId, options, configFingerprint);
109
-
110
- // Evict oldest entries if at capacity
111
- if (_cache.size >= MAX_ENTRIES && !_cache.has(key)) {
112
- const oldest = _cache.keys().next().value;
113
- if (oldest !== undefined) _cache.delete(oldest);
114
- }
115
-
116
- _cache.set(key, { version: _version, createdAt: Date.now(), result });
117
- }