diff --git a/.env.example b/.env.example index d8fbcaece..59271288d 100644 --- a/.env.example +++ b/.env.example @@ -54,7 +54,11 @@ OPENAI_STRONG_ANSWER_MODEL=gpt-5.6-sol OPENAI_QUERY_CLASSIFIER_MODEL=gpt-5.6-luna OPENAI_SUMMARY_MODEL=gpt-5.6-terra OPENAI_INDEXING_MODEL=gpt-5.6-terra -OPENAI_MAX_OUTPUT_TOKENS=4000 +# Answer-generation output ceiling (billed per token actually used, not a spend +# commitment). Must stay well above reasoning headroom: the old 4000 starved the +# strong route's reasoning budget and discarded the answer after 60-90s (GEN-C1). +# Matches the env.ts default. +OPENAI_MAX_OUTPUT_TOKENS=16000 OPENAI_QUERY_CACHE_SIZE=200 # Max inputs per embeddings request (OpenAI caps a request at 2048 inputs / ~300k tokens). # embedTexts splits a full-corpus re-embed into batches of this size. @@ -76,7 +80,10 @@ OPENAI_PROMPT_CACHE_TTL=30m #OPENAI_SAFETY_IDENTIFIER_SECRET= OPENAI_STORE_RESPONSES=false OPENAI_FAST_REASONING_EFFORT=low -OPENAI_STRONG_REASONING_EFFORT=high +# "high" overruns OPENAI_ANSWER_TIMEOUT_MS and starves the safety-critical +# medication_dose_risk/table_threshold classes; "medium" is ample for answers +# grounded in retrieved sources and matches the env.ts default. +OPENAI_STRONG_REASONING_EFFORT=medium OPENAI_SUMMARY_REASONING_EFFORT=medium OPENAI_VISION_REASONING_EFFORT=low OPENAI_TEXT_VERBOSITY=low @@ -104,6 +111,12 @@ RAG_AWAIT_QUERY_LOGS=false # Design-exploration mockup routes (/mockups/*) 404 in production builds unless # explicitly opted in. Always reachable in dev/test. #NEXT_PUBLIC_MOCKUPS_ENABLED=false +# Allow unauthenticated visitors to upload documents — gates an anonymous write +# path, so leave unset (off) unless you intend a public-intake workspace. When +# enabled, anonymous uploads are owned by PUBLIC_WORKSPACE_OWNER_ID so that +# owner-scoped reads still apply to them. +#NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED=false +#PUBLIC_WORKSPACE_OWNER_ID= # Optional canonical public origin for generated metadata, for example # https://clinical-kb.example.org. Railway deployments otherwise use the # provider-supplied RAILWAY_PUBLIC_DOMAIN; request hosts are only used in dev. diff --git a/playwright.config.ts b/playwright.config.ts index 27f0c2c5b..38b088b92 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,6 +25,10 @@ export default defineConfig({ // hydration, sub-pixel tap targets) without masking a real regression, which // still fails 3× before it is reported. Local stays at 0 so flakes surface. retries: process.env.CI ? 2 : 0, + // Fail the run if a stray `test.only` is committed: otherwise it silently + // narrows CI to that one test (and skips the whole release matrix) while the + // required check still reports green. + forbidOnly: !!process.env.CI, expect: { timeout: 10_000, }, diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts index 9a4e24ccb..cb3350718 100644 --- a/src/app/api/documents/route.ts +++ b/src/app/api/documents/route.ts @@ -193,11 +193,14 @@ export async function GET(request: Request) { const { data, error, count } = await query; - if (error) throw new Error(error.message); + // An `offset` past the end of the result set makes PostgREST return PGRST103 + // ("Requested range not satisfiable"). That is an empty page, not a server + // error, so return an empty page instead of throwing a 500. + if (error && error.code !== "PGRST103") throw new Error(error.message); // An authenticated caller reads PUBLIC (owner_id IS NULL) documents alongside their own via // withOwnerReadScope. Redact operator-internal storage fields on the rows they do not own so a // shared public document never exposes its owner's storage_path/content_hash/etc. (S1/D1). - const rawDocuments = (data ?? []) as unknown as DocumentListRow[]; + const rawDocuments = (error ? [] : (data ?? [])) as unknown as DocumentListRow[]; const ownedDocumentIds = new Set( rawDocuments.filter((document) => callerOwnsDocumentRow(document, access.ownerId)).map((document) => document.id), ); diff --git a/src/lib/rag.ts b/src/lib/rag.ts index 1c913ed74..a2942d577 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -146,6 +146,7 @@ import { queryTextForStorage, } from "@/lib/query-privacy"; import { normalizeSourceMetadata } from "@/lib/source-metadata"; +import { safeErrorLogDetails } from "@/lib/privacy"; import { isClinicalImageEvidence, normalizeImageBbox } from "@/lib/image-filtering"; import { SOURCE_BACKED_REVIEW_FALLBACK_REASON, @@ -3042,7 +3043,7 @@ export function parseAnswerJson(raw: string, results: SearchResult[], query?: st // GEN-C2 / GEN-H2: numeric faithfulness gate. return enrichGroundedReviewCitations(applyNumericVerification(answer), results); } catch (error) { - console.warn("Failed to parse answer payload, falling back to safe text:", error); + console.warn("Failed to parse answer payload, falling back to safe text:", safeErrorLogDetails(error)); return safeFallbackAnswer(raw, results, query); } }