Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.d/2.20.2-ask-session-storage-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Use one shared Global Ask `sessionStorage` key for bootstrap, persist, 404
retry, 409 restart, and logout.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/adr/0113-project-history-links-in-ask-surfaces.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Stale ADR 0112 dependency after rename

The rename of the TEPP-validation ADR from 0112 to 0127 left 0113-project-history-links-in-ask-surfaces.md still reading "Depends on: ADR 0112". Number 0112 is now owned by an unrelated ADR (docs/adr/0112-project-bound-summary-events.md), so the dependency points to the wrong record.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.20.1",
"version": "2.20.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
37 changes: 34 additions & 3 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -91,6 +91,7 @@ describe("App, authenticated", () => {
deferSecondAsk?: boolean;
deferProjectHistory?: boolean;
invalidAskSessionOnce?: boolean;
staleAskCitationsOnce?: boolean;
meFailed?: boolean;
postBody?: string;
manyCustomerHints?: number;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(<App />);

Expand All @@ -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(<App />);

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 () => {
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand All @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
"sentence_excerpts",
]

__version__ = "2.20.1"
__version__ = "2.20.2"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading