@qearlyao/familiar 0.1.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/.env.example +31 -0
- package/HEARTBEAT.md +23 -0
- package/LICENSE +21 -0
- package/MEMORY.md +1 -0
- package/README.md +245 -0
- package/SOUL.md +13 -0
- package/USER.md +13 -0
- package/config.example.toml +221 -0
- package/dist/agent-events.js +167 -0
- package/dist/agent.js +590 -0
- package/dist/browser-tools.js +638 -0
- package/dist/chat-log.js +130 -0
- package/dist/cli.js +168 -0
- package/dist/config.js +804 -0
- package/dist/data-retention.js +54 -0
- package/dist/discord.js +1203 -0
- package/dist/generated-media.js +86 -0
- package/dist/image-derivatives.js +102 -0
- package/dist/image-gen.js +440 -0
- package/dist/inbound-attachments.js +266 -0
- package/dist/index.js +10 -0
- package/dist/media-understanding.js +120 -0
- package/dist/memory/diary/ambient-injector.js +180 -0
- package/dist/memory/diary/ambient.js +124 -0
- package/dist/memory/diary/chunks.js +231 -0
- package/dist/memory/diary/index.js +3 -0
- package/dist/memory/diary/indexer.js +93 -0
- package/dist/memory/doctor.js +250 -0
- package/dist/memory/index/chunk-indexer.js +151 -0
- package/dist/memory/index/embedding-provider.js +119 -0
- package/dist/memory/index/fts-query.js +18 -0
- package/dist/memory/index/retrieval.js +246 -0
- package/dist/memory/index/schema.js +157 -0
- package/dist/memory/index/store.js +513 -0
- package/dist/memory/index/vec.js +72 -0
- package/dist/memory/index/vector-codec.js +27 -0
- package/dist/memory/lcm/backfill.js +247 -0
- package/dist/memory/lcm/condense.js +146 -0
- package/dist/memory/lcm/context-transformer.js +662 -0
- package/dist/memory/lcm/context.js +421 -0
- package/dist/memory/lcm/eviction-score.js +38 -0
- package/dist/memory/lcm/index.js +6 -0
- package/dist/memory/lcm/indexer.js +200 -0
- package/dist/memory/lcm/normalize.js +235 -0
- package/dist/memory/lcm/schema.js +188 -0
- package/dist/memory/lcm/segment-manager.js +136 -0
- package/dist/memory/lcm/store.js +722 -0
- package/dist/memory/lcm/summarizer.js +258 -0
- package/dist/memory/lcm/types.js +1 -0
- package/dist/memory/operator.js +477 -0
- package/dist/memory/service.js +202 -0
- package/dist/memory/tools.js +205 -0
- package/dist/models.js +165 -0
- package/dist/persona.js +54 -0
- package/dist/runtime.js +493 -0
- package/dist/scheduler.js +200 -0
- package/dist/settings.js +116 -0
- package/dist/skills.js +38 -0
- package/dist/tts.js +143 -0
- package/dist/web-auth.js +105 -0
- package/dist/web-events.js +114 -0
- package/dist/web-http.js +29 -0
- package/dist/web-static.js +106 -0
- package/dist/web-tools.js +940 -0
- package/dist/web-types.js +2 -0
- package/dist/web.js +844 -0
- package/package.json +60 -0
- package/web/dist/assets/index-ClgkMgaq.css +2 -0
- package/web/dist/assets/index-Cu2QquuR.js +59 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/icons.svg +24 -0
- package/web/dist/index.html +20 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 8;
|
|
2
|
+
const DEFAULT_CANDIDATE_MULTIPLIER = 4;
|
|
3
|
+
const RRF_K = 60;
|
|
4
|
+
const TEXT_DEDUPE_MIN_CHARS = 24;
|
|
5
|
+
export async function retrieveMemory(options) {
|
|
6
|
+
const query = options.query.trim();
|
|
7
|
+
if (!query)
|
|
8
|
+
return [];
|
|
9
|
+
const limit = positiveIntegerOrDefault(options.limit, DEFAULT_LIMIT);
|
|
10
|
+
const candidateLimit = positiveIntegerOrDefault(options.candidateLimit, Math.max(limit * DEFAULT_CANDIDATE_MULTIPLIER, limit));
|
|
11
|
+
const useLexical = options.useLexical ?? true;
|
|
12
|
+
const useSemantic = options.useSemantic ?? Boolean(options.embeddingProvider);
|
|
13
|
+
const lexicalHits = useLexical ? searchLexicalByScope(options.store, query, candidateLimit, options.scope) : [];
|
|
14
|
+
const semanticHits = useSemantic && options.embeddingProvider
|
|
15
|
+
? await searchSemanticByScope(options.store, options.embeddingProvider, query, candidateLimit, options.scope, options.signal)
|
|
16
|
+
: [];
|
|
17
|
+
return dedupeMemoryHits(mergeRankedHits(lexicalHits, semanticHits, options.scope)).slice(0, limit);
|
|
18
|
+
}
|
|
19
|
+
function searchLexicalByScope(store, query, limit, scope) {
|
|
20
|
+
const corpora = uniqueStrings(scope?.corpora);
|
|
21
|
+
if (corpora.length === 0)
|
|
22
|
+
return store.searchLexical(query, { limit });
|
|
23
|
+
return corpora.flatMap((corpus) => store.searchLexical(query, { limit, corpus }));
|
|
24
|
+
}
|
|
25
|
+
async function searchSemanticByScope(store, provider, query, limit, scope, signal) {
|
|
26
|
+
let vector;
|
|
27
|
+
try {
|
|
28
|
+
vector = await provider.embedOne(query, signal);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (signal?.aborted)
|
|
32
|
+
throw error;
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const corpora = uniqueStrings(scope?.corpora);
|
|
36
|
+
if (corpora.length === 0)
|
|
37
|
+
return store.searchSemantic(vector, { limit });
|
|
38
|
+
return corpora.flatMap((corpus) => store.searchSemantic(vector, { limit, corpus }));
|
|
39
|
+
}
|
|
40
|
+
function mergeRankedHits(lexicalHits, semanticHits, scope) {
|
|
41
|
+
const merged = new Map();
|
|
42
|
+
addHits(merged, lexicalHits, "lexical", scope);
|
|
43
|
+
addHits(merged, semanticHits, "semantic", scope);
|
|
44
|
+
return Array.from(merged.values()).sort(compareRetrievalHits);
|
|
45
|
+
}
|
|
46
|
+
function addHits(merged, hits, channel, scope) {
|
|
47
|
+
const ranksByCorpus = new Map();
|
|
48
|
+
const rankByCorpus = uniqueStrings(scope?.corpora).length > 0;
|
|
49
|
+
for (const hit of hits) {
|
|
50
|
+
if (!matchesScope(hit.chunk, scope))
|
|
51
|
+
continue;
|
|
52
|
+
const corpus = rankByCorpus ? hit.chunk.corpus : "";
|
|
53
|
+
// Corpus-scoped searches are independent retriever lists; each corpus starts
|
|
54
|
+
// RRF rank at 1 so fan-out order does not penalize later corpora.
|
|
55
|
+
const rank = (ranksByCorpus.get(corpus) ?? 0) + 1;
|
|
56
|
+
ranksByCorpus.set(corpus, rank);
|
|
57
|
+
const existing = merged.get(hit.id);
|
|
58
|
+
if (!existing) {
|
|
59
|
+
merged.set(hit.id, {
|
|
60
|
+
id: hit.id,
|
|
61
|
+
score: reciprocalRank(rank),
|
|
62
|
+
chunk: hit.chunk,
|
|
63
|
+
lexicalRank: channel === "lexical" ? rank : null,
|
|
64
|
+
semanticRank: channel === "semantic" ? rank : null,
|
|
65
|
+
lexicalScore: channel === "lexical" ? hit.score : null,
|
|
66
|
+
semanticScore: channel === "semantic" ? hit.score : null,
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
existing.score += reciprocalRank(rank);
|
|
71
|
+
if (channel === "lexical") {
|
|
72
|
+
existing.lexicalRank = rank;
|
|
73
|
+
existing.lexicalScore = hit.score;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
existing.semanticRank = rank;
|
|
77
|
+
existing.semanticScore = hit.score;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function compareRetrievalHits(a, b) {
|
|
82
|
+
return (b.score - a.score ||
|
|
83
|
+
bestRank(a) - bestRank(b) ||
|
|
84
|
+
(a.semanticScore ?? Number.POSITIVE_INFINITY) - (b.semanticScore ?? Number.POSITIVE_INFINITY) ||
|
|
85
|
+
(a.lexicalScore ?? Number.POSITIVE_INFINITY) - (b.lexicalScore ?? Number.POSITIVE_INFINITY) ||
|
|
86
|
+
a.id - b.id);
|
|
87
|
+
}
|
|
88
|
+
function bestRank(hit) {
|
|
89
|
+
return Math.min(hit.lexicalRank ?? Number.POSITIVE_INFINITY, hit.semanticRank ?? Number.POSITIVE_INFINITY);
|
|
90
|
+
}
|
|
91
|
+
function dedupeMemoryHits(hits) {
|
|
92
|
+
const groups = [];
|
|
93
|
+
const groupByKey = new Map();
|
|
94
|
+
for (const hit of hits) {
|
|
95
|
+
const keys = memoryDedupeKeys(hit.chunk);
|
|
96
|
+
const groupIndexes = new Set();
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
const groupIndex = groupByKey.get(key);
|
|
99
|
+
if (groupIndex !== undefined)
|
|
100
|
+
groupIndexes.add(groupIndex);
|
|
101
|
+
}
|
|
102
|
+
const targetIndex = groupIndexes.size > 0 ? Math.min(...groupIndexes) : groups.length;
|
|
103
|
+
const target = groups[targetIndex] ?? [];
|
|
104
|
+
target.push(hit);
|
|
105
|
+
groups[targetIndex] = target;
|
|
106
|
+
for (const groupIndex of groupIndexes) {
|
|
107
|
+
if (groupIndex === targetIndex)
|
|
108
|
+
continue;
|
|
109
|
+
for (const grouped of groups[groupIndex] ?? []) {
|
|
110
|
+
target.push(grouped);
|
|
111
|
+
for (const key of memoryDedupeKeys(grouped.chunk))
|
|
112
|
+
groupByKey.set(key, targetIndex);
|
|
113
|
+
}
|
|
114
|
+
groups[groupIndex] = [];
|
|
115
|
+
}
|
|
116
|
+
for (const key of keys)
|
|
117
|
+
groupByKey.set(key, targetIndex);
|
|
118
|
+
}
|
|
119
|
+
return groups
|
|
120
|
+
.filter((group) => group.length > 0)
|
|
121
|
+
.map((group) => group.sort(compareRetrievalHits)[0])
|
|
122
|
+
.filter((hit) => hit !== undefined)
|
|
123
|
+
.sort(compareRetrievalHits);
|
|
124
|
+
}
|
|
125
|
+
function memoryDedupeKeys(chunk) {
|
|
126
|
+
const keys = new Set();
|
|
127
|
+
const text = normalizeMemoryText(chunk.text);
|
|
128
|
+
if (text) {
|
|
129
|
+
if (text.length >= TEXT_DEDUPE_MIN_CHARS)
|
|
130
|
+
keys.add(`text:${chunk.corpus}:${text}`);
|
|
131
|
+
const kind = metadataString(chunk.metadata, "kind") ?? "";
|
|
132
|
+
const rounded = roundedChunkTimestamp(chunk);
|
|
133
|
+
if (kind && rounded !== null)
|
|
134
|
+
keys.add(`turn:${chunk.corpus}:${kind}:${rounded}:${text}`);
|
|
135
|
+
}
|
|
136
|
+
const sourceMessageId = metadataString(chunk.metadata, "sourceMessageId") ?? metadataSourceString(chunk, "sourceMessageId");
|
|
137
|
+
if (sourceMessageId)
|
|
138
|
+
keys.add(`message:${chunk.corpus}:${sourceMessageId}`);
|
|
139
|
+
return [...keys];
|
|
140
|
+
}
|
|
141
|
+
function normalizeMemoryText(text) {
|
|
142
|
+
const normalized = text
|
|
143
|
+
.replace(/^\s*\[[^\]]+\]\s*/, "")
|
|
144
|
+
.replace(/\s+/g, " ")
|
|
145
|
+
.trim()
|
|
146
|
+
.toLowerCase();
|
|
147
|
+
// Transitional shim for already-indexed legacy chunks that duplicated visible text.
|
|
148
|
+
const half = normalized.length / 2;
|
|
149
|
+
if (Number.isInteger(half) && normalized.slice(0, half).trim() === normalized.slice(half).trim()) {
|
|
150
|
+
return normalized.slice(0, half).trim();
|
|
151
|
+
}
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
function roundedChunkTimestamp(chunk) {
|
|
155
|
+
const timestamp = chunkTimestamp(chunk);
|
|
156
|
+
return timestamp === null ? null : Math.round(timestamp / 60_000);
|
|
157
|
+
}
|
|
158
|
+
function metadataString(metadata, key) {
|
|
159
|
+
const value = metadata?.[key];
|
|
160
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
161
|
+
}
|
|
162
|
+
function metadataSourceString(chunk, key) {
|
|
163
|
+
const source = chunk.metadata?.source;
|
|
164
|
+
if (!source || typeof source !== "object")
|
|
165
|
+
return null;
|
|
166
|
+
const value = source[key];
|
|
167
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
168
|
+
}
|
|
169
|
+
function reciprocalRank(rank) {
|
|
170
|
+
return 1 / (RRF_K + rank);
|
|
171
|
+
}
|
|
172
|
+
function matchesScope(chunk, scope) {
|
|
173
|
+
const corpora = uniqueStrings(scope?.corpora);
|
|
174
|
+
if (corpora.length > 0 && !corpora.includes(chunk.corpus))
|
|
175
|
+
return false;
|
|
176
|
+
if (!matchesTimeScope(chunk, scope))
|
|
177
|
+
return false;
|
|
178
|
+
const sourceIds = uniqueStrings(scope?.sourceIds);
|
|
179
|
+
if (sourceIds.length > 0 &&
|
|
180
|
+
!chunkSources(chunk).some((source) => source.sourceId && sourceIds.includes(source.sourceId))) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const sourceRefs = uniqueStrings(scope?.sourceRefs);
|
|
184
|
+
if (sourceRefs.length > 0 &&
|
|
185
|
+
!chunkSources(chunk).some((source) => source.sourceRef && sourceRefs.includes(source.sourceRef))) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
function matchesTimeScope(chunk, scope) {
|
|
191
|
+
const after = parseIsoTime(scope?.after);
|
|
192
|
+
const before = parseIsoTime(scope?.before);
|
|
193
|
+
if (after === null && before === null)
|
|
194
|
+
return true;
|
|
195
|
+
const timestamp = chunkTimestamp(chunk);
|
|
196
|
+
if (timestamp === null)
|
|
197
|
+
return false;
|
|
198
|
+
if (after !== null && timestamp < after)
|
|
199
|
+
return false;
|
|
200
|
+
if (before !== null && timestamp > before)
|
|
201
|
+
return false;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
function chunkTimestamp(chunk) {
|
|
205
|
+
const raw = firstMetadataValue(chunk.metadata, [
|
|
206
|
+
"timestamp",
|
|
207
|
+
"happenedAt",
|
|
208
|
+
"coverageToHappenedAt",
|
|
209
|
+
"coverageFromHappenedAt",
|
|
210
|
+
]);
|
|
211
|
+
if (typeof raw === "string") {
|
|
212
|
+
const parsed = Date.parse(raw);
|
|
213
|
+
if (Number.isFinite(parsed))
|
|
214
|
+
return parsed;
|
|
215
|
+
}
|
|
216
|
+
if (typeof raw === "number" && Number.isFinite(raw))
|
|
217
|
+
return raw < 10_000_000_000 ? raw * 1000 : raw;
|
|
218
|
+
return chunk.createdAt < 10_000_000_000 ? chunk.createdAt * 1000 : chunk.createdAt;
|
|
219
|
+
}
|
|
220
|
+
function firstMetadataValue(metadata, keys) {
|
|
221
|
+
if (!metadata)
|
|
222
|
+
return null;
|
|
223
|
+
for (const key of keys) {
|
|
224
|
+
const value = metadata[key];
|
|
225
|
+
if (typeof value === "string" || typeof value === "number")
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
function parseIsoTime(value) {
|
|
231
|
+
if (!value)
|
|
232
|
+
return null;
|
|
233
|
+
const parsed = Date.parse(value);
|
|
234
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
235
|
+
}
|
|
236
|
+
function chunkSources(chunk) {
|
|
237
|
+
return chunk.sources.length > 0 || !chunk.sourceId
|
|
238
|
+
? chunk.sources
|
|
239
|
+
: [{ corpus: chunk.corpus, sourceId: chunk.sourceId, sourceRef: chunk.sourceRef, chunkIndex: chunk.chunkIndex }];
|
|
240
|
+
}
|
|
241
|
+
function uniqueStrings(values) {
|
|
242
|
+
return Array.from(new Set(values?.filter((value) => value.trim()) ?? []));
|
|
243
|
+
}
|
|
244
|
+
function positiveIntegerOrDefault(value, fallback) {
|
|
245
|
+
return value !== undefined && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
246
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { loadSqliteVec } from "./vec.js";
|
|
2
|
+
const SCHEMA_VERSION = 4;
|
|
3
|
+
export function runMemoryIndexMigrations(db, options) {
|
|
4
|
+
db.pragma("journal_mode = WAL");
|
|
5
|
+
db.pragma("foreign_keys = ON");
|
|
6
|
+
const vec = loadSqliteVec(db);
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS memory_meta (
|
|
9
|
+
k TEXT PRIMARY KEY,
|
|
10
|
+
v TEXT NOT NULL
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS memory_chunks (
|
|
14
|
+
id INTEGER PRIMARY KEY,
|
|
15
|
+
content_hash TEXT NOT NULL UNIQUE,
|
|
16
|
+
corpus TEXT NOT NULL,
|
|
17
|
+
text_full TEXT NOT NULL,
|
|
18
|
+
snippet TEXT NOT NULL,
|
|
19
|
+
token_count INTEGER,
|
|
20
|
+
metadata_json TEXT,
|
|
21
|
+
embedding_model TEXT NOT NULL,
|
|
22
|
+
embedding_dimensions INTEGER NOT NULL,
|
|
23
|
+
embedding BLOB NOT NULL,
|
|
24
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
25
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_hash ON memory_chunks(content_hash);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_memory_chunks_model ON memory_chunks(embedding_model, embedding_dimensions);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS memory_index_sources (
|
|
32
|
+
chunk_id INTEGER NOT NULL REFERENCES memory_chunks(id) ON DELETE CASCADE,
|
|
33
|
+
corpus TEXT NOT NULL,
|
|
34
|
+
source_id TEXT NOT NULL,
|
|
35
|
+
source_ref TEXT,
|
|
36
|
+
chunk_index INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
PRIMARY KEY(corpus, source_id, chunk_index)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_memory_index_sources_chunk ON memory_index_sources(chunk_id);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS memory_index_source_state (
|
|
43
|
+
corpus TEXT NOT NULL,
|
|
44
|
+
source_id TEXT NOT NULL,
|
|
45
|
+
source_ref TEXT,
|
|
46
|
+
mtime_ms INTEGER NOT NULL,
|
|
47
|
+
size_bytes INTEGER NOT NULL,
|
|
48
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
49
|
+
PRIMARY KEY(corpus, source_id)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Contentless FTS avoids SQLite maintaining shadow copies or stale external-content rows.
|
|
53
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
54
|
+
text_full,
|
|
55
|
+
snippet,
|
|
56
|
+
content='',
|
|
57
|
+
contentless_delete=1
|
|
58
|
+
);
|
|
59
|
+
`);
|
|
60
|
+
migrateMemoryIndexSources(db);
|
|
61
|
+
reconcileEmbeddingConfig(db, options);
|
|
62
|
+
const vectorCapability = reconcileVectorTable(db, options, vec.available);
|
|
63
|
+
writeMeta(db, "schema_version", String(SCHEMA_VERSION));
|
|
64
|
+
writeMeta(db, "embedding_provider", options.embeddingProvider);
|
|
65
|
+
writeMeta(db, "embedding_model", options.embeddingModel);
|
|
66
|
+
writeMeta(db, "embedding_dimensions", String(options.embeddingDimensions));
|
|
67
|
+
writeMeta(db, "vector_capability", vectorCapability);
|
|
68
|
+
}
|
|
69
|
+
export function readMeta(db, key) {
|
|
70
|
+
const row = db.prepare("SELECT v FROM memory_meta WHERE k = ?").get(key);
|
|
71
|
+
return row?.v ?? null;
|
|
72
|
+
}
|
|
73
|
+
export function writeMeta(db, key, value) {
|
|
74
|
+
db.prepare(`INSERT INTO memory_meta(k, v) VALUES (?, ?)
|
|
75
|
+
ON CONFLICT(k) DO UPDATE SET v = excluded.v`).run(key, value);
|
|
76
|
+
}
|
|
77
|
+
function reconcileEmbeddingConfig(db, options) {
|
|
78
|
+
const model = readMeta(db, "embedding_model");
|
|
79
|
+
const dimensions = readMeta(db, "embedding_dimensions");
|
|
80
|
+
if ((model && model !== options.embeddingModel) ||
|
|
81
|
+
(dimensions && dimensions !== String(options.embeddingDimensions))) {
|
|
82
|
+
db.transaction(() => {
|
|
83
|
+
db.prepare("DELETE FROM memory_fts").run();
|
|
84
|
+
db.prepare("DROP TRIGGER IF EXISTS trg_memory_chunks_delete_vec").run();
|
|
85
|
+
db.prepare("DROP TABLE IF EXISTS memory_vec").run();
|
|
86
|
+
db.prepare("DELETE FROM memory_index_source_state").run();
|
|
87
|
+
db.prepare("DELETE FROM memory_index_sources").run();
|
|
88
|
+
db.prepare("DELETE FROM memory_chunks").run();
|
|
89
|
+
writeMeta(db, "requires_reindex", "1");
|
|
90
|
+
}).immediate();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function reconcileVectorTable(db, options, sqliteVecAvailable) {
|
|
94
|
+
const previousCapability = readMeta(db, "vector_capability");
|
|
95
|
+
if (!sqliteVecAvailable) {
|
|
96
|
+
db.prepare("DROP TRIGGER IF EXISTS trg_memory_chunks_delete_vec").run();
|
|
97
|
+
return "blob-js";
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
db.transaction(() => {
|
|
101
|
+
const hadVectorTable = tableExists(db, "memory_vec");
|
|
102
|
+
if (!hadVectorTable) {
|
|
103
|
+
db.prepare(`CREATE VIRTUAL TABLE memory_vec USING vec0(
|
|
104
|
+
embedding float[${options.embeddingDimensions}] distance_metric=cosine
|
|
105
|
+
)`).run();
|
|
106
|
+
}
|
|
107
|
+
if (previousCapability === "blob-js") {
|
|
108
|
+
db.prepare("DELETE FROM memory_vec").run();
|
|
109
|
+
db.prepare("INSERT INTO memory_vec(rowid, embedding) SELECT id, embedding FROM memory_chunks").run();
|
|
110
|
+
}
|
|
111
|
+
// Virtual tables cannot own FK constraints, so this mirrors ON DELETE
|
|
112
|
+
// CASCADE for direct memory_chunks deletes while sqlite-vec is loaded.
|
|
113
|
+
db.prepare(`CREATE TRIGGER IF NOT EXISTS trg_memory_chunks_delete_vec
|
|
114
|
+
AFTER DELETE ON memory_chunks
|
|
115
|
+
BEGIN
|
|
116
|
+
DELETE FROM memory_vec WHERE rowid = old.id;
|
|
117
|
+
END`).run();
|
|
118
|
+
}).immediate();
|
|
119
|
+
return "sqlite-vec";
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
db.prepare("DROP TRIGGER IF EXISTS trg_memory_chunks_delete_vec").run();
|
|
123
|
+
return "blob-js";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function migrateMemoryIndexSources(db) {
|
|
127
|
+
const columns = db.prepare("PRAGMA table_info(memory_chunks)").all();
|
|
128
|
+
const hasSourceColumns = columns.some((column) => column.name === "source_id");
|
|
129
|
+
if (hasSourceColumns) {
|
|
130
|
+
db.transaction(() => {
|
|
131
|
+
db.prepare(`INSERT OR IGNORE INTO memory_index_sources(chunk_id, corpus, source_id, source_ref, chunk_index)
|
|
132
|
+
SELECT id, corpus, source_id, source_ref, chunk_index
|
|
133
|
+
FROM memory_chunks
|
|
134
|
+
WHERE source_id IS NOT NULL`).run();
|
|
135
|
+
}).immediate();
|
|
136
|
+
}
|
|
137
|
+
const ftsSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_fts'").get();
|
|
138
|
+
if (ftsSql && (ftsSql.sql.includes("content='memory_chunks'") || !ftsSql.sql.includes("contentless_delete=1"))) {
|
|
139
|
+
db.transaction(() => {
|
|
140
|
+
db.prepare("DROP TABLE memory_fts").run();
|
|
141
|
+
db.prepare(`CREATE VIRTUAL TABLE memory_fts USING fts5(
|
|
142
|
+
text_full,
|
|
143
|
+
snippet,
|
|
144
|
+
content='',
|
|
145
|
+
contentless_delete=1
|
|
146
|
+
)`).run();
|
|
147
|
+
const rows = db.prepare("SELECT id, text_full, snippet FROM memory_chunks").all();
|
|
148
|
+
const insert = db.prepare("INSERT INTO memory_fts(rowid, text_full, snippet) VALUES (?, ?, ?)");
|
|
149
|
+
for (const row of rows)
|
|
150
|
+
insert.run(row.id, row.text_full, row.snippet);
|
|
151
|
+
}).immediate();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function tableExists(db, name) {
|
|
155
|
+
const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
|
|
156
|
+
return !!row;
|
|
157
|
+
}
|