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 1/2] 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() } From e2b1ac9fd1125e76f22e4ab70d628094537c988a 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:48:06 +0800 Subject: [PATCH 2/2] fix: launch scheduled tasks with the sdk cli entrypoint Scheduled task subprocesses should normalize their child environment instead of inheriting a stale parent entrypoint. Setting the default task entrypoint keeps launcher tests and non-OAuth task execution on the SDK CLI path while still allowing the official OAuth environment overlay to provide its desktop-specific entrypoint. Constraint: Official OAuth tasks intentionally overlay provider-specific environment after the default child task environment is built Rejected: Preserve parent CLAUDE_CODE_ENTRYPOINT | stale parent values can leak into scheduled task launches Confidence: high Scope-risk: narrow Directive: Keep default task launcher environment deterministic before provider-specific overlays are applied Tested: bun test src/server/__tests__/cron-scheduler-launcher.test.ts Tested: bun run check:server Not-tested: Full verify still blocked by existing agent-utils global coverage baseline --- src/server/__tests__/cron-scheduler-launcher.test.ts | 1 + src/server/services/cronScheduler.ts | 1 + 2 files changed, 2 insertions(+) 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',