@oh-my-pi/pi-mnemopi 16.1.6 → 16.1.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 +7 -0
- package/dist/types/config.d.ts +23 -4
- package/dist/types/core/beam/types.d.ts +2 -0
- package/dist/types/core/memory.d.ts +1 -0
- package/dist/types/core/runtime-options.d.ts +3 -0
- package/package.json +4 -4
- package/src/config.ts +32 -7
- package/src/core/beam/index.ts +4 -1
- package/src/core/beam/store.ts +6 -1
- package/src/core/beam/types.ts +2 -0
- package/src/core/embeddings.ts +74 -0
- package/src/core/memory.ts +6 -1
- package/src/core/runtime-options.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.8] - 2026-06-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Capped per-input length in `embed()` at `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS` (default 8192 chars, override via the env var or `embeddings.maxInputChars` runtime option; `0` disables) so a long retention transcript can no longer overflow the embedding model's context window. Oversized inputs are clipped with a head/tail split so chronological transcripts keep both the opening setup and the most recent turns instead of losing the latest content under a naive prefix slice. llama.cpp's `/embeddings` server used to reject the request with `request (N tokens) exceeds the available context size`, silently dropping vector recall for that memory ([#3126](https://github.com/can1357/oh-my-pi/issues/3126)).
|
|
10
|
+
- Fixed the proactive-linking write path ignoring host configuration: `proactiveLinkIfEnabled` read `MNEMOPI_PROACTIVE_LINKING` directly, so a host that enabled proactive linking through `configureRecallFeatures()` had no effect unless the environment variable was also set. `proactiveLinking` is now a `RecallFeatureFlags` option resolved through a `proactiveLinkingEnabled()` fallback, matching the existing polyphonic and enhanced recall flags, with the `MNEMOPI_PROACTIVE_LINKING` environment variable still taking precedence whenever it is set. ([#2440](https://github.com/can1357/oh-my-pi/issues/2440))
|
|
11
|
+
|
|
5
12
|
## [16.1.3] - 2026-06-19
|
|
6
13
|
|
|
7
14
|
### Added
|
package/dist/types/config.d.ts
CHANGED
|
@@ -28,6 +28,23 @@ export declare function embeddingApiKey(env?: Env): string;
|
|
|
28
28
|
export declare function embeddingApiUrl(env?: Env): string;
|
|
29
29
|
export declare function embeddingsViaApi(env?: Env): boolean;
|
|
30
30
|
export declare function embeddingsDisabled(env?: Env): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Per-input character cap applied inside `embed()` before any provider sees the text.
|
|
33
|
+
*
|
|
34
|
+
* Long retention transcripts (full multi-turn session windows) routinely outgrow
|
|
35
|
+
* embedding model context windows: BGE/E5 defaults are 512 tokens, bge-m3 is
|
|
36
|
+
* 8192, and OpenAI's text-embedding-3-* is 8192. llama.cpp's `/embeddings`
|
|
37
|
+
* server rejects oversized requests with `request (N tokens) exceeds the
|
|
38
|
+
* available context size`; OpenAI silently right-truncates. Capping at the
|
|
39
|
+
* source gives both backends deterministic behavior and prevents the silent
|
|
40
|
+
* recall degradation we saw in issue #3126.
|
|
41
|
+
*
|
|
42
|
+
* Default `8192` chars is intentionally conservative for 8192-token embedding
|
|
43
|
+
* contexts (bge-m3, OpenAI text-embedding-3) and CJK-heavy transcripts. Raise
|
|
44
|
+
* it for larger local contexts (for example Qwen3-Embedding with 32k ctx).
|
|
45
|
+
* `0` disables the cap.
|
|
46
|
+
*/
|
|
47
|
+
export declare function embeddingMaxInputChars(env?: Env): number;
|
|
31
48
|
export declare function isApiEmbeddingModel(model?: string, env?: Env): boolean;
|
|
32
49
|
export declare function apiEmbeddingsAvailable(env?: Env): boolean;
|
|
33
50
|
export declare function workingMemoryMaxItems(env?: Env): number;
|
|
@@ -57,21 +74,23 @@ export declare function ftsWeight(env?: Env): number;
|
|
|
57
74
|
export declare function importanceWeight(env?: Env): number;
|
|
58
75
|
export declare function normalizedRecallWeights(vec?: number, fts?: number, importance?: number): readonly [number, number, number];
|
|
59
76
|
export declare function autoMigrateEnabled(env?: Env): boolean;
|
|
60
|
-
export declare function proactiveLinkingEnabled(env?: Env): boolean;
|
|
61
77
|
export interface RecallFeatureFlags {
|
|
62
78
|
polyphonicRecall?: boolean;
|
|
63
79
|
enhancedRecall?: boolean;
|
|
80
|
+
proactiveLinking?: boolean;
|
|
64
81
|
}
|
|
65
82
|
/**
|
|
66
83
|
* Sets process-wide defaults for the env-gated recall features. Host configuration
|
|
67
|
-
* (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall`
|
|
68
|
-
* settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` /
|
|
69
|
-
* environment variables still
|
|
84
|
+
* (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` /
|
|
85
|
+
* `mnemopi.proactiveLinking` settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` /
|
|
86
|
+
* `MNEMOPI_ENHANCED_RECALL` / `MNEMOPI_PROACTIVE_LINKING` environment variables still
|
|
87
|
+
* win whenever they are set.
|
|
70
88
|
*/
|
|
71
89
|
export declare function configureRecallFeatures(flags: RecallFeatureFlags): void;
|
|
72
90
|
export declare function polyphonicRecallEnabled(env?: Env): boolean;
|
|
73
91
|
export declare function temporalHalflifeHours(env?: Env): number;
|
|
74
92
|
export declare function enhancedRecallEnabled(env?: Env): boolean;
|
|
93
|
+
export declare function proactiveLinkingEnabled(env?: Env): boolean;
|
|
75
94
|
export declare function llmEnabled(env?: Env): boolean;
|
|
76
95
|
export declare function llmMaxTokens(env?: Env): number;
|
|
77
96
|
export declare function llmThreads(env?: Env): number;
|
|
@@ -39,6 +39,7 @@ export interface BeamConfig {
|
|
|
39
39
|
useCloud: boolean;
|
|
40
40
|
localLlmEnabled: boolean;
|
|
41
41
|
maxEpisodeChars: number;
|
|
42
|
+
proactiveLinking?: boolean;
|
|
42
43
|
}
|
|
43
44
|
export interface BeamMemoryOptions {
|
|
44
45
|
sessionId?: string;
|
|
@@ -47,6 +48,7 @@ export interface BeamMemoryOptions {
|
|
|
47
48
|
authorType?: string | null;
|
|
48
49
|
channelId?: string | null;
|
|
49
50
|
useCloud?: boolean;
|
|
51
|
+
proactiveLinking?: boolean;
|
|
50
52
|
eventEmitter?: (event: BeamEvent) => void;
|
|
51
53
|
pluginManager?: BeamPluginManager | null;
|
|
52
54
|
annotations?: AnnotationStoreLike | null;
|
|
@@ -27,6 +27,7 @@ export interface MnemopiOptions {
|
|
|
27
27
|
readonly llmApiKey?: ApiKey;
|
|
28
28
|
readonly llmModel?: string | Model<Api>;
|
|
29
29
|
readonly llm?: false | MnemopiLlmRuntimeOptions | Model<Api> | MnemopiLlmCompletion;
|
|
30
|
+
readonly proactiveLinking?: boolean;
|
|
30
31
|
/** Escalate best-effort failure logs (embedding pipeline) from debug to warn. */
|
|
31
32
|
readonly debug?: boolean;
|
|
32
33
|
/**
|
|
@@ -24,6 +24,8 @@ export interface MnemopiEmbeddingRuntimeOptions {
|
|
|
24
24
|
apiUrl?: string;
|
|
25
25
|
apiKey?: ApiKey;
|
|
26
26
|
provider?: MnemopiEmbeddingProvider | ((texts: readonly string[]) => EmbeddingOutput | Promise<EmbeddingOutput>);
|
|
27
|
+
/** Override `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`. `0` disables the cap. See `config.embeddingMaxInputChars`. */
|
|
28
|
+
maxInputChars?: number;
|
|
27
29
|
}
|
|
28
30
|
export interface MnemopiLlmRuntimeOptions {
|
|
29
31
|
enabled?: boolean;
|
|
@@ -49,6 +51,7 @@ export interface ResolvedMnemopiEmbeddingRuntimeOptions {
|
|
|
49
51
|
apiUrl?: string;
|
|
50
52
|
apiKey?: ApiKey;
|
|
51
53
|
provider?: MnemopiEmbeddingProvider;
|
|
54
|
+
maxInputChars?: number;
|
|
52
55
|
}
|
|
53
56
|
export interface ResolvedMnemopiLlmRuntimeOptions {
|
|
54
57
|
enabled?: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.8",
|
|
5
5
|
"description": "Local SQLite memory engine for Oh My Pi agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
44
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
42
|
+
"@oh-my-pi/pi-ai": "16.1.8",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "16.1.8",
|
|
44
|
+
"@oh-my-pi/pi-utils": "16.1.8",
|
|
45
45
|
"lru-cache": "11.5.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
package/src/config.ts
CHANGED
|
@@ -99,6 +99,26 @@ export function embeddingsDisabled(env: Env = process.env): boolean {
|
|
|
99
99
|
return envString("MNEMOPI_NO_EMBEDDINGS", "", env) !== "";
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Per-input character cap applied inside `embed()` before any provider sees the text.
|
|
104
|
+
*
|
|
105
|
+
* Long retention transcripts (full multi-turn session windows) routinely outgrow
|
|
106
|
+
* embedding model context windows: BGE/E5 defaults are 512 tokens, bge-m3 is
|
|
107
|
+
* 8192, and OpenAI's text-embedding-3-* is 8192. llama.cpp's `/embeddings`
|
|
108
|
+
* server rejects oversized requests with `request (N tokens) exceeds the
|
|
109
|
+
* available context size`; OpenAI silently right-truncates. Capping at the
|
|
110
|
+
* source gives both backends deterministic behavior and prevents the silent
|
|
111
|
+
* recall degradation we saw in issue #3126.
|
|
112
|
+
*
|
|
113
|
+
* Default `8192` chars is intentionally conservative for 8192-token embedding
|
|
114
|
+
* contexts (bge-m3, OpenAI text-embedding-3) and CJK-heavy transcripts. Raise
|
|
115
|
+
* it for larger local contexts (for example Qwen3-Embedding with 32k ctx).
|
|
116
|
+
* `0` disables the cap.
|
|
117
|
+
*/
|
|
118
|
+
export function embeddingMaxInputChars(env: Env = process.env): number {
|
|
119
|
+
return Math.max(0, envInt("MNEMOPI_EMBEDDING_MAX_INPUT_CHARS", 8192, env));
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
export function isApiEmbeddingModel(model = embeddingModel(), env: Env = process.env): boolean {
|
|
103
123
|
if (model.startsWith("openai/") || model.includes("text-embedding") || model.startsWith("text-embedding"))
|
|
104
124
|
return true;
|
|
@@ -248,27 +268,27 @@ export function autoMigrateEnabled(env: Env = process.env): boolean {
|
|
|
248
268
|
return envString("MNEMOPI_AUTO_MIGRATE", "1", env) !== "0";
|
|
249
269
|
}
|
|
250
270
|
|
|
251
|
-
export function proactiveLinkingEnabled(env: Env = process.env): boolean {
|
|
252
|
-
return envString("MNEMOPI_PROACTIVE_LINKING", "0", env) === "1";
|
|
253
|
-
}
|
|
254
|
-
|
|
255
271
|
export interface RecallFeatureFlags {
|
|
256
272
|
polyphonicRecall?: boolean;
|
|
257
273
|
enhancedRecall?: boolean;
|
|
274
|
+
proactiveLinking?: boolean;
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
let polyphonicRecallDefault = false;
|
|
261
278
|
let enhancedRecallDefault = false;
|
|
279
|
+
let proactiveLinkingDefault = false;
|
|
262
280
|
|
|
263
281
|
/**
|
|
264
282
|
* Sets process-wide defaults for the env-gated recall features. Host configuration
|
|
265
|
-
* (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall`
|
|
266
|
-
* settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` /
|
|
267
|
-
* environment variables still
|
|
283
|
+
* (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` /
|
|
284
|
+
* `mnemopi.proactiveLinking` settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` /
|
|
285
|
+
* `MNEMOPI_ENHANCED_RECALL` / `MNEMOPI_PROACTIVE_LINKING` environment variables still
|
|
286
|
+
* win whenever they are set.
|
|
268
287
|
*/
|
|
269
288
|
export function configureRecallFeatures(flags: RecallFeatureFlags): void {
|
|
270
289
|
if (flags.polyphonicRecall !== undefined) polyphonicRecallDefault = flags.polyphonicRecall;
|
|
271
290
|
if (flags.enhancedRecall !== undefined) enhancedRecallDefault = flags.enhancedRecall;
|
|
291
|
+
if (flags.proactiveLinking !== undefined) proactiveLinkingDefault = flags.proactiveLinking;
|
|
272
292
|
}
|
|
273
293
|
|
|
274
294
|
export function polyphonicRecallEnabled(env: Env = process.env): boolean {
|
|
@@ -285,6 +305,11 @@ export function enhancedRecallEnabled(env: Env = process.env): boolean {
|
|
|
285
305
|
return value === undefined ? enhancedRecallDefault : value === "1";
|
|
286
306
|
}
|
|
287
307
|
|
|
308
|
+
export function proactiveLinkingEnabled(env: Env = process.env): boolean {
|
|
309
|
+
const value = envOptionalString("MNEMOPI_PROACTIVE_LINKING", env);
|
|
310
|
+
return value === undefined ? proactiveLinkingDefault : value === "1";
|
|
311
|
+
}
|
|
312
|
+
|
|
288
313
|
export function llmEnabled(env: Env = process.env): boolean {
|
|
289
314
|
return envBool("MNEMOPI_LLM_ENABLED", true, env);
|
|
290
315
|
}
|
package/src/core/beam/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { ftsWeight, importanceWeight, maxEpisodeChars, vectorWeight } from "../../config";
|
|
3
|
+
import { ftsWeight, importanceWeight, maxEpisodeChars, proactiveLinkingEnabled, vectorWeight } from "../../config";
|
|
4
4
|
import { closeQuietly, openDatabase } from "../../db";
|
|
5
5
|
import { AnnotationStore } from "../annotations";
|
|
6
6
|
import { EpisodicGraph } from "../episodic-graph";
|
|
@@ -69,11 +69,13 @@ const DEFAULT_CONFIG: BeamConfig = {
|
|
|
69
69
|
useCloud: false,
|
|
70
70
|
localLlmEnabled: false,
|
|
71
71
|
maxEpisodeChars: 100_000,
|
|
72
|
+
proactiveLinking: false,
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
function normalizeConfig(options: BeamMemoryOptions): BeamConfig {
|
|
75
76
|
const configured = options.config ?? {};
|
|
76
77
|
const useCloud = options.useCloud ?? configured.useCloud ?? DEFAULT_CONFIG.useCloud;
|
|
78
|
+
const proactiveLinking = options.proactiveLinking ?? configured.proactiveLinking ?? proactiveLinkingEnabled({});
|
|
77
79
|
return {
|
|
78
80
|
workingMemoryLimit: configured.workingMemoryLimit ?? DEFAULT_CONFIG.workingMemoryLimit,
|
|
79
81
|
workingMemoryTtlHours: configured.workingMemoryTtlHours ?? DEFAULT_CONFIG.workingMemoryTtlHours,
|
|
@@ -84,6 +86,7 @@ function normalizeConfig(options: BeamMemoryOptions): BeamConfig {
|
|
|
84
86
|
useCloud,
|
|
85
87
|
localLlmEnabled: configured.localLlmEnabled ?? DEFAULT_CONFIG.localLlmEnabled,
|
|
86
88
|
maxEpisodeChars: configured.maxEpisodeChars ?? maxEpisodeChars(),
|
|
89
|
+
proactiveLinking,
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
function autoMigrateAnnotations(db: Database, dbPath: string | undefined): void {
|
package/src/core/beam/store.ts
CHANGED
|
@@ -187,13 +187,18 @@ function addTemporalAnnotations(beam: BeamMemoryState, memoryId: string, timesta
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function proactiveLinkingAllowed(beam: BeamMemoryState): boolean {
|
|
191
|
+
const override = process.env.MNEMOPI_PROACTIVE_LINKING;
|
|
192
|
+
return override === undefined ? beam.config.proactiveLinking === true : override === "1";
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
function proactiveLinkIfEnabled(
|
|
191
196
|
beam: BeamMemoryState,
|
|
192
197
|
memoryId: string,
|
|
193
198
|
content: string,
|
|
194
199
|
extractEntities: boolean,
|
|
195
200
|
): void {
|
|
196
|
-
if (
|
|
201
|
+
if (!proactiveLinkingAllowed(beam)) return;
|
|
197
202
|
try {
|
|
198
203
|
const graph =
|
|
199
204
|
beam.episodicGraph instanceof EpisodicGraph
|
package/src/core/beam/types.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface BeamConfig {
|
|
|
54
54
|
useCloud: boolean;
|
|
55
55
|
localLlmEnabled: boolean;
|
|
56
56
|
maxEpisodeChars: number;
|
|
57
|
+
proactiveLinking?: boolean;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface BeamMemoryOptions {
|
|
@@ -63,6 +64,7 @@ export interface BeamMemoryOptions {
|
|
|
63
64
|
authorType?: string | null;
|
|
64
65
|
channelId?: string | null;
|
|
65
66
|
useCloud?: boolean;
|
|
67
|
+
proactiveLinking?: boolean;
|
|
66
68
|
eventEmitter?: (event: BeamEvent) => void;
|
|
67
69
|
pluginManager?: BeamPluginManager | null;
|
|
68
70
|
annotations?: AnnotationStoreLike | null;
|
package/src/core/embeddings.ts
CHANGED
|
@@ -120,6 +120,79 @@ export function embeddingsDisabled(): boolean {
|
|
|
120
120
|
return $flag("MNEMOPI_NO_EMBEDDINGS");
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Resolved per-input character cap for {@link embed}.
|
|
125
|
+
*
|
|
126
|
+
* Reads (in order): the active runtime scope's `embeddings.maxInputChars`, then
|
|
127
|
+
* `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`, then the bundled `8192` default. `0`
|
|
128
|
+
* disables the cap entirely.
|
|
129
|
+
*/
|
|
130
|
+
function effectiveMaxInputChars(): number {
|
|
131
|
+
const override = activeEmbeddingOptions()?.maxInputChars;
|
|
132
|
+
if (override !== undefined) return Math.max(0, Math.trunc(override));
|
|
133
|
+
const envValue = Number.parseInt($env.MNEMOPI_EMBEDDING_MAX_INPUT_CHARS ?? "", 10);
|
|
134
|
+
if (Number.isFinite(envValue) && envValue >= 0) return envValue;
|
|
135
|
+
return 8192;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Elision marker injected between the retained head and tail of an oversized input. */
|
|
139
|
+
const EMBEDDING_ELISION_MARKER = "\n\n[...]\n\n";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Right-clip a single oversized input to {@link max} chars while preserving
|
|
143
|
+
* both ends. Retention transcripts are chronological (oldest → newest), so a
|
|
144
|
+
* naive `slice(0, max)` would drop the most recent — and most semantically
|
|
145
|
+
* loaded — turns once a session passed the cap, leaving every later retained
|
|
146
|
+
* episode with essentially the same prefix vector. Keeping a head/tail split
|
|
147
|
+
* lets the embedding capture the topic setup at the start AND the latest
|
|
148
|
+
* exchanges at the end. Falls back to a tail-only clip when `max` is too
|
|
149
|
+
* small to fit the elision marker plus a useful slice on either side.
|
|
150
|
+
*/
|
|
151
|
+
function clipToWindow(text: string, max: number): string {
|
|
152
|
+
if (text.length <= max) return text;
|
|
153
|
+
if (max <= EMBEDDING_ELISION_MARKER.length + 16) return text.slice(text.length - max);
|
|
154
|
+
const budget = max - EMBEDDING_ELISION_MARKER.length;
|
|
155
|
+
const headLen = budget >>> 1;
|
|
156
|
+
const tailLen = budget - headLen;
|
|
157
|
+
return text.slice(0, headLen) + EMBEDDING_ELISION_MARKER + text.slice(text.length - tailLen);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Clip every input to {@link effectiveMaxInputChars} so a runaway retention
|
|
162
|
+
* transcript can't blow past the embedding model's context window. Uses a
|
|
163
|
+
* head/tail split via {@link clipToWindow} so the embedding still sees the
|
|
164
|
+
* tail of the conversation (where the latest topic shifts live) and not just
|
|
165
|
+
* the stale prefix. Returns the original array when no input needs trimming
|
|
166
|
+
* (the common case); the new array is allocated only when at least one input
|
|
167
|
+
* is oversized so we don't churn arrays for the typical short-query path
|
|
168
|
+
* through `embedQuery`. Emits one debug-or-warn log per call summarizing how
|
|
169
|
+
* many inputs were trimmed and by how much — silent truncation was the
|
|
170
|
+
* original bug (#3126).
|
|
171
|
+
*/
|
|
172
|
+
function capInputs(texts: readonly string[]): readonly string[] {
|
|
173
|
+
const max = effectiveMaxInputChars();
|
|
174
|
+
if (max === 0) return texts;
|
|
175
|
+
let trimmed: string[] | null = null;
|
|
176
|
+
let trimmedCount = 0;
|
|
177
|
+
let maxOriginalLen = 0;
|
|
178
|
+
for (let i = 0; i < texts.length; i++) {
|
|
179
|
+
const text = texts[i] ?? "";
|
|
180
|
+
if (text.length <= max) continue;
|
|
181
|
+
if (trimmed === null) trimmed = texts.slice() as string[];
|
|
182
|
+
trimmed[i] = clipToWindow(text, max);
|
|
183
|
+
trimmedCount++;
|
|
184
|
+
if (text.length > maxOriginalLen) maxOriginalLen = text.length;
|
|
185
|
+
}
|
|
186
|
+
if (trimmed === null) return texts;
|
|
187
|
+
logger[mnemopiDebugEnabled() ? "warn" : "debug"]("mnemopi: embedding input truncated", {
|
|
188
|
+
inputCount: texts.length,
|
|
189
|
+
trimmedCount,
|
|
190
|
+
maxOriginalLen,
|
|
191
|
+
maxInputChars: max,
|
|
192
|
+
});
|
|
193
|
+
return trimmed;
|
|
194
|
+
}
|
|
195
|
+
|
|
123
196
|
function embeddingApiKey(): ApiKey {
|
|
124
197
|
const active = activeEmbeddingOptions();
|
|
125
198
|
if (active?.apiKey !== undefined) {
|
|
@@ -408,6 +481,7 @@ export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix |
|
|
|
408
481
|
if (texts.length === 0 || embeddingsDisabled()) {
|
|
409
482
|
return null;
|
|
410
483
|
}
|
|
484
|
+
texts = capInputs(texts);
|
|
411
485
|
const activeProvider = resolveEmbeddingProvider(activeEmbeddingOptions()?.provider);
|
|
412
486
|
if (activeProvider !== undefined) {
|
|
413
487
|
try {
|
package/src/core/memory.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface MnemopiOptions {
|
|
|
43
43
|
readonly llmApiKey?: ApiKey;
|
|
44
44
|
readonly llmModel?: string | Model<Api>;
|
|
45
45
|
readonly llm?: false | MnemopiLlmRuntimeOptions | Model<Api> | MnemopiLlmCompletion;
|
|
46
|
+
readonly proactiveLinking?: boolean;
|
|
46
47
|
/** Escalate best-effort failure logs (embedding pipeline) from debug to warn. */
|
|
47
48
|
readonly debug?: boolean;
|
|
48
49
|
/**
|
|
@@ -161,19 +162,22 @@ function resolveRuntimeOptions(options: MnemopiOptions): ResolvedMnemopiRuntimeO
|
|
|
161
162
|
const embeddingApiUrl = options.embeddingApiUrl ?? nestedEmbeddings?.apiUrl;
|
|
162
163
|
const embeddingApiKey = options.embeddingApiKey ?? nestedEmbeddings?.apiKey;
|
|
163
164
|
const embeddingProvider = resolveEmbeddingProvider(nestedEmbeddings?.provider);
|
|
165
|
+
const embeddingMaxInputChars = nestedEmbeddings?.maxInputChars;
|
|
164
166
|
|
|
165
167
|
const embeddings =
|
|
166
168
|
embeddingDisabled !== undefined ||
|
|
167
169
|
embeddingModel !== undefined ||
|
|
168
170
|
embeddingApiUrl !== undefined ||
|
|
169
171
|
embeddingApiKey !== undefined ||
|
|
170
|
-
embeddingProvider !== undefined
|
|
172
|
+
embeddingProvider !== undefined ||
|
|
173
|
+
embeddingMaxInputChars !== undefined
|
|
171
174
|
? {
|
|
172
175
|
disabled: embeddingDisabled,
|
|
173
176
|
model: embeddingModel,
|
|
174
177
|
apiUrl: embeddingApiUrl,
|
|
175
178
|
apiKey: embeddingApiKey,
|
|
176
179
|
provider: embeddingProvider,
|
|
180
|
+
maxInputChars: embeddingMaxInputChars,
|
|
177
181
|
}
|
|
178
182
|
: undefined;
|
|
179
183
|
|
|
@@ -380,6 +384,7 @@ export class Mnemopi {
|
|
|
380
384
|
authorId: this.authorId,
|
|
381
385
|
authorType: this.authorType,
|
|
382
386
|
channelId: this.channelId,
|
|
387
|
+
proactiveLinking: options.proactiveLinking,
|
|
383
388
|
});
|
|
384
389
|
this.#ownsDb = options.db === undefined;
|
|
385
390
|
if (options.db !== undefined) {
|
|
@@ -33,6 +33,8 @@ export interface MnemopiEmbeddingRuntimeOptions {
|
|
|
33
33
|
apiUrl?: string;
|
|
34
34
|
apiKey?: ApiKey;
|
|
35
35
|
provider?: MnemopiEmbeddingProvider | ((texts: readonly string[]) => EmbeddingOutput | Promise<EmbeddingOutput>);
|
|
36
|
+
/** Override `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`. `0` disables the cap. See `config.embeddingMaxInputChars`. */
|
|
37
|
+
maxInputChars?: number;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface MnemopiLlmRuntimeOptions {
|
|
@@ -61,6 +63,7 @@ export interface ResolvedMnemopiEmbeddingRuntimeOptions {
|
|
|
61
63
|
apiUrl?: string;
|
|
62
64
|
apiKey?: ApiKey;
|
|
63
65
|
provider?: MnemopiEmbeddingProvider;
|
|
66
|
+
maxInputChars?: number;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export interface ResolvedMnemopiLlmRuntimeOptions {
|