Skip to content

Fix re-login hang on auth page after logout#5535

Merged
FadhlanR merged 10 commits into
mainfrom
cs-12207-google-signin-logout-bug
Jul 22, 2026
Merged

Fix re-login hang on auth page after logout#5535
FadhlanR merged 10 commits into
mainfrom
cs-12207-google-signin-logout-bug

Conversation

@FadhlanR

@FadhlanR FadhlanR commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

After an in-app logout → re-login (logout is a router.transitionTo, not a page reload), several things break. They share one root cause: the host centralizes session teardown but not session re-initialization. ResetService.resetAll() fanned resetState() out to ~30 services on logout, but there was no symmetric "session started" broadcast — re-initialization lived in one-shot sites (service constructors, boot-only loadSDK(), the index route's start guard, user clicks). So anything torn down in resetState() whose re-establishment lived in a one-shot site stayed broken for the life of the singleton after re-login.

Three confirmed instances of this family:

  1. Auth page hangs on re-login. #clientReadyDeferred (a boot-only barrier) was recreated unfulfilled by resetState(); the next login's createRealmSession() awaited it forever.
  2. AI assistant panel spins forever. loadRoomsTask was armed only in the panel service constructor and the openPanel() click. Logout cancelled it and cleared currentRoomId; nothing re-armed it, so isReady never became true (perpetual loading indicator).
  3. Billing push updates go silently dead. RealmServerService.resetState() wiped the eventSubscribers map, but BillingService subscribes to billing-notification only in its constructor — which never re-runs after an in-app logout.

Approach

Classify every piece of host state as app-scoped (page-load → page-unload: SDK bundle, event-binding tables, subscriber wiring) or session-scoped (login → logout: tokens, room caches, subscription data, current room). The enforceable contract:

Anything you tear down in resetState() must be re-established in sessionStarted() or derived lazily; anything that should survive logout doesn't belong in resetState() at all.

All three bugs are misclassifications under this model.

ResetService becomes SessionService — a single session authority:

  • @tracked isAuthenticated, the single source of truth for "a session is established" (written only by MatrixService, read everywhere; MatrixService.postLoginCompleted folds into it).
  • Symmetric notifySessionStarted() / notifySessionEnded() broadcasts. MatrixService.start() fires the former only on the unauthenticated → authenticated edge — so re-running start() on an already-established session (e.g. the /connect route, which runs it on every visit) doesn't re-fire participants; logout() fires the latter (replacing resetAll()).
  • Replay-on-register: registering while a session is already established replays sessionStarted(), so participants need no constructor arming and the hook runs exactly once per participant per session regardless of construction timing.
  • Per-participant error isolation on both edges (a throwing resetState()/sessionStarted() no longer skips the rest of the registry).

MatrixService is the orchestrator, not a participant, which also ends the double-resetState() it previously ran on logout. On logout it now tears down its own session state synchronously, before the awaited network logout, so an interleaved re-login or a late /sync response can't act on a half-reset session.

The fixes

  • Bug 1 (already on this branch): resetState() leaves the once-fulfilled #clientReadyDeferred alone rather than recreating it unfulfilled.
  • Bug 2: the panel re-arms via sessionStarted()ensureRoomsLoaded(); the remount-dependent constructor arming is removed. openPanel() keeps performing loadRoomsTask unconditionally (an explicit open must reselect the persisted/latest room); ensureRoomsLoaded()'s "leave the current room alone" guard is only for the passive session-start hook.
  • Bug 3: RealmServerService.resetState() no longer clears the app-scoped eventSubscribers; BillingService.sessionStarted() repopulates its session-scoped data on re-login.

Audit / hygiene

  • matrix-sdk-loader is removed from the participant registry — the loaded SDK bundle is app-scoped and must survive logout (its resetState() clearing was a latent bug).
  • message-service's subscriber-map wipe is kept (its callbacks are component/resource-scoped and die with the <Auth/> swap) but documented as a test-environment safety net, distinct from the app-scoped wiring in realm-server.
  • The remaining ~20 resetState() implementations are confirmed either explicitly re-driven by start() or lazily derived, so they need no hook.

Testing

  • New: session-test.ts (broadcasts reach all participants, error isolation both directions, isAuthenticated flips, replay semantics); ai-assistant-panel-relogin-test.ts (panel re-enters a room after logout → re-login instead of spinning); realm-server-subscriber-test.ts (subscribers survive resetState()). Bug 1's matrix-service-relogin-test.ts remains.
  • pnpm lint on @cardstack/host is green (lint:js, lint:types/glint, lint:hbs).
  • The browser suite (integration/acceptance, plus the matrix E2E re-login spec) runs on CI — worth watching the panel re-login integration test, which drives real room creation.

Out of scope

Reload-on-logout (window.location.replace('/') instead of an in-app transition, matching ember-simple-auth's default) is a separate product decision. The session registry here is needed regardless (tests, the failed-boot start() → logout() path, any future soft-logout).

🤖 Generated with Claude Code

…tate)

MatrixService is a singleton whose client-ready barrier
(#clientReadyDeferred) was only ever fulfilled once, at boot in loadSDK().
Logout stays in-app (a router transition, not a page reload) and its finally
runs resetState(), which recreates the client synchronously but then replaced
#clientReadyDeferred with a fresh, never-fulfilled Deferred. The next login
awaits that deferred in createRealmSession(), so it never resolves and the auth
page hangs.

Fulfill the freshly created deferred when _client exists, restoring the
invariant "the deferred is fulfilled whenever _client exists" while keeping the
memory-cleanup intent of a stale-reference-free deferred. Add an integration
regression test covering re-login after a resetState().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

  1 files  ±    0    1 suites  ±0   5m 21s ⏱️ - 2h 54m 11s
152 tests  - 3 412  152 ✅  - 3 397  0 💤  - 15  0 ❌ ±0 
152 runs   - 3 431  152 ✅  - 3 416  0 💤  - 15  0 ❌ ±0 

Results for commit c7f5e4a. ± Comparison against earlier commit f9f2ee2.

Realm Server Test Results

    1 files  ± 0      1 suites  ±0   11m 48s ⏱️ - 5m 19s
1 913 tests +11  1 913 ✅ +11  0 💤 ±0  0 ❌ ±0 
1 992 runs  +11  1 992 ✅ +11  0 💤 ±0  0 ❌ ±0 

Results for commit c7f5e4a. ± Comparison against earlier commit f9f2ee2.

FadhlanR and others added 8 commits July 17, 2026 20:53
resetState() now leaves the client-ready barrier untouched instead of
recreating and immediately fulfilling it — a once-fulfilled deferred stays
fulfilled, and the reset recreates the client synchronously whenever the
SDK is loaded, so the two are equivalent.

The new matrix E2E test covers the full in-app logout -> login-form ->
re-login flow that the host integration test can't: logout is a router
transition, not a page reload, so the second login runs against the
client that resetState() recreated. The test waits for logout's
transition to index-root before filling the form, since that transition
remounts the login form and would wipe an earlier fill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The host centralized session teardown (ResetService.resetAll fanning out
resetState to ~30 services) but had no symmetric re-initialization. Because
logout is an in-app router transition (no page reload), anything torn down in
resetState whose re-establishment lived in a one-shot site (constructor, boot)
stayed broken after an in-app logout -> re-login.

Evolve ResetService into SessionService: a single tracked isAuthenticated
authority (written only by MatrixService), symmetric notifySessionStarted /
notifySessionEnded broadcasts, replay-on-register so sessionStarted runs
exactly once per participant per session regardless of construction timing,
and per-participant error isolation on both edges.

Fixes two re-login bugs that were misclassifications under the two-scope model:
- AI assistant panel spun forever: loadRoomsTask was armed only in the
  constructor / openPanel click. It now re-arms via sessionStarted ->
  ensureRoomsLoaded.
- Billing push updates went silently dead: realm-server.resetState wiped the
  app-scoped eventSubscribers map. That wipe is removed; billing repopulates
  its session-scoped data via sessionStarted.

Also reclassifies matrix-sdk-loader (app-scoped SDK, no longer a participant)
and documents message-service's subscriber wipe. MatrixService is no longer a
participant (it orchestrates the broadcasts), ending the double-reset on
logout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two regressions from the session-lifecycle refactor, both in the AI panel:

- The panel constructor registered with SessionService before calling
  resetState(). register() can synchronously replay sessionStarted(), which
  arms loadRoomsTask; resetState() then cancelled it. Reorder so resetState()
  runs first and the replayed arm survives — this is what enters a room when
  the panel renders already-open.
- openPanel() went through ensureRoomsLoaded(), whose "leave the current room
  alone" guard is only meant for the passive session-start hook. On an explicit
  open it must always re-run enterRoomInitially so the panel reselects the
  right room (the persisted room may have changed while closed). Restore the
  unconditional loadRoomsTask.perform().

Also move the Bug 3 subscriber-preservation regression test from a bare unit
test to an integration test: the realm-server constructor's fire-and-forget
fetchCatalogRealms() rejects without the virtual network that the mock-matrix
harness wires up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The <Room> component's constructor hydrated persisted optimistic sends
synchronously, which writes the room's tracked `_events`. Once a room is
entered earlier in boot (the session-start hook now loads rooms during the
route model rather than at panel-component construction), that write lands in
the same render computation that already read `_events` in the parent panel,
tripping Ember's backtracking assertion. Schedule the hydration in
`afterRender` (guarded against teardown) so the write happens in a separate
computation. Behavior is unchanged — the failed-bubble / retry-alert state
still appears once rendering settles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rors in tests

- BillingService.loadSubscriptionData() coalesces concurrent callers into a
  single in-flight /_user fetch, so the session-start hook and component
  mounts no longer stack duplicate requests at boot
- SessionService rethrows participant errors under isTesting() after the
  full broadcast completes: production keeps per-participant isolation,
  while a throwing resetState() in tests fails the responsible test instead
  of silently leaking state into a later one
- Document that session participants must be app-lifetime singletons (no
  unregister exists) and that all isAuthenticated writes are driven by
  MatrixService

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
loadSubscriptionData() always fetches fresh again, so callers reacting to a
server-side change (billing-notification pushes, out-of-credits reloads)
never get satisfied by a response that predates the change. The boot-time
dedupe moves to initializeSubscriptionData(), which coalesces the
session-start hook and <WithSubscriptionData/> mounts onto one in-flight
/_user fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The coalescing added for a presumed boot double-fetch caused a regression:
<WithSubscriptionData/> only mounts inside the profile popover / settings
modal (both closed at boot), so there was never a boot double-fetch to
dedupe. Routing its mount through initializeSubscriptionData() made it
early-return on already-present data, so the popover stopped re-fetching on
open and displayed stale credits after a billing-notification push
(account-popover acceptance test).

Restore origin/main behavior: loadSubscriptionData() always fetches fresh
(pushes, error-reloads, and every popover mount observe current server
state), initializeSubscriptionData() is the simple present-or-load guard.
The only net change vs main stays the additive sessionStarted() eager
repopulate on re-login.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…teardown, async participant-error surfacing

Addresses review findings on the SessionService refactor:

- start() now broadcasts notifySessionStarted() only on the
  not-authenticated -> authenticated edge. start() re-runs on an already
  established session (the /connect route runs it every visit); the old
  unconditional broadcast re-fired every participant's sessionStarted()
  with no intervening session end, breaking the "exactly once per
  established session" contract and duplicating re-arm work.

- logout() tears down MatrixService's own session state synchronously,
  before the awaited network logout, instead of deferring it to the
  finally. Deferring left a window where an in-flight /sync response could
  run against already-reset participants, and where a re-login completing
  during the await would be silently wiped by the deferred reset. The old
  client is captured before the reset, so the network logout is unaffected.

- SessionService re-raises participant errors asynchronously (floated
  rejection) instead of throwing synchronously. Both broadcast sites call
  the notify methods inside their own try/catch, which swallowed the
  synchronous throw and let a broken participant pass in tests. The floated
  rejection can't be caught by the callers and is failed by the harness's
  unhandledrejection hook. Exposed via an overridable seam so unit tests
  observe the surfaced errors without the rejection failing them.

- Broadcast loops iterate a snapshot of participants so a sessionStarted()
  that lazily first-injects another participant can't cause a double
  invocation in one broadcast.

- setupRenderingTest resets the AI panel before MatrixService so an
  in-flight loadRoomsTask is cancelled before resetState() replaces the
  initial-sync barrier, avoiding a teardown hang.

- Document why billing loadSubscriptionData() stays always-fresh and is
  deliberately not coalesced (coalescing was tried and reverted because it
  showed stale credits in the profile popover).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR
FadhlanR marked this pull request as ready for review July 21, 2026 13:39
@FadhlanR
FadhlanR requested a review from a team July 21, 2026 13:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9f2ee2d92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// an explicit catch: the SessionService broadcast's try/catch only guards
// synchronous throws, so an unhandled fetch rejection would otherwise
// escape here.
this.loadSubscriptionData().catch((e) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Coalesce the billing load started by session replay

When BillingService is first touched by the index route after matrixService.start(), session.register(this) replays sessionStarted() because session.isAuthenticated is already true. This line starts loadSubscriptionData() fire-and-forget; immediately afterward routes/index.gts calls initializeSubscriptionData() and, since subscriptionData is still null while the first request is in flight, issues a second _user request. The same duplicate fetch happens after re-login because resetState() clears the data before the session-start broadcast, so this should share an in-flight/initialized path or avoid the eager load when the route will initialize it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖]

Traced and confirmed — efficiency, not correctness. We're keeping this as-is rather than coalescing. The eager load is the only path that repopulates the cleared subscription data on re-login (where the index route's model() may not re-run), and coalescing was tried and reverted earlier on this branch because routing the profile popover through the present-or-load path showed stale credits after a billing-notification push.

Documented the accepted duplicate at the sessionStarted() call site in c7f5e4a so the decision is visible here rather than only on loadSubscriptionData().

@habdelra habdelra 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.

[Claude Code 🤖]

Review lens: I focused on the session-lifecycle contract this PR introduces — whether every resetState() teardown has a matching re-establishment, whether the app-scoped vs. session-scoped reclassifications are correct, and whether the new broadcast edges (start-success, logout, replay-on-register) each fire exactly once without racing an in-flight /sync or a re-login. I checked out the head branch and traced each edge through the code.

Bottom line: no blocking issues. The refactor is sound and all three bug fixes are real and verified. Auth-state accounting is consistent (every isAuthenticated write routes through MatrixService, and #clientReadyDeferred correctly survives resetState()), no lingering reset-service references remain anywhere, and the ~20 trivial reset → session renames are behavior-preserving — those services define no sessionStarted(), so replay-on-register is a no-op for them, making the blast radius genuinely small. One minor efficiency nit and two test-coverage follow-ups, all non-blocking.

What lands right (mechanistically):

  • Bug 1 — verified: #clientReadyDeferred is fulfilled exactly once in loadSDK(), and createRealmSession() awaits it. Leaving it untouched in resetState() (which recreates _client synchronously whenever the SDK is loaded) preserves the "fulfilled whenever a client exists" invariant; the old fresh-Deferred stranded every post-logout await forever.
  • Bug 3 — verified: subscribeEvent() has exactly one caller (BillingService's constructor, an app-lifetime singleton), so preserving eventSubscribers across resetState() is correct. The integration test drives handleEvent() after a resetState() and asserts delivery — a real pin, not a shape assertion.
  • Edge-guarded broadcast — capturing wasAuthenticated before setPostLoginCompleted(true) is the correct ordering; a /connect re-run of start() on an already-established session correctly no-ops the broadcast.
  • Synchronous logout teardown — capturing client before resetState() keeps the network logout pointed at the old client while closing the window where an in-flight /sync or a re-login-during-await could act on a half-reset session.
  • The inline comments throughout are unusually good: they explain the why at each seam, which is what makes a refactor this broad reviewable.

On the open Codex thread (billing double-fetch): confirmed real and traced — it's a minor efficiency issue, not correctness. Mechanism plus a narrow fix that avoids the previously-reverted stale-popover regression is in the inline comment on billing-service.ts.

Recommendations (all non-blocking):

  1. Billing boot double-fetch — see inline on billing-service.ts sessionStarted().
  2. Add a unit test for the snapshot-iteration double-invocation guard — see inline on session.ts notifySessionStarted(). The same test file could also pin the start-success edge guard (assert a second start() on an established session does not re-broadcast).
  3. Consider whether the test-only participant-error surfacing (reraiseParticipantErrorsInTests → floated Promise.reject) can misattribute failures: it runs inside notifySessionEnded() in the afterEach teardown, so a rejection floated there may be observed by the next test's unhandledrejection hook rather than the one whose resetState() actually threw. The loud-failure intent is right; worth confirming the attribution is deterministic. CI is currently green.

Adjacent, out of scope: the ai-assistant-panel-relogin-test drives matrixService.resetState() before session.notifySessionEnded(), the reverse of logout()'s real order (notifySessionEnded() then resetState()). The end state is identical so the test is valid, but the accompanying comment describes the production order. Not asked of this PR.


Generated by Claude Code

// an explicit catch: the SessionService broadcast's try/catch only guards
// synchronous throws, so an unhandled fetch rejection would otherwise
// escape here.
this.loadSubscriptionData().catch((e) => {

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.

[Claude Code 🤖]

Confirmed: a duplicate /_user fetch fires at boot. Verified end-to-end, non-blocking, low severity. This is the Codex P2 lead, traced.

The mechanism. The index route's model() does await this.matrixService.start(); start() fires notifySessionStarted() on the auth edge, which reaches BillingService.sessionStarted() here and kicks off loadSubscriptionData() fire-and-forget (fetch #1). A few lines later the same model() calls this.billingService.initializeSubscriptionData(); subscriptionData is still null because fetch #1 hasn't resolved, so it falls through to a second loadSubscriptionData() (fetch #2). Two GETs to /_user, same server state, last-writer-wins on _subscriptionData — so this is efficiency, not correctness, and it's newly introduced here (before, only the route fetched).

Why the documented anti-coalescing rationale doesn't quite cover this. The comment on loadSubscriptionData() says coalescing was reverted because it made the profile popover show stale credits. But the popover mounts <WithSubscriptionData>, whose constructor calls loadSubscriptionData() directly, not initializeSubscriptionData(). So the boot double-fetch lives entirely on the sessionStarted() + route-initializeSubscriptionData() pair. Coalescing those two (in-flight tracking inside initializeSubscriptionData(), leaving loadSubscriptionData() always-fresh for the popover) would drop the duplicate without reintroducing the stale-popover regression.

Scope. Non-blocking follow-up. Don't just delete the eager load — it's genuinely needed on re-login, where the route model() may not re-run and sessionStarted() is the only thing that repopulates the cleared data. If one extra boot request isn't worth the machinery, leaving it is defensible; flagging so the choice is deliberate rather than incidental.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖]

Agreed on all points, and thanks for tracing the narrower initializeSubscriptionData()-only coalescing — you're right that it would avoid the reverted stale-popover regression (the popover calls loadSubscriptionData() directly). We're still choosing to leave it: one duplicate /_user at boot isn't worth reintroducing the in-flight machinery, and the eager load has to stay for the re-login repopulation path.

Made the choice explicit in c7f5e4a — added a comment at sessionStarted() noting the accepted duplicate and pointing at the revert rationale on loadSubscriptionData().

// register() and appends to `participants`. Iterating the live array would
// then reach that late registrant here *and* it already ran via register()'s
// replay — a double sessionStarted() in one broadcast.
for (let p of [...this.participants]) {

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.

[Claude Code 🤖]

Confirmation + a test-coverage gap on this exact guard. Iterating a snapshot ([...this.participants]) is the right call, and the inline comment explains the mechanism precisely: since isAuthenticated is set true at the top of this method, a participant whose sessionStarted() lazily first-injects another participant service triggers that service's constructor → register(), and register() replays sessionStarted() immediately (because the session is already established). Without the snapshot, the live-array loop would then also reach that late registrant — a double sessionStarted() in one broadcast.

The gap. session-test.ts covers replay-on-register, error isolation on both edges, and isAuthenticated flips — but nothing exercises this double-invocation guard. Reverting to for (let p of this.participants) would pass the entire existing suite while silently re-introducing the double invocation (extra re-arm work, no crash) — exactly the kind of regression that needs a pin.

Concrete test. Register participant A whose sessionStarted() calls session.register(B); after notifySessionStarted(), assert B.startedCount === 1 (not 2). The same shape covers the notifySessionEnded() snapshot on the teardown edge.

Class: follow-up (test coverage), non-blocking.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖]

Added in c7f5e4a. Two unit tests pin the guard on both edges:

  • notifySessionStarted(): register A whose sessionStarted() calls session.register(B), then assert B.startedCount === 1 — the replay only, not a second invocation from the loop.
  • notifySessionEnded(): symmetric shape asserting a participant registered mid-teardown is not reached by the same resetState() pass.

Reverting either broadcast to a live-array for…of now fails these.

… snapshot-guard + edge-guard tests

Non-blocking follow-ups from the approving review:

- SessionService buffers participant errors in tests instead of floating a
  rejection. A floated Promise.reject surfaced non-deterministically — the
  unhandledrejection could be attributed to the next test rather than the one
  whose participant threw. The test harness (setup.ts afterEach) now drains the
  buffer after the teardown broadcast and fails the responsible test
  deterministically, and it still can't be swallowed by the broadcast callers'
  try/catch.

- Add unit tests pinning the snapshot-iteration guard on both broadcast edges:
  a participant registered mid-broadcast gets exactly one sessionStarted()
  (the replay), not a second from the loop; and one registered mid-teardown is
  not reset by the same notifySessionEnded() pass. Reverting to a live-array
  loop would silently regress these.

- Add an integration test pinning the start-success edge guard: a second
  start() on an already-authenticated session (the /connect re-run) does not
  re-broadcast sessionStarted().

- Document at BillingService.sessionStarted() that the cold-boot duplicate
  /_user fetch (alongside the index route's initializeSubscriptionData) is an
  accepted cost, not a bug to coalesce: the eager load is what repopulates the
  cleared data on re-login, and coalescing was tried and reverted because it
  regressed the profile popover to stale credits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖]

Thanks for the thorough pass. Addressed the follow-ups in c7f5e4a (all non-blocking):

On the adjacent note — ai-assistant-panel-relogin-test driving resetState() before notifySessionEnded(), the reverse of logout()'s real order: left as-is since the end state is identical, but happy to reorder the test + its comment to mirror production if you'd prefer.

@FadhlanR
FadhlanR merged commit c99c849 into main Jul 22, 2026
97 of 101 checks passed
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