fix(mnemopi): scope proactive linking per memory

This commit is contained in:
Wolfgang Schoenberger
2026-06-18 05:15:56 -07:00
parent 8eb8ec4b74
commit 70f9e604f0
6 changed files with 150 additions and 7 deletions
+4 -4
View File
@@ -421,13 +421,12 @@ export class MnemopiSessionState {
// `per-project-tagged` is implemented by opening both the project bank and the
// shared bank, then merging recall results while keeping writes project-local.
function createScopedResources(config: MnemopiBackendConfig): MnemopiScopedResources {
// Env vars (MNEMOPI_POLYPHONIC_RECALL / MNEMOPI_ENHANCED_RECALL /
// MNEMOPI_PROACTIVE_LINKING) still override these config-driven defaults inside
// the core gates.
// Env vars (MNEMOPI_POLYPHONIC_RECALL / MNEMOPI_ENHANCED_RECALL) still override
// these config-driven defaults inside the core gates. Proactive linking is
// per-memory instance below so concurrent sessions cannot clobber each other.
requireMnemopi().configureRecallFeatures({
polyphonicRecall: config.polyphonicRecall,
enhancedRecall: config.enhancedRecall,
proactiveLinking: config.proactiveLinking,
});
const banks = resolveScopedBanks(config);
const memories = new Map<string, MnemopiScopedMemory>();
@@ -550,6 +549,7 @@ function createMemory(config: MnemopiBackendConfig, bank: string): Mnemopi {
authorType: "agent",
channelId: bank,
...providerOptions,
proactiveLinking: config.proactiveLinking,
} as ConstructorParameters<typeof Mnemopi>[0]);
}
+13 -1
View File
@@ -1,6 +1,6 @@
import type { Database } from "bun:sqlite";
import { existsSync } from "node:fs";
import { ftsWeight, importanceWeight, maxEpisodeChars, vectorWeight } from "../../config";
import { ftsWeight, importanceWeight, maxEpisodeChars, proactiveLinkingEnabled, vectorWeight } from "../../config";
import { closeQuietly, openDatabase } from "../../db";
import { AnnotationStore } from "../annotations";
import { EpisodicGraph } from "../episodic-graph";
@@ -69,11 +69,22 @@ const DEFAULT_CONFIG: BeamConfig = {
useCloud: false,
localLlmEnabled: false,
maxEpisodeChars: 100_000,
proactiveLinking: false,
};
function envProactiveLinkingOverride(): boolean | undefined {
const value = process.env.MNEMOPI_PROACTIVE_LINKING;
return value === undefined ? undefined : value === "1";
}
function normalizeConfig(options: BeamMemoryOptions): BeamConfig {
const configured = options.config ?? {};
const useCloud = options.useCloud ?? configured.useCloud ?? DEFAULT_CONFIG.useCloud;
const proactiveLinking =
envProactiveLinkingOverride() ??
options.proactiveLinking ??
configured.proactiveLinking ??
proactiveLinkingEnabled();
return {
workingMemoryLimit: configured.workingMemoryLimit ?? DEFAULT_CONFIG.workingMemoryLimit,
workingMemoryTtlHours: configured.workingMemoryTtlHours ?? DEFAULT_CONFIG.workingMemoryTtlHours,
@@ -84,6 +95,7 @@ function normalizeConfig(options: BeamMemoryOptions): BeamConfig {
useCloud,
localLlmEnabled: configured.localLlmEnabled ?? DEFAULT_CONFIG.localLlmEnabled,
maxEpisodeChars: configured.maxEpisodeChars ?? maxEpisodeChars(),
proactiveLinking,
};
}
function autoMigrateAnnotations(db: Database, dbPath: string | undefined): void {
+6 -2
View File
@@ -1,6 +1,5 @@
import type { Database, SQLQueryBindings } from "bun:sqlite";
import { logger } from "@oh-my-pi/pi-utils";
import { proactiveLinkingEnabled } from "../../config";
import { transaction } from "../../db";
import { toUtcIso } from "../../util/datetime";
import { generateId } from "../../util/ids";
@@ -188,13 +187,18 @@ function addTemporalAnnotations(beam: BeamMemoryState, memoryId: string, timesta
}
}
function proactiveLinkingAllowed(beam: BeamMemoryState): boolean {
const override = process.env.MNEMOPI_PROACTIVE_LINKING;
return override === undefined ? beam.config.proactiveLinking === true : override === "1";
}
function proactiveLinkIfEnabled(
beam: BeamMemoryState,
memoryId: string,
content: string,
extractEntities: boolean,
): void {
if (!proactiveLinkingEnabled()) return;
if (!proactiveLinkingAllowed(beam)) return;
try {
const graph =
beam.episodicGraph instanceof EpisodicGraph
+2
View File
@@ -54,6 +54,7 @@ export interface BeamConfig {
useCloud: boolean;
localLlmEnabled: boolean;
maxEpisodeChars: number;
proactiveLinking?: boolean;
}
export interface BeamMemoryOptions {
@@ -63,6 +64,7 @@ export interface BeamMemoryOptions {
authorType?: string | null;
channelId?: string | null;
useCloud?: boolean;
proactiveLinking?: boolean;
eventEmitter?: (event: BeamEvent) => void;
pluginManager?: BeamPluginManager | null;
annotations?: AnnotationStoreLike | null;
+2
View File
@@ -43,6 +43,7 @@ export interface MnemopiOptions {
readonly llmApiKey?: ApiKey;
readonly llmModel?: string | Model<Api>;
readonly llm?: false | MnemopiLlmRuntimeOptions | Model<Api> | MnemopiLlmCompletion;
readonly proactiveLinking?: boolean;
/** Escalate best-effort failure logs (embedding pipeline) from debug to warn. */
readonly debug?: boolean;
/**
@@ -380,6 +381,7 @@ export class Mnemopi {
authorId: this.authorId,
authorType: this.authorType,
channelId: this.channelId,
proactiveLinking: options.proactiveLinking,
});
this.#ownsDb = options.db === undefined;
if (options.db !== undefined) {
@@ -3,6 +3,7 @@ import "./setup";
import { configureRecallFeatures } from "@oh-my-pi/pi-mnemopi/config";
import { BeamMemory } from "@oh-my-pi/pi-mnemopi/core/beam";
import type { EpisodicGraph, RelatedMemory } from "@oh-my-pi/pi-mnemopi/core/episodic-graph";
import { Mnemopi } from "@oh-my-pi/pi-mnemopi/core/memory";
const previousProactive = process.env.MNEMOPI_PROACTIVE_LINKING;
@@ -61,6 +62,128 @@ describe("proactive memory linking", () => {
}
});
it("keeps host configuration scoped to each BeamMemory instance", () => {
delete process.env.MNEMOPI_PROACTIVE_LINKING;
const enabled = new BeamMemory({
sessionId: "proactive-instance-on",
dbPath: ":memory:",
proactiveLinking: true,
});
configureRecallFeatures({ proactiveLinking: false });
const disabled = new BeamMemory({
sessionId: "proactive-instance-off",
dbPath: ":memory:",
proactiveLinking: false,
});
try {
const enabledFirst = enabled.remember("Alice set up the CI/CD pipeline for backend deployment", {
importance: 0.8,
});
const enabledSecond = enabled.remember("Alice configured the deployment pipeline for continuous integration", {
importance: 0.8,
});
const disabledFirst = disabled.remember("Alice set up the CI/CD pipeline for backend deployment", {
importance: 0.8,
});
const disabledSecond = disabled.remember(
"Alice configured the deployment pipeline for continuous integration",
{
importance: 0.8,
},
);
expect(linkedIds(graphOf(enabled).findRelatedMemories(enabledSecond, 1)).has(enabledFirst)).toBe(true);
expect(linkedIds(graphOf(disabled).findRelatedMemories(disabledSecond, 1)).has(disabledFirst)).toBe(false);
} finally {
enabled.close();
disabled.close();
}
});
it("keeps host configuration scoped to each Mnemopi instance", () => {
delete process.env.MNEMOPI_PROACTIVE_LINKING;
const enabled = new Mnemopi({
sessionId: "proactive-mnemopi-on",
dbPath: ":memory:",
proactiveLinking: true,
});
configureRecallFeatures({ proactiveLinking: false });
const disabled = new Mnemopi({
sessionId: "proactive-mnemopi-off",
dbPath: ":memory:",
proactiveLinking: false,
});
try {
const enabledFirst = enabled.remember("Database indexing improves query performance significantly", {
importance: 0.8,
});
const enabledSecond = enabled.remember("Database indexing optimizes query performance and efficiency", {
importance: 0.8,
});
const disabledFirst = disabled.remember("Database indexing improves query performance significantly", {
importance: 0.8,
});
const disabledSecond = disabled.remember("Database indexing optimizes query performance and efficiency", {
importance: 0.8,
});
expect(linkedIds(graphOf(enabled.beam).findRelatedMemories(enabledSecond, 1)).has(enabledFirst)).toBe(true);
expect(linkedIds(graphOf(disabled.beam).findRelatedMemories(disabledSecond, 1)).has(disabledFirst)).toBe(
false,
);
} finally {
enabled.close();
disabled.close();
}
});
it("lets the environment variable override instance configuration", () => {
process.env.MNEMOPI_PROACTIVE_LINKING = "0";
const disabledByEnv = new BeamMemory({
sessionId: "proactive-env-off",
dbPath: ":memory:",
proactiveLinking: true,
});
try {
const disabledFirst = disabledByEnv.remember("Alice set up the CI/CD pipeline for backend deployment", {
importance: 0.8,
});
const disabledSecond = disabledByEnv.remember(
"Alice configured the deployment pipeline for continuous integration",
{
importance: 0.8,
},
);
process.env.MNEMOPI_PROACTIVE_LINKING = "1";
const enabledByEnv = new BeamMemory({
sessionId: "proactive-env-on",
dbPath: ":memory:",
proactiveLinking: false,
});
try {
const enabledFirst = enabledByEnv.remember("Alice set up the CI/CD pipeline for backend deployment", {
importance: 0.8,
});
const enabledSecond = enabledByEnv.remember(
"Alice configured the deployment pipeline for continuous integration",
{
importance: 0.8,
},
);
expect(linkedIds(graphOf(enabledByEnv).findRelatedMemories(enabledSecond, 1)).has(enabledFirst)).toBe(true);
} finally {
enabledByEnv.close();
}
expect(linkedIds(graphOf(disabledByEnv).findRelatedMemories(disabledSecond, 1)).has(disabledFirst)).toBe(
false,
);
} finally {
disabledByEnv.close();
}
});
it("does not create recall-similarity edges for unrelated content", () => {
process.env.MNEMOPI_PROACTIVE_LINKING = "1";
const beam = new BeamMemory({ sessionId: "proactive-unrelated", dbPath: ":memory:" });