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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))),
|
||||
|
||||
@@ -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<string, MnemopiScopedMemory>();
|
||||
|
||||
@@ -128,6 +128,7 @@ function makeMnemopiConfig(
|
||||
autoRetain: true,
|
||||
polyphonicRecall: false,
|
||||
enhancedRecall: false,
|
||||
proactiveLinking: false,
|
||||
retainEveryNTurns: 3,
|
||||
recallLimit: 10,
|
||||
recallContextTurns: 1,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string> {
|
||||
@@ -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:" });
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user