Skip to content

fix(chat): scope the deferred transcript to its session (E2E fork.spec red on main) - #8581

Merged
iamwhatever merged 2 commits into
mainfrom
fix/e2e-fork-spec-more-actions-8526
Sep 4, 2026
Merged

fix(chat): scope the deferred transcript to its session (E2E fork.spec red on main)#8581
iamwhatever merged 2 commits into
mainfrom
fix/e2e-fork-spec-more-actions-8526

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #8526E2E (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

ChatPage renders the transcript from useDeferredValue(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:

  1. /chat lands on the seeded starter session (4 messages rendered).
  2. "New chat" creates chat-1, messages[]. The list is unmounted behind the welcome state, so the deferred frame silently keeps the starter's 4 turns.
  3. First send: messages[user], welcome state ends, list mounts — with renderedDisplayItems still 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-….
  4. The spec's [data-role="assistant"].last() resolves to a ghost row, toBeVisible() passes immediately, and the 5 s wait for assistant-more-actions starts ~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-content first → 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 and bad30a003 / #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, hydrateSlotMessages all silent during the ghost render).

useDeferredValue(displayItems) was introduced by 436fe5115 / #7916 at 16:03 UTC. main's E2E history around it:

The single green at 824ab57e8 was a lucky roll of a ~50 % timing race, and the issue took it as the last-good commit.

Fix

useSlotDeferredValue(slot, value)useDeferredValue keyed 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 its error-context.md and trace instead of only naming them in the log. docs/ci/e2e-gate.md documents it.

Verification

Local, against the harness gateway + fake ACP backend (the only way to run this spec):

  • Before (main 5bc2fc796): a WS/DOM probe reproduced the ghost transcript on 3 / 3 sends (starter rows visible 150–400 ms after send, href already ?sid=chat-N); fork.spec.ts failed on its first attempt with the exact CI assertion and passed on retry.
  • After: probe shows 0 ghost rows across 3 rounds × 40 polls; fork.spec.ts passed 3 / 3 first attempts with --retries=0.
  • npx vitest run src/hooks/useSlotDeferredValue.test.ts (4 tests, useDeferredValue mocked to hold a stale frame), tsc --noEmit, eslint on 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 starter session was the previous tab). Captured by a Playwright probe that screenshots the instant any [data-role] row exists.

Before (main 5bc2fc796) After
before: New Session tab selected, but the transcript shows the Welcome to KiroCrew session's 4 messages after: New Session tab shows only its own user message and the thinking indicator

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

  1. 666c33658 — the fix (useSlotDeferredValue, ChatPage wiring, unit test, the two screenshots). Fixes #8526.
  2. acf822647 — E2E job uploads test-results/ + playwright-report/ on failure, plus the e2e-gate.md section.

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 the switchSlot.rejected origin-restore and the sameTranscript identity 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 useDeferredValue in useSlotDeferredValue.test.ts.

Inherited red

None expected: #8536 landed on main and the Windows shard is green there (#8534 closed as superseded).

@CrysisDeu
CrysisDeu requested a review from a team September 4, 2026 22:43
@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 4, 2026 22:43
@CrysisDeu
CrysisDeu force-pushed the fix/e2e-fork-spec-more-actions-8526 branch from 73d7bfc to 5a86f48 Compare September 4, 2026 22:44
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

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.

@CrysisDeu CrysisDeu closed this Sep 4, 2026
@CrysisDeu CrysisDeu reopened this Sep 4, 2026
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of acf8226471385d77992e80b145a9c3c87779dfb5 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of acf8226471385d77992e80b145a9c3c87779dfb5 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

  • The before/after PNGs live in website/temp-screenshots/ — a directory named "temp" now permanently in git history; attach evidence images to the PR/issue instead of committing them to the source tree.

[DESIGN-REVIEWED] acf8226

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed acf8226471385d77992e80b145a9c3c87779dfb5 — this comment is updated in place on each push.

Review details

The discovery pass found no candidates. I independently examined each hunk of the diff.

The core change, useSlotDeferredValue, bundles {slot, value} into one memoized frame, defers the frame, and returns deferred.value only when deferred.slot === frame.slot. I traced the states: same-slot streaming updates return the lagging deferred value (intended defer); a session switch changes both slot and value, so the lagging frame's slot mismatches and the current value renders immediately (the #8526 fix). Slot normalization (slot ?? null) is applied identically to both comparison operands, so null/undefined collapse consistently. No path returns another session's data.

ChatPage.tsx swaps the call site cleanly, removes the now-unused useDeferredValue import, and keeps renderedDisplayItems as the single consumed value, preserving index-space consistency. The CI step is failure-only with correct workspace-relative paths and if-no-files-found: ignore. The docs match the CI change. None of these produce a reachable defect, and none trip a matching AUTOSDE rule.

No findings.

[OPUS-REVIEWED] acf8226

Verdict parsed from the review's SHA-scoped output markers for commit acf8226471385d77992e80b145a9c3c87779dfb5.

False positive or not applicable? A repository writer can comment:
/ai-review override fable acf8226471385d77992e80b145a9c3c87779dfb5: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of acf8226471385d77992e80b145a9c3c87779dfb5 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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 (useDeferredValue appears nowhere else over per-session data in website/src). Final review:

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 ships

Intent: stop a new chat from briefly painting the previous session's transcript, which kept the offline E2E gate red on main — a FIX.

  1. A session switch now shows its own transcript in the first frame — justified (fixes fork.spec E2E fails on main: assistant-more-actions not visible (reddens every PR merge ref) #8526 at cause level).
  2. New exported hook useSlotDeferredValue — one consumer (ChatPage.tsx:6447; grep useSlotDeferredValue, 1 non-test call site), but the extraction carries the regression test.
  3. Same-session streaming/history updates still defer — justified (preserves perf(chat): reachable archived history and stable phone scrolling #7916's measured perf motive; the delete-option, reverting perf(chat): reachable archived history and stable phone scrolling #7916, would reintroduce it).
  4. A failed E2E run now uploads Playwright test-results/ + html report (7-day retention) — rides along, declared, justified by the counted triage cost.
  5. New e2e-gate.md section — derived (AGENTS.md same-commit doc invariant).
  6. Two evidence screenshots under temp-screenshots/ — derived (PULL_REQUEST_TEMPLATE.md:47 mandates that path; 17 sibling files already there).

Watch

  • useSlotDeferredValue is a generic <T> hook with exactly one consumer. The inline form in ChatPage would be the same size; the hook earns its file only through useSlotDeferredValue.test.ts, which is the sole unit coverage of the fork.spec E2E fails on main: assistant-more-actions not visible (reddens every PR merge ref) #8526 invariant — acceptable, but a second "reusable" spelling from the description's "Pattern harvest" should wait for a second consumer.
  • No sibling instances: useDeferredValue over per-session data appears nowhere else in website/src (grep count: 1 real usage, now wrapped), so the point fix is also the general fix.

[FIRST-PRINCIPLES-REVIEWED] acf8226

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of acf8226471385d77992e80b145a9c3c87779dfb5 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] acf8226

False positive or not applicable? A repository writer can comment:
/ai-review override gpt acf8226471385d77992e80b145a9c3c87779dfb5: <one-sentence reason>

…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.
@CrysisDeu
CrysisDeu force-pushed the fix/e2e-fork-spec-more-actions-8526 branch from 5a86f48 to acf8226 Compare September 4, 2026 23:03
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@iamwhatever
iamwhatever merged commit fd35744 into main Sep 4, 2026
48 of 55 checks passed
@iamwhatever
iamwhatever deleted the fix/e2e-fork-spec-more-actions-8526 branch September 4, 2026 23:06
@github-actions github-actions Bot removed the readiness: checking Automated validation is still running label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fork.spec E2E fails on main: assistant-more-actions not visible (reddens every PR merge ref)

2 participants