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
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { tool } from "@opencode-ai/plugin"
import { parse, resolve } from "path"
import { buildMemorySystemPrompt } from "./prompt.js"
import { formatRecalledMemories, recallSelectedMemories, type RecalledMemory } from "./recall.js"
import { assertSupportedRecallSelectorClient, selectRelevantMemoryFilenames, type SessionClient } from "./recallSelector.js"
import { isSupportedRecallSelectorClient, selectRelevantMemoryFilenames, type SessionClient } from "./recallSelector.js"
import { scanMemoryFiles, type MemoryHeader } from "./memoryScan.js"
import {
saveMemory,
Expand Down Expand Up @@ -197,7 +197,7 @@ function startRecallPrefetch(input: {
}): RecallPrefetch | undefined {
if (!input.client || !isUsefulRecallQuery(input.query)) return undefined

assertSupportedRecallSelectorClient(input.client)
if (!isSupportedRecallSelectorClient(input.client)) return undefined

const memoryDir = getMemoryDir(input.worktree)
const headers = scanMemoryFiles(memoryDir).filter((header) => !input.alreadySurfaced.has(alreadySurfacedKey(header)))
Expand Down
30 changes: 18 additions & 12 deletions src/recallSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SELECT_MEMORIES_FORMAT = {
} as const

export const UNSUPPORTED_RECALL_SELECTOR_CLIENT_MESSAGE =
"opencode-claude-memory LLM recall requires an OpenCode SDK with structured output session.prompt support. Please upgrade OpenCode/@opencode-ai/plugin."
"opencode-claude-memory LLM recall requires an OpenCode SDK session client with create/prompt/delete support."

export type SessionClient = {
session?: {
Expand Down Expand Up @@ -93,12 +93,9 @@ function extractSelectedMemories(response: unknown): string[] {
export function isSupportedRecallSelectorClient(client: SessionClient | undefined): boolean {
const session = client?.session
return Boolean(
session?.create &&
session?.prompt &&
session?.delete &&
session.create.length >= 2 &&
session.prompt.length >= 2 &&
session.delete.length >= 2,
typeof session?.create === "function" &&
typeof session.prompt === "function" &&
typeof session.delete === "function",
)
}

Expand All @@ -116,9 +113,11 @@ async function createSelectorSession(
if (!client.session?.create) return undefined

const response = await client.session.create({
directory,
parentID: parentSessionID,
title: "opencode-memory recall selector",
body: {
parentID: parentSessionID,
title: "opencode-memory recall selector",
},
query: { directory },
})

return extractSessionID(response)
Expand All @@ -143,7 +142,11 @@ async function promptSelectorSession(
parts: [{ type: "text", text: content }],
}

return client.session.prompt({ sessionID, directory, ...body })
return client.session.prompt({
path: { id: sessionID },
query: { directory },
body,
})
}

async function deleteSelectorSession(
Expand All @@ -154,7 +157,10 @@ async function deleteSelectorSession(
if (!client.session?.delete) return

try {
await client.session.delete({ sessionID, directory })
await client.session.delete({
path: { id: sessionID },
query: { directory },
})
} catch {
// Best-effort cleanup. A failed selector deletion should not affect recall.
}
Expand Down
60 changes: 34 additions & 26 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ function makeCompletedSelectorClient(selections: string[][]) {
let sessionCount = 0
return {
session: {
async create(_parameters?: unknown, _requestOptions?: unknown) {
async create(_parameters?: unknown) {
sessionCount += 1
return { data: { id: `selector-session-${sessionCount}` } }
},
async prompt(_parameters: unknown, _requestOptions?: unknown) {
async prompt(_parameters: unknown) {
const selected = selections[promptCount] ?? selections.at(-1) ?? []
promptCount += 1
return {
Expand All @@ -85,7 +85,7 @@ function makeCompletedSelectorClient(selections: string[][]) {
},
}
},
async delete(_parameters: unknown, _requestOptions?: unknown) {
async delete(_parameters: unknown) {
return { data: true }
},
},
Expand Down Expand Up @@ -261,16 +261,22 @@ describe("MemoryPlugin system transform", () => {
})

describe("MemoryPlugin LLM recall prefetch", () => {
test("fails fast when recall prefetch receives an unsupported session client", async () => {
test("prefetches without failing user message transform when runtime client uses the default SDK shape", async () => {
const repo = makeTempGitRepo()
saveMemory(repo, "testing_pref_unsupported", "Testing Preference", "Database integration test guidance", "feedback", "Use real databases in integration tests.")
let createCalled = false
const client = {
session: {
async create(_parameters?: unknown) {
createCalled = true
return { data: { id: "selector-session" } }
},
async prompt(_parameters: unknown) {
return { data: { parts: [] } }
return {
data: {
parts: [{ text: JSON.stringify({ selected_memories: ["testing_pref_unsupported.md"] }) }],
},
}
},
async delete(_parameters: unknown) {
return { data: true }
Expand All @@ -280,26 +286,28 @@ describe("MemoryPlugin LLM recall prefetch", () => {

const plugin = await MemoryPlugin({ worktree: repo, directory: repo, client } as never)
const messagesTransform = plugin["experimental.chat.messages.transform"] as unknown as MessagesTransform
const transform = plugin["experimental.chat.system.transform"] as unknown as SystemTransform

let error: unknown
try {
await messagesTransform(
{},
{
messages: [
{
info: { role: "user", sessionID: "ses_unsupported_prefetch" },
parts: [{ type: "text", text: "How should we test database changes?" }],
},
],
},
)
} catch (caught) {
error = caught
}
await messagesTransform(
{},
{
messages: [
{
info: { role: "user", sessionID: "ses_unsupported_prefetch" },
parts: [{ type: "text", text: "How should we test database changes?" }],
},
],
},
)
await flushPromises()

const output = { system: [] as string[] }
await transform({ model: "test-model", sessionID: "ses_unsupported_prefetch" }, output)

expect(error).toBeInstanceOf(Error)
expect((error as Error).message).toContain("requires an OpenCode SDK with structured output session.prompt support")
expect(createCalled).toBe(true)
expect(output.system[0]).toContain("## MEMORY.md")
expect(output.system[0]).toContain("## Recalled Memories")
expect(output.system[0]).toContain("Testing Preference")
})

test("does not wait for an unfinished selector and injects completed recall on the next loop", async () => {
Expand All @@ -309,13 +317,13 @@ describe("MemoryPlugin LLM recall prefetch", () => {
const promptResult = deferred<unknown>()
const client = {
session: {
async create(_parameters?: unknown, _requestOptions?: unknown) {
async create(_parameters?: unknown) {
return { data: { id: "selector-session" } }
},
async prompt(_parameters: unknown, _requestOptions?: unknown) {
async prompt(_parameters: unknown) {
return promptResult.promise
},
async delete(_parameters: unknown, _requestOptions?: unknown) {
async delete(_parameters: unknown) {
return { data: true }
},
},
Expand Down
8 changes: 4 additions & 4 deletions test/memory-recall-prefetch-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async function flushPromises(): Promise<void> {
}

function selectorPromptText(options: unknown): string {
const parts = (options as { parts?: Array<{ text?: string }> }).parts
const parts = (options as { body?: { parts?: Array<{ text?: string }> } }).body?.parts
return parts?.[0]?.text ?? ""
}

Expand All @@ -82,11 +82,11 @@ function makeManifestSelectingClient() {
calls,
client: {
session: {
async create(_parameters?: unknown, _requestOptions?: unknown) {
async create(_parameters?: unknown) {
calls.create += 1
return { data: { id: `selector-session-${calls.create}` } }
},
async prompt(options: unknown, _requestOptions?: unknown) {
async prompt(options: unknown) {
calls.prompt += 1
calls.promptText = selectorPromptText(options)
const selected = calls.promptText.includes("database_rules.md") ? ["database_rules.md"] : []
Expand All @@ -102,7 +102,7 @@ function makeManifestSelectingClient() {
},
}
},
async delete(_parameters: unknown, _requestOptions?: unknown) {
async delete(_parameters: unknown) {
calls.delete += 1
return { data: true }
},
Expand Down
Loading
Loading