fix(researcher): retired title model broke every new chat — plus tooltip, auto-resume, pyodide, and sprint-export fixes - #673
Open
nikola0x0 wants to merge 5 commits into
Open
Conversation
Found on production (researcher.demo.memwal.ai), then reproduced and A/B'd
locally against the same condition.
getTitleModel() hardcoded "google/gemini-2.0-flash-001", which OpenRouter has
retired — it returns 404 "No endpoints found" for our own production key. Every
other call site already used google/gemini-2.5-flash; this one was missed.
Because titlePromise was awaited with no try/catch inside createUIMessageStream's
execute(), the rejection reached the stream's onError, which injected an
{"type":"error"} part — so the client rendered a fatal chat error even though the
real answer had streamed successfully underneath it.
Production impact when this was written: 6 of 43 chats have a user message and no
assistant reply (4 of them on or after the 2026-08-14 deploy), and 11 of 43 chats
are still titled "New chat".
apps/chatbot already had this guard from 334cf91 and researcher was missed at the
time. This ports it, and also fixes the root cause that commit left in place: the
retired model id itself. Moving it to a TITLE_MODEL constant beside chatModels
keeps it from drifting away from the models we actually support.
Auditing the rest of the picker with the production key turned up two more retired
ids — anthropic/claude-3.5-haiku and anthropic/claude-3.5-sonnet 404 identically.
Three of the six selectable models were dead. Removed, and recorded in the new
unit test so they cannot be reintroduced by copy-paste.
Verified locally (docker pgvector + next dev, real OpenRouter key, PLAYWRIGHT
unset so getTitleModel is not swapped for the mock):
- with the guard, retired title model: answer streams, no error frame
- without the guard, retired title model: answer streams, error frame injected
- with the production key: title generates for real, no error frame
- pnpm test:unit 11/11; tsc --noEmit clean on the changed files
Not addressed here: production also shows chats with no persisted assistant row at
all, which did not reproduce locally — the assistant message persisted in both arms
of the A/B. That may be the older deployed build or the unhandledRejection aborting
before onFinish, and is worth re-checking once this ships. apps/chatbot still
carries the same retired model ids.
Two UI defects on the empty-chat screen. Tooltips rendered as a ~2px sliver under their trigger. Not overflow clipping — every ancestor was overflow:visible. TooltipContent was not wrapped in TooltipPrimitive.Portal, so it stayed inside the chat header, and `position: sticky` always creates a stacking context. The content's z-50 therefore only applied within that 50px-tall header, and the part of the tooltip below 50px painted underneath the page content that follows the header in DOM order. Only the overlapping sliver was visible. Wrapping in Portal moves it to the body and out of the header's stacking context, matching what noter's tooltip.tsx already does. Affects every tooltip in the app, not just the header ones. Suggestion pills wrapped to two lines when the text was long, making one pill taller than the rest. The pills now hold a single line at a fixed 42px (matching the loading skeleton) and truncate with an ellipsis, with the full string in `title` — sprint-generated suggestions are arbitrary length, so the copy change alone would not have held. Shortening the default prompts also surfaced a trap: DEFAULT_SUGGESTIONS was declared twice, byte-identical, in suggested-actions.tsx and use-sprint-greeting. Only the hook's copy is reachable, because useSprintGreeting always supplies the fallback through the sprintSuggestions prop — editing the component's copy changed nothing on screen. Collapsed to one exported constant. Verified in the browser at each step: tooltip renders in full on both the sidebar toggle and My Stuff; all four pills sit on one line at equal height. tsc --noEmit clean on the changed files; pnpm test:unit 11/11. apps/chatbot has the same unportaled tooltip.
Reopening a chat whose last message is from the user threw a client
TypeError: Cannot read properties of undefined (reading 'state'), from
AbstractChat.resumeStream in AI SDK 6.0.37.
Root cause (diagnosed via live repro on a chat with one user message and no
assistant reply): chat/[id]/page.tsx enabled autoResume, so useAutoResume
called resumeStream() on mount — but resumable streaming was never
implemented; api/chat/[id]/stream/route.ts is a stub that always returns
204. Overlapping resume attempts (easy under dev double-invoked effects, but
not inherently dev-only) race inside the SDK: one clears activeResponse
while the other reads its .state.
Two changes:
- autoResume={false} on the chat page — honest, since there is nothing to
resume until the stream route is real.
- useAutoResume now guards with a ref so at most one resume attempt fires.
The hook's "we intentionally run this once" comment was aspiration, not
enforcement — the dep array does not guarantee it.
This path matters beyond dev: production has 6 chats with a user message and
no assistant reply (fallout tracked in c031c47), and reopening any of them
walks this exact code path.
Verified: the repro chat now loads with a clean console (previously threw on
every load). tsc --noEmit clean; pnpm test:unit 11/11.
app/(chat)/layout.tsx loaded pyodide from a CDN with strategy "beforeInteractive" — a leftover from the Vercel ai-chatbot template's Python code-runner. Nothing in this app calls it: there is no loadPyodide/runPython reference anywhere, and no code-execution feature. It cost a multi-MB script download on every chat page for nothing. As of Next 16 it is also an error, not just waste: beforeInteractive inside a nested layout renders a raw <script> tag in the React tree, and every page under (chat) logs "Encountered a script tag while rendering React component" (surfaced right after login, at Layout in app/(chat)/layout.tsx:13). Removing the tag fixes the error and the dead download in one move. Verified: chat loads with a clean console after login; tsc --noEmit clean. apps/chatbot has the same tag in its (chat)/layout.tsx — but chatbot may actually use pyodide for its code artifacts, so it needs its own check rather than a blind copy of this deletion.
…page title
Saved sprints were read-only in the narrow My Stuff panel — no way to copy or
export a report short of drag-selecting the text.
- Copy / Download / Expand actions in the sprint detail header. Copy puts the
full report on the clipboard as markdown (toast-confirmed); Download saves it
as <title-slug>.md. Both go through one pure helper, buildSprintMarkdown
(title + summary + report + References), so clipboard and file are
byte-identical. Helper is unit-tested (3 tests, repo test:unit style).
- Expand opens the sprint in a wide centered dialog with the same
Report/Citations/Sources tabs; the tabbed body is extracted into an internal
SprintBody component rendered by both the panel and the dialog.
onOpenAutoFocus is suppressed so the Copy button's tooltip doesn't pop over
the title when the dialog opens.
- Page metadata was still the template's ("Next.js Chatbot Template",
metadataBase chat.vercel.ai) — now "Researcher | Walrus Memory" with the
real domain, on the local branch and prod alike.
Verified in the browser against a real saved sprint: copy toast + clipboard
content (starts "# <title>", full markdown), expanded dialog opens/closes
clean, tab title updated. tsc --noEmit clean; pnpm test:unit 14/14.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the production P0 where every new Researcher chat rendered as a failed response, plus four more defects found while verifying it live on
researcher.demo.memwal.ai, and one UX gap in saved sprints.The P0 (
c031c472)getTitleModel()hardcodedgoogle/gemini-2.0-flash-001, which OpenRouter has retired — it 404s (No endpoints found) for our own production key. The unguardedawait titlePromiseinsidecreateUIMessageStream'sexecute()then surfaced that rejection as anerrorpart on the stream, so the client showed a fatal chat error even though the real answer had streamed fine underneath.TITLE_MODELconstant beside the curated list, pointed atgemini-2.5-flash334cf915) that researcher was missed fromclaude-3.5-haiku/claude-3.5-sonnetalso retired (3 of 6 selectable models were dead) — removed, with a regression test pinning all three retired idsProduction impact when found: 11 of 43 chats untitled; the error path live since the 08-14 deploy.
Also in this branch
754327f4— portalTooltipContent(sticky-header stacking context clipped every tooltip to a 2px sliver); single-line suggestion pills; collapse a duplicate unreachableDEFAULT_SUGGESTIONS39e102ae— stop autoResume firing against the stub stream route (always 204): raced in AI SDK 6.0.37 and threwCannot read properties of undefined (reading 'state')when reopening any reply-less chat9db6db10— delete the template-leftover pyodide<Script>: nothing calls it, multi-MB download per chat page, and a React error under Next 16b9b57890— sprint Copy-as-markdown / Download.md/ expanded reading view (shared unit-testedbuildSprintMarkdown); real page title (Researcher | Walrus Memory) instead of "Next.js Chatbot Template"Verification
tsc --noEmitclean on changed files;pnpm test:unit14/14 (7 new tests)Not in this branch / follow-ups
ec6a9b81afrom the unmergedhotfix/noter-mainnet-rpc-corsbranch — this PR can't reach prod through normal promotion until that's reconciledapps/chatbotshares the dead title model, (probably) the dead Anthropic picker entries, the unportaled tooltip, and the pyodide tag — needs its own branch