Skip to content

Preserve session.json on transient ListenFatalError#20

Open
DiscoBard wants to merge 1 commit intoMaxGhenis:mainfrom
DiscoBard:fix/preserve-session-on-transient-disconnect
Open

Preserve session.json on transient ListenFatalError#20
DiscoBard wants to merge 1 commit intoMaxGhenis:mainfrom
DiscoBard:fix/preserve-session-on-transient-disconnect

Conversation

@DiscoBard
Copy link
Copy Markdown

Summary

OnDisconnect was firing for every libgm ListenFatalError and unconditionally calling os.Remove(SessionPath). That includes transient fatals (network blip, RPC failure, server-side hiccup), so any such event silently invalidated the saved session and required a full re-pair on the next start.

Issue #1's intent — per the closing comment — was for session.json to be removed only when the session itself is invalid (user unpaired from phone, cookies revoked). This change forwards the underlying error to the OnDisconnect callback and gates removal on a 401 / SESSION_COOKIE_INVALID classification. The macOS wrapper's needs_pairing flow continues to work for true invalidation; transient disconnects now leave session.json in place so reconnection can proceed without re-pairing.

Background

Observed on a long-running headless deployment: the bridge was paired and healthy, then went quiet for a few hours. After the next process restart, startup logged Google Messages unavailable error="load session ... session.json: no such file or directory". No user-initiated unpair, no obvious 401 in the surviving journal — the session file had been removed by an earlier transient ListenFatalError.

Implementation

  • internal/client/events.goOnDisconnect is now func(err error). The ListenFatalError handler forwards evt.Error.
  • internal/app/app.go — classifies the disconnect cause via a new isSessionInvalidated(err) helper. Only when the helper returns true does the handler call os.Remove(SessionPath) and surface "session invalidated; pair again". Other errors set "will retry with existing session".
  • internal/app/app_test.goTestIsSessionInvalidated covers nil, 401, SESSION_COOKIE_INVALID, a fmt.Errorf %w-wrapped 401, and several transient-error strings.

The classifier matches on error message substrings ("401", "SESSION_COOKIE_INVALID") since libgm surfaces the cause through the error chain rather than a typed sentinel.

Test plan

  • go build ./... — clean
  • go test ./internal/app/ ./internal/client/ — passes (existing + 7 new sub-tests)
  • Spot-check that the macOS app still transitions to the pairing view when the user unpairs from their phone (path of isSessionInvalidated == true is unchanged from current behavior)

🤖 Generated with Claude Code

OnDisconnect was wired to fire for any libgm ListenFatalError -- including
transient network or RPC failures -- and unconditionally removed
session.json. On long-running headless deployments this caused a full
re-pair to be required after every transient blip, since the session
file would not be present at the next start.

Issue MaxGhenis#1's intent was for session.json to be removed only when the
session itself is invalid (user unpaired from phone, cookies revoked).
This forwards the underlying error to the OnDisconnect callback and
gates removal on a 401 / SESSION_COOKIE_INVALID classification. Other
fatal errors now leave the session intact so the next reconnect can
succeed without re-pairing.

- internal/client/events.go: OnDisconnect signature now takes an error
- internal/app/app.go: classify error via isSessionInvalidated; only
  delete session.json on confirmed invalidation, otherwise preserve
  for retry
- internal/app/app_test.go: TestIsSessionInvalidated covers nil,
  401-status, SESSION_COOKIE_INVALID, wrapped 401, and several
  transient-error variants

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant