@oh-my-pi/pi-mnemopi 18.18.0 → 18.18.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 +13 -0
- package/package.json +5 -5
- package/src/core/beam/recall.ts +6 -3
- package/src/core/beam/store.ts +63 -39
- package/src/core/polyphonic-recall.ts +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.18.2] - 2026-11-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed recall returning episodic summaries for working memories that had been invalidated.
|
|
10
|
+
|
|
11
|
+
## [18.18.1] - 2026-11-08
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed recall scoring memories against vectors from a previous embedding model while a model-change rebuild was still running or had failed: dense search now only uses vectors stamped with the active model, falling back to lexical matching for memories awaiting re-embedding ([#18442](https://github.com/can1357/oh-my-pi/pull/18442) by [@eggpeat](https://github.com/eggpeat))
|
|
16
|
+
- Fixed model-change embedding rebuilds overwriting newer vectors for updated memories or recreating vectors for deleted memories ([#18441](https://github.com/can1357/oh-my-pi/pull/18441) by [@eggpeat](https://github.com/eggpeat))
|
|
17
|
+
|
|
5
18
|
## [18.18.0] - 2026-11-07
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "18.18.
|
|
4
|
+
"version": "18.18.2",
|
|
5
5
|
"description": "Local SQLite memory engine for omp agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.18.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.18.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.18.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.18.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.18.2",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.18.2",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.18.2",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.18.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fastembed": "3.0.0",
|
package/src/core/beam/recall.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
unknownWeight,
|
|
14
14
|
} from "../../config";
|
|
15
15
|
import { hasCjk, matchesWordForm } from "../../util/regex";
|
|
16
|
-
import { embedQuery } from "../embeddings";
|
|
16
|
+
import { currentEmbeddingModel, embedQuery } from "../embeddings";
|
|
17
17
|
import { mmrRerank } from "../mmr";
|
|
18
18
|
import { type OrchestratedRecallResult, orchestrateRecall } from "../orchestrator";
|
|
19
19
|
import { POLYPHONIC_MAX_COMBINED_SCORE } from "../polyphonic-recall";
|
|
@@ -604,12 +604,15 @@ function vectorSimilarities(
|
|
|
604
604
|
) {
|
|
605
605
|
return out;
|
|
606
606
|
}
|
|
607
|
+
// Only vectors stamped with the active embedding model are comparable to the
|
|
608
|
+
// query embedding; stale-model rows stay on disk for the rebuild but must not score.
|
|
609
|
+
const model = currentEmbeddingModel();
|
|
607
610
|
for (let offset = 0; offset < memoryIds.length; offset += 500) {
|
|
608
611
|
const chunk = memoryIds.slice(offset, offset + 500);
|
|
609
612
|
const rows = queryAll(
|
|
610
613
|
beam,
|
|
611
|
-
`SELECT memory_id, embedding_json FROM memory_embeddings WHERE memory_id IN (${placeholders(chunk.length)})`,
|
|
612
|
-
chunk,
|
|
614
|
+
`SELECT memory_id, embedding_json FROM memory_embeddings WHERE model IS ? AND memory_id IN (${placeholders(chunk.length)})`,
|
|
615
|
+
[model, ...chunk],
|
|
613
616
|
);
|
|
614
617
|
for (const row of rows) {
|
|
615
618
|
const vector = parseEmbedding(row.embedding_json);
|
package/src/core/beam/store.ts
CHANGED
|
@@ -407,7 +407,7 @@ export function reconcileEmbeddingModel(beam: BeamMemoryState): void {
|
|
|
407
407
|
// Embed the whole corpus before touching anything stored. embed() handles the provider's
|
|
408
408
|
// per-request cap; a rejected chunk abandons the rebuild and leaves the stored vectors alone.
|
|
409
409
|
const toEmbed = live.filter(item => item.content.trim() !== "");
|
|
410
|
-
const staged: { memoryId: string; json: string }[] = [];
|
|
410
|
+
const staged: { memoryId: string; content: string; json: string }[] = [];
|
|
411
411
|
for (let offset = 0; offset < toEmbed.length; offset += EMBED_REBUILD_BATCH) {
|
|
412
412
|
const chunk = toEmbed.slice(offset, offset + EMBED_REBUILD_BATCH);
|
|
413
413
|
const vectors = await embed(chunk.map(item => item.content));
|
|
@@ -422,14 +422,18 @@ export function reconcileEmbeddingModel(beam: BeamMemoryState): void {
|
|
|
422
422
|
chunk.forEach((item, index) => {
|
|
423
423
|
const vector = vectors[index];
|
|
424
424
|
if (vector !== undefined) {
|
|
425
|
-
staged.push({
|
|
425
|
+
staged.push({
|
|
426
|
+
memoryId: item.memoryId,
|
|
427
|
+
content: item.content,
|
|
428
|
+
json: JSON.stringify(Array.from(vector)),
|
|
429
|
+
});
|
|
426
430
|
}
|
|
427
431
|
});
|
|
428
432
|
}
|
|
429
433
|
|
|
430
434
|
transaction(beam.db, () => {
|
|
431
|
-
// Only stale-model rows go. A vector that a concurrent remember()
|
|
432
|
-
// model survives
|
|
435
|
+
// Only stale-model rows go. A vector that a concurrent remember() or update() stamped
|
|
436
|
+
// under the active model survives. Each staged insert also revalidates its source text.
|
|
433
437
|
beam.db.query("DELETE FROM memory_embeddings WHERE model IS NOT ?").run(active);
|
|
434
438
|
beam.db.run("UPDATE episodic_memory SET binary_vector = NULL");
|
|
435
439
|
if (vecAvailable(beam.db)) {
|
|
@@ -439,10 +443,28 @@ export function reconcileEmbeddingModel(beam: BeamMemoryState): void {
|
|
|
439
443
|
// sqlite-vec cleanup is best-effort; rebuild correctness takes precedence.
|
|
440
444
|
}
|
|
441
445
|
}
|
|
442
|
-
using insertEmbedding = beam.db.prepare(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
+
using insertEmbedding = beam.db.prepare(`
|
|
447
|
+
INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model)
|
|
448
|
+
SELECT ?, ?, ?
|
|
449
|
+
WHERE EXISTS (
|
|
450
|
+
SELECT 1 FROM working_memory
|
|
451
|
+
WHERE id = ? AND superseded_by IS NULL AND COALESCE(embed_text, content) = ?
|
|
452
|
+
) OR EXISTS (
|
|
453
|
+
SELECT 1 FROM episodic_memory
|
|
454
|
+
WHERE id = ? AND superseded_by IS NULL AND content = ?
|
|
455
|
+
)
|
|
456
|
+
`);
|
|
457
|
+
for (const row of staged) {
|
|
458
|
+
insertEmbedding.run(
|
|
459
|
+
row.memoryId,
|
|
460
|
+
row.json,
|
|
461
|
+
active,
|
|
462
|
+
row.memoryId,
|
|
463
|
+
row.content,
|
|
464
|
+
row.memoryId,
|
|
465
|
+
row.content,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
446
468
|
});
|
|
447
469
|
beam.caches.queryCache?.invalidate();
|
|
448
470
|
|
|
@@ -693,37 +715,39 @@ export function getContext(beam: BeamMemoryState, limit = 10): Row[] {
|
|
|
693
715
|
|
|
694
716
|
export function invalidate(beam: BeamMemoryState, memoryId: string, replacementId: string | null = null): boolean {
|
|
695
717
|
const now = toUtcIso();
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
718
|
+
const changed = transaction(beam.db, () => {
|
|
719
|
+
const working = beam.db.run(
|
|
720
|
+
`
|
|
721
|
+
UPDATE working_memory
|
|
722
|
+
SET valid_until = ?, superseded_by = ?
|
|
723
|
+
WHERE id = ? AND (session_id = ? OR scope = 'global')
|
|
724
|
+
`,
|
|
725
|
+
[now, replacementId, memoryId, beam.sessionId],
|
|
726
|
+
);
|
|
727
|
+
if (working.changes > 0) {
|
|
728
|
+
// Retire episodic summaries built from this source: they still carry its text. Siblings
|
|
729
|
+
// covered by the same summary stay recallable via their (consolidated, never TTL-trimmed)
|
|
730
|
+
// working rows.
|
|
731
|
+
beam.db.run(
|
|
732
|
+
`UPDATE episodic_memory SET valid_until = ?, superseded_by = ?
|
|
733
|
+
WHERE (session_id = ? OR scope = 'global')
|
|
734
|
+
AND instr(',' || summary_of || ',', ',' || ? || ',') > 0`,
|
|
735
|
+
[now, replacementId, beam.sessionId, memoryId],
|
|
736
|
+
);
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
const episodic = beam.db.run(
|
|
740
|
+
`
|
|
741
|
+
UPDATE episodic_memory
|
|
742
|
+
SET valid_until = ?, superseded_by = ?
|
|
743
|
+
WHERE id = ? AND (session_id = ? OR scope = 'global')
|
|
744
|
+
`,
|
|
745
|
+
[now, replacementId, memoryId, beam.sessionId],
|
|
746
|
+
);
|
|
747
|
+
return episodic.changes > 0;
|
|
748
|
+
});
|
|
749
|
+
if (changed) invalidateCaches(beam);
|
|
750
|
+
return changed;
|
|
727
751
|
}
|
|
728
752
|
|
|
729
753
|
export function getWorkingStats(
|
|
@@ -5,6 +5,7 @@ import { closeQuietly, type DatabasePath, openDatabase } from "../db";
|
|
|
5
5
|
import { backfillConsolidatedFacts, ensureVeracityConsolidator } from "./beam/consolidate";
|
|
6
6
|
import type { BeamMemoryState, JsonValue, Metadata, RecallResult, RecallTierLabel } from "./beam/types";
|
|
7
7
|
import { EpisodicGraph, extractProperNouns } from "./episodic-graph";
|
|
8
|
+
import { currentEmbeddingModel } from "./embeddings";
|
|
8
9
|
import { STOP_WORDS } from "./synonyms";
|
|
9
10
|
import { VeracityConsolidator } from "./veracity-consolidation";
|
|
10
11
|
|
|
@@ -342,6 +343,7 @@ export class PolyphonicRecallEngine {
|
|
|
342
343
|
const queryUnit = normalizeVector(queryEmbedding);
|
|
343
344
|
if (queryUnit === null) return [];
|
|
344
345
|
const now = new Date().toISOString();
|
|
346
|
+
const model = currentEmbeddingModel();
|
|
345
347
|
const working = this.#visibility("wm", options.channelId);
|
|
346
348
|
const episodic = this.#visibility("em", options.channelId);
|
|
347
349
|
let rows: EmbeddingRow[] = [];
|
|
@@ -351,19 +353,21 @@ export class PolyphonicRecallEngine {
|
|
|
351
353
|
SELECT me.memory_id, me.embedding_json, 'working' AS embedding_tier
|
|
352
354
|
FROM memory_embeddings me
|
|
353
355
|
JOIN working_memory wm ON wm.id = me.memory_id
|
|
354
|
-
WHERE
|
|
356
|
+
WHERE me.model IS ?
|
|
357
|
+
AND wm.superseded_by IS NULL
|
|
355
358
|
AND (wm.valid_until IS NULL OR wm.valid_until > ?)
|
|
356
359
|
AND ${working.clause}
|
|
357
360
|
UNION ALL
|
|
358
361
|
SELECT me.memory_id, me.embedding_json, 'episodic' AS embedding_tier
|
|
359
362
|
FROM memory_embeddings me
|
|
360
363
|
JOIN episodic_memory em ON em.id = me.memory_id
|
|
361
|
-
WHERE
|
|
364
|
+
WHERE me.model IS ?
|
|
365
|
+
AND em.superseded_by IS NULL
|
|
362
366
|
AND (em.valid_until IS NULL OR em.valid_until > ?)
|
|
363
367
|
AND ${episodic.clause}
|
|
364
368
|
LIMIT 50000
|
|
365
369
|
`)
|
|
366
|
-
.all(now, ...working.params, now, ...episodic.params) as EmbeddingRow[];
|
|
370
|
+
.all(model, now, ...working.params, model, now, ...episodic.params) as EmbeddingRow[];
|
|
367
371
|
} catch {
|
|
368
372
|
return [];
|
|
369
373
|
}
|