From 5505d7aa92366b1846b55ae525ed83e7a92a99fa Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 13:28:19 +0900 Subject: [PATCH 1/8] fix(ask): retire answers across authorization changes --- AGENTS.md | 9 ++++++ docs/product-technical-gap-baseline.md | 36 ++++++++++++++++++++++ frontend/src/App.tsx | 37 ++++++++++++++++------- frontend/src/AskAgentPanel.test.tsx | 42 +++++++++++++++++++++++++- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3af4169bc..e378e1c7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -422,3 +422,12 @@ columns). Do not silently rewrite either historical form. The SHACL shapes graph (`docs/ontology/lineageweave-kg-shapes.ttl`) is the closed-world data-validation boundary for DB-to-RDF projections and is published beside the ontology. + +## Authorization-sensitive asynchronous UI + +Bind imperative result, error, and loading completion to the originating +component authorization lifecycle. Token equality alone cannot distinguish +retired A from current A after A → B → A. Clear prior questions, answers, and +evidence selection when credentials change, and verify that a retired success +or failure cannot end the current request's loading state. Keep behavioral +regressions synthetic; UI retirement does not itself cancel a server-side job. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b5d31877b..99ed7f934 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -932,3 +932,39 @@ The ONET rows stacked into base branches (#743/#745/#746/#740/#732) reached `main` together through the #759 promotion; their per-base merge records are historical evidence only. The job-architecture artifact ship originally via #749 is now re-verified on `main` from the promotion. + + +## Ask authorization lifecycle repair (2026-09-07) + +A pending Ask response could enter a later authorization lifecycle after +A → B → A. Deferred success displayed the retired answer; deferred HTTP 403 +displayed the retired transport error. Both were assertion failures, not test +deadlines, in the initial two-case reproduction. + +The component now clears question, cutoff, answer, error, external-verification +selection, loading, and evidence selection when its credential changes. It reuses +the existing component-generation pattern from Customer Master: request success, +failure, and loading completion require both the current credential and the +originating authorization generation. Cleanup retires the generation on token +change or unmount. No global session service, token persistence, API, or schema +is introduced. Server ABAC and ADR 0216's cutoff evidence remain authoritative. + +The regression cases also start a new request after re-entry and require the +retired success/failure to leave that request loading until its own answer arrives. +This is UI admission evidence; it does not establish cancellation of a running +server job or the existing client poll loop. Those transport lifetimes remain a +separate gap, as does current-error copy that can expose transport details. + +The repair is based on protected main `83eba56149eb802cd63642c507c324c9976ec78e` +and is independent of the unmerged translation-ledger foundation in PR #929/#932. +The initial experiment used the PR #932 worktree, then moved only the Ask diff to +an isolated main-based branch; the translation worktree was restored clean. +No real source records, new containers, or deployment were used. + +Validation on the main-based candidate: 2/3 Ask tests passed; the deferred-success +case exceeded its unchanged five-second deadline. The expanded deferred-denial +case and existing cutoff/public-evidence case passed. The earlier narrower +two-case repair run passed 3/3 before adding the current-request loading check; +it is not substituted for the final run. Lint and TypeScript/production build +passed; the existing large-chunk warning remains. The change stays Draft pending +complete verification, independent review, and real-account acceptance. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e3fb6c796..9f6c4e237 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5052,6 +5052,21 @@ export function AskAgentPanel({ const [asking, setAsking] = useState(false); const [verifyExternal, setVerifyExternal] = useState(false); const [evidenceLayerPostId, setEvidenceLayerPostId] = useState(null); + const authGeneration = useRef(0); + const currentAccessTokenRef = useRef(accessToken); + currentAccessTokenRef.current = accessToken; + const [inputAccessToken, setInputAccessToken] = useState(accessToken); + if (inputAccessToken !== accessToken) { + setInputAccessToken(accessToken); + setQuestion(""); + setKnowledgeCutoff(""); + setAnswer(null); + setError(null); + setAsking(false); + setVerifyExternal(false); + setEvidenceLayerPostId(null); + } + useEffect(() => () => { authGeneration.current += 1; }, [accessToken]); const now = new Date(); const localKnowledgeCutoffMax = new Date( now.getTime() - now.getTimezoneOffset() * 60_000, @@ -5068,22 +5083,22 @@ export function AskAgentPanel({ setError(t("Enter a valid knowledge cutoff, then ask again.")); return; } + const requestAuthGeneration = authGeneration.current; + const requestAccessToken = accessToken; setAsking(true); setError(null); try { - setAnswer( - await askAgent( - accessToken, - normalized, - verifyExternal, - cutoff, - ), - ); + const response = await askAgent(accessToken, normalized, verifyExternal, cutoff); + if (requestAccessToken === currentAccessTokenRef.current && requestAuthGeneration === authGeneration.current) { + setAnswer(response); + } } catch (err) { - setAnswer(null); - setError(orchestratorUnavailableMessage(err, t("Ask Agent"))); + if (requestAccessToken === currentAccessTokenRef.current && requestAuthGeneration === authGeneration.current) { + setAnswer(null); + setError(orchestratorUnavailableMessage(err, t("Ask Agent"))); + } } finally { - setAsking(false); + if (requestAccessToken === currentAccessTokenRef.current && requestAuthGeneration === authGeneration.current) setAsking(false); } } diff --git a/frontend/src/AskAgentPanel.test.tsx b/frontend/src/AskAgentPanel.test.tsx index 5eeb0549c..9a6fec095 100644 --- a/frontend/src/AskAgentPanel.test.tsx +++ b/frontend/src/AskAgentPanel.test.tsx @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { render, screen, waitFor, within } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { AskAgentPanel } from "./App"; @@ -8,6 +8,46 @@ describe("AskAgentPanel public verification", () => { vi.unstubAllGlobals(); }); + it.each([200, 403])("discards a retired %s response after credential re-entry", async (status) => { + let finishRetired!: (response: Response) => void; + let finishCurrent!: (response: Response) => void; + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ ask_job_id: "retired-job", job_status_code: "queued" }), { status: 202 })) + .mockImplementationOnce(() => new Promise((resolve) => { finishRetired = resolve; })) + .mockResolvedValueOnce(new Response(JSON.stringify({ ask_job_id: "current-job", job_status_code: "queued" }), { status: 202 })) + .mockImplementationOnce(() => new Promise((resolve) => { finishCurrent = resolve; })); + vi.stubGlobal("fetch", fetchMock); + const onOpenPost = vi.fn(); + const { rerender, container } = render(); + fireEvent.change(screen.getByLabelText("Ask a question"), { target: { value: "Retired question" } }); + fireEvent.click(screen.getByRole("button", { name: "Ask" })); + await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)); + rerender(); + rerender(); + expect(screen.getByLabelText("Ask a question")).toHaveValue(""); + fireEvent.change(screen.getByLabelText("Ask a question"), { target: { value: "Current question" } }); + fireEvent.click(screen.getByRole("button", { name: "Ask" })); + await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(4)); + await act(async () => { + finishRetired(new Response(JSON.stringify({ + ask_job_id: "retired-job", job_status_code: "succeeded", + answer: { answer_text: "Retired answer", cited_post_ids: [], cited_posts: [], + cited_post_evidence: [], source_post_ids: [], external_claims: [], limitations: [] }, + }), { status })); + }); + expect(screen.queryByText("Retired answer")).not.toBeInTheDocument(); + expect(container.querySelector(".error")).toBeNull(); + expect(screen.getByRole("button", { name: "Asking..." })).toBeDisabled(); + await act(async () => { + finishCurrent(new Response(JSON.stringify({ + ask_job_id: "current-job", job_status_code: "succeeded", + answer: { answer_text: "Current answer", cited_post_ids: [], source_post_ids: [] }, + }), { status: 200 })); + }); + expect(screen.getByText("Current answer")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Ask" })).toBeEnabled(); + }); + it("keeps public verification separate and renders cutoff provenance", async () => { const fetchMock = vi .fn() From db541bbe04689c544778b2dea7d91548b4b8bdc0 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 13:33:41 +0900 Subject: [PATCH 2/8] fix(ask): abort retired client polling --- AGENTS.md | 6 ++++ docs/product-technical-gap-baseline.md | 31 +++++++++++++++++++++ frontend/src/App.tsx | 10 +++++-- frontend/src/AskAgentPanel.test.tsx | 15 ++++++++++ frontend/src/api.test.ts | 38 ++++++++++++++++++++++++++ frontend/src/api.ts | 24 ++++++++++++---- 6 files changed, 117 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e378e1c7e..024c31257 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -431,3 +431,9 @@ retired A from current A after A → B → A. Clear prior questions, answers, an evidence selection when credentials change, and verify that a retired success or failure cannot end the current request's loading state. Keep behavioral regressions synthetic; UI retirement does not itself cancel a server-side job. + +Pass a native AbortSignal through client polling and fetch when retiring an +authenticated screen. Preserve the abort reason instead of reporting a network +failure, and clear delay timers/listeners. Result-admission guards remain necessary +for already-resolved work. Verify both transport cancellation and A → B → A +state admission; neither establishes server-job cancellation. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 99ed7f934..cb169893b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -968,3 +968,34 @@ two-case repair run passed 3/3 before adding the current-request loading check; it is not substituted for the final run. Lint and TypeScript/production build passed; the existing large-chunk warning remains. The change stays Draft pending complete verification, independent review, and real-account acceptance. + + +### Ask client transport retirement follow-up (2026-09-07) + +The UI guard alone did not retire transport: cancellation during a running-job +poll delay still caused a third HTTP request, and credential/unmount cleanup +supplied no AbortSignal. Three behavioral assertions reproduced those gaps. + +The existing backendFetch RequestInit now carries a native AbortSignal from +AskAgentPanel through job submission and status reads. Component cleanup aborts +its request controller. The shared polling loop checks cancellation before +submission, before each status request, and after a returned status; delay abort +clears its timer, while normal delay completion removes the listener. The fetch +error boundary preserves the native cancellation reason. Existing generation +checks still reject retired results, errors, and loading completions. + +This closes the client-poll-loop gap recorded above. It does not add a server +job cancellation endpoint or change the existing execution/deadline policy. +Current-error copy can still expose transport details and remains a separate +verified gap. No provider SDK, session store, or dependency was introduced. + +Reference: MDN contributors. (2026, August 27). *AbortSignal: throwIfAborted() +method*. MDN Web Docs. +https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/throwIfAborted + +Final focused validation passed 15/15 tests across the Ask and API files in +3.04 s, including the previously timed-out expanded re-entry success case, +pre-submission abort, in-flight fetch abort, and delay cancellation without another +request. Lint and TypeScript/production build passed with the existing chunk +warning. This supersedes the earlier focused timeout result, not the outstanding +full-suite, hosted, real-account, or page-latency acceptance requirements. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9f6c4e237..2f57b202a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5053,6 +5053,7 @@ export function AskAgentPanel({ const [verifyExternal, setVerifyExternal] = useState(false); const [evidenceLayerPostId, setEvidenceLayerPostId] = useState(null); const authGeneration = useRef(0); + const requestController = useRef(null); const currentAccessTokenRef = useRef(accessToken); currentAccessTokenRef.current = accessToken; const [inputAccessToken, setInputAccessToken] = useState(accessToken); @@ -5066,7 +5067,10 @@ export function AskAgentPanel({ setVerifyExternal(false); setEvidenceLayerPostId(null); } - useEffect(() => () => { authGeneration.current += 1; }, [accessToken]); + useEffect(() => () => { + authGeneration.current += 1; + requestController.current?.abort(); + }, [accessToken]); const now = new Date(); const localKnowledgeCutoffMax = new Date( now.getTime() - now.getTimezoneOffset() * 60_000, @@ -5083,12 +5087,14 @@ export function AskAgentPanel({ setError(t("Enter a valid knowledge cutoff, then ask again.")); return; } + const controller = new AbortController(); + requestController.current = controller; const requestAuthGeneration = authGeneration.current; const requestAccessToken = accessToken; setAsking(true); setError(null); try { - const response = await askAgent(accessToken, normalized, verifyExternal, cutoff); + const response = await askAgent(accessToken, normalized, verifyExternal, cutoff, controller.signal); if (requestAccessToken === currentAccessTokenRef.current && requestAuthGeneration === authGeneration.current) { setAnswer(response); } diff --git a/frontend/src/AskAgentPanel.test.tsx b/frontend/src/AskAgentPanel.test.tsx index 9a6fec095..87fcf61cb 100644 --- a/frontend/src/AskAgentPanel.test.tsx +++ b/frontend/src/AskAgentPanel.test.tsx @@ -8,6 +8,21 @@ describe("AskAgentPanel public verification", () => { vi.unstubAllGlobals(); }); + it.each(["credential", "unmount"])("aborts transport on %s retirement", async (retirement) => { + const fetchMock = vi.fn().mockImplementation(() => new Promise(() => {})); + vi.stubGlobal("fetch", fetchMock); + const props = { onOpenPost: vi.fn() }; + const { rerender, unmount } = render(); + fireEvent.change(screen.getByLabelText("Ask a question"), { target: { value: "Question" } }); + fireEvent.click(screen.getByRole("button", { name: "Ask" })); + const signal = fetchMock.mock.calls[0][1].signal; + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal.aborted).toBe(false); + if (retirement === "unmount") unmount(); + else rerender(); + expect(signal.aborted).toBe(true); + }); + it.each([200, 403])("discards a retired %s response after credential re-entry", async (status) => { let finishRetired!: (response: Response) => void; let finishCurrent!: (response: Response) => void; diff --git a/frontend/src/api.test.ts b/frontend/src/api.test.ts index 9495dc241..4587cf0dd 100644 --- a/frontend/src/api.test.ts +++ b/frontend/src/api.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { BackendError, + askAgent, fetchMe, fetchOccupationRatingSources, fetchOccupationRatings, @@ -11,6 +12,7 @@ import { afterEach(() => { vi.unstubAllGlobals(); + vi.useRealTimers(); }); describe("backendFetch provider-error boundary", () => { @@ -118,3 +120,39 @@ describe("backendFetch provider-error boundary", () => { }); }); }); + + +describe("Ask polling cancellation", () => { + it.each(["before submission", "during fetch"])("preserves cancellation %s", async (moment) => { + const controller = new AbortController(); + const fetchMock = vi.fn().mockImplementation((_url: string, init: RequestInit) => + new Promise((_resolve, reject) => { + init.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true }); + }), + ); + vi.stubGlobal("fetch", fetchMock); + if (moment === "before submission") controller.abort(); + const result = askAgent("token", "Question", false, undefined, controller.signal).catch((error: unknown) => error); + if (moment === "during fetch") controller.abort(); + expect(await result).toBe(controller.signal.reason); + expect(fetchMock).toHaveBeenCalledTimes(moment === "before submission" ? 0 : 1); + }); + + it("stops during the poll delay without another request", async () => { + vi.useFakeTimers(); + const controller = new AbortController(); + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ ask_job_id: "job-1", job_status_code: "queued" }), { status: 202 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ job_status_code: "running" }))) + .mockResolvedValueOnce(new Response(JSON.stringify({ job_status_code: "succeeded", answer: {} }))); + vi.stubGlobal("fetch", fetchMock); + const result = askAgent("token", "Question", false, undefined, controller.signal).catch((error: unknown) => error); + await vi.advanceTimersByTimeAsync(0); + expect(fetchMock).toHaveBeenCalledTimes(2); + controller.abort(); + await vi.advanceTimersByTimeAsync(2000); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(await result).toBe(controller.signal.reason); + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 5db021a05..b56f502d7 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -563,6 +563,7 @@ async function backendFetch( }, }); } catch { + init?.signal?.throwIfAborted(); throw new BackendError(path, 0); } if (!response.ok) { @@ -1369,16 +1370,15 @@ export function optionalKnowledgeCutoffIso(value: string): string | undefined { return parsed.toISOString(); } -/** Submit the question as an asynchronous job and poll it to completion. - * The signature and resolved value are unchanged from the old synchronous - * call, so callers (AskAgentPanel) keep their existing pending/complete - * states without modification. */ +/** Submit and poll an Ask job; aborting retires client I/O, not the server job. */ export async function askAgent( accessToken: string, question: string, verifyExternal = false, knowledgeCutoff?: string, + signal?: AbortSignal, ): Promise { + signal?.throwIfAborted(); const requestBody: { question: string; verify_external: boolean; @@ -1386,15 +1386,19 @@ export async function askAgent( } = { question, verify_external: verifyExternal }; if (knowledgeCutoff) requestBody.knowledge_cutoff = knowledgeCutoff; const submitted = await backendFetch("/api/ask", accessToken, { + signal, method: "POST", body: JSON.stringify(requestBody), }); const deadline = Date.now() + ASK_POLL_CEILING_MS; for (;;) { + signal?.throwIfAborted(); const job = await backendFetch( `/api/ask/jobs/${submitted.ask_job_id}`, accessToken, + { signal }, ); + signal?.throwIfAborted(); if (job.job_status_code === "succeeded" && job.answer) { return job.answer; } @@ -1404,7 +1408,17 @@ export async function askAgent( if (Date.now() > deadline) { throw new Error("Ask Agent timed out waiting for an answer. Try again."); } - await new Promise((resolve) => setTimeout(resolve, ASK_POLL_INTERVAL_MS)); + await new Promise((resolve, reject) => { + const onAbort = () => { + clearTimeout(timer); + reject(signal?.reason); + }; + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(); + }, ASK_POLL_INTERVAL_MS); + signal?.addEventListener("abort", onAbort, { once: true }); + }); } } From 2242ef05cbd9cc61e836d87e1a838e7a8edd2589 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 13:43:30 +0900 Subject: [PATCH 3/8] fix(ui): keep transport errors out of operation guidance --- AGENTS.md | 4 ++++ docs/product-technical-gap-baseline.md | 29 ++++++++++++++++++++++++++ frontend/src/App.tsx | 2 +- frontend/src/AskAgentPanel.test.tsx | 13 ++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 024c31257..208448b45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -437,3 +437,7 @@ authenticated screen. Preserve the abort reason instead of reporting a network failure, and clear delay timers/listeners. Result-admission guards remain necessary for already-resolved work. Verify both transport cancellation and A → B → A state admission; neither establishes server-job cancellation. + +Keep transport exceptions at the diagnostic boundary. Buyer-facing error copy +must not use String(error) or provider detail; reuse existing localized recovery +guidance and preserve explicitly supported status-specific behavior. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index cb169893b..d624bd581 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -999,3 +999,32 @@ pre-submission abort, in-flight fetch abort, and delay cancellation without anot request. Lint and TypeScript/production build passed with the existing chunk warning. This supersedes the earlier focused timeout result, not the outstanding full-suite, hosted, real-account, or page-latency acceptance requirements. + + +### Shared operation-error copy (2026-09-07) + +Ask's current-error leak came from the shared orchestratorUnavailableMessage +fallback used by Chat, Keymen extraction, Evaluation, Commitment derivation, +and Ask. Non-503 errors were stringified into buyer copy. Three negative cases +(network failure, HTTP 403, HTTP 500) reproduced missing recovery guidance and +visible transport details. The fallback now reuses the existing translated +view-recovery sentence; the existing 503 saved-evidence guidance is unchanged. +No second catalog or dependency was introduced. This covers this formatter's +five callers, not every error path in the application. + +The Ask/API run passed 17/18 cases, including all three new error-copy checks; +the existing cutoff/public-verification test exceeded its unchanged deadline. +A subsequent sibling-test invocation failed before test execution because the +Vitest forks worker did not respond. Neither is labeled a green suite. + +Separately, hosted Tests run 34082387676 on ontology PR #959 commit +`96ce3de6190f1f66f140663f034427fd4d78d3a4` passed the frontend's 544 tests across +58 files and its lint/build, plus 1768 backend tests (147 skipped, one warning). +Skipped tests and repository-job success do not prove independent review, +all required central checks, private runtime acceptance, or deployment. + +The correctly selected four existing 503 sibling tests passed in 6.86 s: +Chat, Evaluation, Keymen extraction, and Commitment derivation retained their +saved-evidence guidance. Lint and production build passed; the existing chunk +warning remains. The earlier worker-start failure ran no tests and is not +counted as a sibling assertion failure or success. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2f57b202a..d4f6e1afd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -160,7 +160,7 @@ function orchestratorUnavailableMessage(err: unknown, action: string): string { if (err instanceof BackendError && err.status === 503) { return `${action} ${t("is temporarily unavailable.")} ${t("Saved evidence is still available.")}`; } - return String(err); + return t("This view is unavailable. Refresh once; if it fails again, contact your administrator."); } function LanguageSwitcher({ accessToken }: { accessToken?: string }) { diff --git a/frontend/src/AskAgentPanel.test.tsx b/frontend/src/AskAgentPanel.test.tsx index 87fcf61cb..9a9132709 100644 --- a/frontend/src/AskAgentPanel.test.tsx +++ b/frontend/src/AskAgentPanel.test.tsx @@ -8,6 +8,19 @@ describe("AskAgentPanel public verification", () => { vi.unstubAllGlobals(); }); + it.each([0, 403, 500])("shows recovery guidance instead of transport details for %s", async (status) => { + const fetchMock = status === 0 + ? vi.fn().mockRejectedValue(new Error("synthetic private transport detail")) + : vi.fn().mockResolvedValue(new Response(JSON.stringify({ detail: "synthetic private transport detail" }), { status })); + vi.stubGlobal("fetch", fetchMock); + const { container } = render(); + fireEvent.change(screen.getByLabelText("Ask a question"), { target: { value: "Question" } }); + fireEvent.click(screen.getByRole("button", { name: "Ask" })); + expect(await screen.findByText("This view is unavailable. Refresh once; if it fails again, contact your administrator.")).toBeInTheDocument(); + expect(container.textContent).not.toMatch(/BackendError|HTTP|\/api\/ask|synthetic private/); + expect(screen.getByRole("button", { name: "Ask" })).toBeEnabled(); + }); + it.each(["credential", "unmount"])("aborts transport on %s retirement", async (retirement) => { const fetchMock = vi.fn().mockImplementation(() => new Promise(() => {})); vi.stubGlobal("fetch", fetchMock); From e8487b6f25394aaa005d683547dc7d6253cb6a34 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 14:12:09 +0900 Subject: [PATCH 4/8] docs: record hosted Ask integration evidence --- AGENTS.md | 5 +++++ docs/product-technical-gap-baseline.md | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 208448b45..773eccf4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -386,6 +386,11 @@ exist on the post. ## CI gates +Record hosted results with their exact commit and run URL, including skipped +tests and warnings. Counts from independent PRs are not evidence that their +changes were integrated. Preserve local failures alongside later hosted success; +an evidence-only commit still needs its own current-head required checks. + `.github/workflows/tests.yml` runs the full suite on every PR to `main`. Do not weaken, skip, or `continue-on-error` a failing check -- fix the underlying cause or, for a genuine false positive in a third-party scanner, diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d624bd581..9becc00db 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1028,3 +1028,21 @@ Chat, Evaluation, Keymen extraction, and Commitment derivation retained their saved-evidence guidance. Lint and production build passed; the existing chunk warning remains. The earlier worker-start failure ran no tests and is not counted as a sibling assertion failure or success. + +### Ask hosted integration evidence (2026-09-07) + +[Tests run 34084171460](https://github.com/ContextualWisdomLab/LineageWeave/actions/runs/34084171460) +completed successfully on PR #972 code revision +`2242ef05cbd9cc61e836d87e1a838e7a8edd2589`. The frontend job passed 540 tests +across 58 files, lint, and production build. The backend job passed 1768 tests +with 147 skipped and one warning in 1251.00 s. This is the first full hosted +success recorded here for the combined Ask authorization, transport retirement, +and shared error-copy changes; the earlier local timeout evidence remains above. + +The 540-test result belongs to this Ask revision; the 544-test result above +belongs to the independent ontology revision. Neither test count proves that +the other PR's changes were integrated. Skipped integration cases, independent +review, central required checks, product-owned authentication, eight-language +database resources, deployment, and authenticated all-page p95 remain separate +acceptance obligations. Subsequent documentation commits require their own +current-head checks; this run remains evidence for the cited code revision. From 8e14f11295c0ffeb26a270ee9b7d7b21f5f2c895 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 14:18:15 +0900 Subject: [PATCH 5/8] fix(ask): observe live jobs without a browser deadline --- AGENTS.md | 5 +++ .../0039-global-ask-agent-source-boundary.md | 39 +++++++++++++++++++ docs/product-technical-gap-baseline.md | 19 +++++++++ frontend/src/api.test.ts | 17 ++++++++ frontend/src/api.ts | 13 +------ 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 773eccf4e..81adf8ec9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -443,6 +443,11 @@ failure, and clear delay timers/listeners. Result-admission guards remain necess for already-resolved work. Verify both transport cancellation and A → B → A state admission; neither establishes server-job cancellation. +Do not infer Ask job failure from browser observation age. Queue wait and model +execution are distinct; follow durable terminal status or native cancellation. +Before removing a worker execution deadline, inspect orphan recovery: an +age-only requeue rule can duplicate a still-live computation without claim fencing. + Keep transport exceptions at the diagnostic boundary. Buyer-facing error copy must not use String(error) or provider detail; reuse existing localized recovery guidance and preserve explicitly supported status-specific behavior. diff --git a/docs/adr/0039-global-ask-agent-source-boundary.md b/docs/adr/0039-global-ask-agent-source-boundary.md index 889c0b34f..25dd710fa 100644 --- a/docs/adr/0039-global-ask-agent-source-boundary.md +++ b/docs/adr/0039-global-ask-agent-source-boundary.md @@ -38,3 +38,42 @@ explicit next action; the agent never fabricates an answer or citation. response. - The initial context is bounded to 50 recent rows. Retrieval/reranking is a later upgrade if corpus size or prompt budget requires it. + +## Proposed amendment: client observation lifetime (2026-09-07) + +This amendment remains Proposed pending protected review. It does not change +the Accepted evidence-source decision above. + +### Context and decision drivers + +The client currently abandons a durable Ask job fifteen minutes after +submission, including queue wait. A synthetic deferred-status regression +reproduces abandonment after a queued job becomes running, before its next +succeeded response can be read. Elapsed observation time does not establish +provider failure. The requested model policy has no default application limit. + +### Considered options and proposed outcome + +Retaining or increasing the fixed ceiling bounds browser polling but still +rejects valid work solely because time elapsed. A second browser timeout setting +duplicates policy outside contextual-orchestrator. Instead, continue the existing +two-second polling until a terminal response, transport failure, or native +AbortSignal cancellation. Keep credential-generation admission checks for work +that already resolved when the screen was retired. + +### Consequences and confirmation + +The browser can observe late answers without resubmitting paid work. Polling can +continue indefinitely while a visible screen follows a stranded job; cancellation +on unmount or credential change still retires client I/O, not the server job. +The regression must advance past the former ceiling, observe a nonterminal +status, and then receive the actual completed answer. Existing cancellation +tests must continue passing without timer or listener leaks. + +This is only the client observation decision. The backend's 600-second execution +deadline, answer socket limit, and 660-second age-based orphan recovery remain +an unresolved policy conflict. Removing execution limits requires a separate +worker-liveness and claim-fencing decision so recovery cannot duplicate a live +computation. Preserve ADR 0213's rule against holding pooled database connections +during provider work. No model administrator contract or end-to-end unlimited +execution is established by this amendment. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 9becc00db..5d461c8a6 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1046,3 +1046,22 @@ review, central required checks, product-owned authentication, eight-language database resources, deployment, and authenticated all-page p95 remain separate acceptance obligations. Subsequent documentation commits require their own current-head checks; this run remains evidence for the cited code revision. + +### Ask observation lifetime (2026-09-07; proposed ADR 0039 amendment) + +A synthetic API regression moved the browser clock sixteen minutes forward +after a queued response. The next status was running, with a succeeded answer +available afterward. The client threw its fixed fifteen-minute timeout instead +of fetching that answer: 1 failed, 10 passed before repair. Removing the elapsed +observation ceiling from the shared askAgent poller preserved terminal responses +and native cancellation. The correctly selected API and Ask panel suites passed +19/19 in 11.65 s. An initial selection named a nonexistent panel file and ran +only the API suite (11/11); it is not counted as panel coverage. + +The backend still enforces a 600-second execution deadline and a shorter answer +socket timeout. Recovery requeues running rows after 660 seconds because it +assumes the deadline ended any live computation. Removing that deadline alone +would invalidate the recovery assumption and could duplicate work. Worker +liveness, claim fencing, crash recovery, and the contextual-orchestrator model +administrator contract remain required before claiming the requested default-null +end-to-end model lifetime. No provider or private-runtime call was made here. diff --git a/frontend/src/api.test.ts b/frontend/src/api.test.ts index 4587cf0dd..2f9f6f501 100644 --- a/frontend/src/api.test.ts +++ b/frontend/src/api.test.ts @@ -156,3 +156,20 @@ describe("Ask polling cancellation", () => { expect(vi.getTimerCount()).toBe(0); }); }); + +it("keeps reading a live queued job after fifteen minutes", async () => { + vi.useFakeTimers(); + const startedAt = Date.now(); + const answer = { answer: "Completed source-grounded answer", citations: [] }; + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ ask_job_id: "synthetic-job", job_status_code: "queued" }), { status: 202 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ job_status_code: "queued" }))) + .mockResolvedValueOnce(new Response(JSON.stringify({ job_status_code: "running" }))) + .mockResolvedValueOnce(new Response(JSON.stringify({ job_status_code: "succeeded", answer }))); + vi.stubGlobal("fetch", fetchMock); + const result = askAgent("synthetic-token", "Synthetic question").catch((error: unknown) => error); + await vi.advanceTimersByTimeAsync(0); + vi.setSystemTime(startedAt + 16 * 60 * 1000); + await vi.advanceTimersByTimeAsync(4000); + expect(await result).toEqual(answer); +}); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index b56f502d7..ed8c19653 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1343,15 +1343,8 @@ export function askPostChat(accessToken: string, postId: string, question: strin }); } -/** How often the queued Ask job is polled, and for how long overall. - * A live orchestrator answer can take minutes under shared-gateway load, - * so the ceiling is generous; the poll interval keeps the reader's - * "Thinking..." state honest without hammering the backend. */ +/** Poll durable work until completion or client retirement; queue age is not failure. */ const ASK_POLL_INTERVAL_MS = 2000; -// Must exceed the backend's whole pipeline for one job — queue wait plus -// the 600 s job deadline — and the e2e suite's own answer deadline, so a -// stored answer is never abandoned by the client that asked for it. -const ASK_POLL_CEILING_MS = 15 * 60 * 1000; interface AskJobStatus { ask_job_id: string; @@ -1390,7 +1383,6 @@ export async function askAgent( method: "POST", body: JSON.stringify(requestBody), }); - const deadline = Date.now() + ASK_POLL_CEILING_MS; for (;;) { signal?.throwIfAborted(); const job = await backendFetch( @@ -1405,9 +1397,6 @@ export async function askAgent( if (job.job_status_code === "failed") { throw new Error(job.failure_detail || "Ask Agent could not answer this question."); } - if (Date.now() > deadline) { - throw new Error("Ask Agent timed out waiting for an answer. Try again."); - } await new Promise((resolve, reject) => { const onAbort = () => { clearTimeout(timer); From 40742bcf48858c22ee9726693669391836b9a5ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 14:33:30 +0900 Subject: [PATCH 6/8] fix(ask): preserve cancellation reason during body parsing --- frontend/src/api.test.ts | 25 +++++++++++++++++++++++++ frontend/src/api.ts | 8 +++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/api.test.ts b/frontend/src/api.test.ts index 2f9f6f501..930ec8940 100644 --- a/frontend/src/api.test.ts +++ b/frontend/src/api.test.ts @@ -173,3 +173,28 @@ it("keeps reading a live queued job after fifteen minutes", async () => { await vi.advanceTimersByTimeAsync(4000); expect(await result).toEqual(answer); }); + + +it.each([200, 503])("preserves custom cancellation reason while parsing a %i response body", async (status) => { + const controller = new AbortController(); + const retirement = new Error("authorized screen retired"); + let rejectBody: (reason: unknown) => void = () => undefined; + const body = new Promise((_resolve, reject) => { + rejectBody = reject; + }); + const fetchMock = vi.fn().mockResolvedValue({ + ok: status >= 200 && status < 300, + status, + json: vi.fn().mockImplementation(() => body), + } as unknown as Response); + vi.stubGlobal("fetch", fetchMock); + + const result = askAgent("token", "Question", false, undefined, controller.signal).catch( + (error: unknown) => error, + ); + await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + controller.abort(retirement); + rejectBody(new DOMException("The operation was aborted.", "AbortError")); + + expect(await result).toBe(retirement); +}); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index ed8c19653..896fc9ef3 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -574,11 +574,17 @@ async function backendFetch( detail = body.detail; } } catch { + init?.signal?.throwIfAborted(); detail = undefined; } throw new BackendError(path, response.status, detail); } - return response.json() as Promise; + try { + return (await response.json()) as T; + } catch (error) { + init?.signal?.throwIfAborted(); + throw error; + } } export interface LineageGraphNode { From c25fd4aeb6129f94256959d78624f0303176ae09 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 8 Sep 2026 13:39:33 +0900 Subject: [PATCH 7/8] fix(ask): stop polling unavailable completion responses --- .../0039-global-ask-agent-source-boundary.md | 9 + docs/product-technical-gap-baseline.md | 155 ++++++++++++++++++ docs/storybook-inventory.md | 2 +- frontend/src/AskAgentCutoff.stories.tsx | 23 +++ frontend/src/AskAgentPanel.test.tsx | 16 ++ frontend/src/api.test.ts | 30 ++++ frontend/src/api.ts | 6 +- 7 files changed, 237 insertions(+), 4 deletions(-) diff --git a/docs/adr/0039-global-ask-agent-source-boundary.md b/docs/adr/0039-global-ask-agent-source-boundary.md index 25dd710fa..4801b51f8 100644 --- a/docs/adr/0039-global-ask-agent-source-boundary.md +++ b/docs/adr/0039-global-ask-agent-source-boundary.md @@ -70,6 +70,15 @@ The regression must advance past the former ceiling, observe a nonterminal status, and then receive the actual completed answer. Existing cancellation tests must continue passing without timer or listener leaks. +Only the persisted `queued` and `running` states mean observation should +continue. A `succeeded` response without its answer, a missing/unknown state, +or a null response is unavailable to this reader, not ongoing work. Stop +client polling and reuse the existing localized recovery guidance without +changing the durable job or resubmitting it. A failed response likewise must +not expose its diagnostic detail through the client error. Synthetic regressions +must prove prompt settlement, no remaining polling timer, no extra request, +and restored question controls; valid late answers remain observable. + This is only the client observation decision. The backend's 600-second execution deadline, answer socket limit, and 660-second age-based orphan recovery remain an unresolved policy conflict. Removing execution limits requires a separate diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5d461c8a6..f0c73ca3a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,160 @@ # Product & Technical Gap Baseline +## Current development audit — 2026-09-08 + +This snapshot supersedes earlier queue and completion claims below. ADRs are +normative; PRDs describe product acceptance; research references support only +their stated decisions. Candidate code, local tests, authenticated runtime, +and protected-main delivery are separate evidence classes. No new research, +weight, estimator, failure-detector ratio, or population inference is claimed. + +Protected `main`: `83eba56149eb802cd63642c507c324c9976ec78e`. +The paginated live inventory contains **139 open PRs: 17 Ready and 122 Draft**, +plus **22 open issues**. No inspected Ready head has a qualifying current-head +APPROVE. Required organization rules demand one independent approval, stale +review dismissal, resolved review threads, and seven central review/security +workflows. Existing auto-merge is not a merge SHA or release result. + +### Selected buyer gap and ownership + +Ask can keep its controls disabled forever when a status response says +`succeeded` but omits the answer, or returns an unknown/missing state. This +cycle prioritizes restoring an actionable screen over a new feature: the +broken path prevents the requester from completing the current question. +This is a delivery judgment, not a measured ranking of all product gaps. + +The correction extends the existing #972 owner branch from +`40742bcf48858c22ee9726693669391836b9a5ad`, under the Proposed client-observation +amendment to ADR 0039. Only `queued` and `running` continue observation. +Missing completion evidence stops client polling and shows existing localized +recovery copy; it does not mark the server job failed or submit another job. +A failed response no longer propagates its diagnostic detail as a client error. +Cancellation, authorization generations, cutoff, and valid late answers stay +under the same existing contracts. There is no new API, migration, dependency, +release number, model selection, or arithmetic implementation. + +Six synthetic RED cases reproduced four non-settling observations, leaked +failure detail, and a null-response TypeError. The API suite then passed 19 +tests. The integrated API/panel run still failed because the panel fork did not +start; a separate unchanged panel run also timed out before collecting tests. +Neither failure is erased by the API pass. A threads-pool diagnostic then +collected all nine panel cases: seven passed and two exceeded the unchanged +five-second test deadline (the new missing-answer case and the existing cutoff +case). This is a local failure, not an established environmental root cause. +Build and rendered evidence remain pending at this documentation preparation point. + +### Authority and canonical identity + +Read before implementation: LineageWeave `docs/product-requirements.md`, ADR +0039 and ADRs 0204/0213/0246/0251, plus the linked owner +`contextual-orchestrator/docs/product_planning.md` and `docs/architecture.md`. +Remote repository metadata confirms `ContextualWisdomLab/LineageWeave`, +`RankWeave`, `ThreadWeave`, `TEPP`, and `contextual-orchestrator`. The actual +storage repository is **`ContextualWisdomLab/disksage`**, not `DiskSage`; the +PRD register has a case mismatch. Preserve the #847 PRD-authority owner while +converging that register. No DiskSage runtime integration is inferred. + +The twelve atomic Voice concepts remain ADR 0246 vocabulary. In the inspected +tree, ADR 0251 names the FJA I/O psychology layer; the explicit Voice combination +and temporal history contracts are ADR 0256 and ADR 0252 on #780. Preserve the +user-required extensible atomic composition, evidence-bearing PROV derivation, +truth state, cutoff, distinct carrying/evidence actions, and paged JSON-LD +subject-property union. Do not infer equivalence between these differently +named ADRs. This Ask correction does not establish Voice API/UI acceptance. + +### Exact Ready-head inventory + +Check summaries below were read for the listed heads. Failed and queued jobs +remain distinct; no older run or review is accepted as current success. + +| PR | Exact head | Base | Check observations | +| --- | --- | --- | --- | +| #984 | `e52a63adcc46985a36d23f2ab85757f8e956f895` | `main` | in_progress: 1, queued: 10, skipped: 2, success: 4 | +| #983 | `3b0b5d76d9e0a90c255df740437fee991d0de7cd` | `main` | failure: 1, in_progress: 1, queued: 10, skipped: 2, success: 3 | +| #979 | `6662ea5df6f8f026b54a083677d3e0ee98d2d6d2` | `codex/ask-timeout-attribution-20260907` | success: 2 | +| #974 | `def15fc691d4442c0d82103c1642147b1528d7be` | `main` | cancelled: 1, failure: 5, skipped: 5, success: 24 | +| #973 | `182d3c9d4c5f2a8ab2d63e77b8a9ced663a183f6` | `main` | failure: 7, skipped: 9, success: 28 | +| #972 | `40742bcf48858c22ee9726693669391836b9a5ad` | `main` | failure: 5, skipped: 7, success: 25 | +| #970 | `50807d1e7484fd0aa65bdbf6af015bbe8d739d80` | `main` | failure: 6, skipped: 9, success: 29 | +| #969 | `583157c5d007d4903a78b2e30c0caac224f6460e` | `main` | failure: 6, skipped: 7, success: 24 | +| #966 | `f749fb2714fef5975ba042f9356c10a120f671ac` | `main` | queued: 9, skipped: 5, success: 16 | +| #964 | `1cced397600b15258b36e221a33beb62c4cca4cd` | `main` | failure: 6, skipped: 5, success: 24 | +| #961 | `3bdec0504a65e63f44bd49ba15de37182a1672cc` | `main` | failure: 5, skipped: 9, success: 23 | +| #959 | `96ce3de6190f1f66f140663f034427fd4d78d3a4` | `main` | failure: 6, skipped: 7, success: 24 | +| #929 | `0f4fd26a5f0fcf26932d0945188aefb2143d6605` | `main` | failure: 7, skipped: 4, success: 29 | +| #914 | `61ed3a3712d252e3c179a71d297c52f05e1bac20` | `main` | failure: 5, skipped: 7, success: 25 | +| #911 | `5d40eed35a0b6e0d182397f8d02b29c38e9bdd17` | `main` | failure: 7, skipped: 5, success: 28 | +| #802 | `32f1cda10a2a1a6cabd64a3ae6f59bd6f0b20fd6` | `main` | failure: 7, skipped: 4, success: 28 | +| #780 | `1d8fa267b059289e77301a09985dfac70a439814` | `main` | cancelled: 5, failure: 4, skipped: 11, success: 17 | + +### Collision and stack audit + +Changed-file inventories were collected for all 139 PRs. Seventeen ADR +numbers map to different filenames across those diffs; these are collision +candidates, not seventeen adjudicated semantic conflicts. No different-name +migration-number collision appeared in these diffs; this does not certify +full historical schema compatibility. Competing ADR decisions need normative +comparison and owner-order convergence before merge. + +| ADR number | Owning PR candidates | +| --- | --- | +| 0233 | #672, #811 | +| 0245 | #702, #847 | +| 0272 | #802, #850, #888 | +| 0279 | #811, #857, #888 | +| 0290 | #821, #846 | +| 0291 | #822, #848 | +| 0292 | #823, #849 | +| 0293 | #824, #850 | +| 0294 | #825, #851 | +| 0295 | #826, #852 | +| 0296 | #827, #831, #853 | +| 0297 | #832, #833, #854 | +| 0300 | #837, #857, #899 | +| 0301 | #838, #902 | +| 0305 | #843, #844, #845 | +| 0335 | #876, #877, #878, #891 | +| 0355 | #915, #920 | + +Ready overlaps include `backend/app/main.py` (#979/#974/#929/#914/#911/#780), +`frontend/src/App.tsx` (#972/#969), `.github/workflows/tests.yml` (#983/#911), +and dependency manifests (#973/#970/#929/#911/#802). #961 repairs the existing +2.28 runtime identity while #802 advances the report release; inherited +manifest values cannot decide release order. Full API/schema/release merge +compatibility remains unverified until exact bases and heads converge. + +#979 stays based on #974; #932 stays on #929. Merge parents through protected +main first, then retarget children and recollect evidence. #983 already contains +its reviewed authorization-URL recheck; #911 already contains commit/rollback +SQLSTATE translation. Do not duplicate those corrections. The two observed +in-progress runs belonged to #984 and #983 at their current heads; neither +was stale, and neither was cancelled. No CI stall was manually cleared. + +### Current runtime boundary + +The formal Compose project label is `lineageweave`. The live backend image +declares revision `c47cabed2c2a85d5a3dae2d7d5fb512a04720f18`, which is not the +candidate or current protected main. A real synthetic-account token admitted +`GET /api/me`; this proves authentication for that existing runtime only. +A direct current SQL count observed 43,189 Posts without retrieving identities +or source content. It is a database count, not a probability sample or a +population inference. The authorized workload has not been proved to contain +only synthetic Posts, so the general k6 Ask/read harness was not run against +this mixed-data runtime. Concurrency, latency, error rate, throughput, and +PostgreSQL/worker/Valkey/gateway saturation are **unverified in this cycle**. +Do not change capacity or timeout settings without that evidence. + +Fresh authenticated PostgreSQL Ask and Voice acceptance, candidate deployment, +full-suite success, independent approval, protected merge, and release remain +unavailable. #922 eight-locale publication and Customer Master cutover remain +with #929/#932; #983 retains its observed 100% coverage failure (lines 82.48%, +branches 78.63%) without weakened thresholds. UI recovery screenshots alone +will not complete any authenticated runtime acceptance. + +--- + +## Earlier snapshots (historical only) + > Exact-head loop overlay: 2026-08-29 13:20 KST. Protected `main` is > `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map > explained leftover share, #775). Open ready PRs still lack independent diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index f426285a6..c0167a8e9 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -23,7 +23,7 @@ operator-facing control you can click before changing product CSS. | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | | `Workspace/WorkspaceCalendar` | Read observed Naruon events, or open a commitment to land on that post. Fail-closed copy stays `이 범위의 일정을 아직 받을 수 없습니다`. | `--color-chip-border`, `WorkspaceCalendar`, `EvidenceStatusMark` | | `Ask Agent/Public claim verification` | Compare supported, refuted, and not-enough-information states; open only the external evidence link, then review the separate internal citation before changing governed graph state. | `--space-panel-block`, `--space-control-gap`, `--color-border`, `--size-control-min`, `PublicClaimVerification` | -| `Ask Agent/Knowledge cutoff` | Exercise partial historical grounding, retained-revision provenance, later-live-change disclosure, and the narrow viewport before relying on a historical answer. | Native `datetime-local`, `--space-panel-block`, `--space-control-gap`, `--color-border`, `--size-control-min` | +| `Ask Agent/Knowledge cutoff` | Exercise partial historical grounding, retained-revision provenance, later-live-change disclosure, and the narrow viewport before relying on a historical answer. `CompletedAnswerUnavailable` and `CompletedAnswerUnavailableNarrow` restore question controls and show existing recovery guidance when completion has no answer. | Native `datetime-local`, `--space-panel-block`, `--space-control-gap`, `--color-border`, `--size-control-min` | Repeated web objects must use `frontend/src/styles/tokens.css` and a module under `frontend/src/components/`. Do not add a second Node package manager; diff --git a/frontend/src/AskAgentCutoff.stories.tsx b/frontend/src/AskAgentCutoff.stories.tsx index 7c7e22cef..3e1dc757b 100644 --- a/frontend/src/AskAgentCutoff.stories.tsx +++ b/frontend/src/AskAgentCutoff.stories.tsx @@ -63,3 +63,26 @@ export const NarrowViewport: Story = { ...PartialHistoricalEvidence, globals: { viewport: { value: "mobile1", isRotated: false } }, }; + +export const CompletedAnswerUnavailable: Story = { + beforeEach: () => { + const previousFetch = globalThis.fetch; + globalThis.fetch = async (_input, init) => init?.method === "POST" + ? new Response(JSON.stringify({ ask_job_id: "synthetic-job", job_status_code: "queued" }), { status: 202 }) + : new Response(JSON.stringify({ job_status_code: "succeeded", answer: null })); + return () => { globalThis.fetch = previousFetch; }; + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByLabelText("Ask a question"), "Summarize the synthetic evidence."); + await userEvent.click(canvas.getByRole("button", { name: "Ask" })); + await expect(canvas.findByText("This view is unavailable. Refresh once; if it fails again, contact your administrator.")).resolves.toBeVisible(); + await expect(canvas.getByRole("button", { name: "Ask" })).toBeEnabled(); + await expect(canvas.getByLabelText("Ask a question")).toHaveValue("Summarize the synthetic evidence."); + }, +}; + +export const CompletedAnswerUnavailableNarrow: Story = { + ...CompletedAnswerUnavailable, + globals: { viewport: { value: "mobile1", isRotated: false } }, +}; diff --git a/frontend/src/AskAgentPanel.test.tsx b/frontend/src/AskAgentPanel.test.tsx index 9a9132709..2f931a99f 100644 --- a/frontend/src/AskAgentPanel.test.tsx +++ b/frontend/src/AskAgentPanel.test.tsx @@ -8,6 +8,22 @@ describe("AskAgentPanel public verification", () => { vi.unstubAllGlobals(); }); + it("restores question controls when a completed job has no answer", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ ask_job_id: "synthetic-job", job_status_code: "queued" }), { status: 202 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ job_status_code: "succeeded", answer: null }))); + vi.stubGlobal("fetch", fetchMock); + const { container } = render(); + fireEvent.change(screen.getByLabelText("Ask a question"), { target: { value: "Synthetic question" } }); + fireEvent.click(screen.getByRole("button", { name: "Ask" })); + + expect(await screen.findByText("This view is unavailable. Refresh once; if it fails again, contact your administrator.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Ask" })).toBeEnabled(); + expect(screen.getByLabelText("Ask a question")).toHaveValue("Synthetic question"); + expect(container.textContent).not.toMatch(/TypeError|job_status_code|succeeded/); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + it.each([0, 403, 500])("shows recovery guidance instead of transport details for %s", async (status) => { const fetchMock = status === 0 ? vi.fn().mockRejectedValue(new Error("synthetic private transport detail")) diff --git a/frontend/src/api.test.ts b/frontend/src/api.test.ts index 930ec8940..a551aede4 100644 --- a/frontend/src/api.test.ts +++ b/frontend/src/api.test.ts @@ -198,3 +198,33 @@ it.each([200, 503])("preserves custom cancellation reason while parsing a %i res expect(await result).toBe(retirement); }); + +it.each([ + { job_status_code: "succeeded" }, + { job_status_code: "succeeded", answer: null }, + { job_status_code: "failed", failure_detail: "synthetic private diagnostic" }, + { job_status_code: "synthetic private diagnostic" }, + {}, + null, +])("stops observing a non-progress response without leaking its content: %j", async (job) => { + vi.useFakeTimers(); + const controller = new AbortController(); + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ ask_job_id: "synthetic-job", job_status_code: "queued" }), { status: 202 })) + .mockImplementation(() => Promise.resolve(new Response(JSON.stringify(job)))); + vi.stubGlobal("fetch", fetchMock); + let settled = false; + const result = askAgent("synthetic-token", "Synthetic question", false, undefined, controller.signal) + .catch((error: unknown) => error) + .finally(() => { settled = true; }); + await vi.advanceTimersByTimeAsync(0); + const settledBeforeCancellation = settled; + const pendingTimers = vi.getTimerCount(); + controller.abort(); + + expect(await result).toMatchObject({ message: "Ask Agent could not answer this question." }); + expect(settledBeforeCancellation).toBe(true); + expect(pendingTimers).toBe(0); + await vi.advanceTimersByTimeAsync(4000); + expect(fetchMock).toHaveBeenCalledTimes(2); +}); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 896fc9ef3..9c69a088b 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1397,11 +1397,11 @@ export async function askAgent( { signal }, ); signal?.throwIfAborted(); - if (job.job_status_code === "succeeded" && job.answer) { + if (job?.job_status_code === "succeeded" && job.answer) { return job.answer; } - if (job.job_status_code === "failed") { - throw new Error(job.failure_detail || "Ask Agent could not answer this question."); + if (job?.job_status_code !== "queued" && job?.job_status_code !== "running") { + throw new Error("Ask Agent could not answer this question."); } await new Promise((resolve, reject) => { const onAbort = () => { From e436d9a61e78fc63d5927ce0c9aba825033cb50a Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 8 Sep 2026 13:44:47 +0900 Subject: [PATCH 8/8] docs(gaps): record exact Ask recovery rendering evidence --- .../ask-recovery-20260908/desktop.png | Bin 0 -> 49012 bytes .../evidence/ask-recovery-20260908/mobile.png | Bin 0 -> 35347 bytes docs/product-technical-gap-baseline.md | 18 ++++++++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 docs/evidence/ask-recovery-20260908/desktop.png create mode 100644 docs/evidence/ask-recovery-20260908/mobile.png diff --git a/docs/evidence/ask-recovery-20260908/desktop.png b/docs/evidence/ask-recovery-20260908/desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..e5bab201af7957576e81bd87e894360a979ab155 GIT binary patch literal 49012 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 RgJz|xnrgbLA60Cg{THXxphN%w literal 0 HcmV?d00001 diff --git a/docs/evidence/ask-recovery-20260908/mobile.png b/docs/evidence/ask-recovery-20260908/mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..0e7935284ddc8a83f6cb3b187976fa6b7f66f375 GIT binary patch literal 35347 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 literal 0 HcmV?d00001 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index f0c73ca3a..2bb938d2d 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -41,7 +41,15 @@ Neither failure is erased by the API pass. A threads-pool diagnostic then collected all nine panel cases: seven passed and two exceeded the unchanged five-second test deadline (the new missing-answer case and the existing cutoff case). This is a local failure, not an established environmental root cause. -Build and rendered evidence remain pending at this documentation preparation point. +On implementation commit `c25fd4aeb6129f94256959d78624f0303176ae09`, lint, +TypeScript/product build, and Storybook build succeeded. Chromium executed the +new Storybook interaction at 1440 x 1000 and 390 x 844: recovery guidance was +visible, the question remained editable, the Ask action was enabled and focused, +and neither viewport had horizontal overflow. The synthetic screenshots were +visually inspected: [desktop](evidence/ask-recovery-20260908/desktop.png) and +[mobile](evidence/ask-recovery-20260908/mobile.png). This is component rendering +evidence, not authenticated Ask/PostgreSQL acceptance or a full-suite pass. +The product chunk warning remains (551.98 kB; 162.05 kB gzip). ### Authority and canonical identity @@ -62,7 +70,13 @@ truth state, cutoff, distinct carrying/evidence actions, and paged JSON-LD subject-property union. Do not infer equivalence between these differently named ADRs. This Ask correction does not establish Voice API/UI acceptance. -### Exact Ready-head inventory +### Exact Ready-head inventory (pre-correction queue snapshot) + +After the #972 correction, GraphQL confirmed OPEN at +`c25fd4aeb6129f94256959d78624f0303176ae09`, with zero unresolved threads, +REVIEW_REQUIRED, and queued Tests/central checks. Its previous head in the +table below is historical and cannot satisfy new-head validation. GitHub REST +subsequently exhausted its user rate limit; GraphQL remained available. Check summaries below were read for the listed heads. Failed and queued jobs remain distinct; no older run or review is accepted as current success.