From 3107b77fe2dc96fd73f1962c3e4779d85e7b5575 Mon Sep 17 00:00:00 2001 From: Cameron Beeley Date: Thu, 2 Jul 2026 10:23:25 +0100 Subject: [PATCH] feat: support opencodeModel "inherit" to capture with the session model When opencodeModel is set to "inherit", auto-capture resolves the provider/model that opencode actually used for the captured prompt (recorded per message via the chat.params hook and stored on the user_prompts row), instead of a pinned model id. Pinned ids can go stale when providers rename models, silently breaking capture; inherit also means multi-model users capture each conversation with the model that produced it. --- README.md | 2 ++ src/index.ts | 10 +++++++ src/services/auto-capture.ts | 28 +++++++++++++++---- .../user-prompt/user-prompt-manager.ts | 25 ++++++++++++++++- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a3f4b2f..5ad66e7 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ The plugin issues structured-output requests to opencode's session API instead o Supported providers: any provider listed by `opencode providers list` (e.g. `anthropic`, `openai`, `github-copilot`, ...). +**Follow the session model:** set `"opencodeModel": "inherit"` to capture each prompt with the exact provider/model opencode used to answer it (recorded per message via the `chat.params` hook). Useful if you switch models often or run several local backends — captures always use the model that produced the conversation, and pinned ids can never go stale. `opencodeProvider` still needs to be set (it is only used when a prompt predates the upgrade and has no recorded model, in which case capture fails and retries later). + **Fallback:** Manual API configuration (if not using opencodeProvider): ```jsonc diff --git a/src/index.ts b/src/index.ts index d1419f4..70738d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -249,6 +249,16 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => { } }, + "chat.params": async (input) => { + if (!isConfigured() || CONFIG.opencodeModel !== "inherit") return; + + try { + userPromptManager.setPromptModel(input.message.id, input.model.providerID, input.model.id); + } catch (error) { + log("chat.params: ERROR", { error: String(error) }); + } + }, + tool: { memory: tool({ description: `Manage and query project memory (MATCH USER LANGUAGE: ${getLanguageName(CONFIG.autoCaptureLanguage || "en")}). Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Search/list scope: project or all-projects.`, diff --git a/src/services/auto-capture.ts b/src/services/auto-capture.ts index 647ef47..c061f3f 100644 --- a/src/services/auto-capture.ts +++ b/src/services/auto-capture.ts @@ -79,7 +79,7 @@ export async function performAutoCapture( latestMemory ); - const summaryResult = await generateSummary(context, sessionID, prompt.content); + const summaryResult = await generateSummary(context, sessionID, prompt.content, prompt); if (!summaryResult || summaryResult.type === "skip") { userPromptManager.deletePrompt(prompt.id); @@ -294,7 +294,8 @@ function buildMarkdownContext( async function generateSummary( context: string, sessionID: string, - userPrompt: string + userPrompt: string, + prompt?: { providerId: string | null; modelId: string | null } ): Promise<{ summary: string; type: string; tags: string[] } | null> { // Opencode provider path (when opencodeProvider + opencodeModel configured) if (CONFIG.opencodeProvider && CONFIG.opencodeModel) { @@ -305,9 +306,24 @@ async function generateSummary( const { isProviderConnected, getV2Client, generateStructuredOutput } = await import("./ai/opencode-provider.js"); - if (!isProviderConnected(CONFIG.opencodeProvider)) { + // "inherit" resolves to the model opencode used for the captured prompt + // (recorded by the chat.params hook); fall back is a hard error so the + // prompt is retried once a model has been recorded. + let providerID = CONFIG.opencodeProvider; + let modelID = CONFIG.opencodeModel; + if (modelID === "inherit") { + if (!prompt?.providerId || !prompt?.modelId) { + throw new Error( + "opencode-mem: opencodeModel is 'inherit' but no session model was recorded for this prompt" + ); + } + providerID = prompt.providerId; + modelID = prompt.modelId; + } + + if (!isProviderConnected(providerID)) { throw new Error( - `opencode provider '${CONFIG.opencodeProvider}' is not connected. Check your opencode provider configuration.` + `opencode provider '${providerID}' is not connected. Check your opencode provider configuration.` ); } @@ -358,8 +374,8 @@ Analyze this conversation. If it contains technical work (code, bugs, features, const result = await generateStructuredOutput({ client: v2Client, - providerID: CONFIG.opencodeProvider, - modelID: CONFIG.opencodeModel, + providerID, + modelID, systemPrompt, userPrompt: aiPrompt, schema, diff --git a/src/services/user-prompt/user-prompt-manager.ts b/src/services/user-prompt/user-prompt-manager.ts index 5f2eed8..07fa73c 100644 --- a/src/services/user-prompt/user-prompt-manager.ts +++ b/src/services/user-prompt/user-prompt-manager.ts @@ -19,6 +19,8 @@ export interface UserPrompt { userLearningCaptured: boolean; linkedMemoryId: string | null; capture_attempts: number; + providerId: string | null; + modelId: string | null; } export class UserPromptManager { @@ -43,7 +45,9 @@ export class UserPromptManager { captured INTEGER DEFAULT 0, user_learning_captured BOOLEAN DEFAULT 0, linked_memory_id TEXT, - capture_attempts INTEGER DEFAULT 0 + capture_attempts INTEGER DEFAULT 0, + provider_id TEXT, + model_id TEXT ) `); @@ -55,6 +59,16 @@ export class UserPromptManager { } } + for (const column of ["provider_id TEXT", "model_id TEXT"]) { + try { + this.db.run(`ALTER TABLE user_prompts ADD COLUMN ${column}`); + } catch (error: any) { + if (!error.message.includes("duplicate column name")) { + console.warn(`Failed to add ${column.split(" ")[0]} column:`, error.message); + } + } + } + this.db.run("UPDATE user_prompts SET captured = 0 WHERE captured = 2"); this.db.run("CREATE INDEX IF NOT EXISTS idx_user_prompts_session ON user_prompts(session_id)"); @@ -86,6 +100,13 @@ export class UserPromptManager { return id; } + setPromptModel(messageId: string, providerId: string, modelId: string): void { + const stmt = this.db.prepare( + `UPDATE user_prompts SET provider_id = ?, model_id = ? WHERE message_id = ?` + ); + stmt.run(providerId, modelId, messageId); + } + getLastUncapturedPrompt(sessionId: string): UserPrompt | null { const maxRetries = CONFIG.autoCaptureMaxRetries ?? 3; const stmt = this.db.prepare(` @@ -292,6 +313,8 @@ export class UserPromptManager { userLearningCaptured: row.user_learning_captured === 1, linkedMemoryId: row.linked_memory_id, capture_attempts: row.capture_attempts || 0, + providerId: row.provider_id ?? null, + modelId: row.model_id ?? null, }; } }