Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "7c6d8a93-fde5-41c8-8cb0-cc0727c14dc9",
"createdOn": "2026-09-04",
"action": "add",
"payload": {
"pri": "P2",
"type": "issue",
"summary": "Answer generation silently gives up and returns the source-only stub, so no written answer and no inline citation marks",
"detail": "Reported live 2026-09-04: the query 'lithium level timing' returned sourceBackedGenerationTimeoutAnswer (rag-extractive-answer.ts:2951) with three citations and answerSections: []. The owner read the absent in-prose source marks as a rendering bug. It is not: resolveClaimMarks (answer-claim-marks.ts:193) matches a rendered sentence against answer.supportedClaims by exact normalised text, the stub's two sentences are not recorded claims, and rag-answer-instructions.ts:49 forbids the model from writing citation digits in prose at all. So no marker parser exists or should exist, and adding one would change nothing on this path. The defect is upstream: no written answer was produced for a query whose corpus evidence was good enough to cite three documents. Same shape as J8SJQ9 (antipsychotic metabolic monitoring), which names the first diagnostic step as establishing whether generation was attempted at all - that run recorded provider_attempted:false for 11 of 30 targeting cases. Owner reports the stub is rare in normal use, so this is scheduled work rather than an incident. Three trigger sites: rag.ts:3121 extractiveNeedsReviewFallback, rag.ts:4169 sourceBackedReviewReason, rag.ts:4200 finalizedFallbackNeedsReview. Do not relax the eval case to hide it.",
"source": "Owner phone report 2026-09-04 during PR #2590 follow-up; screenshot of Answer mode, query 'lithium level timing'",
"issueUlid": "01M1N79ZX3E9F5GWKSCBF87Y3W"
}
}
40 changes: 17 additions & 23 deletions src/app/api/setup-status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AuthenticationError, requireAuthenticatedUser } from "@/lib/supabase/au
import { formatSupabaseUnavailableError, isSupabaseUnavailableError, probeSupabaseHealth } from "@/lib/supabase/health";
import { checkSupabaseProjectConfig, formatSupabaseProjectCheck } from "@/lib/supabase/project";
import { assertSearchSchemaHealth } from "@/lib/validation/row-contracts";
import { readLastEvidencePreviewReason } from "@/lib/answer-preview";
import { describeEvidencePreviewForAnyCaller, describeEvidencePreviewForOperator } from "@/lib/answer-preview";

export const dynamic = "force-dynamic";
export const runtime = "nodejs";
Expand Down Expand Up @@ -303,10 +303,20 @@ const COARSE_SETUP_DETAIL: Record<SetupCheckStatus, string> = {
unknown: "Status unavailable. Operators can see specifics via the health deep probe or server logs.",
};

// `answerPreview` is the one check whose detail survives, and it is REBUILT here rather than
// passed through. It is a diagnostic for the clinician using the app, not operator telemetry:
// blanking it left the person reporting "the sources never appear" reading `Ready.` whatever had
// happened. `describeEvidencePreviewForAnyCaller` assembles its string from a fixed enum and a
// timestamp and nothing else, so rebuilding rather than copying keeps that guarantee even if the
// authorized detail is later widened.
function coarseSetupStatusPayload(payload: SetupStatusPayload): SetupStatusPayload {
return {
...payload,
checks: payload.checks.map((item) => ({ ...item, detail: COARSE_SETUP_DETAIL[item.status] })),
checks: payload.checks.map((item) =>
item.id === "answerPreview"
? { ...item, detail: describeEvidencePreviewForAnyCaller() }
Comment thread
BigSimmo marked this conversation as resolved.
: { ...item, detail: COARSE_SETUP_DETAIL[item.status] },
),
};
}

Expand All @@ -319,27 +329,11 @@ function coarseSetupStatusPayload(payload: SetupStatusPayload): SetupStatusPaylo
* it. One enum and a timestamp; no query, document, owner, or clinical text ever reaches here.
*/
function answerPreviewCheck(): SetupCheck {
if (!env.RAG_INCREMENTAL_EVIDENCE_PREVIEW) {
return check(
"answerPreview",
"Answer wait shows sources",
"needs_setup",
"Disabled by RAG_INCREMENTAL_EVIDENCE_PREVIEW=false; the answer wait will never show source cards.",
);
}
const last = readLastEvidencePreviewReason();
if (!last) {
return check("answerPreview", "Answer wait shows sources", "ready", "Enabled. No answer has been served yet.");
}
// Two reasons mean the rail was drawn, not withheld: the ordinary path, and the fallback
// that rescues an emptied retry intersection. Reading anything but "ok" as a withholding
// would have this diagnostic report a failure at exactly the moment the fallback worked —
// the reading that sent this investigation down the wrong path in the first place.
const delivered = last.reason === "ok" || last.reason === "empty_intersection_relaxed";
const detail = delivered
? `Enabled. The last answer showed its sources (${last.reason}, ${last.at}).`
: `Enabled. The last answer withheld its sources: ${last.reason} (${last.at}).`;
return check("answerPreview", "Answer wait shows sources", "ready", detail);
// The operator form, with the exact timestamp. `coarseSetupStatusPayload` swaps in the bucketed
// form for an anonymous caller — the only difference between the two is the precision of "when",
// never the decision word, so the anonymous reader is not shown a weaker truth.
const status = env.RAG_INCREMENTAL_EVIDENCE_PREVIEW ? "ready" : "needs_setup";
return check("answerPreview", "Answer wait shows sources", status, describeEvidencePreviewForOperator());
}

async function buildSetupStatusPayload(): Promise<SetupStatusPayload> {
Expand Down
2 changes: 1 addition & 1 deletion src/components/clinical-dashboard/answer-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export function NaturalLanguageAnswer({
query?: string;
preformatted?: boolean;
/**
* The Clinical points rail, rendered at the seam between the answer and its
* The Key points rail, rendered at the seam between the answer and its
* sources. Passed in rather than derived here because the surface owns the
* findings and the sheet they open; this component owns only where the seam
* is.
Expand Down
Loading
Loading