Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
12 changes: 0 additions & 12 deletions opencode-mem.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",

Expand Down
37 changes: 28 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
124 changes: 124 additions & 0 deletions tests/profile-config-deprecation.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>) {
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<string, number>;
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);
});
});
Loading