Problem
Cost accounting currently spans a global JSONL ledger, workflow phase/run JSON files, paid-tool writers, Pulse summaries, and agent-authored org-dashboard cards. The paths do not share one transactional event model and several production semantics are incorrect.
Confirmed findings
-
The global ledger is unsafe and already malformed
costledger.Ledger.Append reads and rewrites the entire _system/costs.jsonl for every event.
- The live ledger is roughly 4.65 MB / 15k lines and its first line is malformed.
- Mutexes protect only individual Ledger instances, so tool writers, agent writers, or multiple processes can race and lose data.
-
The dashboard's “LLM calls” count is not an LLM-call count
- Aggregation increments
CallCount once per ledger entry.
- Production emits cumulative conversation/turn events; the actual
llm_call_count in generation metadata is discarded.
-
Effective model metadata is dropped
- CLI adapters can expose the served/effective model.
- The final cumulative token event builds a new metadata map and does not retain
cost_model_id / effective-model fields.
- Aggregation groups by requested
ModelID, even when an effective model exists.
-
Billing basis is mislabeled
- Token-rate calculations can flow through
TokenUsageEvent.TotalCost and are then labeled provider.
- Provider actuals, token estimates, and subscription shadow estimates are mixed into one total.
-
Reused agents can double-count
- Agent token APIs return cumulative lifetime totals.
- Builder/Pulse persistence can merge those cumulative snapshots as new deltas across sequential turns.
-
Workflow cost writers can overwrite each other
- LLM and paid-tool paths independently read-modify-write the same daily JSON files without one shared transaction.
-
Missing/corrupt evidence is hidden
- The workflow-cost API converts read failures into empty collections and still returns
success: true.
- Pulse can mistake accounting failure for zero cost.
-
Historical Pulse lookup uses the retired path
get_cost_summary checks runs/<run>/token_usage.json for non-current runs instead of the current costs/execution store.
-
Scope attribution is inferred unreliably
- Builder, Pulse, Goal Advisor, evaluation, and background-review costs are inferred from phase/step state.
- Only
planning is currently treated as phase-only, so other maintenance/background work can be misbucketed or omitted.
-
Tool-cost attribution depends on output paths
- Paid operations without output files are not recorded.
- Workflow attribution is inferred from the first output path rather than explicit execution context.
-
Frontend cache tokens can be double-counted
- The fallback to aggregate
cache_tokens is used when cache_read_tokens is zero, then cache-write tokens are added separately.
-
The workflow-cost popup does not scale
- It loads all cost files and then requests execution logs serially per run only to recover titles.
- The workspace already contains more than a thousand cost files.
Proposed architecture
Use a single SQLite-backed accounting repository with immutable event rows.
Each event should include:
- unique
event_id and idempotency key
- timestamp and user/workflow/session/run/execution IDs
- explicit scope:
builder, pulse, workflow_execution, evaluation, chief_of_staff, background_review, tool
- requested and effective provider/model
- turn count and actual LLM-call count
- prompt/output/reasoning/cache-read/cache-write tokens
- cost and currency
- billing basis:
provider_actual, token_estimate, subscription_shadow, unpriced
- pricing source/version
- tool name and operation metadata where applicable
Dashboards, Pulse, and org reporting should derive rollups from these immutable events. JSON/HTML outputs may remain presentation artifacts, not accounting sources of truth.
Migration plan
- Add SQLite event schema and repository with atomic insert/upsert.
- Emit per-turn or per-call deltas, never cumulative snapshots as deltas.
- Route LLM and paid-tool events through the same repository.
- Add explicit accounting scope to execution context.
- Expose paginated summary APIs with coverage and parse/persistence errors.
- Migrate valid legacy JSONL/JSON records with deterministic event IDs; quarantine malformed records.
- Switch Cost Dashboard, workflow popup, Pulse
get_cost_summary, and org cost cards to the canonical API.
- Remove legacy read-modify-write ledgers after a verification window.
Acceptance criteria
- Concurrent LLM and tool events cannot overwrite or duplicate each other.
- Retried events are idempotent.
- Actual LLM-call count is distinct from turns/accounting events.
- Requested and effective models are both retained.
- Actual, estimated, subscription-shadow, and unpriced amounts are separately visible.
- Pulse reports missing/unpriced accounting evidence explicitly.
- Historical and UTC-midnight-spanning runs resolve correctly.
- Builder/Pulse/evaluation/workflow/tool costs are separately attributable.
- Org dashboard cost status includes freshness and accounting coverage.
- Tests exercise real production
EndAgentSession events, reused multi-turn agents, concurrent writers, retries, malformed migration input, and midnight-spanning runs.
Problem
Cost accounting currently spans a global JSONL ledger, workflow phase/run JSON files, paid-tool writers, Pulse summaries, and agent-authored org-dashboard cards. The paths do not share one transactional event model and several production semantics are incorrect.
Confirmed findings
The global ledger is unsafe and already malformed
costledger.Ledger.Appendreads and rewrites the entire_system/costs.jsonlfor every event.The dashboard's “LLM calls” count is not an LLM-call count
CallCountonce per ledger entry.llm_call_countin generation metadata is discarded.Effective model metadata is dropped
cost_model_id/ effective-model fields.ModelID, even when an effective model exists.Billing basis is mislabeled
TokenUsageEvent.TotalCostand are then labeledprovider.Reused agents can double-count
Workflow cost writers can overwrite each other
Missing/corrupt evidence is hidden
success: true.Historical Pulse lookup uses the retired path
get_cost_summarychecksruns/<run>/token_usage.jsonfor non-current runs instead of the currentcosts/executionstore.Scope attribution is inferred unreliably
planningis currently treated as phase-only, so other maintenance/background work can be misbucketed or omitted.Tool-cost attribution depends on output paths
Frontend cache tokens can be double-counted
cache_tokensis used whencache_read_tokensis zero, then cache-write tokens are added separately.The workflow-cost popup does not scale
Proposed architecture
Use a single SQLite-backed accounting repository with immutable event rows.
Each event should include:
event_idand idempotency keybuilder,pulse,workflow_execution,evaluation,chief_of_staff,background_review,toolprovider_actual,token_estimate,subscription_shadow,unpricedDashboards, Pulse, and org reporting should derive rollups from these immutable events. JSON/HTML outputs may remain presentation artifacts, not accounting sources of truth.
Migration plan
get_cost_summary, and org cost cards to the canonical API.Acceptance criteria
EndAgentSessionevents, reused multi-turn agents, concurrent writers, retries, malformed migration input, and midnight-spanning runs.