Skip to content
Open
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 @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
Expand Down
28 changes: 22 additions & 6 deletions src/services/auto-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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.`
);
}

Expand Down Expand Up @@ -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,
Expand Down
25 changes: 24 additions & 1 deletion src/services/user-prompt/user-prompt-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface UserPrompt {
userLearningCaptured: boolean;
linkedMemoryId: string | null;
capture_attempts: number;
providerId: string | null;
modelId: string | null;
}

export class UserPromptManager {
Expand All @@ -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
)
`);

Expand All @@ -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)");
Expand Down Expand Up @@ -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(`
Expand Down Expand Up @@ -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,
};
}
}
Expand Down