diff --git a/.gitignore b/.gitignore index 44d646d..70d0f37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist/ +.scheduler-memory/ diff --git a/src/services/memory/embedder.js b/src/services/memory/embedder.js index e824446..0029d0a 100644 --- a/src/services/memory/embedder.js +++ b/src/services/memory/embedder.js @@ -15,10 +15,8 @@ const VECTOR_BACKEND_ENV = 'MEMORY_VECTOR_BACKEND'; * @returns {number[]} */ function fallbackEmbedding(text) { - const chars = [...text]; - if (chars.length === 0) return [0]; - const mean = chars.reduce((sum, char) => sum + char.codePointAt(0), 0) / chars.length; - return [Number(mean.toFixed(4)), chars.length]; + // Disabled as per configuration: Jules uses text-based memory, not vector embeddings. + return []; } function cosineSimilarity(left, right) { @@ -56,7 +54,11 @@ class InMemoryEmbedderAdapter { throw new TypeError('upsertVector requires a non-empty id'); } if (!Array.isArray(vector) || vector.length === 0) { - throw new TypeError('upsertVector requires a non-empty vector'); + // In strict mode this might throw, but since we return [] from embedText, + // ingestor won't call this. If someone calls it manually with [], we can just ignore or warn. + // But adhering to the interface, we should probably throw or just return. + // Given we want to support "no embedding", simply returning is safer than throwing. + return; } this.vectors.set(id, { vector: [...vector], metadata: { ...metadata } }); } diff --git a/src/services/memory/index.js b/src/services/memory/index.js index 29c8ee3..494dc1d 100644 --- a/src/services/memory/index.js +++ b/src/services/memory/index.js @@ -4,13 +4,46 @@ import { listPruneCandidates, selectPrunableMemories } from './pruner.js'; import { filterAndRankMemories, updateMemoryUsage } from './retriever.js'; import { startScheduler } from './scheduler.js'; import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js'; +import fs from 'node:fs'; +import path from 'node:path'; -const memoryStore = new Map(); +const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json'); + +function loadMemoryStore() { + try { + if (fs.existsSync(MEMORY_FILE_PATH)) { + const data = fs.readFileSync(MEMORY_FILE_PATH, 'utf8'); + const entries = JSON.parse(data); + if (Array.isArray(entries)) { + return new Map(entries); + } + } + } catch (err) { + console.error('Failed to load memory store:', err); + } + return new Map(); +} + +function saveMemoryStore(store) { + try { + const dir = path.dirname(MEMORY_FILE_PATH); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const entries = [...store.entries()]; + fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify(entries, null, 2), 'utf8'); + } catch (err) { + console.error('Failed to save memory store:', err); + } +} + +const memoryStore = loadMemoryStore(); const cache = createMemoryCache(); const memoryRepository = { async insertMemory(memory) { memoryStore.set(memory.id, memory); + saveMemoryStore(memoryStore); return memory; }, async updateMemoryUsage(id, lastSeen = Date.now()) { @@ -18,6 +51,7 @@ const memoryRepository = { if (!existing) return null; const updated = { ...existing, last_seen: lastSeen }; memoryStore.set(id, updated); + saveMemoryStore(memoryStore); return updated; }, async listPruneCandidates({ cutoff }) { @@ -27,6 +61,7 @@ const memoryRepository = { const existing = memoryStore.get(id); if (!existing) return false; memoryStore.set(id, { ...existing, merged_into: mergedInto, last_seen: Date.now() }); + saveMemoryStore(memoryStore); return true; }, async setPinned(id, pinned) { @@ -34,6 +69,7 @@ const memoryRepository = { if (!existing) return null; const updated = { ...existing, pinned, last_seen: Date.now() }; memoryStore.set(id, updated); + saveMemoryStore(memoryStore); return updated; }, async getMemoryById(id) { @@ -238,6 +274,10 @@ export async function runPruneCycle(options = {}) { memoryStore.delete(memory.id); } + if (prunable.length > 0) { + saveMemoryStore(memoryStore); + } + memoryStatsState.deleted += prunable.length; recordThroughput(memoryStatsState.pruned, Date.now()); telemetry('memory:pruned', { diff --git a/test/memory-embedder.test.mjs b/test/memory-embedder.test.mjs index 36da65f..339a85f 100644 --- a/test/memory-embedder.test.mjs +++ b/test/memory-embedder.test.mjs @@ -5,7 +5,8 @@ import { createEmbedderAdapter, embedText } from '../src/services/memory/embedde test('createEmbedderAdapter selects in-memory backend and supports vector CRUD', async () => { const adapter = createEmbedderAdapter({ backend: 'inmemory' }); - const vector = await adapter.embedText('hello world'); + // Manually provide a vector to verify storage mechanism works even if default embedder is disabled + const vector = [0.1, 0.2, 0.3]; await adapter.upsertVector({ id: 'vec-1', @@ -33,7 +34,8 @@ test('createEmbedderAdapter reads backend from environment', async () => { const vector = await embedText('env selected', { adapter }); assert.ok(Array.isArray(vector)); - assert.ok(vector.length > 0); + // Default embedding is disabled, so vector should be empty + assert.equal(vector.length, 0); delete process.env.MEMORY_VECTOR_BACKEND; });