From 12bbefd48b81deed548018bb824637f4b22ba094 Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 12 Jun 2026 04:01:36 +0200 Subject: [PATCH] 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 --- docs/mnemosyne-memory-backend.md | 2 + packages/coding-agent/CHANGELOG.md | 1 + .../src/config/settings-schema.ts | 22 +++++++++ packages/coding-agent/src/mnemopi/config.ts | 4 ++ packages/coding-agent/src/mnemopi/state.ts | 6 +++ .../coding-agent/test/memory-tools.test.ts | 2 + packages/mnemopi/CHANGELOG.md | 8 ++++ packages/mnemopi/src/config.ts | 25 +++++++++- packages/mnemopi/src/core/index.ts | 1 + packages/mnemopi/src/core/query-cache.ts | 5 +- packages/mnemopi/src/index.ts | 1 + .../mnemopi/test/recall-feature-flags.test.ts | 48 +++++++++++++++++++ 12 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 packages/mnemopi/test/recall-feature-flags.test.ts diff --git a/docs/mnemosyne-memory-backend.md b/docs/mnemosyne-memory-backend.md index 482ed8d6a..ff3d9f2a5 100644 --- a/docs/mnemosyne-memory-backend.md +++ b/docs/mnemosyne-memory-backend.md @@ -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. | diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d1bc1f3b0..18b6d412a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 `` system-prompt block (extracted from the former `` 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 diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 539951e59..34bf60951 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -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, diff --git a/packages/coding-agent/src/mnemopi/config.ts b/packages/coding-agent/src/mnemopi/config.ts index 5e3e78ff5..22ed99753 100644 --- a/packages/coding-agent/src/mnemopi/config.ts +++ b/packages/coding-agent/src/mnemopi/config.ts @@ -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"))), diff --git a/packages/coding-agent/src/mnemopi/state.ts b/packages/coding-agent/src/mnemopi/state.ts index b43c3b274..ebd5d8898 100644 --- a/packages/coding-agent/src/mnemopi/state.ts +++ b/packages/coding-agent/src/mnemopi/state.ts @@ -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(); const open = (bank: string): MnemopiScopedMemory => { diff --git a/packages/coding-agent/test/memory-tools.test.ts b/packages/coding-agent/test/memory-tools.test.ts index a3598948d..d9278f9a5 100644 --- a/packages/coding-agent/test/memory-tools.test.ts +++ b/packages/coding-agent/test/memory-tools.test.ts @@ -126,6 +126,8 @@ function makeMnemopiConfig( bank: "test-bank", autoRecall: true, autoRetain: true, + polyphonicRecall: false, + enhancedRecall: false, retainEveryNTurns: 3, recallLimit: 10, recallContextTurns: 1, diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index 7c118aedf..4657a4cec 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -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. diff --git a/packages/mnemopi/src/config.ts b/packages/mnemopi/src/config.ts index 458fb65b3..40431cc1c 100644 --- a/packages/mnemopi/src/config.ts +++ b/packages/mnemopi/src/config.ts @@ -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 { diff --git a/packages/mnemopi/src/core/index.ts b/packages/mnemopi/src/core/index.ts index 843f84a78..5d9f206a8 100644 --- a/packages/mnemopi/src/core/index.ts +++ b/packages/mnemopi/src/core/index.ts @@ -1,3 +1,4 @@ +export { configureRecallFeatures, type RecallFeatureFlags } from "../config"; export * from "./banks"; export * from "./beam/index"; export * from "./memory"; diff --git a/packages/mnemopi/src/core/query-cache.ts b/packages/mnemopi/src/core/query-cache.ts index 4ce9006ca..915c52846 100644 --- a/packages/mnemopi/src/core/query-cache.ts +++ b/packages/mnemopi/src/core/query-cache.ts @@ -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; @@ -39,10 +40,8 @@ interface CacheRow { readonly results_json: string; } -type Env = Readonly>; - 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 { diff --git a/packages/mnemopi/src/index.ts b/packages/mnemopi/src/index.ts index 1d734ed43..7a83864d1 100644 --- a/packages/mnemopi/src/index.ts +++ b/packages/mnemopi/src/index.ts @@ -1,3 +1,4 @@ +export { configureRecallFeatures, type RecallFeatureFlags } from "./config"; export * from "./core/beam/index"; export * from "./core/embeddings"; export * from "./core/llm-backends"; diff --git a/packages/mnemopi/test/recall-feature-flags.test.ts b/packages/mnemopi/test/recall-feature-flags.test.ts new file mode 100644 index 000000000..db1b1484d --- /dev/null +++ b/packages/mnemopi/test/recall-feature-flags.test.ts @@ -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); + }); +});