/** * mega-dashboard.ts — live dashboard snapshot writer. * * Writes dashboard.json (full snapshot) and events.log (JSONL tail) to the * state dir so any process can inspect the extension's real-time state. * * Usage: * cat ~/.pi/agent/extensions/pi-mega-compact/dashboard.json * jq . ~/.pi/agent/extensions/pi-mega-compact/dashboard.json * tail -f ~/.pi/agent/extensions/pi-mega-compact/events.log * * Standalone: the snapshot *shape* is filled in by MegaRuntime.snapshot(); * this module only owns the on-disk write/append mechanics. */ import { join } from "node:path"; import { existsSync, mkdirSync, writeFileSync, appendFileSync } from "node:fs"; export interface DashboardSnapshot { version: 1; updatedAt: string; /** Live pressure band (low/medium/high/ultra/mega) — climbs as context fills. */ tier: string; /** Base compaction preset from env (the S24-removed /mega-tier style selector). */ presetTier: string; /** Live 0–1 pressure ratio (currentTokens / thresholdTokens). */ pressure: number; /** VC0A: count of eval-observer latency samples (0 when disabled/absent). */ vcObserverSamples: number; config: { fastGatePct: number; thresholdTokens: number; /** Compaction threshold as a fraction of the model context window (e.g. * 0.70 for "high"); null for `custom` (absolute token threshold). */ tierPct: number | null; /** Effective threshold as a % of the window (tierPct*100), or null for * `custom`; reflects the live window when known. */ effectiveThresholdPct: number | null; anchorUserMessages: number; preserveRecent: number; auto: boolean; autoInline: boolean; }; session: { id: string; state: string; persistedThisSession: boolean; lastCheckpointId: string | null; lastCompactedFrom: number; lastCompactedTokens: number; dedupSkips: number; dedupAttempts: number; }; context: { tokens: number | null; percent: number | null; contextWindow: number; }; trigger: { armed: boolean; // past fast-gate % ready: boolean; // past threshold (would compact next turn) currentTokens: number | null; thresholdTokens: number; fastGatePct: number; /** Compaction threshold as a fraction of the model context window; null * for `custom`. */ tierPct: number | null; /** Effective threshold as a % of the window; null for `custom`. */ effectiveThresholdPct: number | null; }; store: { checkpointCount: number; totalTokenEstimate: number; originalTokens: number; // Σ original dropped-region tokens (this session) tokensSaved: number; // Σ(original − stored) for this session injectedCount: number; dedupHitRate: number; storageDedupRate: number; dedupAttempts: number; dedupCollapsed: number; }; crew: { activeAgents: number; currentTurn: number; }; repo: { checkpointCount: number; // across all sessions in this repo's store totalTokenEstimate: number; // repo-wide stored checkpoint tokens originalTokens: number; // repo-wide Σ original dropped-region tokens tokensSaved: number; // repo-wide cumulative (original − stored) + deduped orig sessionCount: number; // distinct sessions with checkpoints dedupAttempts: number; // cumulative add() calls (store-wide) dedupCollapsed: number; // cumulative deduped collapses (store-wide) storageDedupRate: number; // deduped / attempts, 0..1 }; /** * Reconciled token accounting — ONE canonical formula for both session + repo * so the dashboard and widget tell the same story: * tokensIn = original conversation dropped into compaction (incl. the * redundant regions skipped by dedup) — the "in". * tokensOut = compact summaries currently held (stored) — the "out". * tokensFreed= tokensIn − tokensOut (the "saved"). * compressionPct = tokensFreed / tokensIn (0..1) — the headline "% saved". * dedupPct = storageDedupRate (0..1) — share of adds that collapsed. * session.Freed = rt.tokensSaved (honest net freed this session, incl. deduped-away); * repo.Freed = repo.tokensSaved meta (honest net freed repo-wide). */ compression: { session: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number }; repo: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number }; }; /** Phase 0 data-safety invariant (trust foundation). */ integrity: { regionsRetained: number; // checkpoints with a recoverable compressed-original compressedOriginalBytes: number; // bytes of compressed-original retained (recoverable) duplicatesCollapsed: number; // dedup duplicates (original kept on survivor) bytesPermanentlyDeleted: number; // ALWAYS 0 — the invariant }; /** Cache-hit / recall-injection counters (live session + store-wide totals). */ cacheHits: { session: number; // dedup skips + recall injections this session total: number; // store-wide deduped collapses + recall injections sessionTokensSaved: number; // tokens saved via cache hits this session totalTokensSaved: number; // store-wide tokens saved via cache hits }; /** Compaction counters (live session + store-wide cumulative). */ compacts: { session: number; // compactions performed this session total: number; // store-wide cumulative compaction count }; /** Estimated wall-clock time saved (rough tokens/sec heuristic). */ timeSaved: { compact: { sessionSec: number; totalSec: number }; cacheHit: { sessionSec: number; totalSec: number }; }; /** Active model/provider (captured live) — shown on the current-repo card. */ model?: { name: string; // Model.name or Model.id provider: string; // ProviderId (Model.provider) providerName: string; // human display name (e.g. "OpenAI") inputRate: number; // USD per input token (Model.cost) outputRate: number; // USD per output token (Model.cost) }; /** v0.8.8 Perf dashboard: live diag counters (skip vs recompute vs replay) * for the Perf tab's "TUI lag proxy" cards. Optional for back-compat. * v0.21.9: headroomTrips added (output-headroom gate trips); readers that * don't know it stay backward-compatible. */ diag?: { ctxFastGate: number; liveTrimFires: number; liveTrimReplays: number; headroomTrips?: number; }; /** S38.8: error-retry state for dashboard "retries" tile. * R7 (retry redesign): sessionRetryCount / sessionMax / poisonedCount are * ADDITIVE fields — older dashboards that don't read them stay * backward-compatible. */ retries?: { errorRetryCount: number; consecutiveErrors: number; maxConsecutiveErrors: number; errorRetryHardStop: boolean; /** R2: total S38 nudges fired this session across ALL bursts. */ sessionRetryCount: number; /** R2: session-global cap (errorRetrySessionMax). 0 = disabled. */ sessionMax: number; /** R3/R7: number of poisoned-context events this session. */ poisonedCount: number; }; } export class Dashboard { private stateDir: string; private snapshotPath: string; private eventsPath: string; constructor(stateDir: string) { this.stateDir = stateDir; this.ensureDir(); this.snapshotPath = join(stateDir, "dashboard.json"); this.eventsPath = join(stateDir, "events.log"); } /** v0.8.8: duration (ms) of the last dashboard.json write — read by * MegaRuntime.snapshot() to record a `disk_write_ms` perf sample without * wrapping the giant snapshot object literal at the call site. */ private _lastWriteMs = 0; get lastWriteMs(): number { return this._lastWriteMs; } /** Re-create the state dir if it was removed since construction. */ private ensureDir(): void { if (!existsSync(this.stateDir)) mkdirSync(this.stateDir, { recursive: true }); } /** Write a full state snapshot (atomically replaces previous). Non-fatal: a * deleted/unwritable dir must never break the agent loop. */ snapshot(data: DashboardSnapshot): void { try { this.ensureDir(); const t = performance.now(); writeFileSync(this.snapshotPath, JSON.stringify(data, null, 2) + "\n"); this._lastWriteMs = performance.now() - t; } catch { /* non-fatal */ } } /** Append a timestamped JSONL event line. Non-fatal: a deleted/unwritable * dir must never break the agent loop. */ event(type: string, data: Record): void { try { this.ensureDir(); const line = JSON.stringify({ ts: new Date().toISOString(), type, ...data }); appendFileSync(this.eventsPath, line + "\n"); } catch { /* non-fatal */ } } }