@oh-my-pi/pi-mnemopi 18.8.9 → 18.9.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.
- package/CHANGELOG.md +10 -0
- package/dist/types/core/beam/index.d.ts +1 -0
- package/dist/types/core/episodic-graph.d.ts +0 -1
- package/package.json +5 -5
- package/src/core/beam/helpers.ts +27 -23
- package/src/core/beam/index.ts +1 -0
- package/src/core/beam/recall.ts +74 -62
- package/src/core/beam/store.ts +8 -6
- package/src/core/embeddings.ts +16 -10
- package/src/core/episodic-graph.ts +2 -19
- package/src/core/extraction.ts +89 -79
- package/src/core/memory.ts +101 -83
- package/src/core/polyphonic-recall.ts +12 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.9.0] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Exported `clipRecallContent` for applications that need to format content consistently with Mnemopi recall previews.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed memory-save performance degradation and excessive memory-database growth when date-related terms such as “today” appeared in otherwise unrelated memories.
|
|
14
|
+
|
|
5
15
|
## [18.8.8] - 2026-10-10
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import type { VeracityConsolidator } from "../veracity-consolidation.js";
|
|
3
3
|
import type { BeamCaches, BeamConfig, BeamEvent, BeamMemoryOptions, BeamMemoryState, BeamStats, ImportStats, MemoriaRetrieveResult, Metadata, RecallEnhancedOptions, RecallOptions, RecallResult, RememberBatchItem, RememberBatchOptions, RememberOptions, SleepResult } from "./types.js";
|
|
4
|
+
export { clipRecallContent } from "./recall.js";
|
|
4
5
|
export { initBeam } from "./schema.js";
|
|
5
6
|
export type * from "./types.js";
|
|
6
7
|
export declare class BeamMemory implements BeamMemoryState {
|
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.
|
|
4
|
+
"version": "18.9.0",
|
|
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.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.9.0",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.9.0",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.9.0",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.9.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fastembed": "3.0.0",
|
package/src/core/beam/helpers.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { logger, withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { generateId as generateTimedId, sha256Hex16, stableMemoryId } from "../../util/ids";
|
|
4
4
|
import { cjkFtsTerms, containsSpacelessCjk, ftsQueryTerms, hasCjk, isCjkChar, recallTokens } from "../../util/regex";
|
|
5
5
|
import { currentEmbeddingModel, embed } from "../embeddings";
|
|
@@ -727,29 +727,33 @@ async function runEmbedding(beam: BeamMemoryState, items: readonly EmbedItem[]):
|
|
|
727
727
|
try {
|
|
728
728
|
const matrix = await embed(items.map(item => item.content));
|
|
729
729
|
if (matrix === null) return;
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
730
|
+
// Persisting the vectors runs synchronously on the caller's event loop; label it so a
|
|
731
|
+
// stall is attributable. The embedding itself is awaited above, outside the label.
|
|
732
|
+
withLoopPhase("mnemopi.embed-persist", () => {
|
|
733
|
+
const model = currentEmbeddingModel();
|
|
734
|
+
using insertEmbedding = beam.db.prepare(
|
|
735
|
+
"INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model) VALUES (?, ?, ?)",
|
|
736
|
+
);
|
|
737
|
+
let committed = 0;
|
|
738
|
+
const insertMany = beam.db.transaction((rows: readonly EmbedItem[]) => {
|
|
739
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
740
|
+
const vector = matrix[i];
|
|
741
|
+
const item = rows[i];
|
|
742
|
+
if (vector === undefined || item === undefined) continue;
|
|
743
|
+
insertEmbedding.run(item.memoryId, JSON.stringify(Array.from(vector)), model);
|
|
744
|
+
committed += 1;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
insertMany(items);
|
|
748
|
+
// A recall taken while these vectors were still generating cached an FTS-only ranking, and
|
|
749
|
+
// committing vectors changes what recall returns -- so that cache must be dropped, or the
|
|
750
|
+
// pre-embedding order keeps being served for the whole cache TTL. Gated on `committed`: a
|
|
751
|
+
// batch whose provider returned a short or empty matrix inserts nothing, changes no ranking,
|
|
752
|
+
// and invalidating there would only discard a still-valid cache. Only the query cache is
|
|
753
|
+
// affected; the polyphonic subject dictionary is built from facts/gists, which an embedding
|
|
754
|
+
// batch never touches.
|
|
755
|
+
if (committed > 0) beam.caches.queryCache?.invalidate();
|
|
743
756
|
});
|
|
744
|
-
insertMany(items);
|
|
745
|
-
// A recall taken while these vectors were still generating cached an FTS-only ranking, and
|
|
746
|
-
// committing vectors changes what recall returns -- so that cache must be dropped, or the
|
|
747
|
-
// pre-embedding order keeps being served for the whole cache TTL. Gated on `committed`: a
|
|
748
|
-
// batch whose provider returned a short or empty matrix inserts nothing, changes no ranking,
|
|
749
|
-
// and invalidating there would only discard a still-valid cache. Only the query cache is
|
|
750
|
-
// affected; the polyphonic subject dictionary is built from facts/gists, which an embedding
|
|
751
|
-
// batch never touches.
|
|
752
|
-
if (committed > 0) beam.caches.queryCache?.invalidate();
|
|
753
757
|
} catch (error) {
|
|
754
758
|
// Background embedding generation is best-effort: a failing provider, a closed DB
|
|
755
759
|
// during shutdown, or a transient API error must never disrupt the synchronous
|
package/src/core/beam/index.ts
CHANGED
package/src/core/beam/recall.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { popLoopPhase, pushLoopPhase, withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import { normalizedRecallWeights, polyphonicRecallEnabled, temporalHalflifeHours } from "../../config";
|
|
2
3
|
import { hasCjk, matchesWordForm } from "../../util/regex";
|
|
3
4
|
import { embedQuery } from "../embeddings";
|
|
@@ -923,35 +924,37 @@ export async function recall(
|
|
|
923
924
|
const derived = query.length > 0 ? await embedQuery(query) : null;
|
|
924
925
|
temporalOptions.queryEmbedding = derived === null ? null : Array.from(derived);
|
|
925
926
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
927
|
+
return withLoopPhase("mnemopi.recall", () => {
|
|
928
|
+
let weights = normalizedRecallWeights(
|
|
929
|
+
options.vecWeight ?? beam.config.vecWeight,
|
|
930
|
+
options.ftsWeight ?? beam.config.ftsWeight,
|
|
931
|
+
options.importanceWeight ?? beam.config.importanceWeight,
|
|
932
|
+
);
|
|
933
|
+
if (options.useIntent === true) {
|
|
934
|
+
const intent = classifyIntent(query);
|
|
935
|
+
weights = adjustWeights(weights[0], weights[1], weights[2], intent);
|
|
936
|
+
}
|
|
937
|
+
const useSynonyms = options.useSynonyms !== false;
|
|
938
|
+
const tokens = expandedTokens(query, useSynonyms);
|
|
939
|
+
const tokenGroups = expandedTokenGroups(query, useSynonyms);
|
|
940
|
+
const candidates = collectMemoryCandidates(beam, query, topK, temporalOptions);
|
|
941
|
+
const scored: RecallResult[] = [];
|
|
942
|
+
for (const candidate of candidates) {
|
|
943
|
+
const result = scoreCandidate(candidate, tokens, tokenGroups, weights, temporalOptions);
|
|
944
|
+
if (result !== null) scored.push(result);
|
|
945
|
+
}
|
|
946
|
+
scored.sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
|
|
947
|
+
let finalResults = dedupCrossTierSummaryLinks(beam, dedupeResults(scored));
|
|
948
|
+
if (query.length > 0 && tokens.length >= 4 && finalResults.length > topK)
|
|
949
|
+
finalResults = diversifyByCoverage(finalResults, tokens, topK);
|
|
950
|
+
if (options.useMmr === true && finalResults.length > 1) {
|
|
951
|
+
finalResults = rerankRecallResults(finalResults, options.mmrLambda ?? 0.7, topK);
|
|
952
|
+
} else {
|
|
953
|
+
finalResults = finalResults.slice(0, topK);
|
|
954
|
+
}
|
|
955
|
+
if (temporalOptions.updateRecallCounts !== false) updateRecallCounts(beam, finalResults, temporalOptions);
|
|
956
|
+
return finalResults;
|
|
957
|
+
});
|
|
955
958
|
}
|
|
956
959
|
|
|
957
960
|
function diversifyByCoverage(
|
|
@@ -1034,37 +1037,44 @@ export async function recallEnhanced(
|
|
|
1034
1037
|
let cache: QueryCache<RecallResult> | null = null;
|
|
1035
1038
|
let token = "";
|
|
1036
1039
|
let scope = "";
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
beam.caches.queryCacheToken
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1040
|
+
pushLoopPhase("mnemopi.recall");
|
|
1041
|
+
try {
|
|
1042
|
+
if (cacheEnabled) {
|
|
1043
|
+
beam.caches.queryCache ??= new QueryCache<RecallResult>({
|
|
1044
|
+
maxSize: ENHANCED_RECALL_CACHE_MAX_ENTRIES,
|
|
1045
|
+
ttlSeconds: ENHANCED_RECALL_CACHE_TTL_SECONDS,
|
|
1046
|
+
});
|
|
1047
|
+
cache = beam.caches.queryCache;
|
|
1048
|
+
// Explicit hooks invalidate on this beam's own writes; the token also catches writes
|
|
1049
|
+
// no hook covers (sleep, graph ingest) and commits from other connections.
|
|
1050
|
+
token = databaseWriteToken(beam);
|
|
1051
|
+
if (beam.caches.queryCacheToken !== token) {
|
|
1052
|
+
cache.invalidate();
|
|
1053
|
+
beam.caches.queryCacheToken = token;
|
|
1054
|
+
}
|
|
1055
|
+
scope = enhancedRecallCacheScope(beam, topK, options, polyphonic);
|
|
1056
|
+
const cached = cache.get(query, queryEmbedding, scope);
|
|
1057
|
+
if (cached !== null) {
|
|
1058
|
+
const results = structuredClone(cached) as RecallResult[];
|
|
1059
|
+
if (countRecalls) countRecallsKeepingCache(beam, results, runOptions, token);
|
|
1060
|
+
return results;
|
|
1061
|
+
}
|
|
1056
1062
|
}
|
|
1063
|
+
} finally {
|
|
1064
|
+
popLoopPhase();
|
|
1057
1065
|
}
|
|
1058
1066
|
|
|
1059
1067
|
const results = polyphonic
|
|
1060
1068
|
? await polyphonicRecallEnhanced(beam, query, topK, runOptions)
|
|
1061
1069
|
: await linearRecallEnhanced(beam, query, topK, runOptions);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1070
|
+
return withLoopPhase("mnemopi.recall", () => {
|
|
1071
|
+
// A write committed while recall awaited may be missing from this ranking: skip the
|
|
1072
|
+
// put and keep the stale token so the next lookup starts from an empty cache.
|
|
1073
|
+
const cacheable = cache !== null && databaseWriteToken(beam) === token;
|
|
1074
|
+
if (cacheable) cache?.put(query, structuredClone(results), queryEmbedding, scope);
|
|
1075
|
+
if (countRecalls) countRecallsKeepingCache(beam, results, runOptions, cacheable ? token : null);
|
|
1076
|
+
return results;
|
|
1077
|
+
});
|
|
1068
1078
|
}
|
|
1069
1079
|
|
|
1070
1080
|
async function linearRecallEnhanced(
|
|
@@ -1084,14 +1094,16 @@ async function linearRecallEnhanced(
|
|
|
1084
1094
|
...enhancedOptions,
|
|
1085
1095
|
updateRecallCounts: false,
|
|
1086
1096
|
});
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1097
|
+
return withLoopPhase("mnemopi.recall", () => {
|
|
1098
|
+
if (options.includeFacts === true) {
|
|
1099
|
+
const facts = factRecall(beam, query, factRecallLimit(topK));
|
|
1100
|
+
results.push(...facts);
|
|
1101
|
+
}
|
|
1102
|
+
results.sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
|
|
1103
|
+
const finalResults = rerankRecallResults(results, options.mmrLambda ?? 0.7, topK);
|
|
1104
|
+
if (enhancedOptions.updateRecallCounts !== false) updateRecallCounts(beam, finalResults, enhancedOptions);
|
|
1105
|
+
return finalResults;
|
|
1106
|
+
});
|
|
1095
1107
|
}
|
|
1096
1108
|
|
|
1097
1109
|
/**
|
package/src/core/beam/store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database, SQLQueryBindings } from "bun:sqlite";
|
|
2
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { logger, withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { transaction } from "../../db";
|
|
4
4
|
import { toUtcIso } from "../../util/datetime";
|
|
5
5
|
import { generateId } from "../../util/ids";
|
|
@@ -307,9 +307,11 @@ function proactiveLinkIfEnabled(
|
|
|
307
307
|
async function runFactExtraction(beam: BeamMemoryState, memoryId: string, content: string): Promise<void> {
|
|
308
308
|
try {
|
|
309
309
|
const extracted = await extractFactCategoriesSafe(content);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
withLoopPhase("mnemopi.extract", () => {
|
|
311
|
+
if (countExtractedFactCategories(extracted) === 0) return;
|
|
312
|
+
storeExtractedFactCategories(beam, extracted, 0, memoryId);
|
|
313
|
+
invalidateCaches(beam);
|
|
314
|
+
});
|
|
313
315
|
} catch {
|
|
314
316
|
// Background fact extraction is best-effort and never surfaces to the caller.
|
|
315
317
|
}
|
|
@@ -740,7 +742,7 @@ export function updateWorking(
|
|
|
740
742
|
export function get(beam: BeamMemoryState, memoryId: string): Row | null {
|
|
741
743
|
using workingStatement = beam.db.prepare(`
|
|
742
744
|
SELECT id, content, source, timestamp, session_id,
|
|
743
|
-
importance, metadata_json, veracity, created_at
|
|
745
|
+
importance, metadata_json, veracity, created_at, valid_until, superseded_by
|
|
744
746
|
FROM working_memory
|
|
745
747
|
WHERE id = ?
|
|
746
748
|
`);
|
|
@@ -749,7 +751,7 @@ export function get(beam: BeamMemoryState, memoryId: string): Row | null {
|
|
|
749
751
|
|
|
750
752
|
using episodicStatement = beam.db.prepare(`
|
|
751
753
|
SELECT id, content, source, timestamp, session_id,
|
|
752
|
-
importance, metadata_json, veracity, created_at
|
|
754
|
+
importance, metadata_json, veracity, created_at, valid_until, superseded_by
|
|
753
755
|
FROM episodic_memory
|
|
754
756
|
WHERE id = ? AND (session_id = ? OR scope = 'global')
|
|
755
757
|
`);
|
package/src/core/embeddings.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
fetchWithRetry,
|
|
12
12
|
getFastembedCacheDir,
|
|
13
13
|
logger,
|
|
14
|
+
withLoopPhase,
|
|
14
15
|
} from "@oh-my-pi/pi-utils";
|
|
15
16
|
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
16
17
|
import type { EmbeddingModel } from "fastembed";
|
|
@@ -294,9 +295,11 @@ export function embeddingDimFor(modelName: string): number {
|
|
|
294
295
|
async function collectMatrix(batches: EmbeddingOutput): Promise<EmbeddingMatrix> {
|
|
295
296
|
const rows: Vector[] = [];
|
|
296
297
|
for await (const batch of batches) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
withLoopPhase("mnemopi.embed", () => {
|
|
299
|
+
for (const row of batch) {
|
|
300
|
+
rows.push(new Float32Array(row));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
300
303
|
}
|
|
301
304
|
return rows;
|
|
302
305
|
}
|
|
@@ -388,12 +391,15 @@ async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | nul
|
|
|
388
391
|
if (!response.ok) {
|
|
389
392
|
return null;
|
|
390
393
|
}
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
const data = (await response.json()) as { data?: Array<{ embedding: number[] }> };
|
|
395
|
+
return withLoopPhase("mnemopi.embed", () => {
|
|
396
|
+
const { data: rows } = data;
|
|
397
|
+
if (rows === undefined) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
apiCallCount += 1;
|
|
401
|
+
return rows.map(row => new Float32Array(row.embedding));
|
|
402
|
+
});
|
|
397
403
|
} catch (error) {
|
|
398
404
|
logger.debug("mnemopi embedding request failed", { status: extractHttpStatusFromError(error) });
|
|
399
405
|
return null;
|
|
@@ -525,7 +531,7 @@ export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix |
|
|
|
525
531
|
return null;
|
|
526
532
|
}
|
|
527
533
|
try {
|
|
528
|
-
const vectors = await collectMatrix(model.embed([...texts]));
|
|
534
|
+
const vectors = await withLoopPhase("mnemopi.embed", () => collectMatrix(model.embed([...texts])));
|
|
529
535
|
if (vectors.length === 1) {
|
|
530
536
|
const vector = vectors[0];
|
|
531
537
|
if (vector !== undefined) {
|
|
@@ -571,17 +571,12 @@ export class EpisodicGraph {
|
|
|
571
571
|
this.addEdge(edge);
|
|
572
572
|
edges.push(edge);
|
|
573
573
|
}
|
|
574
|
-
|
|
575
|
-
lexicalScore,
|
|
576
|
-
entityScore,
|
|
577
|
-
this.temporalContextScore(memoryId, otherId),
|
|
578
|
-
);
|
|
579
|
-
if (!wroteCtxEdge && contextualScore >= minLinkScore) {
|
|
574
|
+
if (!wroteCtxEdge && entityScore >= minLinkScore) {
|
|
580
575
|
const ctxEdge = {
|
|
581
576
|
source: memoryId,
|
|
582
577
|
target: otherId,
|
|
583
578
|
edgeType: "ctx",
|
|
584
|
-
weight:
|
|
579
|
+
weight: entityScore,
|
|
585
580
|
timestamp,
|
|
586
581
|
};
|
|
587
582
|
this.addEdge(ctxEdge);
|
|
@@ -736,18 +731,6 @@ export class EpisodicGraph {
|
|
|
736
731
|
return Math.round(overlapScore(left, right) * 1000) / 1000;
|
|
737
732
|
}
|
|
738
733
|
|
|
739
|
-
private temporalContextScore(sourceMemoryId: string, targetMemoryId: string): number {
|
|
740
|
-
const left = this.db.query("SELECT time_scope FROM gists WHERE memory_id = ?").get(sourceMemoryId) as {
|
|
741
|
-
time_scope: string | null;
|
|
742
|
-
} | null;
|
|
743
|
-
if (left?.time_scope === null || left?.time_scope === undefined) return 0;
|
|
744
|
-
const right = this.db.query("SELECT time_scope FROM gists WHERE memory_id = ?").get(targetMemoryId) as {
|
|
745
|
-
time_scope: string | null;
|
|
746
|
-
} | null;
|
|
747
|
-
if (right?.time_scope === null || right?.time_scope === undefined) return 0;
|
|
748
|
-
return left.time_scope === right.time_scope ? DEFAULT_LINK_THRESHOLD : 0;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
734
|
private memoryFeatures(memoryId: string): Set<string> {
|
|
752
735
|
const gistRows = this.db.query("SELECT * FROM gists WHERE memory_id = ?").all(memoryId) as GistRow[];
|
|
753
736
|
const factRows = this.db.query("SELECT * FROM facts WHERE source_msg_id = ?").all(memoryId) as FactRow[];
|
package/src/core/extraction.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import { getDiagnostics, safeForLog } from "./extraction/diagnostics";
|
|
2
3
|
import { callHostLlm, getHostLlmBackend } from "./llm-backends";
|
|
3
4
|
import {
|
|
@@ -219,51 +220,58 @@ export function countExtractedFactCategories(extracted: ExtractedFactCategories)
|
|
|
219
220
|
|
|
220
221
|
/** Parse extractor output without discarding MEMORIA categories or KG triples. */
|
|
221
222
|
export function parseExtractedFactCategories(rawOutput: string | null | undefined): ExtractedFactCategories {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
223
|
+
return withLoopPhase("mnemopi.extract", () => {
|
|
224
|
+
if (rawOutput === null || rawOutput === undefined) {
|
|
225
|
+
return emptyFactCategories();
|
|
226
|
+
}
|
|
227
|
+
const raw = rawOutput.trim();
|
|
228
|
+
if (raw === "" || raw.toUpperCase() === "NO_FACTS") {
|
|
229
|
+
return emptyFactCategories();
|
|
230
|
+
}
|
|
231
|
+
const rawClean = stripFence(raw);
|
|
232
|
+
if (rawClean.startsWith("{")) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed: unknown = JSON.parse(rawClean);
|
|
235
|
+
if (isRecord(parsed)) {
|
|
236
|
+
return {
|
|
237
|
+
facts: normalizeFactArray(parsed.facts, { fields: FACT_TEXT_FIELD_KEYS }),
|
|
238
|
+
instructions: normalizeFactArray(parsed.instructions, { fields: INSTRUCTION_TEXT_FIELD_KEYS }),
|
|
239
|
+
preferences: normalizeFactArray(parsed.preferences, { fields: PREFERENCE_TEXT_FIELD_KEYS }),
|
|
240
|
+
timelines: normalizeFactArray(parsed.timelines, {
|
|
241
|
+
fields: TIMELINE_TEXT_FIELD_KEYS,
|
|
242
|
+
joinFields: true,
|
|
243
|
+
}),
|
|
244
|
+
kg: normalizeKgArray(parsed.kg),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
const matches = [...raw.matchAll(/"([^"]{10,})"/g)]
|
|
249
|
+
.map(m => m[1])
|
|
250
|
+
.filter((v): v is string => v !== undefined);
|
|
251
|
+
if (matches.length > 0) {
|
|
252
|
+
return {
|
|
253
|
+
...emptyFactCategories(),
|
|
254
|
+
facts: matches
|
|
255
|
+
.map(normalizeFact)
|
|
256
|
+
.filter(f => f !== "")
|
|
257
|
+
.slice(0, FLAT_FACT_LIMIT),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
252
260
|
}
|
|
253
261
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
const cleaned: string[] = [];
|
|
263
|
+
for (const line of raw.split("\n")) {
|
|
264
|
+
const fact = line.replace(/^[\s\d.\-*]+/, "").trim();
|
|
265
|
+
if (fact.length > 10) {
|
|
266
|
+
const normalized = normalizeFact(fact);
|
|
267
|
+
if (normalized !== "") {
|
|
268
|
+
cleaned.push(normalized);
|
|
269
|
+
}
|
|
262
270
|
}
|
|
271
|
+
if (cleaned.length >= FLAT_FACT_LIMIT) break;
|
|
263
272
|
}
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return { ...emptyFactCategories(), facts: cleaned };
|
|
273
|
+
return { ...emptyFactCategories(), facts: cleaned };
|
|
274
|
+
});
|
|
267
275
|
}
|
|
268
276
|
|
|
269
277
|
/** Parse extractor output into the legacy flat string fact list. */
|
|
@@ -283,42 +291,44 @@ function addUnique(out: string[], value: string): void {
|
|
|
283
291
|
}
|
|
284
292
|
|
|
285
293
|
export function heuristicExtractFacts(text: string): string[] {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const facts: string[] = [];
|
|
291
|
-
const clauses = normalized.split(/(?:[.!?;]+|\s+and\s+|\s+but\s+)/i);
|
|
292
|
-
for (const clause of clauses) {
|
|
293
|
-
const c = clause.trim();
|
|
294
|
-
let value = /\bmy name is\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
295
|
-
if (value !== undefined) addUnique(facts, `The user's name is ${value}`);
|
|
296
|
-
value = /\bi (?:am|work as)\s+(?:an?\s+)?([^,.!?;]+)/i.exec(c)?.[1];
|
|
297
|
-
if (value !== undefined) addUnique(facts, `The user is ${value}`);
|
|
298
|
-
value = /\bi work (?:at|for)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
299
|
-
if (value !== undefined) addUnique(facts, `The user works at ${value}`);
|
|
300
|
-
value = /\bi (?:live in|am based in)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
301
|
-
if (value !== undefined) addUnique(facts, `The user lives in ${value}`);
|
|
302
|
-
value = /\bi (?:use|uses|am using)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
303
|
-
if (value !== undefined) addUnique(facts, `The user uses ${value}`);
|
|
304
|
-
value = /\bi (?:like|love|prefer|enjoy)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
305
|
-
if (value !== undefined) addUnique(facts, `The user prefers ${value}`);
|
|
306
|
-
value = /\bi (?:hate|dislike|do not like|don't like)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
307
|
-
if (value !== undefined) addUnique(facts, `The user dislikes ${value}`);
|
|
308
|
-
// Require an explicit `i` or `you` subject before `always|never`. The
|
|
309
|
-
// other heuristics in this block all need an `i` subject (`i live in …`,
|
|
310
|
-
// `i use …`) which keeps them from matching narrative prose; the
|
|
311
|
-
// `Instruction:` pattern used to match any `always|never` token, so
|
|
312
|
-
// assistant prose like "the panel never populates" became stored as a
|
|
313
|
-
// user `Instruction:` memory (coding-agent issue #3372). Subject
|
|
314
|
-
// constraint mirrors how the rest of the heuristics filter for first- /
|
|
315
|
-
// second-person assertions and keeps narrative third-person prose out.
|
|
316
|
-
const instruction = /\b(?:i|you)\s+(always|never)\s+([^,.!?;]+)/i.exec(c);
|
|
317
|
-
if (instruction?.[1] !== undefined && instruction[2] !== undefined) {
|
|
318
|
-
addUnique(facts, `Instruction: ${instruction[1].toLowerCase()} ${instruction[2]}`);
|
|
294
|
+
return withLoopPhase("mnemopi.extract", () => {
|
|
295
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
296
|
+
if (normalized === "") {
|
|
297
|
+
return [];
|
|
319
298
|
}
|
|
320
|
-
|
|
321
|
-
|
|
299
|
+
const facts: string[] = [];
|
|
300
|
+
const clauses = normalized.split(/(?:[.!?;]+|\s+and\s+|\s+but\s+)/i);
|
|
301
|
+
for (const clause of clauses) {
|
|
302
|
+
const c = clause.trim();
|
|
303
|
+
let value = /\bmy name is\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
304
|
+
if (value !== undefined) addUnique(facts, `The user's name is ${value}`);
|
|
305
|
+
value = /\bi (?:am|work as)\s+(?:an?\s+)?([^,.!?;]+)/i.exec(c)?.[1];
|
|
306
|
+
if (value !== undefined) addUnique(facts, `The user is ${value}`);
|
|
307
|
+
value = /\bi work (?:at|for)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
308
|
+
if (value !== undefined) addUnique(facts, `The user works at ${value}`);
|
|
309
|
+
value = /\bi (?:live in|am based in)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
310
|
+
if (value !== undefined) addUnique(facts, `The user lives in ${value}`);
|
|
311
|
+
value = /\bi (?:use|uses|am using)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
312
|
+
if (value !== undefined) addUnique(facts, `The user uses ${value}`);
|
|
313
|
+
value = /\bi (?:like|love|prefer|enjoy)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
314
|
+
if (value !== undefined) addUnique(facts, `The user prefers ${value}`);
|
|
315
|
+
value = /\bi (?:hate|dislike|do not like|don't like)\s+([^,.!?;]+)/i.exec(c)?.[1];
|
|
316
|
+
if (value !== undefined) addUnique(facts, `The user dislikes ${value}`);
|
|
317
|
+
// Require an explicit `i` or `you` subject before `always|never`. The
|
|
318
|
+
// other heuristics in this block all need an `i` subject (`i live in …`,
|
|
319
|
+
// `i use …`) which keeps them from matching narrative prose; the
|
|
320
|
+
// `Instruction:` pattern used to match any `always|never` token, so
|
|
321
|
+
// assistant prose like "the panel never populates" became stored as a
|
|
322
|
+
// user `Instruction:` memory (coding-agent issue #3372). Subject
|
|
323
|
+
// constraint mirrors how the rest of the heuristics filter for first- /
|
|
324
|
+
// second-person assertions and keeps narrative third-person prose out.
|
|
325
|
+
const instruction = /\b(?:i|you)\s+(always|never)\s+([^,.!?;]+)/i.exec(c);
|
|
326
|
+
if (instruction?.[1] !== undefined && instruction[2] !== undefined) {
|
|
327
|
+
addUnique(facts, `Instruction: ${instruction[1].toLowerCase()} ${instruction[2]}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return facts.slice(0, 5);
|
|
331
|
+
});
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
async function tryHostExtraction(prompt: string): Promise<[boolean, string | null]> {
|
|
@@ -345,7 +355,7 @@ async function localFallback(
|
|
|
345
355
|
try {
|
|
346
356
|
const raw = await callLocalLlm(prompt);
|
|
347
357
|
if (raw !== null) {
|
|
348
|
-
const extracted = parseExtractedFactCategories(cleanOutput(raw));
|
|
358
|
+
const extracted = withLoopPhase("mnemopi.extract", () => parseExtractedFactCategories(cleanOutput(raw)));
|
|
349
359
|
const count = countExtractedFactCategories(extracted);
|
|
350
360
|
if (count > 0) {
|
|
351
361
|
diag.recordSuccess("local", count);
|
|
@@ -380,7 +390,7 @@ export async function extractFactCategories(
|
|
|
380
390
|
return emptyFactCategories();
|
|
381
391
|
}
|
|
382
392
|
const input = clipToWindow(text, DEFAULT_INPUT_CHARS);
|
|
383
|
-
const prompt = buildExtractionPrompt(input);
|
|
393
|
+
const prompt = withLoopPhase("mnemopi.extract", () => buildExtractionPrompt(input));
|
|
384
394
|
|
|
385
395
|
// Configured completion (host-injected runtime LLM, e.g. the coding-agent's smol
|
|
386
396
|
// or a local on-device model). Mirrors consolidation's precedence: when a
|
|
@@ -453,7 +463,7 @@ export async function extractFactCategories(
|
|
|
453
463
|
try {
|
|
454
464
|
const raw = await callRemoteLlm(prompt, 0, options);
|
|
455
465
|
if (raw !== null) {
|
|
456
|
-
const extracted = parseExtractedFactCategories(cleanOutput(raw));
|
|
466
|
+
const extracted = withLoopPhase("mnemopi.extract", () => parseExtractedFactCategories(cleanOutput(raw)));
|
|
457
467
|
const count = countExtractedFactCategories(extracted);
|
|
458
468
|
if (count > 0) {
|
|
459
469
|
diag.recordSuccess("remote", count);
|
package/src/core/memory.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { popLoopPhase, pushLoopPhase, withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
3
4
|
|
|
4
5
|
import { dbPath as configuredDbPath } from "../config";
|
|
5
6
|
import { closeQuietly } from "../db";
|
|
@@ -406,55 +407,62 @@ export class Mnemopi {
|
|
|
406
407
|
#closed = false;
|
|
407
408
|
|
|
408
409
|
constructor(options: MnemopiOptions = {}) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (options.db !== undefined) {
|
|
430
|
-
const opened = this.beam.db;
|
|
431
|
-
initBeam(options.db);
|
|
432
|
-
Object.defineProperty(this.beam, "db", { value: options.db });
|
|
433
|
-
Object.defineProperty(this.beam, "annotations", {
|
|
434
|
-
value: buildBeamAnnotations(options.db, this.dbPath),
|
|
435
|
-
});
|
|
436
|
-
Object.defineProperty(this.beam, "episodicGraph", {
|
|
437
|
-
value: buildEpisodicGraph(options.db, this.dbPath),
|
|
410
|
+
pushLoopPhase("mnemopi.open");
|
|
411
|
+
try {
|
|
412
|
+
this.sessionId = options.sessionId ?? options.session_id ?? "default";
|
|
413
|
+
this.bank = options.bank ?? "default";
|
|
414
|
+
|
|
415
|
+
this.authorId = options.authorId ?? options.author_id ?? null;
|
|
416
|
+
this.authorType = options.authorType ?? options.author_type ?? null;
|
|
417
|
+
this.channelId = options.channelId ?? options.channel_id ?? this.sessionId;
|
|
418
|
+
this.dbPath = resolveDbPath(options, this.bank);
|
|
419
|
+
this.runtimeOptions = resolveRuntimeOptions(options);
|
|
420
|
+
|
|
421
|
+
this.beam = new BeamMemory({
|
|
422
|
+
sessionId: this.sessionId,
|
|
423
|
+
dbPath: options.db === undefined ? this.dbPath : ":memory:",
|
|
424
|
+
authorId: this.authorId,
|
|
425
|
+
authorType: this.authorType,
|
|
426
|
+
channelId: this.channelId,
|
|
427
|
+
proactiveLinking: options.proactiveLinking,
|
|
428
|
+
polyphonicRecall: options.polyphonicRecall,
|
|
429
|
+
enhancedRecall: options.enhancedRecall,
|
|
438
430
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
431
|
+
this.#ownsDb = options.db === undefined;
|
|
432
|
+
if (options.db !== undefined) {
|
|
433
|
+
const opened = this.beam.db;
|
|
434
|
+
initBeam(options.db);
|
|
435
|
+
Object.defineProperty(this.beam, "db", { value: options.db });
|
|
436
|
+
Object.defineProperty(this.beam, "annotations", {
|
|
437
|
+
value: buildBeamAnnotations(options.db, this.dbPath),
|
|
438
|
+
});
|
|
439
|
+
Object.defineProperty(this.beam, "episodicGraph", {
|
|
440
|
+
value: buildEpisodicGraph(options.db, this.dbPath),
|
|
441
|
+
});
|
|
442
|
+
closeQuietly(opened);
|
|
443
|
+
}
|
|
444
|
+
this.conn = this.beam.db;
|
|
445
|
+
this.db = this.beam.db;
|
|
446
|
+
// Wipe-and-rebuild stale embeddings when the configured model changed since
|
|
447
|
+
// the vectors were written. Runs inside the runtime scope so
|
|
448
|
+
// `currentEmbeddingModel()` reflects this instance's configured model.
|
|
449
|
+
// Skipped for read-only opens (`reconcile: false`) so an ephemeral stats
|
|
450
|
+
// reader never triggers a destructive migration whose async rebuild it would
|
|
451
|
+
// exit before completing — which would otherwise lose the embeddings.
|
|
452
|
+
if (options.reconcile !== false) {
|
|
453
|
+
withMnemopiRuntimeOptions(this.runtimeOptions, () => reconcileEmbeddingModel(this.beam));
|
|
454
|
+
}
|
|
455
|
+
} finally {
|
|
456
|
+
popLoopPhase();
|
|
451
457
|
}
|
|
452
458
|
}
|
|
453
459
|
|
|
454
460
|
close(): void {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
461
|
+
withLoopPhase("mnemopi.close", () => {
|
|
462
|
+
if (this.#closed) return;
|
|
463
|
+
this.#closed = true;
|
|
464
|
+
if (this.#ownsDb) this.beam.close();
|
|
465
|
+
});
|
|
458
466
|
}
|
|
459
467
|
|
|
460
468
|
async flushExtractions(): Promise<void> {
|
|
@@ -462,12 +470,14 @@ export class Mnemopi {
|
|
|
462
470
|
}
|
|
463
471
|
|
|
464
472
|
remember(memory: string | RememberInput, options: RememberFacadeOptions = {}): string {
|
|
465
|
-
|
|
466
|
-
|
|
473
|
+
return this.#withRuntimeOptions("mnemopi.retain", () => {
|
|
474
|
+
const content = typeof memory === "string" ? memory : memory.content;
|
|
475
|
+
return this.beam.remember(content, toRememberOptions(memory, options));
|
|
476
|
+
});
|
|
467
477
|
}
|
|
468
478
|
|
|
469
479
|
recall(query: string, topK = 5, options: RecallFacadeOptions = {}): Promise<RecallResult[]> {
|
|
470
|
-
return this.#withRuntimeOptions(() => this.beam.recall(query, topK, toRecallOptions(options)));
|
|
480
|
+
return this.#withRuntimeOptions("mnemopi.recall", () => this.beam.recall(query, topK, toRecallOptions(options)));
|
|
471
481
|
}
|
|
472
482
|
|
|
473
483
|
recallEnhanced(
|
|
@@ -475,7 +485,7 @@ export class Mnemopi {
|
|
|
475
485
|
topK = 5,
|
|
476
486
|
options: RecallFacadeOptions & RecallEnhancedOptions = {},
|
|
477
487
|
): Promise<RecallResult[]> {
|
|
478
|
-
return this.#withRuntimeOptions(() =>
|
|
488
|
+
return this.#withRuntimeOptions("mnemopi.recall", () =>
|
|
479
489
|
this.beam.recallEnhanced(query, topK, {
|
|
480
490
|
...toRecallOptions(options),
|
|
481
491
|
useCache: options.useCache,
|
|
@@ -485,7 +495,7 @@ export class Mnemopi {
|
|
|
485
495
|
}
|
|
486
496
|
|
|
487
497
|
getContext(limit = 10): unknown[] {
|
|
488
|
-
return this.#withRuntimeOptions(() => this.beam.getContext(limit));
|
|
498
|
+
return this.#withRuntimeOptions("mnemopi.read", () => this.beam.getContext(limit));
|
|
489
499
|
}
|
|
490
500
|
|
|
491
501
|
getStats(
|
|
@@ -493,62 +503,70 @@ export class Mnemopi {
|
|
|
493
503
|
authorType: string | null = null,
|
|
494
504
|
channelId: string | null = null,
|
|
495
505
|
): MemoryFacadeStats {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
506
|
+
return withLoopPhase("mnemopi.stats", () => {
|
|
507
|
+
const working = withMnemopiRuntimeOptions(this.runtimeOptions, () =>
|
|
508
|
+
this.beam.getWorkingStats(authorId, authorType, channelId),
|
|
509
|
+
);
|
|
510
|
+
const episodic = withMnemopiRuntimeOptions(this.runtimeOptions, () =>
|
|
511
|
+
this.beam.getEpisodicStats(authorId, authorType, channelId),
|
|
512
|
+
);
|
|
513
|
+
const totalMemories = countRows(this.conn, "SELECT COUNT(*) AS total FROM working_memory");
|
|
514
|
+
const totalSessions = countRows(this.conn, "SELECT COUNT(DISTINCT session_id) AS total FROM working_memory");
|
|
515
|
+
using lastStatement = this.conn.prepare(
|
|
516
|
+
"SELECT timestamp FROM working_memory ORDER BY timestamp DESC LIMIT 1",
|
|
517
|
+
);
|
|
518
|
+
const last = lastStatement.get() as {
|
|
519
|
+
timestamp: string | null;
|
|
520
|
+
} | null;
|
|
521
|
+
const tripleTotal = countRows(this.conn, "SELECT COUNT(*) AS total FROM triples");
|
|
522
|
+
let banks = ["default"];
|
|
523
|
+
if (this.dbPath !== undefined && this.dbPath !== ":memory:") {
|
|
524
|
+
const dataDir = dataDirForDbPath(this.dbPath);
|
|
525
|
+
banks = new BankManager(dataDir).listBanks();
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
total_memories: totalMemories,
|
|
529
|
+
total_sessions: totalSessions,
|
|
530
|
+
sources: sourceCounts(this.conn),
|
|
531
|
+
last_memory: last?.timestamp ?? null,
|
|
532
|
+
database: this.dbPath ?? ":memory:",
|
|
533
|
+
mode: "beam",
|
|
534
|
+
banks,
|
|
535
|
+
beam: { working_memory: working, episodic_memory: episodic, triples: { total: tripleTotal } },
|
|
536
|
+
};
|
|
537
|
+
});
|
|
520
538
|
}
|
|
521
539
|
|
|
522
540
|
get(memoryId: string): unknown | null {
|
|
523
|
-
return this.#withRuntimeOptions(() => this.beam.get(memoryId));
|
|
541
|
+
return this.#withRuntimeOptions("mnemopi.read", () => this.beam.get(memoryId));
|
|
524
542
|
}
|
|
525
543
|
|
|
526
544
|
forget(memoryId: string): boolean {
|
|
527
|
-
return this.#withRuntimeOptions(() => this.beam.forgetWorking(memoryId));
|
|
545
|
+
return this.#withRuntimeOptions("mnemopi.edit", () => this.beam.forgetWorking(memoryId));
|
|
528
546
|
}
|
|
529
547
|
|
|
530
548
|
update(memoryId: string, content: string | null = null, importance: number | null = null): boolean {
|
|
531
|
-
return this.#withRuntimeOptions(() => this.beam.updateWorking(memoryId, content, importance));
|
|
549
|
+
return this.#withRuntimeOptions("mnemopi.edit", () => this.beam.updateWorking(memoryId, content, importance));
|
|
532
550
|
}
|
|
533
551
|
|
|
534
552
|
sleep(dryRun = false): SleepResult {
|
|
535
|
-
return this.#withRuntimeOptions(() => this.beam.sleep(dryRun));
|
|
553
|
+
return this.#withRuntimeOptions("mnemopi.consolidate", () => this.beam.sleep(dryRun));
|
|
536
554
|
}
|
|
537
555
|
|
|
538
556
|
sleepAllSessions(dryRun = false): SleepResult {
|
|
539
|
-
return this.#withRuntimeOptions(() => this.beam.sleepAllSessions(dryRun));
|
|
557
|
+
return this.#withRuntimeOptions("mnemopi.consolidate", () => this.beam.sleepAllSessions(dryRun));
|
|
540
558
|
}
|
|
541
559
|
|
|
542
560
|
scratchpadWrite(content: string): string {
|
|
543
|
-
return this.#withRuntimeOptions(() => this.beam.scratchpadWrite(content));
|
|
561
|
+
return this.#withRuntimeOptions("mnemopi.retain", () => this.beam.scratchpadWrite(content));
|
|
544
562
|
}
|
|
545
563
|
|
|
546
564
|
scratchpadRead(): unknown[] {
|
|
547
|
-
return this.#withRuntimeOptions(() => this.beam.scratchpadRead());
|
|
565
|
+
return this.#withRuntimeOptions("mnemopi.read", () => this.beam.scratchpadRead());
|
|
548
566
|
}
|
|
549
567
|
|
|
550
568
|
scratchpadClear(): void {
|
|
551
|
-
this.#withRuntimeOptions(() => this.beam.scratchpadClear());
|
|
569
|
+
this.#withRuntimeOptions("mnemopi.edit", () => this.beam.scratchpadClear());
|
|
552
570
|
}
|
|
553
571
|
|
|
554
572
|
addMemory(memory: string | RememberInput, options: RememberFacadeOptions = {}): string {
|
|
@@ -574,8 +592,8 @@ export class Mnemopi {
|
|
|
574
592
|
consolidate(dryRun = false): SleepResult {
|
|
575
593
|
return this.sleep(dryRun);
|
|
576
594
|
}
|
|
577
|
-
#withRuntimeOptions<T>(fn: () => T): T {
|
|
578
|
-
return withMnemopiRuntimeOptions(this.runtimeOptions, fn);
|
|
595
|
+
#withRuntimeOptions<T>(label: string, fn: () => T): T {
|
|
596
|
+
return withLoopPhase(label, () => withMnemopiRuntimeOptions(this.runtimeOptions, fn));
|
|
579
597
|
}
|
|
580
598
|
}
|
|
581
599
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { logger, withLoopPhase } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type Env, polyphonicRecallEnabled } from "../config";
|
|
4
4
|
import { closeQuietly, type DatabasePath, openDatabase } from "../db";
|
|
5
5
|
import { backfillConsolidatedFacts, ensureVeracityConsolidator } from "./beam/consolidate";
|
|
@@ -759,11 +759,15 @@ export function polyphonicRecall(
|
|
|
759
759
|
topK = 10,
|
|
760
760
|
options: PolyphonicRecallOptions = {},
|
|
761
761
|
): PolyphonicMemoryResult[] {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
762
|
+
// Voice fan-out, graph walk and fusion run synchronously on the caller's event loop;
|
|
763
|
+
// label them so a stall is attributable.
|
|
764
|
+
return withLoopPhase("mnemopi.recall", () => {
|
|
765
|
+
return getPolyphonicEngine(beam).recall(
|
|
766
|
+
query,
|
|
767
|
+
options.queryEmbedding ?? null,
|
|
768
|
+
topK,
|
|
769
|
+
options.contextBudget ?? 4000,
|
|
770
|
+
options,
|
|
771
|
+
);
|
|
772
|
+
});
|
|
769
773
|
}
|