From 1f2fbcb142b526201f1ab3473e0068c3e2a0bf56 Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Sun, 16 Aug 2026 22:35:31 +0800 Subject: [PATCH] fix(web): map codex-enveloped compact-summary to the chat block (#1582) * fix(web): map codex-enveloped compact-summary to the chat block The merged #1570 renders Pi compaction summaries as a dedicated chat block via the event envelope. A compact-summary arriving in the codex payload envelope (older import paths, future producers) would still be silently dropped by the codex-content filter; map it to the same agent-event the live pi wrapper emits, mirroring the existing context_compacted handling. Adds a normalizeAgent regression test and tightens the /compact thinking-state test to assert the last keepAlive flips true (RPC outstanding) then false (settled). Verified: bun typecheck clean; web normalizeAgent 12/12, cli runPi 57/57 in an isolated TMPDIR. * fix(web): remove duplicate showSessionSummaryInChat in markdown-a fixture Regression from #1530: the fixture object literal sets the key twice, which breaks `bun typecheck` on upstream/main (Test workflow failing on push). One-line cleanup; no behavior change. Verified: bun typecheck exit 0 across cli/web/hub. --- cli/src/pi/runPi.test.ts | 11 ++++++++++ web/src/chat/normalizeAgent.test.ts | 21 ++++++++++++++++++ web/src/chat/normalizeAgent.ts | 22 +++++++++++++++++++ .../assistant-ui/markdown-a.test.tsx | 1 - 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/cli/src/pi/runPi.test.ts b/cli/src/pi/runPi.test.ts index 735532da..041d0c16 100644 --- a/cli/src/pi/runPi.test.ts +++ b/cli/src/pi/runPi.test.ts @@ -1332,6 +1332,11 @@ describe('Pi built-in slash commands', () => { expect(harness.sent).not.toContainEqual(expect.objectContaining({ type: 'prompt' })); const compact = harness.sent.find((item) => (item as { type?: string }).type === 'compact') as { id: string }; + // The compact RPC holds the session in the thinking state for its + // whole duration (web would otherwise mark it idle after the hub's + // 15s queued-thinking grace). + const keepAliveCalls = harness.session.keepAlive.mock.calls; + expect(keepAliveCalls[keepAliveCalls.length - 1]?.[0]).toBe(true); harness.onEvent!({ type: 'response', id: compact.id, command: 'compact', success: true, data: { summary: 'API design focused summary', tokensBefore: 1000, estimatedTokensAfter: 120 }, @@ -1351,6 +1356,12 @@ describe('Pi built-in slash commands', () => { })); await vi.waitFor(() => expect(harness.session.keepAlive).toHaveBeenCalledWith(false, expect.anything(), expect.anything())); + // The thinking state is released once the RPC completes. + await vi.waitFor(() => { + const calls = harness.session.keepAlive.mock.calls; + expect(calls[calls.length - 1]?.[0]).toBe(false); + }); + // The queued prompt may only flow once compaction completed. await vi.waitFor(() => expect(harness.sent).toContainEqual(expect.objectContaining({ type: 'prompt', message: 'continue after compaction', diff --git a/web/src/chat/normalizeAgent.test.ts b/web/src/chat/normalizeAgent.test.ts index 5bb00d1b..1e5f1126 100644 --- a/web/src/chat/normalizeAgent.test.ts +++ b/web/src/chat/normalizeAgent.test.ts @@ -209,3 +209,24 @@ describe('normalizeAgentRecord — agentTimestamp exposure', () => { expect(normalized).toMatchObject({ role: 'agent', agentTimestamp: null }) }) }) + +describe('normalizeAgentRecord — imported pi compact-summary (codex envelope)', () => { + it('maps an imported compaction summary to the compact-summary agent event', () => { + const normalized = normalizeAgentRecord('pi-compact-1', null, 1, { + type: 'codex', + data: { + type: 'compact-summary', + summary: '## Goal\ncondensed context' + } + }) + + expect(normalized).toMatchObject({ + role: 'event', + content: { + type: 'compact-summary', + summary: '## Goal\ncondensed context' + } + }) + expect((normalized as { content: { tokensBefore?: number } }).content.tokensBefore).toBeUndefined() + }) +}) diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index d65afba4..867988f2 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -1034,6 +1034,28 @@ export function normalizeAgentRecord( } } + // Defensive parity with context_compacted above: a compact-summary + // arriving in the codex envelope (e.g. from an older import path or a + // future producer) must not be silently dropped by the codex-content + // filter — map it to the same agent-event the live pi wrapper emits + // so it renders as the dedicated chat block. + if (data.type === 'compact-summary' && typeof data.summary === 'string') { + return { + id: messageId, + localId, + createdAt, + role: 'event', + content: { + type: 'compact-summary', + summary: data.summary, + tokensBefore: asNumber(data.tokensBefore) ?? undefined, + estimatedTokensAfter: asNumber(data.estimatedTokensAfter) ?? undefined + }, + isSidechain: false, + meta + } + } + if (data.type === 'token_count') { const usage = normalizeCodexTokenUsage(data.info, data) return usage ? { diff --git a/web/src/components/assistant-ui/markdown-a.test.tsx b/web/src/components/assistant-ui/markdown-a.test.tsx index 36f5ca72..d5c16131 100644 --- a/web/src/components/assistant-ui/markdown-a.test.tsx +++ b/web/src/components/assistant-ui/markdown-a.test.tsx @@ -61,7 +61,6 @@ function chatContext(overrides: Partial = {}): HappyChatC isLoadingMoreMessages: false, showSessionSummaryInChat: false, loadOlderMessagesPreservingScroll: async () => 'loaded', - showSessionSummaryInChat: false, ...overrides, } }