Skip to content

Agent Reflection: Summarize Sessions with Local Agents#9

Merged
alexchaomander merged 2 commits intomainfrom
feat/agent-reflection
Mar 28, 2026
Merged

Agent Reflection: Summarize Sessions with Local Agents#9
alexchaomander merged 2 commits intomainfrom
feat/agent-reflection

Conversation

@alexchaomander
Copy link
Copy Markdown
Owner

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.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +363 to +367
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 {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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:

Suggested change
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.
}

Comment on lines +270 to +375
.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);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${Date.now()}.md`);
const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${nanoid(8)}.md`);

@alexchaomander alexchaomander merged commit 0b543f5 into main Mar 28, 2026
1 check failed
@alexchaomander alexchaomander deleted the feat/agent-reflection branch March 28, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant