Explorar o código

fix(web): preserve reconnect metrics baseline

Hypatia May hai 2 meses
pai
achega
47205cbb82

+ 4 - 6
packages/client/runtime/src/client/sessions/session.ts

@@ -309,11 +309,10 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
    *  in-flight open first — its history request rode the dead connection and must not settle
    *  the fresh generation into 'error' (audit S4). */
   async resync(): Promise<void> {
-    // The queue mirror is NOT cleared here: onConnected (which drives resync)
-    // races the mux frames — the fresh generation's baseline may have landed
-    // already, and the host never resends it. The mirror re-baselines on the
-    // session/subscribed frame instead (same stream as the queue snapshot
-    // that follows it, so ordering is guaranteed).
+    // Queue, metrics, and request capacity are NOT cleared here: onConnected
+    // (which drives resync) races the mux frames — fresh-generation state may
+    // have landed already, and the host never resends it. session/subscribed
+    // owns the generation reset before the queue snapshot and metrics frames.
     if (this.openState === 'cold') return // never opened: no window to rebuild (doOpen flips to 'loading' synchronously, so cold implies no in-flight open)
     this.openGeneration++
     this.openPromise = null
@@ -322,7 +321,6 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
     this.events = []
     this.views = []
     this.baseSeq = 0
-    this.metrics = null
     // Superseded, not settled: the baseline replay re-sends still-pending requested frames verbatim
     // (same rpcId), re-minting fresh waits; a stale reference's respond() still reaches the host.
     this.pending.clear()

+ 34 - 0
packages/client/runtime/tests/session.spec.ts

@@ -813,6 +813,40 @@ describe('remaining branches', () => {
 })
 
 describe('resync', () => {
+  it('preserves fresh-generation metrics that arrive before a failing history refresh', async () => {
+    const { api, session } = makeSession()
+    const oldMetrics = metrics(8, 10)
+    api.onHistory = () => histResponse(plainTurn(0, 0, 'a', 'b'), false, undefined, oldMetrics)
+    await session.open()
+    expect(session.getSnapshot().metrics).toBe(oldMetrics)
+
+    session.handleMuxEnvelope('sub' as never, {
+      type: 'session/subscribed',
+      sessionId: SID,
+      lastSeq: 5,
+    })
+    expect(session.getSnapshot().metrics).toBeNull()
+
+    const freshMetrics = metrics(0, 10, { contextTokens: 20 })
+    session.handleMuxEnvelope('fresh-metrics' as never, {
+      type: 'session/metrics',
+      sessionId: SID,
+      metrics: freshMetrics,
+    })
+    api.onHistory = () => Promise.resolve(err({
+      code: 'internal',
+      message: 'history refresh failed',
+      details: {},
+    }))
+
+    await session.resync()
+
+    expect(session.getSnapshot()).toMatchObject({
+      openState: 'error',
+      metrics: freshMetrics,
+    })
+  })
+
   it('rebuilds the window and clears pending; cold instances no-op', async () => {
     const { api, session } = makeSession()
     api.onHistory = () => histResponse(plainTurn(0, 0, 'a', 'b'))