Live-driven via the owner's dev server (browser tools), reproduced on
/chat/new: send fires, optimistic appends, POST never happens, and the
page shows the frozen empty state. Three compounding defects:
1. session-core: default fetch stored UNBOUND — the browser's native
fetch requires this === Window, so every call threw 'Illegal
invocation' and the POST /api/chat never fired. Node's undici has no
receiver discipline, which is why 994/994 tests passed while the app
was dead. Now defaulted through a receiver-preserving wrapper; pinned
by a strict-this regression test (the browser's semantics simulated.
2. session.ts: state memo keyed on [model, forceRender] — the reducer
dispatch is STABLE by contract, so the memo computed once per mount:
no optimistic bubble, no stream events, no error banner. The view was
frozen at first render. Rekeyed on a version counter bumped by the
model's onChange.
3. view.ts: turn_end carries the clean 'Question posed' placeholder as
a toolResult message (details: null) — applyResult flipped the quiz
part to output-available with a null output and AnsweredCard crashed
on answer.dismissed (killed the whole page render). Fork 2 now holds
on the message-attachment path too: clean question results with null
details are ignored; the AnsweredCard renders only the learner's real
answer. Regression pin extended in session-core.test.ts.
Verified live end to end: new session → text send → probe questions in
the dock → ask answer → graded ✓ quiz card with explanation → reload
renders the full transcript from the native record with the pending
question intact.
EOF
)