fix: stop aborting live answer streams at a flat 60s timeout#466
Conversation
Live answer generation failed with 'Answer generation timed out. Please try again.' whenever the fast answer failed a quality gate and escalated to the strong model: the pipeline (fast <=30s + strong <=30s + optional strong quality repair <=30s, plus retrieval) legitimately exceeds 60s, but the dashboard aborted every answer request at a flat 60s wall clock even while tokens were still streaming. - Replace the flat timer with a stall watchdog: the 60s window now resets on every received stream chunk, so a slow-but-live generation is never aborted mid-stream; a 180s absolute ceiling still bounds runaway requests. Document searches keep the old flat 60s behavior. - Emit SSE comment heartbeats every 15s from /api/answer/stream so silent generation windows (strong-route reasoning before the first output token) stay visibly alive to proxies and the stall detector. - Guard controller.close() against an already-cancelled stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TAXfecBdBBCdrnYcPj5cd
📝 WalkthroughWalkthroughThe answer stream now emits SSE heartbeat frames, guards stream closure after cancellation, and replaces the dashboard’s fixed timeout with an activity-based watchdog supporting stall and maximum-duration limits. Differential search also tracks the active evidence query and updates smoke-test expectations. ChangesAnswer stream liveness
Differential evidence state
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant ClinicalDashboard
participant AnswerStreamRoute
participant StreamController
participant AnswerRequestWatchdog
ClinicalDashboard->>AnswerRequestWatchdog: create watchdog
ClinicalDashboard->>AnswerStreamRoute: request answer stream
AnswerStreamRoute->>StreamController: enqueue tokens, progress, and heartbeats
StreamController-->>ClinicalDashboard: streamed bytes
ClinicalDashboard->>AnswerRequestWatchdog: touch on activity
AnswerRequestWatchdog->>ClinicalDashboard: timeout and abort on stall or max duration
ClinicalDashboard->>AnswerStreamRoute: cancel watchdog in finally
AnswerStreamRoute->>StreamController: close with cancellation guard
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error, 1 warning)
✅ Passed checks (9 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Advisory UI regression check: failures are pre-existing on All three failing Playwright tests reproduce identically on unmodified
They all point at the same thing: on phone-width mode-home pages the shared search renders as the bottom composer dock rather than in the hero, while these tests still assert hero placement — likely fallout from the #456/#464 mobile composer work rather than anything in this diff (which only touches stream timeout/heartbeat logic; 116 other UI tests pass). The job is Generated by Claude Code |
Resolves conflicts with main's stream-boundary payload validation (isAnswerPayload) and executeSearch scope/query-mode threading; the stall-watchdog wiring and SSE heartbeat are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TAXfecBdBBCdrnYcPj5cd
|
Advisory UI regression on the merged head ( After merging
This PR's diff (stream timeout/heartbeat only) doesn't touch either area; 122 other UI tests pass. Both belong to Generated by Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/api/answer/stream/route.ts`:
- Around line 147-149: Update the send function in the SSE stream route to wrap
controller.enqueue in a defensive try/catch, silently ignoring enqueue failures
after the stream is closed or canceled. Preserve the existing encodeSse behavior
for active streams and ensure error handling cannot fail while attempting to
emit an error event.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 04eea357-7f87-4835-be69-62f5c9e462ba
📒 Files selected for processing (6)
src/app/api/answer/stream/route.tssrc/components/ClinicalDashboard.tsxsrc/components/clinical-dashboard/search-utils.tssrc/lib/sse-heartbeat.tstests/clinical-dashboard-search-utils.test.tstests/sse-heartbeat.test.ts
# Conflicts: # docs/branch-review-ledger.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e33a148dc1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # docs/branch-review-ledger.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f29b9488a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/differentials/differentials-home-page.tsx`:
- Line 56: Update the evidenceQuery state flow around setEvidenceQuery so it
uses the repository’s shared query normalizer, not only trimming via normalized.
Apply that same normalization to the active query used by the downstream
evidence gate before string comparison, ensuring casing and repeated whitespace
are handled consistently.
In `@tests/ui-smoke.spec.ts`:
- Around line 1886-1892: Strengthen the request-count assertions around the
history.pushState navigation in the smoke test: retain the initial request
baseline, then assert the navigation produces the expected single request delta
rather than merely any increase. Add an upper-bound or exact-count check so
duplicate requests fail while preserving the existing wait for at least one
initial request.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 55bbffb5-a743-46d8-8ec5-4fdb7e149afb
📒 Files selected for processing (4)
docs/branch-review-ledger.mdsrc/components/ClinicalDashboard.tsxsrc/components/differentials/differentials-home-page.tsxtests/ui-smoke.spec.ts
✅ Files skipped from review due to trivial changes (1)
- docs/branch-review-ledger.md
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/ClinicalDashboard.tsx
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/differentials/differentials-home-page.tsx`:
- Line 56: Update the evidenceQuery state flow around setEvidenceQuery so it
uses the repository’s shared query normalizer, not only trimming via normalized.
Apply that same normalization to the active query used by the downstream
evidence gate before string comparison, ensuring casing and repeated whitespace
are handled consistently.
In `@tests/ui-smoke.spec.ts`:
- Around line 1886-1892: Strengthen the request-count assertions around the
history.pushState navigation in the smoke test: retain the initial request
baseline, then assert the navigation produces the expected single request delta
rather than merely any increase. Add an upper-bound or exact-count check so
duplicate requests fail while preserving the existing wait for at least one
initial request.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 55bbffb5-a743-46d8-8ec5-4fdb7e149afb
📒 Files selected for processing (4)
docs/branch-review-ledger.mdsrc/components/ClinicalDashboard.tsxsrc/components/differentials/differentials-home-page.tsxtests/ui-smoke.spec.ts
✅ Files skipped from review due to trivial changes (1)
- docs/branch-review-ledger.md
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/ClinicalDashboard.tsx
🛑 Comments failed to post (2)
src/components/differentials/differentials-home-page.tsx (1)
56-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the same normalization contract for
evidenceQueryand the active query.
evidenceQueryreceivesnormalized, but that value is only trimmed at Line 35. The repository normalizer also lowercases and collapses repeated whitespace, while the downstream evidence gate uses string equality. Queries such as"acute confusion"can therefore return matches but remain marked as pending. Normalize both sides with the shared helper before storing/comparing this state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/differentials/differentials-home-page.tsx` at line 56, Update the evidenceQuery state flow around setEvidenceQuery so it uses the repository’s shared query normalizer, not only trimming via normalized. Apply that same normalization to the active query used by the downstream evidence gate before string comparison, ensuring casing and repeated whitespace are handled consistently.tests/ui-smoke.spec.ts (1)
1886-1892: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve the duplicate-request regression guard.
toBeGreaterThanOrEqual(1)andtoBeGreaterThan(baselineRequestCount)only prove that requests occurred; they also pass when navigation triggers duplicate requests. Assert the expected exact count/delta, or add an upper-bound assertion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/ui-smoke.spec.ts` around lines 1886 - 1892, Strengthen the request-count assertions around the history.pushState navigation in the smoke test: retain the initial request baseline, then assert the navigation produces the expected single request delta rather than merely any increase. Add an upper-bound or exact-count check so duplicate requests fail while preserving the existing wait for at least one initial request.
Summary
answerRequestTimeoutMs), even while the stream was alive and delivering tokens. The server pipeline legitimately exceeds 60s whenever the fast answer fails a quality gate and escalates: fast generation (≤30sOPENAI_ANSWER_TIMEOUT_MS) + strong retry (≤30s) + optional strong quality repair (≤30s) + retrieval/finalization.createAnswerRequestWatchdoginsearch-utils.ts) — the 60s inactivity window resets on every received stream chunk (progress events, token deltas, heartbeats), so a slow-but-live fast→strong escalation is never aborted mid-stream; a 180s absolute ceiling still bounds runaway requests. Document searches never touch the watchdog and keep the previous flat 60s behavior./api/answer/streamnow emits SSE comment heartbeats (: heartbeat) every 15s via a newsrc/lib/sse-heartbeat.ts, so silent generation windows (strong-route reasoning before the first output token) stay visibly alive to proxies and to the client's stall detector. Also guardscontroller.close()against an already-cancelled stream.Verification
npm run verify:pr-localDuring development, use
npm run verify:cheapas the faster iteration gate before the final PR-local preflight.npm run verify:cheap— check:runtime, check:github-actions, sitemap:check, lint, typecheck, vitest all green (155 files, 1445 tests passed)tests/clinical-dashboard-search-utils.test.ts,tests/sse-heartbeat.test.ts,tests/private-access-routes.test.ts,tests/private-rag-access.test.ts,tests/api-route-coverage.test.ts,tests/architecture-boundaries.test.tsnpm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changednpm run verify:releasebefore release or handoff confidence claimsnpm run eval:retrieval:quality— not run: no retrieval, ranking, selection, chunking, or scoring behavior changed (transport/timeout handling only)npm run eval:rag -- --limit 15+npm run eval:quality -- --rag-only— not run: the synthesis prompt, generation routing, and answer post-processing are unchanged; only stream liveness and client abort behavior changed, and these evals need live keys unavailable in this environmentnpm run check:production-readiness— only failures are missing Supabase/OpenAI secrets, expected in the sandbox per repo Cloud Agent notesnpm run check:deployment-readiness— deployment startup/hosting behavior unchangedClinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy) — no env or project changesNotes
🤖 Generated with Claude Code
https://claude.ai/code/session_017TAXfecBdBBCdrnYcPj5cd
Generated by Claude Code