@dot-ai/ext-sqlite-memory 0.9.0
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/LICENSE +21 -0
- package/dist/extension.d.ts +3 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +102 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/index.spec.d.ts +2 -0
- package/dist/index.spec.d.ts.map +1 -0
- package/dist/index.spec.js +108 -0
- package/dist/index.spec.js.map +1 -0
- package/dist/migrate-files.d.ts +3 -0
- package/dist/migrate-files.d.ts.map +1 -0
- package/dist/migrate-files.js +177 -0
- package/dist/migrate-files.js.map +1 -0
- package/dist/sqlite-memory.d.ts +15 -0
- package/dist/sqlite-memory.d.ts.map +1 -0
- package/dist/sqlite-memory.js +184 -0
- package/dist/sqlite-memory.js.map +1 -0
- package/package.json +35 -0
- package/src/__tests__/lifecycle.test.ts +209 -0
- package/src/__tests__/sqlite-memory.test.ts +99 -0
- package/src/extension.ts +102 -0
- package/src/index.spec.ts +138 -0
- package/src/index.ts +2 -0
- package/src/migrate-files.ts +193 -0
- package/src/sqlite-memory.ts +242 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Compute Jaccard similarity between two strings (word-level).
|
|
5
|
+
* similarity = |intersection| / |union|
|
|
6
|
+
*/
|
|
7
|
+
function jaccardSimilarity(a, b) {
|
|
8
|
+
const wordsA = new Set(a.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter(w => w.length > 1));
|
|
9
|
+
const wordsB = new Set(b.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter(w => w.length > 1));
|
|
10
|
+
if (wordsA.size === 0 && wordsB.size === 0)
|
|
11
|
+
return 1;
|
|
12
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
13
|
+
return 0;
|
|
14
|
+
let intersection = 0;
|
|
15
|
+
for (const w of wordsA) {
|
|
16
|
+
if (wordsB.has(w))
|
|
17
|
+
intersection++;
|
|
18
|
+
}
|
|
19
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
20
|
+
return intersection / union;
|
|
21
|
+
}
|
|
22
|
+
export class SqliteMemoryProvider {
|
|
23
|
+
db;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
const root = options.root ?? process.cwd();
|
|
26
|
+
const rawPath = options.path ?? ':memory:';
|
|
27
|
+
const dbPath = rawPath === ':memory:' ? rawPath : (rawPath.startsWith('/') ? rawPath : join(root, rawPath));
|
|
28
|
+
this.db = new Database(dbPath);
|
|
29
|
+
this.db.pragma('journal_mode = WAL');
|
|
30
|
+
this.init();
|
|
31
|
+
}
|
|
32
|
+
init() {
|
|
33
|
+
this.db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
content TEXT NOT NULL,
|
|
37
|
+
type TEXT NOT NULL DEFAULT 'log',
|
|
38
|
+
date TEXT,
|
|
39
|
+
labels TEXT DEFAULT '[]',
|
|
40
|
+
node TEXT,
|
|
41
|
+
source TEXT DEFAULT 'sqlite-memory',
|
|
42
|
+
created INTEGER DEFAULT (unixepoch())
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
46
|
+
content,
|
|
47
|
+
labels,
|
|
48
|
+
node,
|
|
49
|
+
content='memories',
|
|
50
|
+
content_rowid='id'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
54
|
+
INSERT INTO memories_fts(rowid, content, labels, node)
|
|
55
|
+
VALUES (new.id, new.content, new.labels, new.node);
|
|
56
|
+
END;
|
|
57
|
+
|
|
58
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
59
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, labels, node)
|
|
60
|
+
VALUES ('delete', old.id, old.content, old.labels, old.node);
|
|
61
|
+
END;
|
|
62
|
+
`);
|
|
63
|
+
// Graceful migration: add lifecycle columns if they don't exist
|
|
64
|
+
const existingCols = this.db.prepare(`PRAGMA table_info(memories)`).all()
|
|
65
|
+
.map(c => c.name);
|
|
66
|
+
if (!existingCols.includes('score')) {
|
|
67
|
+
this.db.exec(`ALTER TABLE memories ADD COLUMN score REAL DEFAULT 1.0`);
|
|
68
|
+
}
|
|
69
|
+
if (!existingCols.includes('last_recalled')) {
|
|
70
|
+
this.db.exec(`ALTER TABLE memories ADD COLUMN last_recalled TEXT`);
|
|
71
|
+
}
|
|
72
|
+
if (!existingCols.includes('recall_count')) {
|
|
73
|
+
this.db.exec(`ALTER TABLE memories ADD COLUMN recall_count INTEGER DEFAULT 0`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async store(entry) {
|
|
77
|
+
const content = entry.content;
|
|
78
|
+
// Extract key terms for FTS dedup check
|
|
79
|
+
const queryWords = content
|
|
80
|
+
.replace(/[^\w\s]/g, ' ')
|
|
81
|
+
.split(/\s+/)
|
|
82
|
+
.filter(w => w.length > 1);
|
|
83
|
+
if (queryWords.length > 0) {
|
|
84
|
+
const ftsQuery = queryWords.join(' OR ');
|
|
85
|
+
let candidates = [];
|
|
86
|
+
try {
|
|
87
|
+
candidates = this.db.prepare(`
|
|
88
|
+
SELECT m.id, m.content
|
|
89
|
+
FROM memories_fts
|
|
90
|
+
JOIN memories m ON m.id = memories_fts.rowid
|
|
91
|
+
WHERE memories_fts MATCH ?
|
|
92
|
+
LIMIT 10
|
|
93
|
+
`).all(ftsQuery);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// FTS error — fall through to insert
|
|
97
|
+
}
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
const similarity = jaccardSimilarity(content, candidate.content);
|
|
100
|
+
if (similarity > 0.85) {
|
|
101
|
+
// Duplicate found — update existing entry instead of inserting
|
|
102
|
+
this.db.prepare(`UPDATE memories SET content = ?, date = ?, score = MIN(score + 0.1, 5.0) WHERE id = ?`).run(content, entry.date ?? new Date().toISOString().slice(0, 10), candidate.id);
|
|
103
|
+
// Update FTS index for the modified row
|
|
104
|
+
this.db.prepare(`INSERT INTO memories_fts(memories_fts, rowid, content, labels, node) VALUES ('delete', ?, ?, ?, ?)`).run(candidate.id, candidate.content, '[]', null);
|
|
105
|
+
const updated = this.db.prepare(`SELECT content, labels, node FROM memories WHERE id = ?`).get(candidate.id);
|
|
106
|
+
this.db.prepare(`INSERT INTO memories_fts(rowid, content, labels, node) VALUES (?, ?, ?, ?)`).run(candidate.id, updated.content, updated.labels, updated.node);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// No duplicate found — insert new entry
|
|
112
|
+
const stmt = this.db.prepare('INSERT INTO memories (content, type, date, labels, node, score, recall_count) VALUES (?, ?, ?, ?, ?, 1.0, 0)');
|
|
113
|
+
stmt.run(entry.content, entry.type ?? 'log', entry.date ?? new Date().toISOString().slice(0, 10), JSON.stringify(entry.labels ?? []), entry.node ?? null);
|
|
114
|
+
}
|
|
115
|
+
async search(query, labels) {
|
|
116
|
+
const queryWords = query
|
|
117
|
+
.replace(/[^\w\s]/g, ' ')
|
|
118
|
+
.split(/\s+/)
|
|
119
|
+
.filter(w => w.length > 1);
|
|
120
|
+
const labelWords = (labels ?? [])
|
|
121
|
+
.map(l => l.replace(/[^\w\s]/g, '').trim())
|
|
122
|
+
.filter(w => w.length > 1);
|
|
123
|
+
const allTerms = [...new Set([...queryWords, ...labelWords])];
|
|
124
|
+
const cleanQuery = allTerms.join(' OR ');
|
|
125
|
+
if (!cleanQuery)
|
|
126
|
+
return [];
|
|
127
|
+
let rows;
|
|
128
|
+
try {
|
|
129
|
+
rows = this.db.prepare(`
|
|
130
|
+
SELECT m.id, m.content, m.type, m.date, m.labels, m.node, m.score,
|
|
131
|
+
bm25(memories_fts) AS rank
|
|
132
|
+
FROM memories_fts
|
|
133
|
+
JOIN memories m ON m.id = memories_fts.rowid
|
|
134
|
+
WHERE memories_fts MATCH ?
|
|
135
|
+
ORDER BY rank
|
|
136
|
+
LIMIT 20
|
|
137
|
+
`).all(cleanQuery);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
// Score bump: increment score, update last_recalled, increment recall_count
|
|
143
|
+
if (rows.length > 0) {
|
|
144
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
145
|
+
const updateStmt = this.db.prepare(`
|
|
146
|
+
UPDATE memories
|
|
147
|
+
SET score = MIN(score + 0.1, 5.0),
|
|
148
|
+
last_recalled = ?,
|
|
149
|
+
recall_count = recall_count + 1
|
|
150
|
+
WHERE id = ?
|
|
151
|
+
`);
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
updateStmt.run(today, row.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return rows.map(row => ({
|
|
157
|
+
content: row.content,
|
|
158
|
+
type: row.type,
|
|
159
|
+
source: 'sqlite-memory',
|
|
160
|
+
date: row.date ?? undefined,
|
|
161
|
+
labels: JSON.parse(row.labels),
|
|
162
|
+
node: row.node ?? undefined,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
async consolidate() {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const day = 86400000;
|
|
168
|
+
const deletedLogs = this.db.prepare(`DELETE FROM memories WHERE type = 'log' AND date < ? AND score < 0.3`).run(new Date(now - 30 * day).toISOString().slice(0, 10));
|
|
169
|
+
const deletedNotes = this.db.prepare(`DELETE FROM memories WHERE type = 'note' AND date < ? AND score < 0.3`).run(new Date(now - 60 * day).toISOString().slice(0, 10));
|
|
170
|
+
const deletedOld = this.db.prepare(`DELETE FROM memories WHERE type NOT IN ('lesson', 'decision', 'fact') AND date < ? AND score < 0.1`).run(new Date(now - 90 * day).toISOString().slice(0, 10));
|
|
171
|
+
return {
|
|
172
|
+
archived: 0,
|
|
173
|
+
deleted: deletedLogs.changes + deletedNotes.changes + deletedOld.changes,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
describe() {
|
|
177
|
+
const count = this.db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
|
|
178
|
+
return `Memory: SQLite with FTS5 full-text search (${count} entries). Use memory_recall to search, memory_store to save. This is the ONLY memory system — do not read or write memory/*.md files.`;
|
|
179
|
+
}
|
|
180
|
+
close() {
|
|
181
|
+
this.db.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=sqlite-memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-memory.js","sourceRoot":"","sources":["../src/sqlite-memory.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dot-ai/ext-sqlite-memory",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "SQLite FTS5 memory extension for dot-ai",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/extension.js",
|
|
7
|
+
"types": "dist/extension.d.ts",
|
|
8
|
+
"dot-ai": {
|
|
9
|
+
"extensions": [
|
|
10
|
+
"./dist/extension.js"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"better-sqlite3": "^11.0.0",
|
|
15
|
+
"@dot-ai/core": "0.9.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"typescript": "^5.9.3",
|
|
21
|
+
"vitest": "^4.0.0"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/jogelin/dot-ai",
|
|
29
|
+
"directory": "packages/ext-sqlite-memory"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"test": "vitest run"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { SqliteMemoryProvider } from '../sqlite-memory.js';
|
|
3
|
+
|
|
4
|
+
describe('SqliteMemoryProvider — deduplication on store()', () => {
|
|
5
|
+
let provider: SqliteMemoryProvider;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
provider = new SqliteMemoryProvider({ path: ':memory:' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
provider.close();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('stores similar content twice → only 1 entry in DB', async () => {
|
|
16
|
+
await provider.store({ content: 'Jo prefers French language for conversations', type: 'fact' });
|
|
17
|
+
await provider.store({ content: 'Jo prefers French language for conversations', type: 'fact' });
|
|
18
|
+
|
|
19
|
+
const results = await provider.search('Jo prefers French');
|
|
20
|
+
expect(results).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('stores nearly identical content → deduplicates (Jaccard > 0.85)', async () => {
|
|
24
|
+
await provider.store({ content: 'Jonathan prefers working in French language', type: 'fact' });
|
|
25
|
+
await provider.store({ content: 'Jonathan prefers working in French language', type: 'fact' });
|
|
26
|
+
|
|
27
|
+
const results = await provider.search('Jonathan French language');
|
|
28
|
+
expect(results).toHaveLength(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('stores different content → 2 entries', async () => {
|
|
32
|
+
await provider.store({ content: 'Jo prefers French language for conversations', type: 'fact' });
|
|
33
|
+
await provider.store({ content: 'The authentication system uses JWT tokens for authorization', type: 'fact' });
|
|
34
|
+
|
|
35
|
+
const frenchResults = await provider.search('Jo prefers French');
|
|
36
|
+
const authResults = await provider.search('authentication JWT tokens');
|
|
37
|
+
expect(frenchResults).toHaveLength(1);
|
|
38
|
+
expect(authResults).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('describes correct count after dedup (2 similar → 1 entry)', async () => {
|
|
42
|
+
await provider.store({ content: 'Jo prefers French language for conversations', type: 'fact' });
|
|
43
|
+
await provider.store({ content: 'Jo prefers French language for conversations', type: 'fact' });
|
|
44
|
+
|
|
45
|
+
const description = provider.describe();
|
|
46
|
+
expect(description).toContain('1 entries');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('SqliteMemoryProvider — score bump on search()', () => {
|
|
51
|
+
let provider: SqliteMemoryProvider;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
provider = new SqliteMemoryProvider({ path: ':memory:' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
provider.close();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('searching for memories increments recall_count and sets last_recalled', async () => {
|
|
62
|
+
await provider.store({ content: 'Fixed the auth middleware N+1 bug properly', type: 'log' });
|
|
63
|
+
|
|
64
|
+
// Search once
|
|
65
|
+
const results1 = await provider.search('auth middleware');
|
|
66
|
+
expect(results1).toHaveLength(1);
|
|
67
|
+
|
|
68
|
+
// Verify recall_count was bumped in the DB
|
|
69
|
+
const row = (provider as unknown as { db: import('better-sqlite3').Database })
|
|
70
|
+
.db
|
|
71
|
+
.prepare(`SELECT recall_count, last_recalled, score FROM memories WHERE content LIKE '%auth middleware%'`)
|
|
72
|
+
.get() as { recall_count: number; last_recalled: string; score: number } | undefined;
|
|
73
|
+
|
|
74
|
+
expect(row).toBeDefined();
|
|
75
|
+
expect(row!.recall_count).toBe(1);
|
|
76
|
+
expect(row!.last_recalled).toBe(new Date().toISOString().slice(0, 10));
|
|
77
|
+
expect(row!.score).toBeCloseTo(1.1, 5);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('searching multiple times accumulates score', async () => {
|
|
81
|
+
await provider.store({ content: 'The routing logic determines model selection carefully', type: 'fact' });
|
|
82
|
+
|
|
83
|
+
await provider.search('routing logic');
|
|
84
|
+
await provider.search('routing logic');
|
|
85
|
+
await provider.search('routing logic');
|
|
86
|
+
|
|
87
|
+
const row = (provider as unknown as { db: import('better-sqlite3').Database })
|
|
88
|
+
.db
|
|
89
|
+
.prepare(`SELECT recall_count, score FROM memories WHERE content LIKE '%routing%'`)
|
|
90
|
+
.get() as { recall_count: number; score: number } | undefined;
|
|
91
|
+
|
|
92
|
+
expect(row).toBeDefined();
|
|
93
|
+
expect(row!.recall_count).toBe(3);
|
|
94
|
+
expect(row!.score).toBeCloseTo(1.3, 5);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('score is capped at 5.0', async () => {
|
|
98
|
+
await provider.store({ content: 'Critical architectural decision for the system', type: 'decision' });
|
|
99
|
+
|
|
100
|
+
// Search 50 times to exceed cap
|
|
101
|
+
for (let i = 0; i < 50; i++) {
|
|
102
|
+
await provider.search('architectural decision');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const row = (provider as unknown as { db: import('better-sqlite3').Database })
|
|
106
|
+
.db
|
|
107
|
+
.prepare(`SELECT score FROM memories WHERE content LIKE '%architectural%'`)
|
|
108
|
+
.get() as { score: number } | undefined;
|
|
109
|
+
|
|
110
|
+
expect(row).toBeDefined();
|
|
111
|
+
expect(row!.score).toBeLessThanOrEqual(5.0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('SqliteMemoryProvider — consolidate()', () => {
|
|
116
|
+
let provider: SqliteMemoryProvider;
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
provider = new SqliteMemoryProvider({ path: ':memory:' });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
provider.close();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('deletes old logs with low score', async () => {
|
|
127
|
+
// Insert an old log entry directly with low score
|
|
128
|
+
const db = (provider as unknown as { db: import('better-sqlite3').Database }).db;
|
|
129
|
+
const oldDate = new Date(Date.now() - 35 * 86400000).toISOString().slice(0, 10); // 35 days ago
|
|
130
|
+
db.prepare(
|
|
131
|
+
`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'log', ?, '[]', 0.1)`
|
|
132
|
+
).run('Old low-score log entry from long ago', oldDate);
|
|
133
|
+
|
|
134
|
+
const before = db.prepare('SELECT COUNT(*) as c FROM memories').get() as { c: number };
|
|
135
|
+
expect(before.c).toBe(1);
|
|
136
|
+
|
|
137
|
+
const report = await provider.consolidate();
|
|
138
|
+
expect(report.deleted).toBeGreaterThanOrEqual(1);
|
|
139
|
+
|
|
140
|
+
const after = db.prepare('SELECT COUNT(*) as c FROM memories').get() as { c: number };
|
|
141
|
+
expect(after.c).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('keeps recent entries even with low score', async () => {
|
|
145
|
+
const db = (provider as unknown as { db: import('better-sqlite3').Database }).db;
|
|
146
|
+
const recentDate = new Date().toISOString().slice(0, 10); // today
|
|
147
|
+
db.prepare(
|
|
148
|
+
`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'log', ?, '[]', 0.1)`
|
|
149
|
+
).run('Recent low-score log entry', recentDate);
|
|
150
|
+
|
|
151
|
+
const report = await provider.consolidate();
|
|
152
|
+
expect(report.deleted).toBe(0);
|
|
153
|
+
|
|
154
|
+
const after = db.prepare('SELECT COUNT(*) as c FROM memories').get() as { c: number };
|
|
155
|
+
expect(after.c).toBe(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('keeps lessons/decisions/facts even when old and low score', async () => {
|
|
159
|
+
const db = (provider as unknown as { db: import('better-sqlite3').Database }).db;
|
|
160
|
+
const veryOldDate = new Date(Date.now() - 100 * 86400000).toISOString().slice(0, 10); // 100 days ago
|
|
161
|
+
|
|
162
|
+
db.prepare(
|
|
163
|
+
`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'lesson', ?, '[]', 0.05)`
|
|
164
|
+
).run('Important lesson learned from a past mistake', veryOldDate);
|
|
165
|
+
db.prepare(
|
|
166
|
+
`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'decision', ?, '[]', 0.05)`
|
|
167
|
+
).run('Key architectural decision made last year', veryOldDate);
|
|
168
|
+
db.prepare(
|
|
169
|
+
`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'fact', ?, '[]', 0.05)`
|
|
170
|
+
).run('Critical fact about the system design', veryOldDate);
|
|
171
|
+
|
|
172
|
+
const report = await provider.consolidate();
|
|
173
|
+
expect(report.deleted).toBe(0);
|
|
174
|
+
|
|
175
|
+
const after = db.prepare('SELECT COUNT(*) as c FROM memories').get() as { c: number };
|
|
176
|
+
expect(after.c).toBe(3);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('consolidate returns archived=0 and deleted count', async () => {
|
|
180
|
+
const report = await provider.consolidate();
|
|
181
|
+
expect(report).toHaveProperty('archived', 0);
|
|
182
|
+
expect(report).toHaveProperty('deleted');
|
|
183
|
+
expect(typeof report.deleted).toBe('number');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('deletes mixed old entries but preserves protected types', async () => {
|
|
187
|
+
const db = (provider as unknown as { db: import('better-sqlite3').Database }).db;
|
|
188
|
+
const oldDate = new Date(Date.now() - 35 * 86400000).toISOString().slice(0, 10);
|
|
189
|
+
const veryOldDate = new Date(Date.now() - 100 * 86400000).toISOString().slice(0, 10);
|
|
190
|
+
|
|
191
|
+
// Should be deleted (old log, low score)
|
|
192
|
+
db.prepare(`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'log', ?, '[]', 0.1)`)
|
|
193
|
+
.run('Old log entry that should be removed', oldDate);
|
|
194
|
+
|
|
195
|
+
// Should be kept (lesson, even very old and low score)
|
|
196
|
+
db.prepare(`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'lesson', ?, '[]', 0.05)`)
|
|
197
|
+
.run('Important lesson to keep forever', veryOldDate);
|
|
198
|
+
|
|
199
|
+
// Should be kept (recent log, even low score)
|
|
200
|
+
db.prepare(`INSERT INTO memories (content, type, date, labels, score) VALUES (?, 'log', ?, '[]', 0.1)`)
|
|
201
|
+
.run('Recent log entry to keep', new Date().toISOString().slice(0, 10));
|
|
202
|
+
|
|
203
|
+
const report = await provider.consolidate();
|
|
204
|
+
expect(report.deleted).toBe(1);
|
|
205
|
+
|
|
206
|
+
const after = db.prepare('SELECT COUNT(*) as c FROM memories').get() as { c: number };
|
|
207
|
+
expect(after.c).toBe(2);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { SqliteMemoryProvider } from '../sqlite-memory.js';
|
|
3
|
+
|
|
4
|
+
describe('SqliteMemoryProvider', () => {
|
|
5
|
+
let provider: SqliteMemoryProvider;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Use in-memory DB for tests — fast, no cleanup needed
|
|
9
|
+
provider = new SqliteMemoryProvider({ path: ':memory:' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
provider.close();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns empty array when no memories stored', async () => {
|
|
17
|
+
const results = await provider.search('anything');
|
|
18
|
+
expect(results).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('stores and retrieves a memory', async () => {
|
|
22
|
+
await provider.store({ content: 'Fixed the auth middleware N+1 bug', type: 'log' });
|
|
23
|
+
const results = await provider.search('auth middleware');
|
|
24
|
+
expect(results).toHaveLength(1);
|
|
25
|
+
expect(results[0].content).toContain('auth middleware');
|
|
26
|
+
expect(results[0].source).toBe('sqlite-memory');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('stores with labels and date', async () => {
|
|
30
|
+
await provider.store({
|
|
31
|
+
content: 'Rate limiting added to auth endpoints',
|
|
32
|
+
type: 'decision',
|
|
33
|
+
date: '2026-03-01',
|
|
34
|
+
labels: ['auth', 'api'],
|
|
35
|
+
});
|
|
36
|
+
const results = await provider.search('rate limiting');
|
|
37
|
+
expect(results[0].date).toBe('2026-03-01');
|
|
38
|
+
expect(results[0].labels).toContain('auth');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('ranks results by BM25 relevance', async () => {
|
|
42
|
+
await provider.store({ content: 'The auth system uses JWT tokens', type: 'fact' });
|
|
43
|
+
await provider.store({ content: 'Auth auth auth security auth', type: 'log' });
|
|
44
|
+
await provider.store({ content: 'Unrelated content about React components', type: 'log' });
|
|
45
|
+
|
|
46
|
+
const results = await provider.search('auth');
|
|
47
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
48
|
+
// "auth" appears more in second entry, should rank higher
|
|
49
|
+
expect(results.some(r => r.content.includes('React'))).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('expands search with labels (OR semantics)', async () => {
|
|
53
|
+
await provider.store({ content: 'Auth fix for backend', type: 'log', labels: ['auth', 'backend'] });
|
|
54
|
+
await provider.store({ content: 'Testing framework setup', type: 'log', labels: ['testing'] });
|
|
55
|
+
await provider.store({ content: 'Unrelated React component', type: 'log', labels: ['frontend'] });
|
|
56
|
+
|
|
57
|
+
// Labels expand the search: 'auth' matches first, 'testing' matches second
|
|
58
|
+
const results = await provider.search('auth', ['testing']);
|
|
59
|
+
expect(results).toHaveLength(2);
|
|
60
|
+
expect(results.some(r => r.content.includes('Auth fix'))).toBe(true);
|
|
61
|
+
expect(results.some(r => r.content.includes('Testing framework'))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles multiple stores and searches', async () => {
|
|
65
|
+
const topics = [
|
|
66
|
+
'Fixed the authentication bug in the login flow',
|
|
67
|
+
'Refactored the database connection pooling logic',
|
|
68
|
+
'Added rate limiting to the REST API endpoints',
|
|
69
|
+
'Migrated the frontend components to TypeScript',
|
|
70
|
+
'Implemented caching layer using Redis for performance',
|
|
71
|
+
'Updated the deployment pipeline for continuous delivery',
|
|
72
|
+
'Resolved memory leak in the background job processor',
|
|
73
|
+
'Designed the new microservices architecture diagram',
|
|
74
|
+
'Configured monitoring alerts for production incidents',
|
|
75
|
+
'Optimized SQL queries reducing response time by half',
|
|
76
|
+
];
|
|
77
|
+
for (const content of topics) {
|
|
78
|
+
await provider.store({ content, type: 'log' });
|
|
79
|
+
}
|
|
80
|
+
// All entries have unique content — count them directly via describe()
|
|
81
|
+
const description = provider.describe();
|
|
82
|
+
expect(description).toContain('10 entries');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns empty for queries with no matching words', async () => {
|
|
86
|
+
await provider.store({ content: 'Something about JavaScript', type: 'fact' });
|
|
87
|
+
const results = await provider.search('python django');
|
|
88
|
+
expect(results).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('satisfies the same contract as FileMemoryProvider', async () => {
|
|
92
|
+
// This test validates the contract: search returns MemoryEntry[]
|
|
93
|
+
await provider.store({ content: 'Test entry', type: 'fact' });
|
|
94
|
+
const results = await provider.search('test');
|
|
95
|
+
expect(results[0]).toHaveProperty('content');
|
|
96
|
+
expect(results[0]).toHaveProperty('type');
|
|
97
|
+
expect(results[0]).toHaveProperty('source');
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@dot-ai/core';
|
|
2
|
+
import { SqliteMemoryProvider } from './sqlite-memory.js';
|
|
3
|
+
|
|
4
|
+
export default function(api: ExtensionAPI) {
|
|
5
|
+
const provider = new SqliteMemoryProvider({ ...api.config, root: api.workspaceRoot });
|
|
6
|
+
|
|
7
|
+
api.on('session_end', async () => {
|
|
8
|
+
try {
|
|
9
|
+
await provider.consolidate?.();
|
|
10
|
+
} catch { /* ignore */ }
|
|
11
|
+
try {
|
|
12
|
+
provider.close();
|
|
13
|
+
} catch { /* ignore */ }
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
api.on('context_enrich', async (event) => {
|
|
17
|
+
const labelNames = event.labels.map(l => l.name);
|
|
18
|
+
const memories = await provider.search(event.prompt, labelNames);
|
|
19
|
+
if (memories.length === 0) return;
|
|
20
|
+
|
|
21
|
+
const lines = memories.slice(0, 10).map(m => {
|
|
22
|
+
const date = m.date ? ` (${m.date})` : '';
|
|
23
|
+
return `- ${m.content}${date}`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
sections: [{
|
|
28
|
+
id: 'memory:recall',
|
|
29
|
+
title: 'Relevant Memory',
|
|
30
|
+
content: `> ${provider.describe()}\n\n${lines.join('\n')}`,
|
|
31
|
+
priority: 80,
|
|
32
|
+
source: 'ext-sqlite-memory',
|
|
33
|
+
trimStrategy: 'truncate' as const,
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
api.on('agent_end', async (event) => {
|
|
39
|
+
const response = event.response;
|
|
40
|
+
if (!response || response.length < 100) return;
|
|
41
|
+
if (response.includes('NO_REPLY') || response.includes('HEARTBEAT_OK')) return;
|
|
42
|
+
const CONVERSATIONAL_PREFIXES = ['OK', 'Done', "Here's", "I've", 'Sure', 'No problem', 'Voilà', "C'est fait"];
|
|
43
|
+
const trimmed = response.trimStart();
|
|
44
|
+
if (CONVERSATIONAL_PREFIXES.some(prefix => trimmed.startsWith(prefix))) return;
|
|
45
|
+
|
|
46
|
+
const MAX_LEARN_LENGTH = 500;
|
|
47
|
+
const truncated = response.length > MAX_LEARN_LENGTH
|
|
48
|
+
? response.slice(0, MAX_LEARN_LENGTH) + '…'
|
|
49
|
+
: response;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await provider.store({
|
|
53
|
+
content: truncated,
|
|
54
|
+
type: 'log',
|
|
55
|
+
date: new Date().toISOString().slice(0, 10),
|
|
56
|
+
});
|
|
57
|
+
} catch { /* ignore store errors */ }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
api.registerTool({
|
|
61
|
+
name: 'memory_recall',
|
|
62
|
+
description: `Search stored memories. ${provider.describe()}`,
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
query: { type: 'string', description: 'Search query to find relevant memories.' },
|
|
67
|
+
limit: { type: 'number', description: 'Maximum results. Defaults to 10.' },
|
|
68
|
+
},
|
|
69
|
+
required: ['query'],
|
|
70
|
+
},
|
|
71
|
+
async execute(input) {
|
|
72
|
+
const query = input['query'];
|
|
73
|
+
if (typeof query !== 'string') return { content: 'Error: "query" must be a string.', isError: true };
|
|
74
|
+
const limit = typeof input['limit'] === 'number' ? input['limit'] : 10;
|
|
75
|
+
const entries = await provider.search(query);
|
|
76
|
+
const results = entries.slice(0, limit);
|
|
77
|
+
if (results.length === 0) return { content: 'No memories found.' };
|
|
78
|
+
const lines = results.map((e, i) => `${i + 1}. [${e.type}] ${e.content}${e.date ? ` (${e.date})` : ''}`);
|
|
79
|
+
return { content: lines.join('\n'), details: { count: results.length } };
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
api.registerTool({
|
|
84
|
+
name: 'memory_store',
|
|
85
|
+
description: 'Store a new entry in memory for future recall.',
|
|
86
|
+
parameters: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
text: { type: 'string', description: 'Content to store.' },
|
|
90
|
+
type: { type: 'string', description: 'Memory type: fact, decision, lesson, log, pattern. Defaults to log.' },
|
|
91
|
+
},
|
|
92
|
+
required: ['text'],
|
|
93
|
+
},
|
|
94
|
+
async execute(input) {
|
|
95
|
+
const text = input['text'];
|
|
96
|
+
if (typeof text !== 'string') return { content: 'Error: "text" must be a string.', isError: true };
|
|
97
|
+
const type = typeof input['type'] === 'string' ? input['type'] : 'log';
|
|
98
|
+
await provider.store({ content: text, type, date: new Date().toISOString().slice(0, 10) });
|
|
99
|
+
return { content: `Memory stored (type: ${type}).` };
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|