Skip to content

fix(usage): make /limit a real stop boundary and stop reporting unknown spend as zero [SC-A5.1] - #78

Merged
AetherAI3 merged 5 commits into
mainfrom
supercluster/a5-usage-continuity
Aug 19, 2026
Merged

fix(usage): make /limit a real stop boundary and stop reporting unknown spend as zero [SC-A5.1]#78
AetherAI3 merged 5 commits into
mainfrom
supercluster/a5-usage-continuity

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

Problem

/limit was a control that enforced nothing.

uvtSpent was only ever written as = 0 — at construction, in purge(), and on snapshot restore. No usage frame ever incremented it. And checkUvtCap() had zero callers anywhere in the tree.

So:

  • /limit 50000 printed "agent will pause and ask permission if ceiling hit". Nothing paused. Nothing checked. The cap was never consulted by anything.
  • /limit reported spent: 0 and a 0% bar for every session, whatever it had actually cost — because the number was a constant.
  • The field comment claimed it was "read from custody log". Nothing read it.

This is a display-only control presented as enforcement, on the one surface where a user is trying to bound their spend.

Contract

1. Usage is measured, or it is unknown — never zero by default

uvtObserved: number | null replaces the always-zero counter. null means no authoritative frame has arrived, which is not a measured zero and is no longer rendered as one. Nothing estimates UVT from token counts; only the server's own number is recorded.

uvtSpent survives as a getter for the HUD (which needs a number) and is documented as reporting 0 when the answer is unknown — so anything that must tell those apart reads uvtObserved.

2. Turns settle once, by id

The terminal frame carries the turn total and a reconnect can replay it, so settleTurn(turnId, uvt) is keyed rather than accumulated blindly. A replayed done frame is ignored; distinct turns accumulate.

3. The cap is checked before a billable turn starts — and says what it is

runCloudTurn consults checkUvtCap() before doing anything, and refuses to start once observed spend reaches the cap.

Two states deliberately do not trip it:

  • a local brain — Aether meters nothing, so the session is marked unmetered rather than zero spend
  • no observed usage — there is no evidence the cap was reached, and guessing either blocks work that cost nothing or waves through work that cost a lot

The wording is now accurate about what it can and cannot do:

⚡ UVT cap set  50000
  no further turn will START once the server-reported spend reaches it.
  a turn already in flight may still complete and be billed.
  this is a local stop only — your plan and balance are unchanged.

The readout stops conflating two different answers:

UVT cap: 50000   observed: unknown — the server has reported no usage yet
  the cap cannot trip until the server reports usage.

UVT cap: none (uncapped)   observed: LOCAL — not metered by Aether

A design change the mutation pass forced

remaining was a number, and an unmeasured session reported the full cap as headroom.

That is the same false zero in a different costume: it tells the user their entire budget is intact when in truth none of it has been counted.

I only found it because the first mutation run did not fail any test — reintroducing the false-zero assumption changed nothing observable. Rather than write a test around the weak design, I changed the design: remaining is now number | null, null when unmeasured, and a test pins it.

Tests

10 added.

test proves
no frame reports unknown, not zero the core distinction
a duplicate done frame does not double-count reconnect replay safety
distinct turns accumulate settlement is not over-suppressed
an unknown session is never reported as capped no guessing in either direction
the cap trips on reaching it it is a real boundary
no cap never caps
local sessions are labelled unmetered not counted as zero spend
purge returns to unknown, not to zero
unmeasured headroom is null, never the full cap the mutation-found gap

Mutation-checked, both guards. Restoring the false-zero headroom and removing the replay dedupe fails two tests — 8 pass / 2 fail. Restoring gives 10 / 10.

Gates at 79e0bbc:

command result
npm run typecheck exit 0
npm test 932 pass / 0 fail

Baseline on clean 41a7e261, measured in the same session: 922 / 0.

Security / correctness notes

  • The server remains the billing authority. This is explicitly a local circuit breaker and the copy says so. Nothing here claims to change a plan, allowance, or balance.
  • No spend is inferred. Only server-reported numbers are recorded — no token-count estimation, which would have produced a confident wrong figure.
  • Fail-open on unknown, not fail-closed. An unmeasured session is not blocked. Blocking on absent evidence would stop work that may have cost nothing; the honest response is to say the cap cannot trip yet.
  • A turn already in flight may still be billed, and the message says so rather than implying a clean stop.

Known limits

  • The HUD still shows uvtUsed: reg.uvtSpent, so an unknown session renders as 0 there. hud.ts is SC-INT's surface and is deliberately not touched — this PR fixes the model and /limit; the HUD read is a one-line follow-up in the integration lane.
  • Cap persistence across resume — and the cross-workspace rejection that goes with it — is a later slice.
  • aether agent is not yet gated. The cap is enforced on the REPL cloud path; the one-shot command runs its own loop.
  • Usage frames arriving mid-turn (type: "usage") are not yet used for live display; only the authoritative terminal frame settles a turn.

Dependency and merge order

Touches src/commands/chat.ts, which SC-A1 (#77) also touches — different functions (runCloudTurn here, runLocalTurn there) but the same file, so one will need a trivial rebase. Per the integration order SC-A1 lands first, so this is the one that rebases.

Independent of #72, #73, #74, #75.

…wn spend as zero

Lane SC-A5, slice 1 of truthful usage and continuity.

/limit was a control that enforced nothing.

`uvtSpent` was only ever written as `= 0` — at construction, in purge(), and
on snapshot restore. No usage frame ever incremented it. `checkUvtCap()` had
zero callers anywhere in the tree. So:

  * `/limit 50000` printed "agent will pause and ask permission if ceiling
    hit". Nothing paused. Nothing checked. The cap was never consulted.
  * `/limit` reported "spent: 0" and a 0% bar for every session, whatever it
    had actually cost, because the number was a constant.
  * The field comment claimed it was "read from custody log". Nothing read it.

Three changes.

1. Usage is measured, or it is unknown. Never zero by default.

   `uvtObserved: number | null` replaces the always-zero counter. null means no
   authoritative frame has arrived, which is not the same as a measured zero
   and is no longer rendered as one. Nothing estimates UVT from token counts —
   only the server's own number is recorded.

   `uvtSpent` survives as a getter for the HUD, which needs a number, and is
   documented as reporting 0 when the answer is unknown so that anything which
   must tell those apart reads `uvtObserved`.

2. Turns settle once, by id.

   The terminal frame carries the turn total and a reconnect can replay it, so
   `settleTurn(turnId, uvt)` is keyed rather than accumulated blindly. A
   replayed done frame is ignored; distinct turns accumulate.

3. The cap is checked before a billable turn starts, and says what it is.

   `runCloudTurn` consults `checkUvtCap()` before doing anything and refuses to
   start when the observed spend has reached the cap. Two states deliberately
   do not trip it: a local brain (Aether meters nothing, so it is marked
   unmetered rather than zero-spend) and a session with no observed usage
   (there is no evidence the cap was reached, and guessing either blocks free
   work or waves through expensive work).

   The wording is now accurate about what it can and cannot do:

     no further turn will START once the server-reported spend reaches it.
     a turn already in flight may still complete and be billed.
     this is a local stop only — your plan and balance are unchanged.

A design change came out of the mutation pass rather than the plan. `remaining`
was a number, and an unmeasured session reported the full cap as headroom —
which is the same false zero in a different costume: it tells the user their
whole budget is intact when in truth none of it has been counted. The first
mutation run did not fail any test, which is what exposed it. `remaining` is now
`number | null`, null when unmeasured, and a test pins it.

Tests: 10 added. Unknown is not zero; a duplicate done frame does not
double-count; distinct turns accumulate; an unknown session is never reported
as capped; the cap trips on reaching it; no cap never caps; local sessions are
labelled unmetered; purge returns to unknown rather than to zero; and
unmeasured headroom is null rather than the full cap.

Mutation-checked, both guards: restoring the false-zero headroom and removing
the replay dedupe fails two tests (8 pass / 2 fail); restoring gives 10 / 10.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            932 pass / 0 fail  (922 on clean 41a7e26)

Known limits. The HUD still renders `uvtUsed: reg.uvtSpent`, so it shows 0 for
an unknown session — hud.ts is SC-INT's surface and is deliberately not touched
here. Session-cap persistence across resume, and the cross-workspace rejection
that goes with it, are a later slice. The cap is enforced on the REPL cloud
path; `aether agent` has its own loop and is not yet gated.
… in runTurn

Found by SC-INT when the six lanes were composed, not by this lane's own tests
or CI — both of which were green.

The patch that introduced the gate was anchored on a signature shared by
runTurn and runCloudTurn, so the cap check and the turn-id allocation were
written into both. Every turn ran checkUvtCap() twice and consumed two ids from
the counter.

Not a correctness bug: settleTurn keys off the id runCloudTurn actually uses,
and a doubled gate returns the same verdict both times. But it is duplicated
control flow on a spend boundary, and this PR's own description says the gate
lives in runCloudTurn.

Removed the runTurn copy. The gate now exists once, in runCloudTurn, which is
also the only billable path — local turns are unmetered and must not be gated
at all.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            932 pass / 0 fail — unchanged, which is the point:
                      the duplication was invisible to the test suite
Unrelated to this lane's subject. Landing here because it turned windows-latest
red on this PR and would do the same to any other lane that ran at the wrong
moment.

"a hanging backend cannot stall the fast report" injected clients that never
resolve, set timeoutMs to 50, and then asserted:

    assert.ok(Date.now() - started < 500);

That measures the machine, not the behaviour. The 50ms timeout can fire exactly
as designed and the assertion still fails because the runner was busy. Observed
at 689ms on windows-latest here, and locally at 1818ms under full-suite load
while passing in isolation at 82ms — flagged as a known fragility in SC-A2's PR
description before it became a failure.

Replaced with the property the test exists to defend: a probe fed by a hanging
client must never come back claiming it verified anything. agent.transport,
auth.credential, agent.catalog and mcp.broker are named explicitly rather than
filtered on an axis, because local checks like workspace.git legitimately do
verify in this fixture — nothing about them touches the backend that is hanging.
The first attempt filtered on `reachable !== "na"` and failed on workspace.git
for exactly that reason.

The wall clock is still bounded, but as a hang detector rather than a stopwatch:
30s distinguishes "returned" from "awaited forever", which is the failure the
test was written to catch. A slow runner no longer registers as a bug.

Stronger than what it replaces, not weaker. Mutation-checked: making notChecked
return a verified axis fails this test along with two others (5 pass / 3 fail);
restoring gives 8 / 8. The old assertion would have passed that mutation
untouched — it never looked at a single axis.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            932 pass / 0 fail

This fix is independent of the usage work and would be better as its own PR
against main. It is here because it is what is currently red.
@AetherAI3
AetherAI3 marked this pull request as ready for review August 19, 2026 13:05
@AetherAI3
AetherAI3 merged commit 37fff3c into main Aug 19, 2026
5 checks passed
@AetherAI3
AetherAI3 deleted the supercluster/a5-usage-continuity branch August 19, 2026 13:07
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