Skip to content

Show what a chain run has spent, while it is still running - #206

Merged
VeryComplexAndLongName merged 1 commit into
mainfrom
feat/usage-visible-while-running
Sep 3, 2026
Merged

Show what a chain run has spent, while it is still running#206
VeryComplexAndLongName merged 1 commit into
mainfrom
feat/usage-visible-while-running

Conversation

@VeryComplexAndLongName

Copy link
Copy Markdown
Owner

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.jsonl and in one line of the event
log.

stageStarted, and why it had to exist

A chain publishes every stage under one runId and named a stage only
when it ended. That 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;
  • a chain that stops mid-stage emits no boundary for that stage, so
    the stage that actually spent the money is precisely the one nothing
    ever named.

stageStarted { stage, agentId } 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, kept apart

Recorded total Live figure
Source usageReported, once per finished run ACP usage_update, repeatedly during a run
Tokens consumed by the run used = context occupancy, falls after a compaction
Counts toward the ceiling yes no
Shown as the run's total the agent's own running report

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. The live
usage_update was previously rendered as agent update: usage_update
and thrown away; it is what Claude and Copilot show live in their own
interfaces.

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.

Never inventing a figure

  • A stage whose agent reported nothing reads "not reported", never
    $0.00.
  • A run in which nothing reported says so outright, rather than showing an
    empty panel indistinguishable from a broken one — claude-cli and the
    other raw-text CLIs report nothing at all.
  • Two currencies are shown as two figures; converting would mean inventing
    a rate.
  • Below a cent, cost renders to four decimals, so $0.0031 does not read
    as $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.md 3.2 was corrected to say this rather than claim a state
the 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 used never entering a token total. The panel tests cover
the mid-stage failure case that no stage boundary could ever have covered.

server.test.ts asserted the chain's event sequence exactly and needed
the 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.md 5.4: run a chain on claude-cli-acp and confirm the summary
fills in per stage as the chain advances, and that a stage whose agent
reported nothing says so.

🤖 Generated with Claude Code

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>
@VeryComplexAndLongName
VeryComplexAndLongName merged commit ae78a82 into main Sep 3, 2026
7 checks passed
@VeryComplexAndLongName
VeryComplexAndLongName deleted the feat/usage-visible-while-running branch September 3, 2026 13:09
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