@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 +10 -0
- package/README.md +4 -2
- package/cache.ts +7 -2
- package/package.json +1 -1
- package/stats.ts +21 -0
- package/types.ts +4 -2
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
|
|
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.
|
|
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 =
|
|
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" ||
|
|
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
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:
|
|
60
|
+
version: 4;
|
|
59
61
|
files: Record<string, CachedFileRecord>;
|
|
60
62
|
}
|
|
61
63
|
|