fix(web): reset live metrics between connections
This commit is contained in:
@@ -143,6 +143,7 @@ export function apply(ctx: Context): void {
|
||||
workspaces.handleConnected()
|
||||
ctx.emit('connection/reset')
|
||||
},
|
||||
onDisconnected: () => { sessions.handleReconnecting() },
|
||||
})
|
||||
ctx.effect(() => () => { loop.stop() }, 'runtime: connection stream loop')
|
||||
}
|
||||
|
||||
@@ -430,6 +430,12 @@ export class SessionManager {
|
||||
for (const session of this.sessions.values()) void session.resync()
|
||||
}
|
||||
|
||||
/** Before a replacement stream generation, discard values the Host does not replay. */
|
||||
handleReconnecting(): void {
|
||||
this.modelRequestContextWindows.clear()
|
||||
for (const session of this.sessions.values()) session.handleReconnecting()
|
||||
}
|
||||
|
||||
private buildListSnapshot(): SessionListSnapshot {
|
||||
const merged: TitledSessionSummary[] = this.summaries.map((summary) => {
|
||||
// List rows read the generic 'title' projection key (host-computed unit
|
||||
|
||||
@@ -393,6 +393,11 @@ export class SessionsService {
|
||||
this.manager.handleConnected()
|
||||
}
|
||||
|
||||
/** Clear connection-local Session state before the next stream generation starts. */
|
||||
handleReconnecting(): void {
|
||||
this.manager.handleReconnecting()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a session on the host. Resolution guarantee: by the time the
|
||||
* promise resolves, the created session is in the list store and
|
||||
|
||||
@@ -481,6 +481,14 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
|
||||
/** Connection-loss boundary: clear values that are not replayed before the next stream starts. */
|
||||
handleReconnecting(): void {
|
||||
if (this.metrics === null && this.contextWindow === undefined) return
|
||||
this.metrics = null
|
||||
this.contextWindow = undefined
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
|
||||
/** host/session-removed relay: flag the resident snapshot and clear connection-local capacity. */
|
||||
handleRemoved(): void {
|
||||
const changed = !this.removed || this.contextWindow !== undefined
|
||||
|
||||
@@ -102,6 +102,53 @@ describe('runtime client apply', () => {
|
||||
expect(bench.api.callsOf('session.create')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('clears connection-local Session state on disconnect but not connected', async () => {
|
||||
const bench = await mount()
|
||||
const sessions = bench.ctx.get('sessions') as SessionsService
|
||||
bench.sinks?.onHostEnvelope?.({
|
||||
rpcId: 'session' as never,
|
||||
payload: { type: 'host/session-added', blank: true, sessionId: 's-state' } as never,
|
||||
})
|
||||
await Promise.resolve()
|
||||
const session = sessions.binding('s-state' as never)?.session
|
||||
if (session === undefined) throw new Error('session binding missing')
|
||||
const currentMetrics = {
|
||||
projectionRevision: 4,
|
||||
logRevision: 10,
|
||||
uncachedInputTokens: 10,
|
||||
outputTokens: 4,
|
||||
cacheReadTokens: 90,
|
||||
cacheWriteTokens: 3,
|
||||
contextTokens: 35,
|
||||
}
|
||||
bench.sinks?.onMuxEnvelope?.({
|
||||
rpcId: 'metrics' as never,
|
||||
payload: { type: 'session/metrics', sessionId: 's-state', metrics: currentMetrics } as never,
|
||||
})
|
||||
bench.sinks?.onMuxEnvelope?.({
|
||||
rpcId: 'capacity' as never,
|
||||
payload: {
|
||||
type: 'session/model-request',
|
||||
sessionId: 's-state',
|
||||
turn: 1,
|
||||
step: 1,
|
||||
provider: 'test',
|
||||
model: 'alpha',
|
||||
contextWindow: 128_000,
|
||||
} as never,
|
||||
})
|
||||
|
||||
bench.sinks?.onConnected?.()
|
||||
expect(session.getSnapshot()).toMatchObject({
|
||||
metrics: currentMetrics,
|
||||
modelRequestContextWindow: 128_000,
|
||||
})
|
||||
|
||||
bench.sinks?.onDisconnected?.()
|
||||
expect(session.getSnapshot().metrics).toBeNull()
|
||||
expect(session.getSnapshot().modelRequestContextWindow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('stops the stream loop when the plugin fiber unloads', async () => {
|
||||
const bench = await mount()
|
||||
const fiber = [...bench.ctx.registry.values()].find(f => f.name?.includes('client'))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId, SessionMetrics } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||
import { entries, plainTurn } from './event-script.ts'
|
||||
@@ -162,6 +162,59 @@ describe('instances', () => {
|
||||
expect(manager.get(S2).getSnapshot().modelRequestContextWindow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('clears resident metrics and capacity plus lazy capacity before reconnect', () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const session = manager.get(S1)
|
||||
const currentMetrics: SessionMetrics = {
|
||||
projectionRevision: 4,
|
||||
logRevision: 10,
|
||||
uncachedInputTokens: 10,
|
||||
outputTokens: 4,
|
||||
cacheReadTokens: 90,
|
||||
cacheWriteTokens: 3,
|
||||
contextTokens: 35,
|
||||
}
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'metrics' as never,
|
||||
payload: { type: 'session/metrics', sessionId: S1, metrics: currentMetrics },
|
||||
})
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'resident-capacity' as never,
|
||||
payload: {
|
||||
type: 'session/model-request',
|
||||
sessionId: S1,
|
||||
turn: 1,
|
||||
step: 1,
|
||||
provider: 'test',
|
||||
model: 'resident',
|
||||
contextWindow: 128_000,
|
||||
},
|
||||
})
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'lazy-capacity' as never,
|
||||
payload: {
|
||||
type: 'session/model-request',
|
||||
sessionId: S2,
|
||||
turn: 1,
|
||||
step: 1,
|
||||
provider: 'test',
|
||||
model: 'lazy',
|
||||
contextWindow: 256_000,
|
||||
},
|
||||
})
|
||||
expect(session.getSnapshot()).toMatchObject({
|
||||
metrics: currentMetrics,
|
||||
modelRequestContextWindow: 128_000,
|
||||
})
|
||||
|
||||
manager.handleReconnecting()
|
||||
|
||||
expect(session.getSnapshot().metrics).toBeNull()
|
||||
expect(session.getSnapshot().modelRequestContextWindow).toBeUndefined()
|
||||
expect(manager.get(S2).getSnapshot().modelRequestContextWindow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('caps the pending buffer at 32 keeping the newest, and drops it on session-removed', () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
|
||||
Reference in New Issue
Block a user