Follow-up to #1092, which stops the crash but not its cause.
1. Stale Paykit session is never recovered
A stored Pubky session whose capabilities no longer satisfy requiredSessionCapabilities(paykitSdkConfig()) makes PaykitSdkService.initialize() throw:
code=identity_error, context=Pubky session is missing required Paykit capabilities: /pub/paykit/:rw,/pub/bitkit.to:rw
Nothing recovers from this:
PubkyRepo.initialize() logs the failure and returns early before the block that sets _sessionRestorationFailed, so the "session expired" toast in AppViewModel never fires. The user gets no signal and no re-auth prompt.
- Nothing clears the stale session, so it is reloaded and rejected on every launch, forever.
startAuth() already requests the correct capabilities, so a fresh authorization would succeed — the session just never gets cleared.
- Toggling Paykit off in dev settings is not a workaround:
updatePaykitEnabled(false) clears endpoints and publication state but leaves PAYKIT_SESSION/PUBKY_SECRET_KEY in the keychain.
Suggested: detect the identity error during init, clear the session access, and set _sessionRestorationFailed so the existing toast and re-auth flow trigger.
With #1092 merged, affected wallets no longer crash but their wallet backup fails permanently until this is addressed.
2. A failed init poisons every later Paykit call
PaykitSdkService.initialize() stores the failure via isSetup.completeExceptionally(t). Every method starting with isSetup.await() (currentPublicKey(), importSession(), signIn(), signUp(), identityStatus()) then rethrows that same exception for the rest of the process lifetime, since nothing re-runs initialize() after the first failure.
This is what spread a Paykit init failure into the wallet backup path.
3. No CoroutineExceptionHandler anywhere
PubkyRepo and BackupRepo both use CoroutineScope(ioDispatcher + SupervisorJob()) with no CoroutineExceptionHandler. A SupervisorJob does not handle exceptions — it only stops them propagating between siblings — so any uncaught throw inside scope.launch reaches the default handler and kills the process. There is no CoroutineExceptionHandler in the codebase at all.
Note on blast radius
The Paykit UI is gated behind a dev setting, and a session can only be created through that gated flow, so the observed trigger does not reach users who never enabled Paykit. But the crash path is not gated: BackupRepo injects PubkyRepo directly (so initialize() runs at startup for everyone) and getWalletBackupDataBytes calls backupSnapshot() unconditionally. Any Paykit init failure — corrupted state blob, native init error — would crash a normal user the same way.
Also worth checking with the Paykit SDK owners whether rc29–rc31 tightened the required capability set. If so, sessions granted before that bump break identically on other clients, which is a migration concern beyond this app.
Follow-up to #1092, which stops the crash but not its cause.
1. Stale Paykit session is never recovered
A stored Pubky session whose capabilities no longer satisfy
requiredSessionCapabilities(paykitSdkConfig())makesPaykitSdkService.initialize()throw:Nothing recovers from this:
PubkyRepo.initialize()logs the failure and returns early before the block that sets_sessionRestorationFailed, so the "session expired" toast inAppViewModelnever fires. The user gets no signal and no re-auth prompt.startAuth()already requests the correct capabilities, so a fresh authorization would succeed — the session just never gets cleared.updatePaykitEnabled(false)clears endpoints and publication state but leavesPAYKIT_SESSION/PUBKY_SECRET_KEYin the keychain.Suggested: detect the identity error during init, clear the session access, and set
_sessionRestorationFailedso the existing toast and re-auth flow trigger.With #1092 merged, affected wallets no longer crash but their wallet backup fails permanently until this is addressed.
2. A failed init poisons every later Paykit call
PaykitSdkService.initialize()stores the failure viaisSetup.completeExceptionally(t). Every method starting withisSetup.await()(currentPublicKey(),importSession(),signIn(),signUp(),identityStatus()) then rethrows that same exception for the rest of the process lifetime, since nothing re-runsinitialize()after the first failure.This is what spread a Paykit init failure into the wallet backup path.
3. No CoroutineExceptionHandler anywhere
PubkyRepoandBackupRepoboth useCoroutineScope(ioDispatcher + SupervisorJob())with noCoroutineExceptionHandler. ASupervisorJobdoes not handle exceptions — it only stops them propagating between siblings — so any uncaught throw insidescope.launchreaches the default handler and kills the process. There is noCoroutineExceptionHandlerin the codebase at all.Note on blast radius
The Paykit UI is gated behind a dev setting, and a session can only be created through that gated flow, so the observed trigger does not reach users who never enabled Paykit. But the crash path is not gated:
BackupRepoinjectsPubkyRepodirectly (soinitialize()runs at startup for everyone) andgetWalletBackupDataBytescallsbackupSnapshot()unconditionally. Any Paykit init failure — corrupted state blob, native init error — would crash a normal user the same way.Also worth checking with the Paykit SDK owners whether rc29–rc31 tightened the required capability set. If so, sessions granted before that bump break identically on other clients, which is a migration concern beyond this app.