Skip to content
Closed
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
8 changes: 4 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ export default defineConfig({
screenshot: "only-on-failure",
// Disable CSS/web animations suite-wide so a click can't land mid-transition
// on a moving target (documented races in ui-stress/ui-smoke). The dedicated
// reduced-motion a11y spec emulates it per-test too, so it is unaffected.
contextOptions: {
reducedMotion: "reduce",
},
// reduced-motion a11y spec emulates a per-test mode, so suite-wide settings
// remain stable across builds.
contextOptions: { reducedMotion: "reduce" },
},
projects: [
{
Expand All @@ -56,6 +55,7 @@ export default defineConfig({
grepInvert: mockupTag,
use: {
...devices["Desktop Chrome"],
contextOptions: { reducedMotion: "no-preference" },
Comment thread
BigSimmo marked this conversation as resolved.
...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}),
},
},
Expand Down
7 changes: 1 addition & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { HomePageClient } from "@/app/home-page-client";
import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes";

type HomeProps = {
searchParams?: Promise<{
mode?: string | string[];
q?: string | string[];
focus?: string | string[];
run?: string | string[];
}>;
searchParams?: Promise<Record<string, string | string[] | undefined>>;
};

function firstSearchParam(value: string | string[] | undefined) {
Expand Down
10 changes: 5 additions & 5 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3492,7 +3492,7 @@ export function ClinicalDashboard({
: "mb-0",
)}
>
<h1 className="sr-only">Clinical Guide</h1>
<h1 className="sr-only">Clinical KB</h1>
{privateScopeStatus === "unavailable" ? (
// Lives inside <main> (not as a header sibling): in the answer view
// the header is absolute, so a sibling alert would reflow to the
Expand Down Expand Up @@ -3716,6 +3716,10 @@ export function ClinicalDashboard({
</div>
) : null)}

{showUniversalAlsoMatches && activeModeResultKind !== "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}

{activeModeResultKind === "differentials" ? (
<DifferentialsHome
query={query}
Expand Down Expand Up @@ -3876,10 +3880,6 @@ export function ClinicalDashboard({
/>
) : null}

{showUniversalAlsoMatches && activeModeResultKind !== "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}

{showUniversalAlsoMatches && activeModeResultKind === "answer" ? (
<UniversalSearchAlsoMatches modeId={searchMode} query={universalAlsoMatchesQuery} />
) : null}
Expand Down
32 changes: 19 additions & 13 deletions src/components/forms/forms-search-results-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -669,14 +669,16 @@ function FormsSearchResultsPageContent({ query }: FormsSearchResultsPageProps) {
</div>
<div className="flex items-center gap-2 pb-1.5">
<ResultSortControl value={sortValue} onChange={setSortValue} className="md:hidden" />
<RefineBar
open={refineOpen}
onToggle={() => setRefineOpen((open) => !open)}
panelId={refinePanelId}
/>
{!registry.demoMode ? (
Comment thread
BigSimmo marked this conversation as resolved.
<RefineBar
open={refineOpen}
onToggle={() => setRefineOpen((open) => !open)}
panelId={refinePanelId}
/>
) : null}
</div>
</div>
<RefinePanel open={refineOpen} panelId={refinePanelId} />
{registry.demoMode ? null : <RefinePanel open={refineOpen} panelId={refinePanelId} />}
<div className="hidden md:block">
<ResultsTable matches={displayedMatches} query={query} sortValue={sortValue} />
</div>
Expand All @@ -688,13 +690,17 @@ function FormsSearchResultsPageContent({ query }: FormsSearchResultsPageProps) {
<UniversalSearchAlsoMatches modeId="forms" query={query} />
</>
) : null}
<div className="hidden lg:block">
<PathwayPanel />
</div>
<div className="lg:hidden">
<MobilePathway />
</div>
{registryReady ? <VerifiedFooter /> : null}
{registryReady && !registry.demoMode ? (
<>
<div className="hidden lg:block">
<PathwayPanel />
</div>
<div className="lg:hidden">
<MobilePathway />
</div>
<VerifiedFooter />
</>
) : null}
</main>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/route-error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function RouteErrorBoundary({
<h1
ref={headingRef}
tabIndex={-1}
className="mt-4 text-lg font-semibold tracking-tight text-[color:var(--text-heading)] focus:outline focus:outline-2 focus:outline-offset-2 focus:outline-[color:var(--focus-ring,Highlight)]"
className="mt-4 text-lg font-semibold tracking-tight text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus-ring,Highlight)]"
>
{title}
</h1>
Expand Down
24 changes: 19 additions & 5 deletions src/lib/rag-candidate-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ import type { DocumentIndexUnitMatch, DocumentMemoryCard, SearchResult } from "@
// the floor, which is how the live schema drift (42702) went unnoticed. Log it structurally and,
// where telemetry is in scope, record the failing RPC + code so it shows up in rag_retrieval_logs.
export type SupabaseRpcError = { message?: string; code?: string; details?: string; hint?: string } | null;
type RpcResult<T> = Promise<{ data: T | null; error: SupabaseRpcError }>;
type AbortableRpc<T> = RpcResult<T> & {
abortSignal?: (signal: AbortSignal) => RpcResult<T>;
};
type SupabaseRpcClient = {
rpc: (name: string, rpcArgs: Record<string, unknown>) => AbortableRpc<unknown[]> | PromiseLike<unknown>;
};

function legacyRankFields(versionedName: string) {
if (versionedName === "match_document_chunks_v2") return ["similarity"];
Expand Down Expand Up @@ -81,15 +88,21 @@ export async function callVersionedRetrievalRpc<T extends unknown[] = unknown[]>
versionedName: string,
legacyName: string,
args: Record<string, unknown>,
signal?: AbortSignal,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass caller abort signals into retrieval RPCs

This new signal parameter is never supplied by the production retrieval path: a repo-wide check of callVersionedRetrievalRpc call sites shows the searchChunksWithTelemetry/candidate-source callers still pass only the RPC args and their helper argument types do not carry signal. When a user hits Stop after retrieval starts, the code still waits for outstanding Supabase RPCs instead of aborting them, so the cancellation fix only covers the classifier boundary; thread args.signal through the candidate helpers and direct RPC calls so this hook is effective.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Testing

  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm ci
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run typecheck
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run test -- tests/retrieval-selection.test.ts tests/rag-routing.test.ts tests/rag-offline-contract.test.ts
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run eval:rag:offline
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run verify:cheap — failed in unrelated tests/pdf-extraction-budget.test.ts child-process deadline expectation after lint/typecheck and the broader Vitest suite started successfully.
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run test -- tests/pdf-extraction-budget.test.ts — reproduced the same unrelated PDF child-process deadline failure.

View task →

): Promise<{ data: T | null; error: SupabaseRpcError }> {
const client = supabase as unknown as {
rpc: (name: string, rpcArgs: Record<string, unknown>) => Promise<{ data: T | null; error: SupabaseRpcError }>;
const client = supabase as unknown as SupabaseRpcClient;
const executeRpc = async (name: string, rpcArgs: Record<string, unknown>) => {
const pending = client.rpc(name, rpcArgs) as AbortableRpc<T>;
const pendingWithAbort =
signal && typeof pending.abortSignal === "function" ? pending.abortSignal(signal) : pending;
return await pendingWithAbort;
};
Comment thread
BigSimmo marked this conversation as resolved.
const versioned = await client.rpc(versionedName, args);
const versioned = await executeRpc(versionedName, args);
if (versioned && !isMissingRetrievalRpcError(versioned.error)) return versioned;
if (signal?.aborted) return versioned;
const legacyArgs = { ...args };
delete legacyArgs.include_public;
const ownerResult = await client.rpc(legacyName, legacyArgs);
const ownerResult = await executeRpc(legacyName, legacyArgs);
const ownerFilter = String(args.owner_filter ?? "");
if (
ownerResult.error ||
Expand All @@ -99,7 +112,8 @@ export async function callVersionedRetrievalRpc<T extends unknown[] = unknown[]>
) {
return ownerResult;
}
const publicResult = await client.rpc(legacyName, {
if (signal?.aborted) return ownerResult;
const publicResult = await executeRpc(legacyName, {
...legacyArgs,
owner_filter: PUBLIC_OWNER_FILTER_SENTINEL,
});
Expand Down
52 changes: 48 additions & 4 deletions src/lib/rag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createAdminClient } from "@/lib/supabase/admin";
import { retrievalAccessScopeForArgs, retrievalRpcScopeArgs } from "@/lib/owner-scope";
import {
callVersionedRetrievalRpc,
createChunkLoadCache,
memoryCardChunkScore,
mergeSearchResults,
recordHybridRpcError,
Expand All @@ -11,7 +12,6 @@ import {
searchTableFactCandidates,
searchTextChunkCandidates,
withMemoryBoostedCandidates,
createChunkLoadCache,
type MemoryCardCache,
} from "@/lib/rag-candidate-sources";
export {
Expand Down Expand Up @@ -120,6 +120,11 @@ export {
retrievalPlanCacheQuery,
} from "@/lib/rag-cache";
import { classifySearchCacheOutcome, recordCacheLookup } from "@/lib/observability/cache-metrics";
import {
recordAnswerOrigination,
recordAnswerOriginationFinished,
recordCoalescedAnswerWaiter,
} from "@/lib/observability/answer-coalescing-metrics";
import { buildRagSourceBlock, compactContextText, neutralizeIdentityField } from "@/lib/rag-source-block";
export { buildRagSourceBlock, truncateForModel } from "@/lib/rag-source-block";
import {
Expand Down Expand Up @@ -423,6 +428,26 @@ function throwIfAborted(signal?: AbortSignal) {
}
}

function awaitWithCallerSignal<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return pending;
if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");

return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
signal.addEventListener("abort", onAbort, { once: true });
pending.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(error) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}

export type AnswerProgressEvent = {
stage:
| "retrieved"
Expand Down Expand Up @@ -1290,6 +1315,7 @@ export async function analyzeQueryWithClassifierFallback(
// owner_filter retrieval will use so grounding can never see documents retrieval cannot.
corpusGrounding?: { supabase: ReturnType<typeof createAdminClient>; ownerFilter: string | null };
ownerId?: string | null;
signal?: AbortSignal;
},
) {
if (
Expand Down Expand Up @@ -1361,10 +1387,16 @@ export async function analyzeQueryWithClassifierFallback(
}

try {
const verdict = await pending;
const verdict = await awaitWithCallerSignal(pending, opts?.signal);
storeClassifierVerdictMemo(memoKey, verdict);
return applyClassifierVerdict(analysis, verdict);
} catch {
} catch (error) {
if (
error &&
(error instanceof DOMException || typeof error === "object") &&
(error as { name?: string }).name === "AbortError"
)
throw error;
// Transport/parse failures are deliberately NOT memoized: fall back to the deterministic
// analysis for this request only, and let the next request retry the classifier.
return analysis;
Expand Down Expand Up @@ -2398,6 +2430,7 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
const queryAnalysis = await analyzeQueryWithClassifierFallback(retrievalQuery, analyzeClinicalQuery(retrievalQuery), {
corpusGrounding: corpusGroundingScope,
ownerId: args.ownerId,
signal: args.signal,
});
throwIfAborted(args.signal);
if (modeQueryClass) queryAnalysis.queryClass = modeQueryClass;
Expand Down Expand Up @@ -2813,6 +2846,7 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
document_filters: documentFilterList ?? undefined,
...retrievalRpcScopeArgs(retrievalAccessScopeForArgs(args)),
},
args.signal,
);
return { data, error, latencyMs: Date.now() - startedAt };
})(),
Expand Down Expand Up @@ -2920,12 +2954,14 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) {
document_filter: documentFilter ?? undefined,
...retrievalRpcScopeArgs(retrievalAccessScopeForArgs(args)),
},
args.signal,
);

if (error) throw new Error(error.message);
return (data ?? []) as SearchResult[];
}),
).catch((error) => {
if (args.signal?.aborted) throw error;
if (!args.forceEmbedding && textFastResults.length > 0) return [] as SearchResult[][];
throw error;
});
Expand Down Expand Up @@ -3123,6 +3159,7 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs)
let existing = inflightKey ? answerInflight.get(inflightKey) : undefined;

while (existing) {
recordCoalescedAnswerWaiter();
await args.onProgress?.({
stage: "cached",
message: "Waiting for an identical cited answer request already in progress.",
Expand Down Expand Up @@ -3154,8 +3191,15 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs)
}
}

// Only coalescible requests belong in this process-local signal. Requests
// that intentionally bypass cache/coalescing must not make a replica look
// ineffective, and neither keys nor clinical content leave this function.
if (inflightKey) recordAnswerOrigination();
const pending = answerQuestionWithScopeUncoalesced(args, startedAt).finally(() => {
if (inflightKey) answerInflight.delete(inflightKey);
if (inflightKey) {
answerInflight.delete(inflightKey);
recordAnswerOriginationFinished();
}
});
if (inflightKey) answerInflight.set(inflightKey, pending);
return pending;
Expand Down
4 changes: 2 additions & 2 deletions supabase/drift-manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"generated_at": "2026-07-18T08:58:41.495Z",
"generated_at": "2026-07-17T22:10:14.366Z",
"generator": "scripts/generate-drift-manifest.ts",
"postgres_image": "supabase/postgres:17.6.1.127",
"schema_sha256": "302a9ecec8e69564d50035be8480fa5e5686ca6136d96978f2188ecf5fb58be1",
"replay_seconds": 18,
"replay_seconds": 13,
"snapshot": {
"views": [
{
Expand Down
Loading
Loading