Skip to content

fix(core): harden session, settings, and realtime lifecycle races - #95

Draft
Kiro-XVI wants to merge 2 commits into
XeldarAlz:masterfrom
Kiro-XVI:fix/session-settings-lifecycle
Draft

fix(core): harden session, settings, and realtime lifecycle races#95
Kiro-XVI wants to merge 2 commits into
XeldarAlz:masterfrom
Kiro-XVI:fix/session-settings-lifecycle

Conversation

@Kiro-XVI

@Kiro-XVI Kiro-XVI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What

Fixes several session, settings, and telephony lifecycle bugs on master: Follow Character toggle fighting itself, decrypted DM plaintext surviving sign-out, KeyVault not refreshing on account switch, Confirm/incoming-call UI mutated off the Framework thread, RealtimeConnection Stop/Start races, privacy settings refetch storms, RegionSync no-op after SignIn, and related settings/clock/call edge cases.

Why

These bugs are present on upstream master and cause broken account follow, leftover chat plaintext after sign-out, dropped/corrupt confirm dialogs, overlapping websocket loops on account switch, and settings pages that hammer MeAsync every frame after a failed load.

Closes #

How to test

  • Open Settings → Account with more than one account: turn on Follow Character and confirm it stays on across frames.
  • Sign out while a DM thread with decrypted messages is open: confirm messages/key state clear.
  • Switch between signed-in accounts: confirm KeyVault refreshes for the new identity.
  • Trigger an incoming call / stream decline alert and confirm the confirm overlay still works.
  • Open Settings privacy / Tags & Mentions / Aethergram message privacy with the network offline briefly: confirm it does not refetch every frame forever.
  • Toggle 12-hour clock and confirm Clock alarms follow TimeText.
  • Open Appearance with HomeGridRows previously set to 8: confirm it does not silently rewrite to 6 until you change density.

Checklist

  • dotnet build -c Release passes
  • Verified in-game: opened the phone and exercised the affected app/screen
  • If this changes user-visible behavior, README is updated
  • Matches existing style: uses the shared Windows/Components/ widgets, no what comments

Made with Cursor

Kiro-XVI and others added 2 commits August 6, 2026 09:59
Stop Follow Character fighting itself, clear DM/key state across account changes, marshal confirms and incoming calls onto Framework, and fix realtime Stop/Start plus settings privacy retry storms.

Co-authored-by: Cursor <cursoragent@cursor.com>
Ask/Proceed were null-refing Plugin.Framework outside the game, which broke ModerationNoticeTests on CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

@XeldarAlz XeldarAlz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Build: dotnet build -c Release clean, 512/512 tests pass on ab98f8f. Not verified in-game. Two blockers inline (realtime Stop joins on the Framework thread, privacy pages latch loaded on failure); the rest are non-blocking.

{
try
{
toWait.Wait(StopJoinTimeout);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: AethernetSession raises Changed inside RunOnFrameworkThread and CallHub.Reconcile subscribes to it, so router.Stop() runs on the Framework thread. Sign-in, sign-out, account switch and 401 now stall the game up to 2s here (CallHub.SetEnabled reaches it from the settings toggle too). The generation guard already covers the race the join was meant to close, so drop the Wait or move Stop off the main thread.

catch (Exception exception)
{
AepLog.Warning($"Chat privacy load failed: {exception.Message}");
chatPrivacyLoaded = true;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: latching on failure kills the refetch storm but makes the failure permanent for the session. The page then renders the shareReadReceipts/sharePresence defaults (both true) as authoritative, and Push sends both booleans, so flipping one silently overwrites the other server-side. Latch only when me is not null, and give the failure path a retry cooldown instead.

catch (Exception exception)
{
AepLog.Warning($"Tag privacy load failed: {exception.Message}");
loaded = true;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: same shape as PrivacyPage. After a failed load the page shows default policies as real values, and PushTags sends tagPolicy and requireTagApproval together, so touching one overwrites the other with a default. Same fix: cooldown rather than a permanent latch.

return false;
}

if (localSlot < 0)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Non-blocking: CallHub.HandleRoster still sets state = CallState.Active when this returns false, so a roster that omits the local slot leaves the call Active with no audio session and no elapsed timer, and nothing retries until the next roster frame.

{
session.SignIn(auth.Token, auth.User);
signedIn?.Invoke();
_ = Plugin.Framework.RunOnFrameworkThread(() => signedIn?.Invoke());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: Reset() now runs before signedIn fires, so handlers observe the cleared flow state instead of the post-verify state. Also Plugin.Framework is unguarded here while ConfirmService gained a null guard in ab98f8f; worth keeping the two consistent.

@@ -67,6 +67,5 @@ public void Run(
public void Dispose()
{
cancellation.Cancel();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: dropping cancellation.Dispose() is the right call since in-flight tasks still hold the token, but it reads as an accidental leak. A one-line why comment would stop someone re-adding it.

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