diff --git a/src/tools/skill/session-skill-cache.ts b/src/tools/skill/session-skill-cache.ts new file mode 100644 index 000000000..040979ddb --- /dev/null +++ b/src/tools/skill/session-skill-cache.ts @@ -0,0 +1,10 @@ +const seenSessionIDs = new Set() + +export function shouldInvalidateSkillCacheForSession(sessionID?: string): boolean { + if (!sessionID || seenSessionIDs.has(sessionID)) { + return false + } + + seenSessionIDs.add(sessionID) + return true +} diff --git a/src/tools/skill/tools.factory.test.ts b/src/tools/skill/tools.factory.test.ts index e52f0abb5..5b9a5ba30 100644 --- a/src/tools/skill/tools.factory.test.ts +++ b/src/tools/skill/tools.factory.test.ts @@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:te import type { ToolContext } from "@opencode-ai/plugin/tool" import type { LoadedSkill } from "../../features/opencode-skill-loader/types" import * as skillContent from "../../features/opencode-skill-loader/skill-content" -import * as commandDiscovery from "../slashcommand/command-discovery" const discoverCommandsSync = mock(() => []) @@ -94,19 +93,21 @@ describe("createSkillTool", () => { expect(getAllSkills.mock.calls.length).toBe(baselineGetAllSkillsCalls + 1) }) - it("does not clear the shared skill cache during description or execute refresh", async () => { + it("clears the shared skill cache once on first execute in a session", async () => { // given const baselineClearSkillCacheCalls = clearSkillCache.mock.calls.length + const sessionContext = createMockContext("session-clear-once") // when const { createSkillTool } = await import("./tools") const skillTool = createSkillTool({}) void skillTool.description await flushMicrotasks() - await skillTool.execute({ name: "lazy-skill" }, mockContext) + await skillTool.execute({ name: "lazy-skill" }, sessionContext) + await skillTool.execute({ name: "lazy-skill" }, sessionContext) // then - expect(clearSkillCache.mock.calls.length).toBe(baselineClearSkillCacheCalls) + expect(clearSkillCache.mock.calls.length).toBe(baselineClearSkillCacheCalls + 1) }) it("clears the skill discovery cache once per session", async () => { diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index f60d06492..d49936f95 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -2,9 +2,10 @@ import { dirname } from "node:path" import { tool, type ToolDefinition } from "@opencode-ai/plugin" import type { ToolContext } from "@opencode-ai/plugin/tool" import { TOOL_DESCRIPTION_PREFIX } from "./constants" +import { shouldInvalidateSkillCacheForSession } from "./session-skill-cache" import type { SkillArgs, SkillLoadOptions } from "./types" import type { LoadedSkill } from "../../features/opencode-skill-loader" -import { getAllSkills } from "../../features/opencode-skill-loader/skill-content" +import { clearSkillCache, getAllSkills } from "../../features/opencode-skill-loader/skill-content" import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content" import { discoverCommandsSync } from "../slashcommand/command-discovery" import type { CommandInfo } from "../slashcommand/types" @@ -27,7 +28,11 @@ import { export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition { let cachedDescription: string | null = null - const getSkills = async (): Promise => { + const getSkills = async (context?: ToolContext): Promise => { + if (shouldInvalidateSkillCacheForSession(context?.sessionID)) { + clearSkillCache() + } + const discovered = (await getAllSkills({ disabledSkills: options?.disabledSkills, browserProvider: options?.browserProvider, @@ -108,7 +113,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition .describe("Optional arguments or context for command invocation. Example: name='publish', user_message='patch'"), }, async execute(args: SkillArgs, ctx?: ToolContext) { - const skills = await getSkills() + const skills = await getSkills(ctx) const commands = getCommands() cachedDescription = formatCombinedDescription(skills.map(loadedSkillToInfo), commands)