@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.
- package/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- 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
|
-
|
|
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
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
168
|
-
if (vectors.length !==
|
|
169
|
-
throw new Error(`Embedding backend returned ${vectors.length} vectors for ${
|
|
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;
|
package/src/memory/indexer.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/memory/job-utils.ts
CHANGED
|
@@ -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
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|