MCPcopy Create free account
hub / github.com/SethGammon/Citadel / logSessionCost

Function logSessionCost

hooks_src/session-end.js:131–272  ·  view source on GitHub ↗

* 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)

Source from the content-addressed store, hash-verified

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 */
143function 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 */ }

Callers 1

mainFunction · 0.85

Calls

no outgoing calls

Tested by

no test coverage detected