@oh-my-pi/pi-mnemopi 18.2.0 → 18.2.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/package.json +5 -5
- package/src/core/beam/consolidate.ts +20 -2
- package/src/core/beam/helpers.ts +16 -0
- package/src/core/beam/store.ts +15 -2
- package/src/core/local-llm.ts +14 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.1] - 2026-09-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed an explicitly invalidated memory still being returned by an identical repeat query until the recall cache expired.
|
|
10
|
+
- Fixed recall continuing to serve a stale, pre-embedding ranking for up to an hour after background embeddings finished, when the enhanced recall cache is enabled.
|
|
11
|
+
|
|
5
12
|
## [18.0.11] - 2026-08-29
|
|
6
13
|
|
|
7
14
|
### 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.2.
|
|
4
|
+
"version": "18.2.2",
|
|
5
5
|
"description": "Local SQLite memory engine for Oh My Pi agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "18.2.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
44
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
45
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
42
|
+
"@oh-my-pi/pi-ai": "18.2.2",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "18.2.2",
|
|
44
|
+
"@oh-my-pi/pi-natives": "18.2.2",
|
|
45
|
+
"@oh-my-pi/pi-utils": "18.2.2"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"fastembed": "2.1.0",
|
|
@@ -576,8 +576,26 @@ export function extractAndStoreFacts(
|
|
|
576
576
|
const rawUnit = match[2] ?? "";
|
|
577
577
|
let unit = rawUnit.toLowerCase();
|
|
578
578
|
if (unit.endsWith("s") && !unit.endsWith("ms")) unit = unit.slice(0, -1);
|
|
579
|
-
|
|
580
|
-
|
|
579
|
+
// The naive fixed 50-char lookbehind can cross a markdown table cell, a code fence, or
|
|
580
|
+
// a sentence boundary and glue unrelated words into one underscore-joined key (e.g.
|
|
581
|
+
// "iam-role_smrx/sm-secrets_ssm-parameters_api" -> "2api" from a table row, or
|
|
582
|
+
// "cadence_90/60/5_30-_second" from a run-on list). Clamping the window to start after
|
|
583
|
+
// the nearest preceding structural boundary -- newline, table pipe, sentence-ending
|
|
584
|
+
// punctuation, or a backtick -- keeps the phrase within one cell/sentence/line, matching
|
|
585
|
+
// what a human would read as "the words right before this number" rather than 50 raw
|
|
586
|
+
// characters regardless of what they cross.
|
|
587
|
+
//
|
|
588
|
+
// A bare `.` is NOT always a sentence boundary: "v1.2", "3.14", "18.0.8" all contain
|
|
589
|
+
// dots that are part of a version/decimal number, not sentence punctuation. Only count
|
|
590
|
+
// `.`/`!`/`?` as a boundary when followed by whitespace or the end of the window -- i.e.
|
|
591
|
+
// it actually ends a sentence.
|
|
592
|
+
const windowStart = Math.max(0, (match.index ?? 0) - 50);
|
|
593
|
+
let window = text.slice(windowStart, match.index ?? 0);
|
|
594
|
+
const boundaryMatch = /\n|\||`|[.!?](?=\s|$)|;/g;
|
|
595
|
+
let lastBoundary = -1;
|
|
596
|
+
for (let m = boundaryMatch.exec(window); m; m = boundaryMatch.exec(window)) lastBoundary = m.index;
|
|
597
|
+
if (lastBoundary !== -1) window = window.slice(lastBoundary + 1);
|
|
598
|
+
const prefixWords = window
|
|
581
599
|
.replace(/`[^`]*`/g, " ")
|
|
582
600
|
.split(/\s+/)
|
|
583
601
|
.map(w => w.replace(/[.,:;!?()[\]"'`*_]/g, ""))
|
package/src/core/beam/helpers.ts
CHANGED
|
@@ -794,15 +794,31 @@ async function runEmbedding(beam: BeamMemoryState, items: readonly EmbedItem[]):
|
|
|
794
794
|
using insertEmbedding = beam.db.prepare(
|
|
795
795
|
"INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model) VALUES (?, ?, ?)",
|
|
796
796
|
);
|
|
797
|
+
let committed = 0;
|
|
797
798
|
const insertMany = beam.db.transaction((rows: readonly EmbedItem[]) => {
|
|
798
799
|
for (let i = 0; i < rows.length; i += 1) {
|
|
799
800
|
const vector = matrix[i];
|
|
800
801
|
const item = rows[i];
|
|
801
802
|
if (vector === undefined || item === undefined) continue;
|
|
802
803
|
insertEmbedding.run(item.memoryId, JSON.stringify(Array.from(vector)), model);
|
|
804
|
+
committed += 1;
|
|
803
805
|
}
|
|
804
806
|
});
|
|
805
807
|
insertMany(items);
|
|
808
|
+
// A recall taken while these vectors were still generating cached an FTS-only ranking, and
|
|
809
|
+
// committing vectors changes what recall returns -- so that cache must be dropped, or the
|
|
810
|
+
// pre-embedding order keeps being served for the whole cache TTL. Gated on `committed`: a
|
|
811
|
+
// batch whose provider returned a short or empty matrix inserts nothing, changes no ranking,
|
|
812
|
+
// and invalidating there would only discard a still-valid cache. Only the query cache is
|
|
813
|
+
// affected; the polyphonic subject dictionary is built from facts/gists, which an embedding
|
|
814
|
+
// batch never touches.
|
|
815
|
+
if (committed > 0) {
|
|
816
|
+
const caches = beam.caches as
|
|
817
|
+
| { queryCache?: { invalidate?: () => void }; _queryCache?: { invalidate?: () => void } }
|
|
818
|
+
| undefined;
|
|
819
|
+
caches?.queryCache?.invalidate?.();
|
|
820
|
+
caches?._queryCache?.invalidate?.();
|
|
821
|
+
}
|
|
806
822
|
} catch (error) {
|
|
807
823
|
// Background embedding generation is best-effort: a failing provider, a closed DB
|
|
808
824
|
// during shutdown, or a transient API error must never disrupt the synchronous
|
package/src/core/beam/store.ts
CHANGED
|
@@ -651,7 +651,16 @@ export function invalidate(beam: BeamMemoryState, memoryId: string, replacementI
|
|
|
651
651
|
`,
|
|
652
652
|
[now, replacementId, memoryId, beam.sessionId],
|
|
653
653
|
);
|
|
654
|
-
if (working.changes > 0)
|
|
654
|
+
if (working.changes > 0) {
|
|
655
|
+
// Recall filters `valid_until`/`superseded_by` in SQL, but the enhanced path consults the
|
|
656
|
+
// query cache BEFORE it reaches SQL. Without this the row a caller just retired keeps being
|
|
657
|
+
// served to an identical query for the rest of the cache TTL -- the one thing an explicit
|
|
658
|
+
// invalidation is supposed to guarantee against. Every other mutator here already does this;
|
|
659
|
+
// this one was the omission. Gated on an actual row change, like `forgetWorking`, so a
|
|
660
|
+
// no-op invalidation never discards a valid cache.
|
|
661
|
+
invalidateCaches(beam);
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
655
664
|
const episodic = beam.db.run(
|
|
656
665
|
`
|
|
657
666
|
UPDATE episodic_memory
|
|
@@ -660,7 +669,11 @@ export function invalidate(beam: BeamMemoryState, memoryId: string, replacementI
|
|
|
660
669
|
`,
|
|
661
670
|
[now, replacementId, memoryId, beam.sessionId],
|
|
662
671
|
);
|
|
663
|
-
|
|
672
|
+
if (episodic.changes > 0) {
|
|
673
|
+
invalidateCaches(beam);
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
return false;
|
|
664
677
|
}
|
|
665
678
|
|
|
666
679
|
export function getWorkingStats(
|
package/src/core/local-llm.ts
CHANGED
|
@@ -189,18 +189,20 @@ export async function callConfiguredCompletion(
|
|
|
189
189
|
return null;
|
|
190
190
|
}
|
|
191
191
|
try {
|
|
192
|
-
const message = await retryTransientCompletion(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
192
|
+
const message = await retryTransientCompletion(
|
|
193
|
+
() =>
|
|
194
|
+
completeSimple(
|
|
195
|
+
model,
|
|
196
|
+
{
|
|
197
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
apiKey: llmApiKey() || undefined,
|
|
201
|
+
maxTokens: opts.maxTokens ?? llmMaxTokens(),
|
|
202
|
+
temperature,
|
|
203
|
+
},
|
|
204
|
+
),
|
|
205
|
+
{ provider: model.provider },
|
|
204
206
|
);
|
|
205
207
|
return assistantText(message).trim() || null;
|
|
206
208
|
} catch {
|