From cea20b2a5d4bbfdafa2c30c1ab0b2bbe8dd87ed7 Mon Sep 17 00:00:00 2001 From: moonold Date: Mon, 21 Sep 2026 21:14:19 +0800 Subject: [PATCH] fix(runtime-host): log why a transcript page read failed SessionContinuityCoordinator#readTranscriptPage collapses every error that is not a TranscriptPageRequestError into persistence_failed with "Session transcript is unavailable", and returns it as an operation outcome. Because the handler returns instead of throwing, the "[runtime-host] unexpected failure" log in operation-dispatcher.ts - the only caller of boundedFailureDiagnostic - never runs for this operation. The cause then reaches neither the Runtime Host log, the desktop diagnostic report, nor the Renderer, which falls back to generic copy. Record the cause with boundedFailureDiagnostic before returning the generic outcome. The outcome the caller receives is unchanged. Refs #5572 Generated-by: Maka --- .../session-continuity-coordinator.test.ts | 63 +++++++++++++++++++ .../server/session-continuity-coordinator.ts | 7 +++ 2 files changed, 70 insertions(+) diff --git a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts index c584e4a3aa..d9a69b7913 100644 --- a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts @@ -2196,6 +2196,69 @@ function completionOrder(sink: { frames: SubscriptionFrame[] }): string[] { ); } +test('a failed transcript page records the underlying cause before the generic outcome', async () => { + const message = assistantMessage('界'.repeat(20_000)); + const baseReader = transcriptReader([message]); + const reader: SessionTranscriptReader = { + ...baseReader, + // The bootstrap request carries no position; a page always does. Throw only + // for the page so the subscription still opens. + readDurablePage: async (sessionId, request, project) => { + if (request.position !== undefined) throw new Error('injected oversized Turn'); + return baseReader.readDurablePage(sessionId, request, project); + }, + }; + const coordinator = new SessionContinuityCoordinator( + HOST_EPOCH, + async () => canonical(), + new SessionAdmissionGate(), + undefined, + reader, + ); + attachTestConnection(coordinator, 'connection-page-failure', new RecordingSink()); + const opened = await open(coordinator, 'connection-page-failure', { + kind: 'tail', + maxBytes: SESSION_TRANSCRIPT_BOOTSTRAP_MAX_BYTES, + }); + const transcript = opened.transcript; + const cursor = transcript?.durable.nextCursor; + assert.ok(transcript); + assert.ok(cursor); + if (!transcript || !cursor) { + coordinator.close(); + return; + } + + // The generic outcome carries no cause, so the Host has to log one instead. + const logged: string[] = []; + const originalConsoleError = console.error; + console.error = (...values: unknown[]) => logged.push(values.map(String).join(' ')); + try { + const outcome = await coordinator.handlers['session.transcript.page']( + { + subscriptionId: opened.subscriptionId, + direction: 'older', + throughSequence: transcript.durable.throughSequence, + cursor, + anchorSequence: null, + maxBytes: SESSION_TRANSCRIPT_PAGE_MAX_BYTES, + }, + connectionContext('connection-page-failure'), + ); + assert.deepEqual(outcome, { + ok: false, + error: { code: 'persistence_failed', message: 'Session transcript is unavailable' }, + }); + } finally { + console.error = originalConsoleError; + coordinator.close(); + } + + assert.equal(logged.length, 1); + assert.match(logged[0] ?? '', /session\.transcript\.page failed/); + assert.match(logged[0] ?? '', /injected oversized Turn/); +}); + function textCompleteEvent(messageId: string, text: string) { return { type: 'text_complete' as const, diff --git a/packages/runtime-host/src/server/session-continuity-coordinator.ts b/packages/runtime-host/src/server/session-continuity-coordinator.ts index 4e10ff14cf..47964130bc 100644 --- a/packages/runtime-host/src/server/session-continuity-coordinator.ts +++ b/packages/runtime-host/src/server/session-continuity-coordinator.ts @@ -61,6 +61,7 @@ import type { SessionContinuityOperationHandlerMap, } from './operation-dispatcher.js'; import type { RuntimeHostAccessAuthority } from './access-authority.js'; +import { boundedFailureDiagnostic } from './failure-diagnostic.js'; import { type SessionAdmissionLease, SessionAdmissionGate } from './session-admission-gate.js'; import { type CanonicalSessionProjection, @@ -1122,6 +1123,12 @@ export class SessionContinuityCoordinator implements SessionContinuityService { error: { code: 'invalid_request', message: error.message }, }; } + // The client can only retry, but a transcript page that failed for any + // other reason is a Host-side defect: the generic outcome the caller + // receives carries none of the cause, so record it here or it is lost. + console.error( + `[runtime-host] session.transcript.page failed: ${boundedFailureDiagnostic(error)}`, + ); return { ok: false, error: { code: 'persistence_failed', message: 'Session transcript is unavailable' },