diff --git a/.env.example b/.env.example index 054ab4eabc..c8ad634da0 100644 --- a/.env.example +++ b/.env.example @@ -270,6 +270,10 @@ CHUNK_STRATEGY=page WORKER_POLL_MS=30000 WORKER_BATCH_SIZE=3 WORKER_CONCURRENCY=1 +# Optional comma-separated claim allowlists; both must match when set. +# Requires the scoped claim RPC migration; blank means unrestricted. +WORKER_ALLOWED_DOCUMENT_IDS= +WORKER_ALLOWED_FILE_TYPES= WORKER_MAX_ATTEMPTS=3 WORKER_STALE_AFTER_MINUTES=45 WORKER_HEALTH_BACKOFF_MS=120000 diff --git a/docs/branch-review-records/98cae095170833559454abb42fc538e3a295a1536724bbf956e26b442827879b.record.md b/docs/branch-review-records/98cae095170833559454abb42fc538e3a295a1536724bbf956e26b442827879b.record.md new file mode 100644 index 0000000000..7c066139de --- /dev/null +++ b/docs/branch-review-records/98cae095170833559454abb42fc538e3a295a1536724bbf956e26b442827879b.record.md @@ -0,0 +1 @@ +| 2026-10-17 | HEAD | 0537cc935b86824db40ca0c117894b67638e2d95 | PR 3628 review and fixes | Found atomic worker-scope, comparator-unit, telemetry-budget, and document-source retention defects; fixes prepared with regression coverage. | Independent standards and specification inspection of the pinned PR diff; follow-up working-tree targeted proof 295/295 tests and 10 local SQL scenarios; hosted CI pending. | diff --git a/docs/clinical-hazard-controls.json b/docs/clinical-hazard-controls.json index 7d566a85ba..2f03865882 100644 --- a/docs/clinical-hazard-controls.json +++ b/docs/clinical-hazard-controls.json @@ -198,6 +198,13 @@ "recordedOn": "2026-10-14", "expiresOn": "2026-10-21" }, + { + "path": "tests/answer-verification.test.ts", + "reason": "PR #3628 adds regression tests for the 'no more than' comparator/unit parsing in src/lib/answer-verification.ts (itself under the existing PR #3546 drift exception). Engineering drift exception per owner ruling 2026-10-18: the owner will review and reseal this test file before this exception expires. Not a clinical review, sign-off or reseal.", + "recordedBy": "Engineering, Grok Bot sweep session for PR #3628, per owner ruling 2026-10-18. Engineering drift record only; not clinical-owner approval or release signoff.", + "recordedOn": "2026-10-18", + "expiresOn": "2026-12-02" + }, { "path": "src/lib/clinical-safety.ts", "reason": "Drift predates the blocking gate. Safety-behaviour changes since the 2026-08-23 review (e.g. 41b5a025a escalate/monitoring matching, 64db243a9 stop-instruction labelling, 1aa717e88 no all-clear over a failed medication check) need clinical re-review, then a re-seal.", diff --git a/docs/worker-deploy-runbook.md b/docs/worker-deploy-runbook.md index 58a542a5ec..a107d9a68a 100644 --- a/docs/worker-deploy-runbook.md +++ b/docs/worker-deploy-runbook.md @@ -661,3 +661,22 @@ Redeploy the previous image tag. The worker holds no durable local state; all artifact writes are idempotent per generation/chunk-key, and completion is gated by the strict completion RPCs plus the edge agent, so a redeploy costs at most one stale window of latency on any in-flight job and zero data loss. + +### Scoped Node worker claims + +`WORKER_ALLOWED_DOCUMENT_IDS` is a comma-separated UUID allowlist; +`WORKER_ALLOWED_FILE_TYPES` is a comma-separated MIME-type allowlist (for example, +`text/plain,application/pdf`). Blank lists leave that dimension unrestricted; a +non-blank list with no valid entries (for example `,`), or any document ID that is not +a UUID, claims no jobs at all and logs the configuration error once; +when both lists are set, a job must satisfy both. MIME types are case-insensitive +and ignore parameters such as `charset`. UUIDs are normalized to lowercase. + +The worker passes active lists to the five-argument `claim_ingestion_jobs` overload. +The database filters candidates before acquiring leases, then rechecks the locked +document's MIME type. Unrelated queued jobs neither block scoped jobs nor consume +attempts. Existing workers with no scope keep using the three-argument contract. +A scoped worker requires the scope migration; a missing overload fails closed +rather than falling back to an unfiltered claim. The migration changes the live +database when merged to `main`: merge only inside the owner's specifically +approved database-change window. No live database change is authorized by this PR. diff --git a/scripts/claude-session-start.mjs b/scripts/claude-session-start.mjs index 971c954729..70da79791b 100644 --- a/scripts/claude-session-start.mjs +++ b/scripts/claude-session-start.mjs @@ -55,7 +55,10 @@ for (const [label, command, args, timeout] of steps) { const result = spawnSync(command, args, { cwd: repoRoot, input: payload, encoding: "utf8", timeout }); collect(result.stdout || "", label === "cloud setup"); if (result.stderr) process.stderr.write(result.stderr); - if (result.status !== 0 || result.error) + // A dependent that exits 0 without reading stdin races the payload write and leaves + // EPIPE in result.error; it still completed, so only other errors count as failure. + const failed = result.status !== 0 || (result.error && result.error.code !== "EPIPE"); + if (failed) contexts.push(`SessionStart ${label} did not complete; check its diagnostics before trusting related checks.`); } if (contexts.length) diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 402aa6d805..9dcdd6b5c9 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -15,6 +15,7 @@ import { import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { documentDisplayTitle } from "@/components/DocumentOrganizationBadges"; import { ContextualBackLink } from "@/components/contextual-back-link"; +import { documentFileKindFromType } from "@/components/clinical-dashboard/document-ui"; import { useActiveScrollOwner } from "@/components/clinical-dashboard/use-active-scroll-owner"; import { PhoneHeaderCollapsePortal } from "@/components/clinical-dashboard/phone-header-collapse-portal"; import { useDocumentViewerChromeScroll } from "@/components/clinical-dashboard/use-document-viewer-chrome-scroll"; @@ -1010,6 +1011,7 @@ export function DocumentViewer({ const generatedAnswerIsSummary = summaryQuery === documentSummaryQuestion; const storedSummaryText = document?.summary?.summary ?? null; const documentLabels = document?.labels; + const viewerFileKind = documentFileKindFromType(document?.file_type, document?.file_name, "PDF"); const formattedStoredSummary = useMemo(() => formatDocumentSummary(storedSummaryText), [storedSummaryText]); const summaryBadges = useMemo( () => buildDocumentSummaryBadges({ labels: documentLabels, summaryText: storedSummaryText }), @@ -1332,7 +1334,7 @@ export function DocumentViewer({ - Open original PDF + {`Open original ${viewerFileKind}`} ) : ( )} {canUseAdministrativeApis ? ( @@ -1476,7 +1478,7 @@ export function DocumentViewer({ src={previewFrameSource} controls={pdfFrameControls} {...(effectiveLoadingDocument - ? { state: "loading" as const, loadingLabel: "Preparing PDF preview" } + ? { state: "loading" as const, loadingLabel: `Preparing ${viewerFileKind} preview` } : effectiveViewerError || previewError ? { state: "error" as const, @@ -1488,7 +1490,7 @@ export function DocumentViewer({ effectiveLoadingDocument ? ( ) : undefined @@ -1498,7 +1500,7 @@ export function DocumentViewer({ {signedUrl ? ( ) : null} {downloadSignedUrl ? ( @@ -1513,7 +1515,7 @@ export function DocumentViewer({ ) : (