fix(desktop): preserve profile when restoring sessions - #63590
fix(desktop): preserve profile when restoring sessions#63590oliviaaaa7788 wants to merge 1 commit into
Conversation
1e917dd to
2d139b0
Compare
2d139b0 to
391c123
Compare
Remember Desktop's last open session per gateway profile, create branches in the parent session's profile, and pass the stored session profile when recovering after a session-not-found response. Tests: npm run test:ui --workspace apps/desktop -- src/store/session.test.ts src/app/session/hooks/use-session-actions.test.tsx src/app/session/hooks/use-prompt-actions/index.test.tsx; npm run typecheck --workspace apps/desktop
391c123 to
f519cc9
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the profile-routing paths. The global remembered-session key remains a real defect on current main: apps/desktop/src/store/session.ts:26-29 and apps/desktop/src/app/desktop-controller.tsx:273-301 still share it across profiles.
Problems
- The recovery helpers added in
apps/desktop/src/app/session/hooks/use-prompt-actions/submit.ts:53-62andindex.ts:62-71only inspect$sessions. That cache is paginated (use-session-list-actions.ts:161-183), so a selected cross-profile session outside the loaded window still resumes withoutprofile. The backend then uses the launch-profile DB whenprofileis absent (tui_gateway/server.py:5596-5613). - The added tests seed
$sessions, so they cover only this cache-hit case.
Suggested changes
- Reuse or extract the profile-aware resolver in
apps/desktop/src/app/session/hooks/use-session-actions/utils.ts:209-251, which probes uncached session IDs across profiles, and carry its resolved profile into each recovery resume. - Add cache-miss recovery coverage for the three updated resume paths.
This is an automated hermes-sweeper review.
| return null | ||
| } | ||
|
|
||
| const stored = $sessions.get().find(session => session.id === storedSessionId || session._lineage_root_id === storedSessionId) |
There was a problem hiding this comment.
This only searches the paginated $sessions cache. A selected cross-profile session can be absent from that cache, which leaves profile omitted and makes session.resume query the launch-profile DB. Please resolve uncached stored IDs through the existing cross-profile resolver (or a shared equivalent) before retrying.
|
Superseded by #69861, which carries this profile-routing fix (credited to @oliviaaaa7788 via Two things changed the shape: (1) the main resume path on current |
…bleed, and [System:] bubble (#69861) * fix(desktop): stop model-switch dup + route recovery resumes to the owning profile Fixes two Desktop session-reconciliation symptoms from #67603. Symptom 1 — duplicated user bubble after a model switch. The gateway persists model-switch / personality notices as role=user `[System: …]` rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't reject a non-leading system message (#48338). `preserveLocalPendingTurnMessages` paired local optimistic rows with the stored transcript by user-role ordinal, so a marker between two real user turns shifted every later ordinal and the optimistic row was re-appended at the bottom. The single trailing-marker case is already covered by the compression-era `latestAuthoritativeUser` guard, but two switches around one turn (marker before AND after the committed prompt) still duplicated it. Exclude `[System:` bookkeeping markers from ordinal pairing on both sides. Symptom 2 — a session appearing under two profiles. The main resume path already resolves a session's owning profile via `resolveStoredSession` (cache → active backend → cross-profile probe), but the recovery `session.resume` calls (stale runtime id, session-not-found, wedged loop, redirect) omitted `profile`, so the gateway fell back to the launch-profile DB and forked the conversation into the wrong profile. Route every recovery resume — and an uncached right-click branch — through the same resolver so the profile is carried even for sessions outside the paginated sidebar window (the cache-miss gap). Tests: discriminating two-switch marker test (fails before, passes after); cache-hit + cross-profile cache-miss coverage for the recovery resume and for branching an uncached session. Supersedes #68665 and #63590. Closes #67603. Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com> * fix(desktop): scope the remembered session id per profile A single global `hermes.desktop.lastSessionId` key remembered ONE session across every profile, so a relaunch or cold start under profile B would try to restore a session owned by profile A — reinforcing the impression that a conversation had bled between profiles (#67603, second symptom). Key the remembered id by the session's owning profile (resolved from the session row's `profile`, falling back to the active gateway profile), read it back for the active profile on restore, and clear an exhausted session under its owner. The default profile keeps the original unsuffixed key so existing installs' remembered session survives the upgrade. Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com> * fix(gateway): hide [System:] bookkeeping markers from every transcript Model-switch and personality notices are persisted as role=user `[System: …]` rows so strict providers accept them mid-history, but they are model-facing runtime metadata, not user turns. `_history_to_messages` — the single display projection every client reads — passed them straight through, so on resume or reload they rendered as a fake user bubble in the desktop, TUI, CLI, and web transcripts. Drop them in that projection. The raw marker stays in `session["history"]` for the model, so nothing changes for inference; only the display loses a row that never belonged to the user. This also removes the stored marker from the payload the desktop reconciles against, killing the ordinal shift that duplicated the optimistic prompt (#67603) at its source — the desktop-side marker exclusion remains as a fallback for older backends. Co-authored-by: Dolverin <Dolverin@users.noreply.github.com> --------- Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com> Co-authored-by: Dolverin <Dolverin@users.noreply.github.com>
…wning profile Fixes two Desktop session-reconciliation symptoms from NousResearch#67603. Symptom 1 — duplicated user bubble after a model switch. The gateway persists model-switch / personality notices as role=user `[System: …]` rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't reject a non-leading system message (NousResearch#48338). `preserveLocalPendingTurnMessages` paired local optimistic rows with the stored transcript by user-role ordinal, so a marker between two real user turns shifted every later ordinal and the optimistic row was re-appended at the bottom. The single trailing-marker case is already covered by the compression-era `latestAuthoritativeUser` guard, but two switches around one turn (marker before AND after the committed prompt) still duplicated it. Exclude `[System:` bookkeeping markers from ordinal pairing on both sides. Symptom 2 — a session appearing under two profiles. The main resume path already resolves a session's owning profile via `resolveStoredSession` (cache → active backend → cross-profile probe), but the recovery `session.resume` calls (stale runtime id, session-not-found, wedged loop, redirect) omitted `profile`, so the gateway fell back to the launch-profile DB and forked the conversation into the wrong profile. Route every recovery resume — and an uncached right-click branch — through the same resolver so the profile is carried even for sessions outside the paginated sidebar window (the cache-miss gap). Tests: discriminating two-switch marker test (fails before, passes after); cache-hit + cross-profile cache-miss coverage for the recovery resume and for branching an uncached session. Supersedes NousResearch#68665 and NousResearch#63590. Closes NousResearch#67603. Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>
…wning profile Fixes two Desktop session-reconciliation symptoms from NousResearch#67603. Symptom 1 — duplicated user bubble after a model switch. The gateway persists model-switch / personality notices as role=user `[System: …]` rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't reject a non-leading system message (NousResearch#48338). `preserveLocalPendingTurnMessages` paired local optimistic rows with the stored transcript by user-role ordinal, so a marker between two real user turns shifted every later ordinal and the optimistic row was re-appended at the bottom. The single trailing-marker case is already covered by the compression-era `latestAuthoritativeUser` guard, but two switches around one turn (marker before AND after the committed prompt) still duplicated it. Exclude `[System:` bookkeeping markers from ordinal pairing on both sides. Symptom 2 — a session appearing under two profiles. The main resume path already resolves a session's owning profile via `resolveStoredSession` (cache → active backend → cross-profile probe), but the recovery `session.resume` calls (stale runtime id, session-not-found, wedged loop, redirect) omitted `profile`, so the gateway fell back to the launch-profile DB and forked the conversation into the wrong profile. Route every recovery resume — and an uncached right-click branch — through the same resolver so the profile is carried even for sessions outside the paginated sidebar window (the cache-miss gap). Tests: discriminating two-switch marker test (fails before, passes after); cache-hit + cross-profile cache-miss coverage for the recovery resume and for branching an uncached session. Supersedes NousResearch#68665 and NousResearch#63590. Closes NousResearch#67603. Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>
…ile session bleed, and [System:] bubble (NousResearch#69861) * fix(desktop): stop model-switch dup + route recovery resumes to the owning profile Fixes two Desktop session-reconciliation symptoms from NousResearch#67603. Symptom 1 — duplicated user bubble after a model switch. The gateway persists model-switch / personality notices as role=user `[System: …]` rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't reject a non-leading system message (NousResearch#48338). `preserveLocalPendingTurnMessages` paired local optimistic rows with the stored transcript by user-role ordinal, so a marker between two real user turns shifted every later ordinal and the optimistic row was re-appended at the bottom. The single trailing-marker case is already covered by the compression-era `latestAuthoritativeUser` guard, but two switches around one turn (marker before AND after the committed prompt) still duplicated it. Exclude `[System:` bookkeeping markers from ordinal pairing on both sides. Symptom 2 — a session appearing under two profiles. The main resume path already resolves a session's owning profile via `resolveStoredSession` (cache → active backend → cross-profile probe), but the recovery `session.resume` calls (stale runtime id, session-not-found, wedged loop, redirect) omitted `profile`, so the gateway fell back to the launch-profile DB and forked the conversation into the wrong profile. Route every recovery resume — and an uncached right-click branch — through the same resolver so the profile is carried even for sessions outside the paginated sidebar window (the cache-miss gap). Tests: discriminating two-switch marker test (fails before, passes after); cache-hit + cross-profile cache-miss coverage for the recovery resume and for branching an uncached session. Supersedes NousResearch#68665 and NousResearch#63590. Closes NousResearch#67603. Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com> * fix(desktop): scope the remembered session id per profile A single global `hermes.desktop.lastSessionId` key remembered ONE session across every profile, so a relaunch or cold start under profile B would try to restore a session owned by profile A — reinforcing the impression that a conversation had bled between profiles (NousResearch#67603, second symptom). Key the remembered id by the session's owning profile (resolved from the session row's `profile`, falling back to the active gateway profile), read it back for the active profile on restore, and clear an exhausted session under its owner. The default profile keeps the original unsuffixed key so existing installs' remembered session survives the upgrade. Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com> * fix(gateway): hide [System:] bookkeeping markers from every transcript Model-switch and personality notices are persisted as role=user `[System: …]` rows so strict providers accept them mid-history, but they are model-facing runtime metadata, not user turns. `_history_to_messages` — the single display projection every client reads — passed them straight through, so on resume or reload they rendered as a fake user bubble in the desktop, TUI, CLI, and web transcripts. Drop them in that projection. The raw marker stays in `session["history"]` for the model, so nothing changes for inference; only the display loses a row that never belonged to the user. This also removes the stored marker from the payload the desktop reconciles against, killing the ordinal shift that duplicated the optimistic prompt (NousResearch#67603) at its source — the desktop-side marker exclusion remains as a fallback for older backends. Co-authored-by: Dolverin <Dolverin@users.noreply.github.com> --------- Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com> Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com> Co-authored-by: Dolverin <Dolverin@users.noreply.github.com>
Summary
source: 'desktop'resume/create tagging while adding profile routingWhy
Desktop could display or restore a stored session from one profile while routing follow-up actions through another profile. In a multi-profile setup this made sessions appear to jump profiles after profile switches or app restarts.
Test Plan
npm run test:ui --workspace apps/desktop -- src/store/session.test.ts src/app/session/hooks/use-session-actions.test.tsx src/app/session/hooks/use-prompt-actions/index.test.tsx— 84 tests passednpm run typecheck --workspace apps/desktopgit diff HEAD^ --checkScope / Safety