diff --git a/packages/client/runtime/src/client/sessions/request-inspection.ts b/packages/client/runtime/src/client/sessions/request-inspection.ts index 49ff8d974f..499b9acab1 100644 --- a/packages/client/runtime/src/client/sessions/request-inspection.ts +++ b/packages/client/runtime/src/client/sessions/request-inspection.ts @@ -94,7 +94,8 @@ export interface RequestInspectionSnapshot { /** * Derive the request-centric read model from one immutable history window. * Compaction participates as a request purpose rather than a parallel - * top-level collection. + * top-level collection. A leading resume/change header exposes its prompt but + * cannot project a change until the preceding header enters the window. * @param entries - Contiguous raw session history. * @returns Requests and call-time schemas derived from that history. */ @@ -218,6 +219,7 @@ function promptChange( prompt: ConversationPromptSnapshot, event: SessionEvent<'request/header'>, ): RequestPromptChange | undefined { + if (previous === undefined && event.data.reason !== 'initial') return const systemChanged = previous !== undefined && previous.system !== prompt.system const toolsChanged = previous !== undefined && JSON.stringify(previous.tools) !== JSON.stringify(prompt.tools) diff --git a/packages/client/runtime/tests/request-inspection.spec.ts b/packages/client/runtime/tests/request-inspection.spec.ts index f354d14654..bb7f6e1a9a 100644 --- a/packages/client/runtime/tests/request-inspection.spec.ts +++ b/packages/client/runtime/tests/request-inspection.spec.ts @@ -85,6 +85,56 @@ describe('inspectRequests', () => { expect(snapshot.callSchemas.get('call-1')?.name).toBe('read') }) + it('does not promote a truncated resume or change header to the initial prompt', () => { + for (const reason of ['resume', 'change'] as const) { + const snapshot = inspectRequests(entriesOf([ + at(10, 'step/start', { turn: 3, step: 1 }), + at(11, 'request/header', { + reason, + header: { + config: { provider: 'fake', model: 'model' }, + system: 'tail-window prompt', + }, + }), + ])) + + expect(snapshot.requests[0]).toMatchObject({ + purpose: 'assistant', + prompt: { system: 'tail-window prompt' }, + }) + expect(snapshot.requests[0]).not.toHaveProperty('promptChange') + } + }) + + it('classifies a prompt change once the preceding header is loaded', () => { + const snapshot = inspectRequests(entriesOf([ + at(0, 'step/start', { turn: 1, step: 1 }), + at(1, 'request/header', { + reason: 'initial', + header: { + config: { provider: 'fake', model: 'model' }, + system: 'before', + }, + }), + at(2, 'step/start', { turn: 1, step: 2 }), + at(3, 'request/header', { + reason: 'change', + header: { + config: { provider: 'fake', model: 'model' }, + system: 'after', + }, + }), + ])) + + expect(snapshot.requests[1]).toMatchObject({ + promptChange: { + seq: 3, + kind: 'system', + previous: { system: 'before' }, + }, + }) + }) + it('preserves a standalone compaction owner without widening assistant turns', () => { const snapshot = inspectRequests(entriesOf([ at(0, 'compact/start', { turn: null }),