From 5e1b51d16f0c1404bf2d45cb3ac238b2189e7b18 Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Mon, 25 May 2026 11:14:15 +0200 Subject: [PATCH] docs: add canonical session resolution contract --- CHANGELOG.md | 3 + docs/CONTRACTS.md | 5 + docs/rfcs/README.md | 3 + docs/rfcs/canonical-session-resolution.md | 124 ++++++++++++++++++ .../test_canonical_session_resolution_rfc.py | 37 ++++++ 5 files changed, 172 insertions(+) create mode 100644 docs/rfcs/canonical-session-resolution.md create mode 100644 tests/test_canonical_session_resolution_rfc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 42919130..f1a89c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## [Unreleased] +### Added +- Added a proposed canonical session resolution RFC covering URL routes, query parameters, localStorage, sidebar rows, and compression-lineage IDs so future session-routing fixes have one review contract. + ## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) ### Fixed diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 44649946..777a7436 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -27,6 +27,11 @@ does not change runtime behavior, maintainer policy, bot behavior, or CI gates. model-context reconstruction, compression, UI scene/cache, and sidebar metadata repairs. Start here for narrow fixes that keep the existing WebUI execution path. +- [`docs/rfcs/canonical-session-resolution.md`](rfcs/canonical-session-resolution.md): + proposed contract for resolving URL routes, query parameters, localStorage, + sidebar rows, and compression-lineage IDs to one canonical visible session + target. Start here for session routing, boot restore, stale parent, or + compression-tip selection changes. - [`docs/rfcs/hermes-run-adapter-contract.md`](rfcs/hermes-run-adapter-contract.md): proposed event/control contract, runtime-state ownership matrix, acceptance-test catalog, and reversible migration gates for moving WebUI diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md index 58b9291c..9cd72388 100644 --- a/docs/rfcs/README.md +++ b/docs/rfcs/README.md @@ -46,5 +46,8 @@ First-time contributor RFCs should be discussed in an issue before opening a PR. — #2361 consistency rules for keeping transcript, model context, live streams, replay, compression, and session metadata coherent during active and recovered WebUI runs. +- [`canonical-session-resolution.md`](canonical-session-resolution.md) — #2361 + focused contract for resolving URL, query parameter, localStorage, sidebar, + and compression-lineage session IDs to one canonical visible chat target. - [`turn-journal.md`](turn-journal.md) — Crash-safe WebUI turn journal for recovering interrupted chat submissions. diff --git a/docs/rfcs/canonical-session-resolution.md b/docs/rfcs/canonical-session-resolution.md new file mode 100644 index 00000000..5f1a416c --- /dev/null +++ b/docs/rfcs/canonical-session-resolution.md @@ -0,0 +1,124 @@ +# Canonical Session Resolution Contract + +- **Status:** Proposed +- **Author:** @ai-ag2026 +- **Created:** 2026-05-25 +- **Tracking issue:** [#2361](https://github.com/nesquena/hermes-webui/issues/2361) +- **Related architecture:** [#1925](https://github.com/nesquena/hermes-webui/issues/1925), [`webui-run-state-consistency-contract.md`](webui-run-state-consistency-contract.md) + +## Problem + +WebUI can reach the same conversation through several browser-facing entrypoints: + +- a URL route such as `/session/`, +- a query parameter such as `?session=` or `?session_id=`, +- the browser's `localStorage` active-session value, +- sidebar rows built from `/api/sessions`, +- direct session open actions from links, search, or imported session lists, +- browser boot restore after reload, auth redirect, or PWA resume. + +After automatic compression, those entrypoints can point at different rows in one +logical conversation lineage. A pre-compression parent snapshot can remain a +valid archived session while the user-facing conversation tip has moved to a +newer continuation. If each caller resolves IDs independently, the UI can appear +to lose the session, reopen an old one-message snapshot, duplicate sidebar rows, +or prefer the wrong transcript even though durable data is still present. + +This contract defines the expected resolution semantics for those entrypoints. It +is intentionally narrower than the run adapter RFC: this is about choosing the +correct visible session target, not moving execution ownership. + +## Goals + +- Define one canonical browser-facing resolution concept for sessions and + compression lineage. +- Make URL, query parameter, localStorage, sidebar, and direct-open behavior use + the same mental model. +- Preserve archived parent snapshots without letting them become the default + active target when a continuation exists. +- Give reviewers a small checklist for future session-routing, sidebar, and + compression-lineage changes. + +## Non-goals + +- Do not delete archived `pre_compression_snapshot` rows. +- Do not merge or rewrite session files as part of this contract. +- Do not replace state.db/session sidecar reconciliation. +- Do not require a new backend endpoint before narrow frontend guards can land. +- Do not change explicit history browsing when the user deliberately opens an + archived snapshot as a record. + +## Terms + +| Term | Meaning | +|---|---| +| Requested session ID | The ID supplied by route, query parameter, localStorage, sidebar click, or direct session open. | +| Canonical visible session | The session row WebUI should display by default for normal chat navigation. | +| `canonical_visible_session_id` | Proposed field/name for an API or helper output that identifies the canonical visible session. | +| Compression snapshot | A preserved archived parent row with `pre_compression_snapshot` set. | +| Continuation session | The active child/tip created after compression, usually represented by `continuation_session_id`, `_lineage_tip_id`, or newer lineage metadata. | +| Lineage relation | Links such as `parent_session_id`, `_lineage_root_id`, `_lineage_tip_id`, and `_compression_segment_count` that connect rows belonging to one logical conversation. | + +## Resolution Rules + +1. **Directly valid non-snapshot IDs stay stable.** If the requested session ID + exists and is not a `pre_compression_snapshot`, it should normally resolve to + itself. +2. **Snapshot parents defer to visible continuation tips.** If the requested + session ID is a `pre_compression_snapshot` and the session list has a newer + non-snapshot continuation in the same lineage, normal chat navigation should + resolve to that continuation as the `canonical_visible_session_id`. +3. **Explicit archive/history inspection remains possible.** A future UI affordance + may intentionally open a snapshot as a historical record, but that should be a + distinct mode from ordinary boot restore, URL restore, or sidebar continuation. +4. **Local browser state is advisory.** `localStorage` may remember the last active + ID, but browser boot restore must treat it as a requested session ID and still + run canonical resolution before rendering. +5. **Query aliases share the same resolver.** `?session=...`, `?session_id=...`, + and `/session/...` should feed the same requested-ID path instead of carrying + separate precedence rules. +6. **Sidebar collapse and session loading agree.** The row chosen as the visible + representative for a lineage should match the target opened by `loadSession()` + for that lineage during ordinary navigation. +7. **404 self-heal is separate from lineage resolution.** Missing/deleted sessions + should still use the stale-route recovery path. A present archived parent with + a live continuation is not a 404; it is a canonicalization problem. + +## Entry Point Matrix + +| Entry point | Input | Expected resolution | +|---|---|---| +| URL route | `/session/` | Treat `` as requested; resolve to canonical visible session before ordinary render. | +| Query parameter | `?session=` or `?session_id=` | Same as URL route. Query spelling must not change the target semantics. | +| localStorage | last active session ID | Advisory requested ID during browser boot restore; canonicalize before render. | +| Sidebar click | visible row ID or lineage representative | Open the same canonical visible session that the row represents. | +| Direct session open | programmatic call/search/import link | Use the shared requested-ID resolver unless the caller explicitly opts into archive inspection. | +| Browser boot restore | URL and/or localStorage state after reload/auth/PWA resume | Prefer explicit URL/query input, then localStorage, then canonicalize the requested ID. | + +## Review Checklist + +For PRs that touch session routing, compression lineage, sidebar collapse, boot +restore, direct session open, or URL parsing, answer: + +- Which entrypoints provide the requested session ID? +- Does the code path accept both route and query parameter forms where relevant? +- Does localStorage go through the same canonicalization path as URL restore? +- Can a `pre_compression_snapshot` become the default active chat when a + non-snapshot `continuation_session_id` / `_lineage_tip_id` exists? +- Do sidebar collapse and `loadSession()` pick the same visible representative? +- Is missing-session 404 recovery kept distinct from present-but-archived lineage + canonicalization? +- What regression proves route, query parameter, localStorage, and sidebar paths + agree for compressed lineage rows? + +## Rollout Plan + +1. Document this proposed contract and link it from the public contract index. +2. Keep narrow bugfixes small while referencing the relevant rule they preserve. +3. Add shared frontend helper coverage for URL/query/localStorage/sidebar + requested-ID inputs. +4. If backend session APIs later expose `canonical_visible_session_id`, make the + frontend resolver prefer the backend value while preserving client fallback for + older WebUI servers. +5. If #1925 moves execution/session ownership behind an adapter, carry this + contract forward as an adapter-facing session-navigation invariant. diff --git a/tests/test_canonical_session_resolution_rfc.py b/tests/test_canonical_session_resolution_rfc.py new file mode 100644 index 00000000..f3edc687 --- /dev/null +++ b/tests/test_canonical_session_resolution_rfc.py @@ -0,0 +1,37 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RFC = ROOT / "docs" / "rfcs" / "canonical-session-resolution.md" +RFC_INDEX = ROOT / "docs" / "rfcs" / "README.md" +CONTRACTS = ROOT / "docs" / "CONTRACTS.md" + + +def test_canonical_session_resolution_rfc_is_indexed(): + assert RFC.exists(), "canonical session resolution RFC must exist" + + rel = "docs/rfcs/canonical-session-resolution.md" + rfc_index = RFC_INDEX.read_text(encoding="utf-8") + contracts = CONTRACTS.read_text(encoding="utf-8") + + assert "canonical-session-resolution.md" in rfc_index + assert rel in contracts + + +def test_canonical_session_resolution_contract_names_entrypoints_and_outputs(): + text = RFC.read_text(encoding="utf-8") + + required_terms = [ + "URL route", + "query parameter", + "localStorage", + "sidebar", + "pre_compression_snapshot", + "canonical_visible_session_id", + "continuation_session_id", + "parent_session_id", + "direct session open", + "browser boot restore", + ] + + missing = [term for term in required_terms if term not in text] + assert missing == []