@oh-my-pi/pi-mnemopi 17.0.0 → 17.0.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/CHANGELOG.md +7 -0
- package/dist/types/core/fastembed-runtime.d.ts +27 -0
- package/package.json +4 -4
- package/src/core/beam/store.ts +101 -17
- package/src/core/fastembed-runtime.ts +78 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.1] - 2026-07-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed working-memory TTL trim silently deleting restored or imported durable rows: rows keeping `consolidated_at = NULL` with an old `timestamp` are no longer trimmed when flagged `IMPORTED`, `importFromDict` stamps imported rows as consolidated, and every working-memory delete path (trim, `forgetWorking`, force-import overwrite) now cascades linked annotations, embeddings, facts, memoria projections, gists, and graph edges instead of leaving orphans. ([#4819](https://github.com/can1357/oh-my-pi/issues/4819))
|
|
10
|
+
- Fixed Mnemopi local embeddings on Windows loading an unrelated `onnxruntime.dll` from the inherited system path instead of fastembed's cached ORT runtime. ([#4849](https://github.com/can1357/oh-my-pi/issues/4849))
|
|
11
|
+
|
|
5
12
|
## [16.3.9] - 2026-07-06
|
|
6
13
|
|
|
7
14
|
### Fixed
|
|
@@ -10,5 +10,32 @@ export interface FastembedRuntimeInstallPlan {
|
|
|
10
10
|
}
|
|
11
11
|
/** Build the deterministic fastembed runtime install plan used by local embeddings. */
|
|
12
12
|
export declare function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan;
|
|
13
|
+
/** Inputs for selecting the Windows DLL directory paired with a fastembed installation. */
|
|
14
|
+
export interface WindowsFastembedRuntimeOptions {
|
|
15
|
+
/** Resolved fastembed package entry whose dependency graph owns the ORT binding. */
|
|
16
|
+
fastembedEntry: string;
|
|
17
|
+
/** Directory containing fastembed's manifest and nested dependency graph. */
|
|
18
|
+
fastembedPackageDir: string;
|
|
19
|
+
/** Native architecture to select; defaults to the current process architecture. */
|
|
20
|
+
arch?: string;
|
|
21
|
+
/** Environment receiving the DLL search path; defaults to the subprocess environment. */
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}
|
|
24
|
+
/** The ORT module and DLL directory selected from fastembed's own dependency graph. */
|
|
25
|
+
export interface WindowsFastembedRuntime {
|
|
26
|
+
/** Resolved entry for fastembed's own ONNX Runtime dependency. */
|
|
27
|
+
ortEntry: string;
|
|
28
|
+
/** Package directory containing the selected ORT manifest and native assets. */
|
|
29
|
+
ortPackageDir: string;
|
|
30
|
+
/** Directory prepended to `PATH` so Windows finds the paired native DLL. */
|
|
31
|
+
dllDir: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Prepend the ORT DLL directory paired with fastembed before Bun loads its
|
|
35
|
+
* native binding. Compiled Windows binaries extract `.node` files to a
|
|
36
|
+
* temporary directory, so the default DLL search can otherwise select an
|
|
37
|
+
* unrelated `onnxruntime.dll` from the inherited system path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function prepareWindowsFastembedRuntime({ fastembedEntry, fastembedPackageDir, arch, env, }: WindowsFastembedRuntimeOptions): Promise<WindowsFastembedRuntime>;
|
|
13
40
|
export declare function loadFastembed(): Promise<FastembedModule>;
|
|
14
41
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.2",
|
|
5
5
|
"description": "Local SQLite memory engine for Oh My Pi agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
44
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.0.2",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "17.0.2",
|
|
44
|
+
"@oh-my-pi/pi-utils": "17.0.2",
|
|
45
45
|
"lru-cache": "11.5.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
package/src/core/beam/store.ts
CHANGED
|
@@ -163,27 +163,106 @@ function findDuplicate(beam: BeamMemoryState, content: string): string | null {
|
|
|
163
163
|
return row?.id ?? null;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function tableExists(db: BeamMemoryState["db"], table: string): boolean {
|
|
167
|
+
return (
|
|
168
|
+
db
|
|
169
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table','virtual table') AND name = ? LIMIT 1")
|
|
170
|
+
.get(table) !== null
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Tables whose rows point back to a `working_memory` id via `source_memory_id`. */
|
|
175
|
+
const MEMORIA_SOURCE_TABLES = [
|
|
176
|
+
"memoria_facts",
|
|
177
|
+
"memoria_instructions",
|
|
178
|
+
"memoria_kg",
|
|
179
|
+
"memoria_preferences",
|
|
180
|
+
"memoria_timelines",
|
|
181
|
+
] as const;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Remove every artifact linked to the given `working_memory` ids so no deletion
|
|
185
|
+
* path leaves orphans behind. Covers annotations, embeddings, extracted facts
|
|
186
|
+
* (`facts.source_msg_id`), memoria projections (`*.source_memory_id`), episodic
|
|
187
|
+
* gists, and the graph edges tied to those memory / gist / fact node ids.
|
|
188
|
+
*
|
|
189
|
+
* Idempotent and schema-tolerant: `gists` / `graph_edges` only exist once an
|
|
190
|
+
* `EpisodicGraph` has initialised, so they are guarded. Callers own the
|
|
191
|
+
* transaction and the base `working_memory` delete.
|
|
192
|
+
*/
|
|
193
|
+
function purgeWorkingMemoryArtifacts(db: BeamMemoryState["db"], ids: readonly string[]): void {
|
|
194
|
+
if (ids.length === 0) return;
|
|
195
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
196
|
+
|
|
197
|
+
const graphRefs = new Set<string>(ids);
|
|
198
|
+
for (const id of ids) graphRefs.add(`gist_${id}`);
|
|
199
|
+
if (tableExists(db, "facts")) {
|
|
200
|
+
const factRows = db.prepare(`SELECT fact_id FROM facts WHERE source_msg_id IN (${placeholders})`).all(...ids) as {
|
|
201
|
+
fact_id: string;
|
|
202
|
+
}[];
|
|
203
|
+
for (const row of factRows) graphRefs.add(row.fact_id);
|
|
204
|
+
db.prepare(`DELETE FROM facts WHERE source_msg_id IN (${placeholders})`).run(...ids);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
db.prepare(`DELETE FROM annotations WHERE memory_id IN (${placeholders})`).run(...ids);
|
|
208
|
+
db.prepare(`DELETE FROM memory_embeddings WHERE memory_id IN (${placeholders})`).run(...ids);
|
|
209
|
+
for (const table of MEMORIA_SOURCE_TABLES) {
|
|
210
|
+
db.prepare(`DELETE FROM ${table} WHERE source_memory_id IN (${placeholders})`).run(...ids);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (tableExists(db, "gists")) {
|
|
214
|
+
db.prepare(`DELETE FROM gists WHERE memory_id IN (${placeholders})`).run(...ids);
|
|
215
|
+
}
|
|
216
|
+
if (tableExists(db, "graph_edges")) {
|
|
217
|
+
const refs = [...graphRefs];
|
|
218
|
+
const refPlaceholders = refs.map(() => "?").join(", ");
|
|
219
|
+
db.prepare(`DELETE FROM graph_edges WHERE source IN (${refPlaceholders}) OR target IN (${refPlaceholders})`).run(
|
|
220
|
+
...refs,
|
|
221
|
+
...refs,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* TTL / overflow trim for transient working memory. Only genuine scratch is
|
|
228
|
+
* eligible: `consolidated_at IS NULL` no longer suffices on its own, since
|
|
229
|
+
* restored or imported durable rows legitimately carry a NULL consolidation
|
|
230
|
+
* marker with an old event timestamp (issue #4819). Rows flagged `IMPORTED`
|
|
231
|
+
* are treated as durable and never trimmed, and trimmed rows cascade all linked
|
|
232
|
+
* artifacts via `purgeWorkingMemoryArtifacts`.
|
|
233
|
+
*/
|
|
166
234
|
function trimWorkingMemory(beam: BeamMemoryState): void {
|
|
167
235
|
const limit = beam.config.workingMemoryLimit;
|
|
168
236
|
if (!Number.isFinite(limit) || limit <= 0) return;
|
|
169
237
|
const ttlHours = beam.config.workingMemoryTtlHours;
|
|
170
238
|
const cutoff = toUtcIso(new Date(Date.now() - ttlHours * 3_600_000));
|
|
171
|
-
beam.db
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
AND consolidated_at IS NULL
|
|
176
|
-
AND (
|
|
177
|
-
timestamp < ? OR
|
|
178
|
-
id NOT IN (
|
|
239
|
+
transaction(beam.db, () => {
|
|
240
|
+
const ids = (
|
|
241
|
+
beam.db
|
|
242
|
+
.prepare(`
|
|
179
243
|
SELECT id FROM working_memory
|
|
180
|
-
WHERE session_id = ?
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
244
|
+
WHERE session_id = ?
|
|
245
|
+
AND consolidated_at IS NULL
|
|
246
|
+
AND trust_tier IS NOT 'IMPORTED'
|
|
247
|
+
AND (
|
|
248
|
+
timestamp < ? OR
|
|
249
|
+
id NOT IN (
|
|
250
|
+
SELECT id FROM working_memory
|
|
251
|
+
WHERE session_id = ? AND consolidated_at IS NULL AND trust_tier IS NOT 'IMPORTED'
|
|
252
|
+
ORDER BY timestamp DESC
|
|
253
|
+
LIMIT ?
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
`)
|
|
257
|
+
.all(beam.sessionId, cutoff, beam.sessionId, limit) as { id: string }[]
|
|
258
|
+
).map(row => row.id);
|
|
259
|
+
if (ids.length === 0) return;
|
|
260
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
261
|
+
beam.db
|
|
262
|
+
.prepare(`DELETE FROM working_memory WHERE id IN (${placeholders}) AND session_id = ?`)
|
|
263
|
+
.run(...ids, beam.sessionId);
|
|
264
|
+
purgeWorkingMemoryArtifacts(beam.db, ids);
|
|
265
|
+
});
|
|
187
266
|
}
|
|
188
267
|
|
|
189
268
|
function addTemporalAnnotations(beam: BeamMemoryState, memoryId: string, timestamp: string, source: string): void {
|
|
@@ -715,7 +794,7 @@ export function forgetWorking(beam: BeamMemoryState, memoryId: string): boolean
|
|
|
715
794
|
.run(memoryId, beam.sessionId);
|
|
716
795
|
deleted = result.changes;
|
|
717
796
|
if (deleted > 0) {
|
|
718
|
-
beam.db
|
|
797
|
+
purgeWorkingMemoryArtifacts(beam.db, [memoryId]);
|
|
719
798
|
}
|
|
720
799
|
});
|
|
721
800
|
if (deleted > 0) invalidateCaches(beam);
|
|
@@ -811,6 +890,10 @@ export function importFromDict(beam: BeamMemoryState, data: Record<string, unkno
|
|
|
811
890
|
consolidation_log: { inserted: 0 },
|
|
812
891
|
} satisfies ImportStats;
|
|
813
892
|
const db: Database = beam.db;
|
|
893
|
+
// Imported working-memory rows are durable, not scratch: stamp any that
|
|
894
|
+
// arrive unconsolidated so the TTL trim treats them as consolidated and can
|
|
895
|
+
// never silently discard a restored bank (issue #4819).
|
|
896
|
+
const importedAt = toUtcIso();
|
|
814
897
|
const oldToNewRowid = new Map<number, number>();
|
|
815
898
|
|
|
816
899
|
transaction(db, () => {
|
|
@@ -825,6 +908,7 @@ export function importFromDict(beam: BeamMemoryState, data: Record<string, unkno
|
|
|
825
908
|
}
|
|
826
909
|
if (exists) {
|
|
827
910
|
db.prepare("DELETE FROM working_memory WHERE id = ?").run(id);
|
|
911
|
+
purgeWorkingMemoryArtifacts(db, [id]);
|
|
828
912
|
stats.working_memory.overwritten++;
|
|
829
913
|
} else {
|
|
830
914
|
stats.working_memory.inserted++;
|
|
@@ -851,7 +935,7 @@ export function importFromDict(beam: BeamMemoryState, data: Record<string, unkno
|
|
|
851
935
|
sqlBinding(item.last_recalled, null),
|
|
852
936
|
sqlBinding(item.created_at, null),
|
|
853
937
|
clampVeracity(item.veracity),
|
|
854
|
-
sqlBinding(item.consolidated_at,
|
|
938
|
+
item.consolidated_at == null ? importedAt : sqlBinding(item.consolidated_at, importedAt),
|
|
855
939
|
sqlBinding(item.memory_type, "unknown"),
|
|
856
940
|
sqlBinding(item.embed_text, null),
|
|
857
941
|
sqlBinding(item.author_id, null),
|
|
@@ -48,6 +48,67 @@ export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan {
|
|
|
48
48
|
}
|
|
49
49
|
let fastembedLoad: Promise<FastembedModule> | null = null;
|
|
50
50
|
|
|
51
|
+
/** Inputs for selecting the Windows DLL directory paired with a fastembed installation. */
|
|
52
|
+
export interface WindowsFastembedRuntimeOptions {
|
|
53
|
+
/** Resolved fastembed package entry whose dependency graph owns the ORT binding. */
|
|
54
|
+
fastembedEntry: string;
|
|
55
|
+
/** Directory containing fastembed's manifest and nested dependency graph. */
|
|
56
|
+
fastembedPackageDir: string;
|
|
57
|
+
/** Native architecture to select; defaults to the current process architecture. */
|
|
58
|
+
arch?: string;
|
|
59
|
+
/** Environment receiving the DLL search path; defaults to the subprocess environment. */
|
|
60
|
+
env?: NodeJS.ProcessEnv;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The ORT module and DLL directory selected from fastembed's own dependency graph. */
|
|
64
|
+
export interface WindowsFastembedRuntime {
|
|
65
|
+
/** Resolved entry for fastembed's own ONNX Runtime dependency. */
|
|
66
|
+
ortEntry: string;
|
|
67
|
+
/** Package directory containing the selected ORT manifest and native assets. */
|
|
68
|
+
ortPackageDir: string;
|
|
69
|
+
/** Directory prepended to `PATH` so Windows finds the paired native DLL. */
|
|
70
|
+
dllDir: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prepend the ORT DLL directory paired with fastembed before Bun loads its
|
|
75
|
+
* native binding. Compiled Windows binaries extract `.node` files to a
|
|
76
|
+
* temporary directory, so the default DLL search can otherwise select an
|
|
77
|
+
* unrelated `onnxruntime.dll` from the inherited system path.
|
|
78
|
+
*/
|
|
79
|
+
export async function prepareWindowsFastembedRuntime({
|
|
80
|
+
fastembedEntry,
|
|
81
|
+
fastembedPackageDir,
|
|
82
|
+
arch = process.arch,
|
|
83
|
+
env = process.env,
|
|
84
|
+
}: WindowsFastembedRuntimeOptions): Promise<WindowsFastembedRuntime> {
|
|
85
|
+
const nestedNodeModules = path.join(fastembedPackageDir, "node_modules");
|
|
86
|
+
const rootNodeModules = path.dirname(fastembedPackageDir);
|
|
87
|
+
const nestedOrtEntry = resolveRuntimeModule(nestedNodeModules, "onnxruntime-node");
|
|
88
|
+
const ortEntry = nestedOrtEntry ?? resolveRuntimeModule(rootNodeModules, "onnxruntime-node");
|
|
89
|
+
const ortPackageDir = path.join(nestedOrtEntry ? nestedNodeModules : rootNodeModules, "onnxruntime-node");
|
|
90
|
+
if (!ortEntry) {
|
|
91
|
+
throw new Error(`Cannot find module onnxruntime-node beside ${fastembedEntry}`);
|
|
92
|
+
}
|
|
93
|
+
const dllGlob = new Bun.Glob(`bin/napi-*/win32/${arch}/onnxruntime.dll`);
|
|
94
|
+
let dllDir: string | undefined;
|
|
95
|
+
for await (const dll of dllGlob.scan({ cwd: ortPackageDir, absolute: true, onlyFiles: true })) {
|
|
96
|
+
dllDir = path.dirname(dll);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (!dllDir) {
|
|
100
|
+
throw new Error(`Cannot find module onnxruntime-node Windows DLL for ${arch} beside ${ortEntry}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const currentPath = env.PATH;
|
|
104
|
+
const normalizedDllDir = path.resolve(dllDir).toLowerCase();
|
|
105
|
+
const alreadyPresent = currentPath
|
|
106
|
+
?.split(path.delimiter)
|
|
107
|
+
.some(entry => path.resolve(entry).toLowerCase() === normalizedDllDir);
|
|
108
|
+
if (!alreadyPresent) env.PATH = currentPath ? `${dllDir}${path.delimiter}${currentPath}` : dllDir;
|
|
109
|
+
return { ortEntry, ortPackageDir, dllDir };
|
|
110
|
+
}
|
|
111
|
+
|
|
51
112
|
export function loadFastembed(): Promise<FastembedModule> {
|
|
52
113
|
fastembedLoad ??= loadFastembedOnce().catch(error => {
|
|
53
114
|
fastembedLoad = null;
|
|
@@ -57,16 +118,14 @@ export function loadFastembed(): Promise<FastembedModule> {
|
|
|
57
118
|
}
|
|
58
119
|
|
|
59
120
|
async function loadFastembedOnce(): Promise<FastembedModule> {
|
|
60
|
-
// Dynamic imports: both packages are optional peers that eagerly load
|
|
61
|
-
// native addons and may be absent at runtime — a static import would load
|
|
62
|
-
// the addon at module-init and crash every consumer without the peers.
|
|
63
121
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
122
|
+
const requireDirect = createRequire(import.meta.url);
|
|
123
|
+
const manifestPath = requireDirect.resolve("fastembed/package.json");
|
|
124
|
+
const manifest: { version?: unknown } = requireDirect(manifestPath);
|
|
125
|
+
if (manifest.version !== FASTEMBED_SPEC) {
|
|
126
|
+
throw new Error(`Cannot find package fastembed@${FASTEMBED_SPEC}; resolved ${String(manifest.version)}`);
|
|
68
127
|
}
|
|
69
|
-
return await
|
|
128
|
+
return await loadResolvedFastembed(requireDirect.resolve("fastembed"), path.dirname(manifestPath));
|
|
70
129
|
} catch (error) {
|
|
71
130
|
if (!isRecoverableFastembedLoadError(error)) throw error;
|
|
72
131
|
logger.debug("mnemopi: fastembed not loadable, using on-demand runtime install", {
|
|
@@ -76,6 +135,16 @@ async function loadFastembedOnce(): Promise<FastembedModule> {
|
|
|
76
135
|
}
|
|
77
136
|
}
|
|
78
137
|
|
|
138
|
+
async function loadResolvedFastembed(entry: string, fastembedPackageDir: string): Promise<FastembedModule> {
|
|
139
|
+
const requireFastembed = createRequire(entry);
|
|
140
|
+
if (process.platform === "win32") {
|
|
141
|
+
const { ortEntry } = await prepareWindowsFastembedRuntime({ fastembedEntry: entry, fastembedPackageDir });
|
|
142
|
+
requireFastembed(ortEntry);
|
|
143
|
+
}
|
|
144
|
+
const loaded: FastembedModule = requireFastembed(entry);
|
|
145
|
+
return loaded;
|
|
146
|
+
}
|
|
147
|
+
|
|
79
148
|
async function loadFromRuntimeInstall(): Promise<FastembedModule> {
|
|
80
149
|
const plan = fastembedRuntimeInstallPlan();
|
|
81
150
|
const runtimeDir = await ensureRuntimeInstalled({
|
|
@@ -89,14 +158,9 @@ async function loadFromRuntimeInstall(): Promise<FastembedModule> {
|
|
|
89
158
|
// onnxruntime-node, @anush008/tokenizers → platform binding, …) through
|
|
90
159
|
// the runtime cache.
|
|
91
160
|
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules });
|
|
92
|
-
if (process.platform === "win32") {
|
|
93
|
-
const ortEntry = resolveRuntimeModule(nodeModules, "onnxruntime-node");
|
|
94
|
-
if (ortEntry) createRequire(ortEntry)(ortEntry);
|
|
95
|
-
}
|
|
96
161
|
const entry = resolveRuntimeModule(nodeModules, "fastembed");
|
|
97
162
|
if (!entry) throw new Error(`fastembed runtime install at ${runtimeDir} has no loadable entry`);
|
|
98
|
-
|
|
99
|
-
return requireRuntime(entry) as FastembedModule;
|
|
163
|
+
return loadResolvedFastembed(entry, path.join(nodeModules, "fastembed"));
|
|
100
164
|
}
|
|
101
165
|
|
|
102
166
|
function isRecoverableFastembedLoadError(error: unknown): boolean {
|