feat: local observability — token capture + cost tracking#618
feat: local observability — token capture + cost tracking#618kokevidaurre merged 1 commit intodevelopfrom
Conversation
Every squads run now logs to .agents/observability/executions.jsonl: - Tokens (input, output, cache read, cache write) - Cost in USD (calculated from model pricing table) - Duration, status, squad, agent, model, trigger Token capture: after each run, parses Claude Code's session JSONL files (~/.claude/projects/) to extract actual usage data. No API calls, no external dependencies. New commands: - squads obs history — execution history with tokens and cost - squads obs cost — spend summary by squad, model, time period Tested: research-scanner run captured 5,976 tokens, $0.503 on claude-sonnet-4-6. Cost summary shows per-squad breakdown. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a local observability system that logs execution history and token costs to a JSONL file. It includes new CLI commands for viewing history and cost summaries, as well as logic to capture usage data from Claude Code session files. The review feedback highlights several areas for improvement: using a more robust method for retrieving the user's home directory to ensure cross-platform compatibility, reducing code duplication when logging observability records, and avoiding empty catch blocks that swallow errors and hinder debugging.
| * Claude Code writes sessions to ~/.claude/projects/<hash>/*.jsonl | ||
| */ | ||
| function findRecentSessionFile(afterTimestamp: number): string | null { | ||
| const home = process.env.HOME || ''; |
There was a problem hiding this comment.
process.env.HOME is not a reliable way to get the user's home directory, especially on Windows. Please use os.homedir() for cross-platform compatibility.
You will need to add import { homedir } from 'os'; at the top of the file.
| const home = process.env.HOME || ''; | |
| const home = homedir(); |
| logObservability({ | ||
| ts: new Date().toISOString(), | ||
| id: config.execContext.executionId, | ||
| squad: config.squadName, | ||
| agent: config.agentName, | ||
| provider: config.provider || 'anthropic', | ||
| model: 'unknown', | ||
| trigger: (config.execContext.trigger || 'manual') as ObservabilityRecord['trigger'], | ||
| status: 'failed', | ||
| duration_ms: durationMs, | ||
| input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, | ||
| cost_usd: 0, context_tokens: 0, | ||
| error: String(err), | ||
| }); |
There was a problem hiding this comment.
There's significant code duplication between this on('error') handler and the on('close') handler (lines 467-484) for creating the ObservabilityRecord. This can make maintenance harder, as changes might need to be applied in two places.
Consider creating a helper function to construct and log the ObservabilityRecord. This function could take parameters like status, error, and session usage to handle both success and failure cases, reducing duplication and centralizing the logic.
| } catch { continue; } | ||
| } | ||
| } | ||
| } catch { /* projects dir read error */ } |
There was a problem hiding this comment.
This empty catch block silently swallows all errors that might occur when reading the projects directory (e.g., permission errors). This can make debugging very difficult.
Consider logging the error, at least as a warning, to provide visibility into potential issues with the environment setup. This also applies to the other empty catch blocks in this function.
Summary
Every
squads runnow logs execution data with real token/cost capture:.agents/observability/executions.jsonlsquads obs historyandsquads obs costTested
Test plan
obs historyshows records with filtersobs costshows per-squad and per-model breakdowns🤖 Generated with Claude Code