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/server/__tests__/cron-scheduler-launcher.test.ts b/src/server/__tests__/cron-scheduler-launcher.test.ts index 8e9eee04..87e09842 100644 --- a/src/server/__tests__/cron-scheduler-launcher.test.ts +++ b/src/server/__tests__/cron-scheduler-launcher.test.ts @@ -244,6 +244,7 @@ describe('cron scheduler launcher resolution', () => { process.env.CLAUDE_APP_ROOT = appRoot process.env.ANTHROPIC_BASE_URL = 'https://stale-parent.example' process.env.ANTHROPIC_MODEL = 'stale-parent-model' + process.env.CLAUDE_CODE_ENTRYPOINT = 'stale-parent-entrypoint' const provider = await new ProviderService().addProvider({ presetId: 'custom', diff --git a/src/server/services/cronScheduler.ts b/src/server/services/cronScheduler.ts index e7d9bf0f..a231c397 100644 --- a/src/server/services/cronScheduler.ts +++ b/src/server/services/cronScheduler.ts @@ -662,6 +662,7 @@ export class CronScheduler { return { ...cleanEnv, CLAUDE_CODE_ENABLE_TASKS: '1', + CLAUDE_CODE_ENTRYPOINT: 'sdk-cli', CALLER_DIR: workDir, PWD: workDir, CC_HAHA_SKIP_DOTENV: '1', 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() }