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); }); });