@oh-my-pi/pi-mnemopi 18.8.6 → 18.8.8
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 +12 -0
- package/dist/types/core/text-window.d.ts +4 -0
- package/package.json +5 -5
- package/src/core/embeddings.ts +2 -23
- package/src/core/episodic-graph.ts +83 -74
- package/src/core/extraction.ts +8 -6
- package/src/core/text-window.ts +14 -0
- package/src/db.ts +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.8] - 2026-10-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 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)).
|
|
10
|
+
|
|
11
|
+
## [18.8.7] - 2026-10-09
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- 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)).
|
|
16
|
+
|
|
5
17
|
## [18.8.5] - 2026-10-08
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "18.8.
|
|
4
|
+
"version": "18.8.8",
|
|
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.8.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.8.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.8.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.8.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.8.8",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.8.8",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.8.8",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.8.8"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fastembed": "3.0.0",
|
package/src/core/embeddings.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
mnemopiDebugEnabled,
|
|
22
22
|
resolveEmbeddingProvider,
|
|
23
23
|
} from "./runtime-options";
|
|
24
|
+
import { clipToWindow, DEFAULT_INPUT_CHARS } from "./text-window";
|
|
24
25
|
|
|
25
26
|
export type { EmbeddingOutput } from "./runtime-options";
|
|
26
27
|
export { cosineSimilarity } from "./vector-math";
|
|
@@ -166,29 +167,7 @@ function effectiveMaxInputChars(): number {
|
|
|
166
167
|
if (override !== undefined) return Math.max(0, Math.trunc(override));
|
|
167
168
|
const envValue = Number.parseInt($env.MNEMOPI_EMBEDDING_MAX_INPUT_CHARS ?? "", 10);
|
|
168
169
|
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);
|
|
170
|
+
return DEFAULT_INPUT_CHARS;
|
|
192
171
|
}
|
|
193
172
|
|
|
194
173
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import { closeQuietly, type DatabasePath, openDatabase } from "../db";
|
|
2
|
+
import { closeQuietly, type DatabasePath, openDatabase, transaction } from "../db";
|
|
3
3
|
|
|
4
4
|
export interface Gist {
|
|
5
5
|
readonly id: string;
|
|
@@ -506,83 +506,92 @@ export class EpisodicGraph {
|
|
|
506
506
|
const extractEntities = options.extractEntities ?? true;
|
|
507
507
|
const gist = this.extractGist(content, memoryId);
|
|
508
508
|
const facts = extractEntities ? this.extractFacts(content, memoryId) : [];
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
this.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
509
|
+
// One transaction for every gist/fact/edge write: autocommitted, each
|
|
510
|
+
// `addEdge` paid its own WAL commit (an fsync), so linking a memory
|
|
511
|
+
// against a large bank blocked the caller's event loop for seconds (#14998).
|
|
512
|
+
return transaction(this.db, () => {
|
|
513
|
+
const edges: GraphEdge[] = [];
|
|
514
|
+
const timestamp = nowIso();
|
|
515
|
+
|
|
516
|
+
const previousMemoryIds = linkExisting ? this.knownMemoryIds(memoryId) : [];
|
|
517
|
+
this.storeGist(gist, memoryId);
|
|
518
|
+
const gistEdge = { source: memoryId, target: gist.id, edgeType: "ctx", weight: 1, timestamp };
|
|
519
|
+
this.addEdge(gistEdge);
|
|
520
|
+
edges.push(gistEdge);
|
|
521
|
+
|
|
522
|
+
for (const fact of facts) {
|
|
523
|
+
this.storeFact(fact, memoryId, sessionId);
|
|
524
|
+
const edge = {
|
|
525
|
+
source: gist.id,
|
|
526
|
+
target: fact.id,
|
|
527
|
+
edgeType: "rel",
|
|
528
|
+
weight: fact.confidence,
|
|
529
|
+
timestamp,
|
|
530
|
+
};
|
|
531
|
+
this.addEdge(edge);
|
|
532
|
+
edges.push(edge);
|
|
533
|
+
}
|
|
530
534
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
535
|
+
if (linkExisting) {
|
|
536
|
+
const sourceTokens = contentTokenSet(content);
|
|
537
|
+
for (const otherId of previousMemoryIds) {
|
|
538
|
+
const otherContent = this.memoryContent(otherId, timestamp);
|
|
539
|
+
const lexicalScore = Math.round(jaccard(sourceTokens, contentTokenSet(otherContent)) * 1000) / 1000;
|
|
540
|
+
let wroteCtxEdge = false;
|
|
541
|
+
if (lexicalScore >= minLinkScore) {
|
|
542
|
+
const edge = {
|
|
543
|
+
source: memoryId,
|
|
544
|
+
target: otherId,
|
|
545
|
+
edgeType: "related_to",
|
|
546
|
+
weight: lexicalScore,
|
|
547
|
+
timestamp,
|
|
548
|
+
};
|
|
549
|
+
this.addEdge(edge);
|
|
550
|
+
edges.push(edge);
|
|
551
|
+
const ctxEdge = {
|
|
552
|
+
source: memoryId,
|
|
553
|
+
target: otherId,
|
|
554
|
+
edgeType: "ctx",
|
|
555
|
+
weight: lexicalScore,
|
|
556
|
+
timestamp,
|
|
557
|
+
};
|
|
558
|
+
this.addEdge(ctxEdge);
|
|
559
|
+
edges.push(ctxEdge);
|
|
560
|
+
wroteCtxEdge = true;
|
|
561
|
+
}
|
|
562
|
+
const entityScore = this.entityOverlapScore(memoryId, otherId);
|
|
563
|
+
if (entityScore > 0) {
|
|
564
|
+
const edge = {
|
|
565
|
+
source: memoryId,
|
|
566
|
+
target: otherId,
|
|
567
|
+
edgeType: "references",
|
|
568
|
+
weight: entityScore,
|
|
569
|
+
timestamp,
|
|
570
|
+
};
|
|
571
|
+
this.addEdge(edge);
|
|
572
|
+
edges.push(edge);
|
|
573
|
+
}
|
|
574
|
+
const contextualScore = Math.max(
|
|
575
|
+
lexicalScore,
|
|
576
|
+
entityScore,
|
|
577
|
+
this.temporalContextScore(memoryId, otherId),
|
|
578
|
+
);
|
|
579
|
+
if (!wroteCtxEdge && contextualScore >= minLinkScore) {
|
|
580
|
+
const ctxEdge = {
|
|
581
|
+
source: memoryId,
|
|
582
|
+
target: otherId,
|
|
583
|
+
edgeType: "ctx",
|
|
584
|
+
weight: contextualScore,
|
|
585
|
+
timestamp,
|
|
586
|
+
};
|
|
587
|
+
this.addEdge(ctxEdge);
|
|
588
|
+
edges.push(ctxEdge);
|
|
589
|
+
}
|
|
581
590
|
}
|
|
582
591
|
}
|
|
583
|
-
}
|
|
584
592
|
|
|
585
|
-
|
|
593
|
+
return { memoryId, gist, facts, edges };
|
|
594
|
+
});
|
|
586
595
|
}
|
|
587
596
|
getStats(): GraphStats {
|
|
588
597
|
const gists = this.count("gists");
|
package/src/core/extraction.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type RemoteLlmOptions,
|
|
11
11
|
} from "./local-llm";
|
|
12
12
|
import { getMnemopiRuntimeOptions } from "./runtime-options";
|
|
13
|
+
import { clipToWindow, DEFAULT_INPUT_CHARS } from "./text-window";
|
|
13
14
|
|
|
14
15
|
const TRUE_VALUES: Record<string, true> = { "1": true, true: true, yes: true, on: true };
|
|
15
16
|
|
|
@@ -378,7 +379,8 @@ export async function extractFactCategories(
|
|
|
378
379
|
if (typeof text !== "string" || text.trim() === "") {
|
|
379
380
|
return emptyFactCategories();
|
|
380
381
|
}
|
|
381
|
-
const
|
|
382
|
+
const input = clipToWindow(text, DEFAULT_INPUT_CHARS);
|
|
383
|
+
const prompt = buildExtractionPrompt(input);
|
|
382
384
|
|
|
383
385
|
// Configured completion (host-injected runtime LLM, e.g. the coding-agent's smol
|
|
384
386
|
// or a local on-device model). Mirrors consolidation's precedence: when a
|
|
@@ -389,7 +391,7 @@ export async function extractFactCategories(
|
|
|
389
391
|
try {
|
|
390
392
|
const raw = await callConfiguredCompletion(prompt, 0, {
|
|
391
393
|
maxTokens: llmMaxTokens(),
|
|
392
|
-
task: { kind: "memory-extraction", input
|
|
394
|
+
task: { kind: "memory-extraction", input },
|
|
393
395
|
});
|
|
394
396
|
if (typeof raw === "string" && raw.trim() !== "") {
|
|
395
397
|
const extracted = parseExtractedFactCategories(raw);
|
|
@@ -407,7 +409,7 @@ export async function extractFactCategories(
|
|
|
407
409
|
console.warn(`extractFacts: configured completion raised: ${safeForLog(exc)}`);
|
|
408
410
|
return emptyFactCategories();
|
|
409
411
|
}
|
|
410
|
-
return localFallback(prompt,
|
|
412
|
+
return localFallback(prompt, input, diag);
|
|
411
413
|
}
|
|
412
414
|
|
|
413
415
|
try {
|
|
@@ -424,7 +426,7 @@ export async function extractFactCategories(
|
|
|
424
426
|
}
|
|
425
427
|
}
|
|
426
428
|
diag.recordNoOutput("host");
|
|
427
|
-
return localFallback(prompt,
|
|
429
|
+
return localFallback(prompt, input, diag);
|
|
428
430
|
}
|
|
429
431
|
} catch (exc) {
|
|
430
432
|
diag.recordAttempt("host");
|
|
@@ -436,7 +438,7 @@ export async function extractFactCategories(
|
|
|
436
438
|
|
|
437
439
|
if (!llmAvailable()) {
|
|
438
440
|
diag.recordAttempt("local");
|
|
439
|
-
const heuristic = heuristicExtractFacts(
|
|
441
|
+
const heuristic = heuristicExtractFacts(input);
|
|
440
442
|
if (heuristic.length > 0) {
|
|
441
443
|
diag.recordSuccess("local", heuristic.length);
|
|
442
444
|
diag.recordCall({ succeeded: true });
|
|
@@ -465,7 +467,7 @@ export async function extractFactCategories(
|
|
|
465
467
|
console.warn(`extractFacts: remote LLM raised: ${safeForLog(exc)}`);
|
|
466
468
|
}
|
|
467
469
|
|
|
468
|
-
return localFallback(prompt,
|
|
470
|
+
return localFallback(prompt, input, diag);
|
|
469
471
|
}
|
|
470
472
|
|
|
471
473
|
/** Extract legacy flat fact strings from text. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Default character budget for a single embedding or fact-extraction input. */
|
|
2
|
+
export const DEFAULT_INPUT_CHARS = 8192;
|
|
3
|
+
|
|
4
|
+
const ELISION_MARKER = "\n\n[...]\n\n";
|
|
5
|
+
|
|
6
|
+
/** Preserve the opening context and newest turns of an oversized chronological input. */
|
|
7
|
+
export function clipToWindow(text: string, max: number): string {
|
|
8
|
+
if (text.length <= max) return text;
|
|
9
|
+
if (max <= ELISION_MARKER.length + 16) return text.slice(text.length - max);
|
|
10
|
+
const budget = max - ELISION_MARKER.length;
|
|
11
|
+
const headLen = budget >>> 1;
|
|
12
|
+
const tailLen = budget - headLen;
|
|
13
|
+
return text.slice(0, headLen) + ELISION_MARKER + text.slice(text.length - tailLen);
|
|
14
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -119,10 +119,12 @@ export function transaction<T>(db: Database, fn: () => T): T {
|
|
|
119
119
|
state.depth--;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
// Caller-owned Bun/SQLite transactions already batch these writes; do not
|
|
123
|
+
// commit or roll them back from this helper.
|
|
124
|
+
if (db.inTransaction) return fn();
|
|
125
|
+
db.exec("BEGIN DEFERRED");
|
|
123
126
|
state = { depth: 1 };
|
|
124
127
|
txDb[TX_STATE] = state;
|
|
125
|
-
db.exec("BEGIN DEFERRED");
|
|
126
128
|
try {
|
|
127
129
|
const result = fn();
|
|
128
130
|
state.depth = 0;
|
|
@@ -154,10 +156,10 @@ export async function transactionAsync<T>(db: Database, fn: () => Promise<T>): P
|
|
|
154
156
|
state.depth--;
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
|
-
|
|
159
|
+
if (db.inTransaction) return await fn();
|
|
160
|
+
db.exec("BEGIN DEFERRED");
|
|
158
161
|
state = { depth: 1 };
|
|
159
162
|
txDb[TX_STATE] = state;
|
|
160
|
-
db.exec("BEGIN DEFERRED");
|
|
161
163
|
try {
|
|
162
164
|
const result = await fn();
|
|
163
165
|
state.depth = 0;
|