refactor(token-meter): make context occupancy durable projection state
Replace the transient `session/model-request` mux frame with ordinary durable session state. Occupancy now rides two last-wins projection fields instead of a non-replayable frame that needed removal tombstones and cross-stream fencing. The frame was the only non-replayable class on the mux stream. Because host and mux are independent SSE streams with no cross-stream order, a request emitted before a removal could arrive after `host/session-removed`, and a legitimate request for a new lifecycle reusing the same id could be fenced by a late removal. Fixing that needed a lifecycle generation on every frame; the frame itself was the problem. Removed: the `session/model-request` frame and schema, the `agent/model-request` core event, the ApiProxy measurement point, the client-side telemetry map and removal tombstone, and the synthetic `cancelled` open error used to signal reconnect through the error channel. Added: `request/context`, a log-only session event recording the registration-bound capacity of the route a request resolved to, appended beside `request/header` from the lookup that already prepared the call and skipped when the route is unchanged. Capacity stays out of `EpochHeader` because it is adapter metadata about a route, not an input the request was built from, so it must not join request reconstruction or header equality. The `contextPressure` projection pairs the newest provider-reported prompt size with the newest recorded capacity. The two are deliberately not one atomic request observation: switching models can pair a fresh capacity with the prior route's pressure until the next request reports usage. The figure is a user-facing reference, and this matches how the TUI status line has always computed occupancy.
This commit is contained in:
@@ -1,124 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import type { MuxFrame, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import { createApiProxy } from '../src/api-proxy.ts'
|
||||
|
||||
async function nextFrame<K extends MuxFrame['type']>(
|
||||
iterator: AsyncIterator<RpcRequest<MuxFrame>>,
|
||||
type: K,
|
||||
): Promise<Extract<MuxFrame, { type: K }>> {
|
||||
for (;;) {
|
||||
const next = await iterator.next()
|
||||
if (next.done) throw new Error(`mux ended before ${type}`)
|
||||
if (next.value.payload.type === type) {
|
||||
return next.value.payload as Extract<MuxFrame, { type: K }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('ApiProxy model-request telemetry', () => {
|
||||
it('atomically measures the observed request, forwards only live, and degrades per field', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const session = ctx.sessions.create(SessionId('model-request-telemetry'))
|
||||
const agent = {
|
||||
id: session.id,
|
||||
session,
|
||||
status: 'running',
|
||||
ctx,
|
||||
} as Agent
|
||||
ctx.agents.register(agent)
|
||||
const measure = vi.fn(() => ({ totalTokens: 321 }))
|
||||
const removeTokenMeter = ctx.provide('tokenMeter', { measure })
|
||||
const api = createApiProxy(ctx, {
|
||||
provider: 'test',
|
||||
model: 'alpha',
|
||||
cwd: '/tmp',
|
||||
workspaceRoot: '/tmp',
|
||||
})
|
||||
|
||||
const primaryAbort = new AbortController()
|
||||
const primary = api.events.mux(
|
||||
{ rpcId: RpcId('primary'), payload: {} },
|
||||
primaryAbort.signal,
|
||||
)[Symbol.asyncIterator]()
|
||||
expect((await nextFrame(primary, 'session/subscribed')).sessionId).toBe(session.id)
|
||||
|
||||
agentEvents(ctx, agent).emit('agent/model-request', 1, 2, {
|
||||
provider: 'test',
|
||||
model: 'alpha',
|
||||
contextWindow: 128_000,
|
||||
})
|
||||
expect(measure).toHaveBeenCalledWith(session)
|
||||
expect(await nextFrame(primary, 'session/model-request')).toEqual({
|
||||
type: 'session/model-request',
|
||||
sessionId: session.id,
|
||||
turn: 1,
|
||||
step: 2,
|
||||
provider: 'test',
|
||||
model: 'alpha',
|
||||
contextTokens: 321,
|
||||
contextWindow: 128_000,
|
||||
})
|
||||
|
||||
const history = await api.sessions.history({
|
||||
rpcId: RpcId('history'),
|
||||
payload: { sessionId: session.id },
|
||||
})
|
||||
if (!history.result.ok) throw new Error('history failed')
|
||||
expect(history.result.value).not.toHaveProperty('metrics')
|
||||
expect(history.result.value).not.toHaveProperty('modelRequest')
|
||||
|
||||
const reconnectAbort = new AbortController()
|
||||
const reconnect = api.events.mux(
|
||||
{ rpcId: RpcId('reconnect'), payload: {} },
|
||||
reconnectAbort.signal,
|
||||
)[Symbol.asyncIterator]()
|
||||
expect((await nextFrame(reconnect, 'session/subscribed')).sessionId).toBe(session.id)
|
||||
|
||||
measure.mockImplementation(() => { throw new Error('unmeasurable replay') })
|
||||
agentEvents(ctx, agent).emit('agent/model-request', 2, 1, {
|
||||
provider: 'test',
|
||||
model: 'without-capacity',
|
||||
})
|
||||
for (const iterator of [primary, reconnect]) {
|
||||
expect(await nextFrame(iterator, 'session/model-request')).toEqual({
|
||||
type: 'session/model-request',
|
||||
sessionId: session.id,
|
||||
turn: 2,
|
||||
step: 1,
|
||||
provider: 'test',
|
||||
model: 'without-capacity',
|
||||
})
|
||||
}
|
||||
|
||||
removeTokenMeter()
|
||||
agentEvents(ctx, agent).emit('agent/model-request', 3, 1, {
|
||||
provider: 'test',
|
||||
model: 'without-meter',
|
||||
contextWindow: 64_000,
|
||||
})
|
||||
expect(await nextFrame(primary, 'session/model-request')).toEqual({
|
||||
type: 'session/model-request',
|
||||
sessionId: session.id,
|
||||
turn: 3,
|
||||
step: 1,
|
||||
provider: 'test',
|
||||
model: 'without-meter',
|
||||
contextWindow: 64_000,
|
||||
})
|
||||
|
||||
primaryAbort.abort()
|
||||
reconnectAbort.abort()
|
||||
await primary.return?.()
|
||||
await reconnect.return?.()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
})
|
||||
@@ -153,13 +153,11 @@ describe('sessions domain schemas', () => {
|
||||
expect(sessionCreateValueSchema.parse({ sessionId: 's1' }).sessionId).toBe('s1')
|
||||
expect(sessionHistoryRequestSchema.parse({ sessionId: 's1', beforeSeq: 3, maxMessages: 5 }).beforeSeq).toBe(3)
|
||||
expect(() => sessionHistoryRequestSchema.parse({ sessionId: 's1', maxMessages: 0 })).toThrow()
|
||||
const history = sessionHistoryValueSchema.parse({
|
||||
expect(sessionHistoryValueSchema.parse({
|
||||
events: [],
|
||||
hasMore: false,
|
||||
projections: { asOfSeq: 11, values: { todos: [] } },
|
||||
modelTarget: { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
expect(history.projections).toEqual({ asOfSeq: 11, values: { todos: [] } })
|
||||
}).hasMore).toBe(false)
|
||||
expect(sessionModelsRequestSchema.parse({ sessionId: 's1' }).sessionId).toBe('s1')
|
||||
expect(sessionModelsValueSchema.parse({
|
||||
current: { provider: 'deepseek', model: 'deepseek-v4-flash', reasoningEffort: 'max' },
|
||||
@@ -361,24 +359,6 @@ describe('events frame schemas', () => {
|
||||
const frames = [
|
||||
{ type: 'session/event', sessionId: 's', event: { type: 't', seq: 0, time: 1, data: null } },
|
||||
{ type: 'session/subscribed', sessionId: 's', lastSeq: -1 },
|
||||
{
|
||||
type: 'session/model-request',
|
||||
sessionId: 's',
|
||||
turn: 2,
|
||||
step: 1,
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-chat',
|
||||
contextTokens: 8_000,
|
||||
contextWindow: 128_000,
|
||||
},
|
||||
{
|
||||
type: 'session/model-request',
|
||||
sessionId: 's',
|
||||
turn: 3,
|
||||
step: 1,
|
||||
provider: 'deepseek',
|
||||
model: 'unknown-capacity',
|
||||
},
|
||||
{ type: 'approval/requested', sessionId: 's', approvalId: 'a', toolName: 'bash', callId: 'c', reason: 'r' },
|
||||
{ type: 'approval/resolved', sessionId: 's', approvalId: 'a', outcome: 'allowed-once' },
|
||||
{ type: 'question/requested', sessionId: 's', questions: [{ id: 'q', question: 'Q?', options: [{ label: 'L' }], multiSelect: true }] },
|
||||
@@ -391,10 +371,6 @@ describe('events frame schemas', () => {
|
||||
for (const frame of frames) expect(muxFrameSchema.parse(frame)).toMatchObject({ type: frame.type })
|
||||
expect(() => muxFrameSchema.parse({ type: 'unknown/frame' })).toThrow()
|
||||
for (const invalid of [
|
||||
{ type: 'session/model-request', sessionId: 's', turn: 0, step: 1, provider: 'p', model: 'm' },
|
||||
{ type: 'session/model-request', sessionId: 's', turn: 1, step: 0, provider: 'p', model: 'm' },
|
||||
{ type: 'session/model-request', sessionId: 's', turn: 1, step: 1, provider: 'p', model: 'm', contextTokens: -1 },
|
||||
{ type: 'session/model-request', sessionId: 's', turn: 1, step: 1, provider: 'p', model: 'm', contextWindow: 0 },
|
||||
{ type: 'session/projection', sessionId: 's', key: '', value: null, seq: 0 },
|
||||
{ type: 'session/projection', sessionId: 's', key: 'todos', value: null, seq: -1 },
|
||||
{ type: 'session/projection', sessionId: 's', key: 'todos', value: null, seq: 0.5 },
|
||||
|
||||
Reference in New Issue
Block a user