Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md
#	.agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md
#	THIRD_PARTY_NOTICES.md
#	apps/cli/composition.md
#	apps/cli/config/base.cordis.yml
#	apps/cli/package.json
#	apps/cli/src/app-cli-entry.ts
#	apps/cli/src/bin.ts
#	apps/cli/tests/args.spec.ts
#	apps/web/tests/built-boot.snapshot.ts
#	apps/web/tests/navigation-panes.e2e.ts
#	docs/architecture.i18n.yaml
#	docs/architecture.md
#	docs/architecture.zh.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/README.i18n.yaml
#	packages/bundle/README.i18n.yaml
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/http-bridge.ts
#	packages/client/connection/src/index.ts
#	packages/client/connection/tests/fixture.spec.ts
#	packages/client/connection/tests/node-half.spec.ts
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/README.md
#	packages/client/runtime/README.zh.md
#	packages/client/runtime/src/client/contract/session.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/MessageItem.module.css
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/src/client/input/contract.ts
#	packages/client/ui-conversation/src/client/input/facade.ts
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/src/client/locales.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/client/ui-conversation/tests/apply-inject.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/input-matrix.spec.tsx
#	packages/client/ui-conversation/tests/input-scenarios.spec.tsx
#	packages/client/ui-conversation/tests/service-orchestration.spec.ts
#	packages/client/ui-conversation/tests/skeleton.spec.tsx
#	packages/client/ui-trajectory/tests/views.spec.tsx
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/rpc.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/host/apiproxy/tests/rpc-schemas.spec.ts
#	packages/llm/llm-pi-ai/README.i18n.yaml
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/README.zh.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm/README.i18n.yaml
#	packages/ui/tui/README.md
#	packages/ui/tui/README.zh.md
#	packages/ui/tui/src/components/content.ts
#	packages/ui/tui/src/components/transcript.ts
#	packages/ui/tui/tests/tui.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 01:56:29 +08:00
3973 changed files with 144864 additions and 60887 deletions

View File

@@ -0,0 +1,309 @@
// contextBreakdown projection: heuristic system/tools/message composition,
// plus the shared estimator's pricing branches.
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import type { ContextBreakdownProjection } from '@deepseek-ai/dsh-token-meter/client'
import { contextBreakdownProjectionDefinition } from '../src/breakdown-projection.ts'
import {
estimateContent,
estimateHeader,
estimateMessage,
estimateSystemTokens,
estimateToolsTokens,
} from '../src/estimate.ts'
const CONFIG = { provider: 'test', model: 'test-model' }
const TOOLS: ToolSchema[] = [{
name: 'bash',
description: 'run a command',
parameters: { type: 'object', properties: {} },
}]
async function harness(): Promise<{ ctx: Context; session: Session }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(TokenMeterService)
return { ctx, session: ctx.sessions.create() }
}
const projected = (ctx: Context, session: Session): ContextBreakdownProjection => {
const value = ctx.sessionProjections.snapshot(session).values.contextBreakdown
if (value === undefined) throw new Error('contextBreakdown projection is not registered')
return value
}
function appendUser(session: Session, text: string): number {
return session.append('user/message', createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'user' },
}), { surfaceOp: 'append' }).seq
}
/**
* Meter one upcoming replacement the way compact-basic does: price the
* replaced span from the measurement service's own nodes and log the
* shadow-price event directly before the replace.
*/
function appendSummaryMeter(ctx: Context, session: Session, start: number, end: number): void {
const nodes = ctx.tokenMeter.measure(session).nodes
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const shadowed = nodes.slice(startIdx, endIdx + 1)
session.append('compact/summary', {
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start, end },
shadowedSeqs: shadowed.map(node => node.seq),
shadowedTokenCount: shadowed.reduce((total, node) => total + node.tokens, 0),
provider: 'mock',
model: 'mock',
})
}
describe('contextBreakdown session projection', () => {
it('serves zeros for an empty log', async () => {
const { ctx, session } = await harness()
expect(projected(ctx, session)).toEqual({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 })
})
it('prices the newest envelope last-wins and pushes no change for a restated one', async () => {
const { ctx, session } = await harness()
session.append('request/header', {
header: { config: CONFIG, system: 'You are terse.', tools: TOOLS },
reason: 'initial',
})
expect(projected(ctx, session)).toEqual({
systemTokens: estimateSystemTokens({ config: CONFIG, system: 'You are terse.' }),
toolsTokens: estimateToolsTokens({ config: CONFIG, tools: TOOLS }),
messageTokens: 0,
})
const changed: string[] = []
ctx.sessionProjections.onChanged((_session, key) => { changed.push(key) })
session.append('request/header', {
header: { config: CONFIG, system: 'You are terse.', tools: TOOLS },
reason: 'change',
})
session.append('todo/write', { todos: [] })
expect(changed).not.toContain('contextBreakdown')
// A system-less, tool-less envelope prices back to zero.
session.append('request/header', { header: { config: CONFIG }, reason: 'change' })
expect(projected(ctx, session)).toEqual({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 })
})
it('sums surface appends and skips an empty-content assistant message', async () => {
const { ctx, session } = await harness()
appendUser(session, 'abcd')
session.append('step/start', { turn: 1, step: 1 })
session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [],
source: { kind: 'model', provider: 'mock', model: 'mock' },
}),
usage: { inputTokens: 9, outputTokens: 0 },
}, { surfaceOp: 'append', sourceEventSeqs: [] })
session.append('step/end', { turn: 1, step: 1 })
// 'abcd' prices to 9 (1 text + 4 block + 4 role); the usage-only assistant
// message derives to no transcript entry and adds nothing.
expect(projected(ctx, session).messageTokens).toBe(9)
})
it('shrinks the message figure when a metered replacement compacts the surface', async () => {
const { ctx, session } = await harness()
const first = appendUser(session, 'before compaction, a longer message')
const second = appendUser(session, 'and a second entry')
const summary = createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
})
appendSummaryMeter(ctx, session, first, second)
session.append('user/message', summary, {
surfaceOp: { op: 'replace', start: first, end: second },
sourceEventSeqs: [first, second],
})
expect(projected(ctx, session).messageTokens).toBe(estimateMessage(summary))
})
it('keeps the message figure equal to the service surface across appends and a compaction', async () => {
const { ctx, session } = await harness()
// The panel's composition rows and `measure()` answer the same question in
// the same vocabulary; one shared fold is what makes that true.
const agree = (): number => {
const messageTokens = projected(ctx, session).messageTokens
expect(messageTokens).toBe(ctx.tokenMeter.measure(session).surfaceTokens)
return messageTokens
}
session.append('request/header', {
header: { config: CONFIG, system: 'You are terse.', tools: TOOLS },
reason: 'initial',
})
expect(agree()).toBe(0)
const question = appendUser(session, 'a first question, long enough to price above zero')
session.append('step/start', { turn: 1, step: 1 })
const answer = session.append('assistant/message', {
turn: 1,
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'a considered answer' }],
source: { kind: 'model', provider: 'mock', model: 'mock' },
}),
usage: { inputTokens: 40, outputTokens: 7 },
}, { surfaceOp: 'append', sourceEventSeqs: [] }).seq
session.append('step/end', { turn: 1, step: 1 })
const grown = agree()
expect(grown).toBeGreaterThan(0)
appendSummaryMeter(ctx, session, question, answer)
// The armed shadow price must not move the published figure by itself.
expect(agree()).toBe(grown)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
}), {
surfaceOp: { op: 'replace', start: question, end: answer },
sourceEventSeqs: [question, answer],
})
expect(agree()).toBeLessThan(grown)
})
it('folds a replacement without a claim at zero and fails on a mismatched claim', () => {
const definition = contextBreakdownProjectionDefinition
const replace = (start: number, end: number): SessionEvent => ({
type: 'user/message',
seq: 9,
time: 0,
data: createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
surfaceOp: { op: 'replace', start, end },
sourceEventSeqs: [start, end],
} as unknown as SessionEvent)
const append = (seq: number): SessionEvent => ({
type: 'user/message',
seq,
time: 0,
data: createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
surfaceOp: 'append',
} as unknown as SessionEvent)
const meter = (start: number, end: number, seq: number): SessionEvent => ({
type: 'compact/prune',
seq,
time: 0,
data: { shadowedRange: { start, end }, shadowedSeqs: [start, end], shadowedTokenCount: 5 },
} as unknown as SessionEvent)
let state = definition.init()
state = definition.apply(state, append(1))
state = definition.apply(state, append(3))
// No metering event: the replacement contributes zero instead of throwing.
expect(definition.view(definition.apply(state, replace(1, 3))).messageTokens)
.toBe(definition.view(state).messageTokens)
// An adjacent claim for another range contradicts the replacement.
const mismatched = definition.apply(state, meter(1, 1, 8))
expect(() => definition.apply(mismatched, replace(1, 3))).toThrow('no adjacent shadow price')
// A claim expires after one intervening event, so replacement delta is zero.
let expired = definition.apply(state, meter(1, 3, 8))
expired = definition.apply(expired, { type: 'todo/write', seq: 9, time: 0, data: { todos: [] } } as unknown as SessionEvent)
expect(definition.view(definition.apply(expired, replace(1, 3))).messageTokens)
.toBe(definition.view(state).messageTokens)
// The armed claim prices exactly the next event's matching replacement.
const armed = definition.apply(state, meter(1, 3, 8))
expect(definition.view(definition.apply(armed, replace(1, 3))).messageTokens)
.toBe(definition.view(state).messageTokens - 5 + estimateMessage(
createUserMessage({ content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }),
))
})
it('keeps the persisted checkpoint O(1) as the surface grows and compacts', async () => {
const { ctx, session } = await harness()
const first = appendUser(session, 'the first of many messages')
for (let index = 0; index < 24; index += 1) appendUser(session, `message number ${index} with some text`)
const last = appendUser(session, 'the last message before compaction')
const stateKeys = (): string[] => {
const row = ctx.sessionProjections.checkpoint(session)['contextBreakdown']
if (row === undefined) throw new Error('contextBreakdown checkpoint row is missing')
return Object.keys(row.val as Record<string, unknown>).sort()
}
// Growth adds no per-node bookkeeping to the durable state.
expect(stateKeys()).toEqual(['messageTokens', 'systemTokens', 'toolsTokens'])
const shadowed = session.surface.nodes.slice(
session.surface.nodes.indexOf(first),
session.surface.nodes.indexOf(last) + 1,
)
appendSummaryMeter(ctx, session, first, last)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
}), {
surfaceOp: { op: 'replace', start: first, end: last },
sourceEventSeqs: [...shadowed],
})
expect(stateKeys()).toEqual(['messageTokens', 'systemTokens', 'toolsTokens'])
expect(projected(ctx, session).messageTokens)
.toBe(ctx.tokenMeter.measure(session).surfaceTokens)
})
it('restores from a JSON checkpoint and unregisters with the token-meter fiber', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
const meterFiber = await ctx.plugin(TokenMeterService)
const session = ctx.sessions.create()
session.append('request/header', {
header: { config: CONFIG, system: 'You are terse.' },
reason: 'initial',
})
appendUser(session, 'abcd')
const checkpoint = JSON.parse(JSON.stringify(
ctx.sessionProjections.checkpoint(session),
)) as ReturnType<typeof ctx.sessionProjections.checkpoint>
await meterFiber.dispose()
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextBreakdown')
await ctx.plugin(TokenMeterService)
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextBreakdown).toEqual({
systemTokens: estimateSystemTokens({ config: CONFIG, system: 'You are terse.' }),
toolsTokens: 0,
messageTokens: 9,
})
})
})
describe('shared estimator', () => {
it('prices every content-block shape under the fixed heuristic', () => {
expect(estimateContent([{ type: 'text', text: 'abcd' }])).toBe(5)
expect(estimateContent([{ type: 'reasoning', text: 'abcdefgh' }] as ContentBlock[])).toBe(6)
expect(estimateContent([{ type: 'tool-call', id: 'c' as never, name: 'bash', arguments: '{"a":1}' }])).toBe(7)
expect(estimateContent([{
type: 'tool-result', toolCallId: 'c' as never,
content: [{ type: 'text', text: 'abcd' }],
}])).toBe(9)
const unknown = { type: 'mystery', payload: 'abc' } as unknown as ContentBlock
expect(estimateContent([unknown])).toBe(4 + Math.ceil(JSON.stringify(unknown).length / 4))
})
it('prices envelope parts independently and absent parts to zero', () => {
expect(estimateSystemTokens(undefined)).toBe(0)
expect(estimateSystemTokens({ config: CONFIG })).toBe(0)
expect(estimateSystemTokens({ config: CONFIG, system: 'abcdefgh' })).toBe(6)
expect(estimateToolsTokens(undefined)).toBe(0)
expect(estimateToolsTokens({ config: CONFIG, tools: [] })).toBe(0)
expect(estimateToolsTokens({ config: CONFIG, tools: TOOLS }))
.toBe(Math.ceil(JSON.stringify(TOOLS).length / 4) + 4)
expect(estimateHeader(undefined)).toBe(0)
expect(estimateHeader({ config: CONFIG, system: 'abcdefgh', tools: TOOLS }))
.toBe(6 + Math.ceil(JSON.stringify(TOOLS).length / 4) + 4)
})
})

View File

@@ -147,7 +147,7 @@ describe('TokenMeterService pricing', () => {
it('returns a detached deeply immutable empty measurement', () => {
const service = meter()
const session = new Session(SessionId('empty'))
const session = Session.create(SessionId('empty'))
const result = service.measure(session)
expect(result).toEqual({
logRevision: 0,
@@ -168,7 +168,7 @@ describe('TokenMeterService pricing', () => {
it('keeps an earlier unified snapshot detached from later replay', () => {
const service = meter()
const session = new Session(SessionId('detached'))
const session = Session.create(SessionId('detached'))
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'first' }],
source: { kind: 'user' },
@@ -200,7 +200,7 @@ describe('TokenMeterService pricing', () => {
it('prices header, tools, and surface when no reusable usage exists', () => {
const service = meter()
const session = new Session(SessionId('heuristic'))
const session = Session.create(SessionId('heuristic'))
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'question' }],
source: { kind: 'user' },
@@ -218,7 +218,7 @@ describe('TokenMeterService pricing', () => {
it('keeps request-header overrides out of the returned surface', () => {
const service = meter()
const session = new Session(SessionId('override-surface'))
const session = Session.create(SessionId('override-surface'))
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'question' }],
source: { kind: 'user' },
@@ -246,7 +246,7 @@ describe('replay anchors and surface folds', () => {
it('uses disjoint provider usage and signed durable-output rewrites', () => {
const service = meter()
const session = new Session(SessionId('usage'))
const session = Session.create(SessionId('usage'))
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'before' }],
source: { kind: 'user' },
@@ -267,7 +267,7 @@ describe('replay anchors and surface folds', () => {
it('selects a heuristic anchor when provider usage would undercut its scale', () => {
const service = meter()
const session = new Session(SessionId('low-usage-anchor'))
const session = Session.create(SessionId('low-usage-anchor'))
const system = 'system context'
const requestHeader = header('deepseek-v4-flash', { system })
appendSuccessfulCall(session, requestHeader, {
@@ -297,7 +297,7 @@ describe('replay anchors and surface folds', () => {
it('uses an estimated anchor when provider usage is absent', () => {
const service = meter()
const session = new Session(SessionId('missing-usage'))
const session = Session.create(SessionId('missing-usage'))
appendSuccessfulCall(session, header('deepseek-v4-flash', { system: 's' }), {
providerText: 'provider',
durableText: 'rewritten',
@@ -314,8 +314,8 @@ describe('replay anchors and surface folds', () => {
})
it('distinguishes explicit empty provenance from absent legacy provenance', () => {
const explicit = new Session(SessionId('explicit-empty'))
const legacy = new Session(SessionId('legacy-absent'))
const explicit = Session.create(SessionId('explicit-empty'))
const legacy = Session.create(SessionId('legacy-absent'))
appendSuccessfulCall(explicit, header('deepseek-v4-flash'), {
durableText: 'listener injected text',
providerText: '',
@@ -335,7 +335,7 @@ describe('replay anchors and surface folds', () => {
it('keeps only the latest successful request anchor across model switches', () => {
const service = meter()
const session = new Session(SessionId('switch'))
const session = Session.create(SessionId('switch'))
const alphaHeader = header('alpha', { system: 'same envelope' })
appendSuccessfulCall(session, alphaHeader, { usage: USAGE, providerText: 'alpha' })
expect(service.measure(session).baseline).toMatchObject({ kind: 'usage', tokens: 34 })
@@ -356,7 +356,7 @@ describe('replay anchors and surface folds', () => {
it('invalidates usage for any canonical envelope change or explicit override', () => {
const service = meter()
const session = new Session(SessionId('envelope'))
const session = Session.create(SessionId('envelope'))
const anchoredHeader = header('deepseek-v4-flash', { system: 'one' })
appendSuccessfulCall(session, anchoredHeader, { usage: USAGE })
expect(service.measure(session, { ...anchoredHeader, tools: [] }).baseline.kind).toBe('usage')
@@ -375,7 +375,7 @@ describe('replay anchors and surface folds', () => {
})
it('folds the latest full header snapshot into the effective envelope', () => {
const session = new Session(SessionId('header-snapshot'))
const session = Session.create(SessionId('header-snapshot'))
appendHeader(session, header('deepseek-v4-flash'))
session.append('request/header', {
header: header('deepseek-v4-pro'),
@@ -388,7 +388,7 @@ describe('replay anchors and surface folds', () => {
it('replays seeded append and replace operations with signed deltas', () => {
const service = meter()
const original = new Session(SessionId('surface-original'))
const original = Session.create(SessionId('surface-original'))
appendSuccessfulCall(original, header('deepseek-v4-flash'), {
usage: USAGE,
providerText: 'long provider answer '.repeat(100),
@@ -397,7 +397,7 @@ describe('replay anchors and surface folds', () => {
content: [{ type: 'text', text: 'new tail' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
const seeded = new Session(SessionId('surface-seeded'), original.events)
const seeded = Session.create(SessionId('surface-seeded'), original.events)
const before = service.measure(seeded)
expect(before.nodes).toHaveLength(2)
expect(before.surfaceDeltaTokens).toBeGreaterThan(0)
@@ -423,7 +423,7 @@ describe('replay anchors and surface folds', () => {
})
it('prices an empty assistant surface anchor as zero', () => {
const session = new Session(SessionId('empty-assistant'))
const session = Session.create(SessionId('empty-assistant'))
appendSuccessfulCall(session, header('deepseek-v4-flash'), {
providerText: '',
durableText: '',
@@ -444,7 +444,7 @@ describe('malformed replay and listener lifecycle', () => {
}
it('rejects an assistant without its step boundary transactionally', () => {
const session = new Session(SessionId('bad-step'))
const session = Session.create(SessionId('bad-step'))
appendHeader(session, header('deepseek-v4-flash'))
session.append('assistant/message', {
turn: 1,
@@ -462,7 +462,7 @@ describe('malformed replay and listener lifecycle', () => {
})
it('clears completed step boundaries and rejects overlapping or late step events', () => {
const overlapping = new Session(SessionId('overlapping-step'))
const overlapping = Session.create(SessionId('overlapping-step'))
overlapping.append('step/start', { turn: 1, step: 1 })
overlapping.append('step/start', { turn: 1, step: 2 })
expectRepeatedFailure(
@@ -471,7 +471,7 @@ describe('malformed replay and listener lifecycle', () => {
/arrived before turn 1\/step 1 ended/,
)
const late = new Session(SessionId('late-assistant'))
const late = Session.create(SessionId('late-assistant'))
late.append('step/start', { turn: 1, step: 1 })
appendHeader(late, header('deepseek-v4-flash'))
late.append('step/end', { turn: 1, step: 1 })
@@ -493,7 +493,7 @@ describe('malformed replay and listener lifecycle', () => {
/no matching step\/start/,
)
const mismatchedEnd = new Session(SessionId('mismatched-end'))
const mismatchedEnd = Session.create(SessionId('mismatched-end'))
mismatchedEnd.append('step/start', { turn: 1, step: 1 })
mismatchedEnd.append('step/end', { turn: 1, step: 2 })
expectRepeatedFailure(
@@ -532,7 +532,7 @@ describe('malformed replay and listener lifecycle', () => {
},
]
for (const testCase of cases) {
const session = new Session(SessionId(`bad-source-${testCase.name}`))
const session = Session.create(SessionId(`bad-source-${testCase.name}`))
session.append('step/start', { turn: 1, step: 1 })
appendHeader(session, header('deepseek-v4-flash'))
const sourceEventSeqs = testCase.appendSource(session)
@@ -554,7 +554,7 @@ describe('malformed replay and listener lifecycle', () => {
})
it('rejects repeated and non-earlier assistant provenance', () => {
const duplicate = new Session(SessionId('duplicate-source'))
const duplicate = Session.create(SessionId('duplicate-source'))
duplicate.append('step/start', { turn: 1, step: 1 })
appendHeader(duplicate, header('deepseek-v4-flash'))
const source = duplicate.append('assistant/chunk', {
@@ -584,7 +584,7 @@ describe('malformed replay and listener lifecycle', () => {
})
expect(() => meter().measure(duplicate)).toThrow(/repeats source seq/)
const future = new Session(SessionId('future-source'))
const future = Session.create(SessionId('future-source'))
future.append('step/start', { turn: 1, step: 1 })
appendHeader(future, header('deepseek-v4-flash'))
appendUnchecked(future, {
@@ -611,7 +611,7 @@ describe('malformed replay and listener lifecycle', () => {
})
it('does not partially apply a malformed assistant replacement', () => {
const session = new Session(SessionId('transactional-replace'))
const session = Session.create(SessionId('transactional-replace'))
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'head' }],
source: { kind: 'user' },
@@ -638,7 +638,7 @@ describe('malformed replay and listener lifecycle', () => {
})
it('rejects corrupt replacement ranges without advancing the replay cursor', () => {
const session = new Session(SessionId('bad-replace'))
const session = Session.create(SessionId('bad-replace'))
const head = session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'head' }],
source: { kind: 'user' },
@@ -671,7 +671,7 @@ describe('malformed replay and listener lifecycle', () => {
type: 'turn/start',
seq: 0,
time: 1,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
data: { turn: 1 },
}] })
activeMeter.measure(session)
session.append('user/message', createUserMessage({

View File

@@ -70,6 +70,26 @@ const projected = (ctx: Context, session: Session): TokenUsageProjection => {
return value
}
/**
* Meter one upcoming replacement the way compact-basic does: price the
* replaced span from the measurement service's own nodes and log the
* shadow-price event directly before the replace.
*/
function appendSummaryMeter(ctx: Context, session: Session, start: number, end: number): void {
const nodes = ctx.tokenMeter.measure(session).nodes
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const shadowed = nodes.slice(startIdx, endIdx + 1)
session.append('compact/summary', {
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start, end },
shadowedSeqs: shadowed.map(node => node.seq),
shadowedTokenCount: shadowed.reduce((total, node) => total + node.tokens, 0),
provider: 'mock',
model: 'mock',
})
}
describe('tokenUsage session projection', () => {
it('serves zero buckets for an empty log', async () => {
const { ctx, session } = await harness()
@@ -184,6 +204,7 @@ describe('tokenUsage session projection', () => {
content: [{ type: 'text', text: 'before compaction' }],
source: { kind: 'user' },
}), { surfaceOp: 'append' })
appendSummaryMeter(ctx, session, before.seq, before.seq)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted' }],
source: { kind: 'plugin', plugin: 'test' },
@@ -235,6 +256,34 @@ function recordContext(session: Session, model: string, contextWindow?: number):
})
}
/** Append one model-visible user turn and return its surface seq. */
function appendUser(session: Session, text: string): number {
return session.append('user/message', createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'user' },
}), { surfaceOp: 'append' }).seq
}
/** Append one finalized assistant turn carrying its provider usage. */
function appendAssistant(
session: Session,
text: string,
usage: TokenUsage,
turn: number,
step: number,
): number {
return session.append('assistant/message', {
turn,
step,
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text }],
source: { kind: 'model', provider: 'mock', model: 'mock' },
}),
usage,
}, { surfaceOp: 'append', sourceEventSeqs: [] }).seq
}
describe('contextPressure session projection', () => {
it('serves no pressure or capacity for an empty log', async () => {
const { ctx, session } = await harness()
@@ -277,9 +326,13 @@ describe('contextPressure session projection', () => {
startStep(session, 1, 1)
recordContext(session, 'small', 64_000)
usageChunk(session, { inputTokens: 100, outputTokens: 10 }, 1, 1)
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100, contextWindow: 64_000 })
expect(pressure(ctx, session)).toEqual({
pressureTokens: 100, projectedTokens: 100, contextWindow: 64_000,
})
recordContext(session, 'large', 256_000)
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100, contextWindow: 256_000 })
expect(pressure(ctx, session)).toEqual({
pressureTokens: 100, projectedTokens: 100, contextWindow: 256_000,
})
})
it('removes an older capacity when the newest route advertises none', async () => {
@@ -288,7 +341,7 @@ describe('contextPressure session projection', () => {
recordContext(session, 'small', 64_000)
usageChunk(session, { inputTokens: 100, outputTokens: 10 }, 1, 1)
recordContext(session, 'unknown')
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100 })
expect(pressure(ctx, session)).toEqual({ pressureTokens: 100, projectedTokens: 100 })
})
it('pushes no change for unrelated events or a restated capacity', async () => {
@@ -319,7 +372,7 @@ describe('contextPressure session projection', () => {
const checkpoint = JSON.parse(JSON.stringify(
ctx.sessionProjections.checkpoint(session),
)) as ReturnType<typeof ctx.sessionProjections.checkpoint>
expect(checkpoint.contextPressure?.ver).toBe(2)
expect(checkpoint.contextPressure?.ver).toBe(4)
await meterFiber.dispose()
expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('contextPressure')
@@ -327,7 +380,81 @@ describe('contextPressure session projection', () => {
await ctx.plugin(TokenMeterService)
expect(ctx.sessionProjections.viewCheckpoint(checkpoint).contextPressure).toEqual({
pressureTokens: 42,
projectedTokens: 42,
contextWindow: 64_000,
})
})
it('carries the sample forward over surface growth and a compaction', async () => {
const { ctx, session } = await harness()
recordContext(session, 'large', 128_000)
const question = appendUser(session, 'a first question worth a few tokens')
startStep(session, 1, 1)
// The provider prices the prompt its request actually carried; the sample
// must anchor against the surface as of that request, not after the
// assistant message joins it.
const answer = appendAssistant(session, 'an answer of some length', { inputTokens: 900, outputTokens: 20 }, 1, 1)
session.append('step/end', { turn: 1, step: 1 })
const afterTurn = pressure(ctx, session)
expect(afterTurn.pressureTokens).toBe(900)
// The assistant message landed after the sample, so it already shows.
expect(afterTurn.projectedTokens).toBeGreaterThan(900)
const grown = appendUser(session, 'a follow-up question that grows the surface further')
const beforeCompaction = pressure(ctx, session).projectedTokens
expect(beforeCompaction).toBeGreaterThan(afterTurn.projectedTokens!)
// Compaction reports no usage of its own, so `pressureTokens` cannot move;
// the projected figure must shrink anyway — the defect this field fixes.
appendSummaryMeter(ctx, session, question, grown)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'test' },
}), {
surfaceOp: { op: 'replace', start: question, end: grown },
sourceEventSeqs: [question, answer, grown],
})
const compacted = pressure(ctx, session)
expect(compacted.pressureTokens).toBe(900)
expect(compacted.projectedTokens).toBeLessThan(beforeCompaction!)
})
it('folds a replacement without a claim at zero', async () => {
const { ctx, session } = await harness()
const question = appendUser(session, 'a question from an unmetered log')
startStep(session, 1, 1)
usageChunk(session, { inputTokens: 100, outputTokens: 1 }, 1, 1)
session.append('step/end', { turn: 1, step: 1 })
const before = pressure(ctx, session)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'summary without a preceding claim' }],
source: { kind: 'plugin', plugin: 'test' },
}), {
surfaceOp: { op: 'replace', start: question, end: question },
sourceEventSeqs: [question],
})
expect(pressure(ctx, session)).toEqual(before)
})
it('clamps a projection that heuristic error drove below zero', async () => {
const { ctx, session } = await harness()
recordContext(session, 'large', 128_000)
const question = appendUser(session, 'a question long enough to outprice the sample'.repeat(4))
startStep(session, 1, 1)
// A provider sample far below the heuristic price of what it replaced:
// shadowing that span subtracts more than the sample holds.
appendAssistant(session, 'ok', { inputTokens: 3, outputTokens: 1 }, 1, 1)
session.append('step/end', { turn: 1, step: 1 })
appendSummaryMeter(ctx, session, question, question)
session.append('user/message', createUserMessage({
content: [{ type: 'text', text: '.' }],
source: { kind: 'plugin', plugin: 'test' },
}), {
surfaceOp: { op: 'replace', start: question, end: question },
sourceEventSeqs: [question],
})
expect(pressure(ctx, session).projectedTokens).toBe(0)
})
})