feat(host): show net freed tokens on compaction and prune rows

The row carried the producer's gross shadow price, while the trend chart
visualizes the net drop. The shadow-price protocol guarantees the
replacement folds in synchronously after the metering event, so rewrite
the event's tokens to removed-minus-replacement at that moment; a
metering event left without a replacement keeps the gross price.
This commit is contained in:
Bowen Liang committed 2026-08-24 03:08:59 +08:00
1 parent d0560b6b6b
commit 185c7ffced
3 files changed
+26

No files matched your search

+20
View File
@@ -123,6 +123,14 @@ export interface TimelineState {
* to keep the state plain JSON for the projection cache.
*/
pendingShadowedSeqs?: number[]
/**
* The seq of the compaction/prune event that armed `pendingShadowedSeqs` —
* the shadowed path rewrites that event's `tokens` from the gross shadow
* price to the NET freed amount (removed nodes minus the synchronous
* replacement), so the row matches the drop the trend chart shows. Same
* arm/remove lifecycle as `pendingShadowedSeqs`.
*/
pendingShadowEventSeq?: number
}
/** Keep only the trailing `maxTurns` turn-runs of a request timeline. */
@@ -341,7 +349,9 @@ function applySurface(
// persisted-state precondition (see TimelineState) and would fail the
// whole session's projection-cache write.
const shadowedSeqs = st.pendingShadowedSeqs
const shadowEventSeq = st.pendingShadowEventSeq
delete st.pendingShadowedSeqs
delete st.pendingShadowEventSeq
const op = ev.surfaceOp as { op?: string; start?: number; end?: number } | null | undefined
if (op !== null && typeof op === 'object' && op.op === 'replace') {
@@ -362,6 +372,15 @@ function applySurface(
st.surface = kept
st.sums[cat] += node.tokens
st.surface.push(node)
// Rewrite the metering event's row from its gross shadow price to the
// NET freed amount (the replacement re-adds its own tokens), so the
// number matches the drop the trend chart shows. The record is cloned:
// the events array's elements are shared with the persisted state.
if (shadowEventSeq !== undefined) {
const removedSum = removed.reduce((sum, n) => sum + n.tokens, 0)
const i = st.events.findIndex(e => e.seq === shadowEventSeq)
if (i >= 0) st.events[i] = { ...st.events[i], tokens: Math.max(0, removedSum - node.tokens) }
}
return node
}
let si = -1
@@ -635,6 +654,7 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds
// event synchronously shadows exactly these node seqs.
if (data && Array.isArray(data.shadowedSeqs)) {
s.pendingShadowedSeqs = data.shadowedSeqs.filter((x): x is number => typeof x === 'number')
s.pendingShadowEventSeq = event.seq
}
s.events.push({
seq: event.seq, time: event.time, kind: event.type === 'compaction/summary' ? 'compaction' : 'prune',
+1
View File
@@ -147,6 +147,7 @@ const timelineStateSchema = z.object({
archiveFloor: z.number().optional(),
callNames: z.record(z.string(), z.string()),
pendingShadowedSeqs: z.array(z.number()).optional(),
pendingShadowEventSeq: z.number().optional(),
}) as unknown as z.ZodType<TimelineState>
/**
+5
View File
@@ -163,6 +163,7 @@ test('host half: projection unit, fold semantics, attribution, retention, determ
assert.equal(compactEv.step, undefined, 'no step for a trailing event')
assert.equal(compactEv.fromTurn, 1, 'trailing event still knows the request before it')
assert.equal(compactEv.fromStep, 1, 'trailing event keeps its from-step')
assert.equal(compactEv.tokens, 5000, 'a metering event with no synchronous replacement keeps the gross shadow price')
// -- determinism + reference-stability of the projection contract --
// Unrelated events must return the SAME state reference (Object.is gates the
@@ -236,6 +237,10 @@ test('host half: projection unit, fold semantics, attribution, retention, determ
assert.equal(shadow.current.tool, 13, 'shadowed seqs beyond the range end are removed too (only the replacement survives)')
assert.equal(shadow.current.user, 18, 'unrelated nodes keep their price (40 chars: 10 + 4 + 4)')
assert.equal(shadow.current.total, 18 + 13, 'total reflects the seq-based removal')
// the prune row reads as the NET freed amount — what the trend chart's bar
// drop shows — not the gross shadow price: 64 shadowed - 13 replacement
const pruneEv = shadow.events.find(e => e.kind === 'prune')
assert.equal(pruneEv.tokens, 51, 'prune event shows net freed (removed nodes minus the synchronous replacement)')
// no tool/call events in this log, so no call name is registered — the tool
// label stays empty (never a crash)
assert.equal(shadow.nodes.find(n => n.seq === 5).tool, undefined, 'no registered call name -> no tool label')