From 8eb8ec4b7407e24e05cc3e3022391bccc9007f3c Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:24:45 -0700 Subject: [PATCH 1/3] fix(mnemopi): make proactive linking configurable from host settings Proactive linking (ingesting new memories into the episodic graph as they are stored) could only be toggled through the MNEMOPI_PROACTIVE_LINKING environment variable, unlike its sibling recall features polyphonicRecall and enhancedRecall, which hosts drive through configureRecallFeatures() and the coding-agent mnemopi.polyphonicRecall / mnemopi.enhancedRecall config.yml settings. The write-path gate in store.ts read process.env directly and the existing env-only proactiveLinkingEnabled() helper was dead code, so host configuration never reached it. - Add proactiveLinking to RecallFeatureFlags / configureRecallFeatures() and rewrite proactiveLinkingEnabled() to fall back to the configured default, matching the polyphonic/enhanced resolvers. The MNEMOPI_PROACTIVE_LINKING env var still takes precedence when set. - Route the store.ts proactiveLinkIfEnabled gate through proactiveLinkingEnabled() instead of reading process.env directly. - Add the mnemopi.proactiveLinking coding-agent config.yml setting (off by default, /settings -> Memory -> Mnemopi) and wire it through loadMnemopiConfig and createScopedResources. Closes #2440 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/coding-agent/CHANGELOG.md | 3 +++ .../src/config/settings-schema.ts | 12 +++++++++ packages/coding-agent/src/mnemopi/config.ts | 2 ++ packages/coding-agent/src/mnemopi/state.ts | 6 +++-- .../coding-agent/test/memory-tools.test.ts | 1 + packages/mnemopi/CHANGELOG.md | 3 +++ packages/mnemopi/src/config.ts | 19 ++++++++----- packages/mnemopi/src/core/beam/store.ts | 3 ++- .../mnemopi/test/proactive-linking.test.ts | 22 +++++++++++++++ .../mnemopi/test/recall-feature-flags.test.ts | 27 ++++++++++++++----- 10 files changed, 82 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c4f0c4eb7..cf06331e6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -92,6 +92,9 @@ ### Security - Secured PDF image reads by validating requested image members against the extracted member list before opening files and refusing traversal-style names +### Fixed + +- Fixed mnemopi proactive linking being configurable only through the `MNEMOPI_PROACTIVE_LINKING` environment variable, unlike the sibling `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` settings: added a `mnemopi.proactiveLinking` config.yml setting (off by default, `/settings` → Memory → Mnemopi) that ingests new memories into the episodic graph as they are stored, linking them to related entities and memories; `MNEMOPI_PROACTIVE_LINKING` still overrides the configured value when set ([#2440](https://github.com/can1357/oh-my-pi/issues/2440)). ## [16.0.5] - 2026-06-17 diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 6883ca107..4e2ac084c 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -2219,6 +2219,18 @@ export const SETTINGS_SCHEMA = { condition: "mnemopiActive", }, }, + "mnemopi.proactiveLinking": { + type: "boolean", + default: false, + ui: { + tab: "memory", + group: "Mnemopi", + label: "Mnemopi Proactive Linking", + description: + "Ingest new memories into the episodic graph as they are stored, linking them to related entities and memories", + condition: "mnemopiActive", + }, + }, "mnemopi.noEmbeddings": { type: "boolean", default: false, diff --git a/packages/coding-agent/src/mnemopi/config.ts b/packages/coding-agent/src/mnemopi/config.ts index 79fa4a366..16570b783 100644 --- a/packages/coding-agent/src/mnemopi/config.ts +++ b/packages/coding-agent/src/mnemopi/config.ts @@ -26,6 +26,7 @@ export interface MnemopiBackendConfig { autoRetain: boolean; polyphonicRecall: boolean; enhancedRecall: boolean; + proactiveLinking: boolean; retainEveryNTurns: number; recallLimit: number; recallContextTurns: number; @@ -71,6 +72,7 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi autoRetain: settings.get("mnemopi.autoRetain"), polyphonicRecall: settings.get("mnemopi.polyphonicRecall"), enhancedRecall: settings.get("mnemopi.enhancedRecall"), + proactiveLinking: settings.get("mnemopi.proactiveLinking"), retainEveryNTurns: Math.max(1, Math.floor(settings.get("mnemopi.retainEveryNTurns"))), recallLimit: Math.max(1, Math.floor(settings.get("mnemopi.recallLimit"))), recallContextTurns: Math.max(1, Math.floor(settings.get("mnemopi.recallContextTurns"))), diff --git a/packages/coding-agent/src/mnemopi/state.ts b/packages/coding-agent/src/mnemopi/state.ts index ebd5d8898..4f2f1ccf9 100644 --- a/packages/coding-agent/src/mnemopi/state.ts +++ b/packages/coding-agent/src/mnemopi/state.ts @@ -421,11 +421,13 @@ 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) still override - // these config-driven defaults inside the core gates. + // Env vars (MNEMOPI_POLYPHONIC_RECALL / MNEMOPI_ENHANCED_RECALL / + // MNEMOPI_PROACTIVE_LINKING) still override these config-driven defaults inside + // the core gates. requireMnemopi().configureRecallFeatures({ polyphonicRecall: config.polyphonicRecall, enhancedRecall: config.enhancedRecall, + proactiveLinking: config.proactiveLinking, }); const banks = resolveScopedBanks(config); const memories = new Map(); diff --git a/packages/coding-agent/test/memory-tools.test.ts b/packages/coding-agent/test/memory-tools.test.ts index d9278f9a5..9bd830e98 100644 --- a/packages/coding-agent/test/memory-tools.test.ts +++ b/packages/coding-agent/test/memory-tools.test.ts @@ -128,6 +128,7 @@ function makeMnemopiConfig( autoRetain: true, polyphonicRecall: false, enhancedRecall: false, + proactiveLinking: false, retainEveryNTurns: 3, recallLimit: 10, recallContextTurns: 1, diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index e0b81b92a..f680cb5c3 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -11,6 +11,9 @@ ### Changed - Updated OpenRouter request headers to use standard shared headers from the pi-ai package +### Fixed + +- 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)) ## [16.0.5] - 2026-06-17 diff --git a/packages/mnemopi/src/config.ts b/packages/mnemopi/src/config.ts index 30630516f..c493bd537 100644 --- a/packages/mnemopi/src/config.ts +++ b/packages/mnemopi/src/config.ts @@ -248,27 +248,27 @@ export function autoMigrateEnabled(env: Env = process.env): boolean { return envString("MNEMOPI_AUTO_MIGRATE", "1", env) !== "0"; } -export function proactiveLinkingEnabled(env: Env = process.env): boolean { - return envString("MNEMOPI_PROACTIVE_LINKING", "0", env) === "1"; -} - export interface RecallFeatureFlags { polyphonicRecall?: boolean; enhancedRecall?: boolean; + proactiveLinking?: boolean; } let polyphonicRecallDefault = false; let enhancedRecallDefault = false; +let proactiveLinkingDefault = false; /** * Sets process-wide defaults for the env-gated recall features. Host configuration - * (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` - * settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` / `MNEMOPI_ENHANCED_RECALL` - * environment variables still win whenever they are set. + * (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` / + * `mnemopi.proactiveLinking` settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` / + * `MNEMOPI_ENHANCED_RECALL` / `MNEMOPI_PROACTIVE_LINKING` environment variables still + * win whenever they are set. */ export function configureRecallFeatures(flags: RecallFeatureFlags): void { if (flags.polyphonicRecall !== undefined) polyphonicRecallDefault = flags.polyphonicRecall; if (flags.enhancedRecall !== undefined) enhancedRecallDefault = flags.enhancedRecall; + if (flags.proactiveLinking !== undefined) proactiveLinkingDefault = flags.proactiveLinking; } export function polyphonicRecallEnabled(env: Env = process.env): boolean { @@ -285,6 +285,11 @@ export function enhancedRecallEnabled(env: Env = process.env): boolean { return value === undefined ? enhancedRecallDefault : value === "1"; } +export function proactiveLinkingEnabled(env: Env = process.env): boolean { + const value = envOptionalString("MNEMOPI_PROACTIVE_LINKING", env); + return value === undefined ? proactiveLinkingDefault : value === "1"; +} + export function llmEnabled(env: Env = process.env): boolean { return envBool("MNEMOPI_LLM_ENABLED", true, env); } diff --git a/packages/mnemopi/src/core/beam/store.ts b/packages/mnemopi/src/core/beam/store.ts index d47b484a5..2a98e7a0a 100644 --- a/packages/mnemopi/src/core/beam/store.ts +++ b/packages/mnemopi/src/core/beam/store.ts @@ -1,5 +1,6 @@ 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"; @@ -193,7 +194,7 @@ function proactiveLinkIfEnabled( content: string, extractEntities: boolean, ): void { - if (process.env.MNEMOPI_PROACTIVE_LINKING !== "1") return; + if (!proactiveLinkingEnabled()) return; try { const graph = beam.episodicGraph instanceof EpisodicGraph diff --git a/packages/mnemopi/test/proactive-linking.test.ts b/packages/mnemopi/test/proactive-linking.test.ts index 9eb3980e7..5a3766fdb 100644 --- a/packages/mnemopi/test/proactive-linking.test.ts +++ b/packages/mnemopi/test/proactive-linking.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it } from "bun:test"; 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"; @@ -8,6 +9,7 @@ const previousProactive = process.env.MNEMOPI_PROACTIVE_LINKING; afterEach(() => { if (previousProactive === undefined) delete process.env.MNEMOPI_PROACTIVE_LINKING; else process.env.MNEMOPI_PROACTIVE_LINKING = previousProactive; + configureRecallFeatures({ proactiveLinking: false }); }); function linkedIds(edges: readonly RelatedMemory[]): Set { @@ -39,6 +41,26 @@ describe("proactive memory linking", () => { } }); + it("honors host configuration when the environment variable is unset", () => { + delete process.env.MNEMOPI_PROACTIVE_LINKING; + configureRecallFeatures({ proactiveLinking: true }); + const beam = new BeamMemory({ sessionId: "proactive-host-config", dbPath: ":memory:" }); + try { + const first = beam.remember("Alice set up the CI/CD pipeline for backend deployment", { + importance: 0.8, + }); + const second = beam.remember("Alice configured the deployment pipeline for continuous integration", { + importance: 0.8, + }); + + const edges = graphOf(beam).findRelatedMemories(second, 1); + expect(linkedIds(edges).has(first)).toBe(true); + expect(edges.some(edge => edge.memoryId === first && edge.edgeType === "related_to")).toBe(true); + } finally { + beam.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:" }); diff --git a/packages/mnemopi/test/recall-feature-flags.test.ts b/packages/mnemopi/test/recall-feature-flags.test.ts index db1b1484d..4b4a1153d 100644 --- a/packages/mnemopi/test/recall-feature-flags.test.ts +++ b/packages/mnemopi/test/recall-feature-flags.test.ts @@ -1,39 +1,48 @@ import { afterEach, describe, expect, it } from "bun:test"; -import { configureRecallFeatures, enhancedRecallEnabled, polyphonicRecallEnabled } from "@oh-my-pi/pi-mnemopi/config"; +import { + configureRecallFeatures, + enhancedRecallEnabled, + polyphonicRecallEnabled, + proactiveLinkingEnabled, +} from "@oh-my-pi/pi-mnemopi/config"; import { polyphonicRecallIsEnabled } from "@oh-my-pi/pi-mnemopi/core/polyphonic-recall"; import { isEnhancedRecallEnabled, isQueryCacheEnabled } from "@oh-my-pi/pi-mnemopi/core/query-cache"; afterEach(() => { - configureRecallFeatures({ polyphonicRecall: false, enhancedRecall: false }); + configureRecallFeatures({ polyphonicRecall: false, enhancedRecall: false, proactiveLinking: false }); }); describe("configureRecallFeatures", () => { - it("keeps both recall gates off by default", () => { + it("keeps all recall gates off by default", () => { expect(polyphonicRecallEnabled({})).toBe(false); expect(enhancedRecallEnabled({})).toBe(false); + expect(proactiveLinkingEnabled({})).toBe(false); expect(isEnhancedRecallEnabled({})).toBe(false); expect(isQueryCacheEnabled(true, {})).toBe(false); }); it("enables the gates from host configuration when the env vars are unset", () => { - configureRecallFeatures({ polyphonicRecall: true, enhancedRecall: true }); + configureRecallFeatures({ polyphonicRecall: true, enhancedRecall: true, proactiveLinking: true }); expect(polyphonicRecallEnabled({})).toBe(true); expect(polyphonicRecallIsEnabled({})).toBe(true); expect(enhancedRecallEnabled({})).toBe(true); + expect(proactiveLinkingEnabled({})).toBe(true); expect(isEnhancedRecallEnabled({})).toBe(true); expect(isQueryCacheEnabled(true, {})).toBe(true); expect(isQueryCacheEnabled(false, {})).toBe(false); }); it("lets the env vars override the configured value in both directions", () => { - configureRecallFeatures({ polyphonicRecall: true, enhancedRecall: true }); + configureRecallFeatures({ polyphonicRecall: true, enhancedRecall: true, proactiveLinking: true }); expect(polyphonicRecallEnabled({ MNEMOPI_POLYPHONIC_RECALL: "0" })).toBe(false); expect(enhancedRecallEnabled({ MNEMOPI_ENHANCED_RECALL: "0" })).toBe(false); + expect(proactiveLinkingEnabled({ MNEMOPI_PROACTIVE_LINKING: "0" })).toBe(false); expect(isQueryCacheEnabled(true, { MNEMOPI_ENHANCED_RECALL: "0" })).toBe(false); - configureRecallFeatures({ polyphonicRecall: false, enhancedRecall: false }); + configureRecallFeatures({ polyphonicRecall: false, enhancedRecall: false, proactiveLinking: false }); expect(polyphonicRecallEnabled({ MNEMOPI_POLYPHONIC_RECALL: "1" })).toBe(true); expect(enhancedRecallEnabled({ MNEMOPI_ENHANCED_RECALL: "1" })).toBe(true); + expect(proactiveLinkingEnabled({ MNEMOPI_PROACTIVE_LINKING: "1" })).toBe(true); expect(isQueryCacheEnabled(true, { MNEMOPI_ENHANCED_RECALL: "1" })).toBe(true); }); @@ -41,8 +50,14 @@ describe("configureRecallFeatures", () => { configureRecallFeatures({ polyphonicRecall: true }); expect(polyphonicRecallEnabled({})).toBe(true); expect(enhancedRecallEnabled({})).toBe(false); + expect(proactiveLinkingEnabled({})).toBe(false); configureRecallFeatures({ enhancedRecall: true }); expect(polyphonicRecallEnabled({})).toBe(true); expect(enhancedRecallEnabled({})).toBe(true); + expect(proactiveLinkingEnabled({})).toBe(false); + configureRecallFeatures({ proactiveLinking: true }); + expect(polyphonicRecallEnabled({})).toBe(true); + expect(enhancedRecallEnabled({})).toBe(true); + expect(proactiveLinkingEnabled({})).toBe(true); }); }); From 70f9e604f0e4a76b5a6e3e23f8ba4b37893f7ac9 Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:15:56 -0700 Subject: [PATCH 2/3] fix(mnemopi): scope proactive linking per memory --- packages/coding-agent/src/mnemopi/state.ts | 8 +- packages/mnemopi/src/core/beam/index.ts | 14 +- packages/mnemopi/src/core/beam/store.ts | 8 +- packages/mnemopi/src/core/beam/types.ts | 2 + packages/mnemopi/src/core/memory.ts | 2 + .../mnemopi/test/proactive-linking.test.ts | 123 ++++++++++++++++++ 6 files changed, 150 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/src/mnemopi/state.ts b/packages/coding-agent/src/mnemopi/state.ts index 4f2f1ccf9..43e41876f 100644 --- a/packages/coding-agent/src/mnemopi/state.ts +++ b/packages/coding-agent/src/mnemopi/state.ts @@ -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(); @@ -550,6 +549,7 @@ function createMemory(config: MnemopiBackendConfig, bank: string): Mnemopi { authorType: "agent", channelId: bank, ...providerOptions, + proactiveLinking: config.proactiveLinking, } as ConstructorParameters[0]); } diff --git a/packages/mnemopi/src/core/beam/index.ts b/packages/mnemopi/src/core/beam/index.ts index 407fda522..fb56c0d11 100644 --- a/packages/mnemopi/src/core/beam/index.ts +++ b/packages/mnemopi/src/core/beam/index.ts @@ -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 { diff --git a/packages/mnemopi/src/core/beam/store.ts b/packages/mnemopi/src/core/beam/store.ts index 2a98e7a0a..e2885763a 100644 --- a/packages/mnemopi/src/core/beam/store.ts +++ b/packages/mnemopi/src/core/beam/store.ts @@ -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 diff --git a/packages/mnemopi/src/core/beam/types.ts b/packages/mnemopi/src/core/beam/types.ts index dc52c6575..751327853 100644 --- a/packages/mnemopi/src/core/beam/types.ts +++ b/packages/mnemopi/src/core/beam/types.ts @@ -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; diff --git a/packages/mnemopi/src/core/memory.ts b/packages/mnemopi/src/core/memory.ts index 912d787cd..19729cafb 100644 --- a/packages/mnemopi/src/core/memory.ts +++ b/packages/mnemopi/src/core/memory.ts @@ -43,6 +43,7 @@ export interface MnemopiOptions { readonly llmApiKey?: ApiKey; readonly llmModel?: string | Model; readonly llm?: false | MnemopiLlmRuntimeOptions | Model | 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) { diff --git a/packages/mnemopi/test/proactive-linking.test.ts b/packages/mnemopi/test/proactive-linking.test.ts index 5a3766fdb..b705df908 100644 --- a/packages/mnemopi/test/proactive-linking.test.ts +++ b/packages/mnemopi/test/proactive-linking.test.ts @@ -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:" }); From 19ceca25b2f0abb3884731fffb94283d97bf5375 Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:28:45 -0700 Subject: [PATCH 3/3] fix(mnemopi): keep env override out of proactive defaults --- packages/coding-agent/CHANGELOG.md | 6 ++--- packages/mnemopi/CHANGELOG.md | 7 +++--- packages/mnemopi/src/core/beam/index.ts | 11 +--------- .../mnemopi/test/proactive-linking.test.ts | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cf06331e6..d955732a0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -39,6 +39,9 @@ - Fixed `/model` in the TUI to open the model setup picker again, leaving `/switch` as the temporary session model switcher ([#2933](https://github.com/can1357/oh-my-pi/issues/2933)). - Fixed OpenCode Go sessions recording per-request cost history so `/usage` can show local cap utilization. ([#2942](https://github.com/can1357/oh-my-pi/issues/2942)) +### Fixed + +- Fixed mnemopi proactive linking being configurable only through the `MNEMOPI_PROACTIVE_LINKING` environment variable, unlike the sibling `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` settings: added a `mnemopi.proactiveLinking` config.yml setting (off by default, `/settings` → Memory → Mnemopi) that ingests new memories into the episodic graph as they are stored, linking them to related entities and memories; `MNEMOPI_PROACTIVE_LINKING` still overrides the configured value when set ([#2440](https://github.com/can1357/oh-my-pi/issues/2440)). ## [16.0.6] - 2026-06-18 @@ -92,9 +95,6 @@ ### Security - Secured PDF image reads by validating requested image members against the extracted member list before opening files and refusing traversal-style names -### Fixed - -- Fixed mnemopi proactive linking being configurable only through the `MNEMOPI_PROACTIVE_LINKING` environment variable, unlike the sibling `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` settings: added a `mnemopi.proactiveLinking` config.yml setting (off by default, `/settings` → Memory → Mnemopi) that ingests new memories into the episodic graph as they are stored, linking them to related entities and memories; `MNEMOPI_PROACTIVE_LINKING` still overrides the configured value when set ([#2440](https://github.com/can1357/oh-my-pi/issues/2440)). ## [16.0.5] - 2026-06-17 diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index f680cb5c3..ae5114e84 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- 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)) + ## [16.0.6] - 2026-06-18 ### Fixed @@ -11,9 +15,6 @@ ### Changed - Updated OpenRouter request headers to use standard shared headers from the pi-ai package -### Fixed - -- 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)) ## [16.0.5] - 2026-06-17 diff --git a/packages/mnemopi/src/core/beam/index.ts b/packages/mnemopi/src/core/beam/index.ts index fb56c0d11..a279aa7b1 100644 --- a/packages/mnemopi/src/core/beam/index.ts +++ b/packages/mnemopi/src/core/beam/index.ts @@ -72,19 +72,10 @@ const DEFAULT_CONFIG: BeamConfig = { 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(); + const proactiveLinking = options.proactiveLinking ?? configured.proactiveLinking ?? proactiveLinkingEnabled({}); return { workingMemoryLimit: configured.workingMemoryLimit ?? DEFAULT_CONFIG.workingMemoryLimit, workingMemoryTtlHours: configured.workingMemoryTtlHours ?? DEFAULT_CONFIG.workingMemoryTtlHours, diff --git a/packages/mnemopi/test/proactive-linking.test.ts b/packages/mnemopi/test/proactive-linking.test.ts index b705df908..0e7b799de 100644 --- a/packages/mnemopi/test/proactive-linking.test.ts +++ b/packages/mnemopi/test/proactive-linking.test.ts @@ -184,6 +184,28 @@ describe("proactive memory linking", () => { } }); + it("does not snapshot a construction-time environment override into instance defaults", () => { + process.env.MNEMOPI_PROACTIVE_LINKING = "1"; + const beam = new BeamMemory({ + sessionId: "proactive-env-snapshot", + dbPath: ":memory:", + proactiveLinking: false, + }); + delete process.env.MNEMOPI_PROACTIVE_LINKING; + try { + const first = beam.remember("Alice set up the CI/CD pipeline for backend deployment", { + importance: 0.8, + }); + const second = beam.remember("Alice configured the deployment pipeline for continuous integration", { + importance: 0.8, + }); + + expect(linkedIds(graphOf(beam).findRelatedMemories(second, 1)).has(first)).toBe(false); + } finally { + beam.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:" });