* Log session cost data to .planning/telemetry/session-costs.jsonl. * * Two-layer approach: * 1. Real tokens: Read Claude Code's session JSONL for exact token counts and * compute real cost from API pricing. This is the source of truth. * 2. Estimation fallback: If session JSONL isn't
(event)
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Log session cost data to .planning/telemetry/session-costs.jsonl. |
| 133 | * |
| 134 | * Two-layer approach: |
| 135 | * 1. Real tokens: Read Claude Code's session JSONL for exact token counts and |
| 136 | * compute real cost from API pricing. This is the source of truth. |
| 137 | * 2. Estimation fallback: If session JSONL isn't available (permissions, path |
| 138 | * issues), fall back to the heuristic model (base + agents + duration). |
| 139 | * |
| 140 | * The daemon and dashboard read this file. Real cost fields are present only when |
| 141 | * real data was available -- consumers check for their presence. |
| 142 | */ |
| 143 | function logSessionCost(event) { |
| 144 | try { |
| 145 | const telemetryDir = path.join(PROJECT_ROOT, '.planning', 'telemetry'); |
| 146 | if (!fs.existsSync(telemetryDir)) { |
| 147 | fs.mkdirSync(telemetryDir, { recursive: true }); |
| 148 | } |
| 149 | |
| 150 | const now = new Date(); |
| 151 | // Resolve session ID: prefer event input, fall back to latest session file |
| 152 | const sessionId = event.session_id |
| 153 | || (sessionTokens ? sessionTokens.getCurrentSessionId() : null); |
| 154 | |
| 155 | // Count agent-start events from this session by scanning agent-runs.jsonl. |
| 156 | const agentRunsPath = path.join(telemetryDir, 'agent-runs.jsonl'); |
| 157 | let agentCount = 0; |
| 158 | let sessionStartTime = null; |
| 159 | |
| 160 | if (fs.existsSync(agentRunsPath)) { |
| 161 | const lines = fs.readFileSync(agentRunsPath, 'utf8').split('\n').filter(Boolean); |
| 162 | const fourHoursAgo = new Date(now.getTime() - 4 * 60 * 60 * 1000).toISOString(); |
| 163 | |
| 164 | for (let i = lines.length - 1; i >= 0; i--) { |
| 165 | try { |
| 166 | const entry = JSON.parse(lines[i]); |
| 167 | if (!entry.timestamp || entry.timestamp < fourHoursAgo) break; |
| 168 | if (entry.event === 'agent-start') agentCount++; |
| 169 | if (!sessionStartTime && (entry.event === 'campaign-start' || entry.event === 'wave-start')) { |
| 170 | sessionStartTime = entry.timestamp; |
| 171 | } |
| 172 | } catch { /* skip malformed lines */ } |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | // Find active campaign slug |
| 177 | let campaignSlug = null; |
| 178 | const campaignsDir = path.join(PROJECT_ROOT, '.planning', 'campaigns'); |
| 179 | if (fs.existsSync(campaignsDir)) { |
| 180 | const files = fs.readdirSync(campaignsDir).filter(f => f.endsWith('.md')); |
| 181 | for (const f of files) { |
| 182 | try { |
| 183 | const content = fs.readFileSync(path.join(campaignsDir, f), 'utf8'); |
| 184 | if (/^status:\s*active/mi.test(content)) { |
| 185 | campaignSlug = f.replace(/\.md$/, ''); |
| 186 | break; |
| 187 | } |
| 188 | } catch { /* skip unreadable files */ } |