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.
Symptom
/api/componentsreports 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:527computes the per-component act flag from the saving alone: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
Actedexactly like the paid extraction that derived those bytes.components.Report.Replaysis available onrat that line and is not read.Everything downstream inherits it.
dash/query.go:776-782:so
acted,acted_tokens,act_rateandact_rate_structuralall 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: 239besidereapplied_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: 239was 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_rateis worse thanactedhere, because a component replaying on every turn of a long session approachesact_rate: 1.0— maximally busy-looking while spending nothing.What a fix looks like
/statswas fixed in #178, which addedcomponents.Report.Replays(set throughReport.Replay(name)) and split the rollup intoacted_fresh/acted_replay, partitioningactedby whether the saving cost anything:The dashboard needs the same distinction carried through
dash/event.gointo theComponentRow, the query rollup and the/api/componentspayload. The signal is already on the Report, so this is plumbing rather than new mechanism —actedshould keep its name and value, with the split added beside it, exactly as/statsdid.Note
act_rate_structuralis derived asmutated − 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 amaindefect 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.