From f9f8b4c18503cef0da01acc90a86266e0bb25cf5 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 10 Feb 2026 10:41:47 +0100 Subject: [PATCH] refactor(agent): Use agent name instead of ID for log directory paths Aligns agent-logs directory naming with agent-workdirs so both use the human-readable agent name, making filesystem correlation trivial. --- src/agent/cleanup-manager.ts | 14 +++++++------- src/agent/manager.ts | 8 ++++---- src/agent/process-manager.test.ts | 21 +++++++++++++-------- src/agent/process-manager.ts | 3 ++- src/trpc/routers/agent.ts | 2 +- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/agent/cleanup-manager.ts b/src/agent/cleanup-manager.ts index 6ed1551..ea07469 100644 --- a/src/agent/cleanup-manager.ts +++ b/src/agent/cleanup-manager.ts @@ -108,8 +108,8 @@ export class CleanupManager { /** * Remove log directory for an agent. */ - async removeAgentLogs(agentId: string): Promise { - const logDir = join(this.workspaceRoot, '.cw', 'agent-logs', agentId); + async removeAgentLogs(agentName: string): Promise { + const logDir = join(this.workspaceRoot, '.cw', 'agent-logs', agentName); await rm(logDir, { recursive: true, force: true }); } @@ -191,10 +191,10 @@ export class CleanupManager { } const agents = await this.repository.findAll(); - const knownIds = new Set(agents.map(a => a.id)); + const knownNames = new Set(agents.map(a => a.name)); for (const entry of entries) { - if (!knownIds.has(entry)) { + if (!knownNames.has(entry)) { log.info({ orphan: entry }, 'removing orphaned agent log dir'); try { await rm(join(logsPath, entry), { recursive: true, force: true }); @@ -251,8 +251,8 @@ export class CleanupManager { async archiveForDebug(alias: string, agentId: string): Promise { const agentWorkdir = this.getAgentWorkdir(alias); const debugWorkdir = join(this.workspaceRoot, '.cw', 'debug', 'workdirs', alias); - const logDir = join(this.workspaceRoot, '.cw', 'agent-logs', agentId); - const debugLogDir = join(this.workspaceRoot, '.cw', 'debug', 'agent-logs', agentId); + const logDir = join(this.workspaceRoot, '.cw', 'agent-logs', alias); + const debugLogDir = join(this.workspaceRoot, '.cw', 'debug', 'agent-logs', alias); try { if (existsSync(agentWorkdir)) { @@ -307,7 +307,7 @@ export class CleanupManager { } try { - await this.removeAgentLogs(agentId); + await this.removeAgentLogs(alias); } catch (err) { log.warn({ agentId, err: err instanceof Error ? err.message : String(err) }, 'auto-cleanup: failed to remove logs'); } diff --git a/src/agent/manager.ts b/src/agent/manager.ts index 935e5d6..a234907 100644 --- a/src/agent/manager.ts +++ b/src/agent/manager.ts @@ -300,7 +300,7 @@ export class MultiProviderAgentManager implements AgentManager { // 6. Spawn detached subprocess const { pid, outputFilePath, tailer } = this.processManager.spawnDetached( - agentId, command, args, cwd ?? agentCwd, processEnv, providerName, prompt, + agentId, alias, command, args, cwd ?? agentCwd, processEnv, providerName, prompt, (event) => this.outputHandler.handleStreamEvent(agentId, event, this.activeAgents.get(agentId), this.outputBuffers), ); @@ -451,7 +451,7 @@ export class MultiProviderAgentManager implements AgentManager { } const { pid, outputFilePath, tailer } = this.processManager.spawnDetached( - agentId, command, args, agentCwd, processEnv, provider.name, commitPrompt, + agentId, agent.name, command, args, agentCwd, processEnv, provider.name, commitPrompt, (event) => this.outputHandler.handleStreamEvent(agentId, event, this.activeAgents.get(agentId), this.outputBuffers), this.createLogChunkCallback(agentId, agent.name, commitSessionNumber), ); @@ -624,7 +624,7 @@ export class MultiProviderAgentManager implements AgentManager { } const { pid, outputFilePath, tailer } = this.processManager.spawnDetached( - agentId, command, args, agentCwd, processEnv, provider.name, prompt, + agentId, agent.name, command, args, agentCwd, processEnv, provider.name, prompt, (event) => this.outputHandler.handleStreamEvent(agentId, event, this.activeAgents.get(agentId), this.outputBuffers), this.createLogChunkCallback(agentId, agent.name, resumeSessionNumber), ); @@ -696,7 +696,7 @@ export class MultiProviderAgentManager implements AgentManager { try { await this.cleanupManager.removeAgentBranches(agent.name, agent.initiativeId); } catch (err) { log.warn({ agentId, err: err instanceof Error ? err.message : String(err) }, 'failed to remove branches'); } - try { await this.cleanupManager.removeAgentLogs(agentId); } + try { await this.cleanupManager.removeAgentLogs(agent.name); } catch (err) { log.warn({ agentId, err: err instanceof Error ? err.message : String(err) }, 'failed to remove logs'); } // 3b. Delete log chunks from DB diff --git a/src/agent/process-manager.test.ts b/src/agent/process-manager.test.ts index 15c9a9f..ca7b5f6 100644 --- a/src/agent/process-manager.test.ts +++ b/src/agent/process-manager.test.ts @@ -220,13 +220,14 @@ describe('ProcessManager', () => { it('validates cwd exists before spawn', () => { const agentId = 'agent-123'; + const agentName = 'test-agent'; const command = 'claude'; const args = ['--help']; const cwd = '/test/workspace/agent-workdirs/test-agent'; const env = { TEST_VAR: 'value' }; const providerName = 'claude'; - processManager.spawnDetached(agentId, command, args, cwd, env, providerName); + processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName); expect(mockExistsSync).toHaveBeenCalledWith(cwd); expect(mockSpawn).toHaveBeenCalledWith(command, args, { @@ -241,6 +242,7 @@ describe('ProcessManager', () => { mockExistsSync.mockReturnValue(false); const agentId = 'agent-123'; + const agentName = 'test-agent'; const command = 'claude'; const args = ['--help']; const cwd = '/nonexistent/path'; @@ -248,19 +250,20 @@ describe('ProcessManager', () => { const providerName = 'claude'; expect(() => { - processManager.spawnDetached(agentId, command, args, cwd, env, providerName); + processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName); }).toThrow('Agent working directory does not exist: /nonexistent/path'); }); it('passes correct cwd parameter to spawn', () => { const agentId = 'agent-123'; + const agentName = 'test-agent'; const command = 'claude'; const args = ['--help']; const cwd = '/test/workspace/agent-workdirs/test-agent'; const env = { CLAUDE_CONFIG_DIR: '/config' }; const providerName = 'claude'; - processManager.spawnDetached(agentId, command, args, cwd, env, providerName); + processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName); expect(mockSpawn).toHaveBeenCalledTimes(1); const spawnCall = mockSpawn.mock.calls[0]; @@ -279,27 +282,29 @@ describe('ProcessManager', () => { it('logs comprehensive spawn information', () => { const agentId = 'agent-123'; + const agentName = 'test-agent'; const command = 'claude'; const args = ['--json-schema', 'schema.json']; const cwd = '/test/workspace/agent-workdirs/test-agent'; const env = { CLAUDE_CONFIG_DIR: '/config' }; const providerName = 'claude'; - const result = processManager.spawnDetached(agentId, command, args, cwd, env, providerName); + const result = processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName); expect(result).toHaveProperty('pid', 12345); expect(result).toHaveProperty('outputFilePath'); expect(result).toHaveProperty('tailer'); - // Verify log directory creation + // Verify log directory creation uses agent name, not ID expect(mockMkdirSync).toHaveBeenCalledWith( - '/test/workspace/.cw/agent-logs/agent-123', + '/test/workspace/.cw/agent-logs/test-agent', { recursive: true } ); }); it('writes prompt file when provided', () => { const agentId = 'agent-123'; + const agentName = 'test-agent'; const command = 'claude'; const args = ['--help']; const cwd = '/test/workspace/agent-workdirs/test-agent'; @@ -307,10 +312,10 @@ describe('ProcessManager', () => { const providerName = 'claude'; const prompt = 'Test prompt'; - processManager.spawnDetached(agentId, command, args, cwd, env, providerName, prompt); + processManager.spawnDetached(agentId, agentName, command, args, cwd, env, providerName, prompt); expect(mockWriteFileSync).toHaveBeenCalledWith( - '/test/workspace/.cw/agent-logs/agent-123/PROMPT.md', + '/test/workspace/.cw/agent-logs/test-agent/PROMPT.md', 'Test prompt', 'utf-8' ); diff --git a/src/agent/process-manager.ts b/src/agent/process-manager.ts index f55a6c8..4c8df5e 100644 --- a/src/agent/process-manager.ts +++ b/src/agent/process-manager.ts @@ -236,6 +236,7 @@ export class ProcessManager { */ spawnDetached( agentId: string, + agentName: string, command: string, args: string[], cwd: string, @@ -271,7 +272,7 @@ export class ProcessManager { throw new Error(`Agent working directory does not exist: ${cwd}`); } - const logDir = join(this.workspaceRoot, '.cw', 'agent-logs', agentId); + const logDir = join(this.workspaceRoot, '.cw', 'agent-logs', agentName); mkdirSync(logDir, { recursive: true }); const outputFilePath = join(logDir, 'output.jsonl'); const stderrFilePath = join(logDir, 'stderr.log'); diff --git a/src/trpc/routers/agent.ts b/src/trpc/routers/agent.ts index b96aec9..712f634 100644 --- a/src/trpc/routers/agent.ts +++ b/src/trpc/routers/agent.ts @@ -201,7 +201,7 @@ export function agentProcedures(publicProcedure: ProcedureBuilder) { return ''; } - const outputFilePath = join(workspaceRoot, '.cw', 'agent-logs', agent.id, 'output.jsonl'); + const outputFilePath = join(workspaceRoot, '.cw', 'agent-logs', agent.name, 'output.jsonl'); try { const content = await readFile(outputFilePath, 'utf-8');