diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index d413e80f69..723439a3fd 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -41,6 +41,32 @@ async function resolveRelative(instruction: string): Promise { } export namespace InstructionPrompt { + const state = Instance.state(() => { + return { + claims: new Map>(), + } + }) + + function isClaimed(messageID: string, filepath: string) { + const claimed = state().claims.get(messageID) + if (!claimed) return false + return claimed.has(filepath) + } + + function claim(messageID: string, filepath: string) { + const current = state() + let claimed = current.claims.get(messageID) + if (!claimed) { + claimed = new Set() + current.claims.set(messageID, claimed) + } + claimed.add(filepath) + } + + export function clear(messageID: string) { + state().claims.delete(messageID) + } + export async function systemPaths() { const config = await Config.get() const paths = new Set() @@ -137,7 +163,7 @@ export namespace InstructionPrompt { } } - export async function resolve(messages: MessageV2.WithParts[], filepath: string) { + export async function resolve(messages: MessageV2.WithParts[], filepath: string, messageID: string) { const system = await systemPaths() const already = loaded(messages) const results: { filepath: string; content: string }[] = [] @@ -147,7 +173,8 @@ export namespace InstructionPrompt { while (current.startsWith(root)) { const found = await find(current) - if (found && !system.has(found) && !already.has(found)) { + if (found && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) { + claim(messageID, found) const content = await Bun.file(found) .text() .catch(() => undefined) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 23ca473541..94eabdef7f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -551,6 +551,7 @@ export namespace SessionPrompt { model, abort, }) + using _ = defer(() => InstructionPrompt.clear(processor.message.id)) // Check if user explicitly invoked an agent via @ in this turn const lastUserMsg = msgs.findLast((m) => m.info.role === "user") @@ -839,6 +840,7 @@ export namespace SessionPrompt { system: input.system, variant: input.variant, } + using _ = defer(() => InstructionPrompt.clear(info.id)) const parts = await Promise.all( input.parts.map(async (part): Promise => { diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 746e0b173c..f230cdf44c 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -60,7 +60,7 @@ export const ReadTool = Tool.define("read", { throw new Error(`File not found: ${filepath}`) } - const instructions = await InstructionPrompt.resolve(ctx.messages, filepath) + const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) const isImage = diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index 2c44a266e0..67719fa339 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -18,7 +18,7 @@ describe("InstructionPrompt.resolve", () => { const system = await InstructionPrompt.systemPaths() expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true) - const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts")) + const results = await InstructionPrompt.resolve([], path.join(tmp.path, "src", "file.ts"), "test-message-1") expect(results).toEqual([]) }, }) @@ -37,7 +37,11 @@ describe("InstructionPrompt.resolve", () => { const system = await InstructionPrompt.systemPaths() expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false) - const results = await InstructionPrompt.resolve([], path.join(tmp.path, "subdir", "nested", "file.ts")) + const results = await InstructionPrompt.resolve( + [], + path.join(tmp.path, "subdir", "nested", "file.ts"), + "test-message-2", + ) expect(results.length).toBe(1) expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md")) },