From 5d424f27adba1cba38c5e7c1ff289b021a5b0204 Mon Sep 17 00:00:00 2001 From: Lucas <35551999+Phototonic@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:30:15 +1100 Subject: [PATCH] fix(config): deprecate unused profile limits Stop advertising legacy no-op settings in generated and example config, retain backward-compatible parsing, and document the live injection limits. Add regression coverage for templates, legacy files and profile rendering. Fixes #492 --- README.md | 2 + opencode-mem.example.jsonc | 12 --- src/config.ts | 37 +++++-- tests/profile-config-deprecation.test.ts | 124 +++++++++++++++++++++++ 4 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 tests/profile-config-deprecation.test.ts diff --git a/README.md b/README.md index 0dc9786f..0c479f7f 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,8 @@ Commonly tuned top-level options: | `showAutoCaptureToasts` / `showUserProfileToasts` / `showErrorToasts` | `true` | Toasts for auto-capture, profile updates and errors. | | `autoUpdate` | `true` | Clear OpenCode's cached install when a newer npm release is available. | +Profile context limits are set by `userProfileInjectPreferences` (default `5`), `userProfileInjectPatterns` (`5`), and `userProfileInjectWorkflows` (`3`). These control how many items are injected into the LLM conversation, not how many are displayed in the web UI. The legacy `maxProfileItems`, `userProfileDisplayPreferences`, `userProfileDisplayPatterns`, and `userProfileDisplayWorkflows` options are deprecated and have no effect. Existing config files still load, but you can remove these four keys. + ### Markdown vault backend (`memoryBackend`) By default memories live only in Turso under `storagePath`. Optionally set a **generic Markdown folder** as the source of truth (Obsidian/Raneto-compatible plain `.md` files — not an Obsidian-specific integration): diff --git a/opencode-mem.example.jsonc b/opencode-mem.example.jsonc index 2b58ba07..48c0f48e 100644 --- a/opencode-mem.example.jsonc +++ b/opencode-mem.example.jsonc @@ -428,15 +428,6 @@ // Days before inactive items (all types) are eligible for removal "userProfileStaleDays": 2, - // Number of preferences shown in UI - "userProfileDisplayPreferences": 20, - - // Number of patterns shown in UI - "userProfileDisplayPatterns": 15, - - // Number of workflows shown in UI - "userProfileDisplayWorkflows": 10, - // Number of preferences injected into LLM conversation context // Keep this small — the strongest signals are enough; more dilute LLM attention "userProfileInjectPreferences": 5, @@ -487,9 +478,6 @@ // Maximum number of memories to return in search results "maxMemories": 10, - // Maximum profile items considered for injection / ranking - "maxProfileItems": 5, - // Prefix for user/project container tags "containerTagPrefix": "opencode", diff --git a/src/config.ts b/src/config.ts index ee7fce4e..40280806 100644 --- a/src/config.ts +++ b/src/config.ts @@ -72,6 +72,10 @@ interface OpenCodeMemConfig { embeddingIdleTimeoutMs?: number; similarityThreshold?: number; maxMemories?: number; + /** + * @deprecated No effect. Use userProfileInjectPreferences, userProfileInjectPatterns, + * and userProfileInjectWorkflows to limit injected profile context. + */ maxProfileItems?: number; injectProfile?: boolean; containerTagPrefix?: string; @@ -151,8 +155,17 @@ interface OpenCodeMemConfig { deduplicationSimilarityThreshold?: number; userProfileAnalysisInterval?: number; userProfileMaxContextBytes?: number; + /** + * @deprecated No effect on the UI. Use userProfileInjectPreferences for LLM context only. + */ userProfileDisplayPreferences?: number; + /** + * @deprecated No effect on the UI. Use userProfileInjectPatterns for LLM context only. + */ userProfileDisplayPatterns?: number; + /** + * @deprecated No effect on the UI. Use userProfileInjectWorkflows for LLM context only. + */ userProfileDisplayWorkflows?: number; userProfileStaleDays?: number; userProfileInjectPreferences?: number; @@ -339,6 +352,7 @@ const DEFAULTS: Required< embeddingIdleTimeoutMs: 0, similarityThreshold: 0.6, maxMemories: 10, + // Deprecated compatibility value; not used for profile selection. maxProfileItems: 5, injectProfile: true, containerTagPrefix: "opencode", @@ -362,6 +376,7 @@ const DEFAULTS: Required< deduplicationSimilarityThreshold: 0.9, userProfileAnalysisInterval: 10, userProfileMaxContextBytes: 32768, + // Deprecated compatibility values; not used by the UI. userProfileDisplayPreferences: 20, userProfileDisplayPatterns: 15, userProfileDisplayWorkflows: 10, @@ -845,15 +860,6 @@ const CONFIG_TEMPLATE = `{ // Days before inactive items (all types) are eligible for removal "userProfileStaleDays": 2, - // Number of preferences shown in UI - "userProfileDisplayPreferences": 20, - - // Number of patterns shown in UI - "userProfileDisplayPatterns": 15, - - // Number of workflows shown in UI - "userProfileDisplayWorkflows": 10, - // Number of preferences injected into LLM conversation context // Keep this small — the strongest signals are enough; more dilute LLM attention "userProfileInjectPreferences": 5, @@ -1345,6 +1351,10 @@ function buildConfig(fileConfig: OpenCodeMemConfig) { : undefined, similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold, maxMemories: fileConfig.maxMemories ?? DEFAULTS.maxMemories, + /** + * @deprecated No effect. Use userProfileInjectPreferences, userProfileInjectPatterns, + * and userProfileInjectWorkflows to limit injected profile context. + */ maxProfileItems: fileConfig.maxProfileItems ?? DEFAULTS.maxProfileItems, injectProfile: fileConfig.injectProfile ?? DEFAULTS.injectProfile, containerTagPrefix: fileConfig.containerTagPrefix ?? DEFAULTS.containerTagPrefix, @@ -1407,10 +1417,19 @@ function buildConfig(fileConfig: OpenCodeMemConfig) { fileConfig.userProfileAnalysisInterval ?? DEFAULTS.userProfileAnalysisInterval, userProfileMaxContextBytes: fileConfig.userProfileMaxContextBytes ?? DEFAULTS.userProfileMaxContextBytes, + /** + * @deprecated No effect on the UI. Use userProfileInjectPreferences for LLM context only. + */ userProfileDisplayPreferences: fileConfig.userProfileDisplayPreferences ?? DEFAULTS.userProfileDisplayPreferences, + /** + * @deprecated No effect on the UI. Use userProfileInjectPatterns for LLM context only. + */ userProfileDisplayPatterns: fileConfig.userProfileDisplayPatterns ?? DEFAULTS.userProfileDisplayPatterns, + /** + * @deprecated No effect on the UI. Use userProfileInjectWorkflows for LLM context only. + */ userProfileDisplayWorkflows: fileConfig.userProfileDisplayWorkflows ?? DEFAULTS.userProfileDisplayWorkflows, userProfileInjectPreferences: diff --git a/tests/profile-config-deprecation.test.ts b/tests/profile-config-deprecation.test.ts new file mode 100644 index 00000000..555f1933 --- /dev/null +++ b/tests/profile-config-deprecation.test.ts @@ -0,0 +1,124 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { stripJsoncComments } from "../src/services/jsonc.js"; + +const tempDirs: string[] = []; +const legacyKeys = [ + "maxProfileItems", + "userProfileDisplayPreferences", + "userProfileDisplayPatterns", + "userProfileDisplayWorkflows", +]; +const injectionDefaults = { + userProfileInjectPreferences: 5, + userProfileInjectPatterns: 5, + userProfileInjectWorkflows: 3, +}; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +function loadProfileConfig(settings?: Record) { + const home = mkdtempSync(join(tmpdir(), "opencode-mem-profile-config-")); + tempDirs.push(home); + const configDir = join(home, ".config", "opencode"); + const configPath = join(configDir, "opencode-mem.jsonc"); + if (settings) { + mkdirSync(configDir, { recursive: true }); + writeFileSync(configPath, JSON.stringify(settings)); + } + const configUrl = new URL("../src/config.ts", import.meta.url).href; + const managerUrl = new URL( + "../src/services/user-profile/user-profile-manager.ts", + import.meta.url + ).href; + const contextUrl = new URL("../src/services/user-profile/profile-context.ts", import.meta.url) + .href; + const loggerUrl = new URL("../src/services/logger.ts", import.meta.url).href; + const scriptPath = join(home, "scenario.mjs"); + writeFileSync( + scriptPath, + `import { mock } from "bun:test"; +const { CONFIG } = await import(${JSON.stringify(configUrl)}); +const items = (kind) => Array.from({ length: 8 }, (_, i) => ({ + category: kind + i, description: kind + i, confidence: 0.9, + frequency: 1, lastSeen: Date.now(), +})); +mock.module(${JSON.stringify(managerUrl)}, () => ({ userProfileManager: { + getActiveProfile: async () => ({ profileData: JSON.stringify({ + preferences: items("preference"), patterns: items("pattern"), workflows: items("workflow"), + }) }), +} })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {} })); +const { getUserProfileContext } = await import(${JSON.stringify(contextUrl)}); +console.log(JSON.stringify({ config: CONFIG, context: await getUserProfileContext("fixture") }));` + ); + const result = Bun.spawnSync([process.execPath, "run", scriptPath], { + cwd: home, + env: { ...process.env, HOME: home, USERPROFILE: home }, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) throw new Error(result.stderr.toString()); + const output = JSON.parse(result.stdout.toString().trim().split("\n").at(-1)!) as { + config: Record; + context: string; + }; + return { ...output, file: readFileSync(configPath, "utf8") }; +} + +function expectInjectionCounts( + context: string, + preferences: number, + patterns: number, + workflows: number +) { + expect(context.match(/^- \[preference/gm) ?? []).toHaveLength(preferences); + expect(context.match(/^- \[pattern/gm) ?? []).toHaveLength(patterns); + expect(context.match(/^- workflow/gm) ?? []).toHaveLength(workflows); +} + +describe("deprecated profile configuration", () => { + it("omits no-op options from newly generated config and retains injection defaults", () => { + const { file, config, context } = loadProfileConfig(); + const template = JSON.parse(stripJsoncComments(file)); + for (const key of legacyKeys) expect(template).not.toHaveProperty(key); + expect(template).toMatchObject(injectionDefaults); + expect(config).toMatchObject(injectionDefaults); + expectInjectionCounts(context, 5, 5, 3); + }); + + it("omits no-op options from the example config and retains injection defaults", () => { + const example = JSON.parse( + stripJsoncComments( + readFileSync(new URL("../opencode-mem.example.jsonc", import.meta.url), "utf8") + ) + ); + for (const key of legacyKeys) expect(example).not.toHaveProperty(key); + expect(example).toMatchObject(injectionDefaults); + }); + + it("keeps existing legacy settings without rewriting files or changing injection", () => { + const settings = Object.fromEntries(legacyKeys.map((key) => [key, 1])); + const { config, file, context } = loadProfileConfig(settings); + expect(config).toMatchObject(settings); + expect(file).toBe(JSON.stringify(settings)); + expect(config).toMatchObject(injectionDefaults); + expectInjectionCounts(context, 5, 5, 3); + }); + + it("uses the live injection limits independently of legacy settings", () => { + const settings = { + ...Object.fromEntries(legacyKeys.map((key) => [key, 99])), + userProfileInjectPreferences: 2, + userProfileInjectPatterns: 3, + userProfileInjectWorkflows: 1, + }; + const { config, context } = loadProfileConfig(settings); + expect(config).toMatchObject(settings); + expectInjectionCounts(context, 2, 3, 1); + }); +});