Fix cumulative token accounting and live refresh

This commit is contained in:
2026-07-11 21:15:38 +02:00
parent 55172d747e
commit 0f556fe1a7
3 changed files with 19 additions and 7 deletions
+16 -6
View File
@@ -33,14 +33,20 @@ async function files(dir) {
async function parse(file) {
let session={ id:path.basename(file,'.jsonl'), started:'', cwd:'unknown', project:'unknown', source:'unknown', models:new Set(), usage:empty(), events:[] };
let model='unknown';
let model='unknown', previousTotal=null;
const rl=readline.createInterface({input:fs.createReadStream(file),crlfDelay:Infinity});
for await (const line of rl) try {
const x=JSON.parse(line), p=x.payload||{};
if(x.type==='session_meta'){ session.id=p.id||session.id; session.started=p.timestamp||x.timestamp; session.cwd=p.cwd||'unknown'; session.project=path.basename(session.cwd)||session.cwd; session.source=typeof p.source==='string'?p.source:(p.source?.subagent?'subagent':'app'); }
if(x.type==='turn_context' && p.model){ model=p.model; session.models.add(model); }
if(x.type==='event_msg' && p.type==='token_count' && p.info?.last_token_usage){
const t=p.info.last_token_usage, u={input:t.input_tokens||0,cached:t.cached_input_tokens||0,output:t.output_tokens||0,reasoning:t.reasoning_output_tokens||0,total:t.total_tokens||0,turns:0,duration:0};
if(x.type==='event_msg' && p.type==='token_count' && (p.info?.total_token_usage||p.info?.last_token_usage)){
// `total_token_usage` is Codex's authoritative session counter and is what
// the CLI prints on exit. Deriving deltas from it avoids double-counting
// occasional replayed/revised `last_token_usage` records.
const t=p.info.total_token_usage||p.info.last_token_usage, cumulative=!!p.info.total_token_usage;
const value=k=>Math.max(0,(t[k]||0)-(cumulative&&previousTotal?(previousTotal[k]||0):0));
const u={input:value('input_tokens'),cached:value('cached_input_tokens'),output:value('output_tokens'),reasoning:value('reasoning_output_tokens'),total:value('total_tokens'),turns:0,duration:0};
if(cumulative) previousTotal=t;
u.cost=cost(u,model); u.priced=prices[model]?u.total:0; add(session.usage,u); session.events.push({at:x.timestamp||session.started,model,...u});
}
if(x.type==='event_msg' && p.type==='task_complete'){ session.usage.turns++; session.usage.duration+=(p.duration_ms||0); }
@@ -48,8 +54,12 @@ async function parse(file) {
session.models=[...session.models]; return session;
}
let cache={loading:true,scannedAt:null,sessions:[],error:null};
async function scan(){ cache.loading=true; try { const list=await files(SESSIONS); const sessions=[]; for(let i=0;i<list.length;i+=12) sessions.push(...await Promise.all(list.slice(i,i+12).map(parse))); cache={loading:false,scannedAt:new Date().toISOString(),sessions,error:null}; } catch(e){cache={...cache,loading:false,error:e.message};} }
let cache={loading:true,scannedAt:null,sessions:[],error:null}, scanPromise=null;
async function scan(){
if(scanPromise)return scanPromise;
scanPromise=(async()=>{cache.loading=true;try{const list=await files(SESSIONS);const sessions=[];for(let i=0;i<list.length;i+=12)sessions.push(...await Promise.all(list.slice(i,i+12).map(parse)));cache={loading:false,scannedAt:new Date().toISOString(),sessions,error:null};}catch(e){cache={...cache,loading:false,error:e.message};}finally{scanPromise=null;}})();
return scanPromise;
}
function stats(url){
const from=url.searchParams.get('from'), to=url.searchParams.get('to'), modelFilter=url.searchParams.get('model'), projectFilter=url.searchParams.get('project'), sourceFilter=url.searchParams.get('source');
@@ -81,4 +91,4 @@ const server=http.createServer(async(req,res)=>{
const types={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8'};
send(res,200,fs.readFileSync(file),types[path.extname(file)]||'application/octet-stream');
});
scan().then(()=>server.listen(PORT,'127.0.0.1',()=>console.log(`Codex Lens → http://127.0.0.1:${PORT}`)));
scan().then(()=>{server.listen(PORT,'127.0.0.1',()=>console.log(`Codex Lens → http://127.0.0.1:${PORT}`));setInterval(scan,30_000).unref();});