Skip to content

fix(bot-mode): prevent ordinary sessions from being hidden - #89901

Closed
Owen-narcissus wants to merge 1 commit into
NousResearch:mainfrom
Owen-narcissus:fix/bot-mode-ordinary-session-hiding
Closed

fix(bot-mode): prevent ordinary sessions from being hidden#89901
Owen-narcissus wants to merge 1 commit into
NousResearch:mainfrom
Owen-narcissus:fix/bot-mode-ordinary-session-hiding

Conversation

@Owen-narcissus

@Owen-narcissus Owen-narcissus commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Fix a Bot Mode session-ownership bug that can hide ordinary desktop conversations from the global Sessions sidebar.

Problem

When a Bot profile had no canonical Bot Chat pointer, Bot Mode adopted the profile's latest session based only on recency. If that latest session was an ordinary user conversation, its ID was persisted as hermes-bots.chat. The startup reconciliation sweep then treated the ID as Bot Mode-owned and set hidden=true.

This caused unrelated project conversations to disappear from the desktop session list while remaining present in state.db.

Changes

  • Only adopt or recover previewed sessions whose durable root title is Bot Chat.
  • Expose root_title in the precise profiles.list preferred-session summary so compressed canonical chats remain valid even when the live tip is titled Bot Chat (continued) or otherwise differs from the root.
  • Reject a pinned session if the backend resolves it to an ordinary conversation; clear the corrupt pointer and create a fresh Bot Chat.
  • Validate canonical pointers through profiles.list before issuing session.set_hidden.
  • Fail closed when validation is unavailable: an old Bot Chat may remain visible, but an ordinary session is never hidden.
  • Add regression coverage for ordinary latest sessions, stale pins, stale hide-sweep pointers, and compressed canonical chats.

Verification

  • npm run check:test:plugins — 312/312 tests passed.
  • Targeted canonical/hide tests — passed after the final change.
  • Regression sabotage check: the new safety tests fail with the pre-fix implementation.
  • npm run pack completed successfully for macOS arm64 before the root-title compatibility refinement.
  • The targeted backend pytest was not runnable locally because the repository virtual environments do not include pytest; CI should execute it.

Scope

This change does not alter the intended behavior for actual Bot Mode plumbing sessions (Bot Chat, Agent Inbox, and Group: ...). It does not include unrelated working-tree files.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 19, 2026
@zengzheqing

zengzheqing commented Aug 20, 2026

Copy link
Copy Markdown

Production-verified repro of this exact bug class

I hit this in production on 2026-08-19 and forensically traced it before writing this comment — sharing the evidence trail since it independently confirms your diagnosis and should help priority.

Symptom: an ordinary 112-message conversation (title and session id withheld for privacy) vanished from the Desktop session listing. State: hidden=1, archived=0 — present in state.db, invisible everywhere.

Forensic chain (each step a hard artifact):

  1. Three consecutive state.db.pre-update-emergency-* backups (02:28 / 06:53 / 15:58 UTC on 8/19) all carry hidden=1 for the row → the hide predates the earliest backup, windowed to 8/16–8/18.
  2. A full-database FTS + tool_calls scan proves no agent session ever issued a hide write — the write did not come from the agent layer.
  3. The packaged Desktop bundle (built today, d762ed9b, pre-fix(bot-mode): prevent ordinary sessions from being hidden #89901 code) contains the unconditional sweep: hideOwnedBotSessions() maps every bot-meta .chat id to session.set_hidden {hidden: true} with no ownership validation — exactly the code path your patch gates.
  4. Server-side smoking gun: profile.yamlui_meta.hermes-bots.chat = "<the victim's session id>" — the victim session pinned as the canonical chat pointer.
  5. Mechanism: the default profile's bot had no pin; opening it triggered the grandfather path, which adopted the roster's most-recent session (last_session = this ordinary conversation) as the canonical Bot Chat and re-pinned it. Every subsequent startup/reconnect sweep then re-hid it.

Your diagnosis — "adoption based only on recency, sweep trusts the pointer" — matches the artifact record exactly, including the grandfather path being the adoption vector.

Two gaps your patch does not close (offering companion work, not scope creep)

1. No recovery affordance for sessions already hidden. This PR prevents new wrongful hides; the ones already hidden stay hidden forever — there is no CLI, UI or documented path back out of the flag (raw SQL on state.db is currently the only recovery, which is how I restored mine). I opened the companion recovery PR #90388 (sessions unhide + list --include-hidden) — small, edges-only, reuses the existing lineage-flipping setter. Happy to adjust if you'd rather carry recovery inside this PR.

2. Old-backend edge in the sweep gate (worth a line). verifiedCanonical requires preferred.title === 'Bot Chat', and the new root_title field in the preferred-session summary only exists on the patched backend. On an older gateway (where a summary row lacks root_title without the lookup failing), a compression-rotated canonical chat — whose live tip titles Bot Chat (continued) — would fail the gate and get dropped from the sweep. Not catastrophic (worst case: an old Bot Chat stays visible, which is your stated fail-closed tradeoff), but it degrades exactly the legitimate case on the version-skew window the repo's own compatibility guidance calls out. Treating an absent root_title as "older backend: fall back to the tip title, don't treat as ownership mismatch" would close it without weakening the safety check.

Repro evidence and the full forensic record are available if useful for the PR description.

@Owen-narcissus

Copy link
Copy Markdown
Author

Second production instance (2026-08-19) + one extra persistence detail

Independent confirmation from my environment — same day, same mechanism, same recovery path. Sharing because it adds one persistence detail not covered above.

Symptom: an ordinary 80-message conversation (调度会/meeting-optimization project) vanished from the Desktop sidebar on 2026-08-19. state.db: hidden=1, archived=0 — row fully intact, invisible everywhere.

Forensic chain (all hard artifacts, matched yours 1:1):

  1. profile.yamlui_meta.hermes-bots.chat = "<victim session id>" — the ordinary session pinned as the canonical Bot Chat pointer.
  2. Desktop localStorage (bot-meta key in Electron LevelDB) carries the same pointer duplicated{"default":{"chat":"<victim id>"}}.
  3. The gateway REST patch path (api_server PATCH /api/sessions/{id} with hidden) and the startup sweep both honor the pointer; every reconnect re-hides the row.
  4. Recovery required raw SQL on state.db (UPDATE sessions SET hidden=0) — no CLI/UI path exists, exactly as you note. Strong +1 for feat(sessions): add unhide subcommand and --include-hidden to list #90388.

Extra detail you did not list — the server-side cleanup trap: clearing the server pointer does NOT stop the re-hide. I first wiped profile.yaml to ui_meta: {} (empty). That fails silently: the gateway only forwards ui_meta when it is a non-empty dict (if isinstance(ui_meta, dict) and ui_meta: in tui_gateway/methods_profiles.py), and the plugin merge (mergeServerMeta) only deletes the local chat field when bot.ui_meta["hermes-bots"] exists as an object. So the stale pointer survives in localStorage and the next sweep re-hides the session. The only server-side write that actually triggers local cleanup is a non-empty hermes-bots object without chat (e.g. ui_meta: {hermes-bots: {title: ""}}).

That two-location persistence (profile.yaml + localStorage, with an asymmetric merge) is worth a line in the PR or the companion recovery work: a recovery tool that only clears the server pointer will look like it failed. Consider having the sweep or a recovery helper also reconcile the local bot-meta copy (or document the non-empty-object requirement).

Happy to paste the exact SQL/repro steps if useful.

@teknium1

Copy link
Copy Markdown
Contributor

Salvaged into #90358 (merged as 8408edc) with your commit and authorship preserved — thank you @Owen-narcissus! Your fail-closed verify-before-hide design landed intact; we added root_title so compression-rotated Bot Chats ("Bot Chat (continued)") still verify, plus contract tests. Closing this PR in favor of the salvage.

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

Labels

comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants