feat(usage): mirror token usage where usage reporters read it - #173
Merged
Conversation
filipeforattini
enabled auto-merge (squash)
September 8, 2026 14:33
…read Token accounting lived only inside the session store, so nothing outside redcode could report on it — and the store is ours to change. This adds a second, tiny SQLite file that carries only what a usage report needs: one row per assistant turn with tokens, cost, model, provider and timestamps, and no message content at all. ~/.red/code/data/usage/opencode.db The layout is OpenCode's, which `ccusage` reads today: OPENCODE_DATA_DIR=~/.red/code/data/usage ccusage opencode daily That variable takes a comma-separated list, so a machine that also runs OpenCode keeps both sources. Nothing here depends on ccusage and ccusage needs no redcode-specific code — which is the point: when the session store moves off SQLite, this file is the contract that stays. The mirror is written by the same projection that persists the message, so it tracks the store row by row, and a re-published message updates its row rather than adding one. It is best-effort: any failure turns the sidecar off for the process, is reported once through the projector's log, and never fails a turn. `REDCODE_DISABLE_USAGE_SIDECAR=1` opts out. Drivers follow the repo's `#sqlite`/`#pty` convention with a bun and a node variant behind `#usage-sidecar`. Verified end to end: a sidecar written by this code, read back by the published `ccusage`, reports the right days, tokens, cost and model. Claude-Session: https://claude.ai/code/session_01MyZkjLGy7k8uQdRWzU3aSB
The sidecar this builds on is ours and needs `OPENCODE_DATA_DIR` on the command line to be read. This adds the second target that needs nothing: when an OpenCode database exists, each assistant turn is mirrored into it too, so `ccusage opencode daily` reports redcode usage with no setup. Writing into another application's file is only safe if it honours that application's schema, and OpenCode's does not stand alone: `message` points at `session`, and `session` at `project`. The fan-out therefore writes all three, and every id it introduces is marked — `prj_redcode_…`, `ses_redcode_…`, `redcode_…` — so a row this process wrote is recognizable there and removable by hand. Rows go in with INSERT OR IGNORE for the session and project: we add, we never edit what OpenCode owns. The file is opened read-write and never created: a machine without OpenCode gets no fan-out and no invented database. Verified against a copy of a real OpenCode database (1,783 existing messages): `PRAGMA foreign_key_check` clean afterwards, the existing rows untouched, and the published `ccusage` reporting redcode's days beside OpenCode's own. The test suite pins the same properties against the real schema, copied into `test/fixtures/opencode-schema.sql`. Costs of this choice, accepted deliberately: redcode's sessions become visible in OpenCode's own session list (empty, since no parts are written), and deleting one there takes its usage rows with it through the foreign key's ON DELETE CASCADE. `REDCODE_DISABLE_USAGE_FANOUT=1` turns the second target off and keeps the sidecar; `REDCODE_DISABLE_USAGE_SIDECAR=1` turns off both. Claude-Session: https://claude.ai/code/session_01MyZkjLGy7k8uQdRWzU3aSB
filipeforattini
force-pushed
the
feat/usage-sidecar
branch
from
September 8, 2026 14:43
43de459 to
1471c27
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Token accounting lived only inside the session store, so nothing outside redcode could report on it — and that store is ours to change. This writes it a second time, in two places, carrying only what a report needs: tokens, cost, model, provider and timestamps, one row per assistant turn, and never a prompt, a tool result or a file path.
The two targets
Our sidecar —
~/.red/code/data/usage/opencode.db, in OpenCode's layout, whichccusagereads when pointed at the directory:It is ours, so it survives whatever the session store becomes. That was the point of doing this at all: when storage moves to RedDB, this file is the contract that stays, and no reporting tool ever needs a redcode-specific adapter or a dependency on our client.
The fan-out into OpenCode's own database, when one exists, so
ccusage opencodefinds redcode usage with no configuration. OpenCode'smessagerows are keyed tosession, andsessiontoproject, so the fan-out writes those two as well. Every id it introduces is marked —prj_redcode_…,ses_redcode_…,redcode_…— so its rows are recognizable and removable by hand. Session and project go in withINSERT OR IGNORE: this adds rows and never edits what OpenCode owns. The file is opened read-write and never created, so a machine without OpenCode gets no fan-out and no invented database.Why not a PR to ccusage instead
ccusage already ships an OpenCode adapter, and its record shape is ours (redcode forked OpenCode's message layout). Only path discovery differed. Its
CONTRIBUTING.mdalso gates PRs behind an approved issue, and a redcode adapter there would have to be rewritten the day storage moves off SQLite — while a client dependency on RedDB (BSL) inside an MIT CLI is not something to ask a maintainer for. Writing the format ourselves needs nobody's approval and cannot rot upstream.Verification
PRAGMA foreign_key_checkclean afterwards, existing rows untouched, and the publishedccusagereporting redcode's days beside OpenCode's own.ccusage:daily,session,monthlyandweeklyall read the sidecar correctly.packages/core/test/usage.test.tspins the layout, the record shape, the absence of content, the upsert on re-publication, both opt-outs, and the fan-out's foreign keys against OpenCode's real schema (copied intotest/fixtures/opencode-schema.sql).Costs accepted deliberately
Redcode's sessions become visible in OpenCode's own session list (empty, since no parts are written), and deleting one there takes its usage rows with it through
ON DELETE CASCADE— the sidecar keeps its own copy.REDCODE_DISABLE_USAGE_FANOUT=1turns off the second target;REDCODE_DISABLE_USAGE_SIDECAR=1turns off both.Not included: a backfill of the messages already in
redcode.db. The mirrors start from the moment this lands.https://claude.ai/code/session_01MyZkjLGy7k8uQdRWzU3aSB
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.