Skip to content

feat(usage): mirror token usage where usage reporters read it - #173

Merged
filipeforattini merged 2 commits into
mainfrom
feat/usage-sidecar
Sep 8, 2026
Merged

feat(usage): mirror token usage where usage reporters read it#173
filipeforattini merged 2 commits into
mainfrom
feat/usage-sidecar

Conversation

@filipeforattini

@filipeforattini filipeforattini commented Sep 8, 2026

Copy link
Copy Markdown

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, which ccusage reads when pointed at the directory:

OPENCODE_DATA_DIR=~/.red/code/data/usage ccusage opencode daily

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 opencode finds redcode usage with no configuration. OpenCode's message rows are keyed to session, and session to project, 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 with INSERT 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.md also 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

  • Against a copy of a real OpenCode database (1,783 existing messages): PRAGMA foreign_key_check clean afterwards, existing rows untouched, and the published ccusage reporting redcode's days beside OpenCode's own.
  • End to end with the published ccusage: daily, session, monthly and weekly all read the sidecar correctly.
  • packages/core/test/usage.test.ts pins 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 into test/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=1 turns off the second target; REDCODE_DISABLE_USAGE_SIDECAR=1 turns 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


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@filipeforattini
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
filipeforattini merged commit 95914d2 into main Sep 8, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant