@linxiraos/pi-mnemopi 1.1.27 → 1.2.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 +9 -2
- package/README.md +1 -1
- package/dist/types/core/beam/index.d.ts +1 -0
- package/dist/types/core/episodic-graph.d.ts +0 -1
- package/dist/types/core/text-window.d.ts +4 -0
- package/package.json +6 -6
- 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 +18 -33
- package/src/core/episodic-graph.ts +111 -97
- package/src/core/extraction.ts +96 -84
- package/src/core/memory.ts +101 -83
- package/src/core/polyphonic-recall.ts +12 -8
- package/src/core/text-window.ts +14 -0
- package/src/db.ts +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
1
|
## [Unreleased]
|
|
4
2
|
|
|
3
|
+
## [1.2.0] - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- Fixed proactive linking freezing the host for seconds per stored memory on large banks; a memory's graph links are now written in one commit instead of one per link ([#14998](https://github.com/can1357/oh-my-pi/issues/14998)).
|
|
6
|
+
- Fixed long retained transcripts exhausting local memory extraction models by limiting each extraction input to 8192 characters while keeping the opening context and newest turns ([#14956](https://github.com/can1357/oh-my-pi/issues/14956)).
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Fixed new memories gaining graph links (`related_to`, `references`, `ctx`) to memories that were already invalidated, superseded, or expired; proactive linking and consolidation now link only to memories recall can still return, so the graph stops growing toward retired memories ([#14427](https://github.com/can1357/oh-my-pi/pull/14427) by [@tickernelz](https://github.com/tickernelz)).
|
|
11
|
+
|
|
5
12
|
## [1.1.27] - 2026-10-07
|
|
6
13
|
|
|
7
14
|
- 版本线推进至 1.1.27;本版无独立用户可见变化。
|
package/README.md
CHANGED
|
@@ -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 {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Default character budget for a single embedding or fact-extraction input. */
|
|
2
|
+
export declare const DEFAULT_INPUT_CHARS = 8192;
|
|
3
|
+
/** Preserve the opening context and newest turns of an oversized chronological input. */
|
|
4
|
+
export declare function clipToWindow(text: string, max: number): string;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-mnemopi",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "Local SQLite memory engine for
|
|
4
|
+
"version": "1.2.0",
|
|
5
|
+
"description": "Local SQLite memory engine for Zeta agents",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Linxira-OS",
|
|
8
8
|
"contributors": [
|
|
@@ -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
|
-
"@linxiraos/pi-ai": "1.
|
|
43
|
-
"@linxiraos/pi-catalog": "1.
|
|
44
|
-
"@linxiraos/pi-natives": "1.
|
|
45
|
-
"@linxiraos/pi-utils": "1.
|
|
42
|
+
"@linxiraos/pi-ai": "1.2.0",
|
|
43
|
+
"@linxiraos/pi-catalog": "1.2.0",
|
|
44
|
+
"@linxiraos/pi-natives": "1.2.0",
|
|
45
|
+
"@linxiraos/pi-utils": "1.2.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"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 "@linxiraos/pi-utils";
|
|
2
|
+
import { logger, withLoopPhase } from "@linxiraos/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 "@linxiraos/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 "@linxiraos/pi-utils";
|
|
2
|
+
import { logger, withLoopPhase } from "@linxiraos/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 "@linxiraos/pi-utils";
|
|
15
16
|
import { LRUCache } from "@linxiraos/pi-utils/lru";
|
|
16
17
|
import type { EmbeddingModel } from "fastembed";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
mnemopiDebugEnabled,
|
|
22
23
|
resolveEmbeddingProvider,
|
|
23
24
|
} from "./runtime-options";
|
|
25
|
+
import { clipToWindow, DEFAULT_INPUT_CHARS } from "./text-window";
|
|
24
26
|
|
|
25
27
|
export type { EmbeddingOutput } from "./runtime-options";
|
|
26
28
|
export { cosineSimilarity } from "./vector-math";
|
|
@@ -166,29 +168,7 @@ function effectiveMaxInputChars(): number {
|
|
|
166
168
|
if (override !== undefined) return Math.max(0, Math.trunc(override));
|
|
167
169
|
const envValue = Number.parseInt($env.MNEMOPI_EMBEDDING_MAX_INPUT_CHARS ?? "", 10);
|
|
168
170
|
if (Number.isFinite(envValue) && envValue >= 0) return envValue;
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Elision marker injected between the retained head and tail of an oversized input. */
|
|
173
|
-
const EMBEDDING_ELISION_MARKER = "\n\n[...]\n\n";
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Right-clip a single oversized input to {@link max} chars while preserving
|
|
177
|
-
* both ends. Retention transcripts are chronological (oldest → newest), so a
|
|
178
|
-
* naive `slice(0, max)` would drop the most recent — and most semantically
|
|
179
|
-
* loaded — turns once a session passed the cap, leaving every later retained
|
|
180
|
-
* episode with essentially the same prefix vector. Keeping a head/tail split
|
|
181
|
-
* lets the embedding capture the topic setup at the start AND the latest
|
|
182
|
-
* exchanges at the end. Falls back to a tail-only clip when `max` is too
|
|
183
|
-
* small to fit the elision marker plus a useful slice on either side.
|
|
184
|
-
*/
|
|
185
|
-
function clipToWindow(text: string, max: number): string {
|
|
186
|
-
if (text.length <= max) return text;
|
|
187
|
-
if (max <= EMBEDDING_ELISION_MARKER.length + 16) return text.slice(text.length - max);
|
|
188
|
-
const budget = max - EMBEDDING_ELISION_MARKER.length;
|
|
189
|
-
const headLen = budget >>> 1;
|
|
190
|
-
const tailLen = budget - headLen;
|
|
191
|
-
return text.slice(0, headLen) + EMBEDDING_ELISION_MARKER + text.slice(text.length - tailLen);
|
|
171
|
+
return DEFAULT_INPUT_CHARS;
|
|
192
172
|
}
|
|
193
173
|
|
|
194
174
|
/**
|
|
@@ -315,9 +295,11 @@ export function embeddingDimFor(modelName: string): number {
|
|
|
315
295
|
async function collectMatrix(batches: EmbeddingOutput): Promise<EmbeddingMatrix> {
|
|
316
296
|
const rows: Vector[] = [];
|
|
317
297
|
for await (const batch of batches) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
298
|
+
withLoopPhase("mnemopi.embed", () => {
|
|
299
|
+
for (const row of batch) {
|
|
300
|
+
rows.push(new Float32Array(row));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
321
303
|
}
|
|
322
304
|
return rows;
|
|
323
305
|
}
|
|
@@ -409,12 +391,15 @@ async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | nul
|
|
|
409
391
|
if (!response.ok) {
|
|
410
392
|
return null;
|
|
411
393
|
}
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
});
|
|
418
403
|
} catch (error) {
|
|
419
404
|
logger.debug("mnemopi embedding request failed", { status: extractHttpStatusFromError(error) });
|
|
420
405
|
return null;
|
|
@@ -546,7 +531,7 @@ export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix |
|
|
|
546
531
|
return null;
|
|
547
532
|
}
|
|
548
533
|
try {
|
|
549
|
-
const vectors = await collectMatrix(model.embed([...texts]));
|
|
534
|
+
const vectors = await withLoopPhase("mnemopi.embed", () => collectMatrix(model.embed([...texts])));
|
|
550
535
|
if (vectors.length === 1) {
|
|
551
536
|
const vector = vectors[0];
|
|
552
537
|
if (vector !== undefined) {
|