fix(chat): scope the deferred transcript to its session (E2E fork.spec red on main) - #8581
Conversation
73d7bfc to
5a86f48
Compare
|
Closing: delivery changed — this fix is being cherry-picked into #8534 so that PR reaches a genuinely green board in one go. Commits on this branch: 7a4799c (the fork.spec root-cause fix: slot-scoped deferred transcript, Fixes #8526) and 5a86f48 (E2E job uploads test-results on failure). Root-cause writeup is in the PR body above. |
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Pure defect removal — the wrong session's transcript no longer flashes after a switch; no new copy, controls, or flows to misread, and the screenshots confirm the claim. [UX-REVIEWED] acf8226 |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root cause correctly re-bisected to the deferral itself; keying the deferred frame by slot is the minimal fix that preserves #7916's perf win. Suggestions
[DESIGN-REVIEWED] acf8226 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe discovery pass found no candidates. I independently examined each hunk of the diff. The core change,
No findings. [OPUS-REVIEWED] acf8226 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks complete. The fix is cause-level (keys the deferred frame by the entity that produced it), the rider is declared and carries a named cost, the screenshots follow the repo's own PR-template convention, and I found no unfixed siblings ( First-Principles-Verdict: PASS Every item is derived from the reported defect (#8526) or a documented convention, and the fix sits at cause level: deferred state keyed to the entity that produced it. What this change shipsIntent: stop a new chat from briefly painting the previous session's transcript, which kept the offline E2E gate red on main — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] acf8226 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
…ever paints the previous tab The E2E fork spec red on main since #7916 (issue #8526) was not the fork affordance at all. `useDeferredValue(displayItems)` in ChatPage keeps returning the PREVIOUS list until React finds room for the background render, and under the page's urgent churn (title typewriter, composer keystrokes, heartbeat state) that lag runs to hundreds of milliseconds. A new chat is created while the transcript list is hidden behind the welcome state, so the deferred frame still held the OUTGOING session's turns when the first send mounted the list -- the previous tab's messages rendered under the new tab's URL for 150-400 ms. The spec's `[data-role=assistant]` locator matched one of those ghost rows, started its 5 s wait for the overflow trigger there, and then watched every assistant row vanish ("element(s) not found") while the real reply was still in flight. The deferral is now keyed by slot (`useSlotDeferredValue`): while the deferred frame belongs to another session, the current list renders at urgent priority; same-session updates (streaming flushes, history landings -- the case #7916 deferred for) stay deferred. Verified locally against the harness gateway + fake ACP backend: ghost rows reproduced 3/3 before, 0/3 after; fork.spec.ts 3/3 first-attempt passes with --retries=0. Fixes #8526
…fails Until now a red `E2E (stub ACP backend, offline)` job left nothing but a log line naming `error-context.md`; the ARIA snapshot, on-first-retry trace and html report never left the runner, and #8526 was triaged for hours without them. Failure-only, `if-no-files-found: ignore` so a run that died before the specs started does not fail twice, 7-day retention.
5a86f48 to
acf8226
Compare
Summary
Fixes #8526 —
E2E (stub ACP backend, offline)red on main (fork.spec.ts:49,assistant-more-actions"element(s) not found"), inherited by every open PR's merge ref.The failing locator was never the problem. The page was briefly rendering the previous session's transcript under the new session's URL, and the spec latched onto one of those ghost rows.
Root cause
ChatPagerenders the transcript fromuseDeferredValue(displayItems)(added by #7916 to keep typing responsive while a 1500-message history regroups). A deferred value is the previous value until React finds room for the background render, and under this page's urgent churn (title typewriter frames every ~90 ms, composer keystrokes, heartbeat-driven state) that lag runs to hundreds of milliseconds — measured 90–400 ms locally with a render-time probe.The fork spec's flow makes that lag visible:
/chatlands on the seededstartersession (4 messages rendered).chat-1,messages→[]. The list is unmounted behind the welcome state, so the deferred frame silently keeps the starter's 4 turns.messages→[user], welcome state ends, list mounts — withrenderedDisplayItemsstill equal to the starter's turns. For 150–400 ms the transcript shows the starter's 2 user + 2 assistant rows under?sid=chat-1-….[data-role="assistant"].last()resolves to a ghost row,toBeVisible()passes immediately, and the 5 s wait forassistant-more-actionsstarts ~200 ms after send. The ghost rows then vanish (locator now matches nothing → "element(s) not found"), and the real reply from the stub backend lands ~6 s after send — after the assertion expired.The sibling flaky in the same runs (
chat.spec.ts:62,.msg-contentfirst → hidden) is the same ghost transcript being torn down.Why the issue's window was wrong
The issue bracketed the regression as
824ab57e8..f5596230f(two dashboard candidates:e125819af/ #8131 andbad30a003/ #7693). Neither touches the transcript, and the diag confirms neither is involved: no history write happens at send (rules out the Back guard), and the store never holds the wrong messages (rules out any slot/switch path —switchSlot,replaceMessages,hydrateSlotMessagesall silent during the ghost render).useDeferredValue(displayItems)was introduced by436fe5115/ #7916 at 16:03 UTC. main's E2E history around it:824ab57e8green (17:08), thenf5596230fred,701f8f981red,eaeb70a0agreen,d3e67b7e9red,5bc2fc796redThe single green at
824ab57e8was a lucky roll of a ~50 % timing race, and the issue took it as the last-good commit.Fix
useSlotDeferredValue(slot, value)—useDeferredValuekeyed by the active slot. While the deferred frame still belongs to another session, the current list renders at urgent priority; once React catches up the two agree and the deferred path resumes. Same-session updates (streaming flushes, the history landing #7916 deferred for) are unchanged; a session switch renders the right transcript in its first commit, which is what it did before #7916.Second commit: the E2E job now uploads
website/test-results/+website/playwright-report/on failure (e2e-playwright-failures, 7 days) so the next red carries itserror-context.mdand trace instead of only naming them in the log.docs/ci/e2e-gate.mddocuments it.Verification
Local, against the harness gateway + fake ACP backend (the only way to run this spec):
5bc2fc796): a WS/DOM probe reproduced the ghost transcript on 3 / 3 sends (starter rows visible 150–400 ms after send,hrefalready?sid=chat-N);fork.spec.tsfailed on its first attempt with the exact CI assertion and passed on retry.fork.spec.tspassed 3 / 3 first attempts with--retries=0.npx vitest run src/hooks/useSlotDeferredValue.test.ts(4 tests,useDeferredValuemocked to hold a stale frame),tsc --noEmit,eslinton touched files, 3 ChatPage render suites (38 tests),scripts/docs-lint.sh— all green. No full suite run locally.Screenshots
First paint of the transcript after the first send in a brand-new chat (harness gateway, seeded
startersession was the previous tab). Captured by a Playwright probe that screenshots the instant any[data-role]row exists.5bc2fc796)Before: the New Session tab is selected and "Thinking…", yet the transcript is the Welcome to KiroCrew session's two Q&As. After: the new session's own message, nothing else.
Commits
666c33658— the fix (useSlotDeferredValue, ChatPage wiring, unit test, the two screenshots).Fixes #8526.acf822647— E2E job uploadstest-results/+playwright-report/on failure, plus thee2e-gate.mdsection.Pattern harvest
Rule candidate: a
useDeferredValue(or any "render the previous value while the new one is in flight" mechanism) over per-session data must be keyed by the session — when the key changes, render the current value synchronously rather than the stale frame. The stale frame is a different entity's data, not an older version of the same data, and the lag is unbounded under urgent churn. Same shape as theswitchSlot.rejectedorigin-restore and thesameTranscriptidentity checks: transcript state that outlives a slot switch is a wrong-session bug, not a perf artefact.Detection gap: the E2E job uploaded no failure artifacts, so the ARIA snapshot that would have shown ghost rows in ~a minute was unrecoverable for hours (commit 2 closes that). The unit-level gap — no test renders ChatPage across a slot switch with a lagging deferred value — is covered here by mocking
useDeferredValueinuseSlotDeferredValue.test.ts.Inherited red
None expected: #8536 landed on main and the Windows shard is green there (#8534 closed as superseded).