Skip to content

Commit 45dcd8a

Browse files
authored
fix: use directory for root worktree memory path (#21)
1 parent 14c25a9 commit 45dcd8a

2 files changed

Lines changed: 52 additions & 8 deletions

File tree

src/index.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Plugin } from "@opencode-ai/plugin"
22
import { tool } from "@opencode-ai/plugin"
3+
import { parse, resolve } from "path"
34
import { buildMemorySystemPrompt } from "./prompt.js"
45
import { formatRecalledMemories, recallSelectedMemories, type RecalledMemory } from "./recall.js"
56
import { assertSupportedRecallSelectorClient, selectRelevantMemoryFilenames, type SessionClient } from "./recallSelector.js"
@@ -154,6 +155,16 @@ function getRecallModel(): { providerID: string; modelID: string } | undefined {
154155
}
155156
}
156157

158+
function isRootPath(path: string): boolean {
159+
const resolved = resolve(path)
160+
return resolved === parse(resolved).root
161+
}
162+
163+
function resolveMemoryRoot(worktree: string, directory: string): string {
164+
if (isRootPath(worktree) && !isRootPath(directory)) return directory
165+
return worktree
166+
}
167+
157168
function isUsefulRecallQuery(query: string | undefined): query is string {
158169
const trimmed = query?.trim()
159170
if (!trimmed) return false
@@ -283,7 +294,8 @@ function getCallID(ctx: unknown): string | undefined {
283294

284295
export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
285296
directory ??= worktree
286-
getMemoryDir(worktree)
297+
const memoryRoot = resolveMemoryRoot(worktree, directory)
298+
getMemoryDir(memoryRoot)
287299

288300
return {
289301
config: async (config) => {
@@ -348,7 +360,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
348360
: startRecallPrefetch({
349361
client: client as unknown as SessionClient,
350362
directory,
351-
worktree,
363+
worktree: memoryRoot,
352364
parentSessionID: sessionID,
353365
turnID,
354366
query,
@@ -389,7 +401,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
389401
const recalled = ignoreMemoryContext ? [] : consumeRecallPrefetch(ctx)
390402

391403
const recalledSection = formatRecalledMemories(recalled)
392-
const memoryPrompt = buildMemorySystemPrompt(worktree, recalledSection, {
404+
const memoryPrompt = buildMemorySystemPrompt(memoryRoot, recalledSection, {
393405
includeIndex: !ignoreMemoryContext,
394406
})
395407
output.system.push(memoryPrompt)
@@ -426,7 +438,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
426438
),
427439
},
428440
async execute(args, _ctx) {
429-
const filePath = saveMemory(worktree, args.file_name, args.name, args.description, args.type, args.content)
441+
const filePath = saveMemory(memoryRoot, args.file_name, args.name, args.description, args.type, args.content)
430442
return `Memory saved to ${filePath}`
431443
},
432444
}),
@@ -437,7 +449,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
437449
file_name: tool.schema.string().describe("File name of the memory to delete (with or without .md extension)"),
438450
},
439451
async execute(args, _ctx) {
440-
const deleted = deleteMemory(worktree, args.file_name)
452+
const deleted = deleteMemory(memoryRoot, args.file_name)
441453
return deleted ? `Memory "${args.file_name}" deleted.` : `Memory "${args.file_name}" not found.`
442454
},
443455
}),
@@ -449,7 +461,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
449461
"or when you need to recall what's been stored.",
450462
args: {},
451463
async execute(_args, ctx) {
452-
const entries = listMemories(worktree)
464+
const entries = listMemories(memoryRoot)
453465
const callID = getCallID(ctx)
454466
if (callID) memoryListCountByCallID.set(callID, entries.length)
455467
if (entries.length === 0) {
@@ -470,7 +482,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
470482
query: tool.schema.string().describe("Search query — searches across name, description, and content"),
471483
},
472484
async execute(args, ctx) {
473-
const results = searchMemories(worktree, args.query)
485+
const results = searchMemories(memoryRoot, args.query)
474486
const callID = getCallID(ctx)
475487
if (callID) memorySearchCountByCallID.set(callID, results.length)
476488
if (results.length === 0) {
@@ -489,7 +501,7 @@ export const MemoryPlugin: Plugin = async ({ worktree, directory, client }) => {
489501
file_name: tool.schema.string().describe("File name of the memory to read (with or without .md extension)"),
490502
},
491503
async execute(args, _ctx) {
492-
const entry = readMemory(worktree, args.file_name)
504+
const entry = readMemory(memoryRoot, args.file_name)
493505
if (!entry) {
494506
return `Memory "${args.file_name}" not found.`
495507
}

test/index.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { tmpdir } from "os"
44
import { join } from "path"
55
import { MemoryPlugin } from "../src/index.js"
66
import { saveMemory } from "../src/memory.js"
7+
import { getMemoryDir } from "../src/paths.js"
78

89
const tempDirs: string[] = []
910

@@ -14,6 +15,12 @@ function makeTempGitRepo(): string {
1415
return root
1516
}
1617

18+
function makeTempProject(): string {
19+
const root = mkdtempSync(join(tmpdir(), "index-test-project-"))
20+
tempDirs.push(root)
21+
return root
22+
}
23+
1724
afterEach(() => {
1825
while (tempDirs.length > 0) {
1926
const dir = tempDirs.pop()
@@ -86,6 +93,31 @@ function makeCompletedSelectorClient(selections: string[][]) {
8693
}
8794

8895
describe("MemoryPlugin system transform", () => {
96+
test("uses directory as memory root when OpenCode reports root worktree", async () => {
97+
const project = makeTempProject()
98+
const configDir = mkdtempSync(join(tmpdir(), "index-test-claude-"))
99+
tempDirs.push(configDir)
100+
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
101+
process.env.CLAUDE_CONFIG_DIR = configDir
102+
103+
try {
104+
const expectedMemoryDir = getMemoryDir(project)
105+
const rootMemoryDir = getMemoryDir("/")
106+
const plugin = await MemoryPlugin({ worktree: "/", directory: project } as never)
107+
const transform = plugin["experimental.chat.system.transform"] as unknown as SystemTransform
108+
const output = { system: [] as string[] }
109+
110+
await transform({ model: "test-model", sessionID: "ses_root_worktree" }, output)
111+
112+
expect(output.system).toHaveLength(1)
113+
expect(output.system[0]).toContain(expectedMemoryDir)
114+
expect(output.system[0]).not.toContain(rootMemoryDir)
115+
} finally {
116+
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR
117+
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir
118+
}
119+
})
120+
89121
test("suppresses memory context when user explicitly asks to ignore memory", async () => {
90122
const repo = makeTempGitRepo()
91123
saveMemory(repo, "hidden", "Hidden Memory", "Should be ignored", "user", "Secret context")

0 commit comments

Comments
 (0)