Skip to content

The next account signed in on a tab does not see the previous account's chat draft, conversation or answers - #1238

Merged
WaylandYang merged 3 commits into
deeplethe:devfrom
Maya-Kid:fix/sign-out-clears-chat-state
Oct 16, 2026
Merged

WaylandYang merged 3 commits into
deeplethe:devfrom
Maya-Kid:fix/sign-out-clears-chat-state

Conversation

@Maya-Kid

@Maya-Kid Maya-Kid commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

#533 made sign-in clear the query cache. Its acceptance list asked that signing out and signing in as another user in the same tab leave "no trace of the first user's name, bases or alerts". Chat keeps state outside that cache, and all of it survives both.

Problem

What survives:

  • The unsent draft (chat:draft in sessionStorage). The next account's composer opens with it, on any base.
  • The last conversation per base (chat:last:<kb>) and the answers the page holds in memory (liveAnswer, which keeps a finished answer until the next question).
    • Opening Chat on a base both accounts can read moves the next account to the previous account's conversation and shows its turns from memory. The server answers 404 to that account for the same conversation, because conversation_detail filters by user_id.
    • An answer still being written keeps streaming into the page.
  • The unread marks (utopia.unread and the in-memory sets) and the remembered workspace and base (utopia.ws, utopia.kb).

Single sign-on reloads the page, so memory goes, but sessionStorage stays: the draft and the last-conversation keys come back.

To reproduce on dev 70bf7a7:

  1. Accounts A and B can both read base K.
  2. As A, ask a question in K, then type a second one without sending.
  3. Sign out, sign in as B in the same tab, and open Chat on K.

The composer holds A's text, and the page shows A's conversation.

Change

  • Whose state it is. utopia.account records the signed-in user's id in sessionStorage, for what the tab keeps (draft, last-conversation keys, answers in memory), and in localStorage, for what tabs share (unread marks, remembered workspace and base). One copy alone is not enough:
    • with only localStorage, a tab A left open hands A's draft to B after B signs in in another tab and the tab reloads;
    • with only sessionStorage, every new tab of the same person wipes the shared marks.
  • When it is checked. claimFor(user.id) clears a scope only when its owner is someone else or nobody. It runs in two places:
  • Sign-out. clearSessionState clears everything, including the query cache, and forgets the owner.
  • Answers. liveAnswer.clear() drops every entry, then disconnects its readers. It sends no Stop, so the server finishes and saves the previous account's answer. Unread marks are cleared after the answers, because dropping a running answer marks it unread.

Verification

macOS (arm64), Node 26.3 (CI uses Node 20), pnpm 10.2.1. authSession.test.ts drives the real sign-out and sign-in handlers and fetchMe, with a real QueryClient and the real chat stores. The previous account claims the tab through fetchMe and leaves a base, a draft, a last conversation, a finished unread answer and a running one.

  • clears all of it on sign-out.
  • clears it when another account signs in.
  • keeps it when the same account signs back in after its session expired: fails on this PR's first commit, which cleared on every sign-in.
  • drops a tab's draft on reload after another account signed in in another tab: the new account's shared state stays.

On dev 70bf7a7, with fetchMe stubbed to api.me, all three tests that expect a clear fail. Removing either owner write, or clearing unread marks before answers, also fails a test. liveAnswer.stream.test.ts adds one test: clear() aborts the reader and sends no Stop.

pnpm test: 38 files, 276 tests passed. pnpm build passes.

Manual check on a local server, two accounts sharing a base:

  • On dev, B finds A's unsent text in the composer.
  • On this branch, A signing in again keeps the draft, and B signing in finds the composer empty.

Limits

  • The first load after this ships finds no owner, so each tab starts fresh once.
  • An open tab notices a sign-in in another tab only on its next me fetch. me never goes stale, so that means a reload; until then the tab shows the previous person's state.
  • The previous account's running answers are not stopped. They finish and are saved.

…'s chat draft, conversation or answers

Signed-off-by: dada-yan <BinjunYann@gmail.com>

@WaylandYang WaylandYang left a comment

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.

Thank you for the thorough write-up and the tests that drive the real handlers. The coverage is complete: answers, unread marks, workspace/base, draft, last-conversation keys and the cache are everything the tab keeps for a person, and the clearing order with liveAnswer.clear → convMarks.clear is right.

One change before merging. Tying the clear to "a sign-in happened on this page" causes the regression you list under Limits: the same person whose session expired loses their unsent draft and last conversations. It also misses sign-ins this page doesn't see, such as SSO started from a link or a proxy, or a sign-in in another tab. Please tie it to who is signed in instead: remember the owning user id (e.g. utopia.account), and in the me queryFn and in Login onSuccess clear the tab's state only when that id differs from the signed-in user. Shell doesn't render children until me resolves, so the check runs before Chat reads the draft. Keep the full clear on explicit sign-out. Then the SSO-button hook isn't needed, and a test for "same user signs back in, draft kept" would cover the regression.

…ry sign-in

Signed-off-by: dada-yan <BinjunYann@gmail.com>
@Maya-Kid

Copy link
Copy Markdown
Contributor Author

Thank you. Done in c27c026.

  • The tab now belongs to whoever me says is signed in. fetchMe, which every ["me"] query uses, and Login onSuccess call claimFor(user.id), which clears only when the recorded owner is someone else or nobody.
  • Explicit sign-out still clears everything and forgets the owner.
  • The SSO-button hook is gone.

One change from a single key: the owner is recorded twice under utopia.account.

  • sessionStorage covers what the tab keeps: the draft, the last conversations and the answers in memory.
  • localStorage covers what tabs share: the unread marks and the remembered workspace and base.

Either copy alone fails one case:

  • With only the localStorage copy, a tab A left open would hand A's draft to B after B signs in in another tab and the tab reloads.
  • With only the sessionStorage copy, every new tab the same person opens would wipe the shared unread marks and remembered base.

"Same account signs back in, draft kept" is a test now. It fails on the first commit. I also checked both sign-in cases by hand on a local server.

@WaylandYang WaylandYang left a comment

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.

Thank you, this is the right shape. Recording the owner per tab and per browser separates what belongs to the tab from what all tabs share, and running the claim in fetchMe covers sign-ins this page never sees, as well as a sign-in in another tab once this one reloads. The same person signing back in now keeps their draft, and the four tests cover sign-out, another account, the same account, and another tab. Please update the description: the SSO-redirect hook and the draft-loss limit no longer apply. Merging.

@WaylandYang
WaylandYang merged commit 2293ec9 into deeplethe:dev Oct 16, 2026
6 checks passed
@Maya-Kid
Maya-Kid deleted the fix/sign-out-clears-chat-state branch October 16, 2026 08:14
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