@hank-warren/pi-stats 0.4.2 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @hank-warren/pi-stats
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Count Pi `usage` session entries and de-duplicate them against mirrored extension sidecars (#42).
8
+
9
+ Pi 0.86 added `SessionManager.appendUsage()`, which records model calls made outside the conversation, such as cache warming, as `type: "usage"` session entries. `/stats` now counts them in every total and shows them as `provider/model (<kind>)` rows. Unknown kinds are counted like any other usage.
10
+
11
+ An extension that also mirrors such a call into a usage sidecar for older pi-stats versions gives the sidecar the id `<sessionId>:<usageEntryId>`. That call is counted once, from the session entry. Sidecars without a matching entry, and entries without a matching sidecar, are still counted. The stats cache version is bumped, so existing caches are rescanned once.
12
+
3
13
  ## 0.4.2
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -39,7 +39,7 @@ Every tab is responsive: as the terminal narrows, lower-priority columns move to
39
39
 
40
40
  ## What is counted
41
41
 
42
- Pi writes usage to assistant messages, usage-bearing tool results, compactions, and branch summaries. `pi-stats` recursively reads valid Pi session files and counts every recorded model call, including compacted and abandoned branches. Copied fork history is fingerprinted and counted once.
42
+ Pi writes usage to assistant messages, usage-bearing tool results, compactions, branch summaries, and `usage` entries (Pi 0.86+, for model calls outside the conversation such as cache warming). `pi-stats` recursively reads valid Pi session files and counts every recorded model call, including compacted and abandoned branches. Copied fork history is fingerprinted and counted once.
43
43
 
44
44
  Tool calls are counted separately from tokens, because most of them record no model usage. Every tool result in a session is tallied by name, along with whether it reported an error. Provider tool-call ids are reused verbatim when a session is forked, so they double as the de-duplication key and a forked branch never inflates the counts. Namespaced names such as `functions.bash` are ranked under their base name.
45
45
 
@@ -53,7 +53,9 @@ Some extensions call a model outside the agent loop, so their usage never reache
53
53
  {"v":1,"id":"3f2b…","ts":"2026-08-11T22:41:03.118Z","source":"auto-permissions","label":"guardian","provider":"anthropic","model":"claude-fable-5","usage":{"input":812,"output":96,"cacheRead":18442,"cacheWrite":0,"reasoning":48,"cost":0.0121}}
54
54
  ```
55
55
 
56
- Records are de-duplicated by `id`, counted in every total, and shown as their own `provider/model (label)` row so the overhead stays visible. They never create sessions, so session counts, streaks, and session spans are unaffected. `@hank-warren/pi-auto-permissions` writes one for guardian reviews; set `PI_STATS_DISABLE_USAGE_SIDECARS=1` to ignore all of them.
56
+ Records are de-duplicated by `id`, counted in every total, and shown as their own `provider/model (label)` row so the overhead stays visible. Session `usage` entries appear the same way as `provider/model (kind)`.
57
+
58
+ An extension that records a call both ways — a session `usage` entry for Pi 0.86+ and a sidecar for older `pi-stats` versions — gives the sidecar the id `<sessionId>:<usageEntryId>` (the session header id and the usage entry id). That call is counted once, from the session entry; sidecars without a matching entry, and entries without a matching sidecar, are each counted normally. They never create sessions, so session counts, streaks, and session spans are unaffected. `@hank-warren/pi-auto-permissions` writes one for guardian reviews; set `PI_STATS_DISABLE_USAGE_SIDECARS=1` to ignore all of them.
57
59
 
58
60
  The standard Pi session root, the current external session directory, `PI_CODING_AGENT_SESSION_DIR`, and a configured `subagents.defaultSessionDir` are discovered automatically. Add unusual one-off roots with the platform-delimited `PI_STATS_SESSION_DIRS` environment variable.
59
61
 
package/cache.ts CHANGED
@@ -6,7 +6,7 @@ import { makeIndex, parseSessionText, parseUsageSidecar } from "./stats.ts";
6
6
  import type { CachedFileRecord, ScanDiagnostics, SessionRecord, StatsCacheFile, StatsIndex, UsageRecord } from "./types.ts";
7
7
 
8
8
  /** Bumped to 3 when tool-call records were added; older caches lack them and must be reparsed. */
9
- const CACHE_VERSION = 3 as const;
9
+ const CACHE_VERSION = 4 as const;
10
10
  const SKIP_DIRECTORIES = new Set(["subagent-artifacts"]);
11
11
  /** Extensions record out-of-transcript model usage in <agentDir>/<extension>/usage.jsonl. */
12
12
  const USAGE_SIDECAR_NAMES = new Set(["usage.jsonl", "usage.jsonl.1"]);
@@ -143,7 +143,12 @@ function validUsageRecord(value: unknown): value is UsageRecord {
143
143
  typeof record.fingerprint === "string" &&
144
144
  validNumber(record.timestamp) &&
145
145
  typeof record.model === "string" &&
146
- (record.kind === "assistant" || record.kind === "tool" || record.kind === "summary" || record.kind === "sidecar") &&
146
+ (record.kind === "assistant" ||
147
+ record.kind === "tool" ||
148
+ record.kind === "summary" ||
149
+ record.kind === "usage" ||
150
+ record.kind === "sidecar") &&
151
+ (record.sharedId === undefined || typeof record.sharedId === "string") &&
147
152
  Boolean(usage) &&
148
153
  validNumber(usage?.input) &&
149
154
  validNumber(usage?.output) &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-stats",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "A compact /stats dashboard for all-time Pi token usage, including persisted subagents.",
5
5
  "type": "module",
6
6
  "keywords": [
package/stats.ts CHANGED
@@ -203,6 +203,22 @@ function normalizeEntry(
203
203
  kind: "summary",
204
204
  };
205
205
  }
206
+
207
+ // Pi >=0.86 appendUsage(): model usage outside the conversation, such as cache warming.
208
+ if (entry.type === "usage") {
209
+ const usage = normalizeUsage(entry.usage);
210
+ if (!usage) return undefined;
211
+ const usageKind = typeof entry.kind === "string" && entry.kind.length > 0 ? entry.kind : "usage";
212
+ const model = `${modelName(entry.provider, entry.model)} (${usageKind})`;
213
+ return {
214
+ fingerprint: fingerprint([id, entry.timestamp, "usage", model, usage]),
215
+ timestamp: entryTimestamp,
216
+ model,
217
+ usage,
218
+ kind: "usage",
219
+ ...(typeof entry.id === "string" && entry.id.length > 0 ? { sharedId: `${sessionId}:${entry.id}` } : {}),
220
+ };
221
+ }
206
222
  return undefined;
207
223
  }
208
224
 
@@ -342,6 +358,7 @@ export function parseUsageSidecar(filePath: string, text: string): { records: Us
342
358
  usage,
343
359
  kind: "sidecar",
344
360
  toolName: source,
361
+ ...(typeof entry.id === "string" && entry.id.length > 0 ? { sharedId: entry.id } : {}),
345
362
  });
346
363
  }
347
364
  return { records, malformedLines };
@@ -391,9 +408,12 @@ function deduplicateToolCalls(sessions: readonly SessionRecord[]): ToolCallRecor
391
408
  export function deduplicateUsage(sessions: readonly SessionRecord[], sidecar: readonly UsageRecord[] = []): UsageRecord[] {
392
409
  const sessionPaths = new Set(sessions.map((session) => resolve(session.path)));
393
410
  const seen = new Set<string>();
411
+ const sharedIds = new Set<string>();
394
412
  const result: UsageRecord[] = [];
395
413
  for (const session of sessions) {
396
414
  for (const record of session.usage) {
415
+ // Registered before the fork check so a copied entry still claims the original's sidecar.
416
+ if (record.sharedId) sharedIds.add(record.sharedId);
397
417
  if (hasPersistedSubagentChild(record, sessionPaths)) continue;
398
418
  if (seen.has(record.fingerprint)) continue;
399
419
  seen.add(record.fingerprint);
@@ -401,6 +421,7 @@ export function deduplicateUsage(sessions: readonly SessionRecord[], sidecar: re
401
421
  }
402
422
  }
403
423
  for (const record of sidecar) {
424
+ if (record.sharedId && sharedIds.has(record.sharedId)) continue;
404
425
  if (seen.has(record.fingerprint)) continue;
405
426
  seen.add(record.fingerprint);
406
427
  result.push(record);
package/types.ts CHANGED
@@ -15,8 +15,10 @@ export interface UsageRecord {
15
15
  timestamp: number;
16
16
  model: string;
17
17
  usage: UsageTotals;
18
- kind: "assistant" | "tool" | "summary" | "sidecar";
18
+ kind: "assistant" | "tool" | "summary" | "usage" | "sidecar";
19
19
  toolName?: string;
20
+ /** `<sessionId>:<usageEntryId>` on usage entries, the record id on sidecars; a match counts the call once. */
21
+ sharedId?: string;
20
22
  childSessionFiles?: string[];
21
23
  }
22
24
 
@@ -55,7 +57,7 @@ export interface CachedFileRecord {
55
57
  }
56
58
 
57
59
  export interface StatsCacheFile {
58
- version: 3;
60
+ version: 4;
59
61
  files: Record<string, CachedFileRecord>;
60
62
  }
61
63