/** * @file Per-hour token usage (`token_usage_hourly`): written from a Claude * transcript's `tokensByHour` (`server/lib/transcript-cache.js`) by the hook * writer, back-filled once for sessions recorded before the table existed, and * read only by the daily cost split in `GET /api/pricing/cost` — so a session * that runs for days books each day's spend to that day instead of its start. * @author Nguyễn Ngọc Trí Vĩ */ const { db, stmts } = require("../db"); const TranscriptCache = require("./transcript-cache"); /** Write every hour of one transcript parse (MAX per hour, see the statement). */ function writeHourly(sessionId, tokensByHour) { // ponytail: rewrites every hour of the session on each hook — ~1 row per // active hour per model, cheap; write only the newest hours if it ever shows. for (const t of Object.values(tokensByHour || {})) { stmts.upsertHourlyUsage.run( sessionId, t.hour, t.model, t.speed, t.geo, t.tier, t.input, t.output, t.cacheRead, t.cacheWrite, t.cacheWrite1h, t.webSearch, t.webFetch, t.codeExec ); } } /** * Fill the table for Claude sessions that have tokens but no hourly rows yet, * one transcript per tick so a large history never blocks the event loop. * Resolves with the number of sessions written. */ function backfillHourly() { const rows = db .prepare( `SELECT s.id, s.transcript_path FROM sessions s WHERE s.transcript_path IS NOT NULL AND s.transcript_path != '' AND EXISTS (SELECT 1 FROM token_usage tu WHERE tu.session_id = s.id) AND NOT EXISTS (SELECT 1 FROM token_usage_hourly h WHERE h.session_id = s.id)` ) .all(); const cache = new TranscriptCache(1); let written = 0; return new Promise((resolve) => { const next = (i) => { if (i >= rows.length) return resolve(written); const { id, transcript_path } = rows[i]; try { const result = cache.extract(transcript_path); if (result?.tokensByHour && Object.keys(result.tokensByHour).length > 0) { db.transaction(() => writeHourly(id, result.tokensByHour))(); written++; } } catch { /* an unreadable transcript keeps its start-date booking */ } setImmediate(() => next(i + 1)); }; next(0); }); } module.exports = { writeHourly, backfillHourly };