Skip to content

Meter Amp, Cline, Gemini CLI, OpenCode and Qwen Code from what each one keeps - #772

Merged
iamenahs merged 15 commits into
mainfrom
claude/kind-morse-83c3b0
Sep 18, 2026
Merged

iamenahs merged 15 commits into
mainfrom
claude/kind-morse-83c3b0

Conversation

@iamenahs

@iamenahs iamenahs commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Follows #769, which metered Antigravity and merged as 0502b80d. This PR is based on main.

Summary

Besides Antigravity, five agents read not metered on the Dashboard: Amp, Cline, Gemini CLI, OpenCode and Qwen Code. I checked each one's own source, on 2026-09-18, for what it can tell a client. Each card now shows what that agent actually exposes:

  • Amp and Cline: a balance, read with the agent's own sign-in.
  • Gemini CLI, Qwen Code, OpenCode and Cline: what they spent, read from the records each one already keeps.

Before and after

The same desk and the same fixture records, captured on the real host with HOME pointed at the fixtures. Fake CLIs stand in for amp and agy, and a local server stands in for Cline's API. No real account or transcript is in either frame. Before is main at 0502b80d, where only Antigravity is metered (#769); after is this branch.

Before (main) After
Only Antigravity is metered; the other agents read not metered Each agent shows a balance, a spend or its quota

Screenshots

From the same fixture desk as the before and after above; no real account is in any frame.

Each agent's card

Agent Light Dark
Amp: balance from amp usage Amp card, light Amp card, dark
Cline: balance from Cline's account API Cline card, light Cline card, dark
Gemini CLI: spend from its chat logs, at list price Gemini CLI card, light Gemini CLI card, dark
OpenCode: spend at the cost OpenCode recorded OpenCode card, light OpenCode card, dark
Qwen Code: spend from its transcripts, at list price Qwen Code card, light Qwen Code card, dark

A spent balance

The Cline stand-in answering an overdrawn balance, −$0.016.

  • Line above the cards: "Cline is out of credits." (it used to say "No agent here reports plan usage.").
  • Card: says the balance waits for a top-up, not a reset.
  • Accounts rail: shows the balance in red where a percentage would go.
Light Dark
Cline out of credits, light Cline out of credits, dark
Accounts rail with balances, light Accounts rail with balances, dark

The money band

The four agents' spend is stacked by day. The sentence under the total now says what the figure is made of: part is what OpenCode and Cline recorded, and part is list price (mixed). Before, it called every figure "what these tokens would have cost at public API rates", which is no longer true of all of it (second commit, 66a277d9).

What it cost band with the new agents, light

Two more sentences called every cost a public price, which is no longer true when some prices are what an agent recorded. Both now say where the prices came from, the way the band's subtitle does: public rates, the agents' own records, or both, as here (fourth commit, f0531fc2).

  • The page's own sentence under Dashboard: "what the work cost at public rates" now reads "what the work cost as the agents recorded it or at public rates".
  • The footnote under Where it went, whenever no row was short: "Every call in this window has a public price, so these figures are exact." now reads "Every call in this window carries a cost — its agent's record or a public price — so none is left out."

Where it went, with the footnote for mixed prices, light

What each agent offers, and what the card now shows

Agent What it offers a client (checked in its source) Card
Amp amp usage prints the credit balance in a short markdown report written by Amp's server; there is no JSON form. Account lookups share a limit of 60 per hour with Amp's own. amp-acp receives the SDK's per-message usage and discards it. balance, e.g. $10.00 left; re-asked at most every 5 minutes
Cline Cline's CLI account screen calls /api/v1/users/me, then the balance of the active organization or the user (in micro-dollars), with the session stored in providers.json. Its ACP server receives a usage event from its engine and drops it (case "usage": return []). sessions.db keeps each session's usage.totalCost. balance, plus the spend Cline billed
Gemini CLI A Code Assist sign-in has a quota (the existing meter). An API-key sign-in has no quota to read anywhere. The chat logs record tokens and the model for every call. spend at list price
OpenCode No endpoint lets an API key read the Zen balance (requested upstream in anomalyco/opencode#10448). The Go limits can be read: GET /zen/go/v1/usage with the Go key returns the rolling 5-hour, weekly and monthly windows (shipped from anomalyco/opencode#16017). This PR doesn't read that endpoint yet, because there's no Go subscription here to test against. OpenCode sends usage_update (context size and session cost) and stores each session's tokens and cost in opencode.db. spend at OpenCode's own cost; a free model shows $0
Qwen Code No quota endpoint. The transcripts record usageMetadata for every call. spend at list price

Design

  • Cline's token is used while it is valid, never refreshed.
    • Refreshing rotates the refresh token. The desk never writes the new pair back to another application's file, so a refresh would sign Cline out.
    • Cline refreshes the token itself whenever it runs, including on every turn the desk sends it.
    • Between those refreshes the card keeps the last reading and shows how old it is.
  • Amp is read sparingly. Only two parts of the report are parsed: the **<name>:** $<n> remaining lines and Signed in as. A report with neither shows nothing. Running amp usage left Amp's binary and log files untouched.
  • Chat logs:
    • Gemini CLI appends a message again as its counts arrive, and a rewind hides messages but does not un-spend them. Each log is therefore re-read whole whenever it changes, counting the last record of each call.
    • Qwen appends one record per call, so it resumes from where the last scan stopped.
    • Both follow the Gemini API's counting rules: the prompt count includes cached tokens, while thinking and tool-use tokens are counted separately.
  • Agent-billed cost:
    • Requests the agent priced and requests it did not never share a ledger row (the scanner's key and the store's primary key both carry the difference), so a row's cost and its provenance are always one or the other. An older ledger is rebuilt in place with its rows.
    • A ledger row can now carry the agent's own vendorCost, and the ledger uses it in place of list price. A free model's $0 stays $0; it is not reported as unpriced.
    • Spend is labelled vendorMetered, or mixed when it also includes list-priced rows. The protocol already defined both labels; nothing used them.
    • For Cline, only each session's own usage is read, never aggregateUsage: subagents are separate rows, so adding the aggregate would count them twice.
  • Reading another application's database without writing next to it, and without guessing whether its owner is running. A plain read-only open of a WAL database whose owner isn't running creates -shm and -wal files in the owner's folder (measured), and neither a leftover -shm nor a missing one says who has it open: Cline's has outlived its last run by weeks, and an owner in exclusive locking mode never makes one. readForeignDatabase chooses from what is on disk instead. A WAL database with neither -wal nor -shm beside it is opened immutable, and the read is checked afterwards, so an owner starting mid-read has its answer discarded. One with either file is copied, with its log, to a private folder and read there; the copy is trusted only if the source's size and time were the same before and after it, and is retried up to three times. Nothing of the owner's is opened, locked or recreated.
  • Binding:
    • Agents with no knowledge-table entry (Amp's registry adapter, Qwen Code run through npx) are identified by the program they run, so their meters are actually used.
    • Paths are resolved with each row's own environment, so a row that moves an agent's home is read from the new location.
  • Card copy:
    • The money band's subtitle follows its provenance: list price, the agents' own figures, or both.
    • So do the page's own sentence and, when no call is missing a price, the footnote under Where it went.
    • A balance now counts as reported usage in the line above the cards. An overdrawn Cline shows Cline is out of credits. instead of No agent here reports plan usage.
    • A spent balance says it waits for a top-up, not a reset.
    • The Accounts rail shows the balance where a percentage would go.

A bug the new tests found

projectRootOf memoised the first repository-less folder's answer onto every ancestor folder. Every later folder without a repository was then filed under that first one. Asking for /work/a and then /work/b returned /work/a both times, which merged unrelated projects in the Where it went band.

It now remembers, for each folder it walks, the repository at or above it, or that there is none. One CRLF test had only passed because of the bug, since it merged the fixture's two folders into one row. It now counts across rows.

Checked against real records

The same build also ran against the development machine's own records, with nothing published from it:

  • Every card showed the figure its agent itself reports.
  • An overdrawn Cline balance produced "is out of credits" in both the header and the card.
  • Nothing was written next to OpenCode's or Cline's databases.

The frames from that run showed real accounts and were deleted.

Upstream

Filed while working on this PR. Each report links this PR as the workaround in use until it's resolved.

Project Filed What it asks for
cline/cline bug #14251, PR #14252 The ACP server should forward the engine's usage as usage_update and PromptResponse.usage; today it drops the event.
tao12345666333/amp-acp PR #63 Each prompt answers with the turn's token usage (PromptResponse.usage), counted from the usage Amp reports for each response.
google-gemini/gemini-cli bug #29389 Send the standard usage_update and PromptResponse.usage, not only _meta.quota. The same request (google-gemini/gemini-cli#24280) was closed as completed without the change.
anomalyco/opencode feature #49693 The ACP server should report the Go limits and the Zen balance. The Zen balance endpoint itself is already requested in anomalyco/opencode#10448.
Amp CLI not filed Amp is closed source with no public tracker. A request for amp usage --json is drafted for [email protected].

Nothing is filed for Qwen Code: it has no quota to report, and its transcripts already carry usage.

Test plan

  • ledger-agents.test.ts:
    • Gemini: a re-appended, rewound and checkpointed message counts once, at its last counts; a changed file replaces its rows.
    • Qwen: a call written on two lines counts once.
    • OpenCode: the free model's $0 and the paid cost are used as recorded, and nothing is written next to the database.
    • Cline: the usage versus aggregateUsage rule.
    • mixed provenance, the store migration, readForeignDatabase on idle, live-owner, exclusive-locking and racing databases, a priced and an unpriced session sharing a day, model and folder, the store's key and its migration, a moved Gemini home, each agent's path overrides, and the projectRootOf regression.
  • credit-meters.test.ts:
    • Amp: report parsing, the five-minute reuse, silence when signed out, and a spent balance.
    • Cline: the personal and organization balances, the micro-dollar scale, an expired token (kept reading, no request), a changed account, 401 and 503 responses, and the row's environment.
  • local-meters.test.ts: each agent binds, including rows with no knowledge-table entry.
  • usage.test.ts and Usage.note.test.tsx: the balance-aware header line, the top-up sentence, and the page sentence and table footnote (costClause, pricedNote).
  • UI suite: 218 files, 2,647 tests. Server ledger and usage tests: 104.
  • node script/verify.mjs on 6206125a, on main at 4762d35c: every step green.
  • Review round 1 of at most 3 (Codex GPT-5.6-Sol, low) found three real defects, fixed in 6206125a; see the reply on this PR.
  • The launched Electron app, on a staged desk (fake HOME with fixture records, fake amp and agy, a local stand-in for Cline's API): every card shows its figure; the page sentence and the Where it went footnote read as in the frames above; switching the stand-in to −$0.016 and pressing Refresh turns the line above the cards to "Cline is out of credits.", the card to the top-up sentence and the rail to a red balance; no -shm or -wal file appears beside the fixture databases.
  • Real host with fixtures on main and on this branch: the frames above. Before was re-shot on main at 0502b80d. The overview, the Accounts rail and the table footnote were re-shot on Meter Antigravity from agy's own /usage, and step around a spent model group #769's ed19ae3d; what reached main since changes nothing these frames show (a server-side hold on agy's last answer, seating, two z-index values turned into tokens, and docs).

🤖 Generated with Claude Code

@iamenahs
iamenahs marked this pull request as ready for review September 18, 2026 06:41
@iamenahs iamenahs self-assigned this Sep 18, 2026
@iamenahs
iamenahs force-pushed the claude/antigravity-cli-metering-e6dae6 branch 2 times, most recently from 7e7e178 to ed19ae3 Compare September 18, 2026 08:25
@iamenahs
iamenahs force-pushed the claude/kind-morse-83c3b0 branch from 78baeac to 397d473 Compare September 18, 2026 08:31
@iamenahs
iamenahs force-pushed the claude/antigravity-cli-metering-e6dae6 branch from 3cee81c to 7991631 Compare September 18, 2026 08:59
@iamenahs
iamenahs force-pushed the claude/kind-morse-83c3b0 branch from 397d473 to e978a80 Compare September 18, 2026 09:01
@iamenahs
iamenahs deleted the branch main September 18, 2026 09:15
@iamenahs iamenahs closed this Sep 18, 2026
@iamenahs iamenahs reopened this Sep 18, 2026
@iamenahs
iamenahs changed the base branch from claude/antigravity-cli-metering-e6dae6 to main September 18, 2026 09:19
@iamenahs
iamenahs force-pushed the claude/kind-morse-83c3b0 branch from e978a80 to ee5d429 Compare September 18, 2026 09:21
@iamenahs iamenahs added bug Something isn't working area: adapter-acp Where it lives severity: medium Wrong behaviour in a narrower path labels Sep 18, 2026
iamenahs and others added 4 commits September 18, 2026 13:06
…ne keeps

Five agents read "not metered" on the Dashboard beside Antigravity (#769).
Each was checked in its own source, on 2026-09-18, for what it can tell a
client, and each now tells it.

Balances, asked of the vendor with the agent's own sign-in:

- Amp: `amp usage` prints the balance in a short report Amp's server writes
  ("**Individual credits:** $10 remaining"); there is no JSON form, so only
  that line and "Signed in as" are read. Lookups share a 60-an-hour limit
  with Amp's own, so a reading stands for five minutes. Running it left
  Amp's binary and log untouched.
- Cline: the same two calls Cline's CLI makes for its account screen
  (`/api/v1/users/me`, then the active organization's or the user's balance,
  in micro-dollars), with the session in `providers.json`. The token is used
  while valid and never refreshed: a refresh rotates the refresh token, and
  not writing it back would sign Cline out. Between Cline's own refreshes the
  last reading stands with its age.

Spend, read from the agents' own records into the ledger:

- Gemini CLI and Qwen Code chat logs, priced at list price. Gemini writes a
  message again as its counts arrive and hides rewound ones without
  un-spending them, so a log is read whole each time it changes, at the last
  record of each call; Qwen appends a record per call and resumes by offset.
  Both use the Gemini API's arithmetic (the prompt includes the cache,
  thinking and tool-use prompts are counted apart).
- OpenCode's `opencode.db` and Cline's `sessions.db`, at the cost the agent
  recorded, a free model's zero included. Rows can now carry that
  `vendorCost`, the ledger prefers it to list price, and spend says
  `vendorMetered` or `mixed`, the provenance the protocol already had and
  nothing used. Cline's own `usage` is read, never `aggregateUsage`, whose
  subagents are rows of their own.
- Another application's SQLite file is opened through `openForeignDatabase`:
  a WAL database whose owner is not running is opened `immutable`, because a
  plain read-only open creates `-shm` and `-wal` in the owner's folder
  (measured).

Binding: agents with no knowledge-table entry (Amp's registry adapter, Qwen
Code through npx) are named by the program they run, so their meters are
reached, and each agent's records are found with its row's environment, so a
row that moves an agent's home is read from where it moved it.

On the card: a balance counts as usage reported in the line above the cards
("Cline is out of credits." where it said "No agent here reports plan
usage."), a spent balance says it waits for a top-up rather than a reset,
and the Accounts rail shows the balance where a percentage would go.

Also a bug the new tests found in `projectRootOf`: its memo stored the first
repository-less folder's answer on every ancestor, so every later folder
without a repository was filed under that first one. It now remembers the
repository at or above each folder walked, or that there is none. A CRLF
test had passed only because of it and now counts across rows.

Checked against this machine's real records (not published): Gemini CLI
$2.15 over 103 calls, Cline $0.65 Cline-billed over 14 sessions, OpenCode
$0 on its free model, Amp $10 left, and nothing written beside OpenCode's
or Cline's databases.

Co-Authored-By: Claude Opus 5 <[email protected]>
The band under "What it cost" said "what these tokens would have cost at
public API rates. Not a bill." of every total. Since the previous commit,
OpenCode's and Cline's rows are the cost each agent recorded (a free model's
zero included, and Cline's is what it billed), so on a desk with either of
them the sentence was false about part of the figure it sat under.

`spendHint` reads the provenance the ledger already reports: the list-price
sentence as before, "what the agents recorded these tokens cost." for
`vendorMetered`, and for `mixed` that it is both and not all of it is a bill.

Co-Authored-By: Claude Opus 5 <[email protected]>
The first commit said Zen's balance and Go's limits were both closed to an
API key. Zen's balance is: no endpoint reads it (asked upstream in
anomalyco/opencode#10448). Go's limits are not: `GET /zen/go/v1/usage` with the
Go key returns the rolling 5-hour, weekly and monthly windows, shipped from
anomalyco/opencode#16017, found while filing the ACP request. The desk does
not read it yet, since there is no Go subscription here to measure it
against; the comment and the doc now say so.

Co-Authored-By: Claude Opus 5 <[email protected]>
…nts' own

Two sentences still called every cost a public price. With Cline's and
OpenCode's calls priced at the cost each agent recorded, both were false, on
a page whose band header already says "what the agents recorded":

- the page's own sentence: "what the work cost at public rates";
- the footnote under *Where it went*, whenever no row was short: "Every call
  in this window has a public price, so these figures are exact."

Both now follow the ledger's provenance, as the band header does: public
rates for list price, the agents' own records for `vendorMetered`, and both
for `mixed` (`costClause`, `pricedNote`). The footnote's sentences for rows
with a model that has no price are unchanged. Pinned in usage.test.ts.

Co-Authored-By: Claude Opus 5 <[email protected]>
@iamenahs
iamenahs force-pushed the claude/kind-morse-83c3b0 branch from ee5d429 to f0531fc Compare September 18, 2026 20:08
@iamenahs

Copy link
Copy Markdown
Member Author

Review by Codex 0.155.0 · GPT-5.6-Sol · Low effort · via HarnessDesk

I found three correctness defects.

  • [P1] packages/server/src/ledger/foreign-db.ts:29 — -shm absence does not establish that the owner is not running. SQLite may omit the shared-memory file while an active connection uses exclusive locking, and there is also a check/open race in which the owner can start after existsSync returns false. This branch then opens the live database with immutable=1, which deliberately skips locking and change detection; if the file changes, SQLite documents incorrect results or SQLITE_CORRUPT as possible outcomes. The resulting scan can silently replace the ledger with a stale/inconsistent snapshot. The test at packages/server/test/ledger-agents.test.ts:277 only closes the writer before opening the reader, so it proves “no side files are created for a quiescent database,” not the claimed owner-detection rule. This needs a read strategy whose safety does not depend on inferring process ownership from -shm, plus an active-writer/race test.

  • [P1] packages/server/src/ledger/scan.ts:162 — aggregating a priced session with a session lacking vendorCost turns both into vendor-priced usage. Rows are keyed only by day/model/project. sumCost(null, 0.50) becomes 0.50, and then packages/server/src/ledger/index.ts:252 treats every request and every token in that row as vendor-metered and uses only $0.50. Thus one OpenCode/Cline session with tokens but no recorded cost, grouped with one that has a recorded cost, loses the former session's list-price fallback (or its unpriced coverage) and falsely reports complete vendorMetered provenance. The SQL upsert at packages/server/src/ledger/store.ts:150 has the same lossy representation, although whole-file replacement currently avoids that path for these two corpora. Please preserve vendor-priced and non-vendor-priced request/token totals separately, and add a same-day/same-model/same-project mixed-session test.

  • [P1] packages/server/src/bootstrap.ts:520 — a moved Gemini home splits quota and spend across different accounts. The new corpus binding correctly resolves chat logs through the row's merged environment, but new GeminiMeter() still resolves oauth_creds.json from the process user's homedir() (packages/server/src/usage/gemini.ts:116). With GEMINI_CLI_HOME on the agent row, the card can therefore show quota/account A from the default home beside spend from account B in the moved home. Cline passes the row environment to both paths; Gemini needs the same treatment and a binding test that asserts both the meter watch path and corpus root move together.

Risks and missing coverage:

  • Gemini's whole-file last-record-by-message-id scheme is internally consistent for the measured shape, and the test covers a repeated reply, later token counts, rewind, and a checkpoint. It does not cover a later checkpoint restating an older/incomplete copy of an already-final message; I cannot tell from this repository alone whether Gemini CLI can emit that ordering. A fixture captured from that transition, or a source-level invariant from its recorder, would settle it.
  • Amp's five-minute minimum interval and narrow Signed in as / balance parsing are clear and tested. There is no fixture for multiple balance lines or a changed Markdown report, so an upstream format change will become silence; that is acceptable fail-closed behavior, but it should be an intentional monitoring risk.
  • The database target includes WAL size and mtime, and whole-file scans replace prior rows, which is the right shape once a consistent snapshot is obtained. There is no concurrent WAL checkpoint/write test.

What I checked and found sound: Cline uses a valid access token without refreshing or writing another application's file; expiry preserves the last reading with its original age and account changes discard it. The billed-account selection (active organization, otherwise user), micro-dollar conversion, and usage rather than aggregateUsage avoid the stated subagent double count. OpenCode/Cline zero vendor cost remains a real priced zero. For homogeneous rows, vendorCost storage, replacement scans, ledger preference, and vendorMetered/mixed propagation agree. The Dashboard's headline, band hint, footnote, balance rail, and top-up wording follow provenance/credit semantics. Gemini/Qwen token arithmetic separates cached prompt tokens and adds thinking/tool tokens in the documented directions. Amp throttling, signed-out handling, and command-name binding are coherent.

I read the full 2,496-line diff and the touched implementation/tests at f0531fc2. Per the read-only instruction I did not run builds or tests; I independently confirmed the live head/base and that the three substantive CI checks are green.

Verdict: request changes

…t guessing, keep priced and unpriced rows apart, follow the Gemini home

Codex GPT-5.6-Sol (low) found three real defects.

- Another application's SQLite file was opened `immutable` whenever no `-shm`
  sat beside it. That absence says nothing: an owner in exclusive locking mode
  never makes one, and a leftover one (Cline's has outlived its run by weeks)
  says nothing either. `readForeignDatabase` now chooses from what is on disk.
  A WAL database with neither `-wal` nor `-shm` is opened `immutable` and the
  read is checked afterwards, so an owner that starts mid-read has its answer
  discarded. One with either file is copied, with its log, to a private folder
  and read there, trusted only if the source's size and time were the same
  before and after, and retried up to three times. Nothing of the owner's is
  opened, locked or recreated.
- A session the agent priced and one it did not, on one day, model and folder,
  were summed into one row, which the ledger then called wholly the agent's:
  the unpriced one lost its list price and the provenance claimed more than
  was known. The scanner's key and the store's primary key now carry whether
  the agent priced a request, so the two never share a row. A ledger written
  before is rebuilt in place, rows kept.
- A moved Gemini home (GEMINI_CLI_HOME) moved the chat logs but not the
  sign-in, so a card could show one account's quota beside another's spend.
  The meter now takes the row's environment, as Cline's already did.

Each fix has a test that fails without it: the reader against an owner with
commits only in its log, an exclusive-locking owner, an owner starting during
an immutable read, and a commit during the copy; the mixed-session row through
the whole ledger, the store's key and its migration; the Gemini meter's path
and the corpus root moving together.

Also stated in usage/amp.ts: a reworded Amp report reads as silence on
purpose. And measured, not changed: on 166 real Gemini chat logs (104 message
ids, 242 checkpoints) no later record restates lower counts than an earlier
one, so the last record of each call is also its largest.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@iamenahs

Copy link
Copy Markdown
Member Author

Round 1 of at most 3 — Codex GPT-5.6-Sol (low) requested changes on f0531fc2. All three P1s were real and are fixed in 6206125a; each fix has a test that fails without it (I mutated the code it guards and watched it fail, then restored it).

[P1] foreign-db.ts: no -shm does not mean the owner is not running — fixed.

  • Agreed on both counts. Measured here: a Cline database has kept a -shm since a run that ended weeks ago, and an owner in exclusive locking mode never makes one.
  • The reader no longer infers the owner. It picks from what is on disk: a rollback-journal database gets an ordinary read-only open; a WAL database with neither -wal nor -shm is opened immutable, and the read is checked afterwards (size and time of both files, and whether a side file appeared), so an owner that starts mid-read has its answer discarded; a WAL database with either file is copied, with its log, into a private folder and read there, trusted only if the source was the same before and after the copy, and retried up to three times.
  • New tests: an owner with commits only in its log (its files are byte-for-byte and time-for-time unchanged, and the rows only the log holds are seen); an exclusive-locking owner; an owner starting during an immutable read; a commit during the copy (retried) and a writer that never stops (refused). Mutated to the old rule, to no check after the immutable read, and to no check after the copy: each fails its own test.
  • One limit worth knowing: a database over 1 GiB that is in use is refused rather than copied. The real ones here are 1 MB and 172 KB.

[P1] scan.ts: a priced and an unpriced session folded into one row — fixed.

  • Confirmed by reading add() and #price. sumCost(null, 0.5) is 0.5, and the ledger then treated every request in the row as the agent's.
  • Priced and unpriced requests now never share a row: the scanner's key and the store's primary key both carry a vendored bit. An existing ledger is rebuilt in place with its rows (empty vendor cost stays unvendored), and reopening it changes nothing.
  • New tests: one day, one model, one folder, one session with a recorded cost and one without, through the whole ledger (the total is $0.50 plus the other's list price, provenance mixed, both requests priced); the store keeps the two kinds apart and adds up within each; the migration test now also proves the rebuilt key. Without the fix, all three fail, and mutating the scanner's key and the store's key separately each fails a test.

[P1] bootstrap.ts: a moved Gemini home split quota from spend — fixed. GeminiMeter now takes the row's environment and reads its sign-in from GEMINI_CLI_HOME when set, the same place corpusRoot reads the chat logs. A binding test asserts the meter's watch path and the corpus root move together; it fails without the change.

Risks and missing coverage

  • Gemini checkpoint restating an older copy of a final message: no code change. Measured on 166 real chat logs here (104 message ids with counts, 242 checkpoints): no later record has lower counts than an earlier one for the same id, so last-record-wins and largest-wins never differ.
  • Amp's report format: now stated in usage/amp.ts as an intentional fail-closed choice (a reworded line reads as "not metered", not a guessed figure; the first balance line is the one shown). The no-balance-line report is already tested.
  • Concurrent WAL checkpoint or write: covered by the new reader tests above.

node script/verify.mjs is green on 6206125a. Round 2 will be asked to review the repair, since that is where the next defect is most likely.

@iamenahs iamenahs left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by Codex GPT-5.6-Sol · Low · via HarnessDesk

Review by Codex 0.155.0 · GPT-5.6-Sol · Low effort · via HarnessDesk

I found no remaining correctness defect on 9bc375a1.

The three round-1 P1 repairs are sound:

  • packages/server/src/ledger/foreign-db.ts:59: readForeignDatabase no longer infers ownership from -shm. A quiescent WAL database is read immutably and rejected if the database/WAL stamps or side-file presence change; a database with either WAL side file is copied with its WAL, checked against the source, retried on a detected race, and read only from the private copy. Errors propagate to the scan, so packages/server/src/ledger/index.ts:210 retains the prior rows rather than replacing them with a torn or empty read. The active-WAL, exclusive-locking, owner-starting, one-commit retry, and continuously-changing refusal tests exercise the important branches.
  • packages/server/src/ledger/scan.ts:154 and packages/server/src/ledger/store.ts:83: the vendored bit is now part of both aggregation identities. The store migration derives it from vendorCost, and the upsert conflict target matches the new primary key, so a priced request cannot absorb an unpriced request while requests within either class still accumulate. The same-day/same-model/same-project ledger test proves the observable result: recorded cost plus list-price fallback with mixed provenance.
  • packages/server/src/usage/gemini.ts:121 and packages/server/src/bootstrap.ts:502: GeminiMeter and corpusRoot('gemini', env) receive the same merged row environment. The moved-home binding test checks both credential watch path and transcript root, closing the account-splitting defect.

Risks and missing tests:

  • packages/server/src/ledger/foreign-db.ts:139 deliberately uses size/mtime stamps rather than a SQLite backup API. That is consistent with the read-only requirement and the tests cover the application-level races this PR can induce, but an adversarial same-size/same-mtime rewrite or atomic replacement is not covered. I do not see evidence that OpenCode or Cline writes that way; proving more would require a live stress test against their actual database writers or adding inode/content/version checks.
  • packages/server/src/ledger/store.ts:109 tests migration from the pre-vendorCost schema and reopening the result, but not a separately constructed intermediate schema that already has vendorCost and lacks vendored. The migration SQL handles that shape directly (carried === true), so this is a coverage gap rather than a supported defect.
  • packages/server/src/ledger/foreign-db.ts:112 intentionally refuses an active database whose database plus WAL exceed 1 GiB. That preserves correctness and previous rows, but a user with a very large OpenCode/Cline store will see stale spend until the database becomes quiescent or smaller; there is no boundary test at the limit.
  • Per the read-only instruction I did not run tests or builds. I confirmed the checkout and live PR head are 9bc375a1, the base is 6b007c6d, and GitHub reports the build/test, browser integration, and native integration checks green on that head. I also inspected both main-merge resolutions; their substantive main-side changes are in menu/delegation and reduced-motion/workbench surfaces, not this PR's usage implementation.

What I checked and found sound beyond the repairs: OpenCode and Cline whole-file scans replace prior file rows only after a successful read; zero vendor cost remains a priced zero; ledger pricing and vendorMetered/mixed propagation agree with the Dashboard's band, page sentence, footnote, balance rail, and top-up wording. Cline continues to avoid refresh-token rotation, selects the active organization's balance before the user's, converts micro-dollars correctly, and reads usage rather than double-counting aggregateUsage. Gemini/Qwen token normalization, Gemini last-record deduplication, Amp's fail-closed parser and five-minute throttle, command-name binding, environment-specific corpus roots, and the project-root memoization fix are coherent with their tests and callers.

I read the complete PR diff and every touched implementation, test, and documentation file at 9bc375a1.

Verdict: approve

… two migrations, state the two accepted limits

Codex GPT-5.6-Sol (low) approved round 1's three fixes with no remaining
correctness defect, and named three residual risks — none an actionable
defect in this PR's own reach.

- The consistency check in `readForeignDatabase` compares size and
  modification time, not bytes, so a rewrite landing on both by chance would
  slip past it. Neither OpenCode's nor Cline's writer does this: every write
  through SQLite's WAL path changes the WAL file's size. Stated as the
  deliberate tradeoff it is, in `foreign-db.ts` and the docs, rather than
  adding a byte-level check to guard a pattern neither writer uses.
- A store already carrying `vendorCost` but not the `vendored` key bit — the
  shape between the two migrations — had no test. Added: built with that
  exact intermediate schema, migrated, read back correct. It fails without
  the migration's `carried` detection.
- The 1 GiB snapshot limit is now stated in `foreign-db.ts` as deliberate: a
  database that size and possibly open is refused rather than copied, so a
  very large store keeps its last good rows rather than paying for a
  whole-file copy on every scan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@iamenahs

Copy link
Copy Markdown
Member Author

Round 2 of at most 3 — Codex GPT-5.6-Sol (low) approved on 9bc375a1, with no remaining correctness defect: "The three round-1 P1 repairs are sound."

Three residual risks, all in code this PR touches but none an actionable defect:

  • A same-size/same-mtime rewrite would slip past the copy's consistency check. Agreed this is a real gap in the check, not in the data: readForeignDatabase compares size and modification time, not bytes. Neither OpenCode's nor Cline's writer does this — every write through SQLite's WAL path changes at least the WAL file's size — so I've stated the tradeoff explicitly in foreign-db.ts and docs/usage-dashboard.md rather than adding a byte- or version-level check that would cost a read of the file on every scan to guard a pattern neither writer uses.
  • A store already carrying vendorCost but not vendored wasn't tested. This is the shape between the two migrations. Added: a store built with that exact intermediate schema, migrated and read back correctly. New, it fails without the migration's carried detection — mutated to always take the "no vendorCost" branch and it fails.
  • No boundary test at the 1 GiB snapshot limit. Also now stated as deliberate in foreign-db.ts: a database that size and possibly open is refused rather than copied, so a very large store keeps its last good rows rather than paying for a whole-file copy on every scan.

node script/verify.mjs is green on 05795ce4.

Both rounds are done: round 1 found and fixed three P1s, round 2 found no defect and closed the one real coverage gap it raised. Merging.

@iamenahs

Copy link
Copy Markdown
Member Author

Review by Codex 0.155.0 · GPT-5.6-Sol · Low effort · via HarnessDesk

I found one remaining correctness defect.

  • [P1] packages/server/src/bootstrap.ts:528 — Cline's meter and ledger ignore the row's supported --data-dir override. The runtime starts Cline with agent.args, and this codebase explicitly records in packages/server/src/installs/known-agents.ts:271 that Cline moves its state with --data-dir <dir> and has no environment variable for that purpose. The existing identity reader already resolves that flag at packages/server/src/installs/identity.ts:192. The new usage binding instead constructs ClineMeter from only env and resolves the corpus through corpusRoot('cline', env); packages/server/src/usage/cline.ts:94 and packages/server/src/ledger/scan.ts:891 therefore fall back to the process user's ~/.cline/data (or to undocumented CLINE_*_DIR variables) even when the row actually runs cline --data-dir /other/account --acp. On such a valid custom row, the Dashboard can show the default Cline account's balance and spend beside a runtime using the other account, or show “not metered” despite records in the configured directory. The binding needs to derive both the credentials path and sessions.db from the same --data-dir argument Cline runs with, including Cline's relative-path resolution against the row's cwd. A regression test should bind a row with args: ['--acp', '--data-dir', moved] and assert both meter.watchPaths() and root point under moved; the current tests cover only invented environment overrides and therefore miss the supported path.

Risks and missing coverage:

  • The same binding layer does not exercise relative --data-dir values with a custom cwd. That is part of the defect above rather than a separate finding; the existing identity implementation documents the required resolution rule.
  • Amp's Markdown parser intentionally fails closed on a changed report and selects the first balance line. The no-balance case is tested, but there is still no multiple-balance fixture; I agree this is a documented upstream-format risk, not a correctness defect in the measured shape.
  • The foreign-database consistency check remains stamp-based, and a possibly-live database above 1 GiB remains unread. Both limits are now explicit in foreign-db.ts and docs/usage-dashboard.md, match the measured SQLite writers, and preserve the last good ledger rows on refusal. I do not consider either an actionable defect here.

What I checked and found sound: the round-1 immutable/snapshot reader repair covers quiescent WAL, active WAL, exclusive locking, start-during-read, commit-during-copy, retry exhaustion, cleanup, and read-only behavior. The round-1 priced/unpriced split remains correct in both the scanner key and store primary key; zero vendor cost stays priced, and the query produces honest vendorMetered or mixed provenance. The round-2 intermediate-schema migration test now covers a store with vendorCost but without vendored, and the migration preserves both null and recorded costs. Gemini's moved-home fix keeps its credential and transcript roots together for GEMINI_CLI_HOME; Gemini/Qwen token arithmetic, whole-file versus incremental scanning, OpenCode/Cline session-total treatment, and Cline's usage rather than aggregateUsage rule are coherent. Amp throttling/error handling, Cline token non-refresh, active-organization balance selection, micro-dollar conversion, stale-reading behavior, and account-change invalidation are sound. The Dashboard's balance headline/rail/top-up copy and list-price/vendor/mixed explanations match the ledger provenance.

I read the complete 2,909-line diff and the touched implementation/tests at 180a7a3b, plus the repository's agent registration and identity contracts that govern these paths. Per the read-only instruction I did not edit files, build, or run tests. I confirmed the exact head and current checks: build/typecheck/test and both UI-system jobs are green; the unsigned package job is still not reported successful.

Verdict: request changes

…was read from the default account, not its own

Codex GPT-5.6-Sol (low) found one real defect, the last round under the
project's 3-round limit.

Cline moves its whole folder with `--data-dir <dir>` on the row, and has no
environment variable for it (`known-agents.ts` already says so, and
`identity.ts` already resolves it for the account chip). The usage binding
only read `CLINE_DATA_DIR`/`CLINE_DIR`, which such a row never sets, so a
second Cline account run this way showed the default account's balance and
spend, or nothing at all, despite `--data-dir` naming exactly where its
records are.

`clineDataFolder` in `identity.ts` is now `clineDataDirOverride`, doing only
the unambiguous half — resolving `--data-dir` the way Cline resolves it,
relative to the row's own `cwd` — and reused from `bootstrap.ts`: a row that
carries the flag has its meter and its ledger corpus both built from that
folder; one that does not falls back to the environment as before.

New test: a row with `--data-dir /work/second-cline` binds its meter and its
ledger root under that folder, a relative `--data-dir` resolves against the
row's `cwd`, and a plain row keeps the environment-based default. It fails
without the fix.

Per the review process's 3-round limit, this is the last round: I am not
running a fourth. `node script/verify.mjs` is green on this commit; the PR
reply and the report to the owner cover what happens next.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@iamenahs

Copy link
Copy Markdown
Member Author

Round 3 of at most 3 — Codex GPT-5.6-Sol (low) requested changes on 180a7a3b. This was the last round under the project's review limit.

[P1] bootstrap.ts: Cline's usage binding ignored the row's --data-dir, its real (and only) way to move accounts — fixed in 776a6995.

  • Agreed. known-agents.ts already documents that Cline has no environment variable for --data-dir, and identity.ts already resolves it for the account chip; the usage binding read only CLINE_DATA_DIR/CLINE_DIR, which such a row never sets. A row running cline --data-dir /other/account --acp could show the default account's balance and spend beside a runtime signed in as someone else.
  • identity.ts now exposes clineDataDirOverride, the unambiguous half of the existing resolver (just --data-dir, resolved relative to the row's cwd), and bootstrap.ts builds both ClineMeter and the ledger's corpus root from it when a row carries the flag.
  • New test: a row with --data-dir /work/second-cline binds both to that folder, a relative --data-dir resolves against the row's cwd, and a plain row keeps the environment-based default. It fails without the fix (mutated the binding back to the env-only path and watched it fail).
  • node script/verify.mjs is green on 776a6995.

Per the project's 3-round limit, I am not running a fourth round or merging. I've reported the full state — every round, this fix, and what's still open — to the owner directly; they'll decide how to proceed.

@iamenahs iamenahs left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by Codex GPT-5.6-Sol · Low · via HarnessDesk

Review by Codex 0.155.0 · GPT-5.6-Sol · Low effort · via HarnessDesk

I reviewed the full 21-file diff at 086058a6 and then read every touched file in this checkout. I did not build or run tests, per the read-only instruction. The current checks I could inspect show Build/typecheck/test, UI-system native integration, and UI-system browser integration passing; the unsigned package job had not completed when I checked.

Findings:

  • packages/server/src/bootstrap.ts:539 drops Cline's CLINE_DB_DATA_DIR as soon as the row also has --data-dir. The no-flag path delegates to corpusRoot, whose Cline branch explicitly gives CLINE_DB_DATA_DIR precedence over the data folder (packages/server/src/ledger/scan.ts:891-893), but the round-3 branch hard-codes <override>/db/sessions.db. A valid row using --data-dir for settings/account state and CLINE_DB_DATA_DIR for the database will therefore bind the meter to the right account but scan the wrong (usually absent or stale) database. The new local-meter test covers absolute, relative, and plain --data-dir, but not this supported combination.
  • packages/server/src/bootstrap.ts:554-556 binds every Amp row to a default new AmpMeter(), even though this function has already constructed the row's merged environment. AmpMeter then resolves amp from the host process PATH/home and launches it without the row environment (packages/server/src/usage/amp.ts:91-107,134-139). An Amp ACP row that runs with a row-specific PATH or home can work normally while the card either finds no Amp CLI or reports the host's default Amp account instead of that row's account. The Amp tests inject a command/runner directly, so they do not cover the actual localUsageFor binding with a moved environment.
  • packages/ui/src/lib/usage.ts:527-528 still hard-codes the plural exhausted headline as “agents are out of quota.” Now that prepaid balances participate in metered and exhausted, two spent balance-only accounts (for example Amp and Cline) produce that sentence even though neither has a quota; a mixed exhausted balance/window set is also mislabeled. The new runway test pins only the single overdrawn-balance branch, so this plural path is untested.

Risks and missing coverage: please add the --data-dir + CLINE_DB_DATA_DIR precedence case, an Amp binding test that proves command discovery and execution use the row environment, and plural exhausted-balance/mixed-state runway cases. I am also unsure whether Amp supports more account-selection inputs beyond PATH/home; a two-profile real-CLI run would be needed to establish that, but the environment mismatch above exists independently in the code.

What I found sound: the round-3 helper is shared by identity and usage binding, and its absolute and cwd-relative --data-dir resolution is internally consistent; Cline settings and the default database stay together in those covered cases. I also read and found sound the Gemini and Qwen token arithmetic/deduplication, OpenCode and Cline session scanners (including avoiding Cline aggregate/subagent double counting), vendor-cost/list-price row separation and store migration, project-root memoization repair, read-only foreign-SQLite snapshot/immutable paths and race checks, host/context root plumbing, Gemini moved-home binding, single-account balance copy/rail rendering, and provenance-specific dashboard copy. The tests cover those named paths with focused fixtures, and the documentation matches the implementation except for the override combinations above.

Verdict: request changes

…give Amp the row's environment, name a plural spent group by what it ran out of

Codex GPT-5.6-Sol (low) reviewed round 3's fix and the full diff. Found three
real defects, all in code this PR added or changed.

- The round-3 Cline fix hard-coded `<override>/db/sessions.db` for a row
  carrying `--data-dir`, dropping `CLINE_DB_DATA_DIR`'s own say over the
  database alone — a rule `corpusRoot` already honours with no override. A
  row using `--data-dir` for the account and `CLINE_DB_DATA_DIR` for the
  database bound the meter correctly but scanned the wrong one. Fixed: the
  environment variable still wins for the database path even under an
  override; only the settings path is unconditionally the override's.
- `AmpMeter` was always built bare (`new AmpMeter()`), so it asked the host
  process's own `PATH`/`HOME` for `amp` and ran it there, ignoring the row's
  merged environment `localUsageFor` had already built. A row with its own
  Amp account, reached through a row-specific PATH, would report the host's
  account instead of its own. Fixed: the meter takes the row's environment,
  used both to locate the binary and to run it.
- The plural "N agents are out of quota." line said "quota" even when every
  exhausted account was a spent balance (Amp, Cline), which has none. Fixed:
  the plural line now says "credits" when every one is balance-only, "quota"
  when none is, and "credits or quota" for a mix — the same distinction the
  singular line already drew.

Each fix has a test that fails without it: a Cline row with both flags set,
proving the database follows the environment variable and the settings
follow the override; an Amp row with a fixture `amp` on a row-only PATH,
proving the row's environment reaches both the lookup and the run; three
runway cases (all-credits, all-quota, mixed) for the plural headline.

Per the review process, the owner approved exactly one round beyond the
project's normal 3-round limit for this PR, given round 3's fix was small.
Round 4 found real defects, so per the same "an issue stops the loop" rule
this is not a clean round either: I am not running a further round on my own
authority. `node script/verify.mjs` is green on this commit.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@iamenahs

Copy link
Copy Markdown
Member Author

Round 4 (one round beyond the project's normal 3-round limit, approved by the owner given round 3's fix was small) — Codex GPT-5.6-Sol (low) requested changes on 086058a6, reviewing round 3's fix and the full diff.

[P1] bootstrap.ts: the round-3 Cline fix dropped CLINE_DB_DATA_DIR's own precedence — fixed in db5ed07b.

  • Agreed. corpusRoot's Cline branch already gives CLINE_DB_DATA_DIR precedence over the data folder when there is no override; the round-3 branch hard-coded <override>/db/sessions.db instead, so a row using --data-dir for the account and CLINE_DB_DATA_DIR for the database bound the right account but scanned the wrong (usually stale or absent) database.
  • The database path now still checks the environment variable first even under an override; only the settings path is unconditionally the override's.
  • New test: a row with both --data-dir and CLINE_DB_DATA_DIR set proves settings follow the override and the database follows the environment variable. Fails without the fix.

[P1] bootstrap.ts/amp.ts: Amp was bound with no row environment — fixed in db5ed07b.

  • Agreed. localUsageFor had already built the row's merged environment for every other case, but AmpMeter was constructed bare, so it asked the host process's own PATH/HOME and would report the host's account, not a row-specific one.
  • AmpMeter now takes the row's environment and uses it both to locate amp and to run it.
  • New test: a row with a fixture amp on a row-only PATH proves the row's environment reaches both. Fails without the fix.

[P1] usage.ts: the plural exhausted headline called every group "quota" — fixed in db5ed07b.

  • Agreed. Two spent balance-only accounts (Amp, Cline) have no quota at all; the plural line said "quota" regardless.
  • It now says "credits" when every exhausted account is balance-only, "quota" when none is, and "credits or quota" for a mix — the distinction the singular line already drew.
  • New tests: all-credits, all-quota, and mixed cases. The all-credits and mixed cases fail without the fix.

node script/verify.mjs is green on db5ed07b. Per the process, an issue found in this extra round stops the loop the same way it would in a normal one — I'm not running a further round on my own authority. Reporting the full state to the owner.

@iamenahs

Copy link
Copy Markdown
Member Author

Review by Codex 0.155.0 · GPT-5.6-Sol · Low effort · via HarnessDesk

I found one remaining row-binding correctness defect.

  • [P1] packages/server/src/bootstrap.ts:512 — a row-specific HOME is discarded by the new local-usage paths. localUsageFor correctly merges agent.env over process.env, but records() calls corpusRoot(corpus, env) without its home argument, so packages/server/src/ledger/scan.ts:874 defaults that argument to the host process's homedir(). The same split exists in GeminiMeter (packages/server/src/usage/gemini.ts:122) and ClineMeter (packages/server/src/usage/cline.ts:188), and Cline's --data-dir ~/... resolver also expands against the host home (packages/server/src/installs/identity.ts:202). This is not how the row runs: ACP spawn overlays the row environment at packages/transport-acp/src/index.ts:937-940, and Node in that child resolves os.homedir() from the overlaid HOME (I confirmed with a focused HOME=/tmp/hd-row-home node -e ... probe). Therefore a Gemini or Cline row isolated with env: { HOME: '/accounts/two' } can run as account two while its quota, credentials, identity, and spend are read from the host account; Qwen and OpenCode spend have the same wrong fallback when their dedicated home variables are absent. Amp's round-4 fix does not have this gap: both its installer fallback and subprocess receive the merged row environment. Pass the effective row home into every fallback (including identity and ~ expansion), and add binding tests for HOME-only Gemini, Cline (plain and --data-dir ~/...), Qwen, and OpenCode rows.

Risks and missing coverage:

  • The new binding tests cover dedicated variables (GEMINI_CLI_HOME, CLINE_DB_DATA_DIR), absolute/relative Cline --data-dir, and Amp's row-only PATH, but none covers the ordinary HOME inherited by each CLI. That is why the shared fallback escaped the earlier rounds.
  • Cline's --data-dir parser is exercised for separate arguments but not an equals form. I did not find code or repository evidence that Cline accepts --data-dir=<dir>, so I am not reporting that as a defect; a real CLI invocation or upstream parser source would be needed to establish it.
  • Per the read-only instruction I did not build or run the test suites. GitHub reports Build/typecheck/test and both UI-system integration jobs green on exact head 9d9b826b; the unsigned package job is skipped.

What I checked and found sound: all three round-4 repairs are internally complete for their covered inputs. Cline keeps CLINE_DB_DATA_DIR ahead of the database derived from --data-dir while its settings remain under the override; Amp uses the merged row environment for both amp discovery and execution; and the plural exhausted headline selects credits, quota, or the mixed phrase from the group. I also re-read the full 3,097-line diff and every touched implementation, test, and documentation file. The foreign-SQLite immutable/snapshot consistency paths, vendored/unvendored row separation and schema migration, Gemini/Qwen token arithmetic and deduplication, OpenCode/Cline whole-file replacement, Cline's non-refreshing account API flow and usage rather than aggregateUsage, ledger provenance propagation, and the Dashboard's balance/provenance copy remain coherent with their tests. The two merges from main do not conflict with the usage work; the host.ts overlap is the separate update-advisory change.

Verdict: request changes

…spoke variables, moves every local usage path

Codex GPT-5.6-Sol (low) reviewed the three round-4 fixes and the full diff,
and found one more real defect, in the fallback beneath all of them.

`localUsageFor` merges a row's own environment over the desk's, and rounds
1, 3 and 4 each taught one path to follow a bespoke variable moved with it —
`GEMINI_CLI_HOME`, `--data-dir`, a row-only `PATH`. None of them followed the
plainest one: a row isolated with a bare `HOME`, ahead of any of the above.
`records()`'s call to `corpusRoot` dropped its `home` argument entirely,
falling back to the desk's own `homedir()`; `GeminiMeter` and Cline's
`clineDataDir` each had the same fallback internally; and `--data-dir ~/...`
expanded `~` against the desk's home too. A row running as a second account
through nothing but `HOME` had its Gemini or Cline sign-in, its Qwen and
OpenCode spend, all read from the desk's own account.

`localUsageFor` now computes the row's effective home once
(`env['HOME']?.trim() || homedir()`) and threads it through every one of
those: `corpusRoot`'s `home` parameter, a new `home` option on
`GeminiMeter`, Cline's internal fallback, and the `home` passed into
`clineDataDirOverride`.

New tests: one row isolated only by `HOME` proves Gemini, Cline (plain),
Qwen and OpenCode all move together; a `--data-dir ~/...` row under that
same `HOME` proves the `~` expansion follows it too; and `GeminiMeter`
constructed directly with only `env` (the path `localUsageFor` never
exercises, since it always supplies `home` explicitly) is pinned on its own.
Each of the five call sites fails under its own mutation with the fix
removed, and passes restored.

Per the review process, the owner approved this as a second round beyond
the normal 3-round limit, given round 4's three findings were fixes to the
same class of gap rather than new scope. Round 5 found one more in that same
class, so per the same rule this is not a clean round either: I am not
running a sixth round on my own authority. `node script/verify.mjs` is
green on this commit.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@iamenahs

Copy link
Copy Markdown
Member Author

Round 5 (second round beyond the project's normal 3-round limit, approved by the owner) — Codex GPT-5.6-Sol (low) requested changes on 9d9b826b, reviewing the round-4 fixes and the full diff, specifically asked to look for any other row-specific binding still falling back to the host process.

[P1] A row isolated with a bare HOME was read from the desk's own account everywhere — fixed in fa24ac81.

  • Agreed, and confirmed at every one of the four call sites named: records()'s call to corpusRoot dropped its home argument (defaulting to the desk's homedir()); GeminiMeter and Cline's internal clineDataDir each had the same bare fallback; and --data-dir ~/... expanded ~ against the desk's home. HOME is the ordinary way to isolate an agent — ahead of any of the bespoke variables rounds 1, 3 and 4 already fixed — so a row using only it had its Gemini/Cline sign-in and its Qwen/OpenCode spend read from whichever account the desk itself runs as.
  • localUsageFor now computes the row's effective home once and threads it through all four: corpusRoot's home parameter, a new home option on GeminiMeter, Cline's fallback, and clineDataDirOverride.
  • New tests: one HOME-only row proves Gemini, Cline, Qwen and OpenCode all move together, plus a --data-dir ~/... case under that same HOME; a GeminiMeter built directly with only env (the path localUsageFor never exercises, since it always supplies home explicitly) is pinned on its own, since that internal fallback would otherwise never be covered.
  • All five checks fail under their own mutation and pass restored.

node script/verify.mjs is green on fa24ac81. This is the second round in a row to find a real issue in the same area (row-specific binding), so per the process I'm not running a sixth round on my own authority — reporting the state to the owner, same as after round 4.

@iamenahs

Copy link
Copy Markdown
Member Author

Merging without a round 6, on the owner's explicit decision.

Five rounds, four of them (1, 3, 4, 5) found a real defect and one (2) approved outright. Every finding was fixed on the head that follows it, each with a test proven to fail under its own mutation:

Round Verdict Fixed in
1 request changes — 3 P1s (foreign-database ownership guess, priced/unpriced rows merged, moved Gemini home) 6206125a
2 approve —
3 request changes — Cline's --data-dir ignored 776a6995
4 request changes — CLINE_DB_DATA_DIR precedence dropped, Amp built with no row environment, plural spent-balance headline db5ed07b
5 request changes — a row's bare HOME bypassed all of the above fa24ac81

Rounds 3 through 5 all found the same class of gap — a row-specific override the binding forgot to thread through — each time in the fix the previous round produced or in a sibling path the reviewer went looking for after. node script/verify.mjs is green on fa24ac81, and CI is green on that exact commit.

Merging now on the strength of that evidence rather than a sixth round.

@iamenahs
iamenahs merged commit 898149f into main Sep 18, 2026
4 checks passed
@iamenahs
iamenahs deleted the claude/kind-morse-83c3b0 branch September 18, 2026 23:03
@iamenahs iamenahs added the screenshots Pull requests with screenshot attachments label Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: adapter-acp Where it lives bug Something isn't working screenshots Pull requests with screenshot attachments severity: medium Wrong behaviour in a narrower path

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant