@vellumai/assistant 0.3.3 → 0.3.5

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 (163) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +45 -18
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +391 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +397 -135
  12. package/src/__tests__/channel-approvals.test.ts +99 -3
  13. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  14. package/src/__tests__/channel-guardian.test.ts +261 -22
  15. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  16. package/src/__tests__/config-schema.test.ts +2 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-lifecycle.test.ts +636 -0
  19. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  20. package/src/__tests__/entity-search.test.ts +615 -0
  21. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  22. package/src/__tests__/handlers-twilio-config.test.ts +480 -0
  23. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  25. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  26. package/src/__tests__/run-orchestrator.test.ts +22 -0
  27. package/src/__tests__/secret-scanner.test.ts +223 -0
  28. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  29. package/src/__tests__/shell-parser-property.test.ts +357 -2
  30. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  31. package/src/__tests__/system-prompt.test.ts +25 -1
  32. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  33. package/src/__tests__/twilio-routes.test.ts +39 -3
  34. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  35. package/src/__tests__/user-reference.test.ts +68 -0
  36. package/src/__tests__/web-search.test.ts +1 -1
  37. package/src/__tests__/work-item-output.test.ts +110 -0
  38. package/src/calls/call-domain.ts +8 -5
  39. package/src/calls/call-orchestrator.ts +85 -22
  40. package/src/calls/twilio-config.ts +17 -11
  41. package/src/calls/twilio-rest.ts +276 -0
  42. package/src/calls/twilio-routes.ts +39 -1
  43. package/src/cli/map.ts +6 -0
  44. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  45. package/src/commands/cc-command-registry.ts +14 -1
  46. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  47. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  48. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  49. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  50. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  51. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  52. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  53. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  54. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  55. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  56. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  57. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  58. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  59. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  60. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  61. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  62. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  63. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  64. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  65. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  66. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  67. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  68. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  69. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  70. package/src/config/bundled-skills/messaging/SKILL.md +24 -5
  71. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  72. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  73. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  74. package/src/config/defaults.ts +2 -1
  75. package/src/config/schema.ts +9 -3
  76. package/src/config/skills.ts +5 -32
  77. package/src/config/system-prompt.ts +40 -0
  78. package/src/config/templates/IDENTITY.md +2 -2
  79. package/src/config/user-reference.ts +29 -0
  80. package/src/config/vellum-skills/catalog.json +58 -0
  81. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  82. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  83. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  84. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
  86. package/src/daemon/auth-manager.ts +103 -0
  87. package/src/daemon/computer-use-session.ts +8 -1
  88. package/src/daemon/config-watcher.ts +253 -0
  89. package/src/daemon/handlers/config.ts +819 -22
  90. package/src/daemon/handlers/dictation.ts +182 -0
  91. package/src/daemon/handlers/identity.ts +14 -23
  92. package/src/daemon/handlers/index.ts +2 -0
  93. package/src/daemon/handlers/sessions.ts +2 -0
  94. package/src/daemon/handlers/shared.ts +3 -0
  95. package/src/daemon/handlers/skills.ts +6 -7
  96. package/src/daemon/handlers/work-items.ts +15 -7
  97. package/src/daemon/ipc-contract-inventory.json +10 -0
  98. package/src/daemon/ipc-contract.ts +114 -4
  99. package/src/daemon/ipc-handler.ts +87 -0
  100. package/src/daemon/lifecycle.ts +18 -4
  101. package/src/daemon/ride-shotgun-handler.ts +11 -1
  102. package/src/daemon/server.ts +111 -504
  103. package/src/daemon/session-agent-loop.ts +10 -15
  104. package/src/daemon/session-runtime-assembly.ts +115 -44
  105. package/src/daemon/session-tool-setup.ts +2 -0
  106. package/src/daemon/session.ts +19 -2
  107. package/src/inbound/public-ingress-urls.ts +3 -3
  108. package/src/memory/channel-guardian-store.ts +2 -1
  109. package/src/memory/db-connection.ts +28 -0
  110. package/src/memory/db-init.ts +1163 -0
  111. package/src/memory/db.ts +2 -2007
  112. package/src/memory/embedding-backend.ts +79 -11
  113. package/src/memory/indexer.ts +2 -0
  114. package/src/memory/job-handlers/media-processing.ts +100 -0
  115. package/src/memory/job-utils.ts +64 -4
  116. package/src/memory/jobs-store.ts +2 -1
  117. package/src/memory/jobs-worker.ts +11 -1
  118. package/src/memory/media-store.ts +759 -0
  119. package/src/memory/recall-cache.ts +107 -0
  120. package/src/memory/retriever.ts +36 -2
  121. package/src/memory/schema-migration.ts +984 -0
  122. package/src/memory/schema.ts +99 -0
  123. package/src/memory/search/entity.ts +208 -25
  124. package/src/memory/search/ranking.ts +6 -1
  125. package/src/memory/search/types.ts +26 -0
  126. package/src/messaging/provider-types.ts +2 -0
  127. package/src/messaging/providers/sms/adapter.ts +204 -0
  128. package/src/messaging/providers/sms/client.ts +93 -0
  129. package/src/messaging/providers/sms/types.ts +7 -0
  130. package/src/permissions/checker.ts +16 -2
  131. package/src/permissions/prompter.ts +14 -3
  132. package/src/permissions/trust-store.ts +7 -0
  133. package/src/runtime/approval-message-composer.ts +143 -0
  134. package/src/runtime/channel-approvals.ts +29 -7
  135. package/src/runtime/channel-guardian-service.ts +44 -18
  136. package/src/runtime/channel-readiness-service.ts +292 -0
  137. package/src/runtime/channel-readiness-types.ts +29 -0
  138. package/src/runtime/gateway-client.ts +2 -1
  139. package/src/runtime/http-server.ts +65 -28
  140. package/src/runtime/http-types.ts +3 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/channel-routes.ts +237 -103
  143. package/src/runtime/routes/run-routes.ts +7 -1
  144. package/src/runtime/run-orchestrator.ts +43 -3
  145. package/src/security/secret-scanner.ts +218 -0
  146. package/src/skills/frontmatter.ts +63 -0
  147. package/src/skills/slash-commands.ts +23 -0
  148. package/src/skills/vellum-catalog-remote.ts +107 -0
  149. package/src/tools/assets/materialize.ts +2 -2
  150. package/src/tools/browser/auto-navigate.ts +132 -24
  151. package/src/tools/browser/browser-manager.ts +67 -61
  152. package/src/tools/calls/call-start.ts +1 -0
  153. package/src/tools/claude-code/claude-code.ts +55 -3
  154. package/src/tools/credentials/vault.ts +1 -1
  155. package/src/tools/execution-target.ts +11 -1
  156. package/src/tools/executor.ts +10 -2
  157. package/src/tools/network/web-search.ts +1 -1
  158. package/src/tools/skills/vellum-catalog.ts +61 -156
  159. package/src/tools/terminal/parser.ts +21 -5
  160. package/src/tools/types.ts +2 -0
  161. package/src/twitter/router.ts +1 -1
  162. package/src/util/platform.ts +43 -1
  163. package/src/util/retry.ts +4 -4
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import type { AssistantConfig } from '../config/types.js';
2
3
  import { getLogger } from '../util/logger.js';
3
4
  import { GeminiEmbeddingBackend } from './embedding-gemini.js';
@@ -10,9 +11,41 @@ const log = getLogger('memory-embeddings');
10
11
  /** Global cache of embedding backend instances, keyed by "provider:model". */
11
12
  const backendCache = new Map<string, EmbeddingBackend>();
12
13
 
13
- /** Clear cached embedding backends so new instances pick up fresh credentials. */
14
+ // ── In-memory embedding vector cache ──────────────────────────────
15
+ // LRU cache keyed by sha256(provider + model + text) → embedding vector.
16
+ // Avoids redundant API calls / local compute for identical content.
17
+ const VECTOR_CACHE_MAX_ENTRIES = 4096;
18
+ const vectorCache = new Map<string, number[]>();
19
+
20
+ function vectorCacheKey(provider: string, model: string, text: string): string {
21
+ return createHash('sha256').update(`${provider}\0${model}\0${text}`).digest('hex');
22
+ }
23
+
24
+ function getFromVectorCache(provider: string, model: string, text: string): number[] | undefined {
25
+ const key = vectorCacheKey(provider, model, text);
26
+ const v = vectorCache.get(key);
27
+ if (v !== undefined) {
28
+ // LRU refresh: move to end of insertion order
29
+ vectorCache.delete(key);
30
+ vectorCache.set(key, v);
31
+ }
32
+ return v;
33
+ }
34
+
35
+ function putInVectorCache(provider: string, model: string, text: string, vector: number[]): void {
36
+ const key = vectorCacheKey(provider, model, text);
37
+ vectorCache.delete(key);
38
+ if (vectorCache.size >= VECTOR_CACHE_MAX_ENTRIES) {
39
+ const oldest = vectorCache.keys().next().value;
40
+ if (oldest !== undefined) vectorCache.delete(oldest);
41
+ }
42
+ vectorCache.set(key, vector);
43
+ }
44
+
45
+ /** Clear cached embedding backends and the in-memory vector cache. */
14
46
  export function clearEmbeddingBackendCache(): void {
15
47
  backendCache.clear();
48
+ vectorCache.clear();
16
49
  }
17
50
 
18
51
  function cacheKey(provider: string, model: string): string {
@@ -153,22 +186,44 @@ export async function embedWithBackend(
153
186
  throw new Error(selection.reason ?? 'No memory embedding backend configured');
154
187
  }
155
188
 
156
- // In auto mode, build a fallback list of backends to try
157
- const backends: EmbeddingBackend[] = [selection.backend];
158
- if (config.memory.embeddings.provider === 'auto' && selection.backend.provider === 'local') {
159
- for (const fallback of selectFallbackBackends(config, 'local')) {
160
- backends.push(fallback);
161
- }
189
+ const expectedDim = config.memory.qdrant.vectorSize;
190
+ const { provider: primaryProvider, model: primaryModel } = selection.backend;
191
+
192
+ // ── Build fallback backends list (needed for embed fallback) ──
193
+ const fallbacks: EmbeddingBackend[] =
194
+ config.memory.embeddings.provider === 'auto' && selection.backend.provider === 'local'
195
+ ? selectFallbackBackends(config, 'local')
196
+ : [];
197
+
198
+ // ── In-memory cache check (primary provider only) ──────────────
199
+ const cached: (number[] | null)[] = texts.map(t => {
200
+ const v = getFromVectorCache(primaryProvider, primaryModel, t);
201
+ if (v && v.length === expectedDim) return v;
202
+ return null;
203
+ });
204
+ const uncachedIndices: number[] = [];
205
+ for (let i = 0; i < cached.length; i++) {
206
+ if (!cached[i]) uncachedIndices.push(i);
207
+ }
208
+ if (uncachedIndices.length === 0) {
209
+ return { provider: primaryProvider, model: primaryModel, vectors: cached as number[][] };
162
210
  }
163
211
 
212
+ // ── Embed uncached texts ────────────────────────────────────────
213
+ const backends: EmbeddingBackend[] = [selection.backend, ...fallbacks];
214
+
164
215
  let lastErr: unknown;
165
216
  for (const backend of backends) {
217
+ const isPrimary = backend === selection.backend;
218
+ // For the primary backend, only embed uncached texts and merge with cached.
219
+ // For fallback backends, embed ALL texts since the cache was keyed to the primary.
220
+ const textsToEmbed = isPrimary ? uncachedIndices.map(i => texts[i]) : texts;
221
+
166
222
  try {
167
- const vectors = await backend.embed(texts, options);
168
- if (vectors.length !== texts.length) {
169
- throw new Error(`Embedding backend returned ${vectors.length} vectors for ${texts.length} texts`);
223
+ const vectors = await backend.embed(textsToEmbed, options);
224
+ if (vectors.length !== textsToEmbed.length) {
225
+ throw new Error(`Embedding backend returned ${vectors.length} vectors for ${textsToEmbed.length} texts`);
170
226
  }
171
- const expectedDim = config.memory.qdrant.vectorSize;
172
227
  for (const vec of vectors) {
173
228
  if (vec.length !== expectedDim) {
174
229
  throw new Error(
@@ -176,6 +231,19 @@ export async function embedWithBackend(
176
231
  );
177
232
  }
178
233
  }
234
+
235
+ // Populate cache with freshly embedded vectors
236
+ for (let i = 0; i < textsToEmbed.length; i++) {
237
+ putInVectorCache(backend.provider, backend.model, textsToEmbed[i], vectors[i]);
238
+ }
239
+
240
+ if (isPrimary) {
241
+ const merged = [...cached] as number[][];
242
+ for (let i = 0; i < uncachedIndices.length; i++) {
243
+ merged[uncachedIndices[i]] = vectors[i];
244
+ }
245
+ return { provider: backend.provider, model: backend.model, vectors: merged };
246
+ }
179
247
  return { provider: backend.provider, model: backend.model, vectors };
180
248
  } catch (err) {
181
249
  lastErr = err;
@@ -7,6 +7,7 @@ import { getDb } from './db.js';
7
7
  import { enqueueMemoryJob, enqueueResolvePendingConflictsForMessageJob } from './jobs-store.js';
8
8
  import { extractTextFromStoredMessageContent } from './message-content.js';
9
9
  import { segmentText } from './segmenter.js';
10
+ import { bumpMemoryVersion } from './recall-cache.js';
10
11
  import { memorySegments } from './schema.js';
11
12
 
12
13
  const log = getLogger('memory-indexer');
@@ -108,6 +109,7 @@ export function indexMessageNow(
108
109
  log.debug(`Skipped ${skippedEmbedJobs}/${segments.length} embed_segment jobs (content unchanged)`);
109
110
  }
110
111
 
112
+ bumpMemoryVersion();
111
113
  enqueueSummaryRollupJobsIfDue();
112
114
 
113
115
  const enqueuedJobs = (segments.length - skippedEmbedJobs) + (shouldExtract ? 2 : 1) + (shouldResolveConflicts ? 1 : 0);
@@ -0,0 +1,100 @@
1
+ import { getLogger } from '../../util/logger.js';
2
+ import { asString } from '../job-utils.js';
3
+ import { getMediaAssetById, updateMediaAssetStatus } from '../media-store.js';
4
+ import type { MemoryJob } from '../jobs-store.js';
5
+ import {
6
+ runPipeline,
7
+ type PipelineStageName,
8
+ type StageHandler,
9
+ } from '../../config/bundled-skills/media-processing/services/processing-pipeline.js';
10
+ import { extractKeyframesForAsset } from '../../config/bundled-skills/media-processing/tools/extract-keyframes.js';
11
+ import { analyzeKeyframesForAsset } from '../../config/bundled-skills/media-processing/tools/analyze-keyframes.js';
12
+ import { generateTimeline } from '../../config/bundled-skills/media-processing/services/timeline-service.js';
13
+ import {
14
+ detectEvents,
15
+ type DetectionConfig,
16
+ } from '../../config/bundled-skills/media-processing/services/event-detection-service.js';
17
+
18
+ const log = getLogger('media-processing-job');
19
+
20
+ const defaultDetectionConfig: DetectionConfig = {
21
+ eventType: 'scene_change',
22
+ rules: [
23
+ {
24
+ ruleType: 'segment_transition',
25
+ params: { field: 'segmentType' },
26
+ weight: 1.0,
27
+ },
28
+ ],
29
+ };
30
+
31
+ export async function mediaProcessingJob(job: MemoryJob): Promise<void> {
32
+ const mediaAssetId = asString(job.payload.mediaAssetId);
33
+ if (!mediaAssetId) {
34
+ log.warn({ jobId: job.id }, 'Missing mediaAssetId in job payload');
35
+ return;
36
+ }
37
+
38
+ const asset = getMediaAssetById(mediaAssetId);
39
+ if (!asset) {
40
+ log.warn({ jobId: job.id, mediaAssetId }, 'Media asset not found');
41
+ return;
42
+ }
43
+
44
+ if (asset.mediaType !== 'video') {
45
+ log.info(
46
+ { assetId: mediaAssetId, mediaType: asset.mediaType },
47
+ 'Skipping media processing pipeline — only video assets are supported',
48
+ );
49
+ updateMediaAssetStatus(mediaAssetId, 'indexed');
50
+ return;
51
+ }
52
+
53
+ // Build detection config, allowing optional eventType override from payload
54
+ const eventType = asString(job.payload.eventType);
55
+ const detectionConfig: DetectionConfig = eventType
56
+ ? { ...defaultDetectionConfig, eventType }
57
+ : defaultDetectionConfig;
58
+
59
+ const handlers: Record<PipelineStageName, StageHandler> = {
60
+ keyframe_extraction: {
61
+ execute: (assetId, onProgress) =>
62
+ extractKeyframesForAsset(assetId, 1, onProgress),
63
+ },
64
+ vision_analysis: {
65
+ execute: (assetId, onProgress) =>
66
+ analyzeKeyframesForAsset(assetId, undefined, undefined, onProgress),
67
+ },
68
+ timeline_generation: {
69
+ execute: async (assetId, onProgress) => {
70
+ generateTimeline(assetId, { onProgress });
71
+ },
72
+ },
73
+ event_detection: {
74
+ execute: async (assetId, onProgress) => {
75
+ detectEvents(assetId, detectionConfig, { onProgress });
76
+ },
77
+ },
78
+ };
79
+
80
+ const result = await runPipeline(mediaAssetId, handlers, {
81
+ onProgress: (msg) => log.info({ mediaAssetId }, msg),
82
+ });
83
+
84
+ log.info(
85
+ {
86
+ mediaAssetId,
87
+ completedStages: result.completedStages,
88
+ failedStage: result.failedStage,
89
+ cancelled: result.cancelled,
90
+ },
91
+ 'Media processing pipeline finished',
92
+ );
93
+
94
+ if (result.failedStage) {
95
+ throw new Error(`Media processing failed at stage ${result.failedStage}: ${result.failureReason}`);
96
+ }
97
+ if (result.cancelled) {
98
+ throw new Error(`Media processing cancelled for asset ${mediaAssetId}`);
99
+ }
100
+ }
@@ -1,6 +1,10 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { eq, and } from 'drizzle-orm';
1
3
  import { getLogger } from '../util/logger.js';
2
4
  import { embedWithBackend, getMemoryBackendStatus } from './embedding-backend.js';
5
+ import { getDb } from './db.js';
3
6
  import { getQdrantClient } from './qdrant-client.js';
7
+ import { memoryEmbeddings } from './schema.js';
4
8
  import type { AssistantConfig } from '../config/types.js';
5
9
 
6
10
  const log = getLogger('memory-jobs-worker');
@@ -111,9 +115,66 @@ export async function embedAndUpsert(
111
115
  );
112
116
  }
113
117
 
114
- const embedded = await embedWithBackend(config, [text]);
115
- const vector = embedded.vectors[0];
116
- if (!vector) return;
118
+ const contentHash = createHash('sha256').update(text).digest('hex');
119
+ let provider = status.provider;
120
+ let model = status.model!;
121
+ let vector: number[];
122
+
123
+ // Check SQLite embedding cache for a matching content hash (primary provider only).
124
+ const db = getDb();
125
+ const expectedDim = config.memory.qdrant.vectorSize;
126
+ let cachedRow = db
127
+ .select({ vectorJson: memoryEmbeddings.vectorJson, dimensions: memoryEmbeddings.dimensions })
128
+ .from(memoryEmbeddings)
129
+ .where(
130
+ and(
131
+ eq(memoryEmbeddings.contentHash, contentHash),
132
+ eq(memoryEmbeddings.provider, provider),
133
+ eq(memoryEmbeddings.model, model),
134
+ ),
135
+ )
136
+ .get();
137
+ if (cachedRow && cachedRow.dimensions !== expectedDim) cachedRow = undefined;
138
+
139
+ if (cachedRow) {
140
+ vector = JSON.parse(cachedRow.vectorJson);
141
+ } else {
142
+ const embedded = await embedWithBackend(config, [text]);
143
+ vector = embedded.vectors[0];
144
+ if (!vector) return;
145
+ provider = embedded.provider;
146
+ model = embedded.model;
147
+ }
148
+
149
+ // Persist embedding in SQLite for cross-restart cache
150
+ const now = Date.now();
151
+ try {
152
+ db.insert(memoryEmbeddings)
153
+ .values({
154
+ id: randomUUID(),
155
+ targetType,
156
+ targetId,
157
+ provider,
158
+ model,
159
+ dimensions: vector.length,
160
+ vectorJson: JSON.stringify(vector),
161
+ contentHash,
162
+ createdAt: now,
163
+ updatedAt: now,
164
+ })
165
+ .onConflictDoUpdate({
166
+ target: [memoryEmbeddings.targetType, memoryEmbeddings.targetId, memoryEmbeddings.provider, memoryEmbeddings.model],
167
+ set: {
168
+ vectorJson: JSON.stringify(vector),
169
+ dimensions: vector.length,
170
+ contentHash,
171
+ updatedAt: now,
172
+ },
173
+ })
174
+ .run();
175
+ } catch (err) {
176
+ log.warn({ err, targetType, targetId }, 'Failed to write embedding cache');
177
+ }
117
178
 
118
179
  let qdrant;
119
180
  try {
@@ -123,7 +184,6 @@ export async function embedAndUpsert(
123
184
  }
124
185
 
125
186
  try {
126
- const now = Date.now();
127
187
  await qdrant.upsert(targetType, targetId, vector, {
128
188
  text,
129
189
  created_at: (extraPayload?.created_at as number) ?? now,
@@ -20,7 +20,8 @@ export type MemoryJobType =
20
20
  | 'build_conversation_summary'
21
21
  | 'backfill'
22
22
  | 'rebuild_index'
23
- | 'delete_qdrant_vectors';
23
+ | 'delete_qdrant_vectors'
24
+ | 'media_processing';
24
25
 
25
26
  const EMBED_JOB_TYPES: MemoryJobType[] = ['embed_segment', 'embed_item', 'embed_summary'];
26
27
 
@@ -18,6 +18,7 @@ import {
18
18
  retryDelayForAttempt,
19
19
  RETRY_MAX_ATTEMPTS,
20
20
  } from './job-utils.js';
21
+ import { bumpMemoryVersion } from './recall-cache.js';
21
22
 
22
23
  // ── Per-job-type handlers ──────────────────────────────────────────
23
24
 
@@ -28,6 +29,7 @@ import { checkContradictionsJob, cleanupStaleSupersededItemsJob } from './job-ha
28
29
  import { buildConversationSummaryJob, buildGlobalSummaryJob } from './job-handlers/summarization.js';
29
30
  import { backfillJob, backfillEntityRelationsJob } from './job-handlers/backfill.js';
30
31
  import { rebuildIndexJob, deleteQdrantVectorsJob } from './job-handlers/index-maintenance.js';
32
+ import { mediaProcessingJob } from './job-handlers/media-processing.js';
31
33
 
32
34
  // Re-export public utilities consumed by tests and other modules
33
35
  export { currentWeekWindow } from './job-utils.js';
@@ -121,9 +123,14 @@ export async function runMemoryJobsOnce(
121
123
  try {
122
124
  await processJob(job, config);
123
125
  completeMemoryJob(job.id);
126
+ bumpMemoryVersion();
124
127
  groupProcessed += 1;
125
128
  } catch (err) {
126
- handleJobError(job, err);
129
+ try {
130
+ handleJobError(job, err);
131
+ } catch (handlerErr) {
132
+ log.error({ err: handlerErr, jobId: job.id, type: job.type }, 'handleJobError itself threw, job left in running status');
133
+ }
127
134
  }
128
135
  }
129
136
  return groupProcessed;
@@ -223,6 +230,9 @@ async function processJob(job: MemoryJob, config: AssistantConfig): Promise<void
223
230
  case 'delete_qdrant_vectors':
224
231
  await deleteQdrantVectorsJob(job);
225
232
  return;
233
+ case 'media_processing':
234
+ await mediaProcessingJob(job);
235
+ return;
226
236
  default:
227
237
  throw new Error(`Unknown memory job type: ${(job as { type: string }).type}`);
228
238
  }