refactor(token-meter): fold the surface once for the meter and the projection

`contextBreakdown.messageTokens` and `measure().surfaceTokens` answer the same
question in the same heuristic vocabulary, and the panel's composition rows are
only honest while they agree. Each owner carried its own copy of the positional
fold — same pricing, same `{seq, tokens}` node list, same replace-range lookup
and guard, differing only in mutable versus immutable application — so an edit
to either one would have moved the panel away from `measure()` with both sides
still green. The duplication gate caught the shared 62 tokens.

`src/surface-fold.ts` now owns `foldSurfaceTokens`: total, allocation-fresh,
returning the event's price, the next surface, and the signed total delta. The
service assigns that result where it used to prepare a commit closure, which
keeps its validate-before-mutate replay transaction intact — the fold throws
before any state is touched, so a malformed event still fails identically on
every retry. `_prepareSurfaceMutation` and `_estimateSurfaceEvent` go away with
it, and the projection's apply drops to one call.

Covers the identity with a session that appends and then compacts, asserting
the projection figure equals the service surface at each boundary; the test
fails when either side of the fold is perturbed.
This commit is contained in:
Yichen Jiang
2026-08-05 16:29:51 +08:00
parent 46562b2c30
commit e62cbe12e4
12 changed files with 138 additions and 100 deletions
@@ -6,23 +6,20 @@
*/
import { z } from 'zod'
import { canonicalHeader, deriveEventMessage, isSurfaceEvent } from '@deepseek-ai/dsh-session'
import { canonicalHeader, isSurfaceEvent } from '@deepseek-ai/dsh-session'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
import { estimateMessage, estimateSystemTokens, estimateToolsTokens } from './estimate.ts'
import type { TokenSurfaceNode } from './types.ts'
import { estimateSystemTokens, estimateToolsTokens } from './estimate.ts'
import { foldSurfaceTokens } from './surface-fold.ts'
// Import for the `contextBreakdown` SessionProjectionMap key merge.
import type {} from './projection.ts'
/** One priced surface node (plain JSON for the persisted projection cache). */
interface BreakdownSurfaceNode {
seq: number
tokens: number
}
interface ContextBreakdownState {
systemTokens: number
toolsTokens: number
messageTokens: number
surface: BreakdownSurfaceNode[]
/** Priced surface nodes (plain JSON for the persisted projection cache). */
surface: TokenSurfaceNode[]
}
const breakdownSchema = z.object({
@@ -35,10 +32,9 @@ const breakdownSchema = z.object({
* Token-meter's context-composition projection unit.
*
* Envelope figures are last-wins per `request/header`; the message figure
* folds surface appends and positional replacements, so compaction shrinks it
* the same way it shrinks the next request. Committed logs are
* surface-validated at append time, so an unresolvable replace range here is
* log corruption and fails loud rather than skipping the event.
* rides {@link foldSurfaceTokens} — the same fold the measurement service
* replays — so it equals `measure().surfaceTokens` at every event boundary and
* compaction shrinks it the way it shrinks the next request.
*/
export const contextBreakdownProjectionDefinition:
ProjectionDefinition<'contextBreakdown', ContextBreakdownState> = {
@@ -54,32 +50,11 @@ ProjectionDefinition<'contextBreakdown', ContextBreakdownState> = {
return { ...state, systemTokens, toolsTokens }
}
if (!isSurfaceEvent(event)) return state
const message = deriveEventMessage(event)
const tokens = message === null ? 0 : estimateMessage(message)
const op = event.surfaceOp
if (op === 'append') {
return {
...state,
messageTokens: state.messageTokens + tokens,
surface: [...state.surface, { seq: event.seq, tokens }],
}
}
const startIdx = state.surface.findIndex(node => node.seq === op.start)
const endIdx = state.surface.findIndex(node => node.seq === op.end)
if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
throw new Error(
`context breakdown: replace at seq ${event.seq} has invalid current range ${op.start}-${op.end}`,
)
}
const removed = state.surface
.slice(startIdx, endIdx + 1)
.reduce((total, node) => total + node.tokens, 0)
const surface = [...state.surface]
surface.splice(startIdx, endIdx - startIdx + 1, { seq: event.seq, tokens })
const fold = foldSurfaceTokens(state.surface, event)
return {
...state,
messageTokens: state.messageTokens + tokens - removed,
surface,
messageTokens: state.messageTokens + fold.deltaTokens,
surface: fold.nodes,
}
},
view: ({ systemTokens, toolsTokens, messageTokens }) => ({ systemTokens, toolsTokens, messageTokens }),
+7 -51
View File
@@ -8,7 +8,7 @@ import { Context, Service } from 'cordis'
import z from 'schemastery'
import { BlockAssembler, deepFreeze } from '@deepseek-ai/dsh-llm'
import type { Message, TokenUsage } from '@deepseek-ai/dsh-llm'
import type { EpochHeader, Session, SessionEvent, SurfaceEvent } from '@deepseek-ai/dsh-session'
import type { EpochHeader, Session, SessionEvent } from '@deepseek-ai/dsh-session'
import { canonicalHeader, headerEquals, isSurfaceEvent } from '@deepseek-ai/dsh-session'
// Type-only: resolves the optional projection registry Context seam.
import type {} from '@deepseek-ai/dsh-session-projection'
@@ -21,6 +21,7 @@ import type {
import { contextBreakdownProjectionDefinition } from './breakdown-projection.ts'
import { contextPressureProjectionDefinition, tokenUsageProjectionDefinition } from './usage-projection.ts'
import { estimateContent, estimateHeader, estimateMessage, ROLE_OVERHEAD } from './estimate.ts'
import { foldSurfaceTokens } from './surface-fold.ts'
export type * from './types.ts'
@@ -39,11 +40,6 @@ interface ReplayState {
anchor: MeasurementAnchor | undefined
}
interface PreparedSurfaceMutation {
readonly tokens: number
commit(state: ReplayState): void
}
/** Sum disjoint provider usage buckets without double-counting reasoning output. */
function usageTokens(usage: TokenUsage): number {
return usage.inputTokens
@@ -219,7 +215,7 @@ export class TokenMeterService extends Service {
}
const surface = isSurfaceEvent(event)
? this._prepareSurfaceMutation(session, state, event)
? foldSurfaceTokens(state.surface, event)
: undefined
if (event.type === 'assistant/message') {
@@ -266,53 +262,13 @@ export class TokenMeterService extends Service {
state.header = nextHeader
state.stepStart = nextStepStart
if (surface !== undefined) surface.commit(state)
if (surface !== undefined) {
state.surface = surface.nodes
state.surfaceTokens += surface.deltaTokens
}
state.anchor = nextAnchor
}
/** Validate one surface operation and return its allocation-light commit. */
private _prepareSurfaceMutation(
session: Session,
state: ReplayState,
event: SurfaceEvent,
): PreparedSurfaceMutation {
const tokens = this._estimateSurfaceEvent(session, event)
const op = event.surfaceOp
if (op === 'append') {
return {
tokens,
commit(target) {
target.surface.push({ seq: event.seq, tokens })
target.surfaceTokens += tokens
},
}
}
const startIdx = state.surface.findIndex(node => node.seq === op.start)
const endIdx = state.surface.findIndex(node => node.seq === op.end)
if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
throw new Error(
`token meter: replace at seq ${event.seq} has invalid current range ${op.start}-${op.end}`,
)
}
const removedTokens = state.surface
.slice(startIdx, endIdx + 1)
.reduce((total, node) => total + node.tokens, 0)
return {
tokens,
commit(target) {
target.surface.splice(startIdx, endIdx - startIdx + 1, { seq: event.seq, tokens })
target.surfaceTokens += tokens - removedTokens
},
}
}
/** Price one current surface event exactly as it projects to a request. */
private _estimateSurfaceEvent(session: Session, event: SurfaceEvent): number {
const message = session.deriveEventMessage(event)
return message === null ? 0 : this.estimateMessage(message)
}
/**
* Reassemble provider output from exact chunk provenance for a usage anchor.
* Missing legacy provenance conservatively treats the durable output as the
+6 -3
View File
@@ -17,9 +17,12 @@ export const inject = ['invariants']
/**
* No runtime invariant: token estimates are per-call outputs and the private
* session cache is invalidated at its event mutation boundary. The package's
* projection does expose an observation stream, but its schema fixes the JSON
* payload and its pure fold replaces same-step samples; totals need not be
* monotone when a final usage sample corrects an earlier chunk.
* three projections do expose observation streams, but their schemas fix the
* JSON payloads; the usage folds replace same-step samples, so totals need not
* be monotone when a final sample corrects an earlier chunk, and the
* composition fold shares `surface-fold.ts` with the measurement service,
* which makes its message figure equal `measure().surfaceTokens` by
* construction rather than by a relation worth observing at runtime.
*/
const install: InvariantInstaller = () => {}
@@ -0,0 +1,63 @@
/**
* The one positional surface fold, shared by the measurement service's replay
* state and the pure `contextBreakdown` projection. Both answer "what does the
* current model-visible conversation cost", so they MUST price and place every
* node identically: a private copy in either owner would let the panel's
* message figure drift away from `measure().surfaceTokens` with both sides
* still passing their own tests.
*
* @module @deepseek-ai/dsh-token-meter/surface-fold
*/
import { deriveEventMessage } from '@deepseek-ai/dsh-session'
import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
import type { TokenSurfaceNode } from './types.ts'
import { estimateMessage } from './estimate.ts'
/** One surface event's placement and cost against the surface preceding it. */
export interface SurfaceTokenFold {
/** Heuristic price of the event's own message; 0 when it derives none. */
readonly tokens: number
/** The surface after the event, detached from the input. */
readonly nodes: TokenSurfaceNode[]
/** Signed change in the surface total: `tokens` minus anything shadowed. */
readonly deltaTokens: number
}
/**
* Fold one surface event onto a priced surface.
*
* Total and allocation-fresh: the caller assigns the result rather than
* mutating in place, so a throw here leaves the caller's state untouched and
* the same malformed event fails identically on every retry.
* @param nodes - the priced surface preceding this event, in model-visible order.
* @param event - the surface event to place.
* @returns the event's price, the next surface, and the signed total delta.
* @throws when a replacement names a range absent from `nodes` — committed
* logs are surface-validated at append time, so an unresolvable range is log
* corruption and must fail loud rather than skip the event.
*/
export function foldSurfaceTokens(
nodes: readonly TokenSurfaceNode[],
event: SurfaceEvent,
): SurfaceTokenFold {
const message = deriveEventMessage(event)
const tokens = message === null ? 0 : estimateMessage(message)
const op = event.surfaceOp
if (op === 'append') {
return { tokens, nodes: [...nodes, { seq: event.seq, tokens }], deltaTokens: tokens }
}
const startIdx = nodes.findIndex(node => node.seq === op.start)
const endIdx = nodes.findIndex(node => node.seq === op.end)
if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
throw new Error(
`token surface: replace at seq ${event.seq} has invalid current range ${op.start}-${op.end}`,
)
}
const removed = nodes
.slice(startIdx, endIdx + 1)
.reduce((total, node) => total + node.tokens, 0)
const next = [...nodes]
next.splice(startIdx, endIdx - startIdx + 1, { seq: event.seq, tokens })
return { tokens, nodes: next, deltaTokens: tokens - removed }
}