@mnemoai/core 1.1.0 → 1.1.2
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/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.d.ts.map +1 -0
- package/{index.ts → dist/index.js} +537 -1333
- package/dist/index.js.map +7 -0
- package/dist/src/access-tracker.d.ts +97 -0
- package/dist/src/access-tracker.d.ts.map +1 -0
- package/dist/src/access-tracker.js +184 -0
- package/dist/src/access-tracker.js.map +7 -0
- package/dist/src/adapters/chroma.d.ts +31 -0
- package/dist/src/adapters/chroma.d.ts.map +1 -0
- package/{src/adapters/chroma.ts → dist/src/adapters/chroma.js} +45 -107
- package/dist/src/adapters/chroma.js.map +7 -0
- package/dist/src/adapters/lancedb.d.ts +29 -0
- package/dist/src/adapters/lancedb.d.ts.map +1 -0
- package/{src/adapters/lancedb.ts → dist/src/adapters/lancedb.js} +41 -109
- package/dist/src/adapters/lancedb.js.map +7 -0
- package/dist/src/adapters/pgvector.d.ts +33 -0
- package/dist/src/adapters/pgvector.d.ts.map +1 -0
- package/{src/adapters/pgvector.ts → dist/src/adapters/pgvector.js} +42 -104
- package/dist/src/adapters/pgvector.js.map +7 -0
- package/dist/src/adapters/qdrant.d.ts +34 -0
- package/dist/src/adapters/qdrant.d.ts.map +1 -0
- package/dist/src/adapters/qdrant.js +132 -0
- package/dist/src/adapters/qdrant.js.map +7 -0
- package/dist/src/adaptive-retrieval.d.ts +14 -0
- package/dist/src/adaptive-retrieval.d.ts.map +1 -0
- package/dist/src/adaptive-retrieval.js +52 -0
- package/dist/src/adaptive-retrieval.js.map +7 -0
- package/dist/src/audit-log.d.ts +56 -0
- package/dist/src/audit-log.d.ts.map +1 -0
- package/dist/src/audit-log.js +139 -0
- package/dist/src/audit-log.js.map +7 -0
- package/dist/src/chunker.d.ts +45 -0
- package/dist/src/chunker.d.ts.map +1 -0
- package/dist/src/chunker.js +157 -0
- package/dist/src/chunker.js.map +7 -0
- package/dist/src/config.d.ts +70 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +142 -0
- package/dist/src/config.js.map +7 -0
- package/dist/src/decay-engine.d.ts +73 -0
- package/dist/src/decay-engine.d.ts.map +1 -0
- package/dist/src/decay-engine.js +119 -0
- package/dist/src/decay-engine.js.map +7 -0
- package/dist/src/embedder.d.ts +94 -0
- package/dist/src/embedder.d.ts.map +1 -0
- package/{src/embedder.ts → dist/src/embedder.js} +119 -317
- package/dist/src/embedder.js.map +7 -0
- package/dist/src/extraction-prompts.d.ts +12 -0
- package/dist/src/extraction-prompts.d.ts.map +1 -0
- package/dist/src/extraction-prompts.js +311 -0
- package/dist/src/extraction-prompts.js.map +7 -0
- package/dist/src/license.d.ts +29 -0
- package/dist/src/license.d.ts.map +1 -0
- package/{src/license.ts → dist/src/license.js} +42 -113
- package/dist/src/license.js.map +7 -0
- package/dist/src/llm-client.d.ts +23 -0
- package/dist/src/llm-client.d.ts.map +1 -0
- package/{src/llm-client.ts → dist/src/llm-client.js} +22 -55
- package/dist/src/llm-client.js.map +7 -0
- package/dist/src/logger.d.ts +33 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +35 -0
- package/dist/src/logger.js.map +7 -0
- package/dist/src/mcp-server.d.ts +16 -0
- package/dist/src/mcp-server.d.ts.map +1 -0
- package/{src/mcp-server.ts → dist/src/mcp-server.js} +81 -181
- package/dist/src/mcp-server.js.map +7 -0
- package/dist/src/memory-categories.d.ts +40 -0
- package/dist/src/memory-categories.d.ts.map +1 -0
- package/dist/src/memory-categories.js +33 -0
- package/dist/src/memory-categories.js.map +7 -0
- package/dist/src/memory-upgrader.d.ts +71 -0
- package/dist/src/memory-upgrader.d.ts.map +1 -0
- package/dist/src/memory-upgrader.js +238 -0
- package/dist/src/memory-upgrader.js.map +7 -0
- package/dist/src/migrate.d.ts +47 -0
- package/dist/src/migrate.d.ts.map +1 -0
- package/{src/migrate.ts → dist/src/migrate.js} +57 -165
- package/dist/src/migrate.js.map +7 -0
- package/dist/src/mnemo.d.ts +67 -0
- package/dist/src/mnemo.d.ts.map +1 -0
- package/dist/src/mnemo.js +66 -0
- package/dist/src/mnemo.js.map +7 -0
- package/dist/src/noise-filter.d.ts +23 -0
- package/dist/src/noise-filter.d.ts.map +1 -0
- package/dist/src/noise-filter.js +62 -0
- package/dist/src/noise-filter.js.map +7 -0
- package/dist/src/noise-prototypes.d.ts +40 -0
- package/dist/src/noise-prototypes.d.ts.map +1 -0
- package/dist/src/noise-prototypes.js +116 -0
- package/dist/src/noise-prototypes.js.map +7 -0
- package/dist/src/observability.d.ts +16 -0
- package/dist/src/observability.d.ts.map +1 -0
- package/dist/src/observability.js +53 -0
- package/dist/src/observability.js.map +7 -0
- package/dist/src/query-tracker.d.ts +27 -0
- package/dist/src/query-tracker.d.ts.map +1 -0
- package/dist/src/query-tracker.js +32 -0
- package/dist/src/query-tracker.js.map +7 -0
- package/dist/src/reflection-event-store.d.ts +44 -0
- package/dist/src/reflection-event-store.d.ts.map +1 -0
- package/dist/src/reflection-event-store.js +50 -0
- package/dist/src/reflection-event-store.js.map +7 -0
- package/dist/src/reflection-item-store.d.ts +58 -0
- package/dist/src/reflection-item-store.d.ts.map +1 -0
- package/dist/src/reflection-item-store.js +69 -0
- package/dist/src/reflection-item-store.js.map +7 -0
- package/dist/src/reflection-mapped-metadata.d.ts +47 -0
- package/dist/src/reflection-mapped-metadata.d.ts.map +1 -0
- package/dist/src/reflection-mapped-metadata.js +40 -0
- package/dist/src/reflection-mapped-metadata.js.map +7 -0
- package/dist/src/reflection-metadata.d.ts +11 -0
- package/dist/src/reflection-metadata.d.ts.map +1 -0
- package/dist/src/reflection-metadata.js +24 -0
- package/dist/src/reflection-metadata.js.map +7 -0
- package/dist/src/reflection-ranking.d.ts +13 -0
- package/dist/src/reflection-ranking.d.ts.map +1 -0
- package/{src/reflection-ranking.ts → dist/src/reflection-ranking.js} +12 -21
- package/dist/src/reflection-ranking.js.map +7 -0
- package/dist/src/reflection-retry.d.ts +30 -0
- package/dist/src/reflection-retry.d.ts.map +1 -0
- package/{src/reflection-retry.ts → dist/src/reflection-retry.js} +24 -64
- package/dist/src/reflection-retry.js.map +7 -0
- package/dist/src/reflection-slices.d.ts +42 -0
- package/dist/src/reflection-slices.d.ts.map +1 -0
- package/{src/reflection-slices.ts → dist/src/reflection-slices.js} +60 -136
- package/dist/src/reflection-slices.js.map +7 -0
- package/dist/src/reflection-store.d.ts +85 -0
- package/dist/src/reflection-store.d.ts.map +1 -0
- package/dist/src/reflection-store.js +407 -0
- package/dist/src/reflection-store.js.map +7 -0
- package/dist/src/resonance-state.d.ts +19 -0
- package/dist/src/resonance-state.d.ts.map +1 -0
- package/{src/resonance-state.ts → dist/src/resonance-state.js} +13 -42
- package/dist/src/resonance-state.js.map +7 -0
- package/dist/src/retriever.d.ts +228 -0
- package/dist/src/retriever.d.ts.map +1 -0
- package/dist/src/retriever.js +1006 -0
- package/dist/src/retriever.js.map +7 -0
- package/dist/src/scopes.d.ts +58 -0
- package/dist/src/scopes.d.ts.map +1 -0
- package/dist/src/scopes.js +252 -0
- package/dist/src/scopes.js.map +7 -0
- package/dist/src/self-improvement-files.d.ts +20 -0
- package/dist/src/self-improvement-files.d.ts.map +1 -0
- package/{src/self-improvement-files.ts → dist/src/self-improvement-files.js} +24 -49
- package/dist/src/self-improvement-files.js.map +7 -0
- package/dist/src/semantic-gate.d.ts +24 -0
- package/dist/src/semantic-gate.d.ts.map +1 -0
- package/dist/src/semantic-gate.js +86 -0
- package/dist/src/semantic-gate.js.map +7 -0
- package/dist/src/session-recovery.d.ts +9 -0
- package/dist/src/session-recovery.d.ts.map +1 -0
- package/{src/session-recovery.ts → dist/src/session-recovery.js} +40 -57
- package/dist/src/session-recovery.js.map +7 -0
- package/dist/src/smart-extractor.d.ts +107 -0
- package/dist/src/smart-extractor.d.ts.map +1 -0
- package/{src/smart-extractor.ts → dist/src/smart-extractor.js} +130 -383
- package/dist/src/smart-extractor.js.map +7 -0
- package/dist/src/smart-metadata.d.ts +103 -0
- package/dist/src/smart-metadata.d.ts.map +1 -0
- package/dist/src/smart-metadata.js +361 -0
- package/dist/src/smart-metadata.js.map +7 -0
- package/dist/src/storage-adapter.d.ts +102 -0
- package/dist/src/storage-adapter.d.ts.map +1 -0
- package/dist/src/storage-adapter.js +22 -0
- package/dist/src/storage-adapter.js.map +7 -0
- package/dist/src/store.d.ts +108 -0
- package/dist/src/store.d.ts.map +1 -0
- package/dist/src/store.js +939 -0
- package/dist/src/store.js.map +7 -0
- package/dist/src/tier-manager.d.ts +57 -0
- package/dist/src/tier-manager.d.ts.map +1 -0
- package/dist/src/tier-manager.js +80 -0
- package/dist/src/tier-manager.js.map +7 -0
- package/dist/src/tools.d.ts +43 -0
- package/dist/src/tools.d.ts.map +1 -0
- package/dist/src/tools.js +1075 -0
- package/dist/src/tools.js.map +7 -0
- package/dist/src/wal-recovery.d.ts +30 -0
- package/dist/src/wal-recovery.d.ts.map +1 -0
- package/{src/wal-recovery.ts → dist/src/wal-recovery.js} +26 -79
- package/dist/src/wal-recovery.js.map +7 -0
- package/package.json +21 -2
- package/openclaw.plugin.json +0 -815
- package/src/access-tracker.ts +0 -341
- package/src/adapters/README.md +0 -78
- package/src/adapters/qdrant.ts +0 -191
- package/src/adaptive-retrieval.ts +0 -90
- package/src/audit-log.ts +0 -238
- package/src/chunker.ts +0 -254
- package/src/config.ts +0 -271
- package/src/decay-engine.ts +0 -238
- package/src/extraction-prompts.ts +0 -339
- package/src/memory-categories.ts +0 -71
- package/src/memory-upgrader.ts +0 -388
- package/src/mnemo.ts +0 -142
- package/src/noise-filter.ts +0 -97
- package/src/noise-prototypes.ts +0 -164
- package/src/observability.ts +0 -81
- package/src/query-tracker.ts +0 -57
- package/src/reflection-event-store.ts +0 -98
- package/src/reflection-item-store.ts +0 -112
- package/src/reflection-mapped-metadata.ts +0 -84
- package/src/reflection-metadata.ts +0 -23
- package/src/reflection-store.ts +0 -602
- package/src/retriever.ts +0 -1510
- package/src/scopes.ts +0 -375
- package/src/semantic-gate.ts +0 -121
- package/src/smart-metadata.ts +0 -561
- package/src/storage-adapter.ts +0 -153
- package/src/store.ts +0 -1330
- package/src/tier-manager.ts +0 -189
- package/src/tools.ts +0 -1292
- package/test/core.test.mjs +0 -301
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
accessSync,
|
|
5
|
+
constants,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
lstatSync
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
import { buildSmartMetadata, parseSmartMetadata, stringifySmartMetadata } from "./smart-metadata.js";
|
|
12
|
+
import { requirePro } from "./license.js";
|
|
13
|
+
import { log } from "./logger.js";
|
|
14
|
+
let _auditCreate = null;
|
|
15
|
+
let _auditUpdate = null;
|
|
16
|
+
let _auditDelete = null;
|
|
17
|
+
let _auditExpire = null;
|
|
18
|
+
if (requirePro("audit-log")) {
|
|
19
|
+
import("./audit-log.js").then((mod) => {
|
|
20
|
+
_auditCreate = mod.auditCreate;
|
|
21
|
+
_auditUpdate = mod.auditUpdate;
|
|
22
|
+
_auditDelete = mod.auditDelete;
|
|
23
|
+
_auditExpire = mod.auditExpire;
|
|
24
|
+
}).catch(() => {
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
let walAppend = null;
|
|
28
|
+
let walMarkCommitted = null;
|
|
29
|
+
let walMarkFailed = null;
|
|
30
|
+
if (requirePro("wal")) {
|
|
31
|
+
import("./wal-recovery.js").then((mod) => {
|
|
32
|
+
walAppend = mod.walAppend;
|
|
33
|
+
walMarkCommitted = mod.walMarkCommitted;
|
|
34
|
+
walMarkFailed = mod.walMarkFailed;
|
|
35
|
+
}).catch(() => {
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const DEDUP_SIMILARITY_THRESHOLD = 0.92;
|
|
39
|
+
const CONFLICT_SIMILARITY_THRESHOLD = 0.7;
|
|
40
|
+
import { createAdapter, listAdapters } from "./storage-adapter.js";
|
|
41
|
+
let _adaptersLoaded = false;
|
|
42
|
+
async function ensureAdaptersLoaded() {
|
|
43
|
+
if (_adaptersLoaded) return;
|
|
44
|
+
_adaptersLoaded = true;
|
|
45
|
+
const dbg = !!process.env.MNEMO_DEBUG;
|
|
46
|
+
try {
|
|
47
|
+
await import("./adapters/lancedb.js");
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (dbg) log.debug("adapter lancedb not available:", e);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await import("./adapters/qdrant.js");
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (dbg) log.debug("adapter qdrant not available:", e);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await import("./adapters/chroma.js");
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (dbg) log.debug("adapter chroma not available:", e);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await import("./adapters/pgvector.js");
|
|
63
|
+
} catch (e) {
|
|
64
|
+
if (dbg) log.debug("adapter pgvector not available:", e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let lancedbImportPromise = null;
|
|
68
|
+
const loadLanceDB = async () => {
|
|
69
|
+
if (!lancedbImportPromise) {
|
|
70
|
+
lancedbImportPromise = import("@lancedb/lancedb");
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return await lancedbImportPromise;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`mnemo: failed to load LanceDB. ${String(err)}`,
|
|
77
|
+
{ cause: err }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function clampInt(value, min, max) {
|
|
82
|
+
if (!Number.isFinite(value)) return min;
|
|
83
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
84
|
+
}
|
|
85
|
+
function escapeSqlLiteral(value) {
|
|
86
|
+
if (typeof value !== "string") return "";
|
|
87
|
+
return value.replace(/[^a-zA-Z0-9\-_.:@ \u4e00-\u9fff\u3400-\u4dbf]/g, "");
|
|
88
|
+
}
|
|
89
|
+
function normalizeSearchText(value) {
|
|
90
|
+
return value.toLowerCase().trim();
|
|
91
|
+
}
|
|
92
|
+
function scoreLexicalHit(query, candidates) {
|
|
93
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
94
|
+
if (!normalizedQuery) return 0;
|
|
95
|
+
let score = 0;
|
|
96
|
+
for (const candidate of candidates) {
|
|
97
|
+
const normalized = normalizeSearchText(candidate.text);
|
|
98
|
+
if (!normalized) continue;
|
|
99
|
+
if (normalized.includes(normalizedQuery)) {
|
|
100
|
+
score = Math.max(score, Math.min(0.95, 0.72 + normalizedQuery.length * 0.02) * candidate.weight);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return score;
|
|
104
|
+
}
|
|
105
|
+
function validateStoragePath(dbPath) {
|
|
106
|
+
let resolvedPath = dbPath;
|
|
107
|
+
try {
|
|
108
|
+
const stats = lstatSync(dbPath);
|
|
109
|
+
if (stats.isSymbolicLink()) {
|
|
110
|
+
try {
|
|
111
|
+
resolvedPath = realpathSync(dbPath);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const e = err;
|
|
114
|
+
throw new Error(
|
|
115
|
+
`dbPath "${dbPath}" is a symlink whose target does not exist.
|
|
116
|
+
Fix: Create the target directory, or update the symlink to point to a valid path.
|
|
117
|
+
Details: ${e.code || ""} ${e.message}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const e = err;
|
|
123
|
+
if (e?.code === "ENOENT") {
|
|
124
|
+
} else if (typeof e?.message === "string" && e.message.includes("symlink whose target does not exist")) {
|
|
125
|
+
throw err;
|
|
126
|
+
} else {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!existsSync(resolvedPath)) {
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(resolvedPath, { recursive: true });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const e = err;
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Failed to create dbPath directory "${resolvedPath}".
|
|
136
|
+
Fix: Ensure the parent directory "${dirname(resolvedPath)}" exists and is writable,
|
|
137
|
+
or create it manually: mkdir -p "${resolvedPath}"
|
|
138
|
+
Details: ${e.code || ""} ${e.message}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
accessSync(resolvedPath, constants.W_OK);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const e = err;
|
|
146
|
+
throw new Error(
|
|
147
|
+
`dbPath directory "${resolvedPath}" is not writable.
|
|
148
|
+
Fix: Check permissions with: ls -la "${dirname(resolvedPath)}"
|
|
149
|
+
Or grant write access: chmod u+w "${resolvedPath}"
|
|
150
|
+
Details: ${e.code || ""} ${e.message}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return resolvedPath;
|
|
154
|
+
}
|
|
155
|
+
const TABLE_NAME = "memories";
|
|
156
|
+
class MemoryStore {
|
|
157
|
+
constructor(config) {
|
|
158
|
+
this.config = config;
|
|
159
|
+
}
|
|
160
|
+
db = null;
|
|
161
|
+
table = null;
|
|
162
|
+
initPromise = null;
|
|
163
|
+
ftsIndexCreated = false;
|
|
164
|
+
updateQueue = Promise.resolve();
|
|
165
|
+
semanticGateInstance = null;
|
|
166
|
+
/** When using a non-LanceDB adapter, this holds the active adapter instance */
|
|
167
|
+
_adapter = null;
|
|
168
|
+
/** True when using the adapter path (non-LanceDB backends) */
|
|
169
|
+
get usingAdapter() {
|
|
170
|
+
return this._adapter !== null;
|
|
171
|
+
}
|
|
172
|
+
/** Inject a SemanticGate instance (created externally with an Embedder). */
|
|
173
|
+
setSemanticGate(gate) {
|
|
174
|
+
this.semanticGateInstance = gate;
|
|
175
|
+
}
|
|
176
|
+
get dbPath() {
|
|
177
|
+
return this.config.dbPath;
|
|
178
|
+
}
|
|
179
|
+
/** Get the active adapter (null if using legacy LanceDB path) */
|
|
180
|
+
get adapter() {
|
|
181
|
+
return this._adapter;
|
|
182
|
+
}
|
|
183
|
+
/** Whether BM25 full-text search is available */
|
|
184
|
+
get hasFtsSupport() {
|
|
185
|
+
if (this._adapter) return this._adapter.hasFullTextSearch();
|
|
186
|
+
return this.ftsIndexCreated;
|
|
187
|
+
}
|
|
188
|
+
async ensureInitialized() {
|
|
189
|
+
if (this.table || this._adapter) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (this.initPromise) {
|
|
193
|
+
return this.initPromise;
|
|
194
|
+
}
|
|
195
|
+
this.initPromise = this.doInitialize().catch((err) => {
|
|
196
|
+
this.initPromise = null;
|
|
197
|
+
throw err;
|
|
198
|
+
});
|
|
199
|
+
return this.initPromise;
|
|
200
|
+
}
|
|
201
|
+
async doInitialize() {
|
|
202
|
+
const backend = this.config.storageBackend;
|
|
203
|
+
if (backend && backend !== "lancedb") {
|
|
204
|
+
await ensureAdaptersLoaded();
|
|
205
|
+
const available = listAdapters();
|
|
206
|
+
if (!available.includes(backend)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Storage backend "${backend}" not available. Installed: ${available.join(", ")}. Check that the adapter is properly imported.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
this._adapter = createAdapter(backend, this.config.storageConfig);
|
|
212
|
+
await this._adapter.connect(this.config.dbPath);
|
|
213
|
+
await this._adapter.ensureTable(this.config.vectorDim);
|
|
214
|
+
this.ftsIndexCreated = this._adapter.hasFullTextSearch();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const lancedb = await loadLanceDB();
|
|
218
|
+
let db;
|
|
219
|
+
try {
|
|
220
|
+
db = await lancedb.connect(this.config.dbPath);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const e = err;
|
|
223
|
+
const code = e.code || "";
|
|
224
|
+
const message = e.message || String(err);
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Failed to open LanceDB at "${this.config.dbPath}": ${code} ${message}
|
|
227
|
+
Fix: Verify the path exists and is writable. Check parent directory permissions.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
let table;
|
|
231
|
+
try {
|
|
232
|
+
table = await db.openTable(TABLE_NAME);
|
|
233
|
+
try {
|
|
234
|
+
const sample2 = await table.query().limit(1).toArray();
|
|
235
|
+
if (sample2.length > 0 && !("scope" in sample2[0])) {
|
|
236
|
+
log.warn(
|
|
237
|
+
"Adding scope column for backward compatibility with existing data"
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
log.warn("Could not check table schema:", err);
|
|
242
|
+
}
|
|
243
|
+
} catch (_openErr) {
|
|
244
|
+
const schemaEntry = {
|
|
245
|
+
id: "__schema__",
|
|
246
|
+
text: "",
|
|
247
|
+
vector: Array.from({ length: this.config.vectorDim }).fill(
|
|
248
|
+
0
|
|
249
|
+
),
|
|
250
|
+
category: "other",
|
|
251
|
+
scope: "global",
|
|
252
|
+
importance: 0,
|
|
253
|
+
timestamp: 0,
|
|
254
|
+
metadata: "{}"
|
|
255
|
+
};
|
|
256
|
+
try {
|
|
257
|
+
table = await db.createTable(TABLE_NAME, [schemaEntry]);
|
|
258
|
+
await table.delete('id = "__schema__"');
|
|
259
|
+
} catch (createErr) {
|
|
260
|
+
if (String(createErr).includes("already exists")) {
|
|
261
|
+
table = await db.openTable(TABLE_NAME);
|
|
262
|
+
} else {
|
|
263
|
+
throw createErr;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const sample = await table.query().limit(1).toArray();
|
|
268
|
+
if (sample.length > 0 && sample[0]?.vector?.length) {
|
|
269
|
+
const existingDim = sample[0].vector.length;
|
|
270
|
+
if (existingDim !== this.config.vectorDim) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Vector dimension mismatch: table=${existingDim}, config=${this.config.vectorDim}. Create a new table/dbPath or set matching embedding.dimensions.`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
await this.createFtsIndex(table);
|
|
278
|
+
this.ftsIndexCreated = true;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
log.warn(
|
|
281
|
+
"Failed to create FTS index, falling back to vector-only search:",
|
|
282
|
+
err
|
|
283
|
+
);
|
|
284
|
+
this.ftsIndexCreated = false;
|
|
285
|
+
}
|
|
286
|
+
this.db = db;
|
|
287
|
+
this.table = table;
|
|
288
|
+
}
|
|
289
|
+
async createFtsIndex(table) {
|
|
290
|
+
try {
|
|
291
|
+
const indices = await table.listIndices();
|
|
292
|
+
const hasFtsIndex = indices?.some(
|
|
293
|
+
// TODO: type this — LanceDB index type lacks proper typings for indexType/columns
|
|
294
|
+
(idx) => idx.indexType === "FTS" || idx.columns?.includes("text")
|
|
295
|
+
);
|
|
296
|
+
if (!hasFtsIndex) {
|
|
297
|
+
const lancedb = await loadLanceDB();
|
|
298
|
+
await table.createIndex("text", {
|
|
299
|
+
// TODO: type this — LanceDB dynamic import doesn't expose Index type
|
|
300
|
+
config: lancedb.Index.fts()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`FTS index creation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async store(entry) {
|
|
310
|
+
await this.ensureInitialized();
|
|
311
|
+
if (this.config.semanticGate !== false && this.semanticGateInstance) {
|
|
312
|
+
try {
|
|
313
|
+
const passed = await this.semanticGateInstance.shouldPass(entry.vector, entry.text);
|
|
314
|
+
if (!passed) {
|
|
315
|
+
return {
|
|
316
|
+
...entry,
|
|
317
|
+
id: "__filtered__",
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
metadata: entry.metadata || "{}"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (this.config.deduplication !== false && entry.vector && entry.vector.length > 0) {
|
|
326
|
+
try {
|
|
327
|
+
const scopeFilter = entry.scope ? [entry.scope] : void 0;
|
|
328
|
+
const similar = await this.vectorSearch(entry.vector, 3, 0.3, scopeFilter);
|
|
329
|
+
for (const match of similar) {
|
|
330
|
+
const cosineSim = 2 - 1 / match.score;
|
|
331
|
+
if (cosineSim > DEDUP_SIMILARITY_THRESHOLD) {
|
|
332
|
+
const existingMeta = parseSmartMetadata(match.entry.metadata, match.entry);
|
|
333
|
+
const accessCount = (existingMeta.access_count ?? 0) + 1;
|
|
334
|
+
const updates = {};
|
|
335
|
+
if (entry.text.length > match.entry.text.length) {
|
|
336
|
+
updates.text = entry.text;
|
|
337
|
+
}
|
|
338
|
+
if (entry.importance > match.entry.importance) {
|
|
339
|
+
updates.importance = entry.importance;
|
|
340
|
+
}
|
|
341
|
+
const patchedMeta = {
|
|
342
|
+
...existingMeta,
|
|
343
|
+
access_count: accessCount,
|
|
344
|
+
updatedAt: Date.now()
|
|
345
|
+
};
|
|
346
|
+
updates.metadata = stringifySmartMetadata(patchedMeta);
|
|
347
|
+
await this.update(match.entry.id, updates, scopeFilter);
|
|
348
|
+
return {
|
|
349
|
+
...match.entry,
|
|
350
|
+
id: match.entry.id,
|
|
351
|
+
timestamp: match.entry.timestamp,
|
|
352
|
+
metadata: updates.metadata
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (this.config.deduplication !== false && entry.vector && entry.vector.length > 0) {
|
|
360
|
+
try {
|
|
361
|
+
const similar = await this.vectorSearch(entry.vector, 3, 0.3);
|
|
362
|
+
for (const match of similar) {
|
|
363
|
+
const cosineSim = 2 - 1 / match.score;
|
|
364
|
+
if (cosineSim > CONFLICT_SIMILARITY_THRESHOLD && cosineSim <= DEDUP_SIMILARITY_THRESHOLD) {
|
|
365
|
+
const oldText = match.entry.text || "";
|
|
366
|
+
const newText = entry.text || "";
|
|
367
|
+
const hasContradictionSignal = /改成|变成|更新为|换成|不再|取消了|changed to|updated to|no longer|switched to/i.test(newText) || oldText.match(/\d+/) && newText.match(/\d+/) && cosineSim > 0.8;
|
|
368
|
+
if (hasContradictionSignal) {
|
|
369
|
+
_auditExpire?.(
|
|
370
|
+
match.entry.id,
|
|
371
|
+
match.entry.scope || "global",
|
|
372
|
+
"contradiction",
|
|
373
|
+
`old: "${match.entry.text?.slice(0, 100)}" \u2192 new: "${newText.slice(0, 100)}"`
|
|
374
|
+
);
|
|
375
|
+
const existingMeta = parseSmartMetadata(match.entry.metadata, match.entry);
|
|
376
|
+
const oldImportance = match.entry.importance ?? 0.7;
|
|
377
|
+
existingMeta.expired_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
378
|
+
existingMeta.expired_reason = `superseded: ${newText.slice(0, 80)}`;
|
|
379
|
+
await this.update(
|
|
380
|
+
match.entry.id,
|
|
381
|
+
{
|
|
382
|
+
importance: Math.max(0.05, oldImportance * 0.1),
|
|
383
|
+
metadata: stringifySmartMetadata(existingMeta)
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const fullEntry = {
|
|
393
|
+
...entry,
|
|
394
|
+
id: randomUUID(),
|
|
395
|
+
timestamp: Date.now(),
|
|
396
|
+
metadata: entry.metadata || "{}"
|
|
397
|
+
};
|
|
398
|
+
try {
|
|
399
|
+
if (this._adapter) {
|
|
400
|
+
await this._adapter.add([fullEntry]);
|
|
401
|
+
} else {
|
|
402
|
+
await this.table.add([fullEntry]);
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
const e = err;
|
|
406
|
+
const code = e.code || "";
|
|
407
|
+
const message = e.message || String(err);
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Failed to store memory in "${this.config.dbPath}": ${code} ${message}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
_auditCreate?.(fullEntry.id, fullEntry.scope, fullEntry.scope, "store", fullEntry.text?.slice(0, 200));
|
|
413
|
+
const textLen = (fullEntry.text || "").length;
|
|
414
|
+
const importance = typeof fullEntry.importance === "number" ? fullEntry.importance : 0.7;
|
|
415
|
+
if (process.env.GRAPHITI_ENABLED === "true" && importance >= 0.5 && textLen >= 20) {
|
|
416
|
+
const graphitiBase = process.env.GRAPHITI_BASE_URL || "http://127.0.0.1:18799";
|
|
417
|
+
const scope = fullEntry.scope || "default";
|
|
418
|
+
const groupId = scope.startsWith("agent:") ? scope.split(":")[1] || "default" : "default";
|
|
419
|
+
const walTs = new Date(fullEntry.timestamp).toISOString();
|
|
420
|
+
if (walAppend) {
|
|
421
|
+
walAppend({
|
|
422
|
+
ts: walTs,
|
|
423
|
+
action: "write",
|
|
424
|
+
text: fullEntry.text,
|
|
425
|
+
scope,
|
|
426
|
+
category: fullEntry.category || "fact",
|
|
427
|
+
groupId,
|
|
428
|
+
importance,
|
|
429
|
+
status: "pending"
|
|
430
|
+
}).catch(() => {
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
fetch(`${graphitiBase}/episodes`, {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: { "Content-Type": "application/json" },
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
text: `[${fullEntry.category || "fact"}] ${fullEntry.text}`,
|
|
438
|
+
group_id: groupId,
|
|
439
|
+
reference_time: walTs,
|
|
440
|
+
source: `lancedb-pro-store-${groupId}`,
|
|
441
|
+
category: fullEntry.category || "fact"
|
|
442
|
+
}),
|
|
443
|
+
signal: AbortSignal.timeout(15e3)
|
|
444
|
+
}).then(() => {
|
|
445
|
+
walMarkCommitted?.(walTs).catch(() => {
|
|
446
|
+
});
|
|
447
|
+
}).catch((err) => {
|
|
448
|
+
walMarkFailed?.(walTs, String(err)).catch(() => {
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return fullEntry;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Import a pre-built entry while preserving its id/timestamp.
|
|
456
|
+
* Used for re-embedding / migration / A/B testing across embedding models.
|
|
457
|
+
* Intentionally separate from `store()` to keep normal writes simple.
|
|
458
|
+
*/
|
|
459
|
+
async importEntry(entry) {
|
|
460
|
+
await this.ensureInitialized();
|
|
461
|
+
if (!entry.id || typeof entry.id !== "string") {
|
|
462
|
+
throw new Error("importEntry requires a stable id");
|
|
463
|
+
}
|
|
464
|
+
const vector = entry.vector || [];
|
|
465
|
+
if (!Array.isArray(vector) || vector.length !== this.config.vectorDim) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Vector dimension mismatch: expected ${this.config.vectorDim}, got ${Array.isArray(vector) ? vector.length : "non-array"}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const full = {
|
|
471
|
+
...entry,
|
|
472
|
+
scope: entry.scope || "global",
|
|
473
|
+
importance: Number.isFinite(entry.importance) ? entry.importance : 0.7,
|
|
474
|
+
timestamp: Number.isFinite(entry.timestamp) ? entry.timestamp : Date.now(),
|
|
475
|
+
metadata: entry.metadata || "{}"
|
|
476
|
+
};
|
|
477
|
+
await this.table.add([full]);
|
|
478
|
+
return full;
|
|
479
|
+
}
|
|
480
|
+
async hasId(id) {
|
|
481
|
+
await this.ensureInitialized();
|
|
482
|
+
if (this._adapter) {
|
|
483
|
+
const results = await this._adapter.query({ where: `id = '${escapeSqlLiteral(id)}'`, limit: 1 });
|
|
484
|
+
return results.length > 0;
|
|
485
|
+
}
|
|
486
|
+
const safeId = escapeSqlLiteral(id);
|
|
487
|
+
const res = await this.table.query().select(["id"]).where(`id = '${safeId}'`).limit(1).toArray();
|
|
488
|
+
return res.length > 0;
|
|
489
|
+
}
|
|
490
|
+
async getById(id, scopeFilter) {
|
|
491
|
+
await this.ensureInitialized();
|
|
492
|
+
if (this._adapter) {
|
|
493
|
+
const results = await this._adapter.query({ where: `id = '${escapeSqlLiteral(id)}'`, limit: 1 });
|
|
494
|
+
if (results.length === 0) return null;
|
|
495
|
+
const r = results[0];
|
|
496
|
+
return { id: r.id, text: r.text, vector: r.vector, category: r.category, scope: r.scope, importance: r.importance, timestamp: r.timestamp, metadata: r.metadata };
|
|
497
|
+
}
|
|
498
|
+
const safeId = escapeSqlLiteral(id);
|
|
499
|
+
const rows = await this.table.query().where(`id = '${safeId}'`).limit(1).toArray();
|
|
500
|
+
if (rows.length === 0) return null;
|
|
501
|
+
const row = rows[0];
|
|
502
|
+
const rowScope = row.scope ?? "global";
|
|
503
|
+
if (scopeFilter && scopeFilter.length > 0 && !scopeFilter.includes(rowScope)) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
id: row.id,
|
|
508
|
+
text: row.text,
|
|
509
|
+
vector: Array.from(row.vector),
|
|
510
|
+
category: row.category,
|
|
511
|
+
scope: rowScope,
|
|
512
|
+
importance: Number(row.importance),
|
|
513
|
+
timestamp: Number(row.timestamp),
|
|
514
|
+
metadata: row.metadata || "{}"
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async vectorSearch(vector, limit = 5, minScore = 0.3, scopeFilter) {
|
|
518
|
+
await this.ensureInitialized();
|
|
519
|
+
if (this._adapter) {
|
|
520
|
+
const results2 = await this._adapter.vectorSearch(vector, limit, minScore, scopeFilter);
|
|
521
|
+
return results2.map((r) => ({
|
|
522
|
+
entry: { id: r.record.id, text: r.record.text, vector: r.record.vector, category: r.record.category, scope: r.record.scope, importance: r.record.importance, timestamp: r.record.timestamp, metadata: r.record.metadata },
|
|
523
|
+
score: r.score
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
const safeLimit = clampInt(limit, 1, 20);
|
|
527
|
+
const fetchLimit = Math.min(safeLimit * 10, 200);
|
|
528
|
+
let query = this.table.vectorSearch(vector).distanceType("cosine").limit(fetchLimit);
|
|
529
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
530
|
+
const scopeConditions = scopeFilter.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`).join(" OR ");
|
|
531
|
+
query = query.where(`(${scopeConditions}) OR scope IS NULL`);
|
|
532
|
+
}
|
|
533
|
+
const results = await query.toArray();
|
|
534
|
+
const mapped = [];
|
|
535
|
+
for (const row of results) {
|
|
536
|
+
const distance = Number(row._distance ?? 0);
|
|
537
|
+
const score = 1 / (1 + distance);
|
|
538
|
+
if (score < minScore) continue;
|
|
539
|
+
const rowScope = row.scope ?? "global";
|
|
540
|
+
if (scopeFilter && scopeFilter.length > 0 && !scopeFilter.includes(rowScope)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
mapped.push({
|
|
544
|
+
entry: {
|
|
545
|
+
id: row.id,
|
|
546
|
+
text: row.text,
|
|
547
|
+
vector: row.vector,
|
|
548
|
+
category: row.category,
|
|
549
|
+
scope: rowScope,
|
|
550
|
+
importance: Number(row.importance),
|
|
551
|
+
timestamp: Number(row.timestamp),
|
|
552
|
+
metadata: row.metadata || "{}"
|
|
553
|
+
},
|
|
554
|
+
score
|
|
555
|
+
});
|
|
556
|
+
if (mapped.length >= safeLimit) break;
|
|
557
|
+
}
|
|
558
|
+
return mapped;
|
|
559
|
+
}
|
|
560
|
+
async bm25Search(query, limit = 5, scopeFilter) {
|
|
561
|
+
await this.ensureInitialized();
|
|
562
|
+
if (this._adapter) {
|
|
563
|
+
const results = await this._adapter.fullTextSearch(query, limit, scopeFilter);
|
|
564
|
+
return results.map((r) => ({
|
|
565
|
+
entry: { id: r.record.id, text: r.record.text, vector: r.record.vector, category: r.record.category, scope: r.record.scope, importance: r.record.importance, timestamp: r.record.timestamp, metadata: r.record.metadata },
|
|
566
|
+
score: r.score
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
const safeLimit = clampInt(limit, 1, 20);
|
|
570
|
+
if (!this.ftsIndexCreated) {
|
|
571
|
+
return this.lexicalFallbackSearch(query, safeLimit, scopeFilter);
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
let searchQuery = this.table.search(query, "fts").limit(safeLimit);
|
|
575
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
576
|
+
const scopeConditions = scopeFilter.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`).join(" OR ");
|
|
577
|
+
searchQuery = searchQuery.where(
|
|
578
|
+
`(${scopeConditions}) OR scope IS NULL`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const results = await searchQuery.toArray();
|
|
582
|
+
const mapped = [];
|
|
583
|
+
for (const row of results) {
|
|
584
|
+
const rowScope = row.scope ?? "global";
|
|
585
|
+
if (scopeFilter && scopeFilter.length > 0 && !scopeFilter.includes(rowScope)) {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const rawScore = row._score != null ? Number(row._score) : 0;
|
|
589
|
+
const normalizedScore = rawScore > 0 ? 1 / (1 + Math.exp(-rawScore / 5)) : 0.5;
|
|
590
|
+
mapped.push({
|
|
591
|
+
entry: {
|
|
592
|
+
id: row.id,
|
|
593
|
+
text: row.text,
|
|
594
|
+
vector: row.vector,
|
|
595
|
+
category: row.category,
|
|
596
|
+
scope: rowScope,
|
|
597
|
+
importance: Number(row.importance),
|
|
598
|
+
timestamp: Number(row.timestamp),
|
|
599
|
+
metadata: row.metadata || "{}"
|
|
600
|
+
},
|
|
601
|
+
score: normalizedScore
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (mapped.length > 0) {
|
|
605
|
+
return mapped;
|
|
606
|
+
}
|
|
607
|
+
return this.lexicalFallbackSearch(query, safeLimit, scopeFilter);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
log.warn("BM25 search failed, falling back to empty results:", err);
|
|
610
|
+
return this.lexicalFallbackSearch(query, safeLimit, scopeFilter);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async lexicalFallbackSearch(query, limit, scopeFilter) {
|
|
614
|
+
const trimmedQuery = query.trim();
|
|
615
|
+
if (!trimmedQuery) return [];
|
|
616
|
+
let searchQuery = this.table.query().select([
|
|
617
|
+
"id",
|
|
618
|
+
"text",
|
|
619
|
+
"vector",
|
|
620
|
+
"category",
|
|
621
|
+
"scope",
|
|
622
|
+
"importance",
|
|
623
|
+
"timestamp",
|
|
624
|
+
"metadata"
|
|
625
|
+
]);
|
|
626
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
627
|
+
const scopeConditions = scopeFilter.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`).join(" OR ");
|
|
628
|
+
searchQuery = searchQuery.where(`(${scopeConditions}) OR scope IS NULL`);
|
|
629
|
+
}
|
|
630
|
+
const rows = await searchQuery.toArray();
|
|
631
|
+
const matches = [];
|
|
632
|
+
for (const row of rows) {
|
|
633
|
+
const rowScope = row.scope ?? "global";
|
|
634
|
+
if (scopeFilter && scopeFilter.length > 0 && !scopeFilter.includes(rowScope)) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const entry = {
|
|
638
|
+
id: row.id,
|
|
639
|
+
text: row.text,
|
|
640
|
+
vector: row.vector,
|
|
641
|
+
category: row.category,
|
|
642
|
+
scope: rowScope,
|
|
643
|
+
importance: Number(row.importance),
|
|
644
|
+
timestamp: Number(row.timestamp),
|
|
645
|
+
metadata: row.metadata || "{}"
|
|
646
|
+
};
|
|
647
|
+
const metadata = parseSmartMetadata(entry.metadata, entry);
|
|
648
|
+
const score = scoreLexicalHit(trimmedQuery, [
|
|
649
|
+
{ text: entry.text, weight: 1 },
|
|
650
|
+
{ text: metadata.l0_abstract, weight: 0.98 },
|
|
651
|
+
{ text: metadata.l1_overview, weight: 0.92 },
|
|
652
|
+
{ text: metadata.l2_content, weight: 0.96 }
|
|
653
|
+
]);
|
|
654
|
+
if (score <= 0) continue;
|
|
655
|
+
matches.push({ entry, score });
|
|
656
|
+
}
|
|
657
|
+
return matches.sort((a, b) => b.score - a.score || b.entry.timestamp - a.entry.timestamp).slice(0, limit);
|
|
658
|
+
}
|
|
659
|
+
async delete(id, scopeFilter) {
|
|
660
|
+
await this.ensureInitialized();
|
|
661
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
662
|
+
const prefixRegex = /^[0-9a-f]{8,}$/i;
|
|
663
|
+
const isFullId = uuidRegex.test(id);
|
|
664
|
+
const isPrefix = !isFullId && prefixRegex.test(id);
|
|
665
|
+
if (!isFullId && !isPrefix) {
|
|
666
|
+
throw new Error(`Invalid memory ID format: ${id}`);
|
|
667
|
+
}
|
|
668
|
+
let candidates;
|
|
669
|
+
if (isFullId) {
|
|
670
|
+
candidates = await this.table.query().where(`id = '${id}'`).limit(1).toArray();
|
|
671
|
+
} else {
|
|
672
|
+
const all = await this.table.query().select(["id", "scope"]).limit(1e3).toArray();
|
|
673
|
+
candidates = all.filter((r) => r.id.startsWith(id));
|
|
674
|
+
if (candidates.length > 1) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Ambiguous prefix "${id}" matches ${candidates.length} memories. Use a longer prefix or full ID.`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (candidates.length === 0) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
const resolvedId = candidates[0].id;
|
|
684
|
+
const rowScope = candidates[0].scope ?? "global";
|
|
685
|
+
if (scopeFilter && scopeFilter.length > 0 && !scopeFilter.includes(rowScope)) {
|
|
686
|
+
throw new Error(`Memory ${resolvedId} is outside accessible scopes`);
|
|
687
|
+
}
|
|
688
|
+
_auditDelete?.([resolvedId], rowScope, "user-request");
|
|
689
|
+
if (this._adapter) {
|
|
690
|
+
await this._adapter.delete(`id = '${escapeSqlLiteral(resolvedId)}'`);
|
|
691
|
+
} else {
|
|
692
|
+
await this.table.delete(`id = '${resolvedId}'`);
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
async list(scopeFilter, category, limit = 20, offset = 0) {
|
|
697
|
+
await this.ensureInitialized();
|
|
698
|
+
let query = this.table.query();
|
|
699
|
+
const conditions = [];
|
|
700
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
701
|
+
const scopeConditions = scopeFilter.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`).join(" OR ");
|
|
702
|
+
conditions.push(`((${scopeConditions}) OR scope IS NULL)`);
|
|
703
|
+
}
|
|
704
|
+
if (category) {
|
|
705
|
+
conditions.push(`category = '${escapeSqlLiteral(category)}'`);
|
|
706
|
+
}
|
|
707
|
+
if (conditions.length > 0) {
|
|
708
|
+
query = query.where(conditions.join(" AND "));
|
|
709
|
+
}
|
|
710
|
+
const results = await query.select([
|
|
711
|
+
"id",
|
|
712
|
+
"text",
|
|
713
|
+
"category",
|
|
714
|
+
"scope",
|
|
715
|
+
"importance",
|
|
716
|
+
"timestamp",
|
|
717
|
+
"metadata"
|
|
718
|
+
]).toArray();
|
|
719
|
+
return results.map(
|
|
720
|
+
(row) => ({
|
|
721
|
+
id: row.id,
|
|
722
|
+
text: row.text,
|
|
723
|
+
vector: [],
|
|
724
|
+
// Don't include vectors in list results for performance
|
|
725
|
+
category: row.category,
|
|
726
|
+
scope: row.scope ?? "global",
|
|
727
|
+
importance: Number(row.importance),
|
|
728
|
+
timestamp: Number(row.timestamp),
|
|
729
|
+
metadata: row.metadata || "{}"
|
|
730
|
+
})
|
|
731
|
+
).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)).slice(offset, offset + limit);
|
|
732
|
+
}
|
|
733
|
+
async stats(scopeFilter) {
|
|
734
|
+
await this.ensureInitialized();
|
|
735
|
+
let query = this.table.query();
|
|
736
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
737
|
+
const scopeConditions = scopeFilter.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`).join(" OR ");
|
|
738
|
+
query = query.where(`((${scopeConditions}) OR scope IS NULL)`);
|
|
739
|
+
}
|
|
740
|
+
const results = await query.select(["scope", "category"]).toArray();
|
|
741
|
+
const scopeCounts = {};
|
|
742
|
+
const categoryCounts = {};
|
|
743
|
+
for (const row of results) {
|
|
744
|
+
const scope = row.scope ?? "global";
|
|
745
|
+
const category = row.category;
|
|
746
|
+
scopeCounts[scope] = (scopeCounts[scope] || 0) + 1;
|
|
747
|
+
categoryCounts[category] = (categoryCounts[category] || 0) + 1;
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
totalCount: results.length,
|
|
751
|
+
scopeCounts,
|
|
752
|
+
categoryCounts
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async update(id, updates, scopeFilter) {
|
|
756
|
+
await this.ensureInitialized();
|
|
757
|
+
return this.runSerializedUpdate(async () => {
|
|
758
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
759
|
+
const prefixRegex = /^[0-9a-f]{8,}$/i;
|
|
760
|
+
const isFullId = uuidRegex.test(id);
|
|
761
|
+
const isPrefix = !isFullId && prefixRegex.test(id);
|
|
762
|
+
if (!isFullId && !isPrefix) {
|
|
763
|
+
throw new Error(`Invalid memory ID format: ${id}`);
|
|
764
|
+
}
|
|
765
|
+
let rows;
|
|
766
|
+
if (isFullId) {
|
|
767
|
+
const safeId = escapeSqlLiteral(id);
|
|
768
|
+
rows = await this.table.query().where(`id = '${safeId}'`).limit(1).toArray();
|
|
769
|
+
} else {
|
|
770
|
+
const all = await this.table.query().select([
|
|
771
|
+
"id",
|
|
772
|
+
"text",
|
|
773
|
+
"vector",
|
|
774
|
+
"category",
|
|
775
|
+
"scope",
|
|
776
|
+
"importance",
|
|
777
|
+
"timestamp",
|
|
778
|
+
"metadata"
|
|
779
|
+
]).limit(1e3).toArray();
|
|
780
|
+
rows = all.filter((r) => r.id.startsWith(id));
|
|
781
|
+
if (rows.length > 1) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Ambiguous prefix "${id}" matches ${rows.length} memories. Use a longer prefix or full ID.`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (rows.length === 0) return null;
|
|
788
|
+
const row = rows[0];
|
|
789
|
+
const rowScope = row.scope ?? "global";
|
|
790
|
+
if (scopeFilter && scopeFilter.length > 0 && !scopeFilter.includes(rowScope)) {
|
|
791
|
+
throw new Error(`Memory ${id} is outside accessible scopes`);
|
|
792
|
+
}
|
|
793
|
+
const original = {
|
|
794
|
+
id: row.id,
|
|
795
|
+
text: row.text,
|
|
796
|
+
vector: Array.from(row.vector),
|
|
797
|
+
category: row.category,
|
|
798
|
+
scope: rowScope,
|
|
799
|
+
importance: Number(row.importance),
|
|
800
|
+
timestamp: Number(row.timestamp),
|
|
801
|
+
metadata: row.metadata || "{}"
|
|
802
|
+
};
|
|
803
|
+
const updated = {
|
|
804
|
+
...original,
|
|
805
|
+
text: updates.text ?? original.text,
|
|
806
|
+
vector: updates.vector ?? original.vector,
|
|
807
|
+
category: updates.category ?? original.category,
|
|
808
|
+
scope: rowScope,
|
|
809
|
+
importance: updates.importance ?? original.importance,
|
|
810
|
+
timestamp: original.timestamp,
|
|
811
|
+
// preserve original
|
|
812
|
+
metadata: updates.metadata ?? original.metadata
|
|
813
|
+
};
|
|
814
|
+
_auditUpdate?.(
|
|
815
|
+
original.id,
|
|
816
|
+
rowScope,
|
|
817
|
+
"memory-update",
|
|
818
|
+
JSON.stringify({
|
|
819
|
+
old: { text: original.text?.slice(0, 200), importance: original.importance, category: original.category },
|
|
820
|
+
new: { text: updated.text?.slice(0, 200), importance: updated.importance, category: updated.category }
|
|
821
|
+
})
|
|
822
|
+
);
|
|
823
|
+
const rollbackCandidate = await this.getById(original.id).catch(() => null) ?? original;
|
|
824
|
+
const resolvedId = escapeSqlLiteral(row.id);
|
|
825
|
+
await this.table.delete(`id = '${resolvedId}'`);
|
|
826
|
+
try {
|
|
827
|
+
await this.table.add([updated]);
|
|
828
|
+
} catch (addError) {
|
|
829
|
+
const current = await this.getById(original.id).catch(() => null);
|
|
830
|
+
if (current) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Failed to update memory ${id}: write failed after delete, but an existing record was preserved. Write error: ${addError instanceof Error ? addError.message : String(addError)}`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
await this.table.add([rollbackCandidate]);
|
|
837
|
+
} catch (rollbackError) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`Failed to update memory ${id}: write failed after delete, and rollback also failed. Write error: ${addError instanceof Error ? addError.message : String(addError)}. Rollback error: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
throw new Error(
|
|
843
|
+
`Failed to update memory ${id}: write failed after delete, latest available record restored. Write error: ${addError instanceof Error ? addError.message : String(addError)}`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
return updated;
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
async runSerializedUpdate(action) {
|
|
850
|
+
const previous = this.updateQueue;
|
|
851
|
+
let release;
|
|
852
|
+
const lock = new Promise((resolve) => {
|
|
853
|
+
release = resolve;
|
|
854
|
+
});
|
|
855
|
+
this.updateQueue = previous.then(() => lock);
|
|
856
|
+
await previous;
|
|
857
|
+
try {
|
|
858
|
+
return await action();
|
|
859
|
+
} finally {
|
|
860
|
+
release?.();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async patchMetadata(id, patch, scopeFilter) {
|
|
864
|
+
const existing = await this.getById(id, scopeFilter);
|
|
865
|
+
if (!existing) return null;
|
|
866
|
+
const metadata = buildSmartMetadata(existing, patch);
|
|
867
|
+
return this.update(
|
|
868
|
+
id,
|
|
869
|
+
{ metadata: stringifySmartMetadata(metadata) },
|
|
870
|
+
scopeFilter
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
async bulkDelete(scopeFilter, beforeTimestamp) {
|
|
874
|
+
await this.ensureInitialized();
|
|
875
|
+
const conditions = [];
|
|
876
|
+
if (scopeFilter.length > 0) {
|
|
877
|
+
const scopeConditions = scopeFilter.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`).join(" OR ");
|
|
878
|
+
conditions.push(`(${scopeConditions})`);
|
|
879
|
+
}
|
|
880
|
+
if (beforeTimestamp) {
|
|
881
|
+
conditions.push(`timestamp < ${beforeTimestamp}`);
|
|
882
|
+
}
|
|
883
|
+
if (conditions.length === 0) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
"Bulk delete requires at least scope or timestamp filter for safety"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const whereClause = conditions.join(" AND ");
|
|
889
|
+
const countResults = await this.table.query().where(whereClause).toArray();
|
|
890
|
+
const deleteCount = countResults.length;
|
|
891
|
+
if (deleteCount > 0) {
|
|
892
|
+
await this.table.delete(whereClause);
|
|
893
|
+
}
|
|
894
|
+
return deleteCount;
|
|
895
|
+
}
|
|
896
|
+
/** Last FTS error for diagnostics */
|
|
897
|
+
_lastFtsError = null;
|
|
898
|
+
get lastFtsError() {
|
|
899
|
+
return this._lastFtsError;
|
|
900
|
+
}
|
|
901
|
+
/** Get FTS index health status */
|
|
902
|
+
getFtsStatus() {
|
|
903
|
+
return {
|
|
904
|
+
available: this.ftsIndexCreated,
|
|
905
|
+
lastError: this._lastFtsError
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
/** Rebuild FTS index (drops and recreates). Useful for recovery after corruption. */
|
|
909
|
+
async rebuildFtsIndex() {
|
|
910
|
+
await this.ensureInitialized();
|
|
911
|
+
try {
|
|
912
|
+
const indices = await this.table.listIndices();
|
|
913
|
+
for (const idx of indices) {
|
|
914
|
+
if (idx.indexType === "FTS" || idx.columns?.includes("text")) {
|
|
915
|
+
try {
|
|
916
|
+
await this.table.dropIndex(idx.name || "text");
|
|
917
|
+
} catch (err) {
|
|
918
|
+
log.warn(`dropIndex(${idx.name || "text"}) failed:`, err);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
await this.createFtsIndex(this.table);
|
|
923
|
+
this.ftsIndexCreated = true;
|
|
924
|
+
this._lastFtsError = null;
|
|
925
|
+
return { success: true };
|
|
926
|
+
} catch (err) {
|
|
927
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
928
|
+
this._lastFtsError = msg;
|
|
929
|
+
this.ftsIndexCreated = false;
|
|
930
|
+
return { success: false, error: msg };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
export {
|
|
935
|
+
MemoryStore,
|
|
936
|
+
loadLanceDB,
|
|
937
|
+
validateStoragePath
|
|
938
|
+
};
|
|
939
|
+
//# sourceMappingURL=store.js.map
|