Skip to content

fix(webui): replace cross-profile empty sessions on profile switch - #5460

Closed
ruizanthony wants to merge 2 commits into
nesquena:masterfrom
ruizanthony:fix/profile-switch-cross-profile-empty-session
Closed

ruizanthony wants to merge 2 commits into
nesquena:masterfrom
ruizanthony:fix/profile-switch-cross-profile-empty-session

Conversation

@ruizanthony

Copy link
Copy Markdown
Contributor

Summary

  • Replace a current empty session during profile switch when its recorded profile does not match the newly active profile.
  • Prevent the browser from keeping an old-profile S.session.session_id after /api/profile/switch succeeds.
  • Add regression coverage for the upload failure class where attachments post S.session.session_id and the backend correctly rejects sessions outside the active profile.

Why

The profile switch path previously reused/retagged an empty current session in place. If that session belonged to the previous profile, later attachment uploads used the stale session_id; api/upload.py then returned 404 Session not found under the new profile cookie. Users had to hard reload before uploading.

Tests

  • node --check static/panels.js
  • python -m pytest tests/test_profile_switch_ux.py::TestParallelizedFetches::test_cross_profile_empty_session_is_replaced_before_mutation_or_upload tests/test_chat_upload_attachment_paths.py -q -o 'addopts='

@greptile-apps

greptile-apps Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a stale-session_id bug on profile switch: when the active session belonged to the previous profile (even if empty), the browser kept using it for attachment uploads, which the backend correctly rejected with a 404. The fix detects the cross-profile mismatch post-switch and forces the sessionInProgress replacement path.

  • Core logic (panels.js): after the /api/profile/switch POST resolves, checks whether the current session's profile matches the newly active profile; if not, promotes sessionInProgress = true, ensuring no in-place retag or /api/session/update runs on a foreign-profile session. Also adds _openingExistingSidebarSession to skip blank-session creation when a cross-profile sidebar click will immediately load a concrete session.
  • Shared open helper (sessions.js): consolidates all session-open entry points (main click, lineage segment, child session) into _openSidebarSession, which calls the new _ensureSidebarSessionProfile to switch profiles before loadSession. Adds _showAllProfiles persistence via localStorage so the all-profiles toggle survives profile switches.
  • Tests: eight test files are updated to track the refactored code paths, with three new tests covering the cross-profile replacement, the session-browser helper, and the localStorage persistence.

Confidence Score: 4/5

The core cross-profile empty-session replacement logic is correct and the happy path is well-covered by static tests. Two minor gaps in sessions.js and panels.js are worth addressing before merge but don't block the primary fix.

The _openingExistingSidebarSession fast-path in switchToProfile correctly skips blank-session creation and the cross-profile mismatch detection fires at the right point in the post-API setup. The _profileSwitchOpeningExistingSession flag is captured as a local const before any await, avoiding the most obvious stale-read window. The two open items — _sidebarSessionProfileName returning an empty string for profile-less sessions and the missing syncTopbar() in the new branch — are bounded in impact and do not affect the primary upload-failure regression path this PR targets.

static/sessions.js (_sidebarSessionProfileName return value for profile-less sessions) and static/panels.js (syncTopbar() gap in the _openingExistingSidebarSession branch)

Important Files Changed

Filename Overview
static/sessions.js Introduces _openSidebarSession (consolidating all session-open entry points), _ensureSidebarSessionProfile (cross-profile switch before loadSession), and localStorage persistence for _showAllProfiles. Core logic is sound; the return value of _ensureSidebarSessionProfile is not checked by the caller, so a failed switch still proceeds to loadSession (already flagged in prior review).
static/panels.js Adds cross-profile mismatch detection post-switch, the _openingExistingSidebarSession fast-path branch, and _openProfileSwitchSessionBrowser helper. The new _openingExistingSidebarSession branch omits syncTopbar(), which the other two success branches both call explicitly; profile-specific topbar state (model selector, workspace picker) may lag until loadSession completes.
tests/test_profile_switch_ux.py Adds three new regression tests covering cross-profile session replacement, the session-browser helper presence and ordering, and desktop/mobile UI path coverage. Tests are static-analysis based and well-structured.
tests/test_issue1611_session_profile_filtering.py Adds three new tests: profile-switch-before-open ordering, localStorage persistence for all-profiles toggle, and absence of _showAllProfiles reset in switchToProfile. All correctly track the new code paths.
tests/test_issue1700_parallel_profile_switch.py Updates sessionInProgress declaration extraction to use regex instead of string search, correctly handling the change from const to let.
tests/test_issue3603_external_session_import_gate.py Refactors external-session import gate tests to verify the shared _openSidebarSession helper rather than per-callsite checks. Correctly reflects the consolidated helper pattern.
tests/test_issue4662_profile_switch_skeleton_static.py Introduces _show_session_skeleton_call_idx helper to handle the new showSessionListSkeleton(name) call signature; updates string searches to be more robust. No logic concerns.
tests/test_firefox_sidebar_scroll_stability.py Updates two string assertions to match the new _setShowAllProfiles() calls with deferWhileInteracting:false. Straightforward tracking update.
tests/test_session_lineage_collapse.py Updates two assertions to reflect the consolidated _openSidebarSession helper replacing the inline loadSession calls for lineage/child sessions.
tests/test_session_touch_actions.py Updates touch gesture test to search for _openSidebarSession instead of loadSession in the gesture-finish block. Correctly tracks the refactored open path.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant SidebarRow as Sidebar Row click
    participant OSS as _openSidebarSession
    participant ESP as _ensureSidebarSessionProfile
    participant STP as switchToProfile
    participant API as /api/profile/switch
    participant LS as loadSession

    User->>SidebarRow: click cross-profile session
    SidebarRow->>OSS: _openSidebarSession(session)
    OSS->>ESP: _ensureSidebarSessionProfile(session)
    ESP->>ESP: "check _showAllProfiles && profile mismatch"
    ESP->>ESP: "set _profileSwitchOpeningExistingSession=true"
    ESP->>STP: await switchToProfile(targetProfile)
    STP->>STP: "capture _openingExistingSidebarSession=true (local const)"
    STP->>STP: "sessionInProgress=true (S.session exists)"
    STP->>API: POST /api/profile/switch
    API-->>STP: active, is_default, ...
    STP->>STP: skip profile-mismatch check (sessionInProgress already true)
    STP->>STP: "skip S.session retag (sessionInProgress=true)"
    STP->>STP: renderSessionList() + showToast()
    STP-->>ESP: returns
    ESP->>ESP: "finally: _profileSwitchOpeningExistingSession=false"
    ESP-->>OSS: return (success/failure)
    OSS->>LS: loadSession(session.session_id, loadOpts)
    LS-->>OSS: session loaded
    OSS->>OSS: renderSessionListFromCache()
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 User
    participant SidebarRow as Sidebar Row click
    participant OSS as _openSidebarSession
    participant ESP as _ensureSidebarSessionProfile
    participant STP as switchToProfile
    participant API as /api/profile/switch
    participant LS as loadSession

    User->>SidebarRow: click cross-profile session
    SidebarRow->>OSS: _openSidebarSession(session)
    OSS->>ESP: _ensureSidebarSessionProfile(session)
    ESP->>ESP: "check _showAllProfiles && profile mismatch"
    ESP->>ESP: "set _profileSwitchOpeningExistingSession=true"
    ESP->>STP: await switchToProfile(targetProfile)
    STP->>STP: "capture _openingExistingSidebarSession=true (local const)"
    STP->>STP: "sessionInProgress=true (S.session exists)"
    STP->>API: POST /api/profile/switch
    API-->>STP: active, is_default, ...
    STP->>STP: skip profile-mismatch check (sessionInProgress already true)
    STP->>STP: "skip S.session retag (sessionInProgress=true)"
    STP->>STP: renderSessionList() + showToast()
    STP-->>ESP: returns
    ESP->>ESP: "finally: _profileSwitchOpeningExistingSession=false"
    ESP-->>OSS: return (success/failure)
    OSS->>LS: loadSession(session.session_id, loadOpts)
    LS-->>OSS: session loaded
    OSS->>OSS: renderSessionListFromCache()
Loading

Reviews (7): Last reviewed commit: "fix(webui): open all-profile sidebar row..." | Re-trigger Greptile

Comment thread static/panels.js Outdated
Comment thread static/panels.js Outdated
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read switchToProfile() in static/panels.js at the PR head (the new block around 6485-6500) against origin/master, plus the backend rejection path in api/upload.py and api/profiles.py. The diagnosis is correct and the fix targets the right seam.

Summary

The stale-session-after-switch upload failure is real. api/upload.py:154-165 gates every upload target on the active profile:

def _session_visible_to_active_profile(session) -> bool:
    """Return whether an upload target session belongs to the active profile."""
    session_profile = getattr(session, 'profile', None)
    ...
    return _profiles_match(session_profile, _get_active_profile_name())

and returns 404 Session not found when it fails. Before this PR, an empty current session was retagged in place on switch, but if that session belonged to the previous profile the browser kept the old S.session.session_id; a subsequent attachment upload posted that id and tripped the 404. The PR promotes a profile-mismatched empty session onto the replace path so the browser drops the stale id and newSession() mints a fresh, correctly-tagged one.

Code reference

The new gate in panels.js (after /api/profile/switch returns, so S.activeProfile reflects the target):

if (!sessionInProgress && S.session) {
  const currentSessionProfile = (typeof S.session.profile === 'string' && S.session.profile.trim())
    ? S.session.profile.trim() : 'default';
  sessionProfileMatchesTarget = (typeof _profileMatchesActiveProfile === 'function')
    ? _profileMatchesActiveProfile(currentSessionProfile, targetActiveProfile)
    : (currentSessionProfile === targetActiveProfile || ...);
  if (!sessionProfileMatchesTarget) sessionInProgress = true;
}

Two things I checked that make this correct rather than fragile:

  1. Placement. This runs after S.activeProfile = data.active || name is applied, so targetActiveProfile is the new profile, and it runs before both the in-place S.session.profile retag (panels.js, guarded by if (S.session && !sessionInProgress)) and the /api/session/update call in the workspace block. So flipping sessionInProgress to true here correctly steers past the in-place retag into the if (sessionInProgress) branch that calls newSession(). The test asserts exactly this ordering (promote_idx < first_in_place_patch < first_update < branch_idx), which is a good guard against a future reorder silently breaking it.

  2. Match semantics parity. The client reuses _profileMatchesActiveProfile (sessions.js:38), whose renamed-root handling (eventName === 'default' && S.activeProfileIsDefault) mirrors the backend _profiles_match -> _is_root_profile cross-alias in api/profiles.py:373. So a legacy 'default'-tagged session under a renamed root profile is treated as matching on both sides, and you won't spuriously discard a session that the backend would actually accept.

One thing worth confirming

_profileMatchesActiveProfile leans on S.activeProfileIsDefault for the renamed-root case. The new block sets S.activeProfileIsDefault = !!data.is_default two lines above, so it's fresh at call time — good. The only residual gap is symmetry: the client match treats 'default' as matching the active root only when the session side is 'default'; the backend _profiles_match cross-aliases in both directions via _is_root_profile(row) and _is_root_profile(active). In practice S.session.profile for a freshly created root-profile session is whatever the switch wrote, so this is unlikely to bite, but if you ever see a root session tagged with the renamed display name (not 'default') get needlessly replaced on switch-to-root, that asymmetry is the place to look.

Verification

The added test_cross_profile_empty_session_is_replaced_before_mutation_or_upload is a source-shape assertion (it greps the function text for ordering), not a behavioral DOM test — reasonable given the existing TestParallelizedFetches style in this file, but it would pass even if _profileMatchesActiveProfile returned the wrong boolean. A follow-up worth considering: a small jsdom/unit check that an empty session tagged with profile A, after switching to profile B, results in newSession() being invoked (behavior), not just that the tokens appear in order. Net: the fix is sound and lands on the correct layer.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — RED ⛔ (behavior is sound; 3 brittle STATIC tests misalign with the improved code — CI red)

Certified head: sha:51a5a7b9 (rebased onto current master, git apply clean) · PR: #5460 · ruizanthony, fix(webui): replace cross-profile empty sessions on profile switch
Verdict: The switch fix itself is correct (Codex SAFE, 0 findings; its own test_profile_switch_ux.py + 11827 others pass). But CI is red because 3 brittle static source-string tests slice fragile windows/literals around switchToProfile() that no longer align after the PR's (legitimate) code changes. All 3 are test-brittleness, not behavior loss — but they must be updated for CI to go green.

What I ran (rebased worktree /tmp/wt-rebase-5460)

Gate Result
Rebase onto current master ✅ git apply clean
Codex (reproduce) SAFE TO SHIP — 0 findings; switch logic sound, failures are brittle-static-test misalignment
Full pytest suite 5 failed / 11827 passed — 2 known env flakes (nous, issue4536) + the 3 brittle static tests below
PR's own test ✅ test_profile_switch_ux.py passes

Findings — 3 brittle static tests, behavior VERIFIED intact

⛔ CI-red (3 brittle static tests; I confirmed each is a string-match artifact, not behavior loss):

  1. test_issue4662...::test_switch_post_suppresses_generic_timeout_toast — slices a ±200-char window around body.index("/api/profile/switch"). The PR inserted a comment block containing /api/profile/switch BEFORE the actual api(..., timeoutToast: false) call, so index finds the comment and the window misses the call. timeoutToast: false IS still on the switch call (panels.js:6484) — behavior intact. Fix: anchor on the api('/api/profile/switch' call (e.g. scope to api( + the path, or rindex), not a naive first-index window.
  2. test_issue4662...::test_shows_session_skeleton_up_front — asserts the literal "showSessionListSkeleton()". The PR changed the call to showSessionListSkeleton(name) (parameterized) + guarded with typeof … === 'function' (panels.js:6471) — skeleton still shown. Fix: assert showSessionListSkeleton( (allow an argument).
  3. test_issue1700...::test_frontend_treats_active_or_pending_session_as_in_progress — slices fn.find("const sessionInProgress") : fn.find("try {", ...). The PR changed const sessionInProgress → let sessionInProgress (because it legitimately reassigns it at 6498: sessionInProgress = true). So find("const …") returns −1 → empty slice → assert fails. The S.session.active_stream_id in-progress guard IS fully intact (panels.js:6451-6454) — behavior sound. Fix: match sessionInProgress without the const prefix (accept let/const).

✅ Behavior sound: the switch fix (replace-only for a cross-profile empty session vs same-profile retag) is correct; timeoutToast:false, skeleton, and the active_stream_id/pending in-progress guard are all still wired. Codex SAFE (0 findings). Positive nesquena-hermes read ("diagnosis correct, targets the right seam").

Recommendation to the next agent

RED — gate-fail/changes-requested (CI-red, test-only fixes): update the 3 brittle static assertions to match the improved code — anchor timeoutToast on the api('/api/profile/switch' call, accept showSessionListSkeleton( with an arg, and match sessionInProgress regardless of let/const. The PR's behavior is verified correct (Codex SAFE + its own test + the guards all intact) — this is purely static-test brittleness that the legitimate refactor exposed. Fast to fix. Once green, it's a crown-jewel profile-switching reliability fix (stale-session upload-404). concept 4/5. Author @ruizanthony (T1). crit=3. (Gate lesson: static source-string tests that slice fixed windows / match exact call-literals / assume const are brittle — a legitimate refactor breaks them without any behavior change; when a static test fails, verify the actual behavior is intact before treating it as a defect, and fix the test to be structure-tolerant.)


Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Rebased onto current master; all 3 CI failures confirmed brittle-static-test misalignment (timeoutToast:false@6484, showSessionListSkeleton(name)@6471, active_stream_id guard@6451-54 all intact), Codex SAFE 0 findings. Cert valid for sha:51a5a7b9.

@nesquena-hermes nesquena-hermes added gate-fail Gate found blocking issue(s); fix-spec in comment; awaiting fix/re-push changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address labels Jul 3, 2026
@ruizanthony
ruizanthony force-pushed the fix/profile-switch-cross-profile-empty-session branch 2 times, most recently from b215127 to be92ca1 Compare July 3, 2026 18:58
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@ruizanthony

Copy link
Copy Markdown
Contributor Author

Fixed in be92ca13.

Changes made:

  • aligned the fallback profile comparison with _profileMatchesActiveProfile by preserving renamed-default/root alias semantics via S.activeProfileIsDefault;
  • simplified targetActiveProfile after S.activeProfile is assigned;
  • updated the three brittle static assertions called out by the gate:
    • anchor timeoutToast: false on the actual api('/api/profile/switch'...) call;
    • allow showSessionListSkeleton(...) with an argument;
    • match sessionInProgress regardless of let/const.

Local validation:

  • node --check static/panels.js
  • python -m pytest tests/test_profile_switch_ux.py tests/test_issue4662_profile_switch_skeleton_static.py tests/test_issue1700_parallel_profile_switch.py tests/test_chat_upload_attachment_paths.py -q -o 'addopts=' → 53 passed
  • git diff --check HEAD~1..HEAD

Remote validation on be92ca13: browser-smoke, lint, all test shards, and Greptile Review are green.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — GREEN ✅ · CONVERGED (brittle tests fixed + profile-match logic refined)

Certified head: sha:6a93b00d (clean rebase, branch gate-rebase/5460-cross-profile-empty-session) · PR: #5460 · ruizanthony, fix(webui): replace cross-profile empty sessions on profile switch
Verdict: Bounce closed. The 3 brittle static tests from my prior gate are updated (31/31 pass), and the re-push refined the profile-match logic (targetActiveProfile = S.activeProfile || 'default'; match via S.activeProfileIsDefault). Codex SAFE, suite green, all switch guards intact. Crown-jewel profile-switching reliability fix (stale-session upload-404).

What I ran (rebased worktree /tmp/wt-rebase-5460b)

Gate Result
Rebase onto current master ✅ git apply clean; three-dot diff = panels.js + 3 tests ONLY
Codex (reproduce) SAFE TO SHIP — gated the rebased worktree, 0 findings
Full pytest suite 2 failed / 11830 passed — both non-defects (nous env flake + test_issue4536 isolation flake)
Previously-brittle tests ✅ 31/31 pass (test_issue4662 + test_issue1700 + test_profile_switch_ux)

Findings

✅ Brittle tests fixed + logic refined, behavior sound: the 3 static tests I flagged (timeoutToast window-slice, showSessionListSkeleton() literal, const sessionInProgress) are updated to match the refactor. The re-push also refined the profile-match seam: targetActiveProfile = S.activeProfile || 'default' (simpler than || data.active || name), and the cross-profile empty-session match uses (currentSessionProfile === targetActiveProfile || (currentSessionProfile === 'default' && !!S.activeProfileIsDefault)). Codex verified the replace-only decision for a cross-profile empty session is still correct (empty session whose recorded profile ≠ target → replace; same-profile/has-messages → retag/keep), no misclassification of default-profile sessions, and timeoutToast:false + skeleton + active_stream_id in-progress guard all intact. 0 findings.

ℹ️ Stale-base false-alarm noted (resolved): a cross-merge diff (my old fail-sha → new head, spanning the #5454 merge) appeared to show a style.css -9 removing the transparent-event keyframe — but the three-dot diff vs current master confirms #5460 touches ONLY panels.js + tests; #5454's animation removal is intact in the rebased worktree (0 keyframe refs). Not a revert.

Recommendation to the next agent

Ready to merge — use branch gate-rebase/5460-cross-profile-empty-session (sha:6a93b00d), NOT the PR's stale head be92ca13. Converged: the 3 brittle tests are fixed and the profile-match logic refinement is sound (Codex SAFE + 31/31 + suite green bar 2 known flakes, all switch guards intact). Crown-jewel profile-switching reliability (fixes the cross-profile empty-session upload-404). Backend/JS logic — a quick profile-switch-then-upload smoke confirms, but the mechanism + guards are verified. concept 4/5. Credit @ruizanthony (co-authored). crit=3. (Gate note: checked the three-dot diff to dismiss a cross-merge style.css false-alarm — #5454 intact.)


Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Rebased onto current master; 3 brittle tests verified fixed (31/31), profile-match logic refinement Codex-SAFE (replace-decision + guards intact), stale-base style.css false-alarm dismissed via three-dot diff. Cert valid for sha:6a93b00d.

@nesquena-hermes nesquena-hermes added gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent and removed changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address gate-fail Gate found blocking issue(s); fix-spec in comment; awaiting fix/re-push labels Jul 3, 2026
@ruizanthony
ruizanthony force-pushed the fix/profile-switch-cross-profile-empty-session branch from be92ca1 to a456dbd Compare July 3, 2026 19:26
@ruizanthony

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in a456dbdd.

Additional UX fix:

  • after a successful profile switch and after the new profile's session list has rendered, the UI now exposes that session browser by default;
  • desktop: uncollapses the sidebar via expandSidebar();
  • mobile: opens the sidebar drawer (mobile-panel-drawer mobile-open), so users can pick an existing conversation in the new profile or create a new one instead of continuing from the old-profile chat context.

Local validation:

  • watched the new regression tests fail before implementation;
  • node --check static/panels.js
  • python -m pytest tests/test_profile_switch_ux.py tests/test_issue4662_profile_switch_skeleton_static.py tests/test_issue1700_parallel_profile_switch.py tests/test_chat_upload_attachment_paths.py -q -o 'addopts=' → 55 passed
  • git diff --check

Remote validation on a456dbdd: browser-smoke, lint, all test shards, and Greptile Review are green.

@nesquena-hermes nesquena-hermes added the size:M Medium PR (≤10 files, ≤250 LOC) label Jul 3, 2026
@ruizanthony
ruizanthony force-pushed the fix/profile-switch-cross-profile-empty-session branch from ed1a917 to 4e2445b Compare July 3, 2026 19:54
Comment thread static/sessions.js
@ruizanthony
ruizanthony force-pushed the fix/profile-switch-cross-profile-empty-session branch from 4e2445b to deec3cb Compare July 3, 2026 20:02
Comment thread static/panels.js
@ruizanthony
ruizanthony force-pushed the fix/profile-switch-cross-profile-empty-session branch from deec3cb to d0ee292 Compare July 3, 2026 20:17
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Re-review — scope expanded past the certified head (sha:6a93b00d → sha:d0ee2928)

The GREEN cert above was for the panels-only fix. Since then two commits landed (a456dbdd, d0ee2928) that pull static/sessions.js into the change and add new cross-profile open behavior — that's genuinely new surface, so re-reading it here rather than treating the old cert as still valid.

What the new commits actually change

Reading static/sessions.js:1862-1885 at the PR head, all three sidebar open entry points (main row, lineage segment, child) now funnel through _openSidebarSession, which first calls _ensureSidebarSessionProfile:

async function _ensureSidebarSessionProfile(session){
  const targetProfile=_sidebarSessionProfileName(session);
  if(!_showAllProfiles||!targetProfile) return false;
  const activeProfile=S.activeProfile||'default';
  if(_profileMatchesActiveProfile(targetProfile,activeProfile)) return false;
  if(typeof switchToProfile!=='function') return false;
  _profileSwitchOpeningExistingSession=true;
  try{ await switchToProfile(targetProfile); }
  finally{ _profileSwitchOpeningExistingSession=false; }
  return _profileMatchesActiveProfile(targetProfile,S.activeProfile||'default');
}

The _profileSwitchOpeningExistingSession flag is read by switchToProfile (panels.js:6621) to skip minting a blank intermediary session, then loadSession runs. That flag-and-load handshake is coherent and the symbols check out (expandSidebar/_isDesktopWidth are real, boot.js:461/488).

Two greptile open items — my read after tracing the callers

1. "return value of _ensureSidebarSessionProfile not checked." Correct that _openSidebarSession:1883-1884 ignores the boolean and proceeds to loadSession unconditionally. But that degrades to master behavior — before this PR every sidebar click called loadSession with no profile switch at all. If switchToProfile throws, its own catch (panels.js:6691) restores the previous profile and the subsequent loadSession opens the row read-only under the old cookie — the same outcome as today. So it's not a regression, just a missed opportunity to short-circuit. Low priority.

2. "_openingExistingSidebarSession branch omits syncTopbar()." This one is a non-issue and worth closing out explicitly: the branch at panels.js:6621 returns without syncTopbar(), but the caller immediately runs loadSession, and every loadSession success path calls it (sessions.js:1595, 1722, 1747). The topbar is synced one tick later by the load, not stranded. The only window where it'd lag is if loadSession itself bails early — same edge as any other open path.

The change that deserves a product/changelog line (not a blocker)

switchToProfile used to hard-reset _showAllProfiles = false on every switch. The PR removes that (panels.js:6617) and persists the toggle via localStorage (sessions.js _setShowAllProfiles / SHOW_ALL_PROFILES_STORAGE_KEY). Net user-visible shift: clicking a row from another profile in the all-profiles list now silently switches the active profile (mutates the cookie + agent context), and the all-profiles scope stays sticky across switches and browser reloads. Both are defensible, but they change what a sidebar click means, so flagging for the ship/UX review — the earlier cert predates this and shouldn't be read as covering it.

The static tests track the new shape well (test_profile_switch_ux.py browser-helper coverage, test_issue1611 persistence assertions). As before these are source-shape greps, not DOM behavior — a small jsdom check that a cross-profile click actually flips S.activeProfile before loadSession would harden the new path, but that's an enhancement, not a gate. Behavior looks sound; re-gate the current head rather than relying on the 6a93b00d cert.

nesquena-hermes added a commit that referenced this pull request Jul 3, 2026
…open all-profile rows under owning profile

Clean rebase of ruizanthony's #5460 (rebase-first).

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

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — GREEN ✅ (re-gated at expanded head d0ee292)

Certified head: sha:5e5b44e7 (clean rebase of PR head d0ee2928, branch gate-rebase/5460-cross-profile-empty-session) · PR: #5460 · ruizanthony, fix(webui): replace cross-profile empty sessions on profile switch
Verdict: Re-gated after a scope expansion (+288/-61 since my earlier GREEN @be92ca13). The PR now does two things: (A) replace cross-profile EMPTY sessions on switch (original), and (B) NEW: in show-all-profiles view, open a cross-profile sidebar row under its OWNING profile (switch first, then load). The new behavior strengthens profile isolation; the import-gate and profile-filtering are not weakened. Codex SAFE, suite green.

What I ran (rebased worktree /tmp/wt-rebase-5460c)

Gate Result
Rebase onto current master ✅ git apply clean; three-dot diff = panels.js + sessions.js + 8 test files
Codex (reproduce) SAFE TO SHIP — gated the rebased worktree, 0 findings
Full pytest suite 2 failed / 11859 passed — both non-defects (nous env flake + test_issue4536 isolation flake)
Touched profile-isolation tests ✅ 131/131 (test_profile_switch_ux, #1611 filtering, #3603 import-gate, #1700 parallel, #4662 skeleton, lineage)

Findings

✅ Expanded behavior sound, profile isolation strengthened: (A) the cross-profile empty-session replace (my prior GREEN) is unchanged. (B) the new _ensureSidebarSessionProfile(session): when _showAllProfiles and the clicked session's profile ≠ active, it await switchToProfile(targetProfile) BEFORE loadSession() — so a cross-profile row opens under its OWNING profile (no misattribution to the active profile), gated on _showAllProfiles + _profileMatchesActiveProfile (skips if already matching), coordinated via a _profileSwitchOpeningExistingSession flag. Codex verified: no cross-profile misattribution/leak, the external-session-import-gate (#3603) is NOT weakened, profile-filtering (#1611) still scopes rows correctly, the coordination flag doesn't desync on switch failure, and the empty-session-replace + parallel-switch (#1700) + skeleton (#4662) paths don't regress. _showAllProfiles persists via localStorage safely. 0 findings, 131/131 touched tests.

Recommendation to the next agent

Ready to merge — use branch gate-rebase/5460-cross-profile-empty-session (sha:5e5b44e7), NOT the PR's stale head d0ee2928. Supersedes my earlier GREEN @be92ca13 (scope expanded). The added "open all-profile row under owning profile" behavior actually strengthens the crown-jewel profile-switching isolation (a cross-profile session opens under its owner), with the import-gate + profile-filtering verified intact. Codex SAFE + 131/131 touched + suite green bar 2 known flakes. Profile-switching surface — a quick all-profiles-view cross-profile click smoke confirms, but the switch-before-load + isolation are verified. concept 4/5. Credit @ruizanthony (co-authored). crit=3.


Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Re-gated at expanded head d0ee292; the new open-under-owning-profile behavior verified isolation-strengthening (switch-before-load, import-gate + filtering intact, coordination flag safe), Codex SAFE + 131/131 touched + suite green bar 2 known flakes. Cert valid for sha:5e5b44e7.

@nesquena-hermes nesquena-hermes added size:L Large PR (>10 files or >250 LOC) and removed size:M Medium PR (≤10 files, ≤250 LOC) labels Jul 3, 2026
nesquena-hermes added a commit that referenced this pull request Jul 4, 2026
Release — replace cross-profile empty sessions on profile switch (#5460)
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.848 — thanks @ruizanthony! 🎉

Cross-profile empty-session replacement on profile switch is live: switching profiles with an empty current chat no longer leaves a stale session id that 404s your next upload, and clicking a session owned by another profile now switches to that profile before loading (instead of 404ing).

Full crown-jewel gate: Codex SAFE + Opus SAFE (no cross-profile data exposure/mis-scoping; empty-session replacement only fires for a genuinely-empty differing-profile session; no regression to the profile-switch skeleton/parallel-switch race/session-profile filter/external-import gate), full suite green (2 unrelated env flakes), 54/54 targeted. Credited via Co-authored-by.

lincoln-mackay pushed a commit to lincoln-mackay/hermes-webui that referenced this pull request Jul 4, 2026
lincoln-mackay pushed a commit to lincoln-mackay/hermes-webui that referenced this pull request Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent size:L Large PR (>10 files or >250 LOC)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants