fix(token-meter): bound projection state via logged shadow prices

The contextBreakdown and contextPressure units carried the full priced
surface, so each session's persisted projection checkpoint grew without
bound. A surface replacement is now priced by the shadow-price event
logged directly before it — compact/summary for compaction, the new
compact/prune from tool-result pruning (priced through the injected
token meter) — and the unit states shrink to a fixed handful of numbers.
Regenerate the persistence/cordis/module/config catalogs.
This commit is contained in:
imccyu
2026-08-06 02:50:38 +08:00
parent 6400e521ba
commit 59df683ef1
21 changed files with 403 additions and 81 deletions

View File

@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
let root: string | undefined
@@ -23,6 +24,7 @@ describe('compact-tool-result-prune real Loader composition', () => {
root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
"- name: '@deepseek-ai/dsh-token-meter'",
"- name: '@deepseek-ai/dsh-compact-tool-result-prune'",
' config:',
' thresholdChars: 100',
@@ -38,10 +40,9 @@ describe('compact-tool-result-prune real Loader composition', () => {
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (specifier !== '@deepseek-ai/dsh-compact-tool-result-prune') {
throw new Error(`unexpected Loader import: ${specifier}`)
}
return ToolResultPruneService
if (specifier === '@deepseek-ai/dsh-token-meter') return TokenMeterService
if (specifier === '@deepseek-ai/dsh-compact-tool-result-prune') return ToolResultPruneService
throw new Error(`unexpected Loader import: ${specifier}`)
},
} as unknown as NonNullable<typeof context.loader.internal>
await context.loader.create({
@@ -60,6 +61,9 @@ describe('compact-tool-result-prune real Loader composition', () => {
it('rejects stale config after plugin schema normalization', async () => {
context = new Context()
// Satisfy the declared injection first: config normalization runs in the
// service constructor, which a pending fiber never reaches.
await context.plugin(TokenMeterService)
await expect(context.plugin(ToolResultPruneService, {
maxChars: 100,
} as never)).rejects.toThrow(/unknown key "maxChars"/)

View File

@@ -9,6 +9,7 @@ import SessionStore, {
import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import ToolResultPruneService, {
codePointLength,
DEFAULTS,
@@ -25,9 +26,16 @@ const SMALL: ToolResultPruneConfig = {
}
function service(config: ToolResultPruneConfig = SMALL): ToolResultPruneService {
return new ToolResultPruneService(new Context(), config)
const ctx = new Context()
// Service constructors self-register, so `ctx.tokenMeter` resolves for the
// shadow-price pricing without a full plugin boot.
void new TokenMeterService(ctx)
return new ToolResultPruneService(ctx, config)
}
/** Pricing oracle mirroring the service's estimator for expectations. */
const METER = new TokenMeterService(new Context())
function appendToolStep(
session: Session,
turn: number,
@@ -203,6 +211,18 @@ describe('ToolResultPruneService session transaction', () => {
sourceEventSeqs: [originalSeq],
})
expect(session.surface.nodes).not.toContain(originalSeq)
// Shadow-price protocol: the metering event sits directly before the
// replacement and prices the shadowed node with the shared estimator.
if (original.type !== 'tool/result') throw new Error('original is not a tool/result')
expect(session.events[entry.replacementSeq - 1]).toMatchObject({
type: 'compact/prune',
data: {
shadowedRange: { start: originalSeq, end: originalSeq },
shadowedSeqs: [originalSeq],
shadowedTokenCount: METER.estimateMessage(original.data.message),
},
})
})
it('prunes multiple results, skips short ones, and converges in one pass', () => {
@@ -240,6 +260,7 @@ describe('ToolResultPruneService session transaction', () => {
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(SessionInvariant)
await ctx.plugin(TokenMeterService)
const prune = new ToolResultPruneService(ctx, SMALL)
const session = ctx.sessions.create(SessionId('invariants'))
appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }])