@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
@@ -2,26 +2,15 @@ import { and, asc, eq, inArray, lt } from "drizzle-orm";
2
2
 
3
3
  import type { AssistantConfig } from "../../config/types.js";
4
4
  import { getLogger } from "../../util/logger.js";
5
- import { checkContradictions } from "../contradiction-checker.js";
6
5
  import { getDb, rawAll, rawRun } from "../db.js";
7
- import { asPositiveMs, asString } from "../job-utils.js";
6
+ import { asPositiveMs } from "../job-utils.js";
8
7
  import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
9
- import {
10
- memoryEmbeddings,
11
- memoryItemEntities,
12
- memoryItems,
13
- } from "../schema.js";
8
+ import { memoryEmbeddings, memoryItems } from "../schema.js";
14
9
 
15
10
  const log = getLogger("memory-jobs-worker");
16
11
 
17
12
  const CLEANUP_BATCH_LIMIT = 250;
18
13
 
19
- export async function checkContradictionsJob(job: MemoryJob): Promise<void> {
20
- const itemId = asString(job.payload.itemId);
21
- if (!itemId) return;
22
- await checkContradictions(itemId);
23
- }
24
-
25
14
  export function cleanupStaleSupersededItemsJob(
26
15
  job: MemoryJob,
27
16
  config: AssistantConfig,
@@ -46,9 +35,6 @@ export function cleanupStaleSupersededItemsJob(
46
35
  if (stale.length === 0) return;
47
36
 
48
37
  const ids = stale.map((row) => row.id);
49
- db.delete(memoryItemEntities)
50
- .where(inArray(memoryItemEntities.memoryItemId, ids))
51
- .run();
52
38
  db.delete(memoryEmbeddings)
53
39
  .where(
54
40
  and(
@@ -1,21 +1,11 @@
1
1
  import { eq } from "drizzle-orm";
2
2
 
3
- import { getConfig } from "../../config/loader.js";
4
- import type { AssistantConfig } from "../../config/types.js";
5
3
  import { getLogger } from "../../util/logger.js";
6
4
  import { getDb } from "../db.js";
7
- import {
8
- extractEntitiesWithLLM,
9
- linkMemoryItemToEntity,
10
- resolveEntityName,
11
- upsertEntity,
12
- upsertEntityRelation,
13
- } from "../entity-extractor.js";
14
5
  import { extractAndUpsertMemoryItemsForMessage } from "../items-extractor.js";
15
6
  import { asString } from "../job-utils.js";
16
- import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
17
- import { extractTextFromStoredMessageContent } from "../message-content.js";
18
- import { memoryItemSources, messages } from "../schema.js";
7
+ import type { MemoryJob } from "../jobs-store.js";
8
+ import { messages } from "../schema.js";
19
9
  import { isConversationFailed } from "../task-memory-cleanup.js";
20
10
 
21
11
  const log = getLogger("memory-jobs-worker");
@@ -47,130 +37,4 @@ export async function extractItemsJob(job: MemoryJob): Promise<void> {
47
37
  scopeId,
48
38
  msg?.conversationId,
49
39
  );
50
- // Queue entity extraction for this message after items are extracted
51
- const config = getConfig();
52
- if (config.memory.entity.enabled) {
53
- enqueueMemoryJob("extract_entities", { messageId, scopeId });
54
- }
55
- }
56
-
57
- export async function extractEntitiesJob(
58
- job: MemoryJob,
59
- config: AssistantConfig,
60
- ): Promise<void> {
61
- const messageId = asString(job.payload.messageId);
62
- if (!messageId) return;
63
-
64
- const db = getDb();
65
-
66
- // Guard: skip entity extraction for failed conversations. Entity extraction
67
- // jobs are enqueued by extractItemsJob after items are extracted; while new
68
- // jobs won't be queued (extractItemsJob returns early for failed convos),
69
- // any entity jobs enqueued before the failure marker was set must be caught.
70
- const msgRow = db
71
- .select({ conversationId: messages.conversationId })
72
- .from(messages)
73
- .where(eq(messages.id, messageId))
74
- .get();
75
- if (msgRow && isConversationFailed(msgRow.conversationId)) {
76
- log.info(
77
- { messageId, conversationId: msgRow.conversationId },
78
- "Skipping entity extraction for failed conversation",
79
- );
80
- return;
81
- }
82
-
83
- const message = db
84
- .select({
85
- id: messages.id,
86
- content: messages.content,
87
- })
88
- .from(messages)
89
- .where(eq(messages.id, messageId))
90
- .get();
91
- if (!message) return;
92
-
93
- const text = extractTextFromStoredMessageContent(message.content);
94
- if (text.trim().length < 15) return;
95
-
96
- const extracted = await extractEntitiesWithLLM(text, config.memory.entity);
97
- const entities = extracted.entities;
98
- const relations = extracted.relations;
99
- if (entities.length === 0 && relations.length === 0) return;
100
-
101
- // Find all memory items linked to this message via memory_item_sources
102
- const linkedItems = db
103
- .select({ memoryItemId: memoryItemSources.memoryItemId })
104
- .from(memoryItemSources)
105
- .where(eq(memoryItemSources.messageId, messageId))
106
- .all();
107
- const itemIds = linkedItems.map((row) => row.memoryItemId);
108
- const entityNameToId = new Map<string, string>();
109
-
110
- for (const entity of entities) {
111
- const entityId = upsertEntity(entity);
112
- entityNameToId.set(entity.name.toLowerCase(), entityId);
113
- for (const alias of entity.aliases) {
114
- entityNameToId.set(alias.toLowerCase(), entityId);
115
- }
116
- // Link all memory items from this message to the entity
117
- for (const itemId of itemIds) {
118
- linkMemoryItemToEntity(itemId, entityId);
119
- }
120
- }
121
-
122
- const relationTelemetry = {
123
- attempted: 0,
124
- parsed: relations.length,
125
- persisted: 0,
126
- dropped: 0,
127
- };
128
-
129
- if (config.memory.entity.extractRelations.enabled && relations.length > 0) {
130
- const seenRelationKeys = new Set<string>();
131
- for (const relation of relations) {
132
- relationTelemetry.attempted += 1;
133
- const sourceLookup = relation.sourceEntityName.toLowerCase();
134
- const targetLookup = relation.targetEntityName.toLowerCase();
135
- const sourceEntityId =
136
- entityNameToId.get(sourceLookup) ??
137
- resolveEntityName(relation.sourceEntityName);
138
- const targetEntityId =
139
- entityNameToId.get(targetLookup) ??
140
- resolveEntityName(relation.targetEntityName);
141
- if (
142
- !sourceEntityId ||
143
- !targetEntityId ||
144
- sourceEntityId === targetEntityId
145
- ) {
146
- relationTelemetry.dropped += 1;
147
- continue;
148
- }
149
-
150
- const dedupeKey = `${sourceEntityId}|${targetEntityId}|${relation.relation}`;
151
- if (seenRelationKeys.has(dedupeKey)) continue;
152
- seenRelationKeys.add(dedupeKey);
153
-
154
- upsertEntityRelation({
155
- sourceEntityId,
156
- targetEntityId,
157
- relation: relation.relation,
158
- evidence: relation.evidence,
159
- });
160
- relationTelemetry.persisted += 1;
161
- }
162
- }
163
-
164
- log.debug(
165
- {
166
- messageId,
167
- entityCount: entities.length,
168
- linkedItems: itemIds.length,
169
- relationAttempts: relationTelemetry.attempted,
170
- relationParsed: relationTelemetry.parsed,
171
- relationPersisted: relationTelemetry.persisted,
172
- relationDropped: relationTelemetry.dropped,
173
- },
174
- "Extracted entity graph from message",
175
- );
176
40
  }
@@ -2,7 +2,7 @@ import { eq, like } from "drizzle-orm";
2
2
 
3
3
  import { getConfig } from "../../config/loader.js";
4
4
  import { getLogger } from "../../util/logger.js";
5
- import { getDb, rawExec } from "../db.js";
5
+ import { getDb } from "../db.js";
6
6
  import { selectedBackendSupportsMultimodal } from "../embedding-backend.js";
7
7
  import { asString, BackendUnavailableError } from "../job-utils.js";
8
8
  import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
@@ -22,11 +22,6 @@ const log = getLogger("memory-jobs-worker");
22
22
 
23
23
  export function rebuildIndexJob(): void {
24
24
  const db = getDb();
25
- rawExec(/*sql*/ `DELETE FROM memory_segment_fts`);
26
- rawExec(/*sql*/ `
27
- INSERT INTO memory_segment_fts(segment_id, text)
28
- SELECT id, text FROM memory_segments
29
- `);
30
25
  db.delete(memoryEmbeddings).run();
31
26
 
32
27
  const items = db
@@ -1,4 +1,4 @@
1
- import { and, desc, eq, gte, isNull, lt, sql } from "drizzle-orm";
1
+ import { and, desc, eq, sql } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
4
  import type { AssistantConfig } from "../../config/types.js";
@@ -12,14 +12,9 @@ import {
12
12
  import { getLogger } from "../../util/logger.js";
13
13
  import { getConversationMemoryScopeId } from "../conversation-crud.js";
14
14
  import { getDb } from "../db.js";
15
- import {
16
- asString,
17
- currentMonthWindow,
18
- currentWeekWindow,
19
- truncate,
20
- } from "../job-utils.js";
15
+ import { asString, truncate } from "../job-utils.js";
21
16
  import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
22
- import { memoryItems, memorySegments, memorySummaries } from "../schema.js";
17
+ import { memorySegments, memorySummaries } from "../schema.js";
23
18
 
24
19
  const log = getLogger("memory-jobs-worker");
25
20
 
@@ -38,18 +33,6 @@ const CONVERSATION_SUMMARY_SYSTEM_PROMPT = [
38
33
  "- If updating an existing summary with new data, merge new information and remove anything that was superseded.",
39
34
  ].join("\n");
40
35
 
41
- const GLOBAL_SUMMARY_SYSTEM_PROMPT = [
42
- "You are a memory summarization system. Your job is to synthesize a higher-level summary from multiple conversation summaries and memory items.",
43
- "",
44
- "Guidelines:",
45
- "- Identify recurring themes, cross-cutting decisions, and persistent user preferences.",
46
- "- Highlight the most important facts, active projects, and ongoing concerns.",
47
- "- De-duplicate information that appears across multiple conversations.",
48
- "- Use concise sections with bullet points.",
49
- "- Target 400-600 tokens. Be dense but readable.",
50
- "- If updating an existing summary with new data, merge new information and remove anything that was superseded.",
51
- ].join("\n");
52
-
53
36
  export async function buildConversationSummaryJob(
54
37
  job: MemoryJob,
55
38
  config: AssistantConfig,
@@ -144,134 +127,6 @@ export async function buildConversationSummaryJob(
144
127
  }
145
128
  }
146
129
 
147
- export async function buildGlobalSummaryJob(
148
- scope: "weekly_global" | "monthly_global",
149
- config: AssistantConfig,
150
- ): Promise<void> {
151
- const db = getDb();
152
- const now = new Date();
153
- const { startMs, endMs, scopeKey } =
154
- scope === "weekly_global"
155
- ? currentWeekWindow(now)
156
- : currentMonthWindow(now);
157
-
158
- const items = db
159
- .select()
160
- .from(memoryItems)
161
- .where(
162
- and(
163
- eq(memoryItems.status, "active"),
164
- isNull(memoryItems.invalidAt),
165
- eq(memoryItems.scopeId, "default"),
166
- gte(memoryItems.lastSeenAt, startMs),
167
- lt(memoryItems.lastSeenAt, endMs),
168
- ),
169
- )
170
- .orderBy(desc(memoryItems.lastSeenAt))
171
- .limit(80)
172
- .all();
173
-
174
- // Gather conversation summaries from this period for higher-level synthesis
175
- const convSummaries = db
176
- .select()
177
- .from(memorySummaries)
178
- .where(
179
- and(
180
- eq(memorySummaries.scope, "conversation"),
181
- eq(memorySummaries.scopeId, "default"),
182
- gte(memorySummaries.endAt, startMs),
183
- lt(memorySummaries.startAt, endMs),
184
- ),
185
- )
186
- .orderBy(desc(memorySummaries.endAt))
187
- .limit(20)
188
- .all();
189
-
190
- if (items.length === 0 && convSummaries.length === 0) return;
191
-
192
- // Build input for LLM: conversation summaries + active items
193
- const parts: string[] = [];
194
- if (convSummaries.length > 0) {
195
- parts.push("## Conversation Summaries");
196
- for (const cs of convSummaries) {
197
- parts.push(`### ${cs.scopeKey}\n${truncate(cs.summary, 600)}`);
198
- }
199
- }
200
- if (items.length > 0) {
201
- parts.push("## Active Memory Items");
202
- for (const item of items.slice(0, 40)) {
203
- parts.push(
204
- `- [${item.kind}] ${item.subject}: ${truncate(item.statement, 180)}`,
205
- );
206
- }
207
- }
208
- const inputText = parts.join("\n\n");
209
-
210
- const existing = db
211
- .select()
212
- .from(memorySummaries)
213
- .where(
214
- and(
215
- eq(memorySummaries.scope, scope),
216
- eq(memorySummaries.scopeKey, scopeKey),
217
- ),
218
- )
219
- .get();
220
-
221
- const label = scope === "weekly_global" ? "weekly" : "monthly";
222
- const summaryText = await summarizeWithLLM(
223
- config,
224
- GLOBAL_SUMMARY_SYSTEM_PROMPT,
225
- existing?.summary ?? null,
226
- inputText,
227
- label,
228
- );
229
-
230
- const ts = Date.now();
231
- const summaryId = existing?.id ?? uuid();
232
- db.insert(memorySummaries)
233
- .values({
234
- id: summaryId,
235
- scope,
236
- scopeKey,
237
- summary: summaryText,
238
- tokenEstimate: estimateTextTokens(summaryText),
239
- version: (existing?.version ?? 0) + 1,
240
- startAt: startMs,
241
- endAt: endMs,
242
- createdAt: ts,
243
- updatedAt: ts,
244
- })
245
- .onConflictDoUpdate({
246
- target: [memorySummaries.scope, memorySummaries.scopeKey],
247
- set: {
248
- summary: summaryText,
249
- tokenEstimate: estimateTextTokens(summaryText),
250
- version: sql`${memorySummaries.version} + 1`,
251
- startAt: startMs,
252
- endAt: endMs,
253
- updatedAt: ts,
254
- },
255
- })
256
- .run();
257
-
258
- // Re-query to get the actual persisted row ID — during a race the ON CONFLICT
259
- // path keeps the winner's ID, not the pre-generated UUID from the loser.
260
- const actualRow = db
261
- .select({ id: memorySummaries.id })
262
- .from(memorySummaries)
263
- .where(
264
- and(
265
- eq(memorySummaries.scope, scope),
266
- eq(memorySummaries.scopeKey, scopeKey),
267
- ),
268
- )
269
- .get();
270
- if (actualRow) {
271
- enqueueMemoryJob("embed_summary", { summaryId: actualRow.id });
272
- }
273
- }
274
-
275
130
  async function summarizeWithLLM(
276
131
  config: AssistantConfig,
277
132
  systemPrompt: string,
@@ -8,6 +8,7 @@ import { getLogger } from "../util/logger.js";
8
8
  import { getDb } from "./db.js";
9
9
  import {
10
10
  embedWithBackend,
11
+ generateSparseEmbedding,
11
12
  getMemoryBackendStatus,
12
13
  } from "./embedding-backend.js";
13
14
  import type { EmbeddingInput } from "./embedding-types.js";
@@ -201,6 +202,14 @@ export async function embedAndUpsert(
201
202
  ? normalized.text
202
203
  : `[${normalized.type}:${normalized.mimeType}]`;
203
204
 
205
+ // Generate sparse embedding from the same source text used for dense embedding.
206
+ // For non-text (media) inputs, sparse vectors are skipped since tokenization
207
+ // only applies to text content.
208
+ const sparseVector =
209
+ normalized.type === "text"
210
+ ? generateSparseEmbedding(normalized.text)
211
+ : undefined;
212
+
204
213
  // Persist embedding in SQLite for cross-restart cache
205
214
  const now = Date.now();
206
215
  try {
@@ -249,12 +258,18 @@ export async function embedAndUpsert(
249
258
  try {
250
259
  const modality = normalized.type;
251
260
  await withQdrantBreaker(() =>
252
- qdrant.upsert(targetType, targetId, vector, {
253
- text: payloadText,
254
- modality,
255
- created_at: (extraPayload?.created_at as number) ?? now,
256
- ...(extraPayload as Record<string, unknown> | undefined),
257
- }),
261
+ qdrant.upsert(
262
+ targetType,
263
+ targetId,
264
+ vector,
265
+ {
266
+ text: payloadText,
267
+ modality,
268
+ created_at: (extraPayload?.created_at as number) ?? now,
269
+ ...(extraPayload as Record<string, unknown> | undefined),
270
+ },
271
+ sparseVector,
272
+ ),
258
273
  );
259
274
  } catch (err) {
260
275
  log.warn(
@@ -265,56 +280,3 @@ export async function embedAndUpsert(
265
280
  }
266
281
  }
267
282
 
268
- // ── Time window utilities ──────────────────────────────────────────
269
-
270
- export function currentWeekWindow(now: Date): {
271
- scopeKey: string;
272
- startMs: number;
273
- endMs: number;
274
- } {
275
- const day = (now.getUTCDay() + 6) % 7;
276
- const start = new Date(
277
- Date.UTC(
278
- now.getUTCFullYear(),
279
- now.getUTCMonth(),
280
- now.getUTCDate() - day,
281
- 0,
282
- 0,
283
- 0,
284
- 0,
285
- ),
286
- );
287
- const end = new Date(start);
288
- end.setUTCDate(end.getUTCDate() + 7);
289
- const scopeKey = `${start.getUTCFullYear()}-W${weekNumber(start)
290
- .toString()
291
- .padStart(2, "0")}`;
292
- return { scopeKey, startMs: start.getTime(), endMs: end.getTime() };
293
- }
294
-
295
- export function currentMonthWindow(now: Date): {
296
- scopeKey: string;
297
- startMs: number;
298
- endMs: number;
299
- } {
300
- const start = new Date(
301
- Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0),
302
- );
303
- const end = new Date(
304
- Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0),
305
- );
306
- const scopeKey = `${start.getUTCFullYear()}-${String(
307
- start.getUTCMonth() + 1,
308
- ).padStart(2, "0")}`;
309
- return { scopeKey, startMs: start.getTime(), endMs: end.getTime() };
310
- }
311
-
312
- function weekNumber(date: Date): number {
313
- const tmp = new Date(
314
- Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
315
- );
316
- const dayNum = tmp.getUTCDay() || 7;
317
- tmp.setUTCDate(tmp.getUTCDate() + 4 - dayNum);
318
- const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
319
- return Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
320
- }
@@ -3,7 +3,7 @@ import { v4 as uuid } from "uuid";
3
3
 
4
4
  import { getLogger } from "../util/logger.js";
5
5
  import { truncate } from "../util/truncate.js";
6
- import { getDb, rawAll, rawChanges, rawGet } from "./db.js";
6
+ import { getDb, rawAll, rawChanges } from "./db.js";
7
7
  import { memoryJobs } from "./schema.js";
8
8
 
9
9
  const log = getLogger("memory-jobs-store");
@@ -14,18 +14,14 @@ export type MemoryJobType =
14
14
  | "embed_summary"
15
15
  | "extract_items"
16
16
  | "extract_entities"
17
- | "resolve_pending_conflicts_for_message"
18
- | "cleanup_resolved_conflicts"
19
17
  | "cleanup_stale_superseded_items"
20
18
  | "prune_old_conversations"
21
19
  | "backfill_entity_relations"
22
- | "check_contradictions"
23
20
  | "refresh_weekly_summary"
24
21
  | "refresh_monthly_summary"
25
22
  | "build_conversation_summary"
26
23
  | "backfill"
27
24
  | "rebuild_index"
28
- | "reconcile_fts"
29
25
  | "delete_qdrant_vectors"
30
26
  | "media_processing"
31
27
  | "embed_media"
@@ -83,143 +79,6 @@ export function enqueueMemoryJob(
83
79
  return id;
84
80
  }
85
81
 
86
- /**
87
- * Ensure there is only one pending relation backfill orchestrator job.
88
- * If `force=true` arrives while a pending job exists, its payload is upgraded.
89
- */
90
- export function enqueueBackfillEntityRelationsJob(force = false): string {
91
- const db = getDb();
92
- const now = Date.now();
93
- const existing = db
94
- .select()
95
- .from(memoryJobs)
96
- .where(
97
- and(
98
- eq(memoryJobs.type, "backfill_entity_relations"),
99
- eq(memoryJobs.status, "pending"),
100
- ),
101
- )
102
- .orderBy(asc(memoryJobs.createdAt))
103
- .get();
104
-
105
- if (existing) {
106
- if (force) {
107
- let payload: Record<string, unknown> = {};
108
- try {
109
- payload = JSON.parse(existing.payload) as Record<string, unknown>;
110
- } catch {
111
- payload = {};
112
- }
113
- if (payload.force !== true) {
114
- db.update(memoryJobs)
115
- .set({
116
- payload: JSON.stringify({ ...payload, force: true }),
117
- updatedAt: now,
118
- })
119
- .where(eq(memoryJobs.id, existing.id))
120
- .run();
121
- }
122
- }
123
- return existing.id;
124
- }
125
-
126
- return enqueueMemoryJob("backfill_entity_relations", { force });
127
- }
128
-
129
- export function enqueueResolvePendingConflictsForMessageJob(
130
- messageId: string,
131
- scopeId = "default",
132
- dbOverride?: Parameters<ReturnType<typeof getDb>["transaction"]>[0] extends (
133
- tx: infer T,
134
- ) => unknown
135
- ? T
136
- : never,
137
- ): string {
138
- const normalizedMessageId = messageId.trim();
139
- if (!normalizedMessageId) {
140
- throw new Error(
141
- "enqueueResolvePendingConflictsForMessageJob requires a non-empty messageId",
142
- );
143
- }
144
- const normalizedScopeId = scopeId.trim() || "default";
145
- // Dedup check always uses root db since tx doesn't expose raw client
146
- const existing = rawGet<{ id: string }>(
147
- `
148
- SELECT id
149
- FROM memory_jobs
150
- WHERE type = 'resolve_pending_conflicts_for_message'
151
- AND status IN ('pending', 'running')
152
- AND json_extract(payload, '$.messageId') = ?
153
- AND COALESCE(json_extract(payload, '$.scopeId'), 'default') = ?
154
- ORDER BY created_at ASC
155
- LIMIT 1
156
- `,
157
- normalizedMessageId,
158
- normalizedScopeId,
159
- );
160
- if (existing?.id) return existing.id;
161
-
162
- return enqueueMemoryJob(
163
- "resolve_pending_conflicts_for_message",
164
- {
165
- messageId: normalizedMessageId,
166
- scopeId: normalizedScopeId,
167
- },
168
- Date.now(),
169
- dbOverride,
170
- );
171
- }
172
-
173
- export function enqueueCleanupResolvedConflictsJob(
174
- retentionMs?: number,
175
- ): string {
176
- const db = getDb();
177
- const now = Date.now();
178
- const existing = db
179
- .select()
180
- .from(memoryJobs)
181
- .where(
182
- and(
183
- eq(memoryJobs.type, "cleanup_resolved_conflicts"),
184
- inArray(memoryJobs.status, ["pending", "running"]),
185
- ),
186
- )
187
- .orderBy(asc(memoryJobs.createdAt))
188
- .get();
189
- if (existing) {
190
- if (
191
- existing.status === "pending" &&
192
- typeof retentionMs === "number" &&
193
- Number.isFinite(retentionMs) &&
194
- retentionMs > 0
195
- ) {
196
- let payload: Record<string, unknown> = {};
197
- try {
198
- payload = JSON.parse(existing.payload) as Record<string, unknown>;
199
- } catch {
200
- payload = {};
201
- }
202
- if (payload.retentionMs !== retentionMs) {
203
- db.update(memoryJobs)
204
- .set({
205
- payload: JSON.stringify({ ...payload, retentionMs }),
206
- updatedAt: now,
207
- })
208
- .where(eq(memoryJobs.id, existing.id))
209
- .run();
210
- }
211
- }
212
- return existing.id;
213
- }
214
- const payload =
215
- typeof retentionMs === "number" &&
216
- Number.isFinite(retentionMs) &&
217
- retentionMs > 0
218
- ? { retentionMs }
219
- : {};
220
- return enqueueMemoryJob("cleanup_resolved_conflicts", payload);
221
- }
222
-
223
82
  export function enqueueCleanupStaleSupersededItemsJob(
224
83
  retentionMs?: number,
225
84
  ): string {
@@ -319,23 +178,6 @@ export function enqueuePruneOldConversationsJob(
319
178
  return enqueueMemoryJob("prune_old_conversations", payload);
320
179
  }
321
180
 
322
- export function enqueueReconcileFtsJob(): string {
323
- const db = getDb();
324
- const existing = db
325
- .select()
326
- .from(memoryJobs)
327
- .where(
328
- and(
329
- eq(memoryJobs.type, "reconcile_fts"),
330
- inArray(memoryJobs.status, ["pending", "running"]),
331
- ),
332
- )
333
- .orderBy(asc(memoryJobs.createdAt))
334
- .get();
335
- if (existing) return existing.id;
336
- return enqueueMemoryJob("reconcile_fts", {});
337
- }
338
-
339
181
  export function claimMemoryJobs(limit: number): MemoryJob[] {
340
182
  if (limit <= 0) return [];
341
183
  const db = getDb();