dsh-token-pulse 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -421,16 +421,26 @@ async function readAllUsageEvents(ctx) {
421
421
  const { join, basename, dirname } = await import("node:path");
422
422
  const dir = sessionsDir();
423
423
  if (existsSync(dir)) {
424
- const files = [];
424
+ const best = /* @__PURE__ */ new Map();
425
425
  (function walk(dir) {
426
426
  try {
427
427
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
428
428
  const p = join(dir, entry.name);
429
429
  if (entry.isDirectory()) walk(p);
430
- else if (/^session(\.v\d+)?\.jsonl(\.zstd)?$/.test(entry.name)) files.push(p);
430
+ else {
431
+ const m = /^session(\.v(\d+))?\.jsonl(\.zstd)?$/.exec(entry.name);
432
+ if (!m) continue;
433
+ const gen = m[2] ? Number(m[2]) : 0;
434
+ const cur = best.get(dir);
435
+ if (!cur || gen > cur.gen) best.set(dir, {
436
+ gen,
437
+ path: p
438
+ });
439
+ }
431
440
  }
432
441
  } catch {}
433
442
  })(dir);
443
+ const files = [...best.values()].sort((a, b) => a.path < b.path ? -1 : 1).map((e) => e.path);
434
444
  for (const f of files) {
435
445
  const sid = basename(dirname(f));
436
446
  try {
package/lib/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["cloneDay"],"sources":["../src/date-bucket.ts","../src/aggregation.ts","../src/host/dsh-home.ts","../src/host/session-reader.ts","../src/host/persist.ts","../src/host/store.ts","../src/index.ts"],"sourcesContent":["const TZ = \"Asia/Shanghai\";\n\nexport function toDayKey(ms: number, tz = TZ): string {\n const fmt = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: tz,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n return fmt.format(new Date(ms));\n}\n\nexport function dayKeyToDate(key: string, _tz = TZ): Date {\n return new Date(key + \"T12:00:00+08:00\");\n}\n\nexport function weekStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n const dow = (d.getDay() + 6) % 7; // Mon=0\n d.setDate(d.getDate() - dow);\n return toDayKey(d.getTime());\n}\n\nexport function monthStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-01`;\n}\n\nexport function quarterStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n const q = Math.floor(d.getMonth() / 3);\n const m = q * 3 + 1;\n return `${d.getFullYear()}-${String(m).padStart(2, \"0\")}-01`;\n}\n\nexport function yearStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n return `${d.getFullYear()}-01-01`;\n}\n\nexport function addDays(dayKey: string, n: number): string {\n const d = dayKeyToDate(dayKey);\n d.setDate(d.getDate() + n);\n return toDayKey(d.getTime());\n}\n\nexport function listDaysInRange(startKey: string, endKey: string): string[] {\n const result: string[] = [];\n let cur = startKey;\n while (cur <= endKey) {\n result.push(cur);\n cur = addDays(cur, 1);\n }\n return result;\n}\n\nexport function weekRangeFor(dateKey: string): [string, string] {\n const s = weekStartOf(dateKey);\n return [s, addDays(s, 6)];\n}\n\nexport function monthRangeFor(dateKey: string): [string, string] {\n const s = monthStartOf(dateKey);\n const d = dayKeyToDate(s);\n const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();\n return [s, `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(lastDay).padStart(2, \"0\")}`];\n}\n\n/** Last ~90 days: sliding window for the \"Quarter\" tab (not calendar quarter). */\nexport function quarterRangeFor(dateKey: string): [string, string] {\n return [addDays(dateKey, -89), dateKey];\n}\n\nexport function yearRangeFor(dateKey: string): [string, string] {\n const s = yearStartOf(dateKey);\n const d = dayKeyToDate(s);\n return [s, `${d.getFullYear()}-12-31`];\n}\n","import {\n toDayKey,\n addDays,\n listDaysInRange,\n weekRangeFor,\n monthRangeFor,\n quarterRangeFor,\n yearRangeFor,\n} from \"./date-bucket.ts\";\n\nexport type RawUsageEvent = {\n time: number;\n provider: string;\n model: string;\n usage: {\n inputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n outputTokens: number;\n };\n /** Session id this usage belongs to. Used by HeatmapStore.refresh() to\n * replace a live session's events in place without duplicating history. */\n sid?: string;\n};\n\nexport type DayAgg = {\n dayKey: string;\n totalTokens: number;\n uncachedInputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n outputTokens: number;\n count: number;\n byModel: Map<string, number>;\n byProvider: Map<string, number>;\n /** 24 hourly buckets in Asia/Shanghai, index 0 = 00:00-01:00 */\n hourlyTokens: number[];\n winnerModel: string | null;\n winnerProvider: string | null;\n};\n\nexport type TopItem = { name: string; tokens: number };\n\nexport type Aggregated = {\n byDay: Map<string, DayAgg>;\n totals: { today: number; thisWeek: number; thisMonth: number; all: number };\n topModels: Array<{ model: string; tokens: number }>;\n topProviders: Array<{ provider: string; tokens: number }>;\n /** @deprecated use topModels */\n top5: Array<{ model: string; tokens: number }>;\n weekDays(dayKey: string): DayAgg[];\n monthDays(dayKey: string): DayAgg[];\n quarterDays(dayKey: string): DayAgg[];\n yearDays(dayKey: string): DayAgg[];\n top5InWindow(days: DayAgg[]): Array<{ model: string; tokens: number }>;\n topProvidersInWindow(days: DayAgg[]): Array<{ provider: string; tokens: number }>;\n};\n\nfunction emptyDay(dayKey: string): DayAgg {\n return {\n dayKey,\n totalTokens: 0,\n uncachedInputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n outputTokens: 0,\n count: 0,\n byModel: new Map(),\n byProvider: new Map(),\n hourlyTokens: new Array(24).fill(0),\n winnerModel: null,\n winnerProvider: null,\n };\n}\n\nfunction sumByDay(days: DayAgg[]): number {\n return days.reduce((s, d) => s + d.totalTokens, 0);\n}\n\nfunction resolveDay(byDay: Map<string, DayAgg>, key: string): DayAgg {\n return byDay.get(key) ?? emptyDay(key);\n}\n\nfunction resolveRange(byDay: Map<string, DayAgg>, start: string, end: string): DayAgg[] {\n return listDaysInRange(start, end).map((k) => resolveDay(byDay, k));\n}\n\n/**\n * 5-level fallback for callers without a view-aware max (e.g. legacy tests).\n * For view-aware quantile, use levelForView().\n */\nexport function clampLevel(value: number): 0 | 1 | 2 | 3 | 4 {\n if (value === 0) return 0;\n if (value <= 2000) return 1;\n if (value <= 10000) return 2;\n if (value <= 50000) return 3;\n return 4;\n}\n\n/** View-aware quantile: 0, (0,25%], (25%,50%], (50%,75%], (75%,100%] so month/quarter spread across the palette. */\nexport function levelForView(value: number, viewMax: number): 0 | 1 | 2 | 3 | 4 {\n if (value === 0) return 0;\n if (viewMax <= 0) return 1;\n const r = value / viewMax;\n if (r <= 0.25) return 1;\n if (r <= 0.5) return 2;\n if (r <= 0.75) return 3;\n return 4;\n}\n\n/** Minimum number of non-zero values before rank-based bucketing kicks in. Below this we fall back to logLevel(). */\nexport const MIN_RANK_POINTS = 5;\n\n/**\n * Rank-based (percentile) level. Outlier-proof: no matter how large one day is,\n * lower values still spread across levels 1-4 by their position among non-zero\n * values — a single huge day no longer flattens everything to level 1.\n * `sortedNonZero` must be the ascending-sorted array of all non-zero values in\n * the view. Uses the midpoint of the \"below\" and \"at-or-below\" ranks, so ties\n * sit in the middle of their band; the maximum always lands on level 4 and the\n * minimum on level 1, guaranteeing all four colored levels appear (n ≥ 2).\n * 0 maps to 0.\n */\nexport function levelByRank(value: number, sortedNonZero: number[]): 0 | 1 | 2 | 3 | 4 {\n if (value <= 0) return 0;\n const n = sortedNonZero.length;\n if (n === 0) return 1;\n let below = 0;\n let atOrBelow = 0;\n for (const v of sortedNonZero) {\n if (v < value) below++;\n if (v <= value) atOrBelow++;\n }\n const rank = (below / n + atOrBelow / n) / 2; // midpoint rank ∈ (0, 1)\n if (rank <= 0.25) return 1; // bottom quarter\n if (rank <= 0.5) return 2; // 25-50%\n if (rank <= 0.75) return 3; // 50-75%\n return 4; // top quarter, max always here\n}\n\n/**\n * Log-scaled ratio fallback for sparse data (< MIN_RANK_POINTS non-zero values).\n * Compresses outliers logarithmically so a single large value doesn't flatten\n * the rest, while keeping \"larger = darker\" intuition.\n */\nexport function logLevel(value: number, viewMax: number): 0 | 1 | 2 | 3 | 4 {\n if (value <= 0) return 0;\n if (viewMax <= 0) return 1;\n const r = Math.log1p(value) / Math.log1p(viewMax);\n if (r <= 0.25) return 1;\n if (r <= 0.5) return 2;\n if (r <= 0.75) return 3;\n return 4;\n}\n\nfunction hourInShanghai(ms: number): number {\n // Convert to Asia/Shanghai hour 0-23. en-GB is used because en-US may emit\n // \"24\" for midnight on some ICU data; we also defensively clamp.\n const s = new Date(ms).toLocaleString(\"en-GB\", { timeZone: \"Asia/Shanghai\", hour: \"2-digit\", hour12: false });\n const h = Number(s);\n return h === 24 ? 0 : h;\n}\n\nexport function aggregate(events: RawUsageEvent[], nowMs: number): Aggregated {\n const byDay = new Map<string, DayAgg>();\n const globalByModel = new Map<string, number>();\n const globalByProvider = new Map<string, number>();\n\n for (const ev of events) {\n const dayKey = toDayKey(ev.time);\n let agg = byDay.get(dayKey);\n if (!agg) {\n agg = emptyDay(dayKey);\n byDay.set(dayKey, agg);\n }\n const u = ev.usage;\n const total = (u.inputTokens ?? 0) + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0) + (u.outputTokens ?? 0);\n agg.totalTokens += total;\n agg.uncachedInputTokens += u.inputTokens ?? 0;\n agg.cacheReadTokens += u.cacheReadTokens ?? 0;\n agg.cacheWriteTokens += u.cacheWriteTokens ?? 0;\n agg.outputTokens += u.outputTokens ?? 0;\n agg.count += 1;\n const h = hourInShanghai(ev.time);\n if (h >= 0 && h < 24) agg.hourlyTokens[h] += total;\n\n const modelKey = ev.model || \"unknown\";\n const providerKey = ev.provider || \"unknown\";\n agg.byModel.set(modelKey, (agg.byModel.get(modelKey) ?? 0) + total);\n agg.byProvider.set(providerKey, (agg.byProvider.get(providerKey) ?? 0) + total);\n globalByModel.set(modelKey, (globalByModel.get(modelKey) ?? 0) + total);\n globalByProvider.set(providerKey, (globalByProvider.get(providerKey) ?? 0) + total);\n }\n\n // compute winners per day\n for (const agg of byDay.values()) {\n let bestM: string | null = null;\n let bestMVal = -1;\n for (const [m, v] of agg.byModel) {\n if (v > bestMVal) {\n bestMVal = v;\n bestM = m;\n }\n }\n agg.winnerModel = bestM;\n\n let bestP: string | null = null;\n let bestPVal = -1;\n for (const [p, v] of agg.byProvider) {\n if (v > bestPVal) {\n bestPVal = v;\n bestP = p;\n }\n }\n agg.winnerProvider = bestP;\n }\n\n const todayKey = toDayKey(nowMs);\n const [wStart, wEnd] = weekRangeFor(todayKey);\n const [mStart, mEnd] = monthRangeFor(todayKey);\n\n const today = byDay.get(todayKey)?.totalTokens ?? 0;\n const thisWeek = sumByDay(resolveRange(byDay, wStart, wEnd));\n const thisMonth = sumByDay(resolveRange(byDay, mStart, mEnd));\n let all = 0;\n for (const v of byDay.values()) all += v.totalTokens;\n\n const topModels = [...globalByModel.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([model, tokens]) => ({ model, tokens }));\n\n const topProviders = [...globalByProvider.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([provider, tokens]) => ({ provider, tokens }));\n\n function top5InWindow(days: DayAgg[]): Array<{ model: string; tokens: number }> {\n const m = new Map<string, number>();\n for (const d of days) {\n for (const [model, tokens] of d.byModel) {\n m.set(model, (m.get(model) ?? 0) + tokens);\n }\n }\n return [...m.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([model, tokens]) => ({ model, tokens }));\n }\n\n function topProvidersInWindow(days: DayAgg[]): Array<{ provider: string; tokens: number }> {\n const m = new Map<string, number>();\n for (const d of days) {\n for (const [provider, tokens] of d.byProvider) {\n m.set(provider, (m.get(provider) ?? 0) + tokens);\n }\n }\n return [...m.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([provider, tokens]) => ({ provider, tokens }));\n }\n\n return {\n byDay,\n totals: { today, thisWeek, thisMonth, all },\n topModels,\n topProviders,\n top5: topModels,\n weekDays: (k: string) => {\n const [s, e] = weekRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n monthDays: (k: string) => {\n const [s, e] = monthRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n quarterDays: (k: string) => {\n const [s, e] = quarterRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n yearDays: (k: string) => {\n const [s, e] = yearRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n top5InWindow,\n topProvidersInWindow,\n };\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Resolve the DSH home directory.\n *\n * A machine may run several independent DSH installs side by side, each with\n * its own directory (sessions, storages, profiles, ...). The active install\n * advertises itself via `$DSH_HOME`, so every path the plugin touches MUST\n * derive from it. Only when the variable is absent (e.g. unit tests, very\n * old launchers) do we fall back to the default `~/.dsh` — mirroring core.\n */\nexport function dshHome(): string {\n const env = process.env.DSH_HOME?.trim();\n if (env) return env;\n return join(homedir(), \".dsh\");\n}\n\n/** `<DSH_HOME>/sessions` — where session logs live. */\nexport function sessionsDir(): string {\n return join(dshHome(), \"sessions\");\n}\n\n/** `<DSH_HOME>/storages` — where plugins persist local data. */\nexport function storagesDir(): string {\n return join(dshHome(), \"storages\");\n}\n","import type { RawUsageEvent } from \"../aggregation.ts\";\nimport { sessionsDir } from \"./dsh-home.ts\";\n\n/** zstd frame magic: bytes 28 B5 2F FD, read as UInt32LE. */\nconst ZSTD_MAGIC = 0xfd2fb528;\n\n/**\n * Locate complete zstd frames inside a concatenated-frame container (the\n * format DSH's session.jsonl.zstd uses: every appended batch is its own\n * compressed frame). Scans the frame structure WITHOUT decompressing, mirroring\n * @deepseek-ai/dsh-session-persistence-jsonl's scanZstdFrames. Returns\n * [start,end) byte ranges, one per complete frame.\n */\nexport function scanZstdFrames(buffer: Buffer): Array<{ start: number; end: number }> {\n const frames: Array<{ start: number; end: number }> = [];\n let offset = 0;\n while (offset < buffer.length) {\n const start = offset;\n if (buffer.length - offset < 4) break;\n if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) break; // torn/corrupt tail: stop\n offset += 4;\n if (offset === buffer.length) break;\n const descriptor = buffer.readUInt8(offset);\n offset += 1;\n if ((descriptor & 24) !== 0) break; // reserved frame-header bit\n const contentSizeFlag = descriptor >>> 6;\n const singleSegment = (descriptor & 32) !== 0;\n const checksum = (descriptor & 4) !== 0;\n const dictionaryFlag = descriptor & 3;\n const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;\n const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag;\n const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;\n if (buffer.length - offset < remainingHeaderBytes) break;\n offset += remainingHeaderBytes;\n for (;;) {\n if (buffer.length - offset < 3) break;\n const blockHeader = buffer.readUIntLE(offset, 3);\n offset += 3;\n const lastBlock = (blockHeader & 1) !== 0;\n const blockType = (blockHeader >>> 1) & 3;\n const blockSize = blockHeader >>> 3;\n if (blockType === 3) break; // reserved block type\n const payloadBytes = blockType === 1 ? 1 : blockSize; // RLE payload is 1 byte\n if (buffer.length - offset < payloadBytes) break;\n offset += payloadBytes;\n if (lastBlock) break;\n }\n if (checksum) {\n if (buffer.length - offset < 4) break;\n offset += 4;\n }\n frames.push({ start, end: offset });\n }\n return frames;\n}\n\n/**\n * Decode a concatenated-frame zstd session log to its full UTF-8 text.\n * Node's one-shot `zstdDecompressSync` decodes only the FIRST frame, so we\n * locate every frame and decode each one separately (per\n * dsh-session-persistence-jsonl's zstd-public-decoder). Returns null on any\n * decode failure (callers treat a null text as \"no data from this file\").\n */\nexport async function decodeSessionZstd(buffer: Buffer): Promise<string | null> {\n const zlib = await import(\"node:zlib\");\n if (typeof zlib.zstdDecompressSync !== \"function\") return null; // caller falls back to CLI\n const frames = scanZstdFrames(buffer);\n if (frames.length === 0) return null;\n const parts: string[] = [];\n for (const { start, end } of frames) {\n try {\n parts.push(zlib.zstdDecompressSync(buffer.subarray(start, end)).toString(\"utf-8\"));\n } catch {\n return null; // corrupt frame: treat whole file as unreadable\n }\n }\n return parts.join(\"\");\n}\n\ntype SessionEvent = {\n type: string;\n time?: number;\n seq?: number;\n data?: any;\n surfaceOp?: any;\n /** Session id this event belongs to. Events do NOT carry it in the log; the\n * reader tags it so turn/step keys stay scoped per session. */\n sid?: string;\n};\n\n/**\n * Parse a batch of raw session events into RawUsageEvents.\n * Rules:\n * - request/header stores provider/model for fallback (per session)\n * - assistant/chunk type=usage is provisional, keyed by sid/turn/step\n * - assistant/message with usage is authoritative, overwrites provisional for same sid/turn/step\n *\n * The dedupe key MUST include the session id: turn/step counters restart per\n * session (every session has a turn 1 step 1), so without `sid` different\n * sessions overwrite each other and older sessions' usage silently disappears.\n */\nexport function parseUsageEvents(events: SessionEvent[]): RawUsageEvent[] {\n // session id -> fallback provider/model\n const fallbackBySid = new Map<string, { provider: string; model: string }>();\n const fallbackFor = (sid: string | undefined) => fallbackBySid.get(sid ?? \"\") ?? { provider: \"\", model: \"\" };\n // sid/turn/step -> RawUsageEvent (provisional or final)\n const byTurnStep = new Map<string, RawUsageEvent>();\n\n for (const ev of events) {\n const sid = ev.sid ?? \"\";\n if (ev.type === \"request/header\" && ev.data?.header?.config) {\n const cfg = ev.data.header.config;\n const fb = fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (cfg.provider) fb.provider = cfg.provider;\n if (cfg.model) fb.model = cfg.model;\n fallbackBySid.set(sid, fb);\n }\n if (ev.type === \"request/context\" && ev.data) {\n const fb = fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (ev.data.provider) fb.provider = ev.data.provider;\n if (ev.data.model) fb.model = ev.data.model;\n fallbackBySid.set(sid, fb);\n }\n if (ev.type === \"assistant/chunk\" && ev.data?.chunk?.type === \"usage\") {\n const key = `${sid}::${ev.data.turn}:${ev.data.step}`;\n // Only set if not already finalized by a message\n if (!byTurnStep.has(key)) {\n const fb = fallbackFor(sid);\n const u = ev.data.chunk.usage;\n byTurnStep.set(key, {\n time: ev.time ?? Date.now(),\n provider: fb.provider,\n model: fb.model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n });\n }\n }\n if (ev.type === \"assistant/message\" && ev.data?.usage) {\n const msg = ev.data.message;\n const src = msg?.source;\n const fb = fallbackFor(sid);\n const provider = src?.provider ?? fb.provider;\n const model = src?.model ?? fb.model;\n const turn = ev.data.turn;\n const step = ev.data.step;\n const key = turn !== undefined && step !== undefined ? `${sid}::${turn}:${step}` : `${sid}::msg:${ev.seq ?? Math.random()}`;\n const u = ev.data.usage;\n byTurnStep.set(key, {\n time: ev.time ?? Date.now(),\n provider,\n model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n });\n // Also update fallback from message source for subsequent chunks\n const cur = fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (src?.provider) cur.provider = src.provider;\n if (src?.model) cur.model = src.model;\n fallbackBySid.set(sid, cur);\n }\n }\n\n return [...byTurnStep.values()];\n}\n\n/**\n * Read all sessions' usage events: FULL disk history (every session, old + new)\n * merged with live in-memory events that are not yet flushed to disk.\n *\n * NOTE: ctx.sessions.list() returns only LIVE sessions (created/resumed in the\n * current process) — historical sessions from previous processes are NOT in it.\n * The old implementation returned early whenever any live session had events,\n * so it never scanned the disk and old sessions never showed up. Here we ALWAYS\n * read the whole <DSH_HOME>/sessions tree and merge live events on top, deduping by\n * sessionId:seq (events carry no sessionId; the disk path derives it from the\n * session directory name).\n */\nexport async function readAllUsageEvents(ctx: any): Promise<RawUsageEvent[]> {\n const allEvents: SessionEvent[] = [];\n const seen = new Set<string>();\n\n const relevant = (o: any) =>\n o.type === \"request/header\" || o.type === \"request/context\" ||\n o.type === \"assistant/chunk\" || o.type === \"assistant/message\";\n\n const pushUnique = (sid: string, o: any) => {\n if (typeof o?.seq === \"number\") {\n const key = `${sid}:${o.seq}`;\n if (seen.has(key)) return;\n seen.add(key);\n }\n allEvents.push({ ...o, sid });\n };\n\n // 1) Full history: scan <DSH_HOME>/sessions (all sessions, including old ones).\n try {\n const { readdirSync, existsSync } = await import(\"node:fs\");\n const { join, basename, dirname } = await import(\"node:path\");\n const dir = sessionsDir();\n if (existsSync(dir)) {\n const files: string[] = [];\n (function walk(dir: string) {\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, entry.name);\n if (entry.isDirectory()) walk(p);\n // session.jsonl[.zstd] (legacy) and session.v3.jsonl[.zstd] (current DSH\n // session format; vN prefix may keep evolving — accept any version).\n else if (/^session(\\.v\\d+)?\\.jsonl(\\.zstd)?$/.test(entry.name)) files.push(p);\n }\n } catch {}\n })(dir);\n\n for (const f of files) {\n // <root>/<workspace>/<sessionId>/session.jsonl.zstd\n const sid = basename(dirname(f));\n try {\n let text: string;\n if (f.endsWith(\".zstd\")) {\n const { readFileSync } = await import(\"node:fs\");\n // DSH session logs are concatenated-frame zstd: decode EVERY frame,\n // not just the first (one-shot zstdDecompressSync only yields frame 1).\n const textFromFrames = await decodeSessionZstd(readFileSync(f));\n if (textFromFrames !== null) {\n text = textFromFrames;\n } else {\n // Fallback for Node < 23.8 (no built-in zstd): external CLI required.\n const { execSync } = await import(\"node:child_process\");\n text = execSync(`zstd -dc ${JSON.stringify(f)}`, { encoding: \"utf-8\", maxBuffer: 50 * 1024 * 1024 });\n }\n } else {\n const { readFileSync } = await import(\"node:fs\");\n text = readFileSync(f, \"utf-8\");\n }\n for (const line of text.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const obj = JSON.parse(line);\n if (relevant(obj)) pushUnique(sid, obj);\n } catch {}\n }\n } catch {}\n }\n }\n } catch {}\n\n // 2) Live sessions: merge events not yet flushed to disk (dedupe by sid:seq).\n try {\n if (ctx.sessions?.list) {\n const sessions = await ctx.sessions.list();\n for (const s of sessions ?? []) {\n const sid = s?.id ?? \"live\";\n for (const event of (s as any)?.events ?? []) {\n if (relevant(event)) pushUnique(sid, event);\n }\n }\n }\n } catch {}\n\n return parseUsageEvents(allEvents);\n}\n\n/**\n * Read usage from LIVE sessions only (ctx.sessions.list() = sessions created\n * or resumed in the current process). Cheap — no disk scan. Used by\n * store.refresh() after init() has already captured the full disk history,\n * so refreshes do not re-decompress <DSH_HOME>/sessions on every tick.\n *\n * A live session's `events` includes its full stored log, so the result is a\n * complete snapshot per live session; callers must REPLACE (not append) that\n * session's events to avoid double counting.\n */\nexport async function readLiveUsageEvents(ctx: any): Promise<RawUsageEvent[]> {\n const events: SessionEvent[] = [];\n const relevant = (o: any) =>\n o.type === \"request/header\" || o.type === \"request/context\" ||\n o.type === \"assistant/chunk\" || o.type === \"assistant/message\";\n try {\n if (ctx.sessions?.list) {\n const sessions = await ctx.sessions.list();\n for (const s of sessions ?? []) {\n const sid = s?.id ?? \"live\";\n for (const event of (s as any)?.events ?? []) {\n if (relevant(event)) events.push({ ...event, sid });\n }\n }\n }\n } catch {}\n return parseUsageEvents(events);\n}\n","import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { dshHome } from \"./dsh-home.ts\";\nimport type { DayAgg } from \"../aggregation.ts\";\n\nexport type PersistedDay = {\n dayKey: string;\n totalTokens: number;\n uncachedInputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n outputTokens: number;\n count: number;\n byModel: Record<string, number>;\n byProvider: Record<string, number>;\n hourlyTokens: number[];\n winnerModel: string | null;\n winnerProvider: string | null;\n};\n\nexport type PersistedFile = {\n version: 1;\n days: Record<string, PersistedDay>;\n};\n\nfunction persistPath(): string {\n const dir = join(dshHome(), \"storages\", \"dsh-token-pulse\");\n return join(dir, \"daily.json\");\n}\n\n/**\n * One-time migration from the pre-rename identity `dsh-token-heatmap`.\n * The plugin was renamed to `dsh-token-pulse` (to avoid a bare-name collision\n * with an unrelated plugin in the awesome-dsh-plugin catalog). Existing users\n * have their accumulated history under storages/dsh-token-heatmap/ and their\n * settings under localStorage keys dsh-token-heatmap:*; without this they would\n * see an empty heatmap after the update. Copy the persisted file forward the\n * first time the new location is missing but the legacy one exists.\n */\nlet migrated = false;\nfunction migrateLegacyPersist(): void {\n if (migrated) return;\n migrated = true;\n try {\n const next = persistPath();\n if (existsSync(next)) return;\n const legacy = join(dshHome(), \"storages\", \"dsh-token-heatmap\", \"daily.json\");\n if (!existsSync(legacy)) return;\n mkdirSync(dirname(next), { recursive: true });\n copyFileSync(legacy, next);\n } catch {\n // best-effort: a failed migration must not break loading\n }\n}\n\nfunction libDailyPath(): string | null {\n try {\n // Resolve the installed plugin's own lib/ from this module's location\n // instead of hardcoding a profile name: this works under any profile and\n // any DSH install. The browser client fetches via the /api route, so this\n // copy is only a legacy fallback.\n const here = dirname(fileURLToPath(import.meta.url));\n const c = join(here, \"daily.json\");\n try {\n mkdirSync(here, { recursive: true });\n return c;\n } catch {\n return null;\n }\n } catch {\n return null;\n }\n}\n\nfunction toPersistedDay(day: DayAgg): PersistedDay {\n return {\n dayKey: day.dayKey,\n totalTokens: day.totalTokens,\n uncachedInputTokens: day.uncachedInputTokens,\n cacheReadTokens: day.cacheReadTokens,\n cacheWriteTokens: day.cacheWriteTokens,\n outputTokens: day.outputTokens,\n count: day.count,\n byModel: Object.fromEntries(day.byModel),\n byProvider: Object.fromEntries(day.byProvider),\n hourlyTokens: [...(day.hourlyTokens ?? new Array(24).fill(0))],\n winnerModel: day.winnerModel,\n winnerProvider: day.winnerProvider,\n };\n}\n\nfunction fromPersistedDay(p: PersistedDay): DayAgg {\n const byModel = new Map<string, number>(Object.entries(p.byModel ?? {}));\n const byProvider = new Map<string, number>(Object.entries(p.byProvider ?? {}));\n const hourlyTokens = Array.isArray((p as any).hourlyTokens) && (p as any).hourlyTokens.length === 24\n ? [...(p as any).hourlyTokens]\n : new Array(24).fill(0);\n return {\n dayKey: p.dayKey,\n totalTokens: p.totalTokens,\n uncachedInputTokens: p.uncachedInputTokens ?? 0,\n cacheReadTokens: p.cacheReadTokens ?? 0,\n cacheWriteTokens: p.cacheWriteTokens ?? 0,\n outputTokens: p.outputTokens ?? 0,\n count: p.count ?? 0,\n byModel,\n byProvider,\n hourlyTokens,\n winnerModel: p.winnerModel ?? null,\n winnerProvider: p.winnerProvider ?? null,\n };\n}\n\nexport function loadPersisted(): Map<string, DayAgg> {\n try {\n migrateLegacyPersist();\n const p = persistPath();\n if (!existsSync(p)) return new Map();\n const raw = readFileSync(p, \"utf-8\");\n const parsed: PersistedFile = JSON.parse(raw);\n if (!parsed || typeof parsed.days !== \"object\") return new Map();\n const m = new Map<string, DayAgg>();\n for (const [k, v] of Object.entries(parsed.days)) {\n try {\n m.set(k, fromPersistedDay(v));\n } catch {\n // skip corrupt day\n }\n }\n return m;\n } catch {\n return new Map();\n }\n}\n\nexport function savePersisted(days: Map<string, DayAgg>): void {\n try {\n const out: PersistedFile = {\n version: 1,\n days: Object.fromEntries([...days.entries()].map(([k, v]) => [k, toPersistedDay(v)])),\n };\n const json = JSON.stringify(out, null, 2);\n const p = persistPath();\n mkdirSync(dirname(p), { recursive: true });\n const tmp = p + \".tmp\";\n writeFileSync(tmp, json, \"utf-8\");\n renameSync(tmp, p);\n // Also write to lib/ (next to this module) for browser fetch via /plugins/dsh-token-pulse/daily.json\n const libPath = libDailyPath();\n if (libPath) {\n try {\n writeFileSync(libPath, json, \"utf-8\");\n } catch {}\n }\n } catch {\n // best-effort\n }\n}\n\n/**\n * Merge strategy: persisted is cumulative historical truth that never shrinks.\n * For each dayKey, if live has data, take element-wise MAX for additive fields,\n * and merge byModel/byProvider by taking max per key (or summing? No - live total is sum of remaining sessions, persisted is sum of all-time events. Taking max per day total prevents loss on deletion while allowing growth on new events. Per-model we also take max per model per day? But that would undercount if a new model appears. Better: merge by taking max of total, and for byModel: for each model, max(persisted, live). If a new model appears only in live, it propagates.\n * For count: max as well.\n */\nexport function mergePersistedAndLive(\n persisted: Map<string, DayAgg>,\n live: Map<string, DayAgg>,\n): Map<string, DayAgg> {\n const result = new Map<string, DayAgg>();\n\n // Start from persisted\n for (const [k, v] of persisted) {\n result.set(k, cloneDay(v));\n }\n\n for (const [k, liveDay] of live) {\n const prev = result.get(k);\n if (!prev) {\n result.set(k, cloneDay(liveDay));\n continue;\n }\n // Take max for scalar totals (never shrink); hourly also max per hour\n const mergedHourly = (prev.hourlyTokens ?? new Array(24).fill(0)).map((v, i) => Math.max(v, liveDay.hourlyTokens?.[i] ?? 0));\n const merged: DayAgg = {\n dayKey: k,\n totalTokens: Math.max(prev.totalTokens, liveDay.totalTokens),\n uncachedInputTokens: Math.max(prev.uncachedInputTokens, liveDay.uncachedInputTokens),\n cacheReadTokens: Math.max(prev.cacheReadTokens, liveDay.cacheReadTokens),\n cacheWriteTokens: Math.max(prev.cacheWriteTokens, liveDay.cacheWriteTokens),\n outputTokens: Math.max(prev.outputTokens, liveDay.outputTokens),\n count: Math.max(prev.count, liveDay.count),\n byModel: mergeMapMax(prev.byModel, liveDay.byModel),\n byProvider: mergeMapMax(prev.byProvider, liveDay.byProvider),\n hourlyTokens: mergedHourly,\n winnerModel: null,\n winnerProvider: null,\n };\n // recompute winners\n let bestM: string | null = null;\n let bestMVal = -1;\n for (const [m, v2] of merged.byModel) {\n if (v2 > bestMVal) {\n bestMVal = v2;\n bestM = m;\n }\n }\n merged.winnerModel = bestM;\n let bestP: string | null = null;\n let bestPVal = -1;\n for (const [p, v2] of merged.byProvider) {\n if (v2 > bestPVal) {\n bestPVal = v2;\n bestP = p;\n }\n }\n merged.winnerProvider = bestP;\n result.set(k, merged);\n }\n\n return result;\n}\n\nfunction cloneDay(d: DayAgg): DayAgg {\n return {\n dayKey: d.dayKey,\n totalTokens: d.totalTokens,\n uncachedInputTokens: d.uncachedInputTokens,\n cacheReadTokens: d.cacheReadTokens,\n cacheWriteTokens: d.cacheWriteTokens,\n outputTokens: d.outputTokens,\n count: d.count,\n byModel: new Map(d.byModel),\n byProvider: new Map(d.byProvider),\n hourlyTokens: [...(d.hourlyTokens ?? new Array(24).fill(0))],\n winnerModel: d.winnerModel,\n winnerProvider: d.winnerProvider,\n };\n}\n\nfunction mergeMapMax(a: Map<string, number>, b: Map<string, number>): Map<string, number> {\n const r = new Map<string, number>(a);\n for (const [k, v] of b) {\n r.set(k, Math.max(r.get(k) ?? 0, v));\n }\n return r;\n}\n\nexport function getPersistPath(): string {\n return persistPath();\n}\n","import { aggregate, type Aggregated, type RawUsageEvent, type DayAgg } from \"../aggregation.ts\";\nimport { parseUsageEvents, readAllUsageEvents, readLiveUsageEvents } from \"./session-reader.ts\";\nimport { loadPersisted, savePersisted, mergePersistedAndLive } from \"./persist.ts\";\nimport { toDayKey } from \"../date-bucket.ts\";\n\nexport class HeatmapStore {\n private events: RawUsageEvent[] = [];\n private aggregated: Aggregated;\n private listeners = new Set<() => void>();\n private byTurnStep = new Map<string, RawUsageEvent>();\n private fallbackBySid = new Map<string, { provider: string; model: string }>();\n private ctx: any;\n private persistedDays = new Map<string, DayAgg>();\n /** True after init() has finished scanning disk; false until then so\n * getAggregated() does not serve an incomplete (persisted-only) snapshot\n * that makes wide windows like Quarter look empty until a hard refresh. */\n private initialized = false;\n\n constructor(ctx: any) {\n this.ctx = ctx;\n this.aggregated = aggregate([], Date.now());\n }\n\n async init() {\n // Load persisted cumulative history first\n this.persistedDays = loadPersisted();\n // Then scan current sessions\n const evs = await readAllUsageEvents(this.ctx);\n this.events = evs;\n this.rebuildPersistedAndAggregated();\n this.initialized = true;\n }\n\n /**\n * Refresh from LIVE sessions only. The full disk scan happened once at\n * init(); afterwards only live (in-memory) sessions change, so re-scanning\n * the whole <DSH_HOME>/sessions tree on every refresh would be wasted work.\n *\n * A live session's snapshot includes its full stored log, so we REPLACE that\n * session's events in place (never append) — history for sessions that are\n * no longer live is kept as-is from init(). This keeps each event present at\n * most once in this.events, so the max-merge into persistedDays never\n * under- or over-counts.\n */\n async refresh(): Promise<void> {\n try {\n const live = await readLiveUsageEvents(this.ctx);\n const liveSids = new Set(live.map((e) => e.sid).filter(Boolean) as string[]);\n this.events = [...this.events.filter((e) => !liveSids.has(e.sid)), ...live];\n this.persistedDays = loadPersisted();\n this.rebuildPersistedAndAggregated();\n } catch (e: unknown) {\n // B2 fix: a refresh failure must never leave the UI stuck \"Refreshing\" or\n // wipe data — keep the last good snapshot and just log. The client's\n // `finally` clears the spinner once this promise resolves normally.\n this.ctx?.logger?.warn?.(`dsh-token-pulse refresh failed: ${String(e)}`);\n }\n }\n\n private rebuildPersistedAndAggregated() {\n this.invalidateCache();\n // Build live aggregated from current events\n const live = aggregate(this.events, Date.now());\n // Merge live byDay into persisted (persisted never shrinks)\n const mergedDays = mergePersistedAndLive(this.persistedDays, live.byDay);\n // If merged grew or any live day is newer/higher, persist\n if (mergedDays.size !== this.persistedDays.size || hasAnyIncrease(this.persistedDays, mergedDays)) {\n this.persistedDays = mergedDays;\n savePersisted(this.persistedDays);\n } else {\n // Even if no size change, merged may contain new totals (new tokens) - check live vs persisted\n // For simplicity, always update persisted to merged (it contains live's latest via max merge)\n this.persistedDays = mergedDays;\n // Only save if merged differs from what we loaded (avoid unnecessary writes)\n // We already checked hasAnyIncrease\n if (!mapsEqual(loadPersisted(), mergedDays)) {\n savePersisted(this.persistedDays);\n }\n }\n // Build final aggregated from persisted truth + live totals recomputed with fresh nowMs\n // Instead of re-aggregating from raw events (which would lose deleted history), aggregate from persistedDays\n this.aggregated = aggregateFromPersisted(this.persistedDays, Date.now());\n this.emit();\n }\n\n private rebuildAggregated() {\n this.invalidateCache();\n // Legacy path: just rebuild from persisted+live (used by ingest)\n this.aggregated = aggregateFromPersisted(this.persistedDays, Date.now());\n // Also need to incorporate any events not yet persisted? But persisted already contains merged live at init/refresh time.\n // For ingest incremental: we update persistedDays optimistically\n this.emit();\n }\n\n private cachedAgg: Aggregated | null = null;\n private cachedAt = 0;\n private static CACHE_TTL_MS = 30_000;\n // B1 fix: bound per-session provider/model fallbacks (the \"\" global entry is\n // never evicted). Without a cap this Map grew for every new sid, slowly in an\n // always-on host.\n private static MAX_FALLBACK_SIDS = 2000;\n\n getAggregated(): Aggregated {\n // Before init() completes (disk scan + multi-frame zstd decode), return an\n // EMPTY snapshot so the client shows a loading state instead of caching an\n // incomplete (persisted-only) result that makes Quarter look empty until a\n // hard refresh. The client's auto-refresh (10 min) or a manual refresh will\n // pick up the complete data once init() has set this.initialized = true.\n if (!this.initialized) return aggregate([], Date.now());\n // Memoized for 30s to avoid re-aggregating on every poll; invalidated by ingest/refresh\n const now = Date.now();\n if (this.cachedAgg && now - this.cachedAt < HeatmapStore.CACHE_TTL_MS) return this.cachedAgg;\n const live = aggregate(this.events, now);\n const merged = mergePersistedAndLive(this.persistedDays, live.byDay);\n this.cachedAgg = aggregateFromPersisted(merged, now);\n this.cachedAt = now;\n return this.cachedAgg;\n }\n\n /** True once the full disk scan (init) has completed. Exposed to API consumers\n * so partial data can be distinguished from \"still loading\". */\n isReady(): boolean {\n return this.initialized;\n }\n\n private invalidateCache() {\n this.cachedAgg = null;\n this.cachedAt = 0;\n }\n\n /** Ingest a single session event incrementally. Keys are scoped per session\n * via `event.sid` (set by the plugin's session/event handler) so sessions\n * with the same turn/step do not overwrite each other. */\n ingest(event: any) {\n const sid = (event as any)?.sid ?? \"\";\n const fb = (): { provider: string; model: string } => this.fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n const setFb = (provider?: string, model?: string) => {\n const cur = this.fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (provider) cur.provider = provider;\n if (model) cur.model = model;\n this.fallbackBySid.set(sid, cur);\n // B1 fix: FIFO-evict per-session fallback entries when the map exceeds the\n // cap; keep the \"\" global entry so pre-header chunks still resolve. Map\n // iterates in insertion order, so oldest sessions get recycled first.\n if (this.fallbackBySid.size > HeatmapStore.MAX_FALLBACK_SIDS) {\n for (const k of this.fallbackBySid.keys()) {\n if (k === \"\") continue;\n this.fallbackBySid.delete(k);\n if (this.fallbackBySid.size <= HeatmapStore.MAX_FALLBACK_SIDS - 100) break;\n }\n }\n };\n if (event.type === \"request/header\" && event.data?.header?.config) {\n const cfg = event.data.header.config;\n setFb(cfg.provider, cfg.model);\n return;\n }\n if (event.type === \"request/context\" && event.data) {\n setFb(event.data.provider, event.data.model);\n return;\n }\n if (event.type === \"assistant/chunk\" && event.data?.chunk?.type === \"usage\") {\n const key = `${sid}::${event.data.turn}:${event.data.step}`;\n if (!this.byTurnStep.has(key)) {\n const f = fb();\n const u = event.data.chunk.usage;\n const ev: RawUsageEvent = {\n time: event.time ?? Date.now(),\n provider: f.provider,\n model: f.model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n };\n this.byTurnStep.set(key, ev);\n this.events.push(ev);\n this.persistIncremental(ev);\n this.rebuildAggregated();\n }\n return;\n }\n if (event.type === \"assistant/message\" && event.data?.usage) {\n const msg = event.data.message;\n const src = msg?.source;\n const f = fb();\n const provider = src?.provider ?? f.provider;\n const model = src?.model ?? f.model;\n const turn = event.data.turn;\n const step = event.data.step;\n const key = turn !== undefined && step !== undefined ? `${sid}::${turn}:${step}` : `${sid}::msg:${event.seq ?? Math.random()}`;\n const existing = this.byTurnStep.get(key);\n const u = event.data.usage;\n const ev: RawUsageEvent = {\n time: event.time ?? Date.now(),\n provider,\n model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n };\n if (existing) {\n const idx = this.events.indexOf(existing);\n if (idx !== -1) this.events.splice(idx, 1);\n // also revert persisted contribution? No - persisted is max, so removing provisional doesn't shrink persisted; just replace\n }\n this.byTurnStep.set(key, ev);\n this.events.push(ev);\n setFb(src?.provider, src?.model);\n this.persistIncremental(ev);\n this.rebuildAggregated();\n }\n }\n\n private persistIncremental(ev: RawUsageEvent) {\n const live = aggregate(this.events, Date.now());\n const dayKey = toDayKey(ev.time);\n const liveDay = live.byDay.get(dayKey);\n if (!liveDay) return;\n const prev = this.persistedDays.get(dayKey);\n const merged = prev ? mergeTwoDays(prev, liveDay) : cloneDay(liveDay);\n this.persistedDays.set(dayKey, merged);\n savePersisted(this.persistedDays);\n }\n\n onChange(cb: () => void): () => void {\n this.listeners.add(cb);\n return () => this.listeners.delete(cb);\n }\n\n private emit() {\n for (const cb of this.listeners) cb();\n }\n\n dispose() {\n this.listeners.clear();\n }\n\n /** For testing: inject events directly */\n _injectEvents(evs: RawUsageEvent[]) {\n this.events.push(...evs);\n this.rebuildAggregated();\n }\n\n // Exposed for settings UI / manual refresh\n getPersistedDays(): Map<string, DayAgg> {\n return new Map(this.persistedDays);\n }\n\n clearPersisted() {\n this.persistedDays.clear();\n savePersisted(this.persistedDays);\n this.rebuildAggregated();\n }\n}\n\nimport { weekRangeFor, monthRangeFor } from \"../date-bucket.ts\";\n\n// Minimal helpers reused from persist.ts logic but need local clones\nfunction cloneDay(d: DayAgg): DayAgg {\n return {\n dayKey: d.dayKey,\n totalTokens: d.totalTokens,\n uncachedInputTokens: d.uncachedInputTokens,\n cacheReadTokens: d.cacheReadTokens,\n cacheWriteTokens: d.cacheWriteTokens,\n outputTokens: d.outputTokens,\n count: d.count,\n byModel: new Map(d.byModel),\n byProvider: new Map(d.byProvider),\n hourlyTokens: [...(d.hourlyTokens ?? new Array(24).fill(0))],\n winnerModel: d.winnerModel,\n winnerProvider: d.winnerProvider,\n };\n}\n\nfunction mergeTwoDays(prev: DayAgg, live: DayAgg): DayAgg {\n const byModel = new Map(prev.byModel);\n for (const [k, v] of live.byModel) byModel.set(k, Math.max(byModel.get(k) ?? 0, v));\n const byProvider = new Map(prev.byProvider);\n for (const [k, v] of live.byProvider) byProvider.set(k, Math.max(byProvider.get(k) ?? 0, v));\n // hourly also max-merged per hour — dropping it here used to zero every\n // live-ingested day's hour bars on save.\n const hourlyTokens = (prev.hourlyTokens ?? new Array(24).fill(0))\n .map((v, i) => Math.max(v, live.hourlyTokens?.[i] ?? 0));\n const merged: DayAgg = {\n dayKey: prev.dayKey,\n totalTokens: Math.max(prev.totalTokens, live.totalTokens),\n uncachedInputTokens: Math.max(prev.uncachedInputTokens, live.uncachedInputTokens),\n cacheReadTokens: Math.max(prev.cacheReadTokens, live.cacheReadTokens),\n cacheWriteTokens: Math.max(prev.cacheWriteTokens, live.cacheWriteTokens),\n outputTokens: Math.max(prev.outputTokens, live.outputTokens),\n count: Math.max(prev.count, live.count),\n byModel,\n byProvider,\n hourlyTokens,\n winnerModel: null,\n winnerProvider: null,\n };\n let bestM: string | null = null;\n let bestMVal = -1;\n for (const [m, v2] of byModel) if (v2 > bestMVal) { bestMVal = v2; bestM = m; }\n merged.winnerModel = bestM;\n let bestP: string | null = null;\n let bestPVal = -1;\n for (const [p, v2] of byProvider) if (v2 > bestPVal) { bestPVal = v2; bestP = p; }\n merged.winnerProvider = bestP;\n return merged;\n}\n\nfunction hasAnyIncrease(a: Map<string, DayAgg>, b: Map<string, DayAgg>): boolean {\n for (const [k, vb] of b) {\n const va = a.get(k);\n if (!va) return true;\n if (vb.totalTokens > va.totalTokens) return true;\n if (vb.count > va.count) return true;\n }\n return false;\n}\n\nfunction mapsEqual(a: Map<string, DayAgg>, b: Map<string, DayAgg>): boolean {\n if (a.size !== b.size) return false;\n for (const [k, va] of a) {\n const vb = b.get(k);\n if (!vb) return false;\n if (va.totalTokens !== vb.totalTokens || va.count !== vb.count) return false;\n }\n return true;\n}\n\nfunction aggregateFromPersisted(persistedDays: Map<string, DayAgg>, nowMs: number): Aggregated {\n const fakeEvents: RawUsageEvent[] = [];\n for (const day of persistedDays.values()) {\n for (const [model, tokens] of day.byModel) {\n fakeEvents.push({\n time: new Date(day.dayKey + \"T12:00:00+08:00\").getTime(),\n provider: day.winnerProvider ?? \"unknown\",\n model,\n usage: { inputTokens: tokens, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 },\n });\n }\n if (day.byModel.size === 0 && day.totalTokens > 0) {\n fakeEvents.push({\n time: new Date(day.dayKey + \"T12:00:00+08:00\").getTime(),\n provider: day.winnerProvider ?? \"unknown\",\n model: day.winnerModel ?? \"unknown\",\n usage: { inputTokens: day.totalTokens, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 },\n });\n }\n }\n const agg = aggregate(fakeEvents, nowMs);\n const todayKey = toDayKey(nowMs);\n const [wS, wE] = weekRangeFor(todayKey);\n const [mS, mE] = monthRangeFor(todayKey);\n const sumRange = (s: string, e: string) => {\n let sum = 0;\n let cur = s;\n while (cur <= e) {\n sum += persistedDays.get(cur)?.totalTokens ?? 0;\n const d = new Date(cur + \"T12:00:00+08:00\");\n d.setDate(d.getDate() + 1);\n cur = toDayKey(d.getTime());\n if (cur > e) break;\n }\n return sum;\n };\n let all = 0;\n for (const v of persistedDays.values()) all += v.totalTokens;\n const today = persistedDays.get(todayKey)?.totalTokens ?? 0;\n const thisWeek = sumRange(wS, wE);\n const thisMonth = sumRange(mS, mE);\n (agg as any).totals = { today, thisWeek, thisMonth, all };\n (agg as any).byDay = new Map(persistedDays);\n return agg;\n}\n","import { HeatmapStore } from \"./host/store.ts\";\n\nexport const name = \"dsh-token-pulse\";\nexport const inject = [\"sessions\"] as const;\n\nexport function apply(ctx: any) {\n const store = new HeatmapStore(ctx);\n ctx.provide(\"heatmapStore\", store);\n (ctx as any).heatmapStore = store;\n\n let disposed = false;\n\n store.init().catch((e: unknown) => {\n ctx.logger?.warn?.(`dsh-token-pulse init failed: ${String(e)}`);\n });\n\n ctx.on(\"session/event\", (session: any, event: any) => {\n if (disposed) return;\n // Tag with the session id so per-session turn/step keys never collide.\n store.ingest({ ...event, sid: session?.id ?? \"\" });\n });\n\n ctx.on(\"session/flush\", () => {});\n\n // Serve persisted data for browser client via /api/dsh-token-pulse/daily.json\n let unregisterRoute: (() => void) | null = null;\n ctx.inject([\"webServer\"], (wsCtx: any) => {\n try {\n const handler = (req: any, res: any) => {\n try {\n const agg = store.getAggregated();\n const days = [...agg.byDay.values()].map((d) => ({\n dayKey: d.dayKey,\n totalTokens: d.totalTokens,\n uncachedInputTokens: d.uncachedInputTokens,\n cacheReadTokens: d.cacheReadTokens,\n cacheWriteTokens: d.cacheWriteTokens,\n outputTokens: d.outputTokens,\n count: d.count,\n byModel: Object.fromEntries(d.byModel),\n byProvider: Object.fromEntries(d.byProvider),\n hourlyTokens: d.hourlyTokens,\n winnerModel: d.winnerModel,\n winnerProvider: d.winnerProvider,\n }));\n const body = JSON.stringify({\n version: 1 as const,\n ready: store.isReady(),\n totals: agg.totals,\n topModels: agg.topModels.map((x) => ({ name: x.model, tokens: x.tokens })),\n topProviders: agg.topProviders.map((x) => ({ name: x.provider, tokens: x.tokens })),\n days,\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\", \"Cache-Control\": \"no-store\" });\n res.end(body);\n } catch (e) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: String(e) }));\n }\n };\n unregisterRoute = wsCtx.webServer.register({ kind: \"exact\", path: \"/api/dsh-token-pulse/daily.json\", handler });\n ctx.logger.info(\"dsh-token-pulse: registered /api/dsh-token-pulse/daily.json\");\n } catch (e) {\n ctx.logger?.warn?.(`dsh-token-pulse route register failed: ${String(e)}`);\n }\n });\n\n ctx.effect(\n () => () => {\n disposed = true;\n store.dispose();\n if (unregisterRoute) try { unregisterRoute(); } catch {}\n },\n \"dsh-token-pulse.dispose\",\n );\n\n ctx.logger.info(\"dsh-token-pulse host up\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["cloneDay"],"sources":["../src/date-bucket.ts","../src/aggregation.ts","../src/host/dsh-home.ts","../src/host/session-reader.ts","../src/host/persist.ts","../src/host/store.ts","../src/index.ts"],"sourcesContent":["const TZ = \"Asia/Shanghai\";\n\nexport function toDayKey(ms: number, tz = TZ): string {\n const fmt = new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: tz,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n });\n return fmt.format(new Date(ms));\n}\n\nexport function dayKeyToDate(key: string, _tz = TZ): Date {\n return new Date(key + \"T12:00:00+08:00\");\n}\n\nexport function weekStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n const dow = (d.getDay() + 6) % 7; // Mon=0\n d.setDate(d.getDate() - dow);\n return toDayKey(d.getTime());\n}\n\nexport function monthStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-01`;\n}\n\nexport function quarterStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n const q = Math.floor(d.getMonth() / 3);\n const m = q * 3 + 1;\n return `${d.getFullYear()}-${String(m).padStart(2, \"0\")}-01`;\n}\n\nexport function yearStartOf(dayKey: string): string {\n const d = dayKeyToDate(dayKey);\n return `${d.getFullYear()}-01-01`;\n}\n\nexport function addDays(dayKey: string, n: number): string {\n const d = dayKeyToDate(dayKey);\n d.setDate(d.getDate() + n);\n return toDayKey(d.getTime());\n}\n\nexport function listDaysInRange(startKey: string, endKey: string): string[] {\n const result: string[] = [];\n let cur = startKey;\n while (cur <= endKey) {\n result.push(cur);\n cur = addDays(cur, 1);\n }\n return result;\n}\n\nexport function weekRangeFor(dateKey: string): [string, string] {\n const s = weekStartOf(dateKey);\n return [s, addDays(s, 6)];\n}\n\nexport function monthRangeFor(dateKey: string): [string, string] {\n const s = monthStartOf(dateKey);\n const d = dayKeyToDate(s);\n const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();\n return [s, `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(lastDay).padStart(2, \"0\")}`];\n}\n\n/** Last ~90 days: sliding window for the \"Quarter\" tab (not calendar quarter). */\nexport function quarterRangeFor(dateKey: string): [string, string] {\n return [addDays(dateKey, -89), dateKey];\n}\n\nexport function yearRangeFor(dateKey: string): [string, string] {\n const s = yearStartOf(dateKey);\n const d = dayKeyToDate(s);\n return [s, `${d.getFullYear()}-12-31`];\n}\n","import {\n toDayKey,\n addDays,\n listDaysInRange,\n weekRangeFor,\n monthRangeFor,\n quarterRangeFor,\n yearRangeFor,\n} from \"./date-bucket.ts\";\n\nexport type RawUsageEvent = {\n time: number;\n provider: string;\n model: string;\n usage: {\n inputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n outputTokens: number;\n };\n /** Session id this usage belongs to. Used by HeatmapStore.refresh() to\n * replace a live session's events in place without duplicating history. */\n sid?: string;\n};\n\nexport type DayAgg = {\n dayKey: string;\n totalTokens: number;\n uncachedInputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n outputTokens: number;\n count: number;\n byModel: Map<string, number>;\n byProvider: Map<string, number>;\n /** 24 hourly buckets in Asia/Shanghai, index 0 = 00:00-01:00 */\n hourlyTokens: number[];\n winnerModel: string | null;\n winnerProvider: string | null;\n};\n\nexport type TopItem = { name: string; tokens: number };\n\nexport type Aggregated = {\n byDay: Map<string, DayAgg>;\n totals: { today: number; thisWeek: number; thisMonth: number; all: number };\n topModels: Array<{ model: string; tokens: number }>;\n topProviders: Array<{ provider: string; tokens: number }>;\n /** @deprecated use topModels */\n top5: Array<{ model: string; tokens: number }>;\n weekDays(dayKey: string): DayAgg[];\n monthDays(dayKey: string): DayAgg[];\n quarterDays(dayKey: string): DayAgg[];\n yearDays(dayKey: string): DayAgg[];\n top5InWindow(days: DayAgg[]): Array<{ model: string; tokens: number }>;\n topProvidersInWindow(days: DayAgg[]): Array<{ provider: string; tokens: number }>;\n};\n\nfunction emptyDay(dayKey: string): DayAgg {\n return {\n dayKey,\n totalTokens: 0,\n uncachedInputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n outputTokens: 0,\n count: 0,\n byModel: new Map(),\n byProvider: new Map(),\n hourlyTokens: new Array(24).fill(0),\n winnerModel: null,\n winnerProvider: null,\n };\n}\n\nfunction sumByDay(days: DayAgg[]): number {\n return days.reduce((s, d) => s + d.totalTokens, 0);\n}\n\nfunction resolveDay(byDay: Map<string, DayAgg>, key: string): DayAgg {\n return byDay.get(key) ?? emptyDay(key);\n}\n\nfunction resolveRange(byDay: Map<string, DayAgg>, start: string, end: string): DayAgg[] {\n return listDaysInRange(start, end).map((k) => resolveDay(byDay, k));\n}\n\n/**\n * 5-level fallback for callers without a view-aware max (e.g. legacy tests).\n * For view-aware quantile, use levelForView().\n */\nexport function clampLevel(value: number): 0 | 1 | 2 | 3 | 4 {\n if (value === 0) return 0;\n if (value <= 2000) return 1;\n if (value <= 10000) return 2;\n if (value <= 50000) return 3;\n return 4;\n}\n\n/** View-aware quantile: 0, (0,25%], (25%,50%], (50%,75%], (75%,100%] so month/quarter spread across the palette. */\nexport function levelForView(value: number, viewMax: number): 0 | 1 | 2 | 3 | 4 {\n if (value === 0) return 0;\n if (viewMax <= 0) return 1;\n const r = value / viewMax;\n if (r <= 0.25) return 1;\n if (r <= 0.5) return 2;\n if (r <= 0.75) return 3;\n return 4;\n}\n\n/** Minimum number of non-zero values before rank-based bucketing kicks in. Below this we fall back to logLevel(). */\nexport const MIN_RANK_POINTS = 5;\n\n/**\n * Rank-based (percentile) level. Outlier-proof: no matter how large one day is,\n * lower values still spread across levels 1-4 by their position among non-zero\n * values — a single huge day no longer flattens everything to level 1.\n * `sortedNonZero` must be the ascending-sorted array of all non-zero values in\n * the view. Uses the midpoint of the \"below\" and \"at-or-below\" ranks, so ties\n * sit in the middle of their band; the maximum always lands on level 4 and the\n * minimum on level 1, guaranteeing all four colored levels appear (n ≥ 2).\n * 0 maps to 0.\n */\nexport function levelByRank(value: number, sortedNonZero: number[]): 0 | 1 | 2 | 3 | 4 {\n if (value <= 0) return 0;\n const n = sortedNonZero.length;\n if (n === 0) return 1;\n let below = 0;\n let atOrBelow = 0;\n for (const v of sortedNonZero) {\n if (v < value) below++;\n if (v <= value) atOrBelow++;\n }\n const rank = (below / n + atOrBelow / n) / 2; // midpoint rank ∈ (0, 1)\n if (rank <= 0.25) return 1; // bottom quarter\n if (rank <= 0.5) return 2; // 25-50%\n if (rank <= 0.75) return 3; // 50-75%\n return 4; // top quarter, max always here\n}\n\n/**\n * Log-scaled ratio fallback for sparse data (< MIN_RANK_POINTS non-zero values).\n * Compresses outliers logarithmically so a single large value doesn't flatten\n * the rest, while keeping \"larger = darker\" intuition.\n */\nexport function logLevel(value: number, viewMax: number): 0 | 1 | 2 | 3 | 4 {\n if (value <= 0) return 0;\n if (viewMax <= 0) return 1;\n const r = Math.log1p(value) / Math.log1p(viewMax);\n if (r <= 0.25) return 1;\n if (r <= 0.5) return 2;\n if (r <= 0.75) return 3;\n return 4;\n}\n\nfunction hourInShanghai(ms: number): number {\n // Convert to Asia/Shanghai hour 0-23. en-GB is used because en-US may emit\n // \"24\" for midnight on some ICU data; we also defensively clamp.\n const s = new Date(ms).toLocaleString(\"en-GB\", { timeZone: \"Asia/Shanghai\", hour: \"2-digit\", hour12: false });\n const h = Number(s);\n return h === 24 ? 0 : h;\n}\n\nexport function aggregate(events: RawUsageEvent[], nowMs: number): Aggregated {\n const byDay = new Map<string, DayAgg>();\n const globalByModel = new Map<string, number>();\n const globalByProvider = new Map<string, number>();\n\n for (const ev of events) {\n const dayKey = toDayKey(ev.time);\n let agg = byDay.get(dayKey);\n if (!agg) {\n agg = emptyDay(dayKey);\n byDay.set(dayKey, agg);\n }\n const u = ev.usage;\n const total = (u.inputTokens ?? 0) + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0) + (u.outputTokens ?? 0);\n agg.totalTokens += total;\n agg.uncachedInputTokens += u.inputTokens ?? 0;\n agg.cacheReadTokens += u.cacheReadTokens ?? 0;\n agg.cacheWriteTokens += u.cacheWriteTokens ?? 0;\n agg.outputTokens += u.outputTokens ?? 0;\n agg.count += 1;\n const h = hourInShanghai(ev.time);\n if (h >= 0 && h < 24) agg.hourlyTokens[h] += total;\n\n const modelKey = ev.model || \"unknown\";\n const providerKey = ev.provider || \"unknown\";\n agg.byModel.set(modelKey, (agg.byModel.get(modelKey) ?? 0) + total);\n agg.byProvider.set(providerKey, (agg.byProvider.get(providerKey) ?? 0) + total);\n globalByModel.set(modelKey, (globalByModel.get(modelKey) ?? 0) + total);\n globalByProvider.set(providerKey, (globalByProvider.get(providerKey) ?? 0) + total);\n }\n\n // compute winners per day\n for (const agg of byDay.values()) {\n let bestM: string | null = null;\n let bestMVal = -1;\n for (const [m, v] of agg.byModel) {\n if (v > bestMVal) {\n bestMVal = v;\n bestM = m;\n }\n }\n agg.winnerModel = bestM;\n\n let bestP: string | null = null;\n let bestPVal = -1;\n for (const [p, v] of agg.byProvider) {\n if (v > bestPVal) {\n bestPVal = v;\n bestP = p;\n }\n }\n agg.winnerProvider = bestP;\n }\n\n const todayKey = toDayKey(nowMs);\n const [wStart, wEnd] = weekRangeFor(todayKey);\n const [mStart, mEnd] = monthRangeFor(todayKey);\n\n const today = byDay.get(todayKey)?.totalTokens ?? 0;\n const thisWeek = sumByDay(resolveRange(byDay, wStart, wEnd));\n const thisMonth = sumByDay(resolveRange(byDay, mStart, mEnd));\n let all = 0;\n for (const v of byDay.values()) all += v.totalTokens;\n\n const topModels = [...globalByModel.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([model, tokens]) => ({ model, tokens }));\n\n const topProviders = [...globalByProvider.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([provider, tokens]) => ({ provider, tokens }));\n\n function top5InWindow(days: DayAgg[]): Array<{ model: string; tokens: number }> {\n const m = new Map<string, number>();\n for (const d of days) {\n for (const [model, tokens] of d.byModel) {\n m.set(model, (m.get(model) ?? 0) + tokens);\n }\n }\n return [...m.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([model, tokens]) => ({ model, tokens }));\n }\n\n function topProvidersInWindow(days: DayAgg[]): Array<{ provider: string; tokens: number }> {\n const m = new Map<string, number>();\n for (const d of days) {\n for (const [provider, tokens] of d.byProvider) {\n m.set(provider, (m.get(provider) ?? 0) + tokens);\n }\n }\n return [...m.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([provider, tokens]) => ({ provider, tokens }));\n }\n\n return {\n byDay,\n totals: { today, thisWeek, thisMonth, all },\n topModels,\n topProviders,\n top5: topModels,\n weekDays: (k: string) => {\n const [s, e] = weekRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n monthDays: (k: string) => {\n const [s, e] = monthRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n quarterDays: (k: string) => {\n const [s, e] = quarterRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n yearDays: (k: string) => {\n const [s, e] = yearRangeFor(k);\n return resolveRange(byDay, s, e);\n },\n top5InWindow,\n topProvidersInWindow,\n };\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Resolve the DSH home directory.\n *\n * A machine may run several independent DSH installs side by side, each with\n * its own directory (sessions, storages, profiles, ...). The active install\n * advertises itself via `$DSH_HOME`, so every path the plugin touches MUST\n * derive from it. Only when the variable is absent (e.g. unit tests, very\n * old launchers) do we fall back to the default `~/.dsh` — mirroring core.\n */\nexport function dshHome(): string {\n const env = process.env.DSH_HOME?.trim();\n if (env) return env;\n return join(homedir(), \".dsh\");\n}\n\n/** `<DSH_HOME>/sessions` — where session logs live. */\nexport function sessionsDir(): string {\n return join(dshHome(), \"sessions\");\n}\n\n/** `<DSH_HOME>/storages` — where plugins persist local data. */\nexport function storagesDir(): string {\n return join(dshHome(), \"storages\");\n}\n","import type { RawUsageEvent } from \"../aggregation.ts\";\nimport { sessionsDir } from \"./dsh-home.ts\";\n\n/** zstd frame magic: bytes 28 B5 2F FD, read as UInt32LE. */\nconst ZSTD_MAGIC = 0xfd2fb528;\n\n/**\n * Locate complete zstd frames inside a concatenated-frame container (the\n * format DSH's session.jsonl.zstd uses: every appended batch is its own\n * compressed frame). Scans the frame structure WITHOUT decompressing, mirroring\n * @deepseek-ai/dsh-session-persistence-jsonl's scanZstdFrames. Returns\n * [start,end) byte ranges, one per complete frame.\n */\nexport function scanZstdFrames(buffer: Buffer): Array<{ start: number; end: number }> {\n const frames: Array<{ start: number; end: number }> = [];\n let offset = 0;\n while (offset < buffer.length) {\n const start = offset;\n if (buffer.length - offset < 4) break;\n if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) break; // torn/corrupt tail: stop\n offset += 4;\n if (offset === buffer.length) break;\n const descriptor = buffer.readUInt8(offset);\n offset += 1;\n if ((descriptor & 24) !== 0) break; // reserved frame-header bit\n const contentSizeFlag = descriptor >>> 6;\n const singleSegment = (descriptor & 32) !== 0;\n const checksum = (descriptor & 4) !== 0;\n const dictionaryFlag = descriptor & 3;\n const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;\n const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag;\n const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;\n if (buffer.length - offset < remainingHeaderBytes) break;\n offset += remainingHeaderBytes;\n for (;;) {\n if (buffer.length - offset < 3) break;\n const blockHeader = buffer.readUIntLE(offset, 3);\n offset += 3;\n const lastBlock = (blockHeader & 1) !== 0;\n const blockType = (blockHeader >>> 1) & 3;\n const blockSize = blockHeader >>> 3;\n if (blockType === 3) break; // reserved block type\n const payloadBytes = blockType === 1 ? 1 : blockSize; // RLE payload is 1 byte\n if (buffer.length - offset < payloadBytes) break;\n offset += payloadBytes;\n if (lastBlock) break;\n }\n if (checksum) {\n if (buffer.length - offset < 4) break;\n offset += 4;\n }\n frames.push({ start, end: offset });\n }\n return frames;\n}\n\n/**\n * Decode a concatenated-frame zstd session log to its full UTF-8 text.\n * Node's one-shot `zstdDecompressSync` decodes only the FIRST frame, so we\n * locate every frame and decode each one separately (per\n * dsh-session-persistence-jsonl's zstd-public-decoder). Returns null on any\n * decode failure (callers treat a null text as \"no data from this file\").\n */\nexport async function decodeSessionZstd(buffer: Buffer): Promise<string | null> {\n const zlib = await import(\"node:zlib\");\n if (typeof zlib.zstdDecompressSync !== \"function\") return null; // caller falls back to CLI\n const frames = scanZstdFrames(buffer);\n if (frames.length === 0) return null;\n const parts: string[] = [];\n for (const { start, end } of frames) {\n try {\n parts.push(zlib.zstdDecompressSync(buffer.subarray(start, end)).toString(\"utf-8\"));\n } catch {\n return null; // corrupt frame: treat whole file as unreadable\n }\n }\n return parts.join(\"\");\n}\n\ntype SessionEvent = {\n type: string;\n time?: number;\n seq?: number;\n data?: any;\n surfaceOp?: any;\n /** Session id this event belongs to. Events do NOT carry it in the log; the\n * reader tags it so turn/step keys stay scoped per session. */\n sid?: string;\n};\n\n/**\n * Parse a batch of raw session events into RawUsageEvents.\n * Rules:\n * - request/header stores provider/model for fallback (per session)\n * - assistant/chunk type=usage is provisional, keyed by sid/turn/step\n * - assistant/message with usage is authoritative, overwrites provisional for same sid/turn/step\n *\n * The dedupe key MUST include the session id: turn/step counters restart per\n * session (every session has a turn 1 step 1), so without `sid` different\n * sessions overwrite each other and older sessions' usage silently disappears.\n */\nexport function parseUsageEvents(events: SessionEvent[]): RawUsageEvent[] {\n // session id -> fallback provider/model\n const fallbackBySid = new Map<string, { provider: string; model: string }>();\n const fallbackFor = (sid: string | undefined) => fallbackBySid.get(sid ?? \"\") ?? { provider: \"\", model: \"\" };\n // sid/turn/step -> RawUsageEvent (provisional or final)\n const byTurnStep = new Map<string, RawUsageEvent>();\n\n for (const ev of events) {\n const sid = ev.sid ?? \"\";\n if (ev.type === \"request/header\" && ev.data?.header?.config) {\n const cfg = ev.data.header.config;\n const fb = fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (cfg.provider) fb.provider = cfg.provider;\n if (cfg.model) fb.model = cfg.model;\n fallbackBySid.set(sid, fb);\n }\n if (ev.type === \"request/context\" && ev.data) {\n const fb = fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (ev.data.provider) fb.provider = ev.data.provider;\n if (ev.data.model) fb.model = ev.data.model;\n fallbackBySid.set(sid, fb);\n }\n if (ev.type === \"assistant/chunk\" && ev.data?.chunk?.type === \"usage\") {\n const key = `${sid}::${ev.data.turn}:${ev.data.step}`;\n // Only set if not already finalized by a message\n if (!byTurnStep.has(key)) {\n const fb = fallbackFor(sid);\n const u = ev.data.chunk.usage;\n byTurnStep.set(key, {\n time: ev.time ?? Date.now(),\n provider: fb.provider,\n model: fb.model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n });\n }\n }\n if (ev.type === \"assistant/message\" && ev.data?.usage) {\n const msg = ev.data.message;\n const src = msg?.source;\n const fb = fallbackFor(sid);\n const provider = src?.provider ?? fb.provider;\n const model = src?.model ?? fb.model;\n const turn = ev.data.turn;\n const step = ev.data.step;\n const key = turn !== undefined && step !== undefined ? `${sid}::${turn}:${step}` : `${sid}::msg:${ev.seq ?? Math.random()}`;\n const u = ev.data.usage;\n byTurnStep.set(key, {\n time: ev.time ?? Date.now(),\n provider,\n model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n });\n // Also update fallback from message source for subsequent chunks\n const cur = fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (src?.provider) cur.provider = src.provider;\n if (src?.model) cur.model = src.model;\n fallbackBySid.set(sid, cur);\n }\n }\n\n return [...byTurnStep.values()];\n}\n\n/**\n * Read all sessions' usage events: FULL disk history (every session, old + new)\n * merged with live in-memory events that are not yet flushed to disk.\n *\n * NOTE: ctx.sessions.list() returns only LIVE sessions (created/resumed in the\n * current process) — historical sessions from previous processes are NOT in it.\n * The old implementation returned early whenever any live session had events,\n * so it never scanned the disk and old sessions never showed up. Here we ALWAYS\n * read the whole <DSH_HOME>/sessions tree and merge live events on top, deduping by\n * sessionId:seq (events carry no sessionId; the disk path derives it from the\n * session directory name).\n */\nexport async function readAllUsageEvents(ctx: any): Promise<RawUsageEvent[]> {\n const allEvents: SessionEvent[] = [];\n const seen = new Set<string>();\n\n const relevant = (o: any) =>\n o.type === \"request/header\" || o.type === \"request/context\" ||\n o.type === \"assistant/chunk\" || o.type === \"assistant/message\";\n\n const pushUnique = (sid: string, o: any) => {\n if (typeof o?.seq === \"number\") {\n const key = `${sid}:${o.seq}`;\n if (seen.has(key)) return;\n seen.add(key);\n }\n allEvents.push({ ...o, sid });\n };\n\n // 1) Full history: scan <DSH_HOME>/sessions (all sessions, including old ones).\n try {\n const { readdirSync, existsSync } = await import(\"node:fs\");\n const { join, basename, dirname } = await import(\"node:path\");\n const dir = sessionsDir();\n if (existsSync(dir)) {\n // DSH 0.1.7+ writes generation-numbered logs (session.v4.jsonl.zstd, ...)\n // and — like core — a session directory may temporarily hold several\n // generations after an upgrade. Only the numerically HIGHEST generation\n // per directory is canonical; reading lower ones would double-count.\n const best = new Map<string, { gen: number; path: string }>();\n (function walk(dir: string) {\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, entry.name);\n if (entry.isDirectory()) walk(p);\n // session.jsonl[.zstd] (v0) and session.vN.jsonl[.zstd] (generation N)\n else {\n const m = /^session(\\.v(\\d+))?\\.jsonl(\\.zstd)?$/.exec(entry.name);\n if (!m) continue;\n const gen = m[2] ? Number(m[2]) : 0;\n const cur = best.get(dir);\n if (!cur || gen > cur.gen) best.set(dir, { gen, path: p });\n }\n }\n } catch {}\n })(dir);\n const files = [...best.values()].sort((a, b) => (a.path < b.path ? -1 : 1)).map((e) => e.path);\n\n for (const f of files) {\n // <root>/<workspace>/<sessionId>/session.jsonl.zstd\n const sid = basename(dirname(f));\n try {\n let text: string;\n if (f.endsWith(\".zstd\")) {\n const { readFileSync } = await import(\"node:fs\");\n // DSH session logs are concatenated-frame zstd: decode EVERY frame,\n // not just the first (one-shot zstdDecompressSync only yields frame 1).\n const textFromFrames = await decodeSessionZstd(readFileSync(f));\n if (textFromFrames !== null) {\n text = textFromFrames;\n } else {\n // Fallback for Node < 23.8 (no built-in zstd): external CLI required.\n const { execSync } = await import(\"node:child_process\");\n text = execSync(`zstd -dc ${JSON.stringify(f)}`, { encoding: \"utf-8\", maxBuffer: 50 * 1024 * 1024 });\n }\n } else {\n const { readFileSync } = await import(\"node:fs\");\n text = readFileSync(f, \"utf-8\");\n }\n for (const line of text.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const obj = JSON.parse(line);\n if (relevant(obj)) pushUnique(sid, obj);\n } catch {}\n }\n } catch {}\n }\n }\n } catch {}\n\n // 2) Live sessions: merge events not yet flushed to disk (dedupe by sid:seq).\n try {\n if (ctx.sessions?.list) {\n const sessions = await ctx.sessions.list();\n for (const s of sessions ?? []) {\n const sid = s?.id ?? \"live\";\n for (const event of (s as any)?.events ?? []) {\n if (relevant(event)) pushUnique(sid, event);\n }\n }\n }\n } catch {}\n\n return parseUsageEvents(allEvents);\n}\n\n/**\n * Read usage from LIVE sessions only (ctx.sessions.list() = sessions created\n * or resumed in the current process). Cheap — no disk scan. Used by\n * store.refresh() after init() has already captured the full disk history,\n * so refreshes do not re-decompress <DSH_HOME>/sessions on every tick.\n *\n * A live session's `events` includes its full stored log, so the result is a\n * complete snapshot per live session; callers must REPLACE (not append) that\n * session's events to avoid double counting.\n */\nexport async function readLiveUsageEvents(ctx: any): Promise<RawUsageEvent[]> {\n const events: SessionEvent[] = [];\n const relevant = (o: any) =>\n o.type === \"request/header\" || o.type === \"request/context\" ||\n o.type === \"assistant/chunk\" || o.type === \"assistant/message\";\n try {\n if (ctx.sessions?.list) {\n const sessions = await ctx.sessions.list();\n for (const s of sessions ?? []) {\n const sid = s?.id ?? \"live\";\n for (const event of (s as any)?.events ?? []) {\n if (relevant(event)) events.push({ ...event, sid });\n }\n }\n }\n } catch {}\n return parseUsageEvents(events);\n}\n","import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { dshHome } from \"./dsh-home.ts\";\nimport type { DayAgg } from \"../aggregation.ts\";\n\nexport type PersistedDay = {\n dayKey: string;\n totalTokens: number;\n uncachedInputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n outputTokens: number;\n count: number;\n byModel: Record<string, number>;\n byProvider: Record<string, number>;\n hourlyTokens: number[];\n winnerModel: string | null;\n winnerProvider: string | null;\n};\n\nexport type PersistedFile = {\n version: 1;\n days: Record<string, PersistedDay>;\n};\n\nfunction persistPath(): string {\n const dir = join(dshHome(), \"storages\", \"dsh-token-pulse\");\n return join(dir, \"daily.json\");\n}\n\n/**\n * One-time migration from the pre-rename identity `dsh-token-heatmap`.\n * The plugin was renamed to `dsh-token-pulse` (to avoid a bare-name collision\n * with an unrelated plugin in the awesome-dsh-plugin catalog). Existing users\n * have their accumulated history under storages/dsh-token-heatmap/ and their\n * settings under localStorage keys dsh-token-heatmap:*; without this they would\n * see an empty heatmap after the update. Copy the persisted file forward the\n * first time the new location is missing but the legacy one exists.\n */\nlet migrated = false;\nfunction migrateLegacyPersist(): void {\n if (migrated) return;\n migrated = true;\n try {\n const next = persistPath();\n if (existsSync(next)) return;\n const legacy = join(dshHome(), \"storages\", \"dsh-token-heatmap\", \"daily.json\");\n if (!existsSync(legacy)) return;\n mkdirSync(dirname(next), { recursive: true });\n copyFileSync(legacy, next);\n } catch {\n // best-effort: a failed migration must not break loading\n }\n}\n\nfunction libDailyPath(): string | null {\n try {\n // Resolve the installed plugin's own lib/ from this module's location\n // instead of hardcoding a profile name: this works under any profile and\n // any DSH install. The browser client fetches via the /api route, so this\n // copy is only a legacy fallback.\n const here = dirname(fileURLToPath(import.meta.url));\n const c = join(here, \"daily.json\");\n try {\n mkdirSync(here, { recursive: true });\n return c;\n } catch {\n return null;\n }\n } catch {\n return null;\n }\n}\n\nfunction toPersistedDay(day: DayAgg): PersistedDay {\n return {\n dayKey: day.dayKey,\n totalTokens: day.totalTokens,\n uncachedInputTokens: day.uncachedInputTokens,\n cacheReadTokens: day.cacheReadTokens,\n cacheWriteTokens: day.cacheWriteTokens,\n outputTokens: day.outputTokens,\n count: day.count,\n byModel: Object.fromEntries(day.byModel),\n byProvider: Object.fromEntries(day.byProvider),\n hourlyTokens: [...(day.hourlyTokens ?? new Array(24).fill(0))],\n winnerModel: day.winnerModel,\n winnerProvider: day.winnerProvider,\n };\n}\n\nfunction fromPersistedDay(p: PersistedDay): DayAgg {\n const byModel = new Map<string, number>(Object.entries(p.byModel ?? {}));\n const byProvider = new Map<string, number>(Object.entries(p.byProvider ?? {}));\n const hourlyTokens = Array.isArray((p as any).hourlyTokens) && (p as any).hourlyTokens.length === 24\n ? [...(p as any).hourlyTokens]\n : new Array(24).fill(0);\n return {\n dayKey: p.dayKey,\n totalTokens: p.totalTokens,\n uncachedInputTokens: p.uncachedInputTokens ?? 0,\n cacheReadTokens: p.cacheReadTokens ?? 0,\n cacheWriteTokens: p.cacheWriteTokens ?? 0,\n outputTokens: p.outputTokens ?? 0,\n count: p.count ?? 0,\n byModel,\n byProvider,\n hourlyTokens,\n winnerModel: p.winnerModel ?? null,\n winnerProvider: p.winnerProvider ?? null,\n };\n}\n\nexport function loadPersisted(): Map<string, DayAgg> {\n try {\n migrateLegacyPersist();\n const p = persistPath();\n if (!existsSync(p)) return new Map();\n const raw = readFileSync(p, \"utf-8\");\n const parsed: PersistedFile = JSON.parse(raw);\n if (!parsed || typeof parsed.days !== \"object\") return new Map();\n const m = new Map<string, DayAgg>();\n for (const [k, v] of Object.entries(parsed.days)) {\n try {\n m.set(k, fromPersistedDay(v));\n } catch {\n // skip corrupt day\n }\n }\n return m;\n } catch {\n return new Map();\n }\n}\n\nexport function savePersisted(days: Map<string, DayAgg>): void {\n try {\n const out: PersistedFile = {\n version: 1,\n days: Object.fromEntries([...days.entries()].map(([k, v]) => [k, toPersistedDay(v)])),\n };\n const json = JSON.stringify(out, null, 2);\n const p = persistPath();\n mkdirSync(dirname(p), { recursive: true });\n const tmp = p + \".tmp\";\n writeFileSync(tmp, json, \"utf-8\");\n renameSync(tmp, p);\n // Also write to lib/ (next to this module) for browser fetch via /plugins/dsh-token-pulse/daily.json\n const libPath = libDailyPath();\n if (libPath) {\n try {\n writeFileSync(libPath, json, \"utf-8\");\n } catch {}\n }\n } catch {\n // best-effort\n }\n}\n\n/**\n * Merge strategy: persisted is cumulative historical truth that never shrinks.\n * For each dayKey, if live has data, take element-wise MAX for additive fields,\n * and merge byModel/byProvider by taking max per key (or summing? No - live total is sum of remaining sessions, persisted is sum of all-time events. Taking max per day total prevents loss on deletion while allowing growth on new events. Per-model we also take max per model per day? But that would undercount if a new model appears. Better: merge by taking max of total, and for byModel: for each model, max(persisted, live). If a new model appears only in live, it propagates.\n * For count: max as well.\n */\nexport function mergePersistedAndLive(\n persisted: Map<string, DayAgg>,\n live: Map<string, DayAgg>,\n): Map<string, DayAgg> {\n const result = new Map<string, DayAgg>();\n\n // Start from persisted\n for (const [k, v] of persisted) {\n result.set(k, cloneDay(v));\n }\n\n for (const [k, liveDay] of live) {\n const prev = result.get(k);\n if (!prev) {\n result.set(k, cloneDay(liveDay));\n continue;\n }\n // Take max for scalar totals (never shrink); hourly also max per hour\n const mergedHourly = (prev.hourlyTokens ?? new Array(24).fill(0)).map((v, i) => Math.max(v, liveDay.hourlyTokens?.[i] ?? 0));\n const merged: DayAgg = {\n dayKey: k,\n totalTokens: Math.max(prev.totalTokens, liveDay.totalTokens),\n uncachedInputTokens: Math.max(prev.uncachedInputTokens, liveDay.uncachedInputTokens),\n cacheReadTokens: Math.max(prev.cacheReadTokens, liveDay.cacheReadTokens),\n cacheWriteTokens: Math.max(prev.cacheWriteTokens, liveDay.cacheWriteTokens),\n outputTokens: Math.max(prev.outputTokens, liveDay.outputTokens),\n count: Math.max(prev.count, liveDay.count),\n byModel: mergeMapMax(prev.byModel, liveDay.byModel),\n byProvider: mergeMapMax(prev.byProvider, liveDay.byProvider),\n hourlyTokens: mergedHourly,\n winnerModel: null,\n winnerProvider: null,\n };\n // recompute winners\n let bestM: string | null = null;\n let bestMVal = -1;\n for (const [m, v2] of merged.byModel) {\n if (v2 > bestMVal) {\n bestMVal = v2;\n bestM = m;\n }\n }\n merged.winnerModel = bestM;\n let bestP: string | null = null;\n let bestPVal = -1;\n for (const [p, v2] of merged.byProvider) {\n if (v2 > bestPVal) {\n bestPVal = v2;\n bestP = p;\n }\n }\n merged.winnerProvider = bestP;\n result.set(k, merged);\n }\n\n return result;\n}\n\nfunction cloneDay(d: DayAgg): DayAgg {\n return {\n dayKey: d.dayKey,\n totalTokens: d.totalTokens,\n uncachedInputTokens: d.uncachedInputTokens,\n cacheReadTokens: d.cacheReadTokens,\n cacheWriteTokens: d.cacheWriteTokens,\n outputTokens: d.outputTokens,\n count: d.count,\n byModel: new Map(d.byModel),\n byProvider: new Map(d.byProvider),\n hourlyTokens: [...(d.hourlyTokens ?? new Array(24).fill(0))],\n winnerModel: d.winnerModel,\n winnerProvider: d.winnerProvider,\n };\n}\n\nfunction mergeMapMax(a: Map<string, number>, b: Map<string, number>): Map<string, number> {\n const r = new Map<string, number>(a);\n for (const [k, v] of b) {\n r.set(k, Math.max(r.get(k) ?? 0, v));\n }\n return r;\n}\n\nexport function getPersistPath(): string {\n return persistPath();\n}\n","import { aggregate, type Aggregated, type RawUsageEvent, type DayAgg } from \"../aggregation.ts\";\nimport { parseUsageEvents, readAllUsageEvents, readLiveUsageEvents } from \"./session-reader.ts\";\nimport { loadPersisted, savePersisted, mergePersistedAndLive } from \"./persist.ts\";\nimport { toDayKey } from \"../date-bucket.ts\";\n\nexport class HeatmapStore {\n private events: RawUsageEvent[] = [];\n private aggregated: Aggregated;\n private listeners = new Set<() => void>();\n private byTurnStep = new Map<string, RawUsageEvent>();\n private fallbackBySid = new Map<string, { provider: string; model: string }>();\n private ctx: any;\n private persistedDays = new Map<string, DayAgg>();\n /** True after init() has finished scanning disk; false until then so\n * getAggregated() does not serve an incomplete (persisted-only) snapshot\n * that makes wide windows like Quarter look empty until a hard refresh. */\n private initialized = false;\n\n constructor(ctx: any) {\n this.ctx = ctx;\n this.aggregated = aggregate([], Date.now());\n }\n\n async init() {\n // Load persisted cumulative history first\n this.persistedDays = loadPersisted();\n // Then scan current sessions\n const evs = await readAllUsageEvents(this.ctx);\n this.events = evs;\n this.rebuildPersistedAndAggregated();\n this.initialized = true;\n }\n\n /**\n * Refresh from LIVE sessions only. The full disk scan happened once at\n * init(); afterwards only live (in-memory) sessions change, so re-scanning\n * the whole <DSH_HOME>/sessions tree on every refresh would be wasted work.\n *\n * A live session's snapshot includes its full stored log, so we REPLACE that\n * session's events in place (never append) — history for sessions that are\n * no longer live is kept as-is from init(). This keeps each event present at\n * most once in this.events, so the max-merge into persistedDays never\n * under- or over-counts.\n */\n async refresh(): Promise<void> {\n try {\n const live = await readLiveUsageEvents(this.ctx);\n const liveSids = new Set(live.map((e) => e.sid).filter(Boolean) as string[]);\n this.events = [...this.events.filter((e) => !liveSids.has(e.sid)), ...live];\n this.persistedDays = loadPersisted();\n this.rebuildPersistedAndAggregated();\n } catch (e: unknown) {\n // B2 fix: a refresh failure must never leave the UI stuck \"Refreshing\" or\n // wipe data — keep the last good snapshot and just log. The client's\n // `finally` clears the spinner once this promise resolves normally.\n this.ctx?.logger?.warn?.(`dsh-token-pulse refresh failed: ${String(e)}`);\n }\n }\n\n private rebuildPersistedAndAggregated() {\n this.invalidateCache();\n // Build live aggregated from current events\n const live = aggregate(this.events, Date.now());\n // Merge live byDay into persisted (persisted never shrinks)\n const mergedDays = mergePersistedAndLive(this.persistedDays, live.byDay);\n // If merged grew or any live day is newer/higher, persist\n if (mergedDays.size !== this.persistedDays.size || hasAnyIncrease(this.persistedDays, mergedDays)) {\n this.persistedDays = mergedDays;\n savePersisted(this.persistedDays);\n } else {\n // Even if no size change, merged may contain new totals (new tokens) - check live vs persisted\n // For simplicity, always update persisted to merged (it contains live's latest via max merge)\n this.persistedDays = mergedDays;\n // Only save if merged differs from what we loaded (avoid unnecessary writes)\n // We already checked hasAnyIncrease\n if (!mapsEqual(loadPersisted(), mergedDays)) {\n savePersisted(this.persistedDays);\n }\n }\n // Build final aggregated from persisted truth + live totals recomputed with fresh nowMs\n // Instead of re-aggregating from raw events (which would lose deleted history), aggregate from persistedDays\n this.aggregated = aggregateFromPersisted(this.persistedDays, Date.now());\n this.emit();\n }\n\n private rebuildAggregated() {\n this.invalidateCache();\n // Legacy path: just rebuild from persisted+live (used by ingest)\n this.aggregated = aggregateFromPersisted(this.persistedDays, Date.now());\n // Also need to incorporate any events not yet persisted? But persisted already contains merged live at init/refresh time.\n // For ingest incremental: we update persistedDays optimistically\n this.emit();\n }\n\n private cachedAgg: Aggregated | null = null;\n private cachedAt = 0;\n private static CACHE_TTL_MS = 30_000;\n // B1 fix: bound per-session provider/model fallbacks (the \"\" global entry is\n // never evicted). Without a cap this Map grew for every new sid, slowly in an\n // always-on host.\n private static MAX_FALLBACK_SIDS = 2000;\n\n getAggregated(): Aggregated {\n // Before init() completes (disk scan + multi-frame zstd decode), return an\n // EMPTY snapshot so the client shows a loading state instead of caching an\n // incomplete (persisted-only) result that makes Quarter look empty until a\n // hard refresh. The client's auto-refresh (10 min) or a manual refresh will\n // pick up the complete data once init() has set this.initialized = true.\n if (!this.initialized) return aggregate([], Date.now());\n // Memoized for 30s to avoid re-aggregating on every poll; invalidated by ingest/refresh\n const now = Date.now();\n if (this.cachedAgg && now - this.cachedAt < HeatmapStore.CACHE_TTL_MS) return this.cachedAgg;\n const live = aggregate(this.events, now);\n const merged = mergePersistedAndLive(this.persistedDays, live.byDay);\n this.cachedAgg = aggregateFromPersisted(merged, now);\n this.cachedAt = now;\n return this.cachedAgg;\n }\n\n /** True once the full disk scan (init) has completed. Exposed to API consumers\n * so partial data can be distinguished from \"still loading\". */\n isReady(): boolean {\n return this.initialized;\n }\n\n private invalidateCache() {\n this.cachedAgg = null;\n this.cachedAt = 0;\n }\n\n /** Ingest a single session event incrementally. Keys are scoped per session\n * via `event.sid` (set by the plugin's session/event handler) so sessions\n * with the same turn/step do not overwrite each other. */\n ingest(event: any) {\n const sid = (event as any)?.sid ?? \"\";\n const fb = (): { provider: string; model: string } => this.fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n const setFb = (provider?: string, model?: string) => {\n const cur = this.fallbackBySid.get(sid) ?? { provider: \"\", model: \"\" };\n if (provider) cur.provider = provider;\n if (model) cur.model = model;\n this.fallbackBySid.set(sid, cur);\n // B1 fix: FIFO-evict per-session fallback entries when the map exceeds the\n // cap; keep the \"\" global entry so pre-header chunks still resolve. Map\n // iterates in insertion order, so oldest sessions get recycled first.\n if (this.fallbackBySid.size > HeatmapStore.MAX_FALLBACK_SIDS) {\n for (const k of this.fallbackBySid.keys()) {\n if (k === \"\") continue;\n this.fallbackBySid.delete(k);\n if (this.fallbackBySid.size <= HeatmapStore.MAX_FALLBACK_SIDS - 100) break;\n }\n }\n };\n if (event.type === \"request/header\" && event.data?.header?.config) {\n const cfg = event.data.header.config;\n setFb(cfg.provider, cfg.model);\n return;\n }\n if (event.type === \"request/context\" && event.data) {\n setFb(event.data.provider, event.data.model);\n return;\n }\n if (event.type === \"assistant/chunk\" && event.data?.chunk?.type === \"usage\") {\n const key = `${sid}::${event.data.turn}:${event.data.step}`;\n if (!this.byTurnStep.has(key)) {\n const f = fb();\n const u = event.data.chunk.usage;\n const ev: RawUsageEvent = {\n time: event.time ?? Date.now(),\n provider: f.provider,\n model: f.model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n };\n this.byTurnStep.set(key, ev);\n this.events.push(ev);\n this.persistIncremental(ev);\n this.rebuildAggregated();\n }\n return;\n }\n if (event.type === \"assistant/message\" && event.data?.usage) {\n const msg = event.data.message;\n const src = msg?.source;\n const f = fb();\n const provider = src?.provider ?? f.provider;\n const model = src?.model ?? f.model;\n const turn = event.data.turn;\n const step = event.data.step;\n const key = turn !== undefined && step !== undefined ? `${sid}::${turn}:${step}` : `${sid}::msg:${event.seq ?? Math.random()}`;\n const existing = this.byTurnStep.get(key);\n const u = event.data.usage;\n const ev: RawUsageEvent = {\n time: event.time ?? Date.now(),\n provider,\n model,\n sid,\n usage: {\n inputTokens: u.inputTokens ?? 0,\n cacheReadTokens: u.cacheReadTokens ?? 0,\n cacheWriteTokens: u.cacheWriteTokens ?? 0,\n outputTokens: u.outputTokens ?? 0,\n },\n };\n if (existing) {\n const idx = this.events.indexOf(existing);\n if (idx !== -1) this.events.splice(idx, 1);\n // also revert persisted contribution? No - persisted is max, so removing provisional doesn't shrink persisted; just replace\n }\n this.byTurnStep.set(key, ev);\n this.events.push(ev);\n setFb(src?.provider, src?.model);\n this.persistIncremental(ev);\n this.rebuildAggregated();\n }\n }\n\n private persistIncremental(ev: RawUsageEvent) {\n const live = aggregate(this.events, Date.now());\n const dayKey = toDayKey(ev.time);\n const liveDay = live.byDay.get(dayKey);\n if (!liveDay) return;\n const prev = this.persistedDays.get(dayKey);\n const merged = prev ? mergeTwoDays(prev, liveDay) : cloneDay(liveDay);\n this.persistedDays.set(dayKey, merged);\n savePersisted(this.persistedDays);\n }\n\n onChange(cb: () => void): () => void {\n this.listeners.add(cb);\n return () => this.listeners.delete(cb);\n }\n\n private emit() {\n for (const cb of this.listeners) cb();\n }\n\n dispose() {\n this.listeners.clear();\n }\n\n /** For testing: inject events directly */\n _injectEvents(evs: RawUsageEvent[]) {\n this.events.push(...evs);\n this.rebuildAggregated();\n }\n\n // Exposed for settings UI / manual refresh\n getPersistedDays(): Map<string, DayAgg> {\n return new Map(this.persistedDays);\n }\n\n clearPersisted() {\n this.persistedDays.clear();\n savePersisted(this.persistedDays);\n this.rebuildAggregated();\n }\n}\n\nimport { weekRangeFor, monthRangeFor } from \"../date-bucket.ts\";\n\n// Minimal helpers reused from persist.ts logic but need local clones\nfunction cloneDay(d: DayAgg): DayAgg {\n return {\n dayKey: d.dayKey,\n totalTokens: d.totalTokens,\n uncachedInputTokens: d.uncachedInputTokens,\n cacheReadTokens: d.cacheReadTokens,\n cacheWriteTokens: d.cacheWriteTokens,\n outputTokens: d.outputTokens,\n count: d.count,\n byModel: new Map(d.byModel),\n byProvider: new Map(d.byProvider),\n hourlyTokens: [...(d.hourlyTokens ?? new Array(24).fill(0))],\n winnerModel: d.winnerModel,\n winnerProvider: d.winnerProvider,\n };\n}\n\nfunction mergeTwoDays(prev: DayAgg, live: DayAgg): DayAgg {\n const byModel = new Map(prev.byModel);\n for (const [k, v] of live.byModel) byModel.set(k, Math.max(byModel.get(k) ?? 0, v));\n const byProvider = new Map(prev.byProvider);\n for (const [k, v] of live.byProvider) byProvider.set(k, Math.max(byProvider.get(k) ?? 0, v));\n // hourly also max-merged per hour — dropping it here used to zero every\n // live-ingested day's hour bars on save.\n const hourlyTokens = (prev.hourlyTokens ?? new Array(24).fill(0))\n .map((v, i) => Math.max(v, live.hourlyTokens?.[i] ?? 0));\n const merged: DayAgg = {\n dayKey: prev.dayKey,\n totalTokens: Math.max(prev.totalTokens, live.totalTokens),\n uncachedInputTokens: Math.max(prev.uncachedInputTokens, live.uncachedInputTokens),\n cacheReadTokens: Math.max(prev.cacheReadTokens, live.cacheReadTokens),\n cacheWriteTokens: Math.max(prev.cacheWriteTokens, live.cacheWriteTokens),\n outputTokens: Math.max(prev.outputTokens, live.outputTokens),\n count: Math.max(prev.count, live.count),\n byModel,\n byProvider,\n hourlyTokens,\n winnerModel: null,\n winnerProvider: null,\n };\n let bestM: string | null = null;\n let bestMVal = -1;\n for (const [m, v2] of byModel) if (v2 > bestMVal) { bestMVal = v2; bestM = m; }\n merged.winnerModel = bestM;\n let bestP: string | null = null;\n let bestPVal = -1;\n for (const [p, v2] of byProvider) if (v2 > bestPVal) { bestPVal = v2; bestP = p; }\n merged.winnerProvider = bestP;\n return merged;\n}\n\nfunction hasAnyIncrease(a: Map<string, DayAgg>, b: Map<string, DayAgg>): boolean {\n for (const [k, vb] of b) {\n const va = a.get(k);\n if (!va) return true;\n if (vb.totalTokens > va.totalTokens) return true;\n if (vb.count > va.count) return true;\n }\n return false;\n}\n\nfunction mapsEqual(a: Map<string, DayAgg>, b: Map<string, DayAgg>): boolean {\n if (a.size !== b.size) return false;\n for (const [k, va] of a) {\n const vb = b.get(k);\n if (!vb) return false;\n if (va.totalTokens !== vb.totalTokens || va.count !== vb.count) return false;\n }\n return true;\n}\n\nfunction aggregateFromPersisted(persistedDays: Map<string, DayAgg>, nowMs: number): Aggregated {\n const fakeEvents: RawUsageEvent[] = [];\n for (const day of persistedDays.values()) {\n for (const [model, tokens] of day.byModel) {\n fakeEvents.push({\n time: new Date(day.dayKey + \"T12:00:00+08:00\").getTime(),\n provider: day.winnerProvider ?? \"unknown\",\n model,\n usage: { inputTokens: tokens, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 },\n });\n }\n if (day.byModel.size === 0 && day.totalTokens > 0) {\n fakeEvents.push({\n time: new Date(day.dayKey + \"T12:00:00+08:00\").getTime(),\n provider: day.winnerProvider ?? \"unknown\",\n model: day.winnerModel ?? \"unknown\",\n usage: { inputTokens: day.totalTokens, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 },\n });\n }\n }\n const agg = aggregate(fakeEvents, nowMs);\n const todayKey = toDayKey(nowMs);\n const [wS, wE] = weekRangeFor(todayKey);\n const [mS, mE] = monthRangeFor(todayKey);\n const sumRange = (s: string, e: string) => {\n let sum = 0;\n let cur = s;\n while (cur <= e) {\n sum += persistedDays.get(cur)?.totalTokens ?? 0;\n const d = new Date(cur + \"T12:00:00+08:00\");\n d.setDate(d.getDate() + 1);\n cur = toDayKey(d.getTime());\n if (cur > e) break;\n }\n return sum;\n };\n let all = 0;\n for (const v of persistedDays.values()) all += v.totalTokens;\n const today = persistedDays.get(todayKey)?.totalTokens ?? 0;\n const thisWeek = sumRange(wS, wE);\n const thisMonth = sumRange(mS, mE);\n (agg as any).totals = { today, thisWeek, thisMonth, all };\n (agg as any).byDay = new Map(persistedDays);\n return agg;\n}\n","import { HeatmapStore } from \"./host/store.ts\";\n\nexport const name = \"dsh-token-pulse\";\nexport const inject = [\"sessions\"] as const;\n\nexport function apply(ctx: any) {\n const store = new HeatmapStore(ctx);\n ctx.provide(\"heatmapStore\", store);\n (ctx as any).heatmapStore = store;\n\n let disposed = false;\n\n store.init().catch((e: unknown) => {\n ctx.logger?.warn?.(`dsh-token-pulse init failed: ${String(e)}`);\n });\n\n ctx.on(\"session/event\", (session: any, event: any) => {\n if (disposed) return;\n // Tag with the session id so per-session turn/step keys never collide.\n store.ingest({ ...event, sid: session?.id ?? \"\" });\n });\n\n ctx.on(\"session/flush\", () => {});\n\n // Serve persisted data for browser client via /api/dsh-token-pulse/daily.json\n let unregisterRoute: (() => void) | null = null;\n ctx.inject([\"webServer\"], (wsCtx: any) => {\n try {\n const handler = (req: any, res: any) => {\n try {\n const agg = store.getAggregated();\n const days = [...agg.byDay.values()].map((d) => ({\n dayKey: d.dayKey,\n totalTokens: d.totalTokens,\n uncachedInputTokens: d.uncachedInputTokens,\n cacheReadTokens: d.cacheReadTokens,\n cacheWriteTokens: d.cacheWriteTokens,\n outputTokens: d.outputTokens,\n count: d.count,\n byModel: Object.fromEntries(d.byModel),\n byProvider: Object.fromEntries(d.byProvider),\n hourlyTokens: d.hourlyTokens,\n winnerModel: d.winnerModel,\n winnerProvider: d.winnerProvider,\n }));\n const body = JSON.stringify({\n version: 1 as const,\n ready: store.isReady(),\n totals: agg.totals,\n topModels: agg.topModels.map((x) => ({ name: x.model, tokens: x.tokens })),\n topProviders: agg.topProviders.map((x) => ({ name: x.provider, tokens: x.tokens })),\n days,\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\", \"Cache-Control\": \"no-store\" });\n res.end(body);\n } catch (e) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: String(e) }));\n }\n };\n unregisterRoute = wsCtx.webServer.register({ kind: \"exact\", path: \"/api/dsh-token-pulse/daily.json\", handler });\n ctx.logger.info(\"dsh-token-pulse: registered /api/dsh-token-pulse/daily.json\");\n } catch (e) {\n ctx.logger?.warn?.(`dsh-token-pulse route register failed: ${String(e)}`);\n }\n });\n\n ctx.effect(\n () => () => {\n disposed = true;\n store.dispose();\n if (unregisterRoute) try { unregisterRoute(); } catch {}\n },\n \"dsh-token-pulse.dispose\",\n );\n\n ctx.logger.info(\"dsh-token-pulse host up\");\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-token-pulse",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "设置内独立栏的本地 Token 热图:周/月/季度/年视图 + 日周月总统计 + 按模型/供应商 Top5",
6
6
  "repository": {
@@ -93,6 +93,6 @@
93
93
  "vitest": "^4.1.8"
94
94
  },
95
95
  "engines": {
96
- "node": "^22.19.0 || >=24"
96
+ "dsh": ">=0.1.0-rc.1"
97
97
  }
98
98
  }