docs: add canonical session resolution contract
This commit is contained in:
1 parent
4ea762ae0d
commit
5e1b51d16f
5 files changed
+172
No files matched your search
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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)
|
## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -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
|
model-context reconstruction, compression, UI scene/cache, and sidebar metadata
|
||||||
repairs. Start here for narrow fixes that keep the existing WebUI execution
|
repairs. Start here for narrow fixes that keep the existing WebUI execution
|
||||||
path.
|
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):
|
- [`docs/rfcs/hermes-run-adapter-contract.md`](rfcs/hermes-run-adapter-contract.md):
|
||||||
proposed event/control contract, runtime-state ownership matrix,
|
proposed event/control contract, runtime-state ownership matrix,
|
||||||
acceptance-test catalog, and reversible migration gates for moving WebUI
|
acceptance-test catalog, and reversible migration gates for moving WebUI
|
||||||
|
|||||||
@@ -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,
|
— #2361 consistency rules for keeping transcript, model context, live streams,
|
||||||
replay, compression, and session metadata coherent during active and recovered
|
replay, compression, and session metadata coherent during active and recovered
|
||||||
WebUI runs.
|
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
|
- [`turn-journal.md`](turn-journal.md) — Crash-safe WebUI turn journal for
|
||||||
recovering interrupted chat submissions.
|
recovering interrupted chat submissions.
|
||||||
@@ -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/<session_id>`,
|
||||||
|
- a query parameter such as `?session=<session_id>` or `?session_id=<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/<id>` | Treat `<id>` as requested; resolve to canonical visible session before ordinary render. |
|
||||||
|
| Query parameter | `?session=<id>` or `?session_id=<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.
|
||||||
@@ -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 == []
|
||||||
Reference in new issue
Block a user