Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ab59838
fix: stop automatic profile deletion replay
ben-kaufman Oct 24, 2026
99b446a
docs: associate deletion fix with PR 1688
ben-kaufman Oct 24, 2026
a8f0810
fix: preserve pending backups across restart
ben-kaufman Oct 25, 2026
e585fcc
fix: retain backup changes while restart drains
ben-kaufman Oct 25, 2026
c255458
Merge restore recovery parent into backup lifecycle fix
ben-kaufman Oct 26, 2026
a102e0e
fix: recognize native backup request transport failures
ben-kaufman Oct 26, 2026
49e9a99
fix: retain explicit backup retry ownership
ben-kaufman Oct 26, 2026
5fe0bf6
fix: integrate current restore recovery parent
ben-kaufman Oct 28, 2026
5fca5cd
fix: integrate current restore recovery parent
ben-kaufman Oct 28, 2026
e80ca3f
Merge updated restore identity parent into backup lifecycle
ben-kaufman Oct 29, 2026
9a837a6
fix: keep recovery callbacks ahead of backup debounce
ben-kaufman Oct 29, 2026
2aac53e
Merge current identity recovery into profile session recovery
ben-kaufman Oct 29, 2026
1d3ba14
Merge current identity recovery parent into backup observers
ben-kaufman Oct 29, 2026
ff5aa32
Merge restored identity guards into profile deletion recovery
ben-kaufman Oct 30, 2026
9cfe052
Merge restore metadata guards into backup observer lifecycle
ben-kaufman Oct 30, 2026
cfe7daf
Merge validated identity restore parent
ben-kaufman Nov 1, 2026
9226253
Merge restored-identity allowance admission
ben-kaufman Nov 1, 2026
a3693b0
Merge stable allowance activation into backup observation
ben-kaufman Nov 1, 2026
2eb0c49
Merge guarded identity and allowance recovery parent
ben-kaufman Nov 1, 2026
f068aba
Merge current allowance resume fix into recovery branch
ben-kaufman Nov 1, 2026
439b5d0
merge: include identity recovery diagnostics
ben-kaufman Nov 1, 2026
add2c04
merge: integrate current identity recovery
ben-kaufman Nov 3, 2026
8727da0
fix: initialize cold explicit backup retries
ben-kaufman Nov 3, 2026
e7fb0ac
fix: retry backup authentication transport failures
ben-kaufman Nov 3, 2026
1a6048a
fix: retain backup ownership across wallet transitions
ben-kaufman Nov 4, 2026
2fa4098
merge: compose queued backup status
ben-kaufman Nov 4, 2026
1cf5d39
merge: compose admitted backup status
ben-kaufman Nov 4, 2026
a60add9
Merge the current restore identity branch into backup observation
ben-kaufman Nov 9, 2026
be4e1ba
fix: retry wallet and USDT recovery uploads after VSS setup times out
ben-kaufman Nov 9, 2026
f8b2ffc
Preserve full activity uploads across backup observation
ben-kaufman Nov 9, 2026
534b3d8
Preserve Spark backups in admitted observation lifecycle
ben-kaufman Nov 11, 2026
9f60111
fix: compose spark restore backup recovery
ben-kaufman Nov 11, 2026
e0d2598
fix: preserve explicit profile deletion recovery
ben-kaufman Nov 11, 2026
c53a459
fix: preserve profile ownership during recovery
ben-kaufman Nov 11, 2026
62c8cf2
fix: retain recovery guards in the composed parent
ben-kaufman Nov 12, 2026
66ba140
fix: retain recovery with spark balance updates
ben-kaufman Nov 12, 2026
4024e8b
fix: preserve recovery with startup cache loading
ben-kaufman Nov 12, 2026
ac1549f
fix: preserve recovery with node timeout balance updates
ben-kaufman Nov 12, 2026
afb4f06
fix: preserve recovery across parent changes
ben-kaufman Nov 12, 2026
43e314c
fix: preserve recovery with finished-exit balance updates
ben-kaufman Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class UsdtRecoveryBackupStore(
) {
constructor(client: VssBackupClient) : this(
put = { key, data ->
client.setup().getOrThrow()
client.ensureSetupForUpload()
client.putObject(key, data).getOrThrow()
},
keys = {
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import to.bitkit.data.keychain.Keychain
import to.bitkit.di.IoDispatcher
import to.bitkit.env.Env
import to.bitkit.ext.runSuspendCatching
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import javax.inject.Inject
Expand All @@ -28,6 +29,17 @@ import kotlin.time.Duration.Companion.seconds

class MnemonicNotAvailableException : Exception("Mnemonic not available")

internal class BackupClientSetupTimeout(cause: TimeoutCancellationException) :
AppError("Backup client setup timed out", cause)

internal suspend fun VssBackupClient.ensureSetupForUpload() {
val result = setup()
currentCoroutineContext().ensureActive()
val error = result.exceptionOrNull()
if (error is TimeoutCancellationException) throw BackupClientSetupTimeout(error)
result.getOrThrow()
}

@Singleton
class VssBackupClient @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
Expand Down
1,064 changes: 652 additions & 412 deletions app/src/main/java/to/bitkit/repositories/BackupRepo.kt

Large diffs are not rendered by default.

16 changes: 3 additions & 13 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1257,16 +1257,6 @@ class PubkyRepo @Inject constructor(
}
}

suspend fun deleteProfileWithSessionRetry(): Result<Unit> = withContext(ioDispatcher) {
val initialResult = deleteProfile()
if (initialResult.isSuccess) return@withContext initialResult

val refreshedSession = refreshSessionIfPossible().getOrDefault(false)
if (!refreshedSession) return@withContext initialResult

deleteProfile()
}

suspend fun deleteProfile(): Result<Unit> = runSuspendCatching {
withContext(ioDispatcher) {
requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
Expand Down Expand Up @@ -1300,10 +1290,10 @@ class PubkyRepo @Inject constructor(
val publicKey = _publicKey.value ?: return
val known = ownProfileRevision
val revision = known ?: pubkyService.fetchPubkyProfileWithRevision(publicKey)?.revision ?: return
runSuspendCatching { withPubkyAppScope { pubkyService.deletePubkyProfile(revision, publicKey) } }.getOrElse {
runSuspendCatching { pubkyService.deletePubkyProfile(revision, publicKey) }.getOrElse {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[MEDIUM] Retry cannot refresh a failed pubky profile delete

Delete a non-Ring profile while that sign-in is still current and pubkyService.deletePubkyProfile fails with PaykitException.Identity.

deletePubkyAppProfile previously ran that delete inside withPubkyAppScope, which signs in from the local secret and retries the delete once for an identity error. The wrapper is gone, and the concurrent-update retry is also direct. refreshSessionIfPossible is now private and only used by withPubkyAppScope for other profile writes. retryDeleteProfile calls attemptDeleteProfile, which calls deleteProfile() again and never refreshes the session.

Retry therefore returns the same identity error. The dialog's other action signs out and leaves the homeserver profile in place.

While the original sign-in is still current, refresh once and retry only the failed pubky profile delete. Do not sign in after sign-out, and do not start a second contact cleanup or profile deletion after an uncertain acknowledgement. Keep profile deletion does not replay after an uncertain response; pubky app deletion does not renew the session or replay an identity error currently forbids this still-signed-in refresh.

Evidence basis: source analysis at 62c8cf23. This review did not execute the deletion.

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.

Confirmed from the public source: explicit Retry calls the whole deleteProfile() operation again and has no failed-step session recovery. The existing no-replay tests cover automatic replay; they do not establish successful explicit recovery after a confirmed authentication rejection.

I am leaving this concern open. PaykitException.Identity alone does not establish that this exact request was rejected before dispatch, and the current signIn boundary does not demonstrate preservation of the original grant and homeserver. Restoring either broad wrapper would also reintroduce the sign-out/replacement and uncertain-acknowledgement risks you identified.

A safe fix needs the exact refusal contract and a one-use continuation for only the refused step, retaining the original sign-in generation, revision and acknowledgements. It must refuse sign-out/replacement and must not repeat acknowledged contact/profile cleanup. The current cache-startup composition leaves all of these deletion sources and tests unchanged; it does not claim this recovery limitation is fixed. This disposition is source-based, with no new installed deletion execution.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Checked at ac1549f: the deletion sources and tests are unchanged. Explicit Retry still calls the whole deletion without failed-step session recovery. This composition does not claim to fix the limitation described in your reply.

if (known == null || !it.isConcurrentUpdate()) throw it
val current = pubkyService.fetchPubkyProfileWithRevision(publicKey)?.revision
if (current != null) withPubkyAppScope { pubkyService.deletePubkyProfile(current, publicKey) }
if (current != null) pubkyService.deletePubkyProfile(current, publicKey)
}
ownProfileRevision = null
}
Expand Down Expand Up @@ -2344,7 +2334,7 @@ class PubkyRepo @Inject constructor(
}
}

suspend fun refreshSessionIfPossible(): Result<Boolean> = runSuspendCatching {
private suspend fun refreshSessionIfPossible(): Result<Boolean> = runSuspendCatching {
withContext(ioDispatcher) {
val storedSecretKeyHex = activeSecretKeyHex() ?: return@withContext false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ class EditProfileViewModel @Inject constructor(
try {
privatePaykitRepo.beginProfileDeletion()
privatePaykitRepo.removePublishedEndpointsForCleanup(TAG, deferAppSync = true)
val result = pubkyRepo.deleteProfileWithSessionRetry()
val result = pubkyRepo.deleteProfile()
if (result.isSuccess) {
privatePaykitRepo.closeAndClear()
ToastEventBus.send(
Expand Down
Loading
Loading