feat(pi): named pi-format stores as config-declared agents - #1397
Conversation
Tools built on pi (oh-my-pi and other forks) keep pi-format session
stores at their own paths. ccusage could only read one pi path universe
and labeled everything it found as agent "pi". Declare named extra
stores in the config file:
{ "pi": { "stores": [ { "name": "omp", "path": "~/.omp/agent/sessions" } ] } }
Each named store loads through the existing pi parser and surfaces as
its OWN agent in the unified reports: rows tagged in metadata.agents,
sessions with projectPath/lastActivity like pi, model labels prefixed
"[<name>] ". Named stores are additive to the default pi store and use
the same path-list semantics (comma-separated, ~-expansion, dedupe) and
the same date-window filtering as `ccusage pi ... --pi-path`.
Costs are computed from the unprefixed model name — the configurable
store name never participates in pricing lookup (a store named "o3"
cannot fabricate o3 pricing; regression-tested), while prefixed
pricingOverrides keys are consulted first and keep working.
Config validation: names match ^[a-z][a-z0-9_-]{0,31}$, reject
collisions with built-in agents (single source of truth asserted
against the unified loader's registry), duplicates, empty paths, and
stores whose resolved paths overlap the default pi store or another
store (silent double-counting is never possible). Invalid stores error
through the same config-error path as other invalid config content.
Absent store paths yield clean empty results, like default pi.
Backward compatibility: without pi.stores configured, all output is
byte-identical to before (verified against a golden matrix on real
stores, including a known pre-existing until-day session-window quirk
in the default pi unified path, deliberately preserved here and fixed
in a separate patch). Committed config schema regenerated.
No CLI surface changes: per-agent subcommands remain a closed set;
named stores appear in unified reports only.
|
no API key found — this repo is configured to use To fix: add the key as a GitHub Actions secret (referenced from your workflow's Open repo secrets → · Configure model → · Setup docs → · Ask in Discord →
|
📝 WalkthroughWalkthroughThis PR adds named ChangesNamed PI Stores Feature
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
2 issues found across 20 files
Not reviewed (too large): apps/ccusage/config-schema.json (~31 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="rust/crates/ccusage/src/progress.rs">
<violation number="1" location="rust/crates/ccusage/src/progress.rs:26">
P2: `UsageLoadAgent::PiStore` stores the configured store name as `&'static str`, which forces callers to leak runtime `String` values to satisfy the lifetime. In `adapter/all/loader.rs`, a helper `leak_agent_name` uses `Box::leak` to convert each user-configured store name into a permanently retained `&'static str`. This means every unique named store is leaked for the process lifetime, creating an avoidable memory-retention workaround. Consider changing the variant to an owned or reference-counted type (for example, `Arc<str>`) and adjusting the enum's `Copy` derivation if needed, to avoid introducing intentional memory leaks just to fit the progress label signature.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| Codebuff, | ||
| Hermes, | ||
| Pi, | ||
| PiStore(&'static str), |
There was a problem hiding this comment.
P2: UsageLoadAgent::PiStore stores the configured store name as &'static str, which forces callers to leak runtime String values to satisfy the lifetime. In adapter/all/loader.rs, a helper leak_agent_name uses Box::leak to convert each user-configured store name into a permanently retained &'static str. This means every unique named store is leaked for the process lifetime, creating an avoidable memory-retention workaround. Consider changing the variant to an owned or reference-counted type (for example, Arc<str>) and adjusting the enum's Copy derivation if needed, to avoid introducing intentional memory leaks just to fit the progress label signature.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At rust/crates/ccusage/src/progress.rs, line 26:
<comment>`UsageLoadAgent::PiStore` stores the configured store name as `&'static str`, which forces callers to leak runtime `String` values to satisfy the lifetime. In `adapter/all/loader.rs`, a helper `leak_agent_name` uses `Box::leak` to convert each user-configured store name into a permanently retained `&'static str`. This means every unique named store is leaked for the process lifetime, creating an avoidable memory-retention workaround. Consider changing the variant to an owned or reference-counted type (for example, `Arc<str>`) and adjusting the enum's `Copy` derivation if needed, to avoid introducing intentional memory leaks just to fit the progress label signature.</comment>
<file context>
@@ -23,6 +23,7 @@ pub(crate) enum UsageLoadAgent {
Codebuff,
Hermes,
Pi,
+ PiStore(&'static str),
Goose,
Kilo,
</file context>
There was a problem hiding this comment.
This one is a deliberate tradeoff rather than an oversight. The leak is bounded and one-time: store names are validated to ≤32 chars, there are at most a handful per config, and ccusage is a one-shot process. Going to Arc<str>/String would break Copy on UsageLoadAgent and ripple through every &'static str agent field in the unified loader (AllRow.agent, detected-agent lists, AgentLoadSpec) — a substantially larger diff to avoid retaining a few dozen bytes for a process lifetime measured in seconds. Happy to revisit if the maintainer prefers owned strings throughout.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
rust/crates/ccusage/src/adapter/pi/paths.rs (1)
7-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider consolidating the comma-list expansion logic.
existing_named_store_path_list(lines 37-45) reimplements the same comma-split/trim/expand/dedupe pipeline thatexisting_path_list(used bypaths(), lines 8-9) already implements for the default pi path. Extracting a shared helper (e.g.,fn existing_dir_path_list(raw: &str) -> Vec<PathBuf>) used by bothpaths()andnamed_store_paths()would remove the duplicate semantics and prevent future divergence between default-path and named-store-path resolution.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/crates/ccusage/src/adapter/pi/paths.rs` around lines 7 - 45, The path parsing logic is duplicated between existing_path_list and existing_named_store_path_list, leading to two parallel comma-split/trim/dedupe implementations. Extract the shared directory-list pipeline into a single helper (for example, a function that accepts the raw string and an optional expansion step) and have both paths() and named_store_paths() reuse it so default PI paths and named store paths stay consistent.rust/crates/ccusage/src/adapter/pi/parser.rs (1)
118-160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated cost-mode dispatch between default and store-aware paths.
PiStoreContext::cost/missing_pricing_model(Lines 118-159) andcalculate_store_cost/missing_store_pricing_model(Lines 314-331, 345-363) re-implement the sameDisplay/Auto/Calculatethree-way branching that already exists incost::calculate_cost_for_usageandcost::missing_pricing_model_for_usage. If the cost-mode semantics are ever adjusted, both copies must be updated in lockstep or the default and named-store paths will silently diverge.Consider factoring the mode dispatch into a small generic helper (parameterized by a "compute from tokens" closure) shared by both the default and store-aware code paths.
♻️ Sketch of a shared dispatch helper
fn dispatch_cost_mode<F>( mode: CostMode, display_cost: Option<f64>, from_tokens: F, ) -> f64 where F: FnOnce() -> f64, { match mode { CostMode::Display => display_cost.unwrap_or(0.0), CostMode::Auto => display_cost.unwrap_or_else(from_tokens), CostMode::Calculate => from_tokens(), } }Also applies to: 314-331
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/crates/ccusage/src/adapter/pi/parser.rs` around lines 118 - 160, The cost-mode branching is duplicated between PiStoreContext::cost / missing_pricing_model and the store-aware helpers calculate_store_cost / missing_store_pricing_model, which risks the default and named-store paths drifting apart. Refactor the shared Display/Auto/Calculate dispatch into a small helper that takes a closure for token-based computation, and reuse it from both the default and named-store paths. Keep the existing behavior in cost::calculate_cost_for_usage and cost::missing_pricing_model_for_usage as the single source of truth for mode semantics.rust/crates/ccusage/src/config_schema.rs (1)
11-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDe-duplicate the named-store name pattern (defined 3×).
The same regex literal
^[a-z][a-z0-9_-]{0,31}$is hardcoded in theNAMED_PI_STORE_NAME_PATTERNconst, the#[schemars(regex(pattern = "..."))]attribute onPiStoreConfig::name, and again in thegenerated_schema_exposes_named_pi_storestest assertion. If the pattern changes, it's easy to update one spot and silently diverge from the others (the config.rs test only compares two string constants, not the schema attribute or actual matcher behavior).Per schemars docs,
regex(path = ...)can reference any value with ato_string()method (not just aRegex), so the struct attribute could reference the const directly instead of re-typing the literal. The test literal can also just reference the const.♻️ Proposed fix to reduce duplication
pub(crate) struct PiStoreConfig { /// Agent name to use for this pi-format store in all-agent reports. - #[schemars(regex(pattern = "^[a-z][a-z0-9_-]{0,31}$"))] + #[schemars(regex(path = "crate::config_schema::NAMED_PI_STORE_NAME_PATTERN"))] pub(crate) name: String,+ use super::NAMED_PI_STORE_NAME_PATTERN; + #[test] fn generated_schema_exposes_named_pi_stores() { let schema = generated_schema(); let stores = schema_property(&schema, &["pi", "stores"]).unwrap(); assert_eq!(stores["type"], json!("array")); ... assert_eq!( stores["items"]["properties"]["name"]["pattern"], - json!("^[a-z][a-z0-9_-]{0,31}$") + json!(NAMED_PI_STORE_NAME_PATTERN) ); }Please confirm the
regex(path = ...)form compiles as expected with schemars 0.8 for apub(crate) const &strin the same crate before applying.Also applies to: 184-192, 1341-1361
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/crates/ccusage/src/config_schema.rs` around lines 11 - 12, The named-store regex is duplicated in multiple places, so update PiStoreConfig::name to reference NAMED_PI_STORE_NAME_PATTERN through schemars regex(path = ...) instead of repeating the literal, and reuse the same const in the generated_schema_exposes_named_pi_stores test assertion. Verify that schemars 0.8 accepts a pub(crate) const &str via regex(path = ...) in this crate, then remove the hardcoded pattern copies so the const becomes the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rust/crates/ccusage/src/adapter/all/loader.rs`:
- Around line 377-397: The resolver in resolve_named_pi_store_paths only rejects
stores when every resolved path collides, but it still allows partial overlaps
and nested parent/child path relationships to slip through. Update the overlap
detection in the NamedPiStore resolution flow so any collision between a store’s
resolved paths and existing owners is treated as an error, including cases where
one resolved root contains another; use the existing owners/collision_owners
tracking in resolve_named_pi_store_paths and the final ResolvedNamedPiStore
construction to fail fast instead of silently dropping colliding paths.
- Around line 244-265: Named PI session rows are losing the `metadata.agents`
tag because the session handling path clears `metadata_agents` for every row,
even for rows loaded by `load_named_pi_store_rows_from_paths` via
`AgentLoadSpec` in `loader.rs`. Update the session-row logic to only clear
`metadata_agents` for built-in session rows so byte-identical output stays
unchanged when no named stores are configured, but preserve the tag for rows
coming from named stores (`store.name` / `PiStore(agent)`).
---
Nitpick comments:
In `@rust/crates/ccusage/src/adapter/pi/parser.rs`:
- Around line 118-160: The cost-mode branching is duplicated between
PiStoreContext::cost / missing_pricing_model and the store-aware helpers
calculate_store_cost / missing_store_pricing_model, which risks the default and
named-store paths drifting apart. Refactor the shared Display/Auto/Calculate
dispatch into a small helper that takes a closure for token-based computation,
and reuse it from both the default and named-store paths. Keep the existing
behavior in cost::calculate_cost_for_usage and
cost::missing_pricing_model_for_usage as the single source of truth for mode
semantics.
In `@rust/crates/ccusage/src/adapter/pi/paths.rs`:
- Around line 7-45: The path parsing logic is duplicated between
existing_path_list and existing_named_store_path_list, leading to two parallel
comma-split/trim/dedupe implementations. Extract the shared directory-list
pipeline into a single helper (for example, a function that accepts the raw
string and an optional expansion step) and have both paths() and
named_store_paths() reuse it so default PI paths and named store paths stay
consistent.
In `@rust/crates/ccusage/src/config_schema.rs`:
- Around line 11-12: The named-store regex is duplicated in multiple places, so
update PiStoreConfig::name to reference NAMED_PI_STORE_NAME_PATTERN through
schemars regex(path = ...) instead of repeating the literal, and reuse the same
const in the generated_schema_exposes_named_pi_stores test assertion. Verify
that schemars 0.8 accepts a pub(crate) const &str via regex(path = ...) in this
crate, then remove the hardcoded pattern copies so the const becomes the single
source of truth.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b62440ed-2812-4f14-a12a-a537ff4b0758
📒 Files selected for processing (20)
apps/ccusage/config-schema.jsondocs/guide/config-files.mddocs/guide/pi/index.mdrust/crates/ccusage-cli/src/lib.rsrust/crates/ccusage-cli/src/parser.rsrust/crates/ccusage-cli/src/types.rsrust/crates/ccusage/src/adapter/all/loader.rsrust/crates/ccusage/src/adapter/all/mod.rsrust/crates/ccusage/src/adapter/claude/paths.rsrust/crates/ccusage/src/adapter/pi/loader.rsrust/crates/ccusage/src/adapter/pi/mod.rsrust/crates/ccusage/src/adapter/pi/parser.rsrust/crates/ccusage/src/adapter/pi/paths.rsrust/crates/ccusage/src/config.rsrust/crates/ccusage/src/config_schema.rsrust/crates/ccusage/src/cost.rsrust/crates/ccusage/src/main.rsrust/crates/ccusage/src/path_utils.rsrust/crates/ccusage/src/pricing.rsrust/crates/ccusage/src/progress.rs
… parsing - Session files are collected recursively, so a named store rooted at an ancestor or descendant of the default pi store (or another named store) would ingest the same files twice under different dedupe identities. The resolver now rejects any overlap — equal, ancestor, or descendant — and partial collisions error instead of silently dropping the colliding path, matching the documented contract. Regression tests for a store nested inside the default pi path and a partial overlap across two stores. - Extract a shared existing_paths helper in pi/paths.rs; the default and named-store variants now differ only in their path mapper, with the deliberate ~-expansion difference documented. - Update config/pi docs for the stricter overlap wording.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/guide/config-files.md`:
- Line 285: The code span in the `pi.stores` guidance includes a trailing space
in `[name] `, which triggers markdownlint MD038. Update the wording so the space
is moved into surrounding prose while keeping the identifier itself inside
backticks, and verify the affected text still clearly references named stores
and their model prefix behavior.
In `@docs/guide/pi/index.md`:
- Line 47: The markdown example in the guide includes a trailing space inside
the inline code span for the `[omp] ` label, which triggers markdownlint MD038.
Update the text in the affected documentation section so the space is moved
outside the backticks and the prose still reads naturally; use the surrounding
named store/agent description to locate the sentence.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c5ed7e1f-82bd-4219-8412-c77b2f77eaef
📒 Files selected for processing (4)
docs/guide/config-files.mddocs/guide/pi/index.mdrust/crates/ccusage/src/adapter/all/loader.rsrust/crates/ccusage/src/adapter/pi/paths.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- rust/crates/ccusage/src/adapter/pi/paths.rs
- rust/crates/ccusage/src/adapter/all/loader.rs
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="rust/crates/ccusage/src/progress.rs">
<violation number="1" location="rust/crates/ccusage/src/progress.rs:26">
P2: `UsageLoadAgent::PiStore` stores the configured store name as `&'static str`, which forces callers to leak runtime `String` values to satisfy the lifetime. In `adapter/all/loader.rs`, a helper `leak_agent_name` uses `Box::leak` to convert each user-configured store name into a permanently retained `&'static str`. This means every unique named store is leaked for the process lifetime, creating an avoidable memory-retention workaround. Consider changing the variant to an owned or reference-counted type (for example, `Arc<str>`) and adjusting the enum's `Copy` derivation if needed, to avoid introducing intentional memory leaks just to fit the progress label signature.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
ccusage
@ccusage/ccusage-darwin-arm64
@ccusage/ccusage-darwin-x64
@ccusage/ccusage-linux-arm64
@ccusage/ccusage-linux-x64
@ccusage/ccusage-win32-x64
commit: |
Resolve conflicts with ccusage#1394 and ccusage#1396: - keep named pi store specs inside the new load_base_rows so --sections includes config-declared agents - adopt the ccusage#1394 session date filtering fix in load_pi_format_agent_rows - replace the pinned until-day drop bug test with the fixed-behavior test from main, adapted to the named-store test helpers
* chore(ci): remove pullfrog because they dont serve free tokens anymore * Restore `pullfrog.yml` workflow * feat(statusline): show reasoning effort level next to model name (ccusage#1405) * feat(statusline): show reasoning effort level next to model name Claude Code 2.1.119+ includes an optional top-level effort.level field (low, medium, high, xhigh, or max) in the statusline hook JSON, reflecting the live /effort setting. Parse it from the hook input and append it to the model segment, e.g. '🤖 Fable 5 (high)'. The field is absent for models without the effort parameter and on older Claude Code versions, in which case the statusline keeps showing just the model label as before. * test(statusline): add Fable 5 fixture with effort level Adds a manual statusline fixture for the latest model shape, including the effort.level field, plus a test-statusline-fable5 recipe wired into test-statusline-all so the effort display can be smoke-tested from the CLI. * docs(statusline): document effort level next to the model name Updates the statusline guide examples to the current model display ('Fable 5 (high)') and explains that the reasoning effort level comes from Claude Code 2.1.119+, with a fallback example for models or versions that do not report it. * Revert "Restore `pullfrog.yml` workflow" This reverts commit 04f45b0. * fix(codex): skip forked session replay history (ccusage#1369) * feat(json): emit modelBreakdowns in per-agent JSON reports (ccusage#1395) Per-agent subcommands (ccusage pi|opencode|amp|hermes|... daily/weekly/ monthly/session --json) compute per-model cost breakdowns — the table view renders them with --breakdown — but the shared per-agent JSON serializer never emitted them, forcing JSON consumers to re-derive model costs they cannot actually reconstruct. Add "modelBreakdowns" to agent_summary_json, mirroring the unified serializers (summary_json / session_summary_json). Purely additive: every pre-existing key and value is unchanged; the codex-native serializer (models object) is deliberately untouched. Tests: shared-shape insta snapshot now shows populated breakdowns for all four report kinds; pi daily JSON asserts a full single-element breakdown array with non-zero cost (the motivating case); fixture- driven copilot (real pricing via read_otel_file) and qwen (real JSONL fixture line) assertions pin their entire breakdown arrays. * fix(pi): align unified session date filtering (ccusage#1394) * fix(pi): align unified session date filtering Filter default pi unified session entries by date before summarizing, matching `ccusage pi session --pi-path` behavior for inclusive `--until` days. * style: apply treefmt formatting --------- Co-authored-by: ryoppippi <[email protected]> * feat(unified): --sections and --by-agent for single-invocation reporting (ccusage#1396) * feat(unified): --sections and --by-agent for single-invocation reporting Dashboards polling ccusage today need one unified invocation per section plus one per-agent invocation per agent — every call re-scanning all stores. Two additive flags on the unified commands (and the bare root invocation) collapse that to a single call: ccusage daily --json --sections daily,monthly,session --by-agent --sections <csv> emits each requested grouping in one envelope from at most TWO store scans (daily/weekly/monthly share one Daily-kind base load; session adds one Session-kind load), one process, one pricing load. Every section is produced by exactly the code path its standalone command uses — load_sections delegates to the same load_rows machinery, so section output is identical to a standalone invocation by construction (covered by fixture equivalence tests including claude agent-progress usage lines and codex cross-session/model-alias dedupe cases). Envelope order is deterministic via a local ordered serializer: invoked section first, remaining sections in canonical order, totals last; single-section envelopes use the unchanged existing path. --by-agent adds an "agents" array to daily/weekly/monthly rows (the internal per-agent breakdowns, now serialized: tokens, cost, and modelBreakdowns per agent). Session rows are already per-agent, so the flag is a no-op there. Per-agent costs sum exactly to the combined row. Backward compatibility: without the new flags, JSON and table output are byte-identical to before (verified against a 20-invocation golden matrix on real stores). Tables render requested sections sequentially; --by-agent is JSON-only. * refactor(unified): address review feedback on duplication and detected agents - row_json now composes agent_json and layers on the row-level fields (period, metadata, agents), so the shared row shape has a single serialization path; output unchanged. - Extract parse_unified_report_arg so the root, unified-command, and top-level-session parse sites share one --all/--sections/--by-agent block; the root site keeps its mark_used bookkeeping. - Carry daily-load and session-load detected agents separately so each --sections table header shows the same detected list as the equivalent standalone invocation. * style: apply treefmt formatting --------- Co-authored-by: ryoppippi <[email protected]> * feat(pi): named pi-format stores as config-declared agents (ccusage#1397) * feat(pi): named pi-format stores as config-declared agents Tools built on pi (oh-my-pi and other forks) keep pi-format session stores at their own paths. ccusage could only read one pi path universe and labeled everything it found as agent "pi". Declare named extra stores in the config file: { "pi": { "stores": [ { "name": "omp", "path": "~/.omp/agent/sessions" } ] } } Each named store loads through the existing pi parser and surfaces as its OWN agent in the unified reports: rows tagged in metadata.agents, sessions with projectPath/lastActivity like pi, model labels prefixed "[<name>] ". Named stores are additive to the default pi store and use the same path-list semantics (comma-separated, ~-expansion, dedupe) and the same date-window filtering as `ccusage pi ... --pi-path`. Costs are computed from the unprefixed model name — the configurable store name never participates in pricing lookup (a store named "o3" cannot fabricate o3 pricing; regression-tested), while prefixed pricingOverrides keys are consulted first and keep working. Config validation: names match ^[a-z][a-z0-9_-]{0,31}$, reject collisions with built-in agents (single source of truth asserted against the unified loader's registry), duplicates, empty paths, and stores whose resolved paths overlap the default pi store or another store (silent double-counting is never possible). Invalid stores error through the same config-error path as other invalid config content. Absent store paths yield clean empty results, like default pi. Backward compatibility: without pi.stores configured, all output is byte-identical to before (verified against a golden matrix on real stores, including a known pre-existing until-day session-window quirk in the default pi unified path, deliberately preserved here and fixed in a separate patch). Committed config schema regenerated. No CLI surface changes: per-agent subcommands remain a closed set; named stores appear in unified reports only. * fix(pi): reject nested/partial named-store path overlaps, dedupe path parsing - Session files are collected recursively, so a named store rooted at an ancestor or descendant of the default pi store (or another named store) would ingest the same files twice under different dedupe identities. The resolver now rejects any overlap — equal, ancestor, or descendant — and partial collisions error instead of silently dropping the colliding path, matching the documented contract. Regression tests for a store nested inside the default pi path and a partial overlap across two stores. - Extract a shared existing_paths helper in pi/paths.rs; the default and named-store variants now differ only in their path mapper, with the deliberate ~-expansion difference documented. - Update config/pi docs for the stricter overlap wording. * docs(pi): move trailing space out of code spans (markdownlint MD038) --------- Co-authored-by: ryoppippi <[email protected]> * fix(kimi): support Kimi Code new wire format (ccusage#1362) Kimi Code (`~/.kimi-code`) emits a new `wire.jsonl` schema that the old adapter could not parse, so its usage was silently dropped (ccusage#1261). - Detect `~/.kimi-code` and the deeper layout `sessions/<ws>/<session>/agents/<agent>/wire.jsonl` (5 path components) alongside the legacy 3-component layout. - Parse top-level `type == "usage.record"` lines: camelCase token fields (`inputOther`, `inputCacheRead`, `inputCacheCreation`), `time` in milliseconds, and `model` prefixed with `kimi-code/` (stripped for pricing lookup). Skip cumulative `usageScope == "session"` records. - Deserialize `time` leniently so a float- or string-encoded timestamp degrades to the file-mtime fallback instead of dropping the whole line. - Walk the correct number of parents in `kimi_root_from_wire_path` for the deeper layout so config resolution looks at the right root. - Keep full backward compatibility with the old StatusUpdate format. - Update the Kimi guide and data-source docs for `~/.kimi-code`. Fixes ccusage#1261 Co-authored-by: Claude Opus 4.8 <[email protected]> * perf: cache PricingMap::find() results and skip redundant opencode pricing checks (ccusage#1407) * perf(pricing): cache PricingMap::find() results to avoid repeated fuzzy matching PricingMap::find() does an exact HashMap lookup followed by expensive fuzzy matching through all ~2,200 pricing entries when the exact model name is not in the map. When adapters repeatedly query the same model names, a large fraction of lookups miss the HashMap and trigger a full scan of the pricing table for every call. Add a OnceLock<Mutex<FxHashMap>> cache that memoizes find() results by model name (including None for models not found in pricing). Once a model name has been resolved, future lookups complete in O(1) instead of O(n) over the pricing table. Also add clear_find_cache() called from load_json_with_overrides(), load_models_dev_models(), and apply_overrides() so the cache stays consistent when the pricing table is mutated. * perf(opencode): skip redundant missing-pricing check when cost is known calculate_open_code_cost and missing_open_code_pricing independently iterate through the same model candidates. When the cost calculation already found a valid positive cost (either from a stored cost_usd field or from pricing lookup), skip the missing-pricing check entirely since pricing was already resolved. --------- Co-authored-by: turtton <[email protected]> * ci(release): migrate from bumpp to tagpr (ccusage#1406) * ci(release): add tagpr release PR automation Introduce Songmu/tagpr to manage releases via an auto-generated release PR: every push to main creates or updates a PR that bumps all nine workspace package.json versions (tagpr versionFile) and syncs the Rust workspace via the new `just sync-rust-version` recipe run as postVersionCommand. Merging the PR tags the merge commit. GitHub Release creation and CHANGELOG.md generation are disabled in .tagpr because changelogithub keeps generating the release notes in the existing style. Tags pushed with GITHUB_TOKEN do not trigger `on: push: tags` workflows, so tagpr.yaml dispatches release.yaml explicitly with `gh workflow run --ref <tag>`; release.yaml gains a workflow_dispatch trigger for that purpose. * chore(release): drop bumpp local release flow Releases are now driven by tagpr in CI, so the local `just release` recipe and the bumpp dependency are no longer needed. bump.config.ts is deleted because its cargo set-version hook moved to the `just sync-rust-version` recipe that tagpr runs as postVersionCommand. * docs(skills): document tagpr release flow Replace the removed `just release` recipe in the development skill command list with a note on the tagpr release PR flow and the minor/major bump labels. * ci(release): gate release jobs to tag refs and isolate actions:write Gate release.yaml build/publish/release jobs behind startsWith(github.ref, 'refs/tags/') so a workflow_dispatch from a branch cannot bypass the tag-only release flow. tagpr dispatches with --ref <tag>, so the intended path is unaffected. Move the release dispatch out of the tagpr job into a dependent dispatch-release job that alone holds actions: write, keeping tagpr on its documented least-privilege scopes (contents/pull-requests/issues). Co-authored-by: Codesmith <[email protected]> * ci(release): consolidate release pipeline into tagpr workflow Move the build/publish/release jobs from release.yaml into tagpr.yaml, gated on the tagpr job's tag output, and delete release.yaml. Running everything in one workflow removes the workflow_dispatch chaining that worked around GITHUB_TOKEN-pushed tags not triggering `push: tags` workflows, along with the dispatch-release job and its `actions: write` grant. The release jobs check out the freshly created tag explicitly. changelogithub resolves the release tag with `git tag --points-at HEAD`, not GITHUB_REF, so it picks the right release even though the run's ref is refs/heads/main. A failed release is retried with "Re-run failed jobs"; a full re-run finds no new tag and skips the release jobs. --------- Co-authored-by: Codesmith <[email protected]> * ci(release): use Conventional Commits title for tagpr release PRs (ccusage#1409) tagpr titles its release PRs "Release for vX.Y.Z", which fails the check-pr-title workflow because it has no Conventional Commits type prefix (seen on PR ccusage#1408). tagpr takes the first line of the rendered pull request template as the PR title, so point .tagpr at a custom template whose first line is "chore: release {{.NextVersion}}". The rest of the template mirrors tagpr's default body, minus the unused tag-prefix placeholder. The template is a Go text/template, and oxfmt's markdown rewrites break its <details> block and nested list structure, so exclude it from treefmt. This also restores the title style used by the previous bumpp-based release flow ("chore: release v20.0.14"). * feat(pricing): support OpenAI two-stage pricing and add the gpt-5.6 family (ccusage#1414) * feat(pricing): add gpt-5.6 family and OpenAI long-context tier rates OpenAI introduced two-stage (short/long context) pricing with gpt-5.6: requests with more than 272K input tokens are billed at higher long-context rates. The same tier also applies to gpt-5.5, gpt-5.5-pro, gpt-5.4, and gpt-5.4-pro on the current pricing page. The existing tier support hardcoded the LiteLLM 200K boundary, so Pricing gains a per-model long_context_threshold (defaulting to 200K for LiteLLM *_above_200k_tokens data) and tiered_cost takes the threshold as a parameter. New built-in entries cover gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna, including their cache-write rates. Long-context tier rates live in a builtin_long_context_rates overlay that is re-applied after every pricing load: a live LiteLLM refresh replaces whole entries, and LiteLLM currently publishes these models with flat rates only, so tier rates set directly on built-in entries would be silently dropped whenever a refresh succeeds. Entries that already carry tier rates are left untouched so upstream data wins once it exists. Date-pinned keys such as gpt-5.5-2026-04-23 share their base model's overlay rates. The gpt-5.6 context limits mirror the 1,050,000-token window of the other long-context GPT-5 flagship models until upstream data lands. * feat(codex): bill long-context requests at OpenAI two-stage rates OpenAI decides the pricing tier per request: once a request's input exceeds 272K tokens, every token of that request (input, cached input, and output) is billed at the long-context rates. Codex cost calculation runs on per-model sums aggregated across many requests, so the tier cannot be recovered from the totals afterwards. CodexModelUsage now tracks the portion of tokens that came from long-context requests. The split is recorded while token_count events are aggregated, where each event still represents a single request, and merged across parallel shards like the other counters. calculate_codex_model_cost prices the aggregated usage as two independent buckets: the short bucket at the flat rates and the long bucket at the *_above_200k rates, falling back to the flat rates for models without a long-context tier so their costs are unchanged. The existing fast-speed multiplier applies to both buckets. Report JSON and table output are unchanged; only costUSD values for long-context requests differ. * docs(pricing): explain all-or-nothing long-context overlay check Codex review suggested filling missing tier fields independently when a refreshed LiteLLM entry carries partial *_above_200k_tokens data. That would mix rates that assume the 200K LiteLLM boundary with built-in rates that assume the OpenAI 272K boundary under a single per-model threshold, mispricing both tiers, so the overlay defers to upstream entirely once any tier rate exists. Record that rationale next to the check. * fix(pricing): apply two-stage rates to whole request and per-model split Co-authored-by: Codesmith <[email protected]> --------- Co-authored-by: Codesmith <[email protected]> * chore: use black smith more * chore: release v20.0.15 (ccusage#1408) [tagpr] prepare for the next release Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore(ci): rename it back to releaese.yaml * chore: release v20.0.16 (ccusage#1416) [tagpr] prepare for the next release Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * docs: update Lineman affiliate links to CCUsage landing page (ccusage#1417) Point GitHub README and docs site sponsor links at the dedicated LinkJolt redirect for CCUsage traffic (free tier + voucher funnel). Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: ryoppippi <[email protected]> * docs: update Star History chart (ccusage#1419) * docs: update Star History chart Switch the README and sponsorship guide to the current Star History chart endpoint, including light and dark variants. Allowlist the public read-only sealed chart token so secret scanning does not report a false positive. Co-authored-by: ryoppippi <[email protected]> * chore: exclude sealed token from spellcheck Mark the exact public Star History token allowlist line as a spellcheck exclusion so its random character sequence does not fail the documentation preflight. Co-authored-by: ryoppippi <[email protected]> * chore: format sealed token allowlist Use the repository's TOML formatting and bracket the random token with the supported spellchecker block directives. Co-authored-by: ryoppippi <[email protected]> * style: align Gitleaks TOML indentation Match the repository formatter's tab indentation for the multiline allowlist entry. Co-authored-by: ryoppippi <[email protected]> * fix: match full Star History token URL Configure the global Gitleaks allowlist to evaluate the full finding match so the narrowly scoped sealed_token pattern suppresses the six intentional chart URLs. Co-authored-by: ryoppippi <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: ryoppippi <[email protected]> * fix(claude): count advisor model usage (ccusage#1423) * fix(claude): count advisor model usage Expand advisor_message iterations into distinct usage entries so their tokens and model-specific costs are included in every report path. Keep main-model iteration totals unchanged and cover both standard and daily loaders. Co-authored-by: ryoppippi <[email protected]> * docs(claude): clarify advisor cost modes Co-authored-by: ryoppippi <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: ryoppippi <[email protected]> * chore: release v20.0.17 (ccusage#1418) [tagpr] prepare for the next release Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * perf(nix): keep dependency cache across releases (ccusage#1424) * build(perf): migrate benchmark harness to Babashka (ccusage#1432) * build(perf): migrate benchmark harness to Babashka Replace the large Nushell PR benchmark script with a Babashka implementation split by data, system, benchmark, report, and orchestration responsibilities. The new process boundary keeps argv, environment, and working-directory data explicit while preserving hyperfine, package installation, memory, size, and Markdown behavior. Move the CI caller and profiling guidance to the executable Babashka entry point. Add focused tests behind their own Nix shebang so contributors can run the harness suite without adding Babashka to the full development shell. * docs(agents): document implementation language choices Route small command-oriented automation to Nushell and data-heavy, testable automation to Babashka. Keep production binaries in Rust and npm-integrated APIs in TypeScript so future tooling changes follow the same criteria used by the benchmark migration. * test(ci): run Babashka harness tests Execute the self-contained benchmark harness test entry point in the CI test job so changes to CLI parsing, normalization, fallback decisions, and report rendering cannot bypass pull request validation. * fix(perf): harden platform and tarball paths Normalize version-qualified Windows os.name values to win32 so native executable and package paths use the expected suffixes. Resolve relative pnpm pack filenames against the temporary destination while preserving the absolute paths emitted by current pnpm versions. Add regression coverage for both platform normalization and relative or absolute tarball filenames. * fix(perf): size local package fallbacks Use remote tarball sizing only after the corresponding preview package was installed successfully. When either package URL times out, benchmark and size the available local checkout so fallback runs can still produce a complete report. Cover base and head source selection and verify both unavailable URLs through a committed-fixture smoke run. * fix(perf): bound harness child processes and skip RSS on unsupported platforms Add a cancellable timeout to run-process and thread --package-runner-timeout-ms through the package URL probe, install, pnpm pack, and git rev-parse flows so a stalled child cannot outlive the deadline; give the curl probe and download explicit connect and read limits. measure-memory now warns once and skips gracefully when /usr/bin/time is unavailable (unsupported platforms) instead of throwing and aborting the entire benchmark run. Co-authored-by: Codesmith <[email protected]> * Revert "fix(perf): bound harness child processes and skip RSS on unsupported platforms" This reverts commit 9140a99. --------- Co-authored-by: Codesmith <[email protected]> * build(perf): migrate fixture generator to Bun (ccusage#1433) * build(perf): migrate fixture generator to Bun Replace the Nushell fixture generator with a dependency-free Bun script.\n\nKeep the generated Claude and Codex fixture layouts and command-line\ninterface while using Bun file writers and Bun Shell for file operations. * build(perf): type Bun fixture script Add Bun development types so the fixture generator is checked alongside the package tooling.\n\nAwait file writer operations to preserve ordered writes and satisfy the\nrepository promise lint rule. * build(perf): avoid Bun type dependency Keep the fixture generator dependency-free by declaring its small Bun API surface locally.\n\nRemove the Bun type package and restore the package TypeScript configuration so\npublishing the fixture generator does not expand package dependencies. * chroe(ci): fix nix cache * ci: add GitHub-hosted runner fallback Keep Blacksmith runners for the upstream repository while allowing forks\nto use hosted runners by default. Forks with a Blacksmith subscription can\nopt in through HAS_BLACKSMITH=true. * ci: skip pkg-pr previews without the GitHub App Forks do not inherit the pkg-pr-new GitHub App installation. Skip\npreview publishing and its dependent E2E and performance jobs unless a fork\nexplicitly opts in with HAS_PKG_PR_NEW=true. * ci: keep Windows arm release runner defined Supply both matrix runner fields so the release workflow resolves its\nWindows ARM runner consistently with the other native package targets. --------- Co-authored-by: ryoppippi <[email protected]> Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> Co-authored-by: sijie-ni-0214 <[email protected]> Co-authored-by: Ben Vargas <[email protected]> Co-authored-by: Mint Choco <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: turtton <[email protected]> Co-authored-by: Codesmith <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: ryoppippi <[email protected]> Co-authored-by: axisrow <[email protected]>

Proposed in #1393.
Tools built on pi (oh-my-pi and other forks) keep pi-format session stores at their own paths. ccusage can only read one pi path universe at a time (
--pi-path/PI_AGENT_DIR/ default), and labels everything it reads as agentpi. For anyone using pi alongside a fork, that means the fork's usage is either invisible in the unified reports, or reachable only by running a second, separate invocation pointed at the fork's path — a second full load-and-price pass whose output then has to be merged and relabeled externally, since both universes come back indistinguishably taggedpi(including in model labels and session identity). Reading the default store and a fork's store correctly in one report is simply not expressible today.Declare named extra stores in the config file instead:
{ "pi": { "stores": [ { "name": "omp", "path": "~/.omp/agent/sessions" } ] } }Each named store loads through the existing pi parser and surfaces as its OWN agent in the unified reports: rows tagged in
metadata.agents, sessions withprojectPath/lastActivitylike pi, model labels prefixed[<name>]. Named stores are additive to the default pi store and use the same path-list semantics (comma-separated, ~-expansion, dedupe) and the same date-window filtering asccusage pi ... --pi-path.Pricing safety: costs are computed from the unprefixed model name — the configurable store name never participates in pricing lookup (a store named
o3cannot fabricate o3 pricing; regression-tested), while prefixedpricingOverrideskeys are consulted first and keep working.Config validation: names match
^[a-z][a-z0-9_-]{0,31}$; reject collisions with built-in agents (single source of truth asserted against the unified loader's registry), duplicates, empty paths, and stores whose resolved paths overlap the default pi store or another store (silent double-counting is never possible). Invalid stores error through the same config-error path as other invalid config content, and only for unified reports that consume named stores — statusline and focused agent commands are unaffected. Absent store paths yield clean empty results, like default pi.Backward compatibility
Without
pi.storesconfigured, all output is byte-identical to before (verified against a golden matrix on real stores). That includes the pre-existing until-day session-window bug in the default pi unified path (#1390): it is deliberately preserved here, pinned by an explicit test, and fixed separately in #1394. Whichever of the two merges second handles the pinned-test flip. Committed config schema regenerated.Scope
No CLI surface changes: per-agent subcommands remain a closed set; named stores appear in unified reports only.
--pi-path/PI_AGENT_DIRsemantics unchanged (they affect only thepiagent).Docs
Updated
docs/guide/config-files.md,docs/guide/pi/index.md, regenerated committed config schema.Verification
cargo test --workspace(320 tests), fmt, clippy-D warnings, schema regen comparison — all pass.ccusage pi session --pi-pathrun exactly (same windowed session count, all with projectPath).Note for review ordering
This PR and #1396 both restructure
adapter/all/loader.rs— whichever lands second needs a rebase over the other; I'll handle it promptly.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Add config-declared named pi-format stores so unified reports can show
piand forks side-by-side as separate agents, avoiding extra runs and manual merging.New Features
pi.storeswithnameandpath; paths support comma-separated lists,~expansion, and dedupe.[name]followed by a space; JSON metadata includes the agent and sessionprojectPath/lastActivity.pricingOverrideslike[omp] gpt-5.4take priority.piand only for unified reports (daily,weekly,monthly,session);--pi-path/PI_AGENT_DIRstill affect onlypi; no new commands.Bug Fixes
namemust match^[a-z][a-z0-9_-]{0,31}$, be unique, and not use a built-in agent name.pior another store—equal, nested (ancestor/descendant), or partial across path-lists; missing paths load as empty.piand named stores in unified reports to matchccusage pibehavior.pi.storesare reported via the config error path for unified reports only; statusline and per-agent commands are unaffected.Written for commit a9b8cd4. Summary will update on new commits.
Relationship to #1193
Closes #1193, which asked for omp support via auto-detecting
~/.omp/agent/sessionsas a fallback pi path. This PR covers that request (omp is the motivating example — one config line) while fixing what a silent fallback can't:pi, indistinguishable from real pi usage in every report;The diff is larger than a fallback-path one-liner would have been; most of it is validation (name/path collisions, no double-counting) and tests keeping default-pi output byte-identical.
Summary by CodeRabbit
pi.storessupport to configure multiple named PI session stores (name+path) alongside the default PI data source.pi.stores(name/path rules, duplicate and collision detection) and safer config error handling.pi.storesexamples and detailed behavior (path handling, overlaps, labeling).