feat(mnemopi): exposed polyphonic and enhanced recall via config.yml

- Added `mnemopi.polyphonicRecall` and `mnemopi.enhancedRecall` boolean settings (default off) to `settings-schema.ts` and `loadMnemopiConfig`, applied via the new `configureRecallFeatures()` in `createScopedResources`.
- Made `polyphonicRecallEnabled()`, `enhancedRecallEnabled()`, and `isEnhancedRecallEnabled()` fall back to the configured defaults while `MNEMOPI_POLYPHONIC_RECALL` / `MNEMOPI_ENHANCED_RECALL` env vars still win when set.
- Exported `configureRecallFeatures`/`RecallFeatureFlags` from the package root and `core` barrels; documented the settings in `docs/mnemosyne-memory-backend.md`.
- Added `recall-feature-flags.test.ts` covering defaults, config enablement, and env precedence; the mnemopi changelog hunk also carries the adjacent #2322 entry (same contiguous run).

Fixes #2323: Mnemopi: MNEMOPI_POLYPHONIC_RECALL and MNEMOPI_ENHANCED_RECALL not configurable via config.yml
This commit is contained in:
can1357
2026-06-12 04:01:36 +02:00
parent cfeaee0105
commit 12bbefd48b
12 changed files with 120 additions and 5 deletions
+2
View File
@@ -38,6 +38,8 @@ Recalled memory is background context, not instructions. Current user messages a
| `mnemopi.scoping` | `per-project` | Memory visibility mode: `global` = one shared bank, `per-project` = isolated project memory, `per-project-tagged` = project-local writes plus global recall visibility. |
| `mnemopi.autoRecall` | `true` | Recall memory on the first turn of a session. |
| `mnemopi.autoRetain` | `true` | Retain completed turns automatically. |
| `mnemopi.polyphonicRecall` | `false` | Enable 4-voice polyphonic recall (vector, graph, fact, temporal) with reciprocal rank fusion; `MNEMOPI_POLYPHONIC_RECALL` overrides when set. |
| `mnemopi.enhancedRecall` | `false` | Enable the tiered query result cache for repeated/similar recall queries; `MNEMOPI_ENHANCED_RECALL` overrides when set. |
| `mnemopi.retainEveryNTurns` | `4` | Minimum user turns between automatic retain writes. |
| `mnemopi.recallLimit` | `8` | Maximum recalled memories in the prompt block. |
| `mnemopi.recallContextTurns` | `3` | Prior user-bounded turns included in recall queries. |
+1
View File
@@ -8,6 +8,7 @@
- `ModelRegistry.resolver` now accepts a model directly — `resolver(model, sessionId)` — deriving `provider`, `baseUrl`, and `modelId` from it; all model-scoped call sites migrated from the verbose `resolver(model.provider, { sessionId, baseUrl, modelId })` form.
- Added experimental `snapcompact.systemPrompt` and `snapcompact.toolResults` settings (off by default, `/settings` → Context → Experimental) that render the system prompt and large historical tool results as dense snapcompact PNG frames on vision-capable models to cut token cost. Frames are built per-request in the provider-context transform, cached across turns, capped by a per-provider image budget, and gated on a token-savings estimate — they never reach `session.jsonl`.
- Added a Personality selector to `/settings` (Model → Prompt): `default` (the previous built-in reply style), `friendly`, `pragmatic`, or `none`. The selected spec renders into a dedicated `<personality>` system-prompt block (extracted from the former `<reply-guidelines>` section) and applies to the live session immediately; subagents always omit the block.
- Added `mnemopi.polyphonicRecall` and `mnemopi.enhancedRecall` config.yml settings (off by default, `/settings` → Memory → Mnemopi) that enable the mnemopi 4-voice polyphonic recall engine and the tiered query result cache without environment variables; `MNEMOPI_POLYPHONIC_RECALL` / `MNEMOPI_ENHANCED_RECALL` still override the configured values when set ([#2323](https://github.com/can1357/oh-my-pi/issues/2323)).
### Changed
@@ -1748,6 +1748,28 @@ export const SETTINGS_SCHEMA = {
condition: "mnemopiActive",
},
},
"mnemopi.polyphonicRecall": {
type: "boolean",
default: false,
ui: {
tab: "memory",
group: "Mnemopi",
label: "Mnemopi Polyphonic Recall",
description: "Enable 4-voice recall (vector, graph, fact, temporal) fused with reciprocal rank fusion",
condition: "mnemopiActive",
},
},
"mnemopi.enhancedRecall": {
type: "boolean",
default: false,
ui: {
tab: "memory",
group: "Mnemopi",
label: "Mnemopi Enhanced Recall",
description: "Enable the tiered query result cache for repeated and similar recall queries",
condition: "mnemopiActive",
},
},
"mnemopi.noEmbeddings": {
type: "boolean",
default: false,
@@ -23,6 +23,8 @@ export interface MnemopiBackendConfig {
scoping?: MnemopiScoping;
autoRecall: boolean;
autoRetain: boolean;
polyphonicRecall: boolean;
enhancedRecall: boolean;
retainEveryNTurns: number;
recallLimit: number;
recallContextTurns: number;
@@ -52,6 +54,8 @@ export function loadMnemopiConfig(settings: Settings, agentDir: string): Mnemopi
scoping,
autoRecall: settings.get("mnemopi.autoRecall"),
autoRetain: settings.get("mnemopi.autoRetain"),
polyphonicRecall: settings.get("mnemopi.polyphonicRecall"),
enhancedRecall: settings.get("mnemopi.enhancedRecall"),
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,6 +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) still override
// these config-driven defaults inside the core gates.
requireMnemopi().configureRecallFeatures({
polyphonicRecall: config.polyphonicRecall,
enhancedRecall: config.enhancedRecall,
});
const banks = resolveScopedBanks(config);
const memories = new Map<string, MnemopiScopedMemory>();
const open = (bank: string): MnemopiScopedMemory => {
@@ -126,6 +126,8 @@ function makeMnemopiConfig(
bank: "test-bank",
autoRecall: true,
autoRetain: true,
polyphonicRecall: false,
enhancedRecall: false,
retainEveryNTurns: 3,
recallLimit: 10,
recallContextTurns: 1,
+8
View File
@@ -2,6 +2,14 @@
## [Unreleased]
### Added
- Added `configureRecallFeatures()` (exported from the package root, `core`, and `config`) so hosts can enable the polyphonic recall engine and the enhanced recall query cache programmatically. `polyphonicRecallEnabled()`, `enhancedRecallEnabled()`, and `isEnhancedRecallEnabled()` now fall back to these configured defaults, with the `MNEMOPI_POLYPHONIC_RECALL` / `MNEMOPI_ENHANCED_RECALL` environment variables still taking precedence whenever they are set. ([#2323](https://github.com/can1357/oh-my-pi/issues/2323))
### Fixed
- Fixed the embedding pipeline's silent `catch {}` blocks (`runEmbedding()`, `getLocalModel()`, and the local-model path of `embed()`) swallowing failures with zero diagnostics. These best-effort paths still degrade gracefully (return `null` / skip the write), but now emit structured `logger.debug` entries with the error and per-site context (item count, model name). The `mnemopi.debug` config flag now propagates into the core library via runtime options (`MnemopiOptions.debug` → `ResolvedMnemopiRuntimeOptions.debug`) and escalates these logs to `warn` so they surface at the default log level. ([#2322](https://github.com/can1357/oh-my-pi/issues/2322))
### Changed
- Extraction, embedding, and remote-LLM clients now accept an `ApiKey` (static string or resolver) and resolve it per request through `withAuth`, so 401s force-refresh and rotate credentials via the central auth-retry policy instead of failing with a stale key. Empty-key setups (local/proxy endpoints without `Authorization`) and pinned literal keys behave exactly as before.
+23 -2
View File
@@ -248,8 +248,28 @@ export function proactiveLinkingEnabled(env: Env = process.env): boolean {
return envString("MNEMOPI_PROACTIVE_LINKING", "0", env) === "1";
}
export interface RecallFeatureFlags {
polyphonicRecall?: boolean;
enhancedRecall?: boolean;
}
let polyphonicRecallDefault = false;
let enhancedRecallDefault = 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.
*/
export function configureRecallFeatures(flags: RecallFeatureFlags): void {
if (flags.polyphonicRecall !== undefined) polyphonicRecallDefault = flags.polyphonicRecall;
if (flags.enhancedRecall !== undefined) enhancedRecallDefault = flags.enhancedRecall;
}
export function polyphonicRecallEnabled(env: Env = process.env): boolean {
return envString("MNEMOPI_POLYPHONIC_RECALL", "0", env) === "1";
const value = envOptionalString("MNEMOPI_POLYPHONIC_RECALL", env);
return value === undefined ? polyphonicRecallDefault : value === "1";
}
export function temporalHalflifeHours(env: Env = process.env): number {
@@ -257,7 +277,8 @@ export function temporalHalflifeHours(env: Env = process.env): number {
}
export function enhancedRecallEnabled(env: Env = process.env): boolean {
return envString("MNEMOPI_ENHANCED_RECALL", "0", env) === "1";
const value = envOptionalString("MNEMOPI_ENHANCED_RECALL", env);
return value === undefined ? enhancedRecallDefault : value === "1";
}
export function llmEnabled(env: Env = process.env): boolean {
+1
View File
@@ -1,3 +1,4 @@
export { configureRecallFeatures, type RecallFeatureFlags } from "../config";
export * from "./banks";
export * from "./beam/index";
export * from "./memory";
+2 -3
View File
@@ -1,6 +1,7 @@
import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { type Env, enhancedRecallEnabled } from "../config";
import { cosineSimilarity } from "./vector-math";
export type QueryCacheResult = Record<string, unknown>;
@@ -39,10 +40,8 @@ interface CacheRow {
readonly results_json: string;
}
type Env = Readonly<Record<string, string | undefined>>;
export function isEnhancedRecallEnabled(env: Env = process.env): boolean {
return env.MNEMOPI_ENHANCED_RECALL === "1";
return enhancedRecallEnabled(env);
}
export function isQueryCacheEnabled(useCache = true, env: Env = process.env): boolean {
+1
View File
@@ -1,3 +1,4 @@
export { configureRecallFeatures, type RecallFeatureFlags } from "./config";
export * from "./core/beam/index";
export * from "./core/embeddings";
export * from "./core/llm-backends";
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, it } from "bun:test";
import { configureRecallFeatures, enhancedRecallEnabled, polyphonicRecallEnabled } 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 });
});
describe("configureRecallFeatures", () => {
it("keeps both recall gates off by default", () => {
expect(polyphonicRecallEnabled({})).toBe(false);
expect(enhancedRecallEnabled({})).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 });
expect(polyphonicRecallEnabled({})).toBe(true);
expect(polyphonicRecallIsEnabled({})).toBe(true);
expect(enhancedRecallEnabled({})).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 });
expect(polyphonicRecallEnabled({ MNEMOPI_POLYPHONIC_RECALL: "0" })).toBe(false);
expect(enhancedRecallEnabled({ MNEMOPI_ENHANCED_RECALL: "0" })).toBe(false);
expect(isQueryCacheEnabled(true, { MNEMOPI_ENHANCED_RECALL: "0" })).toBe(false);
configureRecallFeatures({ polyphonicRecall: false, enhancedRecall: false });
expect(polyphonicRecallEnabled({ MNEMOPI_POLYPHONIC_RECALL: "1" })).toBe(true);
expect(enhancedRecallEnabled({ MNEMOPI_ENHANCED_RECALL: "1" })).toBe(true);
expect(isQueryCacheEnabled(true, { MNEMOPI_ENHANCED_RECALL: "1" })).toBe(true);
});
it("updates only the flags that are present", () => {
configureRecallFeatures({ polyphonicRecall: true });
expect(polyphonicRecallEnabled({})).toBe(true);
expect(enhancedRecallEnabled({})).toBe(false);
configureRecallFeatures({ enhancedRecall: true });
expect(polyphonicRecallEnabled({})).toBe(true);
expect(enhancedRecallEnabled({})).toBe(true);
});
});