Skip to content

Release: auto-switch profiles for cross-profile session links (#5419) - #5728

Merged
nesquena-hermes merged 1 commit into
masterfrom
release/stage-5419
Jul 7, 2026
Merged

nesquena-hermes merged 1 commit into
masterfrom
release/stage-5419

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release: auto-switch profiles for cross-profile session links (#5419)

A valid session:// deep link to a session owned by a different Hermes profile used to look identical to a deleted session (404 → the frontend self-healed away, "Session not available in web UI"). Now GET /api/session returns a structured 409 session_profile_mismatch envelope (error/code/session_id/profile ONLY — no transcript) when the owning profile is known, and loadSession() switches to the owning profile and retries once. Truly missing/deleted (or legacy None-profile) sessions keep the 404 self-heal.

Source PR: #5419 by @harcek. Diff: +226 (api/routes.py, static/sessions.js, tests).

Maintainer changes + gate fixes (Codex + Fable)

  • Codex CORE: added a post-await stale-load guard after _switchProfileForSessionLoad so a navigation during the profile switch (POST + list re-render) can't hijack the UI back to the old session.
  • Fable Finding 1 (moderate-high, reproduced): only emit the 409 when _session_profile is truthy. _profiles_match coerces None'default', so a truly-missing/legacy None-profile session under a non-default active profile would otherwise emit a useless profile:null 409 — the frontend requires a truthy profile, so it fell into the generic error branch, skipped the 404 self-heal, and re-armed the SSE stream against a dead session id. Fixed on both detail branches + 2 mutation-checked regression tests.
  • Fable Finding 2 (moderate): _switchProfileForSessionLoad now clears the sidebar skeleton + re-renders from cache on switch-POST failure (mirrors the Profile switch: polished loading skeletons (sidebar + workspace) — #4662 phase 1 #4671 canonical-switch catch), then rethrows — a failed switch no longer strands the sidebar on the skeleton.
  • Reconciled the 4 test_issue1611 profile-isolation tests to the 409 contract while preserving the no-leak boundary assertion (envelope-key allowlist + explicit forbidden-key checks).

Gate (all green)

  • Codex: SAFE after the stale-load fix (no transcript leak, exemption safe — both detail branches re-check, no infinite loop).
  • Fable: the 2 findings above fixed; re-verified everything else clean. Info-disclosure delta (session-existence oracle + owning-profile-name) judged acceptable for the single-user/multi-profile model (profile names already enumerable via /api/profiles); Finding-1 fix narrows it further.
  • Full suite: clean (39/39 isolation+sweep tests via prod venv; the 1 TLS failure in the stage worktree is the known worktree-.venv-missing-deps artifact — passes with deps, fix: auto-switch profiles for session links #5419 touches no TLS code).

Attribution preserved: authored as @harcek + maintainer Co-authored-by. CHANGELOG credit.

A valid session:// deep link to a session owned by a DIFFERENT Hermes
profile used to look identical to a deleted session (404 -> frontend
self-heals away). Now GET /api/session returns a structured 409
session_profile_mismatch envelope (error/code/session_id/profile ONLY,
no transcript) ONLY when the owning profile is KNOWN, and loadSession()
catches it, switches to the owning profile, and retries once. Truly
missing/deleted or legacy None-profile sessions keep the 404 self-heal.

Gate fixes applied (Codex + Fable):
- Codex CORE: added a post-await stale-load guard after
  _switchProfileForSessionLoad so a navigation during the switch can't
  hijack the UI back to the old session.
- Fable Finding 1: only emit 409 when _session_profile is truthy;
  a None-profile (missing/legacy) session under a non-default active
  profile now keeps 404 instead of a useless profile=null 409 (which
  skipped self-heal + spun the SSE reconnect against a dead sid). Both
  detail branches. + 2 regression tests.
- Fable Finding 2: _switchProfileForSessionLoad now clears the sidebar
  skeleton + re-renders from cache on switch-POST failure (mirrors the
  #4671 canonical-switch catch), then rethrows, so a failed switch can't
  strand the sidebar on the skeleton.

Reconciled tests/test_issue1611_session_profile_filtering.py (4 tests) to
the 409 contract while preserving the no-leak boundary assertion.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR teaches session deep links how to recover across profiles. The main changes are:

  • /api/session can return a structured profile-mismatch response.
  • The session loader can switch profiles and retry the load once.
  • Missing or unknown-profile sessions keep the old 404 self-heal path.
  • Profile-isolation tests now cover the new envelope shape.

Confidence Score: 4/5

The cross-profile CLI session path needs a fix before merging.

  • The main web-session mismatch path returns a small terminal envelope before content is returned.
  • The frontend retry path has guards for stale navigation during the profile switch.
  • The CLI fallback can still return 404 for a valid foreign-profile CLI session because its owner lookup stays scoped to the active profile.

api/routes.py

Important Files Changed

Filename Overview
api/routes.py Adds route-local profile-mismatch handling for /api/session; the CLI fallback can still miss foreign-profile ownership metadata.
static/sessions.js Adds mismatch parsing, profile switching, stale-load guards, and retry-once behavior for session loads.
tests/test_issue1611_session_profile_filtering.py Updates profile-isolation tests for the 409 envelope and keeps coverage for unknown-profile 404 behavior.
tests/test_bugfix_sweep.py Adds a smoke test for the cross-profile deep-link flow.
CHANGELOG.md Documents the release-visible session-link behavior change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant UI as loadSession()
  participant API as GET /api/session
  participant Switch as POST /api/profile/switch
  UI->>API: Load session under active profile
  API-->>UI: 409 session_profile_mismatch(profile)
  UI->>Switch: Switch to owning profile
  Switch-->>UI: New active profile
  UI->>API: Retry session load once
  API-->>UI: Session data or normal error
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant UI as loadSession()
  participant API as GET /api/session
  participant Switch as POST /api/profile/switch
  UI->>API: Load session under active profile
  API-->>UI: 409 session_profile_mismatch(profile)
  UI->>Switch: Switch to owning profile
  Switch-->>UI: New active profile
  UI->>API: Retry session load once
  API-->>UI: Session data or normal error
Loading

Reviews (1): Last reviewed commit: "fix: auto-switch profiles for session li..." | Re-trigger Greptile

Comment thread api/routes.py
@@ -12061,6 +12080,20 @@ def handle_get(handler, parsed) -> bool:
cli_meta = _lookup_cli_session_metadata(sid)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 CLI Owner Lookup Stays Scoped

When a session:// link points to a CLI session owned by another profile, this lookup still uses the active-profile-only default, so it returns no metadata and _session_profile becomes None. That sends the request down the 404 self-heal path instead of the new session_profile_mismatch response, so valid cross-profile CLI links are still treated as deleted sessions.

@nesquena-hermes
nesquena-hermes merged commit 1d82080 into master Jul 7, 2026
18 checks passed
@nesquena-hermes
nesquena-hermes deleted the release/stage-5419 branch July 7, 2026 02:39
@cutter-sh

cutter-sh Bot commented Jul 7, 2026

Copy link
Copy Markdown

🎬 Cutter preview — PR #5728

Open cross-profile session deep link
Open cross-profile session deep link — Opening a cross-profile session link auto-switches to that session's profile with a notification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants