@oh-my-pi/pi-mnemopi 18.8.7 → 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 CHANGED
@@ -2,6 +2,12 @@
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
+
5
11
  ## [18.8.7] - 2026-10-09
6
12
 
7
13
  ### 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.7",
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.7",
46
- "@oh-my-pi/pi-catalog": "18.8.7",
47
- "@oh-my-pi/pi-natives": "18.8.7",
48
- "@oh-my-pi/pi-utils": "18.8.7"
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",
@@ -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 8192;
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
  /**
@@ -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 prompt = buildExtractionPrompt(text);
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: text },
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, text, diag);
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, text, diag);
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(text);
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, text, diag);
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
+ }