Agent Reflection: Summarize Sessions with Local Agents#9
Agent Reflection: Summarize Sessions with Local Agents#9alexchaomander merged 2 commits intomainfrom
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request introduces the cloudcode summary command, which enables users to generate architectural and session summaries using local AI agents. The implementation involves fetching session transcripts, creating temporary prompt files, and managing agent execution within tmux sessions. Feedback highlights several areas for improvement: replacing brittle fixed-duration timeouts with polling for agent readiness, resolving a race condition in temporary file cleanup by using synchronous file operations, improving type safety by defining interfaces instead of using any, and refactoring the large command handler into smaller, more manageable functions.
| ); | ||
|
|
||
| // Wait a moment for the agent to boot up | ||
| await new Promise(resolve => setTimeout(resolve, 1500)); |
There was a problem hiding this comment.
Using a fixed-duration setTimeout to wait for the agent to boot is brittle. The agent might take longer to start under certain conditions, leading to race conditions where the next command is sent too early. A more robust approach is to poll the tmux pane until a prompt or some initial output appears, indicating the agent is ready. This ensures the script waits for as long as necessary, but no longer.
| try { | ||
| // fs.unlinkSync(tmpPromptPath); | ||
| // Leaving it might be useful if they want to see what was sent, but better to clean up. | ||
| import('fs').then(fs => fs.unlinkSync(tmpPromptPath)).catch(() => {}); | ||
| } catch {} |
There was a problem hiding this comment.
The temporary file cleanup is unreliable. The exit event handler is synchronous, but import('fs').then(...) is asynchronous. This creates a race condition where the process may exit before the file is deleted.
To ensure cleanup happens, you should use the synchronous unlinkSync function.
First, add unlinkSync to your import statement at the top of the file (line 12):
import { writeFileSync, chmodSync, unlinkSync } from 'fs';Then, replace this block with a simple synchronous call:
| try { | |
| // fs.unlinkSync(tmpPromptPath); | |
| // Leaving it might be useful if they want to see what was sent, but better to clean up. | |
| import('fs').then(fs => fs.unlinkSync(tmpPromptPath)).catch(() => {}); | |
| } catch {} | |
| try { | |
| const fs = require('fs'); | |
| fs.unlinkSync(tmpPromptPath); | |
| } catch { | |
| // Best-effort cleanup, ignore errors. | |
| } |
| .action(async (id: string, options: { agent: string }) => { | ||
| runMigrations(); | ||
|
|
||
| // 1. Fetch Session | ||
| const session = db.prepare('SELECT id, title, workdir FROM sessions WHERE public_id = ?').get(id) as { id: string; title: string; workdir: string } | undefined; | ||
| if (!session) { | ||
| console.error(chalk.red(`Error: Session "${id}" not found.`)); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // 2. Fetch Agent | ||
| const profile = db.prepare('SELECT * FROM agent_profiles WHERE slug = ?').get(options.agent) as any; | ||
| if (!profile) { | ||
| console.error(chalk.red(`Error: Agent profile "${options.agent}" not found.`)); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(chalk.blue(`\n📝 Generating summary for: ${session.title} [${id}]`)); | ||
| console.log(chalk.dim(`Using agent: ${profile.name}`)); | ||
|
|
||
| // 3. Get Transcript | ||
| let transcript = ''; | ||
| try { | ||
| transcript = await readTranscript(session.id, { asMarkdown: true }); | ||
| } catch (err) { | ||
| console.error(chalk.red('Failed to read transcript.'), err); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (!transcript || transcript.trim().length === 0) { | ||
| console.log(chalk.yellow('Transcript is empty. Nothing to summarize.')); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| // 4. Create Temp File | ||
| const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${Date.now()}.md`); | ||
| const prompt = `Summarize the following coding session transcript. Focus on: | ||
| 1. The primary goal or issue being addressed. | ||
| 2. The architectural decisions made. | ||
| 3. The specific files changed. | ||
| 4. The final outcome or status. | ||
|
|
||
| --- TRANSCRIPT --- | ||
| ${transcript} | ||
| `; | ||
|
|
||
| try { | ||
| writeFileSync(tmpPromptPath, prompt, 'utf8'); | ||
| chmodSync(tmpPromptPath, 0o600); // Only owner can read/write | ||
| } catch (err) { | ||
| console.error(chalk.red('Failed to write temporary prompt file.'), err); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // 5. Execution Strategy | ||
| const tmuxSessionName = `cc-summary-${nanoid(6)}`; | ||
| const args = JSON.parse(profile.args_json) as string[]; | ||
| const env = JSON.parse(profile.env_json) as Record<string, string>; | ||
|
|
||
| try { | ||
| // Import tmux adapter dynamically or if already imported, use it | ||
| const tmux = await import('./tmux/adapter.js'); | ||
|
|
||
| await tmux.createSession( | ||
| tmuxSessionName, | ||
| profile.command, | ||
| args, | ||
| session.workdir || process.cwd(), | ||
| Object.keys(env).length > 0 ? env : undefined | ||
| ); | ||
|
|
||
| // Wait a moment for the agent to boot up | ||
| await new Promise(resolve => setTimeout(resolve, 1500)); | ||
|
|
||
| // Instruct the agent to read the file | ||
| // Different agents have different ways to read files. | ||
| // For Claude Code and Gemini CLI, usually just asking them to read the absolute path works. | ||
| const readCommand = `Please read the file at ${tmpPromptPath} and provide the summary.`; | ||
|
|
||
| await tmux.sendLiteralText(tmuxSessionName, readCommand); | ||
| await tmux.sendEnter(tmuxSessionName); | ||
|
|
||
| console.log(chalk.green(`\n✅ Summary agent launched.`)); | ||
| console.log(chalk.yellow(`Attaching to session... (Type /exit, Ctrl-D, or Ctrl-b d to leave when finished)\n`)); | ||
|
|
||
| // Attach to show output | ||
| const child = spawn('tmux', ['attach-session', '-t', tmuxSessionName], { | ||
| stdio: 'inherit' | ||
| }); | ||
|
|
||
| child.on('exit', () => { | ||
| console.log(chalk.gray(`\nDetached from summary session.`)); | ||
| // Cleanup temp file | ||
| try { | ||
| // fs.unlinkSync(tmpPromptPath); | ||
| // Leaving it might be useful if they want to see what was sent, but better to clean up. | ||
| import('fs').then(fs => fs.unlinkSync(tmpPromptPath)).catch(() => {}); | ||
| } catch {} | ||
| process.exit(0); | ||
| }); | ||
|
|
||
| } catch (err) { | ||
| console.error(chalk.red('Failed to run summary session:'), err); | ||
| process.exit(1); | ||
| } | ||
| }); |
There was a problem hiding this comment.
This action handler is over 100 lines long and handles multiple distinct steps (fetching data, creating files, running tmux, etc.). For better readability, maintainability, and testability, consider refactoring it into smaller, single-purpose helper functions.
For example, you could have functions like:
getSessionAndProfile(id, agentSlug)createSummaryPromptFile(transcript)launchSummaryAgent(profile, promptPath, workdir)
| } | ||
|
|
||
| // 2. Fetch Agent | ||
| const profile = db.prepare('SELECT * FROM agent_profiles WHERE slug = ?').get(options.agent) as any; |
There was a problem hiding this comment.
Using as any bypasses TypeScript's type safety. It's better to define a type for the data you expect from the database to catch potential errors and improve code clarity.
For example:
type AgentProfileFromDb = {
name: string;
command: string;
args_json: string;
env_json: string;
// ... other fields you use
};
const profile = db.prepare('SELECT * FROM agent_profiles WHERE slug = ?').get(options.agent) as AgentProfileFromDb | undefined;| } | ||
|
|
||
| // 4. Create Temp File | ||
| const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${Date.now()}.md`); |
There was a problem hiding this comment.
Using Date.now() for temporary filenames can lead to collisions if the command is executed multiple times within the same millisecond. Since nanoid is already a dependency, using it would generate a more robust and unique filename.
| const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${Date.now()}.md`); | |
| const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${nanoid(8)}.md`); |
This PR adds the 'cloudcode summary ' command. It allows users to pipe their clean semantic transcripts into their local AI agents (Claude, Gemini, etc.) to generate concise session summaries or PR descriptions. Includes 0600 privacy hardening for temporary context files.