@mnemoai/core 1.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.
Files changed (49) hide show
  1. package/index.ts +3395 -0
  2. package/openclaw.plugin.json +815 -0
  3. package/package.json +59 -0
  4. package/src/access-tracker.ts +341 -0
  5. package/src/adapters/README.md +78 -0
  6. package/src/adapters/chroma.ts +206 -0
  7. package/src/adapters/lancedb.ts +237 -0
  8. package/src/adapters/pgvector.ts +218 -0
  9. package/src/adapters/qdrant.ts +191 -0
  10. package/src/adaptive-retrieval.ts +90 -0
  11. package/src/audit-log.ts +238 -0
  12. package/src/chunker.ts +254 -0
  13. package/src/config.ts +271 -0
  14. package/src/decay-engine.ts +238 -0
  15. package/src/embedder.ts +735 -0
  16. package/src/extraction-prompts.ts +339 -0
  17. package/src/license.ts +258 -0
  18. package/src/llm-client.ts +125 -0
  19. package/src/mcp-server.ts +415 -0
  20. package/src/memory-categories.ts +71 -0
  21. package/src/memory-upgrader.ts +388 -0
  22. package/src/migrate.ts +364 -0
  23. package/src/mnemo.ts +142 -0
  24. package/src/noise-filter.ts +97 -0
  25. package/src/noise-prototypes.ts +164 -0
  26. package/src/observability.ts +81 -0
  27. package/src/query-tracker.ts +57 -0
  28. package/src/reflection-event-store.ts +98 -0
  29. package/src/reflection-item-store.ts +112 -0
  30. package/src/reflection-mapped-metadata.ts +84 -0
  31. package/src/reflection-metadata.ts +23 -0
  32. package/src/reflection-ranking.ts +33 -0
  33. package/src/reflection-retry.ts +181 -0
  34. package/src/reflection-slices.ts +265 -0
  35. package/src/reflection-store.ts +602 -0
  36. package/src/resonance-state.ts +85 -0
  37. package/src/retriever.ts +1510 -0
  38. package/src/scopes.ts +375 -0
  39. package/src/self-improvement-files.ts +143 -0
  40. package/src/semantic-gate.ts +121 -0
  41. package/src/session-recovery.ts +138 -0
  42. package/src/smart-extractor.ts +923 -0
  43. package/src/smart-metadata.ts +561 -0
  44. package/src/storage-adapter.ts +153 -0
  45. package/src/store.ts +1330 -0
  46. package/src/tier-manager.ts +189 -0
  47. package/src/tools.ts +1292 -0
  48. package/src/wal-recovery.ts +172 -0
  49. package/test/core.test.mjs +301 -0
@@ -0,0 +1,237 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * LanceDB Storage Adapter — Default backend for Mnemo.
4
+ *
5
+ * Implements StorageAdapter using @lancedb/lancedb.
6
+ * This is the reference implementation; other backends should
7
+ * produce equivalent behavior.
8
+ */
9
+
10
+ import type {
11
+ StorageAdapter,
12
+ MemoryRecord,
13
+ SearchResult,
14
+ QueryOptions,
15
+ } from "../storage-adapter.js";
16
+ import { registerAdapter } from "../storage-adapter.js";
17
+
18
+ /** Strict allowlist sanitizer — prevents SQL injection in LanceDB filters */
19
+ function sanitize(value: string): string {
20
+ if (typeof value !== "string") return "";
21
+ return value.replace(/[^a-zA-Z0-9\-_.:@ \u4e00-\u9fff\u3400-\u4dbf]/g, "");
22
+ }
23
+
24
+ // Dynamic import to avoid hard dependency at module level
25
+ let _lancedb: typeof import("@lancedb/lancedb") | null = null;
26
+
27
+ async function loadLanceDB() {
28
+ if (!_lancedb) {
29
+ _lancedb = await import("@lancedb/lancedb");
30
+ }
31
+ return _lancedb;
32
+ }
33
+
34
+ const TABLE_NAME = "memories";
35
+
36
+ export class LanceDBAdapter implements StorageAdapter {
37
+ readonly name = "lancedb";
38
+
39
+ private db: any = null;
40
+ private table: any = null;
41
+ private ftsReady = false;
42
+ private vectorDim = 0;
43
+
44
+ async connect(dbPath: string): Promise<void> {
45
+ const lancedb = await loadLanceDB();
46
+ this.db = await lancedb.connect(dbPath);
47
+ }
48
+
49
+ async ensureTable(vectorDimensions: number): Promise<void> {
50
+ this.vectorDim = vectorDimensions;
51
+
52
+ try {
53
+ this.table = await this.db.openTable(TABLE_NAME);
54
+ } catch {
55
+ // Table doesn't exist — create with schema
56
+ const lancedb = await loadLanceDB();
57
+ const schemaEntry: MemoryRecord = {
58
+ id: "__schema__",
59
+ text: "",
60
+ vector: new Array(vectorDimensions).fill(0),
61
+ timestamp: 0,
62
+ scope: "global",
63
+ importance: 0,
64
+ category: "other",
65
+ metadata: "{}",
66
+ };
67
+
68
+ try {
69
+ this.table = await this.db.createTable(TABLE_NAME, [schemaEntry]);
70
+ await this.table.delete('id = "__schema__"');
71
+ } catch (err) {
72
+ if (String(err).includes("already exists")) {
73
+ this.table = await this.db.openTable(TABLE_NAME);
74
+ } else {
75
+ throw err;
76
+ }
77
+ }
78
+ }
79
+
80
+ // Validate dimensions
81
+ const sample = await this.table.query().limit(1).toArray();
82
+ if (sample.length > 0 && sample[0]?.vector?.length) {
83
+ const existing = sample[0].vector.length;
84
+ if (existing !== vectorDimensions) {
85
+ throw new Error(
86
+ `Vector dimension mismatch: table=${existing}, config=${vectorDimensions}`
87
+ );
88
+ }
89
+ }
90
+
91
+ // Create FTS index
92
+ await this.ensureFullTextIndex();
93
+ }
94
+
95
+ async add(records: MemoryRecord[]): Promise<void> {
96
+ if (!this.table) throw new Error("Table not initialized");
97
+ await this.table.add(records);
98
+ }
99
+
100
+ async update(id: string, record: MemoryRecord): Promise<void> {
101
+ if (!this.table) throw new Error("Table not initialized");
102
+ await this.table.delete(`id = '${sanitize(id)}'`);
103
+ await this.table.add([record]);
104
+ }
105
+
106
+ async delete(filter: string): Promise<void> {
107
+ if (!this.table) throw new Error("Table not initialized");
108
+ await this.table.delete(filter);
109
+ }
110
+
111
+ async vectorSearch(
112
+ vector: number[],
113
+ limit: number,
114
+ minScore = 0,
115
+ scopeFilter?: string[],
116
+ ): Promise<SearchResult[]> {
117
+ if (!this.table) throw new Error("Table not initialized");
118
+
119
+ let query = this.table.vectorSearch(vector).distanceType("cosine").limit(limit * 3);
120
+
121
+ if (scopeFilter?.length) {
122
+ const scopeExpr = scopeFilter.map((s) => `'${sanitize(s)}'`).join(", ");
123
+ query = query.where(`scope IN (${scopeExpr})`);
124
+ }
125
+
126
+ const raw = await query.toArray();
127
+
128
+ return raw
129
+ .map((row: any) => {
130
+ const distance = row._distance ?? row.distance ?? 1;
131
+ const score = 1 / (1 + distance);
132
+ return { record: this.toRecord(row), score };
133
+ })
134
+ .filter((r: SearchResult) => r.score >= minScore)
135
+ .slice(0, limit);
136
+ }
137
+
138
+ async fullTextSearch(
139
+ queryText: string,
140
+ limit: number,
141
+ scopeFilter?: string[],
142
+ ): Promise<SearchResult[]> {
143
+ if (!this.table || !this.ftsReady) return [];
144
+
145
+ let query = this.table.search(queryText, "fts").limit(limit * 2);
146
+
147
+ if (scopeFilter?.length) {
148
+ const scopeExpr = scopeFilter.map((s) => `'${sanitize(s)}'`).join(", ");
149
+ query = query.where(`scope IN (${scopeExpr})`);
150
+ }
151
+
152
+ const raw = await query.toArray();
153
+
154
+ return raw
155
+ .map((row: any) => {
156
+ const score = row._relevance_score ?? row.score ?? 0.5;
157
+ return { record: this.toRecord(row), score };
158
+ })
159
+ .slice(0, limit);
160
+ }
161
+
162
+ async query(options: QueryOptions): Promise<MemoryRecord[]> {
163
+ if (!this.table) throw new Error("Table not initialized");
164
+
165
+ let q = this.table.query();
166
+
167
+ if (options.select?.length) {
168
+ q = q.select(options.select);
169
+ }
170
+ if (options.where) {
171
+ q = q.where(options.where);
172
+ }
173
+ if (options.limit) {
174
+ q = q.limit(options.limit);
175
+ }
176
+
177
+ const raw = await q.toArray();
178
+ return raw.map((row: any) => this.toRecord(row));
179
+ }
180
+
181
+ async count(filter?: string): Promise<number> {
182
+ if (!this.table) throw new Error("Table not initialized");
183
+ let q = this.table.query();
184
+ if (filter) q = q.where(filter);
185
+ const rows = await q.toArray();
186
+ return rows.length;
187
+ }
188
+
189
+ async ensureFullTextIndex(): Promise<void> {
190
+ if (!this.table) return;
191
+
192
+ try {
193
+ const indices = await this.table.listIndices();
194
+ const hasFts = indices?.some(
195
+ (idx: any) => idx.indexType === "FTS" || idx.columns?.includes("text"),
196
+ );
197
+
198
+ if (!hasFts) {
199
+ const lancedb = await loadLanceDB();
200
+ await this.table.createIndex("text", {
201
+ config: (lancedb as any).Index.fts(),
202
+ });
203
+ }
204
+ this.ftsReady = true;
205
+ } catch {
206
+ this.ftsReady = false;
207
+ }
208
+ }
209
+
210
+ hasFullTextSearch(): boolean {
211
+ return this.ftsReady;
212
+ }
213
+
214
+ async close(): Promise<void> {
215
+ this.table = null;
216
+ this.db = null;
217
+ }
218
+
219
+ // ── Helpers ──
220
+
221
+ private toRecord(row: any): MemoryRecord {
222
+ return {
223
+ id: row.id,
224
+ text: row.text,
225
+ vector: row.vector ? Array.from(row.vector) : [],
226
+ timestamp: row.timestamp ?? 0,
227
+ scope: row.scope ?? "global",
228
+ importance: row.importance ?? 0.5,
229
+ category: row.category ?? "other",
230
+ metadata: row.metadata ?? "{}",
231
+ ...row, // preserve extra fields
232
+ };
233
+ }
234
+ }
235
+
236
+ // ── Auto-register ──
237
+ registerAdapter("lancedb", () => new LanceDBAdapter());
@@ -0,0 +1,218 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * PGVector Storage Adapter for Mnemo
4
+ *
5
+ * Requirements:
6
+ * npm install pg pgvector
7
+ * PostgreSQL with pgvector extension enabled
8
+ *
9
+ * Config:
10
+ * storage: "pgvector"
11
+ * storageConfig: { connectionString: "postgres://user:pass@localhost:5432/mnemo" }
12
+ */
13
+
14
+ import type {
15
+ StorageAdapter,
16
+ MemoryRecord,
17
+ SearchResult,
18
+ QueryOptions,
19
+ } from "../storage-adapter.js";
20
+ import { registerAdapter } from "../storage-adapter.js";
21
+
22
+ const TABLE = "mnemo_memories";
23
+
24
+ export class PGVectorAdapter implements StorageAdapter {
25
+ readonly name = "pgvector";
26
+
27
+ private pool: any = null;
28
+ private vectorDim = 0;
29
+ private connectionString: string;
30
+
31
+ constructor(config?: Record<string, unknown>) {
32
+ this.connectionString = (config?.connectionString as string) ||
33
+ "postgres://localhost:5432/mnemo";
34
+ }
35
+
36
+ async connect(dbPath: string): Promise<void> {
37
+ const { Pool } = await import("pg");
38
+ this.pool = new Pool({
39
+ connectionString: dbPath || this.connectionString,
40
+ });
41
+
42
+ // Enable pgvector extension
43
+ await this.pool.query("CREATE EXTENSION IF NOT EXISTS vector");
44
+ }
45
+
46
+ async ensureTable(vectorDimensions: number): Promise<void> {
47
+ this.vectorDim = vectorDimensions;
48
+
49
+ await this.pool.query(`
50
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
51
+ id TEXT PRIMARY KEY,
52
+ text TEXT NOT NULL,
53
+ vector vector(${vectorDimensions}),
54
+ timestamp BIGINT DEFAULT 0,
55
+ scope TEXT DEFAULT 'global',
56
+ importance REAL DEFAULT 0.5,
57
+ category TEXT DEFAULT 'other',
58
+ metadata JSONB DEFAULT '{}'::jsonb
59
+ )
60
+ `);
61
+
62
+ // Create HNSW index for vector search
63
+ await this.pool.query(`
64
+ CREATE INDEX IF NOT EXISTS ${TABLE}_vector_idx
65
+ ON ${TABLE} USING hnsw (vector vector_cosine_ops)
66
+ `).catch(() => {}); // ignore if already exists
67
+
68
+ // Create GIN index for full-text search
69
+ await this.pool.query(`
70
+ CREATE INDEX IF NOT EXISTS ${TABLE}_text_idx
71
+ ON ${TABLE} USING gin (to_tsvector('simple', text))
72
+ `).catch(() => {});
73
+
74
+ // Create index on scope for filtering
75
+ await this.pool.query(`
76
+ CREATE INDEX IF NOT EXISTS ${TABLE}_scope_idx ON ${TABLE} (scope)
77
+ `).catch(() => {});
78
+ }
79
+
80
+ async add(records: MemoryRecord[]): Promise<void> {
81
+ for (const r of records) {
82
+ await this.pool.query(
83
+ `INSERT INTO ${TABLE} (id, text, vector, timestamp, scope, importance, category, metadata)
84
+ VALUES ($1, $2, $3::vector, $4, $5, $6, $7, $8)
85
+ ON CONFLICT (id) DO UPDATE SET
86
+ text = EXCLUDED.text,
87
+ vector = EXCLUDED.vector,
88
+ timestamp = EXCLUDED.timestamp,
89
+ scope = EXCLUDED.scope,
90
+ importance = EXCLUDED.importance,
91
+ category = EXCLUDED.category,
92
+ metadata = EXCLUDED.metadata`,
93
+ [r.id, r.text, `[${r.vector.join(",")}]`, r.timestamp, r.scope, r.importance, r.category, r.metadata],
94
+ );
95
+ }
96
+ }
97
+
98
+ async update(id: string, record: MemoryRecord): Promise<void> {
99
+ await this.add([record]);
100
+ }
101
+
102
+ async delete(filter: string): Promise<void> {
103
+ const idMatch = filter.match(/id\s*=\s*'([^']+)'/);
104
+ if (idMatch) {
105
+ await this.pool.query(`DELETE FROM ${TABLE} WHERE id = $1`, [idMatch[1]]);
106
+ } else {
107
+ // Pass filter as-is for simple SQL WHERE clauses
108
+ await this.pool.query(`DELETE FROM ${TABLE} WHERE ${filter}`);
109
+ }
110
+ }
111
+
112
+ async vectorSearch(
113
+ vector: number[],
114
+ limit: number,
115
+ minScore = 0,
116
+ scopeFilter?: string[],
117
+ ): Promise<SearchResult[]> {
118
+ const vectorStr = `[${vector.join(",")}]`;
119
+ let query = `
120
+ SELECT *, 1 - (vector <=> $1::vector) AS score
121
+ FROM ${TABLE}
122
+ WHERE 1 - (vector <=> $1::vector) >= $2
123
+ `;
124
+ const params: any[] = [vectorStr, minScore];
125
+
126
+ if (scopeFilter?.length) {
127
+ query += ` AND scope = ANY($3)`;
128
+ params.push(scopeFilter);
129
+ }
130
+
131
+ query += ` ORDER BY vector <=> $1::vector LIMIT $${params.length + 1}`;
132
+ params.push(limit);
133
+
134
+ const result = await this.pool.query(query, params);
135
+
136
+ return result.rows.map((row: any) => ({
137
+ record: this.toRecord(row),
138
+ score: parseFloat(row.score),
139
+ }));
140
+ }
141
+
142
+ async fullTextSearch(
143
+ queryText: string,
144
+ limit: number,
145
+ scopeFilter?: string[],
146
+ ): Promise<SearchResult[]> {
147
+ // Use PostgreSQL full-text search with ts_rank
148
+ let query = `
149
+ SELECT *, ts_rank(to_tsvector('simple', text), plainto_tsquery('simple', $1)) AS score
150
+ FROM ${TABLE}
151
+ WHERE to_tsvector('simple', text) @@ plainto_tsquery('simple', $1)
152
+ `;
153
+ const params: any[] = [queryText];
154
+
155
+ if (scopeFilter?.length) {
156
+ query += ` AND scope = ANY($2)`;
157
+ params.push(scopeFilter);
158
+ }
159
+
160
+ query += ` ORDER BY score DESC LIMIT $${params.length + 1}`;
161
+ params.push(limit);
162
+
163
+ const result = await this.pool.query(query, params);
164
+
165
+ return result.rows.map((row: any) => ({
166
+ record: this.toRecord(row),
167
+ score: parseFloat(row.score),
168
+ }));
169
+ }
170
+
171
+ async query(options: QueryOptions): Promise<MemoryRecord[]> {
172
+ let query = `SELECT * FROM ${TABLE}`;
173
+ if (options.where) query += ` WHERE ${options.where}`;
174
+ query += ` LIMIT ${options.limit || 100}`;
175
+
176
+ const result = await this.pool.query(query);
177
+ return result.rows.map((row: any) => this.toRecord(row));
178
+ }
179
+
180
+ async count(filter?: string): Promise<number> {
181
+ let query = `SELECT COUNT(*) FROM ${TABLE}`;
182
+ if (filter) query += ` WHERE ${filter}`;
183
+ const result = await this.pool.query(query);
184
+ return parseInt(result.rows[0].count);
185
+ }
186
+
187
+ async ensureFullTextIndex(): Promise<void> {
188
+ // Created in ensureTable
189
+ }
190
+
191
+ hasFullTextSearch(): boolean {
192
+ return true; // PostgreSQL has native full-text search
193
+ }
194
+
195
+ async close(): Promise<void> {
196
+ if (this.pool) {
197
+ await this.pool.end();
198
+ this.pool = null;
199
+ }
200
+ }
201
+
202
+ // ── Helpers ──
203
+
204
+ private toRecord(row: any): MemoryRecord {
205
+ return {
206
+ id: row.id,
207
+ text: row.text,
208
+ vector: row.vector ? (typeof row.vector === "string" ? JSON.parse(row.vector) : Array.from(row.vector)) : [],
209
+ timestamp: parseInt(row.timestamp) || 0,
210
+ scope: row.scope ?? "global",
211
+ importance: parseFloat(row.importance) || 0.5,
212
+ category: row.category ?? "other",
213
+ metadata: typeof row.metadata === "object" ? JSON.stringify(row.metadata) : (row.metadata ?? "{}"),
214
+ };
215
+ }
216
+ }
217
+
218
+ registerAdapter("pgvector", (config) => new PGVectorAdapter(config));
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Qdrant Storage Adapter for Mnemo
4
+ *
5
+ * Requirements:
6
+ * npm install @qdrant/js-client-rest
7
+ *
8
+ * Config:
9
+ * storage: "qdrant"
10
+ * storageConfig: { url: "http://localhost:6333", apiKey?: "..." }
11
+ */
12
+
13
+ import type {
14
+ StorageAdapter,
15
+ MemoryRecord,
16
+ SearchResult,
17
+ QueryOptions,
18
+ } from "../storage-adapter.js";
19
+ import { registerAdapter } from "../storage-adapter.js";
20
+
21
+ const COLLECTION = "mnemo_memories";
22
+
23
+ export class QdrantAdapter implements StorageAdapter {
24
+ readonly name = "qdrant";
25
+
26
+ private client: any = null;
27
+ private url: string = "http://localhost:6333";
28
+ private apiKey?: string;
29
+ private vectorDim = 0;
30
+
31
+ constructor(config?: Record<string, unknown>) {
32
+ if (config?.url) this.url = config.url as string;
33
+ if (config?.apiKey) this.apiKey = config.apiKey as string;
34
+ }
35
+
36
+ async connect(): Promise<void> {
37
+ const { QdrantClient } = await import("@qdrant/js-client-rest");
38
+ this.client = new QdrantClient({
39
+ url: this.url,
40
+ ...(this.apiKey ? { apiKey: this.apiKey } : {}),
41
+ });
42
+ }
43
+
44
+ async ensureTable(vectorDimensions: number): Promise<void> {
45
+ this.vectorDim = vectorDimensions;
46
+ const collections = await this.client.getCollections();
47
+ const exists = collections.collections.some((c: any) => c.name === COLLECTION);
48
+
49
+ if (!exists) {
50
+ await this.client.createCollection(COLLECTION, {
51
+ vectors: { size: vectorDimensions, distance: "Cosine" },
52
+ });
53
+ // Create payload indices for filtering
54
+ await this.client.createPayloadIndex(COLLECTION, {
55
+ field_name: "scope",
56
+ field_schema: "keyword",
57
+ });
58
+ await this.client.createPayloadIndex(COLLECTION, {
59
+ field_name: "category",
60
+ field_schema: "keyword",
61
+ });
62
+ }
63
+ }
64
+
65
+ async add(records: MemoryRecord[]): Promise<void> {
66
+ const points = records.map((r) => ({
67
+ id: r.id,
68
+ vector: r.vector,
69
+ payload: {
70
+ text: r.text,
71
+ timestamp: r.timestamp,
72
+ scope: r.scope,
73
+ importance: r.importance,
74
+ category: r.category,
75
+ metadata: r.metadata,
76
+ },
77
+ }));
78
+ await this.client.upsert(COLLECTION, { points });
79
+ }
80
+
81
+ async update(id: string, record: MemoryRecord): Promise<void> {
82
+ await this.add([record]);
83
+ }
84
+
85
+ async delete(filter: string): Promise<void> {
86
+ // Parse simple "id = 'xxx'" filter
87
+ const idMatch = filter.match(/id\s*=\s*'([^']+)'/);
88
+ if (idMatch) {
89
+ await this.client.delete(COLLECTION, {
90
+ points: [idMatch[1]],
91
+ });
92
+ }
93
+ }
94
+
95
+ async vectorSearch(
96
+ vector: number[],
97
+ limit: number,
98
+ minScore = 0,
99
+ scopeFilter?: string[],
100
+ ): Promise<SearchResult[]> {
101
+ const filter = scopeFilter?.length
102
+ ? { must: [{ key: "scope", match: { any: scopeFilter } }] }
103
+ : undefined;
104
+
105
+ const results = await this.client.search(COLLECTION, {
106
+ vector,
107
+ limit,
108
+ with_payload: true,
109
+ score_threshold: minScore,
110
+ ...(filter ? { filter } : {}),
111
+ });
112
+
113
+ return results.map((r: any) => ({
114
+ record: this.toRecord(r.id, r.payload),
115
+ score: r.score,
116
+ }));
117
+ }
118
+
119
+ async fullTextSearch(
120
+ _query: string,
121
+ _limit: number,
122
+ _scopeFilter?: string[],
123
+ ): Promise<SearchResult[]> {
124
+ // Qdrant doesn't have native BM25 — fall back to empty
125
+ // Users should pair with a separate FTS engine or use vector search
126
+ return [];
127
+ }
128
+
129
+ async query(options: QueryOptions): Promise<MemoryRecord[]> {
130
+ const filter = options.where
131
+ ? this.parseFilter(options.where)
132
+ : undefined;
133
+
134
+ const result = await this.client.scroll(COLLECTION, {
135
+ limit: options.limit || 100,
136
+ with_payload: true,
137
+ with_vectors: true,
138
+ ...(filter ? { filter } : {}),
139
+ });
140
+
141
+ return result.points.map((p: any) => this.toRecord(p.id, p.payload, p.vector));
142
+ }
143
+
144
+ async count(filter?: string): Promise<number> {
145
+ const result = await this.client.count(COLLECTION, {
146
+ ...(filter ? { filter: this.parseFilter(filter) } : {}),
147
+ exact: true,
148
+ });
149
+ return result.count;
150
+ }
151
+
152
+ async ensureFullTextIndex(): Promise<void> {
153
+ // Qdrant uses payload indices, not FTS indices
154
+ // Text search via Qdrant requires external FTS or payload keyword match
155
+ }
156
+
157
+ hasFullTextSearch(): boolean {
158
+ return false; // Qdrant doesn't have native BM25
159
+ }
160
+
161
+ async close(): Promise<void> {
162
+ this.client = null;
163
+ }
164
+
165
+ // ── Helpers ──
166
+
167
+ private toRecord(id: string, payload: any, vector?: number[]): MemoryRecord {
168
+ return {
169
+ id,
170
+ text: payload.text ?? "",
171
+ vector: vector ? Array.from(vector) : [],
172
+ timestamp: payload.timestamp ?? 0,
173
+ scope: payload.scope ?? "global",
174
+ importance: payload.importance ?? 0.5,
175
+ category: payload.category ?? "other",
176
+ metadata: payload.metadata ?? "{}",
177
+ };
178
+ }
179
+
180
+ private parseFilter(where: string): any {
181
+ // Simple parser for common filters
182
+ const scopeMatch = where.match(/scope\s+IN\s*\(([^)]+)\)/i);
183
+ if (scopeMatch) {
184
+ const scopes = scopeMatch[1].split(",").map((s) => s.trim().replace(/'/g, ""));
185
+ return { must: [{ key: "scope", match: { any: scopes } }] };
186
+ }
187
+ return undefined;
188
+ }
189
+ }
190
+
191
+ registerAdapter("qdrant", (config) => new QdrantAdapter(config));