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.1-authorized-entity-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Reuse the materialized authorized-entity identifier list as `$1` on the final
Global Ask source query.
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.
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ 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

- 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 0126).

## [2.20.0] - 2026-08-21

### Added
Expand All @@ -25,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

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.

🔍 ADR 0113 dependency points to renamed ADR

The rename of the TEPP recovery ADR from 0112 to 0127 updated the CHANGELOG 2.19.0 entry but not ADR 0113, which still says Depends on: ADR 0112. That reference now resolves to the unrelated 0112-project-bound-summary-events.md instead of the TEPP recovery ADR. Documentation-only; the line sits outside the changed hunks.

(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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,9 +17,11 @@ push rather than leaving the branch itself correct.
## Decision

1. Bind the cutoff as the fourth argument of the final Global Ask source query.
2. Assign the cutoff schema change the next unique forward migration identity,
2. Reuse the already-materialized `authorized_entity_ids` list as `$1` instead of
re-iterating `authorized_corporate_entity_ids`.
3. Assign the cutoff schema change the next unique forward migration identity,
`0054`, and update rollback, migration dispatch, and contract tests.
3. Keep reproduction and regression checks in committed tests. Do not use a
4. Keep reproduction and regression checks in committed tests. Do not use a
workflow that edits, commits, pushes, or deletes product source at runtime.

## Consequences
Expand All @@ -33,7 +35,7 @@ push rather than leaving the branch itself correct.
## Verification

- The synthetic query contract asserts the fourth argument is the requested
cutoff.
cutoff and the first argument is the materialized authorized-entity list.
- The PostgreSQL integration contract executes the final query against a real
local PostgreSQL parser when `LINEAGEWEAVE_TEST_POSTGRES_ADMIN_DSN` is set.
- Migration identity tests reject duplicate numeric prefixes and require the
Expand Down
Original file line number Diff line number Diff line change
@@ -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
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.0",
"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.0"
__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.0"
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: 2 additions & 0 deletions tests/test_ask_project_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ async def fetch(self, query: str, *args: object):
source_query, source_args = source_calls[0]
assert "source_deleted_flag" in source_query
assert "created_at <= $4" in source_query
assert len(source_args) == 4
assert list(source_args[0]) == ["tenant-a"]
assert source_args[3] == CUTOFF


Expand Down
4 changes: 4 additions & 0 deletions tests/test_global_ask_cutoff_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ async def fetch(self, query: str, *args: object):
)
assert result == []
assert boundary.final_args is not None
assert len(boundary.final_args) == 4
assert list(boundary.final_args[0]) == [
"00000000-0000-4000-8000-000000000001"
]
assert boundary.final_args[3] == CUTOFF
finally:
await connection.close()
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.