Phase 2: analysis layer (analyze.js), cache guard, log hygiene
- analyze.js: burn rate, weekly reconstruction, cycle stagger, rotation rank, underspend alerts, log prune with weekly archive - logger.js: getCachedRun(maxAgeMinutes) — skip probing if recent data exists - monitor.js: cache guard at wake — 20-min dedup, zero extra API calls - test.js: fix type assertion for gemini-api/xai-api providers (+5 passing); add 14 new tests for cache guard and analyze.js (162 total, all green) - docs/analyze.md: usage reference Co-authored-by: Hannibal Smith <hannibal@trentuna.com>
This commit is contained in:
parent
1b4e299461
commit
34898b1196
6 changed files with 745 additions and 2 deletions
30
logger.js
30
logger.js
|
|
@ -2,7 +2,7 @@
|
|||
* logger.js — persistent JSONL log to ~/.logs/token-monitor/YYYY-MM-DD.jsonl
|
||||
*/
|
||||
|
||||
import { appendFileSync, mkdirSync } from 'fs';
|
||||
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
|
|
@ -17,3 +17,31 @@ export function getLogPath() {
|
|||
const today = new Date().toISOString().slice(0, 10);
|
||||
return join(homedir(), '.logs', 'token-monitor', `${today}.jsonl`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last logged run if it was within maxAgeMinutes, otherwise null.
|
||||
* Skips test/empty entries (entries where providers has no typed providers).
|
||||
*/
|
||||
export function getCachedRun(maxAgeMinutes = 20) {
|
||||
const dir = join(homedir(), '.logs', 'token-monitor');
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const file = join(dir, `${today}.jsonl`);
|
||||
if (!existsSync(file)) return null;
|
||||
|
||||
const lines = readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean);
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
const providers = entry.providers || {};
|
||||
// Skip test/empty entries — real entries have at least one provider with a type
|
||||
const hasRealData = Object.values(providers).some(p => p && p.type);
|
||||
if (!hasRealData) continue;
|
||||
|
||||
const ageMinutes = (Date.now() - new Date(entry.ts).getTime()) / 60000;
|
||||
if (ageMinutes <= maxAgeMinutes) return entry;
|
||||
return null; // last real entry is too old
|
||||
} catch { continue; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue