From f86004e7f4586d1a0b97bda4bf3c371c68bf40a1 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 03:27:11 +0800 Subject: [PATCH 1/3] docs: record answer stream review --- docs/branch-review-ledger.md | 1 + src/app/api/answer/stream/route.ts | 30 ++++++++++++++++-- tests/private-rag-access.test.ts | 51 +++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 43cd46874..d51dbe0ea 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,6 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | ------------------------------------------------------ | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-17 | codex/harden-answer-streaming | b35b395 | answer-stream SSE cancellation hardening | Fixed a high-confidence cancellation defect: cancelling an SSE response body did not create the signal passed to retrieval/generation, so paid work could continue after the consumer left. The route now aborts both answer and summary work on request abort or stream cancellation and suppresses post-cancellation SSE errors. No further high-confidence defect was found in the changed scope. | `git diff --check` passed. `npm run eval:rag:offline` validated 36 golden fixtures and 21 suites before its Vitest stage was blocked by missing `node_modules/vitest`. Focused Vitest and `verify:cheap` were blocked by absent local dependencies; `verify:pr-local -- --dry-run` selected runtime, format, lint, typecheck, unit, build, and fixture checks. | | 2026-07-13 | codex/lithium-answer-recovery-pr | c5fde11e64d8976e1c163d1b8618f58a52e0b8ff | lithium answer recovery and source governance | Fixed the provider-failure path with a grounded Australian source-backed fallback, a public-safe progress lifecycle, centralised Australian authority/context selection, and fail-closed locality repair. The live audit exposed and the branch fixed hierarchy identity false conflicts plus registry projections entering clinical metadata gates. The corrected audit/backfill found zero proposals, so no production write was made. No high-confidence defect remains. Residual risk is provider latency; live generation timed out but the grounded fallback completed. | `npm run verify:pr-local` passed: 1,988 tests passed/1 skipped, build/client scan, 36 fixtures, and 267 offline RAG tests. After review fixes, `npm run verify:cheap` passed with 1,992 tests passed/1 skipped. `npm run check:production-readiness` passed 5/5. `npm run test:e2e:critical` passed 9/9. `node scripts/run-playwright.mjs tests/answer-progress-ui-smoke.spec.ts --project=chromium` passed 2/2. `node scripts/run-eval-safe.mjs scripts/eval-rag.ts --question "Lithium dosing" --expect-australian --fail-on-threshold --json` exited 0 with one grounded FSH citation and no threshold/safety failures. `npm run audit:source-governance` reported 0 gaps/conflicts/proposals across 2,851 rows. `npm run backfill:source-metadata -- --locality-only` reported 0 changes. `git diff --check` passed. Full `npm run verify:ui` was not rerun after the aggregate runner lost its local server. | | 2026-07-10 | codex/design-ux-review-fixes | 648abfa3f7c91395b5eeca543f70e0b6ea59e9e0 | design-system + UX + design | Five issue groups confirmed; scoped fixes applied in the worktree. | `npm run check:type-scale`; focused Vitest (19/19); `npm run typecheck`; `npm run lint`; `npm run sitemap:check`; browser/API-backed checks awaiting approval | | 2026-07-11 | codex/design-ux-review-integration | 98093ec7b | branch-integration-review | Replayed the reviewed design and UX fixes onto current `origin/main`, preserved the lightweight evidence-panel boundary, and retained the merged quality fixes. | `npm run check:type-scale`; combined focused Vitest (8 files, 42 tests); runtime/action/sitemap/type-scale/lint stages of `verify:cheap`; typecheck blocked by stale worktree dependencies pending hosted clean install; `git diff --check` | diff --git a/src/app/api/answer/stream/route.ts b/src/app/api/answer/stream/route.ts index eff39611a..ee30b0e00 100644 --- a/src/app/api/answer/stream/route.ts +++ b/src/app/api/answer/stream/route.ts @@ -131,6 +131,19 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope const ownerId = accessScope.ownerId; const encoder = new TextEncoder(); const interactionId = randomUUID(); + // A Request signal is normally aborted when the HTTP client disconnects, but + // a ReadableStream consumer can also cancel its body independently. Combine + // both paths so retrieval and generation never continue after either kind of + // cancellation (and do not emit a misleading SSE error after cancellation). + const streamAbortController = new AbortController(); + const abortFromRequest = () => { + if (!streamAbortController.signal.aborted) { + streamAbortController.abort(signal?.reason ?? new DOMException("The request was aborted.", "AbortError")); + } + }; + if (signal?.aborted) abortFromRequest(); + else signal?.addEventListener("abort", abortFromRequest, { once: true }); + const streamSignal = streamAbortController.signal; return new Response( new ReadableStream({ @@ -202,7 +215,7 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope } const answer = body.summaryMode && body.documentId - ? await summarizeDocument(body.documentId, ownerId, { signal }) + ? await summarizeDocument(body.documentId, ownerId, { signal: streamSignal }) : await answerQuestionWithScope({ query: body.query, documentId: singleDocumentScope ? body.documentId : undefined, @@ -214,7 +227,7 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope allowGlobalSearch: !ownerId, queryMode: body.queryMode, onProgress, - signal, + signal: streamSignal, }); const governedResponse = buildGovernedAnswerClientResponse(answer); @@ -239,11 +252,17 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope sendFinal({ ...buildDemoStreamAnswer(body, fallbackReason), interactionId }); return; } - logStreamError(error, signal); + // Cancellation is a terminal client state, not an SSE failure. In + // particular, never enqueue an error after the client has cancelled + // the body: it can race with a new attempt and appear as a duplicate + // visible failure in the browser. + if (streamSignal.aborted) return; + logStreamError(error, streamSignal); const streamError = streamErrorPayload(error); send("error", { error: streamError.message, status: streamError.status, details: streamError.details }); } finally { stopHeartbeat(); + signal?.removeEventListener("abort", abortFromRequest); // The client may have already cancelled the stream (Stop button / // watchdog abort), in which case close() throws on a closed stream. try { @@ -253,6 +272,11 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope } } }, + cancel(reason) { + if (!streamSignal.aborted) { + streamAbortController.abort(reason ?? new DOMException("The stream was cancelled.", "AbortError")); + } + }, }), { headers: { diff --git a/tests/private-rag-access.test.ts b/tests/private-rag-access.test.ts index b548dfada..9aaa1f882 100644 --- a/tests/private-rag-access.test.ts +++ b/tests/private-rag-access.test.ts @@ -94,7 +94,10 @@ function sampleSearchResult() { }; } -function mockRuntime(options: { demoMode?: boolean } = {}) { +function mockRuntime(options: { + demoMode?: boolean; + answerQuestionWithScope?: (args: Record) => Promise; +} = {}) { vi.resetModules(); class MockAuthenticationError extends Error { @@ -146,13 +149,16 @@ function mockRuntime(options: { demoMode?: boolean } = {}) { rrf_top_score: 0.03, }, })); - const answerQuestionWithScope = vi.fn(async () => ({ - answer: "Source-backed answer.", - grounded: true, - confidence: "high", - citations: [], - sources: [], - })); + const answerQuestionWithScope = vi.fn( + options.answerQuestionWithScope ?? + (async () => ({ + answer: "Source-backed answer.", + grounded: true, + confidence: "high", + citations: [], + sources: [], + })), + ); const fetchRelatedDocuments = vi.fn(async () => []); const demoSearch = vi.fn(() => []); const demoAnswer = vi.fn(() => ({ @@ -427,6 +433,35 @@ describe("private RAG API access", () => { ); }); + it("aborts retrieval and generation when the SSE consumer cancels the response body", async () => { + let generationSignal: AbortSignal | undefined; + let resolveStarted: (() => void) | undefined; + const started = new Promise((resolve) => { + resolveStarted = resolve; + }); + const mocks = mockRuntime({ + answerQuestionWithScope: async (args) => { + generationSignal = args.signal as AbortSignal; + resolveStarted?.(); + await new Promise((_resolve, reject) => { + generationSignal?.addEventListener("abort", () => reject(generationSignal?.reason), { once: true }); + }); + }, + }); + const { POST } = await import("../src/app/api/answer/stream/route"); + + const response = await POST(jsonRequest("/api/answer/stream", { query: "clozapine monitoring" })); + const reader = response.body?.getReader(); + expect(reader).toBeDefined(); + await reader?.read(); + await started; + + await reader?.cancel("client navigated away"); + + expect(generationSignal?.aborted).toBe(true); + expect(mocks.answerQuestionWithScope).toHaveBeenCalledTimes(1); + }); + it("silently strips legacy public skipCache from streamed answer requests", async () => { const mocks = mockRuntime(); const { POST } = await import("../src/app/api/answer/stream/route"); From 1b8bd4dbd12c7e74ffe245dfc9ac7308f8b033a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:53:05 +0000 Subject: [PATCH 2/3] docs: record PR policy verification results in branch review ledger --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 7c8d37755..48537553e 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,6 +20,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-17 | PR #757 / codex/harden-high-value-api-route-family | 23b4a73 | PR policy compliance fix + verification | Updated PR description to include required `## Summary`, `## Verification`, `## Clinical Governance Preflight`, and `## Risk and rollout` sections. All 12 focused Vitest tests pass (tests/private-rag-access.test.ts), ESLint clean, TypeScript clean. | Focused Vitest 12/12 (`tests/private-rag-access.test.ts`); `npm run lint` passed; `npm run typecheck` passed. | | 2026-07-17 | codex/harden-answer-streaming | b35b395 | answer-stream SSE cancellation hardening | Fixed a high-confidence cancellation defect: cancelling an SSE response body did not create the signal passed to retrieval/generation, so paid work could continue after the consumer left. The route now aborts both answer and summary work on request abort or stream cancellation and suppresses post-cancellation SSE errors. No further high-confidence defect was found in the changed scope. | `git diff --check` passed. `npm run eval:rag:offline` validated 36 golden fixtures and 21 suites before its Vitest stage was blocked by missing `node_modules/vitest`. Focused Vitest and `verify:cheap` were blocked by absent local dependencies; `verify:pr-local -- --dry-run` selected runtime, format, lint, typecheck, unit, build, and fixture checks. | | 2026-07-17 | PR #635 / claude/github-actions-codex-issue-f4t4s5 | ab09a8d52cc0a8a7e71b37885aaa358aae2522c8 | post-merge merge-readiness review | Already squash-merged to main on 2026-07-14 by BigSimmo. No open review threads or inline comments. CI required checks all green (Change scope, Static PR checks, Safety and config checks, Unit coverage, PR required, Semgrep, Gitleaks, GitGuardian); UI/build/migration jobs correctly skipped. Landed diff is test/guard hardening only for missing `CODEX_TRIGGER_TOKEN` graceful skip. No high-confidence P0-P2 defect. Source branch already deleted. No further merge action needed. | Hosted CI status via `gh pr checks 635` (all required pass); local `node scripts/check-codex-autofix-workflow.mjs` pass; focused Vitest `tests/codex-autofix-workflow.test.ts` 41/41. No OpenAI/Supabase/provider writes. | | 2026-07-17 | PR #718 / codex/performance-latency-remediation-20260717 | b5f509744d4f4bac74d414644cd1802f64b97fa9 | CodeRabbit performance and SQL correctness follow-up | Resolved nine confirmed findings and dispositioned one stale test comment: document downloads revalidate signed URLs on every action; committed-generation filtering precedes detail pagination; enrichment fallback errors preserve identity; caller cancellation leaves the shared classifier flight alive; registry seeding preserves its cache signal; aliases emit canonical corrections; rate-limit success metadata is coherent; ambiguous upserts use named constraints; and grantable default ACLs fail closed. The proxy mock duplicate was not present. No remaining high-confidence P0-P2 defect was found. | Integrated focused Vitest 122/122; post-format Vitest 71/71; `npm run verify:cheap` passed runtime/policy/static guards, ESLint, TypeScript, and 2,684/2,684 tests; focused Prettier and `git diff --check`; disposable Docker replay, regenerated drift manifest, and transactional local SQL probes. No OpenAI calls, live Supabase DDL/migration/data write, deployment, or production mutation ran. | From 02f9c59595bc08a5177257f809f551ad7852b120 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:05:22 +0000 Subject: [PATCH 3/3] fix: add abort guard in test and consistent fallback message in stream route --- src/app/api/answer/stream/route.ts | 4 ++-- tests/private-rag-access.test.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/api/answer/stream/route.ts b/src/app/api/answer/stream/route.ts index 8f80f9080..af911d0f4 100644 --- a/src/app/api/answer/stream/route.ts +++ b/src/app/api/answer/stream/route.ts @@ -139,7 +139,7 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope const streamAbortController = new AbortController(); const abortFromRequest = () => { if (!streamAbortController.signal.aborted) { - streamAbortController.abort(signal?.reason ?? new DOMException("The request was aborted.", "AbortError")); + streamAbortController.abort(signal?.reason ?? new DOMException("The answer stream was aborted.", "AbortError")); } }; if (signal?.aborted) abortFromRequest(); @@ -275,7 +275,7 @@ function streamAnswer(body: AnswerRequestBody, accessScope: RetrievalAccessScope }, cancel(reason) { if (!streamSignal.aborted) { - streamAbortController.abort(reason ?? new DOMException("The stream was cancelled.", "AbortError")); + streamAbortController.abort(reason ?? new DOMException("The answer stream was aborted.", "AbortError")); } }, }), diff --git a/tests/private-rag-access.test.ts b/tests/private-rag-access.test.ts index 8f46b3214..a3457c406 100644 --- a/tests/private-rag-access.test.ts +++ b/tests/private-rag-access.test.ts @@ -445,7 +445,15 @@ describe("private RAG API access", () => { generationSignal = args.signal as AbortSignal; resolveStarted?.(); await new Promise((_resolve, reject) => { - generationSignal?.addEventListener("abort", () => reject(generationSignal?.reason), { once: true }); + if (!generationSignal) { + reject(new Error("generationSignal was not set")); + return; + } + if (generationSignal.aborted) { + reject(generationSignal.reason); + return; + } + generationSignal.addEventListener("abort", () => reject(generationSignal?.reason), { once: true }); }); }, });