Skip to content

fix(dash): /api/components counts free frozen replays as acts, so act_rate reads as work the component paid for #179

Description

@amiddavid

Symptom

/api/components reports free frozen replays as acts, so the dashboard shows the same conflation that #176 reports for /stats — on the surface an operator actually looks at.

dash/event.go:527 computes the per-component act flag from the saving alone:

row.Mutated = !r.Reverted && !r.Skipped
row.Acted = row.Mutated && row.SavedGross > 0

A same-session replay splices bytes this component froze on an earlier turn: it saves tokens, it makes no model call, and it costs nothing. It therefore sets Acted exactly like the paid extraction that derived those bytes. components.Report.Replays is available on r at that line and is not read.

Everything downstream inherits it. dash/query.go:776-782:

c.ActedTokens = c.Acted
if c.ActedStructural = c.Mutated - c.Acted; c.ActedStructural < 0 { … }
c.ActRate = float64(c.Acted) / float64(c.Runs)

so acted, acted_tokens, act_rate and act_rate_structural all describe "runs that saved tokens" while reading as "runs that did work".

Why it matters

This is the number an operator uses to decide whether an expensive component is still spending. On the measured run behind #176 (iteration 023, arm B) the shape was acted: 239 beside reapplied_same_session: 2,291, on a component whose own debug record showed zero surviving candidates on all 374 requests — i.e. not one fresh model call. acted: 239 was read as 239 paid extractions. The correct reading was "this component made no calls at all and is purely amortizing work done earlier", and those two readings have opposite consequences: turn it off against leave it on.

act_rate is worse than acted here, because a component replaying on every turn of a long session approaches act_rate: 1.0 — maximally busy-looking while spending nothing.

What a fix looks like

/stats was fixed in #178, which added components.Report.Replays (set through Report.Replay(name)) and split the rollup into acted_fresh / acted_replay, partitioning acted by whether the saving cost anything:

// metrics.compStat.act
cs.Acted++
if len(r.Calls) == 0 && r.Replays > 0 {
    cs.ActedReplay++
    return
}
cs.ActedFresh++

The dashboard needs the same distinction carried through dash/event.go into the ComponentRow, the query rollup and the /api/components payload. The signal is already on the Report, so this is plumbing rather than new mechanism — acted should keep its name and value, with the split added beside it, exactly as /stats did.

Note act_rate_structural is derived as mutated − acted, so whatever shape the split takes, the derivation wants re-checking against it rather than being left to compose by accident.

Provenance

Pre-existing on main; not introduced by #178. Raised in review of #178 and filed separately per the standing rule that a main defect unrelated to the change under test gets its own issue and its own PR, so it can ship without waiting. Referenced from #178's known-gaps list.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions