From 571d7710252e23f61428047e0803de3a5c610bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 9 May 2026 21:47:58 +0800 Subject: [PATCH] fix: keep agent listings fresh after file edits Agent definitions are cached across command invocations, so edits made through the shared file helper must invalidate both the agent definition cache and the underlying markdown file cache. This keeps the TUI creation path and /agents command output consistent after creating, updating, or deleting custom agents. Constraint: The TUI agent wizard and /agents command share cached agent definition loaders Rejected: Clear only getAgentDefinitionsWithOverrides | the nested markdown file memoization can still return stale files Confidence: high Scope-risk: narrow Directive: Any future agent file mutation path must call clearAgentDefinitionsCache after a successful write or delete Tested: bun test src/tools/AgentTool/loadAgentsDir.cache.test.ts Tested: bun run check:server Not-tested: Full verify still blocked by existing agent-utils global coverage baseline --- src/components/agents/agentFileUtils.ts | 4 + .../AgentTool/loadAgentsDir.cache.test.ts | 98 +++++++++++++++++++ src/tools/AgentTool/loadAgentsDir.ts | 1 + 3 files changed, 103 insertions(+) create mode 100644 src/tools/AgentTool/loadAgentsDir.cache.test.ts diff --git a/src/components/agents/agentFileUtils.ts b/src/components/agents/agentFileUtils.ts index 87e4e4b0..ba31d069 100644 --- a/src/components/agents/agentFileUtils.ts +++ b/src/components/agents/agentFileUtils.ts @@ -4,6 +4,7 @@ import type { SettingSource } from 'src/utils/settings/constants.js' import { getManagedFilePath } from 'src/utils/settings/managedPath.js' import type { AgentMemoryScope } from '../../tools/AgentTool/agentMemory.js' import { + clearAgentDefinitionsCache, type AgentDefinition, isBuiltInAgent, isPluginAgent, @@ -200,6 +201,7 @@ export async function saveAgentToFile( } throw e } + clearAgentDefinitionsCache() } /** @@ -233,6 +235,7 @@ export async function updateAgentFile( ) await writeFileAndFlush(filePath, content) + clearAgentDefinitionsCache() } /** @@ -255,6 +258,7 @@ export async function deleteAgentFromFile( throw e } } + clearAgentDefinitionsCache() } async function writeFileAndFlush( diff --git a/src/tools/AgentTool/loadAgentsDir.cache.test.ts b/src/tools/AgentTool/loadAgentsDir.cache.test.ts new file mode 100644 index 00000000..989dcc5c --- /dev/null +++ b/src/tools/AgentTool/loadAgentsDir.cache.test.ts @@ -0,0 +1,98 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { getCwdState, setCwdState } from '../../bootstrap/state.js' +import { agentsHandler } from '../../cli/handlers/agents.js' +import { saveAgentToFile } from '../../components/agents/agentFileUtils.js' +import { + clearAgentDefinitionsCache, + getAgentDefinitionsWithOverrides, +} from './loadAgentsDir.js' + +let tmpHome: string +let originalHome: string | undefined +let originalUserProfile: string | undefined +let originalClaudeConfigDir: string | undefined +let originalCwdState: string + +describe('agent definition cache invalidation', () => { + beforeEach(async () => { + tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-def-cache-')) + originalHome = process.env.HOME + originalUserProfile = process.env.USERPROFILE + originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR + originalCwdState = getCwdState() + + process.env.HOME = tmpHome + process.env.USERPROFILE = tmpHome + process.env.CLAUDE_CONFIG_DIR = path.join(tmpHome, '.claude') + clearAgentDefinitionsCache() + }) + + afterEach(async () => { + if (originalHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = originalHome + } + + if (originalUserProfile === undefined) { + delete process.env.USERPROFILE + } else { + process.env.USERPROFILE = originalUserProfile + } + + if (originalClaudeConfigDir === undefined) { + delete process.env.CLAUDE_CONFIG_DIR + } else { + process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir + } + + setCwdState(originalCwdState) + clearAgentDefinitionsCache() + await fs.rm(tmpHome, { recursive: true, force: true }) + }) + + test('shows a newly-created project agent in the /agents output after an initial cached read', async () => { + const projectRoot = path.join(tmpHome, 'project') + await fs.mkdir(projectRoot, { recursive: true }) + setCwdState(projectRoot) + + const agentType = 'cache-created-agent' + const before = await getAgentDefinitionsWithOverrides(projectRoot) + + expect(before.allAgents.some(agent => agent.agentType === agentType)).toBe(false) + + await saveAgentToFile( + 'projectSettings', + agentType, + 'Use this agent to verify cache invalidation.', + undefined, + 'You verify cache invalidation.', + true, + ) + + const after = await getAgentDefinitionsWithOverrides(projectRoot) + + expect(after.allAgents).toContainEqual( + expect.objectContaining({ + agentType, + source: 'projectSettings', + }), + ) + + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.map(String).join(' ')) + } + try { + await agentsHandler() + } finally { + console.log = originalLog + } + + expect(logs.join('\n')).toContain(agentType) + }) +}) diff --git a/src/tools/AgentTool/loadAgentsDir.ts b/src/tools/AgentTool/loadAgentsDir.ts index cb4dc35e..edff88b7 100644 --- a/src/tools/AgentTool/loadAgentsDir.ts +++ b/src/tools/AgentTool/loadAgentsDir.ts @@ -394,6 +394,7 @@ export const getAgentDefinitionsWithOverrides = memoize( export function clearAgentDefinitionsCache(): void { getAgentDefinitionsWithOverrides.cache.clear?.() + loadMarkdownFilesForSubdir.cache.clear?.() clearPluginAgentCache() }