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:
1 parent
d0560b6b6b
commit
185c7ffced
3 files changed
+26
No files matched your search
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
/**
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user