Show what a chain run has spent, while it is still running - #206
Merged
Conversation
A run records its usage now, and nothing displayed it. The figure lived in `.openspec-ui/audit.jsonl` and in one line of the event log, so a person watching a five-stage chain could answer "did it finish?" and not "what has this cost me so far?". Attribution needed a new event, and this is the part that could not be worked around. A chain publishes every stage under one `runId` and named a stage only when it *ended* (`stageCompleted`/`checkpoint`), which fails in the two cases that matter: the first stage has no preceding boundary, so its usage — often the largest — had no stage to belong to; and a chain that stops mid-stage emits no boundary for it, so the stage that actually spent the money is exactly the one nothing named. `stageStarted` is emitted immediately before each stage begins, for every stage including the agent-less `archive` and `git` (`agentId: ""`, the convention `checkpoint.nextAgentId` already uses). It comes after every check that could refuse the stage, so a stage stopped at the budget ceiling is never announced as having started — announcing it would name a stage that spent nothing. Non-terminal, like `agentUpdate`/`cancelling`/`usageReported`. Two kinds of figure are shown, and kept apart. The recorded total is what agents reported for finished runs, and is what a ceiling is compared against; within one stage the last report wins and across stages they sum, matching what `agent-runner.ts` and `buildUsageReport` actually do, so the displayed total cannot exceed the one enforcement sees. A live figure is an ACP `usage_update` arriving during a run — until now rendered as `agent update: usage_update` and thrown away — shown as the agent's own running report; its `used` is context occupancy, falls after a compaction, and never enters a token total. Nothing here enforces anything. `HarnessChainRunner.checkBudget` remains the only thing that does, and a stage already running is never interrupted by it (ADR 0018 decision 7). The ceiling is displayed so it is legible, not applied twice with two different numbers. A stage whose agent reported nothing reads "not reported", never `$0.00`, and a run in which nothing reported says so outright — a panel that simply never moves is indistinguishable from a broken one, and `claude-cli` and the other raw-text CLIs report nothing at all. There is no "not reached" row: the chain never publishes its planned sequence, so a stage that has not started is not listed rather than listed as pending. Re-deriving the sequence in the view would be the chain's decision made twice, and would go wrong the moment the chain skipped a stage. `server.test.ts` asserted the chain's event sequence exactly and needed the new event added — caught by reading the whole failing-file list rather than stopping at the intermittent `git.push.test.ts`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What this adds
A chain run now shows what it has spent while it is still running: a
summary beside the event log, a row per stage that has started, tokens
and money, and the configured ceiling beside the recorded total.
Before this, the figure existed (#204 records it) and nothing displayed
it — it lived in
.openspec-ui/audit.jsonland in one line of the eventlog.
stageStarted, and why it had to existA chain publishes every stage under one
runIdand named a stage onlywhen it ended. That fails in the two cases that matter:
largest — had no stage to belong to;
the stage that actually spent the money is precisely the one nothing
ever named.
stageStarted { stage, agentId }is emitted immediately before each stagebegins, for every stage including the agent-less
archiveandgit(
agentId: "", the conventioncheckpoint.nextAgentIdalready uses).It comes after every check that could refuse the stage, so a stage
stopped at the budget ceiling is never announced as having started —
announcing it would name a stage that spent nothing. Non-terminal, like
agentUpdate/cancelling/usageReported.Two kinds of figure, kept apart
usageReported, once per finished runusage_update, repeatedly during a runused= context occupancy, falls after a compactionWithin one stage the last report wins and across stages they sum —
matching what
agent-runner.tsandbuildUsageReportactually do, so thedisplayed total cannot exceed the one enforcement sees. The live
usage_updatewas previously rendered asagent update: usage_updateand thrown away; it is what Claude and Copilot show live in their own
interfaces.
Nothing here enforces anything
HarnessChainRunner.checkBudgetremains the only thing that does, and astage already running is never interrupted by it (ADR 0018 decision 7).
The ceiling is displayed so it is legible — not applied twice with two
different numbers.
Never inventing a figure
$0.00.empty panel indistinguishable from a broken one —
claude-cliand theother raw-text CLIs report nothing at all.
a rate.
$0.0031does not readas
$0.00.Deliberately not built
There is no "not reached" row. The chain never publishes its planned
sequence, so a stage that has not started is not listed rather than listed
as pending. Re-deriving the sequence in the view would be the chain's
decision made twice, and would go wrong the moment the chain skipped a
stage.
tasks.md3.2 was corrected to say this rather than claim a statethe implementation does not have.
Tests
The accumulator is a pure exported function, tested without rendering:
attribution across stages, usage before any stage, last-wins-within/
sum-across, two currencies, an unreported stage asserted explicitly as
not zero, and
usednever entering a token total. The panel tests coverthe mid-stage failure case that no stage boundary could ever have covered.
server.test.tsasserted the chain's event sequence exactly and neededthe new event added — found by reading the whole failing-file list rather
than stopping at the intermittent
git.push.test.ts.Still to verify by hand
tasks.md5.4: run a chain onclaude-cli-acpand confirm the summaryfills in per stage as the chain advances, and that a stage whose agent
reported nothing says so.
🤖 Generated with Claude Code