From 9a5f230d2f99a3351317f4333ba6d3bea349ea25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:33:05 +0000 Subject: [PATCH 1/2] fix: reuse shared Global Ask session storage key acceptAnswer and the 409 stale-citation restart wrote or cleared a string-literal sessionStorage key while bootstrap, 404 retry, and logout already used GLOBAL_ASK_SESSION_STORAGE_KEY. A later key rename would desynchronize recovery. Export the constant, use it at every site, and prove a 409 restart rewrites the shared key. --- CHANGELOG.d/2.20.2-ask-session-storage-key.md | 4 ++ CHANGELOG.md | 8 ++++ ...3-project-history-links-in-ask-surfaces.md | 4 +- docs/product-technical-gap-baseline.md | 4 +- frontend/package.json | 2 +- frontend/src/App.test.tsx | 37 +++++++++++++++++-- frontend/src/App.tsx | 6 +-- lineageweave/__init__.py | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 10 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 CHANGELOG.d/2.20.2-ask-session-storage-key.md diff --git a/CHANGELOG.d/2.20.2-ask-session-storage-key.md b/CHANGELOG.d/2.20.2-ask-session-storage-key.md new file mode 100644 index 000000000..eb3197e68 --- /dev/null +++ b/CHANGELOG.d/2.20.2-ask-session-storage-key.md @@ -0,0 +1,4 @@ +### Fixed + +- Use one shared Global Ask `sessionStorage` key for bootstrap, persist, 404 + retry, 409 restart, and logout. diff --git a/CHANGELOG.md b/CHANGELOG.md index 89cf4bbef..e329b3efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.20.2] - 2026-08-21 + +### Fixed + +- Global Ask now reads, writes, and clears one shared `sessionStorage` key for + bootstrap, successful answers, 404 retry, 409 stale-citation restart, and + logout, so a restart cannot leave a desynchronized session id (ADR 0113). + ## [2.20.1] - 2026-08-21 ### Fixed diff --git a/docs/adr/0113-project-history-links-in-ask-surfaces.md b/docs/adr/0113-project-history-links-in-ask-surfaces.md index b009fe847..7383a904d 100644 --- a/docs/adr/0113-project-history-links-in-ask-surfaces.md +++ b/docs/adr/0113-project-history-links-in-ask-surfaces.md @@ -29,7 +29,9 @@ reusing it as conversation context can disclose facts no longer authorized. Its prose cannot be safely decomposed by source after access changes. 6. A Global Ask session is rejected and restarted when any citation in its persisted continuity context is no longer authorized. Stored summaries are not reused across - that boundary. + that boundary. The browser persists that session identifier under one shared + `sessionStorage` key for bootstrap, successful answers, 404 retry, 409 restart, and + logout; those sites must not copy the key as a string literal. 7. Ask retrieval itself applies the same cutoff and source eligibility before an LLM sees evidence. Prompt bodies, hidden IDs, and unauthorized project counts never enter the project-history link response. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 813d19da3..fccd061b2 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -338,7 +338,9 @@ runtime note into a shipped/live claim. implemented in either Ask surface. - Source publication eligibility and cutoff are applied before Ask retrieval. Persisted answers are withheld when any citation loses visibility, and a Global Ask session with - stale citations must start a new session before prior answer prose is reused. + stale citations must start a new session before prior answer prose is reused. The + browser session identifier uses one shared `sessionStorage` key across bootstrap, + persist, 404 retry, 409 restart, and logout. - The response bounds citation and project counts, discloses truncated project links, and keeps answers readable when a timeline or TEPP validation is unavailable. - Remaining causal-analysis work is explicitly outside this slice: temporal association diff --git a/frontend/package.json b/frontend/package.json index cbc4050b7..430270dba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.20.1", + "version": "2.20.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index a772e57c0..89ba1c659 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import App from "./App"; +import App, { GLOBAL_ASK_SESSION_STORAGE_KEY } from "./App"; import { setLocale } from "./i18n"; import { isoWeekFromCreatedAt } from "./isoWeek"; @@ -91,6 +91,7 @@ describe("App, authenticated", () => { deferSecondAsk?: boolean; deferProjectHistory?: boolean; invalidAskSessionOnce?: boolean; + staleAskCitationsOnce?: boolean; meFailed?: boolean; postBody?: string; manyCustomerHints?: number; @@ -1699,6 +1700,16 @@ describe("App, authenticated", () => { }), ); } + if (options?.staleAskCitationsOnce && askRequestCount === 1 && requestBody.session_id) { + return Promise.resolve( + new Response( + JSON.stringify({ + detail: "Global Ask session evidence is no longer authorized; start a new session", + }), + { status: 409, headers: { "Content-Type": "application/json" } }, + ), + ); + } const ready = options?.deferSecondAsk && askRequestCount === 2 ? secondAskReady @@ -1951,7 +1962,7 @@ describe("App, authenticated", () => { }); it("replaces an invalid saved Ask session without requiring storage cleanup", async () => { - window.sessionStorage.setItem("lineageweave.globalAskSessionId", "stale-session"); + window.sessionStorage.setItem(GLOBAL_ASK_SESSION_STORAGE_KEY, "stale-session"); const fetchMock = stubBackend({ invalidAskSessionOnce: true }); render(); @@ -1967,7 +1978,27 @@ describe("App, authenticated", () => { .filter(([url]) => String(url).endsWith("/api/ask")) .map(([, init]) => JSON.parse(String((init as RequestInit).body)) as { session_id?: string }); expect(askBodies.map((body) => body.session_id)).toEqual(["stale-session", undefined]); - expect(window.sessionStorage.getItem("lineageweave.globalAskSessionId")).toBe("session-1"); + expect(window.sessionStorage.getItem(GLOBAL_ASK_SESSION_STORAGE_KEY)).toBe("session-1"); + }); + + it("restarts a Global Ask session whose citations lost visibility using the shared storage key", async () => { + window.sessionStorage.setItem(GLOBAL_ASK_SESSION_STORAGE_KEY, "stale-session"); + const fetchMock = stubBackend({ staleAskCitationsOnce: true }); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "Ask Agent" })); + const ask = await screen.findByRole("region", { name: "Ask Agent" }); + await userEvent.type(within(ask).getByRole("textbox", { name: "Ask a question" }), "Which project?"); + await userEvent.click(within(ask).getByRole("button", { name: "Ask" })); + + expect( + await within(ask).findByText("The cited project is supported by the stored semantic evidence."), + ).toBeInTheDocument(); + const askBodies = fetchMock.mock.calls + .filter(([url]) => String(url).endsWith("/api/ask")) + .map(([, init]) => JSON.parse(String((init as RequestInit).body)) as { session_id?: string }); + expect(askBodies.map((body) => body.session_id)).toEqual(["stale-session", undefined]); + expect(window.sessionStorage.getItem(GLOBAL_ASK_SESSION_STORAGE_KEY)).toBe("session-1"); }); it("labels the Customer Master entity level and Keymen side, never the raw lookup code", async () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 526d73316..47ea5cc75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -118,7 +118,7 @@ import { } from "./analysisRunNavigation"; import "./App.css"; -const GLOBAL_ASK_SESSION_STORAGE_KEY = "lineageweave.globalAskSessionId"; +export const GLOBAL_ASK_SESSION_STORAGE_KEY = "lineageweave.globalAskSessionId"; function orchestratorUnavailableMessage(err: unknown, action: string): string { if (err instanceof BackendError && err.status === 503) { @@ -4624,7 +4624,7 @@ function AskAgentPanel({ function acceptAnswer(nextAnswer: AskAgentResponse) { setAnswer(nextAnswer); setSessionId(nextAnswer.session_id); - window.sessionStorage.setItem("lineageweave.globalAskSessionId", nextAnswer.session_id); + window.sessionStorage.setItem(GLOBAL_ASK_SESSION_STORAGE_KEY, nextAnswer.session_id); } async function handleAsk() { @@ -4648,7 +4648,7 @@ function AskAgentPanel({ acceptAnswer(nextAnswer); } catch (err) { if (err instanceof BackendError && err.status === 409 && sessionId) { - window.sessionStorage.removeItem("lineageweave.globalAskSessionId"); + window.sessionStorage.removeItem(GLOBAL_ASK_SESSION_STORAGE_KEY); setSessionId(undefined); try { acceptAnswer(await askAgent(accessToken, normalized)); diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index b07c2a3e9..e41a13a68 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "2.20.1" +__version__ = "2.20.2" diff --git a/pyproject.toml b/pyproject.toml index 23544694d..d9217bc48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.20.1" +version = "2.20.2" description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication." readme = "README.md" license = { text = "MIT" } diff --git a/uv.lock b/uv.lock index c0327195e..0816f8215 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.20.1" +version = "2.20.2" source = { editable = "." } dependencies = [ { name = "certifi" }, From 6a5e429ebfc434900fac7613276e57fd4185e8da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 17:07:45 +0900 Subject: [PATCH 2/2] fix: keep stacked ADR identities unique --- CHANGELOG.md | 4 ++-- ...ty.md => 0126-global-ask-cutoff-and-migration-identity.md} | 2 +- ...7-recover-tepp-validation-on-canonical-project-history.md} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename docs/adr/{0125-global-ask-cutoff-and-migration-identity.md => 0126-global-ask-cutoff-and-migration-identity.md} (96%) rename docs/adr/{0112-recover-tepp-validation-on-canonical-project-history.md => 0127-recover-tepp-validation-on-canonical-project-history.md} (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e329b3efa..23599b470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ All notable changes to this project are documented here. Format follows - Global Ask now reuses the already-materialized authorized corporate-entity identifiers on the final cutoff-bounded source query instead of re-listing the - original input (ADR 0125). + original input (ADR 0126). ## [2.20.0] - 2026-08-21 @@ -41,7 +41,7 @@ All notable changes to this project are documented here. Format follows - Recovered the credential-free TEPP project-history validation boundary on top of the canonical Buyer timeline. TEPP may return only cutoff-safe temporal associations over the exact authorized events; the timeline remains readable - when TEPP is absent, and no result is labelled as a cause (ADR 0112). + when TEPP is absent, and no result is labelled as a cause (ADR 0127). ## [2.18.0] - 2026-08-20 diff --git a/docs/adr/0125-global-ask-cutoff-and-migration-identity.md b/docs/adr/0126-global-ask-cutoff-and-migration-identity.md similarity index 96% rename from docs/adr/0125-global-ask-cutoff-and-migration-identity.md rename to docs/adr/0126-global-ask-cutoff-and-migration-identity.md index 0cf64105b..787393e0c 100644 --- a/docs/adr/0125-global-ask-cutoff-and-migration-identity.md +++ b/docs/adr/0126-global-ask-cutoff-and-migration-identity.md @@ -1,4 +1,4 @@ -# ADR 0125 — Bind Global Ask cutoffs and keep migration identities unique +# ADR 0126 — Bind Global Ask cutoffs and keep migration identities unique **Decision status:** Accepted on the PR #342 repair branch **Date:** 2026-08-21 diff --git a/docs/adr/0112-recover-tepp-validation-on-canonical-project-history.md b/docs/adr/0127-recover-tepp-validation-on-canonical-project-history.md similarity index 98% rename from docs/adr/0112-recover-tepp-validation-on-canonical-project-history.md rename to docs/adr/0127-recover-tepp-validation-on-canonical-project-history.md index ade0d032a..5a42d964e 100644 --- a/docs/adr/0112-recover-tepp-validation-on-canonical-project-history.md +++ b/docs/adr/0127-recover-tepp-validation-on-canonical-project-history.md @@ -1,4 +1,4 @@ -# ADR 0112: Recover TEPP validation on the canonical project history +# ADR 0127: Recover TEPP validation on the canonical project history - Status: Proposed - Date: 2026-08-21