Repository navigation
fix: stop automatic profile deletion replay - #1688
ben-kaufman wants to merge 24 commits into
Conversation
|
Triage: 3.1.0. It fixes shipped profile deletion, and 3.1.0 is due 28 Oct. |
[Medium impact] This narrow change appears safe to merge; no actionable issues were found. SummaryRemoves automatic replay of profile deletion after an error.
Reviews (1) · Last reviewed commit: "fix: stop automatic profile deletion rep..." · Reviewed by Greptile |
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Removes the automatic session-refresh-and-redelete after a failed profile deletion, so EditProfileViewModel calls PubkyRepo.deleteProfile() once and an uncertain failure lands on the existing failure dialog. Reviewed 99b446a, full tier, as the delta on fix/restore-identity-background (stacked on #1686).
What I checked, and 3 candidates I ruled out
Read in full: PubkyRepo.deleteProfile and deleteAllContacts, EditProfileViewModel.attemptDeleteProfile, retryDeleteProfile and disconnectProfile, the rewritten PubkyRepoTest cases
Call sites traced: deleteProfileWithSessionRetry and refreshSessionIfPossible have no caller left under app/src/main (grep on the PR head)
CI: build (debug build and unit tests) and lint green; e2e shards still running when reviewed
Ruled out
- An expired session can no longer be refreshed inside deletion, so Retry keeps failing: deliberate and stated in Out of Scope; the failure dialog still offers Retry and Disconnect, and session restore through
initialize()is unchanged (the renamed contact-edit test now goes through it). - Retry re-runs contact cleanup: it did before too;
deleteAllContactsis unchanged by this diff. - The sign-out race test passing for the wrong reason:
deletePaykitProfileis held on a deferred untilsignOut()returns, and the test assertssignInis never called and the identity stays cleared.
Merge confidence: 5/5, build, unit tests and lint green, and PubkyRepoTest covers the single-delete path and the sign-out race.
jvsena42
left a comment
There was a problem hiding this comment.
Code scan: no findings.
Checked and clean:
- With the replay gone, a failed deletion surfaces the failure dialog after one attempt. Local identity is cleared only inside
signOut(), after the profile delete and the public-endpoint removal, so an interrupted deletion leaves the user signed in with a retryable state and no orphaned remote data from this path. - v3.0.0 holds no persisted pending-deletion state (
PrivatePaykitRepo.kt:130is an in-memory var), so there's nothing to migrate. - Nothing still references the removed
deleteProfileWithSessionRetry/refreshSessionIfPossible, and startup session recovery stays inresolveSessionInitialization. - The new
PubkyRepoTestcases pin one call per tap and nosignIn. - Twin synonymdev/bitkit-ios#1122 does the same. #1690 edits the same lines and builds on this one.
Device gate (partial): 99b446a — not run yet. profile/delete-profile-uncertain-response.xml needs a homeserver fault proxy. I'll approximate it: airplane mode during the delete, expecting the failure dialog once, exactly one Failed to delete profile and no re-sign-in or second delete; Cancel keeps the profile; Retry online completes; Disconnect from the dialog. A plain successful deletion runs too. Device results will follow in a separate review.
There was a problem hiding this comment.
Approving at 99b446a. My code scan stands with no findings, and the device gate found none.
Device gate: 99b446a — Pixel_Tablet, fresh wallet and disposable identity, staging Pubky. journeys: profile/delete-profile-uncertain-response.xml 6/6 actions held, run as an airplane-mode approximation (airplane mode ~0.45 s after "Yes, Delete"). perf: not relevant; a plain deletion takes 8.9 s from confirm to keys cleared, on the unchanged success path. Figma: n/a; l10n: no UI text changed.
- One failure shows "Unable to Delete Profile" with Disconnect / Retry. Through a 65 s wait there's exactly one
Failed to delete profile, no second contact cleanup, no delete call and no sign-in. Cancel keeps the profile, and a relaunch runs only the normal session restore, with no deletion. - Each Retry makes exactly one attempt (3 failures for 3 taps, none automatic). The online Retry completed and reached onboarding.
- Disconnect offline fails at private endpoint removal and keeps the profile, as on master. Online, it signs out.
- For the record, not a finding: a Retry within ~60 s of an interrupted delete fails with
shared_state_busyuntil the homeserver lock expires. The removed automatic retry would have hit the same lock, and the follow-up sits in #1687.
Skipped — covered by CI at 99b446a: unit tests, lint, detekt, e2e (all green).
There was a problem hiding this comment.
Verdict: ✅ Approve
Findings: None.
QA for 99b446a: Pixel 7 emulator, Android 16 (API 36), disposable unfunded wallet and local Pubky testnet identity.
Test 1: J1 — lost profile deletion acknowledgement ✅ Failure appears once; dismissal and cold restart do not replay deletion.
For J1, a debug fixture provisioned the disposable identity and two contacts, then I drove the ordinary profile editor with Paykit 0.1.1-rc1. The proxy log confirms that the profile DELETE returned HTTP 204 before its response was withheld beyond the SDK timeout. No SDK result or deletion decision was substituted.
“Unable to Delete Profile” appeared after about 31 seconds. The recording shows no success toast or onboarding navigation. Dismissing the dialog with Android Back retained the editor. The app trace records one contact cleanup, one profile deletion and one failure, with no sign-in call.
I then force-stopped the app and launched it normally. Home appeared, normal stored-session restoration ran, and the trace contained no additional deletion during the 85-second restart observation.
| Failure and dismissal | Cold restart |
|---|---|
j1-repeat-failure.2x.mp4 |
j1-repeat-restart.2x.mp4 |
| Accepted DELETE, timeout and dialog dismissal, at 2× speed. | Normal launch and observation without Retry, at 2× speed. |
Check 1: build and repository checks ✅ The unmodified head passes the build and required checks.
assembleDevDebug, just compile, just test and just lint pass. I inspected the finalized unit reports and the Detekt and Android Lint reports: no test failures or skips, and no lint findings. The journey XML and git diff --check also pass.
I relied on the earlier Android device report at this same head for interrupted requests, manual Retry/Disconnect and successful deletion; I did not repeat those cases.
Full review of ac2a7c6..99b446a, all six changed files. J1 was driven in full. The removed helper call graph and sign-out interleaving were checked in source and unit tests; manual cleanup ownership remains outside this change, tracked in bitkit-android#1687 Bind profile deletion cleanup to the original sign-in.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay matches the removal of automatic full-flow replay.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
Reviewed by gpt-6.1-sol-high via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Reviewed the full PR diff against its merge base, at 99b446a5 (base and merge base ac2a7c66, fix/restore-identity-background). This is a first full assessment; no baseline coverage is inherited.
No new actionable code findings.
No new actionable code findings. After the change, EditProfileViewModel.attemptDeleteProfile calls PubkyRepo.deleteProfile() once. Any failure, including a lost acknowledgement, now shows the existing failure dialog. Before, the app would sign in again and run contact cleanup and profile deletion a second time. Only that wrapper called the removed refreshSessionIfPossible. Startup re-sign-in in initializeSession / resolveSignedInSession is unchanged. The cost is the trade-off the PR describes: when the homeserver session has expired, deletion now fails until normal session recovery or re-authentication. Manual Retry and ownership of the original sign-in during cleanup stay out of scope, tracked in #1687.
Parity: bitkit-ios#1122 @ ca10ace7 also removes the automatic refresh and second deletion from EditProfileView. Its delete-profile-uncertain-response.xml journey is byte-identical to this one. I compared source only, not runtime behaviour.
Dependency: the head of #1686 has moved past this PR's base, to 0fd5e424. It does not add callers of the removed functions. A local test merge of this head with it was clean, and no references to deleteProfileWithSessionRetry / refreshSessionIfPossible remained.
Validation: no tests or builds were run in this review. The matching CI runs at 99b446a passed (CI with unit tests, Lint, E2E). Those E2E suites do not run J1. The new repository tests check that the error comes back unchanged and that no sign-in follows a failure, including after sign-out. The ViewModel failure test checks that endpoint cleanup runs exactly once. J1 needs a fault-injecting homeserver proxy, which no automated layer has; making that proxy available would allow it to be automated.
Device testing: not performed in this review. Device runs of J1 at 99b446a are reported by other reviewers (jvsena42, ovi-reviewer); they are not this reviewer's executions.
Suggested additional test cases:
- Android, expired session: use a disposable Bitkit-created profile whose homeserver session has been revoked on the server, with the local Pubky secret key still present. Open Edit Profile, then Delete, then confirm. Expected: the deletion request is sent once, the failure dialog appears, and there is no automatic sign-in or second deletion. Then force-stop and relaunch the app, wait for profile initialization, and delete again. Record whether normal startup recovery lets the explicit deletion succeed, or whether re-authentication is needed, as the PR's out-of-scope note describes. Disconnect must still return to profile onboarding.
- Android, Ring-linked identity: use a disposable identity adopted from Pubky Ring and inject the same lost acknowledgement during Unlink pubky. Expected: the unlink failure dialog appears once, no automatic second unlink or sign-in happens, and no success toast is shown.
Reviewer model: claude-opus-5-5
Reasoning effort: high
There was a problem hiding this comment.
Verdict: 💬 Comment — no new code findings; the suggested revoked-session Disconnect assertion did not pass.
Findings: None in the reviewed change.
QA for 5fe0bf6: Pixel 7 emulator, Android 16 (API 36), unfunded disposable identities and a local Pubky homeserver, using the committed Paykit 0.1.1-rc2 dependency. Device cases used a debug provisioning and entry-logging hook; the ordinary editor, ViewModel, repository and native SDK determined every deletion result.
Test 1: J1 — lost deletion acknowledgement ✅ One honest failure, with no automatic replay.
The real homeserver returned HTTP 204 for the profile DELETE before the proxy withheld its response for 45 seconds. “Unable to Delete Profile” appeared after about 31 seconds, without success navigation. The app trace contains one contact-cleanup batch, one profile deletion and one failure, with no sign-in.
j1.2x.mp4
Accepted deletion followed by the timeout failure, at 2× speed.
Android Back retained the editor, checked in the UI hierarchy. I then force-stopped and launched the ordinary app: Home appeared and stored-session initialization ran. Saved app/request logs through the 76-second restart observation contain no additional cleanup, deletion or sign-in; the display slept near the end of that observation. These restart assertions were checked against logs and UI snapshots.
Test 2: revoked session and recovery ❌ Disconnect with revoked authorization retains the profile.
I revoked this disposable identity's three grants on the homeserver while retaining its local credentials. Explicit deletion failed once at contact loading with HTTP 401; no automatic sign-in or second deletion followed. Selecting Disconnect also returned HTTP 401 during endpoint cleanup and kept the editor. The suggested assertion that Disconnect returns directly to profile onboarding therefore did not hold.
r1.2x.mp4
Revoked-session failure and the attempted Disconnect, at 2× speed.
Cold restart subsequently recovered through the normal sign-in path. A later explicit deletion succeeded and reached profile onboarding, checked in the saved app/request logs and UI hierarchy. This is the existing authentication/cleanup limitation described by the PR's scope, rather than a new defect on a changed line; ownership and cleanup follow-up remain tracked in bitkit-android#1687 Bind profile deletion cleanup to the original sign-in.
Test 3: adopted identity unlink ✅ One unlink failure, with no automatic retry or sign-in.
A same-signature Ring-compatible credential fixture supplied a disposable identity to Bitkit's real adoption API. Bitkit recognized it as adopted and showed “Unlink pubky.” The homeserver accepted one profile DELETE with HTTP 204 before its acknowledgement was withheld. One “Unable to Unlink Pubky” dialog appeared after about 31 seconds, with no success navigation. Entry and request logs confirm one attempt and no automatic sign-in or second deletion. Explicit Disconnect with valid authorization then reached profile onboarding, also checked in the UI hierarchy and app/request logs.
r2.2x.mp4
Unlink confirmation, uncertain-response failure and explicit Disconnect, at 2× speed.
Ring's own UI was not exercised; the fixture supplied credentials only. Bitkit's signature checks, adoption, unlink decision and native operations remained active.
Check 1: build and repository checks ✅ The unmodified head passes the required checks.
I built assembleDevDebug and ran just compile, just test and just lint. Finalized unit reports contain no failures, errors or skips; Detekt and Android Lint structured reports contain no findings. git diff --check also passes.
Test hook
Not pushed. Provisions disposable test identities and logs operation entry; it changes no deletion result or decision. The Ring credential literal is omitted below. The fault proxy forwards requests unchanged, then delays only an owner-scoped successful profile DELETE response for 45 seconds.
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index b88fe73a7..7e39641b1 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -11,5 +11,6 @@
android:authorities="${applicationId}.devtools"
android:exported="true"
tools:ignore="ExportedContentProvider" />
+ <activity android:name=".ui.ProfileDeletionProbeActivity" android:exported="true" android:theme="@style/Theme.App" />
</application>
</manifest>
diff --git a/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
new file mode 100644
index 000000000..62d2b343c
--- /dev/null
+++ b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
@@ -0,0 +1,81 @@
+// test hook: disposable provisioning only; the journey runs in ordinary MainActivity.
+package to.bitkit.ui
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.FragmentActivity
+import com.synonym.paykit.ContactUpdate
+import dagger.hilt.android.AndroidEntryPoint
+import to.bitkit.data.keychain.Keychain
+import to.bitkit.repositories.PubkyRepo
+import to.bitkit.repositories.WalletRepo
+import to.bitkit.services.PubkyService
+import to.bitkit.ui.theme.AppThemeSurface
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ProfileDeletionProbeActivity : FragmentActivity() {
+ @Inject lateinit var repo: PubkyRepo
+ @Inject lateinit var service: PubkyService
+ @Inject lateinit var wallet: WalletRepo
+ @Inject lateinit var keychain: Keychain
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var status by remember { mutableStateOf("Preparing disposable identity") }
+ LaunchedEffect(Unit) {
+ try {
+ if (keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) == null) {
+ wallet.createWallet(null).getOrThrow()
+ }
+ repo.initialize().getOrThrow()
+ if (intent.getStringExtra("mode") == "ring") {
+ repo.wipeLocalState()
+ val secret = "<disposable fixture credential omitted>"
+ val owner = service.publicKeyFromSecret(secret)
+ service.signUp(secret, requireNotNull(to.bitkit.env.Env.e2eHomeserverPubky), null)
+ service.signOut()
+ contentResolver.call(android.net.Uri.parse("content://to.pubky.ring.sharedpubky"), "set", owner, null)
+ repo.adoptRingIdentity(owner).getOrThrow()
+ repo.createIdentity("Disposable Ring identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ Log.i("DeletionProbe", "ring-adopted owner=${repo.publicKey.value} unlink=${repo.isRingAdoptedIdentity()}")
+ }
+ if (repo.publicKey.value == null) {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null)
+ .getOrThrow()
+ val peers = listOf("11", "22").map { service.publicKeyFromSecret(it.repeat(32)) }
+ service.saveContacts(peers.mapIndexed { index, key -> ContactUpdate(key, "Peer ${index + 1}") })
+ repo.loadContacts()
+ }
+ if (intent.getStringExtra("mode") == "renew") {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ }
+ Log.i("DeletionProbe", "retained secret=${keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name) != null} session=${keychain.loadString(Keychain.Key.PAYKIT_SESSION.name) != null}")
+ Log.i("DeletionProbe", "ready owner=${repo.publicKey.value} contacts=${service.contactRecords().size}")
+ status = "QA 5fe0bf6: disposable identity ready. Launch ordinary Bitkit for J1."
+ } catch (error: Exception) {
+ status = "Provision failed: ${error.message}"
+ Log.e("DeletionProbe", status, error)
+ }
+ }
+ AppThemeSurface {
+ Column(modifier = Modifier.padding(32.dp)) {
+ Text(status, color = Color.White)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
index fda214486..d91ea8921 100644
--- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@@ -1094,6 +1094,7 @@ class PubkyRepo @Inject constructor(
}
suspend fun deleteProfile(): Result<Unit> = runSuspendCatching {
+ android.util.Log.i("DeletionProbe", "deletion attempt entered") // test hook
withContext(ioDispatcher) {
requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
"No session available"
diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt
index e9105a40c..fd8454904 100644
--- a/app/src/main/java/to/bitkit/services/PubkyService.kt
+++ b/app/src/main/java/to/bitkit/services/PubkyService.kt
@@ -34,6 +34,7 @@ class PubkyService @Inject constructor(
}
suspend fun initializeAndImportSession(secret: String): Result<String> = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "normal initialization entered") // test hook
paykitSdkService.initializeAndImportSession(secret).map { it.publicKey }
}
@@ -43,6 +44,7 @@ class PubkyService @Inject constructor(
// region Session management
suspend fun importSession(secret: String): String = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "stored-session import entered") // test hook
paykitSdkService.importSession(secret).publicKey
}
@@ -111,6 +113,7 @@ class PubkyService @Inject constructor(
}
suspend fun signIn(secretKeyHex: String): Unit = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "sign-in entered") // test hook
paykitSdkService.signIn(secretKeyHex)
Unit
}
@@ -208,6 +211,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePaykitProfile() = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "profile delete entered") // test hook
paykitSdkService.deletePaykitProfile()
}
@@ -216,6 +220,7 @@ class PubkyService @Inject constructor(
}
suspend fun contactRecords(): List<ContactRecord> = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "contact read entered") // test hook
paykitSdkService.contactRecords()
}
@@ -252,6 +257,7 @@ class PubkyService @Inject constructor(
}
suspend fun removeContacts(publicKeys: List<String>): List<ContactRecord> = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "contact remove entered count=${publicKeys.size}") // test hook
paykitSdkService.removeContacts(publicKeys)
}
Deciding entry logs for J1 and the adopted-identity case each show one deletion attempt, one contact-read/remove batch and one profile-delete entry, followed by one timeout failure. There is no sign-in entered entry between confirmation and failure. J1's cold-start trace adds only normal initialization entered.
The earlier fault-proxy review proved J1 at 99b446a; I repeated it because this merge changes native pins and initialization. The earlier device report covers interrupted requests and manual Retry/Disconnect at 99b446a. I did not repeat its airplane-mode or Retry-button variants; their UI and direct-call wiring are unchanged. The two additional cases came from the later QA review.
Delta review of 99b446a..5fe0bf6: the six-file feature patch remains intact after the additive parent merge. I reviewed its deletion/initialization integration and removed-helper callers, with an independent reviewer reaching the same result. Parent-only features were not independently certified by these device cases; sign-out interleaving remains covered by source inspection and the repository checks.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay matches removal of the full-flow retry and deletion-only refresh helper; the journey actions match.
Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Re-reviewed the full PR at 5fe0bf6c against base and merge base 89e2702e (fix/restore-identity-background). This is a full reassessment because the comparison base changed from ac2a7c66 to 89e2702e since the previous QA review at 99b446a5, and the base-branch changes touch PubkyRepo.kt and PubkyRepoTest.kt. No coverage is inherited.
No new actionable code findings.
EditProfileViewModel.attemptDeleteProfile now calls PubkyRepo.deleteProfile() once. Any failure goes straight to the existing Retry / Sign out dialog, without a second contact cleanup, profile delete or sign-in. Nothing at this head still calls the removed deleteProfileWithSessionRetry / refreshSessionIfPossible; automatic sign-in now happens only through the normal startup path (initializeSession → resolveSignedInSession). The ViewModel failure test checks that endpoint cleanup runs exactly once and that closeAndClear is never called on failure.
Intended trade-off: with a revoked or expired session, explicit deletion and Retry now keep failing until the session is recovered through the normal startup path. The PR description states this. ovi-reviewer reports that a cold restart recovered the session and a later deletion succeeded. The same report says Sign out with revoked authorization failed during endpoint cleanup. That disconnectProfile path is unchanged here and is already tracked in #1687 (open; fix proposed in #1693).
Parity: the iOS twin bitkit-ios#1122 at f3df6cda removes the same retry path in EditProfileView and PubkyProfileManager. The old iOS code retried only after auth-classified errors, while the old Android code retried after any error. Both now make one attempt and show the failure dialog. This was checked from source only. Dependency #1686 at 9ce43027 merges cleanly with this head in a local trial merge and adds no new callers of the removed APIs.
Validation: I did not run tests locally. CI for this head reports build and unit tests, lint and E2E as passing; none of the E2E cases runs the J1 journey. I did not inspect the Paykit SDK (0.1.1-rc2) source, so how startup session import recovers is based only on the attributed device report.
Device testing: not performed in this review.
Reviewer model: claude-opus-5-5
Reasoning effort: high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete PR diff against merge base 217b42ef, at 2aac53ed. The comparison base moved from 89e2702e to 217b42ef, and that parent update touches PubkyRepo.kt and PubkyRepoTest.kt. No coverage was inherited.
No new actionable code findings.
Edit Profile calls PubkyRepo.deleteProfile() once. deleteProfileWithSessionRetry and refreshSessionIfPossible are gone, and no remaining caller invokes them. An uncertain deletePaykitProfile failure returns before sign-out, so the existing failure dialog stays up with no second profile deletion or deletion-triggered sign-in. Back or outside dismissal does not retry; Retry and Sign out stay explicit. Parent session restoration from bitkit-android#1686 at 217b42ef still runs through initializeSession, and continueSignIn keeps the current sign-in when the public key is unchanged. That check covers only the overlap with this deletion change, not a full review of #1686. The iOS twin bitkit-ios#1122 at f3df6cda removes the same replay path.
Hosted CI at this head succeeded, including testDevDebugUnitTest, and lint succeeded. This review inspected those tests and did not re-run them. E2E does not execute J1. Device testing was not performed in this review. An earlier J1 report at 5fe0bf6c (review) is not execution at this head. Revoked-session Sign out still fails during endpoint cleanup and remains #1687; this diff does not change that path.
Ready for device testing. Repeat J1 at this revision, including restart, with the journey's disposable identity and acknowledgement-dropping proxy.
Device testing: not performed in this review.
Reviewer model: grok-4.7
Reasoning effort: high
|
@ben-kaufman Moving this PR to 3.2.0 per the 3.1.0 cut line (https://synonymworkspace.slack.com/archives/C07BJ7DNPCG/p1793307568629859): the restore and profile-deletion stacks harden behaviour that already shipped in 3.0, so they keep landing on master and ship in 3.2. |
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — revoked-session Disconnect still fails the requested device assertion.
Findings: No new actionable code findings in this delta. The failed Disconnect assertion reproduces the existing cleanup limitation tracked in bitkit-android#1687 Bind profile deletion cleanup to the original sign-in; its implementation is unchanged here.
QA for 2aac53e: Pixel 7 emulator, Android 16 (API 36), an unfunded disposable identity and a local Pubky homeserver, using the committed Paykit 0.1.1-rc2 dependency. A local debug hook provisioned the identity and logged operation entry; the ordinary editor, repository and native SDK determined every result.
Lost profile deletion acknowledgement and restart ✅ One failure; dismissal and cold restart do not replay deletion.
For J1, the real homeserver accepted one profile DELETE with HTTP 204. The proxy then withheld its response for 45 seconds. “Unable to Delete Profile” appeared after about 31 seconds without a success notification or navigation to onboarding.
Android Back dismissed the dialog and retained the editor. A cold restart returned to Home. App and request logs through the 66-second restart observation show one cleanup batch, one profile deletion and one failure overall, no deletion-triggered sign-in, and only normal stored-session initialization after restart.
j1.2x.mp4
Accepted deletion, honest timeout failure, dismissal and cold restart, at 2× speed. Operation counts were checked against entry and proxy logs.
Revoked session deletion, Disconnect and recovery ❌ Disconnect returns HTTP 401 and retains the profile editor.
I revoked the disposable owner's active grant on the homeserver while retaining local credentials. Explicit deletion failed once during contact loading with HTTP 401; no automatic sign-in or second deletion followed. Selecting Disconnect produced “Failed to disconnect profile” with HTTP 401 during endpoint cleanup and left the editor open. The requested return to profile onboarding did not occur.
Cold restart then recovered through normal sign-in. A later explicit deletion succeeded and reached profile onboarding. Those recovery steps do not turn the earlier failed Disconnect assertion into a pass. This repeats the previous revoked-session result, rather than identifying a new defect on a changed line.
r1.2x.mp4
Revoked-session deletion failure, failed Disconnect, cold recovery and successful explicit deletion, at 2× speed. Request and entry logs confirm the operation counts; the final UI hierarchy confirms onboarding.
Check: Build and repository checks ✅ The unmodified head builds and passes the required checks.
I built assembleDevDebug and ran just compile, just test and just lint. Finalized unit reports have no failures, errors or skips; Detekt and Android Lint structured reports have no findings. git diff --check also passes. The instrumented APK's installed hash matches the locally built APK.
I retained the successful adopted-identity unlink evidence from the 5fe0bf6 device report: adoption, unlink controls, deletion calls and native pins are unchanged. I also relied on the earlier interrupted-request and explicit Retry report at 99b446a for those unchanged controls. These are attributed earlier results, not new executions at this head. No planned device case remains unrun.
Delta review of 5fe0bf6..2aac53e, with an independent reviewer, plus the six-file deletion patch's integration. Missing-profile reconnect loading retains sign-in and write-generation checks; the removed retry helpers have no production callers. The parent update's send, payment-request, hardware-fee, contact-discovery and full wallet-restore journeys were reviewed in source but were not driven by these deletion cases.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay matches removal of automatic full-flow replay and the deletion-only refresh helper; the J1 journey actions match.
Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Re-reviewed the full PR at 916c6d5e after the merge base changed since the saved assessment. Coverage is the complete diff against base and merge base 4056d2c0. Inherited coverage is empty.
No new actionable code findings.
Profile deletion calls deleteProfile() once. An uncertain deletePaykitProfile failure returns that error and leaves the existing failure dialog in place; it does not sign in, sign out, or start a second contact cleanup and profile deletion. Explicit Retry and Disconnect are unchanged. The deferred private-cleanup marker, and the later registry reconciliation described in journeys/profile/README.md, remain existing parent behavior.
Revoked-session Disconnect remains the unchanged cleanup path tracked in bitkit-android#1687. Earlier device reviews reported that failure on older heads, including 2aac53ed and 3d8a328d. This review did not re-run it. bitkit-android#1693 is the separate owner-binding change and was not reviewed here.
Compared with bitkit-ios#1122 at b44f0253: both remove the deletion-only session refresh and the second profile deletion. iOS previously refreshed only authentication failures; Android previously retried any failure. Dependency bitkit-android#1686 at 4056d2c0 is this PR's merge base; this diff leaves its VSS ownership code unchanged.
Inspected PubkyRepoTest and EditProfileViewModelTest; did not execute them. Hosted CI, Lint, and E2E completed successfully on this head. Those E2E shards do not run the lost-acknowledgement journey. Device testing: not performed in this review.
Suggested additional test cases
- Android. Disposable Bitkit profile with at least one contact, and a homeserver proxy that accepts one profile deletion and drops its acknowledgement. Confirm deletion, wait for the failure dialog, dismiss it without Retry or Disconnect, then cold-start. Expected: the failure stays visible, with one contact cleanup, one profile deletion, no success toast, no onboarding navigation, no automatic sign-in, and no second deletion after dismissal or restart.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes. Disconnect after a revoked session still fails with HTTP 401, as it did at 3d8a328. The cause is the unchanged cleanup path tracked in bitkit-android#1687, not a regression in this PR. The lost-acknowledgement journey passes at this head with contacts.
Findings: No new code findings. This push merges the parent and leaves the PR's own patch byte-identical. The Disconnect failure below is the known limitation. Its fix is in bitkit-android#1693 fix: bind profile removal to its original owner, which is not on this branch.
QA for 916c6d5: Pixel 7 emulator on Android 16 (API 36), on a local Pubky homeserver. Each case used a fresh, unfunded disposable identity with two synthetic contacts. An unpushed debug hook provisioned the identity and logged when each operation started. The normal editor, repository and native SDK produced every result, and the installed APK hash matches the hook build. VSS backups went to the staging regtest server.
I re-drove the lost-acknowledgement journey because the merged-in VSS change reaches deletion. Contact cleanup and sign-out both bump backupStateVersion, which schedules a METADATA backup through the rewritten VSS client.
Lost profile deletion acknowledgement with contacts, then restart ✅ One failure; no replay through dismissal and cold start, and the cleanup backup completes.
One deletion attempt removed both contacts and sent one profile DELETE. The homeserver accepted it with HTTP 204, and the proxy withheld the reply. After about 31 seconds the native request timed out, and "Unable to Delete Profile" appeared with Disconnect and Retry. There was no success toast and no onboarding. The METADATA backup marked by contact cleanup ran once the delete returned, and it succeeded through VSS.
Android Back kept the editor open. A cold start returned to Home and ran only normal session initialization. For about 85 seconds after the restart there was no sign-in, no second contact cleanup and no second deletion. The proxy saw one profile DELETE in total.
j1-2x.2x.mp4
Provisioned identity through the confirmed deletion to the failure dialog, at 2× speed. The clip ends on the dialog. Back, the cold start and the counts were checked in UI dumps, the operation-entry log and the proxy log:
14:45:40.725 DeletionProbe: deletion attempt entered
14:45:40.998 DeletionProbe: contact remove entered count=2
14:45:41.510 DeletionProbe: profile delete entered
14:45:41.519 BackupRepo: Marked backup required for: 'METADATA'
14:46:12.032 EditProfileViewModel: Failed to delete profile (delete Pubky resource, request timeout)
14:46:13.118 BackupRepo: Backup succeeded for: 'METADATA'
(Back, then cold start)
14:47:36.417 DeletionProbe: normal initialization entered
Revoked session deletion, Disconnect and recovery ❌ Disconnect fails with HTTP 401 and leaves the editor open.
I revoked the identity's server grant and kept its local credentials. The homeserver database confirms it, with active grants going from 1 to 0. One explicit deletion failed while loading contacts (acquire Pubky write lock, HTTP 401). There was no automatic sign-in and no second deletion, and the failure dialog offered Disconnect and Retry. Disconnect then showed "Failed to disconnect profile" (HTTP 401) and the editor stayed open.
After a cold start, the app recovered through normal sign-in. A later explicit deletion removed both contacts, deleted the profile once and reached profile onboarding. The METADATA and WALLET backups that followed sign-out succeeded through VSS.
| Revoked-session deletion and failed Disconnect | Cold recovery and later explicit deletion |
|---|---|
r1a-2x.2x.mp4 |
r1b-2x.2x.mp4 |
Both clips play at 2× speed. The operation-entry log decides the counts:
14:51:35.551 DeletionProbe: deletion attempt entered
14:51:35.597 DeletionProbe: contact read entered
14:51:35.714 EditProfileViewModel: Failed to delete profile (acquire Pubky write lock, HTTP 401)
(Disconnect: Failed to disconnect profile, HTTP 401; cold start)
14:52:40.148 DeletionProbe: normal initialization entered
14:52:40.556 DeletionProbe: sign-in entered
14:53:27.082 DeletionProbe: deletion attempt entered
14:53:27.554 DeletionProbe: contact remove entered count=2
14:53:28.282 DeletionProbe: profile delete entered
14:53:33.428 BackupRepo: Backup succeeded for: 'METADATA'
Check: Build and hosted checks ✅ The head builds; every hosted check is green, Compose UI included.
The unmodified head passes assembleDevDebug. Hosted build-and-test, compose-ui / ui-tests, lint, detekt, build-local and all seven e2e-tests-local shards pass at this head.
Earlier passing device reports still cover:
Their adoption, unlink and Retry code is unchanged. The successful-deletion backup tail that the merge changed is driven above, by the later explicit deletion in the revoked-session case.
Scope: Delta review of 3d8a328..916c6d5. This is a clean merge of bitkit-android#1686 at 4056d2c, which keeps VSS operations with their original wallet.
- The contact point is the backup after a Pubky state change. It keeps one owner through the coordinator's admission, and both device cases drove it.
- No device case drove the rest: the wallet wipe and restore paths that suspend VSS clients, and the Lightning node restart changes. They belong to #1686.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay is now at b44f025. Its commits since 5a5044a are inherited backup fixes. It still matches the single-attempt deletion and the removal of the deletion-only refresh-and-retry path.
Test hook
Not pushed. It is the same hook as in the 3d8a328 review. It provisions a disposable unfunded identity with two synthetic contacts and logs when each operation starts. It changes no deletion, authentication, cleanup or backup result. The cases run afterward in the normal MainActivity. The fault proxy forwards every request unchanged and only delays the reply to this identity's successful profile DELETE, by 45 seconds.
Reviewed by claude-opus-5-5
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Re-reviewed the full PR at a8c3fd3 after the merge base changed since the saved assessment. Coverage is the complete diff against base and merge base a2c58f2. No coverage baseline was supplied, so inherited coverage is empty.
No new actionable code findings.
Edit Profile makes one deleteProfile() call per explicit attempt. An uncertain deletePaykitProfile failure returns that error and shows the existing failure dialog. Explicit Retry and Disconnect remain. The removed session-refresh helper has no remaining production caller. completeSdkCall returns the first deletion error. A later maintenance pass can still reconcile the existing private-cleanup marker, as described in journeys/profile/README.md; that pass does not delete the profile.
Revoked-session Disconnect remains the open cleanup limitation in bitkit-android#1687. This diff leaves disconnectProfile unchanged. bitkit-android#1693 is the separate owner-binding change and was not reviewed.
Compared with bitkit-ios#1122 at b44f0253: both remove the deletion-only session refresh and the automatic second profile deletion. iOS keeps an explicit Cancel button on the failure alert. On Android, J1's cancel step is system Back or a tap outside the dialog; the secondary button is Sign out. Dependency bitkit-android#1686 at a2c58f2 is this PR's merge base. This diff leaves that parent's backup code unchanged.
Inspected PubkyRepoTest and EditProfileViewModelTest; did not execute them. Hosted build and unit tests and lint succeeded on this head. Local E2E succeeded and does not run the lost-acknowledgement journey. Compose UI is still running. Device testing: not performed in this review. Earlier device reports, including the lost-acknowledgement run attributed at 916c6d5, stay on those revisions. The deletion sources match that commit, and this head also contains the later parent backup merge.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes. Disconnect after session revocation still fails with HTTP 401 and leaves the editor open. This repeats the unchanged cleanup limitation in bitkit-android#1687 Bind profile deletion cleanup to the original sign-in. The lost-acknowledgement journey passes at this head.
Findings: No new code findings in this delta. The separate original-owner removal work is in bitkit-android#1693 fix: bind profile removal to its original owner; it is not present here.
QA for a8c3fd3: Pixel 7 emulator, Android 16 / API 36, committed Paykit 0.1.1-rc2, local Pubky homeserver and staging regtest VSS. Each case used a fresh unfunded identity with two synthetic contacts. An unpushed debug hook provisioned test data and logged operation entry; the normal editor, repository and native SDK determined all outcomes. Installed APK bytes match the built hook APK.
Lost profile deletion acknowledgement with contacts, then restart ✅ One timeout failure, no automatic replay through dismissal and restart, and the cleanup backup completes.
J1: one attempt removed both contacts and sent one profile DELETE. The homeserver accepted it with HTTP 204; the proxy withheld that successful reply for 45 seconds. The native request timed out after about 31 seconds. “Unable to Delete Profile” offered Disconnect and Retry, with no success notification or onboarding. The contact-cleanup METADATA backup succeeded through the changed scheduler.
Android Back retained the editor. Cold startup returned to Home and ran normal session initialization. Entry and proxy logs through about 100 seconds after initialization show one contact-removal batch and one profile DELETE overall, with no sign-in or second deletion.
| Accepted deletion, failure and dismissal | Cold restart |
|---|---|
j1-fault-2x.2x.mp4 |
j1-restart-2x.2x.mp4 |
Both clips play at 2×. The restart clip covers roughly 89 seconds; the longer observation and operation counts were checked in the saved entry and request logs.
Revoked session deletion, Disconnect and recovery ❌ Disconnect returns HTTP 401 and retains the editor.
The homeserver database confirms active grants changed from one to zero and remained zero after opening the app. One explicit deletion failed during contact loading, without automatic sign-in or a second attempt. Disconnect retained the editor; the UI hierarchy captured “Failed to disconnect profile” and acquire Pubky write lock (HTTP 401).
Cold startup then ran normal initialization and sign-in. A later explicit deletion removed both contacts, deleted the profile once and reached profile onboarding. The resulting METADATA and WALLET backups succeeded. Those recovery steps do not change the failed Disconnect assertion.
| Revoked-session deletion and Disconnect | Cold recovery and later explicit deletion |
|---|---|
r1-failure-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
Both clips play at 2×. Database output establishes revocation; entry logs establish operation counts, and the final UI hierarchy confirms onboarding.
Check: Build and backup regressions ✅ The unmodified head builds; focused backup tests pass, and hosted checks report success.
assembleDevDebug, BackupRepoTest and VssClientCoordinatorTest passed. Finalized JUnit XML has no failures, errors or skips. The full local unit/lint gate was not repeated.
Exact-head hosted build/unit/Compose CI, lint and E2E report success. The original Compose attempt failed before instrumentation; its later attempt passed. Migration jobs were skipped. Hosted status is separate from the device evidence above.
Earlier passing reports retained for unchanged mechanisms:
- Adopted-identity unlink at 5fe0bf6.
- Interrupted requests and explicit Retry at 99b446a.
- Queued Settings Running-to-acknowledgement and observation cancellation at the exact a2c58f2 parent. Its backup source matches this merge.
No planned device case remains unrun. J1 was repeated because contact cleanup reaches the scheduler changed by this push; the formerly failed revoked-session case was also repeated. The changed backup scheduling shared by unlink and Retry is exercised above.
Scope: Complete five-file delta 916c6d5..a8c3fd3, reachable scheduler/admission/status paths and the open Disconnect case, with an independent reviewer. The six-file deletion patch and native pins are unchanged. The never-admitted replacement and delayed-startup races were checked in source and focused unit tests; these device cases do not force those races or certify wallet wipe, restore or terminal-identity complete-WALLET recovery.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at b44f025, still matches single-attempt deletion and removal of the deletion-only refresh/retry helper. The new Android backup-status correction is platform-specific.
Test hook
Not pushed. Reuses the previous round's disposable provisioning activity and entry logs. It changes no deletion, authentication, cleanup or backup result. The fault proxy forwards real requests unchanged and delays only this disposable owner's successful profile DELETE reply.
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index b88fe73a7..7e39641b1 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -11,5 +11,6 @@
android:authorities="${applicationId}.devtools"
android:exported="true"
tools:ignore="ExportedContentProvider" />
+ <activity android:name=".ui.ProfileDeletionProbeActivity" android:exported="true" android:theme="@style/Theme.App" />
</application>
</manifest>
diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
index 2008e478a..2332e5301 100644
--- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@@ -1109,6 +1109,7 @@ class PubkyRepo @Inject constructor(
}
suspend fun deleteProfile(): Result<Unit> = runSuspendCatching {
+ android.util.Log.i("DeletionProbe", "deletion attempt entered") // test hook
withContext(ioDispatcher) {
requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
"No session available"
diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt
index e9105a40c..fd8454904 100644
--- a/app/src/main/java/to/bitkit/services/PubkyService.kt
+++ b/app/src/main/java/to/bitkit/services/PubkyService.kt
@@ -34,6 +34,7 @@ class PubkyService @Inject constructor(
}
suspend fun initializeAndImportSession(secret: String): Result<String> = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "normal initialization entered") // test hook
paykitSdkService.initializeAndImportSession(secret).map { it.publicKey }
}
@@ -43,6 +44,7 @@ class PubkyService @Inject constructor(
// region Session management
suspend fun importSession(secret: String): String = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "stored-session import entered") // test hook
paykitSdkService.importSession(secret).publicKey
}
@@ -111,6 +113,7 @@ class PubkyService @Inject constructor(
}
suspend fun signIn(secretKeyHex: String): Unit = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "sign-in entered") // test hook
paykitSdkService.signIn(secretKeyHex)
Unit
}
@@ -208,6 +211,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePaykitProfile() = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "profile delete entered") // test hook
paykitSdkService.deletePaykitProfile()
}
@@ -216,6 +220,7 @@ class PubkyService @Inject constructor(
}
suspend fun contactRecords(): List<ContactRecord> = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "contact read entered") // test hook
paykitSdkService.contactRecords()
}
@@ -252,6 +257,7 @@ class PubkyService @Inject constructor(
}
suspend fun removeContacts(publicKeys: List<String>): List<ContactRecord> = ServiceQueue.CORE.background {
+ android.util.Log.i("DeletionProbe", "contact remove entered count=${publicKeys.size}") // test hook
paykitSdkService.removeContacts(publicKeys)
}
diff --git a/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
new file mode 100644
index 000000000..a724e228f
--- /dev/null
+++ b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
@@ -0,0 +1,70 @@
+// test hook: disposable provisioning only; the journey runs in ordinary MainActivity.
+package to.bitkit.ui
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.FragmentActivity
+import com.synonym.paykit.ContactUpdate
+import dagger.hilt.android.AndroidEntryPoint
+import to.bitkit.data.keychain.Keychain
+import to.bitkit.repositories.PubkyRepo
+import to.bitkit.repositories.WalletRepo
+import to.bitkit.services.PubkyService
+import to.bitkit.ui.theme.AppThemeSurface
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ProfileDeletionProbeActivity : FragmentActivity() {
+ @Inject lateinit var repo: PubkyRepo
+ @Inject lateinit var service: PubkyService
+ @Inject lateinit var wallet: WalletRepo
+ @Inject lateinit var keychain: Keychain
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var status by remember { mutableStateOf("Preparing disposable identity") }
+ LaunchedEffect(Unit) {
+ try {
+ if (keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) == null) {
+ wallet.createWallet(null).getOrThrow()
+ }
+ repo.initialize().getOrThrow()
+ if (repo.publicKey.value == null) {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null)
+ .getOrThrow()
+ val peers = listOf("11", "22").map { service.publicKeyFromSecret(it.repeat(32)) }
+ service.saveContacts(peers.mapIndexed { index, key -> ContactUpdate(key, "Peer ${index + 1}") })
+ repo.loadContacts()
+ }
+ if (intent.getStringExtra("mode") == "renew") {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ }
+ Log.i("DeletionProbe", "retained secret=${keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name) != null} session=${keychain.loadString(Keychain.Key.PAYKIT_SESSION.name) != null}")
+ Log.i("DeletionProbe", "ready owner=${repo.publicKey.value} contacts=${service.contactRecords().size}")
+ status = "QA 916c6d5: disposable identity ready. Launch ordinary Bitkit for J1."
+ } catch (error: Exception) {
+ status = "Provision failed: ${error.message}"
+ Log.e("DeletionProbe", status, error)
+ }
+ }
+ AppThemeSurface {
+ Column(modifier = Modifier.padding(32.dp)) {
+ Text(status, color = Color.White)
+ }
+ }
+ }
+ }
+}J1 entry log, through the final restart observation:
DeletionProbe: deletion attempt entered
DeletionProbe: contact read entered
DeletionProbe: contact remove entered count=2
DeletionProbe: profile delete entered
DeletionProbe: normal initialization entered
R1 entry log, including cold recovery and the later explicit attempt:
DeletionProbe: deletion attempt entered
DeletionProbe: contact read entered
DeletionProbe: normal initialization entered
DeletionProbe: sign-in entered
DeletionProbe: deletion attempt entered
DeletionProbe: contact read entered
DeletionProbe: contact remove entered count=2
DeletionProbe: profile delete entered
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — Disconnect after session revocation still returns HTTP 401 and retains the editor.
Findings: No new code findings in this delta. The previous revoked-session result remains reproducible, tracked in synonymdev/bitkit-android#1687 Bind profile deletion cleanup to the original sign-in. Its separate fix remains in synonymdev/bitkit-android#1693 fix: bind profile removal to its original owner.
QA for dc9a981: Pixel 7 emulator, Android 16 / API 36, committed Paykit 0.1.1-rc7, local Pubky testnet, fresh unfunded identities. A debug hook provisioned identities and recorded operation entry; ordinary editor, repository and native SDK code determined all outcomes. Installed APK bytes match the built QA APK.
Lost pubky.app deletion acknowledgement with contacts, then restart ✅ One honest timeout; dismissal and restart do not replay deletion.
J1: a fresh identity had two contacts. The homeserver accepted one pubky.app/profile.json DELETE with HTTP 204; the proxy withheld its acknowledgement for 45 seconds. The native request timed out after about 30 seconds and showed “Unable to Delete Profile.” Back retained the editor.
Cold startup ran normal initialization and showed Restore Profile for the retained identity. Entry and request logs through 73 seconds after initialization show one contact-removal batch, one pubky.app DELETE, no Paykit-copy DELETE, and no app sign-in or automatic full-flow replay. The logs establish operation counts; the recordings show the UI outcomes.
| Failure and dismissal | Cold startup |
|---|---|
j1-fault-2x.2x.mp4 |
j1-restart-2x.2x.mp4 |
Both clips play at 2×.
Lost Paykit profile deletion acknowledgement, then restart ✅ The later deletion also returns one failure without replay.
J1b: a separate fresh identity had two contacts. Its pubky.app DELETE completed with HTTP 204, then its Paykit-copy DELETE was accepted with HTTP 204 and the acknowledgement withheld for 45 seconds. One native timeout failure appeared; Back retained the editor.
Cold startup showed Create Profile for the same pubky. Entry and request logs through 67 seconds after initialization show one contact-removal batch and one DELETE at each profile location, with no app sign-in or automatic full-flow replay. Counts were checked in logs; the recordings show failure, dismissal and startup.
| Failure and dismissal | Cold startup |
|---|---|
j1b-fault-2x.2x.mp4 |
j1b-restart-2x.2x.mp4 |
Both clips play at 2×.
Revoked session deletion, Disconnect and recovery ❌ Disconnect returns HTTP 401 and retains the editor.
R1: server-side revocation changed active grants from one to zero while retaining the local credentials. One deletion failed during contact loading with HTTP 401, without an app sign-in or second attempt. Disconnect showed “Failed to disconnect profile” with HTTP 401 and kept the editor open.
Cold startup then ran normal initialization and sign-in. A later explicit deletion removed contacts, deleted both profile locations once and reached profile onboarding. The Disconnect assertion remains failed. Grant counts and operation counts were checked in database output and entry/request logs; the recordings show the failed Disconnect and later recovery.
| Revoked-session deletion and Disconnect | Cold recovery and later explicit deletion |
|---|---|
r1-failure-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
Both clips play at 2×.
Adopted identity unlink after a lost acknowledgement ✅ One unlink failure; explicit Disconnect clears the Bitkit identity.
R2: a same-signature Ring-compatible credential fixture supplied a disposable adopted identity, with no contacts. Bitkit showed “Unlink pubky.” One Paykit-copy DELETE was accepted with HTTP 204 before its acknowledgement was withheld; one “Unable to Unlink Pubky” timeout followed. Entry and request logs show no pubky.app deletion, app sign-in or automatic full-flow replay.
Explicit Disconnect cleared the identity. A recorded cold launch and Profile entry confirmed profile onboarding; the credential fixture still listed the adopted identity, checked through its identity-list endpoint. Ring’s own UI was not exercised.
| Unlink timeout and Disconnect | Cold launch and profile onboarding |
|---|---|
r2-unlink-2x.2x.mp4 |
r2-onboarding-2x.2x.mp4 |
Both clips play at 2×. The first ends during navigation; the second confirms the persisted disconnected state.
Interrupted deletion and explicit Retry ✅ Failures remain explicit; Retry after restoring the route completes deletion.
R3: two explicit deletions failed at contact loading with HTTP 502 while the forwarding connection was unavailable. Back retained the editor after the first. The first Retry encountered HTTP 405 because the fault adapter did not handle the SDK’s alternate HTTP route. Once the adapter forwarded both HTTP route forms, one final explicit Retry removed the two contacts, deleted each profile location once and reached onboarding.
Entry/request logs show four explicit attempt entries, one contact-removal batch, one DELETE per profile location, and no app sign-in or automatic full-flow replay. The HTTP 405 was fixture setup, not an app failure.
| Outage, dismissal and early retries | Retry after the route repair |
|---|---|
r3-retry-2x.2x.mp4 |
r3-retry-restored-2x.2x.mp4 |
Both clips play at 2×. Counts and HTTP statuses were checked in entry and proxy logs.
Check: Exact head build and focused regressions ✅ The unmodified head builds and the focused profile/SDK suites pass.
assembleDevDebug passed. PubkyRepoTest, EditProfileViewModelTest and PaykitSdkServiceTest passed on restored unmodified source; finalized JUnit XML has no failures, errors or skips. The full local unit/lint gate was not repeated.
Exact-head hosted build/unit/Compose CI, lint and E2E report success; migration jobs were skipped. Those statuses are separate from the device evidence above.
During fresh-user setup, Android’s event log recorded a Bitkit JobService ANR and a Google Play Services initialization ANR. Their cause was not established; the listed cases ran after a device restart.
No planned device case remains unrun. These are fresh executions after the profile-path and native dependency changes; earlier device passes were not reused as current-head results.
Scope: Delta review of a8c3fd3..dc9a981, the complete six-file child patch and reachable profile/session/contact/backup integration, plus source inspection of inherited runtime/build changes. Deletion now bypasses the session-renewing pubky.app scope; ordinary saves retain the private renewal helper. The typed identity-error guard and conditional revision-conflict handling were checked in source and focused unit tests. These device cases do not certify the inherited wallet-restore, Spark, hardware or terminal complete-WALLET recovery journeys, or the separate original-owner/wipe fixes.
Twin: synonymdev/bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at a214b06, matches single-attempt deletion without error-triggered full-flow replay. Its inherited original-session guards differ, and Android’s new pubky.app deletion path is platform-specific. This was a source comparison, not an iOS device run.
Test hook and deciding entry logs
Not pushed. Provisions disposable test data and logs operation entry. Normal editor, repository, native authentication, deletion and backup decisions remain active. The owner-scoped proxy delays real successful DELETE replies; it substitutes no SDK result. The disposable Ring credential is omitted below.
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index b88fe73a7..7e39641b1 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -11,5 +11,6 @@
android:authorities="${applicationId}.devtools"
android:exported="true"
tools:ignore="ExportedContentProvider" />
+ <activity android:name=".ui.ProfileDeletionProbeActivity" android:exported="true" android:theme="@style/Theme.App" />
</application>
</manifest>
diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
index 08468f730..b68c29a36 100644
--- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@@ -1233,6 +1233,7 @@ class PubkyRepo @Inject constructor(
}
suspend fun deleteProfile(): Result<Unit> = runSuspendCatching {
+ android.util.Log.i("DeletionProbe", "deletion attempt entered") // test hook
withContext(ioDispatcher) {
requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
"No session available"
diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt
index 1b5264c76..27c18cd09 100644
--- a/app/src/main/java/to/bitkit/services/PubkyService.kt
+++ b/app/src/main/java/to/bitkit/services/PubkyService.kt
@@ -37,6 +37,7 @@ class PubkyService @Inject constructor(
}
suspend fun initializeAndImportSession(secret: String): Result<String> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "normal initialization entered") // test hook
paykitSdkService.initializeAndImportSession(secret).map { it.publicKey }
}
@@ -46,6 +47,7 @@ class PubkyService @Inject constructor(
// region Session management
suspend fun importSession(secret: String): String = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "stored-session import entered") // test hook
paykitSdkService.importSession(secret).publicKey
}
@@ -114,6 +116,7 @@ class PubkyService @Inject constructor(
}
suspend fun signIn(secretKeyHex: String): Unit = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "sign-in entered") // test hook
paykitSdkService.signIn(secretKeyHex)
Unit
}
@@ -211,6 +214,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePaykitProfile() = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "profile delete entered") // test hook
paykitSdkService.deletePaykitProfile()
}
@@ -231,6 +235,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePubkyProfile(expectedRevision: String, expectedIdentity: String) = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "pubky app delete entered") // test hook
paykitSdkService.deletePubkyProfile(expectedRevision, expectedIdentity)
}
@@ -242,6 +247,7 @@ class PubkyService @Inject constructor(
}
suspend fun contactRecords(): List<ContactRecord> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "contact read entered") // test hook
paykitSdkService.contactRecords()
}
@@ -278,6 +284,7 @@ class PubkyService @Inject constructor(
}
suspend fun removeContacts(publicKeys: List<String>): List<ContactRecord> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "contact remove entered count=${publicKeys.size}") // test hook
paykitSdkService.removeContacts(publicKeys)
}
diff --git a/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
new file mode 100644
--- /dev/null
+++ b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
@@ -0,0 +1,80 @@
+// test hook: disposable provisioning only; the journey runs in ordinary MainActivity.
+package to.bitkit.ui
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.FragmentActivity
+import com.synonym.paykit.ContactUpdate
+import dagger.hilt.android.AndroidEntryPoint
+import to.bitkit.data.keychain.Keychain
+import to.bitkit.repositories.PubkyRepo
+import to.bitkit.repositories.WalletRepo
+import to.bitkit.services.PubkyService
+import to.bitkit.ui.theme.AppThemeSurface
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ProfileDeletionProbeActivity : FragmentActivity() {
+ @Inject lateinit var repo: PubkyRepo
+ @Inject lateinit var service: PubkyService
+ @Inject lateinit var wallet: WalletRepo
+ @Inject lateinit var keychain: Keychain
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var status by remember { mutableStateOf("Preparing disposable identity") }
+ LaunchedEffect(Unit) {
+ try {
+ if (keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) == null) {
+ wallet.createWallet(null).getOrThrow()
+ }
+ repo.initialize().getOrThrow()
+ if (intent.getStringExtra("mode") == "ring") {
+ val secret = "<disposable credential omitted>"
+ val owner = service.publicKeyFromSecret(secret)
+ service.signUp(secret, requireNotNull(to.bitkit.env.Env.e2eHomeserverPubky), null)
+ service.signOut()
+ contentResolver.call(android.net.Uri.parse("content://to.pubky.ring.sharedpubky"), "set", owner, null)
+ repo.adoptRingIdentity(owner).getOrThrow()
+ repo.createIdentity("Disposable Ring identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ Log.i("DeletionProbe", "ring-adopted owner=${repo.publicKey.value} unlink=${repo.isRingAdoptedIdentity()}")
+ }
+ if (repo.publicKey.value == null) {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null)
+ .getOrThrow()
+ val peers = listOf("11", "22").map { service.publicKeyFromSecret(it.repeat(32)) }
+ service.saveContacts(peers.mapIndexed { index, key -> ContactUpdate(key, "Peer ${index + 1}") })
+ repo.loadContacts()
+ }
+ if (intent.getStringExtra("mode") == "renew") {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ }
+ Log.i("DeletionProbe", "retained secret=${keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name) != null} session=${keychain.loadString(Keychain.Key.PAYKIT_SESSION.name) != null}")
+ Log.i("DeletionProbe", "ready owner=${repo.publicKey.value} contacts=${service.contactRecords().size}")
+ status = "QA dc9a981: disposable identity ready. Launch ordinary Bitkit for J1."
+ } catch (error: Exception) {
+ status = "Provision failed: ${error.message}"
+ Log.e("DeletionProbe", status, error)
+ }
+ }
+ AppThemeSurface {
+ Column(modifier = Modifier.padding(32.dp)) {
+ Text(status, color = Color.White)
+ }
+ }
+ }
+ }
+}J1:
deletion attempt entered
contact read entered
contact remove entered count=2
pubky app delete entered
normal initialization entered
J1b:
deletion attempt entered
contact read entered
contact remove entered count=2
pubky app delete entered
profile delete entered
normal initialization entered
R1:
deletion attempt entered
contact read entered
normal initialization entered
sign-in entered
deletion attempt entered
contact read entered
contact remove entered count=2
pubky app delete entered
profile delete entered
R2:
deletion attempt entered
contact read entered
contact remove entered count=0
profile delete entered
R3:
deletion attempt entered
contact read entered
deletion attempt entered
contact read entered
deletion attempt entered
contact read entered
deletion attempt entered
contact read entered
contact remove entered count=2
pubky app delete entered
profile delete entered
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — revoked-session Disconnect still returns HTTP 401 and retains the editor.
Findings: No new code findings in this delta. The previous Disconnect failure remains reproducible, tracked in synonymdev/bitkit-android#1687 Bind profile deletion cleanup to the original sign-in. The separate original-owner removal fix remains in synonymdev/bitkit-android#1693 fix: bind profile removal to its original owner.
QA for 8a96806: Pixel 7 emulator, Android 16 / API 36, committed Paykit 0.1.1-rc8 and ldk-node 0.7.0-rc.80, local Pubky testnet, fresh unfunded identities. A debug hook provisions identities and logs operation entry; the ordinary editor, repository and native SDK determine outcomes. Installed APK bytes match the built QA APK. Recordings play at 2×; operation counts and HTTP results below were checked in entry/request logs, with UI states also checked in fresh layouts.
Lost pubky.app deletion acknowledgement with contacts, then restart ✅ One honest timeout; Back and cold startup do not replay deletion.
J1, the uncertain-response journey: two contacts were present. The homeserver accepted one pubky.app/profile.json DELETE with HTTP 204 before the proxy delayed its acknowledgement for 45 seconds. A native request timeout showed “Unable to Delete Profile.” Back kept the editor.
Cold startup showed Restore Profile for the retained pubky. Entry/request logs through the recorded restart observation show one contact-removal batch, one pubky.app DELETE, no Paykit-copy DELETE, no app sign-in and no automatic full-flow replay.
| Failure and dismissal | Cold startup |
|---|---|
j1-fault-2x.2x.mp4 |
j1-restart-2x.2x.mp4 |
Lost Paykit profile deletion acknowledgement, then restart ✅ The later timeout also returns one failure without replay.
J1b: a separate identity had two contacts. Its pubky.app DELETE completed with HTTP 204, then the Paykit-copy DELETE was accepted with HTTP 204 before its acknowledgement was delayed for 45 seconds. One native timeout appeared; Back kept the editor.
Cold startup showed Create Profile for the same pubky. Entry/request logs through the recorded restart observation show one contact-removal batch and one DELETE at each profile location, with no app sign-in or automatic full-flow replay.
| Failure and dismissal | Cold startup |
|---|---|
j1b-fault-2x.2x.mp4 |
j1b-restart-2x.2x.mp4 |
Revoked session deletion, Disconnect and recovery ❌ Disconnect returns HTTP 401 and keeps the editor.
R1: database output confirms server-side revocation changed active grants from one to zero while local credentials remained. One deletion failed during contact loading with HTTP 401, without app sign-in or a second attempt. Disconnect showed “Failed to disconnect profile,” HTTP 401, and retained the editor.
Cold startup then ran normal initialization and sign-in. One later explicit deletion removed both contacts, deleted each profile location once and reached profile onboarding. This later success does not change the failed Disconnect assertion.
| Deletion and failed Disconnect | Cold recovery and later explicit deletion |
|---|---|
r1-failure-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
Adopted identity unlink after a lost acknowledgement ✅ One unlink timeout; explicit Disconnect clears Bitkit.
R2: a same-signature Ring-compatible credential fixture supplied a disposable adopted identity with no contacts. The ordinary editor showed “Unlink pubky.” One Paykit-copy DELETE was accepted with HTTP 204 before its acknowledgement was delayed; one “Unable to Unlink Pubky” timeout followed. Entry/request logs show no pubky.app deletion, app sign-in or automatic full-flow replay.
Explicit Disconnect cleared Bitkit. A recorded cold launch and Profile entry confirmed onboarding; the credential fixture still listed the adopted identity, checked through its identity-list endpoint. Ring’s own UI was not exercised.
| Unlink timeout and Disconnect | Persisted onboarding after cold launch |
|---|---|
r2-unlink-2x.2x.mp4 |
r2-onboarding-2x.2x.mp4 |
Interrupted deletion and explicit Retry ✅ Failures stay explicit; Retry after reconnect completes deletion.
R3: two explicit attempts failed at contact loading with HTTP 502 while the app’s homeserver forwarding connection was unavailable. Back after the first failure kept the editor. Once the connection was restored, one explicit Retry removed both contacts, deleted each profile location once and reached onboarding.
Entry/request logs show three explicit attempts, one contact-removal batch, one DELETE per profile location and no app sign-in or automatic full-flow replay.
| Outage and dismissal | Explicit Retry after reconnect |
|---|---|
r3-outage-2x.2x.mp4 |
r3-retry-2x.2x.mp4 |
Check: Exact head build and focused regressions ✅ The unmodified head builds and the focused profile/SDK suites pass.
assembleDevDebug passed on unmodified source. PubkyRepoTest, EditProfileViewModelTest, PaykitSdkServiceTest and PaykitSdkOperationLockTest passed; finalized JUnit XML has no failures, errors or skips. The new same-key sign-in, scheduled profile refresh/wipe and missing-profile setup regressions are present in those passing results. The full local unit/lint gates were not repeated.
No planned device case remains unrun. All five were freshly driven with the current native packages; earlier device passes were not reused.
Scope: Delta review of dc9a981..8a96806, all six delta files, the complete six-file child patch and reachable deletion/session/profile/wipe integration, with an independent source review. Background profile ownership and status publication, SDK wipe admission and native pin changes were checked in source and focused tests; these device cases do not certify the inherited payment, wallet-restore, Spark or hardware journeys.
Twin: synonymdev/bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at a214b06, matches one-attempt deletion without error-triggered full-flow replay. Its inherited original-session guards differ. The uncertain-response journey matches; this was a source comparison, not an iOS device run.
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — revoked-session Disconnect still returns HTTP 401 and retains the editor.
Findings: No new code findings. The previous Disconnect failure remains reproducible. It is the unchanged cleanup limitation tracked in synonymdev/bitkit-android#1687 Bind profile deletion cleanup to the original sign-in; the separate owner-binding repair is in synonymdev/bitkit-android#1693 fix: bind profile removal to its original owner.
QA for 8d10218: Pixel 7 emulator, Android 16 / API 36, committed Paykit 0.1.1-rc8, local Pubky testnet and a fresh unfunded identity with two contacts. The installed APK matches the built QA APK. An unpushed debug hook provisions test data and logs operation entry; ordinary app and native SDK code determine outcomes. Recordings play at 2×.
Revoked session deletion, Disconnect and recovery ❌ Disconnect shows HTTP 401 and keeps the editor.
R1: the homeserver database confirms active grants changed from one to zero while local credentials remained. One explicit deletion failed during contact loading, without automatic sign-in or a second deletion. Disconnect then showed “Failed to disconnect profile” with HTTP 401 and retained Edit Profile.
Cold startup ran normal initialization and sign-in. A later explicit deletion removed both contacts, called each profile deletion once and reached profile onboarding. Database output and operation-entry logs establish revocation and counts; the recordings and UI layouts establish the visible outcomes. Successful recovery does not change the failed Disconnect assertion.
| Revoked-session deletion and failed Disconnect | Cold recovery and explicit deletion |
|---|---|
r1-revoked-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
The following passes are retained from the report and recordings at 8a96806. Deletion, editor controls, session recovery, private cleanup, native pins and the uncertain-response journey are unchanged. These are earlier executions, not fresh device runs at 8d10218.
Lost pubky.app deletion acknowledgement with contacts, then restart ✅ Passed at 8a96806; retained for unchanged paths.
J1: accepted deletion followed by a native timeout, dismissal and cold startup without automatic replay. The linked earlier report includes the restart recording and request/entry-log checks.
j1-fault-2x.2x.mp4
Earlier failure and dismissal recording.
Lost Paykit profile deletion acknowledgement, then restart ✅ Passed at 8a96806; retained for unchanged paths.
J1b: the later profile deletion also returned one honest timeout without full-flow replay through dismissal and restart, as checked in the earlier report.
j1b-fault-2x.2x.mp4
Earlier Paykit-copy timeout and dismissal recording.
Adopted identity unlink after a lost acknowledgement ✅ Passed at 8a96806; retained for unchanged paths.
R2: one unlink timeout followed by explicit Disconnect and persisted onboarding. The earlier run used a same-signature Ring-compatible credential fixture; Ring’s own UI was not exercised.
r2-unlink-2x.2x.mp4
Earlier unlink timeout and Disconnect recording.
Interrupted deletion and explicit Retry ✅ Passed at 8a96806; retained for unchanged paths.
R3: outage failures remained explicit; Retry after the route returned completed deletion. The earlier report includes the outage recording and operation/request counts.
r3-retry-2x.2x.mp4
Earlier explicit Retry after reconnect recording.
Check: Exact head build and focused regressions ✅ The unmodified head builds and the focused suites pass.
assembleDevDebug passed on unmodified source. Focused profile, SDK-lock, backup, Lightning, Spark, send-overlay and payment-error suites passed; finalized JUnit XML has no failures, errors or skips. The full local unit/lint gates were not repeated.
Scope: Delta review of 8a96806..8d10218, all 16 incoming files and the six-file child patch’s deletion integration, with an independent source reviewer. The incoming files match the live base; the child patch is unchanged. Inherited Spark restore, cross-rail payments, stopped-node UI and full-withdrawal changes were checked in source and focused units, not driven on a device. Original-owner removal, wallet wipe and terminal complete-wallet recovery are not certified by this case. No planned deletion case remains unrun.
Twin: synonymdev/bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at a214b06, matches single-attempt deletion without error-triggered full-flow replay. Its original-session guards differ; the uncertain-response journey matches. No iOS device run was performed.
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — revoked-session Disconnect still returns HTTP 401 and retains Edit Profile.
Findings: No new code findings. The previous Disconnect failure remains reproducible. This is the unchanged cleanup limitation tracked separately:
- bitkit-android#1687 Bind profile deletion cleanup to the original sign-in.
- bitkit-android#1693 fix: bind profile removal to its original owner.
QA for 5f0a8ad: Pixel 7 emulator, Android 16 / API 36, committed Paykit 0.1.1-rc8, local Pubky testnet and a fresh unfunded identity with two contacts. An unpushed debug hook provisions the test data and logs operation entry; the ordinary editor, repository and native SDK determine outcomes. Installed APK bytes match the built QA APK. Recordings play at 2×.
Revoked session deletion, Disconnect and recovery ❌ Disconnect reports HTTP 401 and keeps the editor.
R1: the homeserver database confirms active grants changed from one to zero while local credentials remained. One explicit deletion failed during contact loading, with no automatic sign-in or second deletion. Selecting Disconnect left Edit Profile open; the captured UI layout reports “Failed to disconnect profile” and acquire Pubky write lock (HTTP 401).
Cold startup ran normal initialization and sign-in. A later explicit deletion removed both contacts, entered each profile deletion once and reached profile onboarding. Database output and operation-entry logs establish revocation and counts; the recordings and UI layouts establish the visible states. Recovery does not change the failed Disconnect assertion.
| Revoked-session deletion and Disconnect | Cold recovery and explicit deletion |
|---|---|
r1-revoked-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
| One failed attempt, then Disconnect retains the editor. | Normal startup recovery, followed by a successful explicit deletion. |
The following passes are retained from the report and recordings at 8a96806. Deletion, editor controls, session recovery, private cleanup, native pins and the uncertain-response journey are unchanged. These are earlier executions, not fresh device runs at 5f0a8ad.
Lost pubky.app deletion acknowledgement with contacts, then restart ✅ Passed at [8a96806](https://github.com/synonymdev/bitkit-android/commit/8a96806c29e05abe004c1c94ea163202040f66ac); retained for unchanged paths.
J1: accepted pubky.app deletion followed by a native timeout, Back dismissal and cold startup without automatic replay. The earlier report includes the restart recording and request/entry-log checks.
j1-fault-2x.2x.mp4
Earlier failure and dismissal recording.
Lost Paykit profile deletion acknowledgement, then restart ✅ Passed at [8a96806](https://github.com/synonymdev/bitkit-android/commit/8a96806c29e05abe004c1c94ea163202040f66ac); retained for unchanged paths.
J1b: the later Paykit-copy deletion also returned one honest timeout without full-flow replay through dismissal and restart, as checked in the earlier report.
j1b-fault-2x.2x.mp4
Earlier Paykit-copy timeout and dismissal recording.
Adopted identity unlink after a lost acknowledgement ✅ Passed at [8a96806](https://github.com/synonymdev/bitkit-android/commit/8a96806c29e05abe004c1c94ea163202040f66ac); retained for unchanged paths.
R2: one unlink timeout followed by explicit Disconnect and persisted onboarding. The earlier run used a same-signature Ring-compatible credential fixture; Ring’s own UI was not exercised.
r2-unlink-2x.2x.mp4
Earlier unlink timeout and Disconnect recording.
Interrupted deletion and explicit Retry ✅ Passed at [8a96806](https://github.com/synonymdev/bitkit-android/commit/8a96806c29e05abe004c1c94ea163202040f66ac); retained for unchanged paths.
R3: outage failures stayed explicit; Retry after the route returned completed deletion. The earlier report includes the outage recording and operation/request counts.
r3-retry-2x.2x.mp4
Earlier explicit Retry after reconnect recording.
Check: Exact head build and focused regressions ✅ The unmodified head builds and focused suites pass.
assembleDevDebug passed on unmodified source. Focused wallet balance, balance derivation, send navigation, swap observation, profile, SDK-lock, backup, Lightning and Spark suites passed. Finalized JUnit XML has no failures, errors or skips, including the new stopped-node balance, cache-ordering, retirement and result-route regressions. The full local unit and lint gates were not repeated.
No planned deletion case remains unrun.
Scope: Delta review of 8d10218..5f0a8ad, all ten incoming files and the six-file child patch’s integration, with an independent source reviewer. The incoming files match the live base; the child patch is unchanged. The balance-publication changes, cache hydration, wallet retirement, swap observation and payment-result visibility were checked in source and focused units. These deletion recordings do not drive funded Spark sends, withdrawals, complete-wallet restore or the separate original-owner removal work.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at a214b06, matches single-attempt deletion without error-triggered full-flow replay. Its original-session guards differ; the uncertain-response journey matches. No iOS device run was performed.
Test hook and deciding entry logs
Not pushed. Provisions a disposable unfunded identity with two synthetic contacts and logs operation entry. It changes no authentication, deletion, cleanup or backup decision. The case then runs in ordinary MainActivity.
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index b88fe73a7..7e39641b1 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -11,5 +11,6 @@
android:authorities="${applicationId}.devtools"
android:exported="true"
tools:ignore="ExportedContentProvider" />
+ <activity android:name=".ui.ProfileDeletionProbeActivity" android:exported="true" android:theme="@style/Theme.App" />
</application>
</manifest>
diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
index 49c64e381..9f4bd4afa 100644
--- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@@ -1241,6 +1241,7 @@ class PubkyRepo @Inject constructor(
}
suspend fun deleteProfile(): Result<Unit> = runSuspendCatching {
+ android.util.Log.i("DeletionProbe", "deletion attempt entered") // test hook
withContext(ioDispatcher) {
requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
"No session available"
diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt
index 1b5264c76..27c18cd09 100644
--- a/app/src/main/java/to/bitkit/services/PubkyService.kt
+++ b/app/src/main/java/to/bitkit/services/PubkyService.kt
@@ -37,6 +37,7 @@ class PubkyService @Inject constructor(
}
suspend fun initializeAndImportSession(secret: String): Result<String> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "normal initialization entered") // test hook
paykitSdkService.initializeAndImportSession(secret).map { it.publicKey }
}
@@ -46,6 +47,7 @@ class PubkyService @Inject constructor(
// region Session management
suspend fun importSession(secret: String): String = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "stored-session import entered") // test hook
paykitSdkService.importSession(secret).publicKey
}
@@ -114,6 +116,7 @@ class PubkyService @Inject constructor(
}
suspend fun signIn(secretKeyHex: String): Unit = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "sign-in entered") // test hook
paykitSdkService.signIn(secretKeyHex)
Unit
}
@@ -211,6 +214,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePaykitProfile() = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "profile delete entered") // test hook
paykitSdkService.deletePaykitProfile()
}
@@ -231,6 +235,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePubkyProfile(expectedRevision: String, expectedIdentity: String) = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "pubky app delete entered") // test hook
paykitSdkService.deletePubkyProfile(expectedRevision, expectedIdentity)
}
@@ -242,6 +247,7 @@ class PubkyService @Inject constructor(
}
suspend fun contactRecords(): List<ContactRecord> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "contact read entered") // test hook
paykitSdkService.contactRecords()
}
@@ -278,6 +284,7 @@ class PubkyService @Inject constructor(
}
suspend fun removeContacts(publicKeys: List<String>): List<ContactRecord> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "contact remove entered count=${publicKeys.size}") // test hook
paykitSdkService.removeContacts(publicKeys)
}
diff --git a/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
new file mode 100644
--- /dev/null
+++ b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
@@ -0,0 +1,70 @@
+// test hook: disposable provisioning only; the journey runs in ordinary MainActivity.
+package to.bitkit.ui
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.FragmentActivity
+import com.synonym.paykit.ContactUpdate
+import dagger.hilt.android.AndroidEntryPoint
+import to.bitkit.data.keychain.Keychain
+import to.bitkit.repositories.PubkyRepo
+import to.bitkit.repositories.WalletRepo
+import to.bitkit.services.PubkyService
+import to.bitkit.ui.theme.AppThemeSurface
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ProfileDeletionProbeActivity : FragmentActivity() {
+ @Inject lateinit var repo: PubkyRepo
+ @Inject lateinit var service: PubkyService
+ @Inject lateinit var wallet: WalletRepo
+ @Inject lateinit var keychain: Keychain
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var status by remember { mutableStateOf("Preparing disposable identity") }
+ LaunchedEffect(Unit) {
+ try {
+ if (keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) == null) {
+ wallet.createWallet(null).getOrThrow()
+ }
+ repo.initialize().getOrThrow()
+ if (repo.publicKey.value == null) {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null)
+ .getOrThrow()
+ val peers = listOf("11", "22").map { service.publicKeyFromSecret(it.repeat(32)) }
+ service.saveContacts(peers.mapIndexed { index, key -> ContactUpdate(key, "Peer ${index + 1}") })
+ repo.loadContacts()
+ }
+ if (intent.getStringExtra("mode") == "renew") {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ }
+ Log.i("DeletionProbe", "retained secret=${keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name) != null} session=${keychain.loadString(Keychain.Key.PAYKIT_SESSION.name) != null}")
+ Log.i("DeletionProbe", "ready owner=${repo.publicKey.value} contacts=${service.contactRecords().size}")
+ status = "QA 5f0a8ad: disposable identity ready. Launch ordinary Bitkit for J1."
+ } catch (error: Exception) {
+ status = "Provision failed: ${error.message}"
+ Log.e("DeletionProbe", status, error)
+ }
+ }
+ AppThemeSurface {
+ Column(modifier = Modifier.padding(32.dp)) {
+ Text(status, color = Color.White)
+ }
+ }
+ }
+ }
+}(retained credentials; one server grant revoked)
deletion attempt entered
contact read entered
(Disconnect: HTTP 401; editor retained)
(cold startup)
normal initialization entered
sign-in entered
(later explicit Delete)
deletion attempt entered
contact read entered
contact remove entered count=2
pubky app delete entered
profile delete entered
Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete PR diff against merge base ff9275d6, at 5f0a8ad. The comparison base moved from a2c58f24, and that parent update touches the reviewed profile files. There is no inherited baseline.
No new actionable code findings.
Confirm and explicit Retry both call PubkyRepo.deleteProfile() once. A lost Paykit acknowledgement and an identity error from the pubky.app profile delete remain on the failure dialog. Session refresh remains for ordinary initialization and for non-deletion pubky.app writes. Dismissing the dialog leaves the signed-in profile in place, and a later cold start does not resume deletion.
bitkit-ios#1122 at a214b06 removes the same full-flow replay. Android also deletes the pubky.app profile before the Paykit copy; the iOS deleter removes the Paykit profile. This change leaves that Android delete in place and stops refreshing the session around it. bitkit-android#1686 was compared at this merge base.
A failed signOut() still returns before local state is cleared, so Disconnect leaves Edit Profile open. That limitation remains bitkit-android#1687, with the repair in open bitkit-android#1693. The open changes request on this head reports the same HTTP 401 outcome in review 5870574863.
Validation: the deletion source and PubkyRepoTest / EditProfileViewModelTest were inspected and not executed here. On this revision, hosted unit and Compose, lint, and E2E passed. Those jobs do not run J1.
Device testing: not performed in this review. The attributed J1 passes are from 8a96806.
Recommended before device testing
Repeat J1 journeys/profile/delete-profile-uncertain-response.xml on this head. Use a disposable identity and a proxy that accepts one profile deletion and drops its acknowledgement. Expect the failure dialog, no success toast, and no second deletion or sign-in through dismissal and a cold restart without Retry.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — revoked-session Disconnect still returns HTTP 401 and retains Edit Profile.
Findings: No new code findings in this delta. The previous Disconnect failure remains reproducible. The unchanged cleanup limitation is tracked separately:
- bitkit-android#1687 Bind profile deletion cleanup to the original sign-in.
- bitkit-android#1693 fix: bind profile removal to its original owner.
QA for 46d5901: Android 16 / API 36 ARM64 emulator, committed Paykit 0.1.1-rc8, local Pubky testnet and a fresh unfunded identity with two synthetic contacts. I built the unmodified head, then an unpublished Debug hook for provisioning and operation-entry logging. The ordinary editor, repository and native SDK determined outcomes. Installed APK bytes match the QA build; its native libraries match the pristine APK. New recordings play at 2× with all changed frames retained.
Revoked session deletion, Disconnect and recovery ❌ Disconnect reports HTTP 401 and keeps the editor; later cold recovery succeeds.
R1: database readback confirmed the disposable identity's active grants changed from one to zero while local credentials remained. One explicit deletion failed during contact loading, with no automatic sign-in or full-flow replay. Selecting Disconnect left Edit Profile open. The UI layout captured “Failed to disconnect profile” and acquire Pubky write lock (HTTP 401).
Cold startup entered normal initialization and sign-in. A later explicit deletion removed both contacts and reached profile onboarding. Operation-entry logs show each profile deletion entered once; server responses confirm HTTP 204 at both profile paths. The successful recovery does not pass Disconnect.
| Revoked-session deletion and Disconnect | Cold recovery and explicit deletion |
|---|---|
r1-revoked-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
| One failed attempt, then Disconnect retains the editor. | Normal startup recovery, followed by explicit deletion and onboarding. |
The following four passes are retained from the report at 8a96806. Deletion, editor controls, session recovery, private cleanup, native pins and the journey are unchanged. These are earlier executions.
Lost pubky.app deletion acknowledgement with contacts, then restart ✅ Passed at 8a96806; retained for unchanged paths.
J1: accepted pubky.app deletion followed by a native timeout, Back dismissal and cold startup without automatic replay. Request counts and operation-entry logs were checked in the earlier report.
| Failure and explicit controls | Restart or completion |
|---|---|
j1-fault-2x.2x.mp4 |
j1-restart-2x.2x.mp4 |
Earlier case recordings.
Lost Paykit profile deletion acknowledgement, then restart ✅ Passed at 8a96806; retained for unchanged paths.
J1b: the later Paykit-copy deletion returned an honest timeout without full-flow replay through dismissal and restart, as checked in the earlier report.
| Failure and explicit controls | Restart or completion |
|---|---|
j1b-fault-2x.2x.mp4 |
j1b-restart-2x.2x.mp4 |
Earlier case recordings.
Adopted identity unlink after a lost acknowledgement ✅ Passed at 8a96806; retained for unchanged paths.
R2: one unlink timeout followed by explicit Disconnect and persisted onboarding. The earlier run used a same-signature Ring-compatible credential fixture; Ring’s own UI was not exercised.
| Failure and explicit controls | Restart or completion |
|---|---|
r2-unlink-2x.2x.mp4 |
r2-onboarding-2x.2x.mp4 |
Earlier case recordings.
Interrupted deletion and explicit Retry ✅ Passed at 8a96806; retained for unchanged paths.
R3: outage failures stayed explicit; Retry after the route returned completed deletion. The earlier report checked operation/request counts.
| Failure and explicit controls | Restart or completion |
|---|---|
r3-outage-2x.2x.mp4 |
r3-retry-2x.2x.mp4 |
Earlier case recordings.
Read cached balances and receiving details during node startup ✅ Passed on parent 89f3bc6; retained because the affected files match this head.
B1 / parent manual case 6: the exact-parent report independently held native startup and cache loading. After cache release, Home showed 70,000 sats and Spending 20,000 sats, and the cached Savings QR worked before native startup finished. Amounts were display fixtures. Readbacks confirmed persisted address, invoice and BIP21; fresh native zero balances replaced the fixture after startup release.
Auto/Spending receive retained its existing refresh/node gates. This does not establish a usable Lightning invoice UI before startup. Subsequent-refresh ordering and reservation rejection also passed in the focused unit regressions at this head.
startup-cache-ui-2x.2x.mp4
Earlier Home, Spending and Savings-receive recording during held startup, then release. The opening error toast comes from the Debug launch URI used by that harness.
Check: Exact head build and focused regressions ✅ The unmodified head builds and focused suites pass.
assembleDevDebug passed on unmodified source. Focused balance, derivation, profile, SDK-lock, wallet retirement, backup, Lightning, Spark and send-result suites passed. Finalized JUnit XML has no failures, errors or skips, including the new cache-during-serialized-read regression. Full local unit, lint and Compose instrumentation gates were not repeated.
No planned deletion case remains unrun.
Scope: Delta 5f0a8ad..46d5901, all three incoming files and the unchanged six-file deletion patch's integration, with an independent source reviewer. The incoming files match the exact parent. The startup device case is retained; overlapping refresh ordering and cancellation also rely on focused units. Original-owner removal, complete-WALLET recovery and bounded native Reset remain separate work described in the linked child and parent reviews.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at a214b06, matches single-attempt deletion without error-triggered full-flow replay. Its original-session guards and storage targets differ; the uncertain-response journey is identical. No iOS device run was performed.
Unpublished test hook
Provisions one disposable unfunded identity with two synthetic contacts and logs operation entry. It changes no authentication, deletion, cleanup or backup decision. The case runs in ordinary MainActivity.
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index b88fe73a7..7e39641b1 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -11,5 +11,6 @@
android:authorities="${applicationId}.devtools"
android:exported="true"
tools:ignore="ExportedContentProvider" />
+ <activity android:name=".ui.ProfileDeletionProbeActivity" android:exported="true" android:theme="@style/Theme.App" />
</application>
</manifest>
diff --git a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
index 49c64e381..9f4bd4afa 100644
--- a/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@@ -1241,6 +1241,7 @@ class PubkyRepo @Inject constructor(
}
suspend fun deleteProfile(): Result<Unit> = runSuspendCatching {
+ android.util.Log.i("DeletionProbe", "deletion attempt entered") // test hook
withContext(ioDispatcher) {
requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
"No session available"
diff --git a/app/src/main/java/to/bitkit/services/PubkyService.kt b/app/src/main/java/to/bitkit/services/PubkyService.kt
index 1b5264c76..27c18cd09 100644
--- a/app/src/main/java/to/bitkit/services/PubkyService.kt
+++ b/app/src/main/java/to/bitkit/services/PubkyService.kt
@@ -37,6 +37,7 @@ class PubkyService @Inject constructor(
}
suspend fun initializeAndImportSession(secret: String): Result<String> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "normal initialization entered") // test hook
paykitSdkService.initializeAndImportSession(secret).map { it.publicKey }
}
@@ -46,6 +47,7 @@ class PubkyService @Inject constructor(
// region Session management
suspend fun importSession(secret: String): String = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "stored-session import entered") // test hook
paykitSdkService.importSession(secret).publicKey
}
@@ -114,6 +116,7 @@ class PubkyService @Inject constructor(
}
suspend fun signIn(secretKeyHex: String): Unit = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "sign-in entered") // test hook
paykitSdkService.signIn(secretKeyHex)
Unit
}
@@ -211,6 +214,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePaykitProfile() = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "profile delete entered") // test hook
paykitSdkService.deletePaykitProfile()
}
@@ -231,6 +235,7 @@ class PubkyService @Inject constructor(
}
suspend fun deletePubkyProfile(expectedRevision: String, expectedIdentity: String) = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "pubky app delete entered") // test hook
paykitSdkService.deletePubkyProfile(expectedRevision, expectedIdentity)
}
@@ -242,6 +247,7 @@ class PubkyService @Inject constructor(
}
suspend fun contactRecords(): List<ContactRecord> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "contact read entered") // test hook
paykitSdkService.contactRecords()
}
@@ -278,6 +284,7 @@ class PubkyService @Inject constructor(
}
suspend fun removeContacts(publicKeys: List<String>): List<ContactRecord> = ServiceQueue.PUBKY.background {
+ android.util.Log.i("DeletionProbe", "contact remove entered count=${publicKeys.size}") // test hook
paykitSdkService.removeContacts(publicKeys)
}
diff --git a/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
new file mode 100644
--- /dev/null
+++ b/app/src/debug/java/to/bitkit/ui/ProfileDeletionProbeActivity.kt
@@ -0,0 +1,70 @@
+// test hook: disposable provisioning only; the journey runs in ordinary MainActivity.
+package to.bitkit.ui
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.FragmentActivity
+import com.synonym.paykit.ContactUpdate
+import dagger.hilt.android.AndroidEntryPoint
+import to.bitkit.data.keychain.Keychain
+import to.bitkit.repositories.PubkyRepo
+import to.bitkit.repositories.WalletRepo
+import to.bitkit.services.PubkyService
+import to.bitkit.ui.theme.AppThemeSurface
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class ProfileDeletionProbeActivity : FragmentActivity() {
+ @Inject lateinit var repo: PubkyRepo
+ @Inject lateinit var service: PubkyService
+ @Inject lateinit var wallet: WalletRepo
+ @Inject lateinit var keychain: Keychain
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ var status by remember { mutableStateOf("Preparing disposable identity") }
+ LaunchedEffect(Unit) {
+ try {
+ if (keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) == null) {
+ wallet.createWallet(null).getOrThrow()
+ }
+ repo.initialize().getOrThrow()
+ if (repo.publicKey.value == null) {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null)
+ .getOrThrow()
+ val peers = listOf("11", "22").map { service.publicKeyFromSecret(it.repeat(32)) }
+ service.saveContacts(peers.mapIndexed { index, key -> ContactUpdate(key, "Peer ${index + 1}") })
+ repo.loadContacts()
+ }
+ if (intent.getStringExtra("mode") == "renew") {
+ repo.createIdentity("Disposable cleanup identity", "Local testnet", emptyList(), emptyList(), null).getOrThrow()
+ }
+ Log.i("DeletionProbe", "retained secret=${keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name) != null} session=${keychain.loadString(Keychain.Key.PAYKIT_SESSION.name) != null}")
+ Log.i("DeletionProbe", "ready owner=${repo.publicKey.value} contacts=${service.contactRecords().size}")
+ status = "QA 46d5901: disposable identity ready. Launch ordinary Bitkit for J1."
+ } catch (error: Exception) {
+ status = "Provision failed: ${error.message}"
+ Log.e("DeletionProbe", status, error)
+ }
+ }
+ AppThemeSurface {
+ Column(modifier = Modifier.padding(32.dp)) {
+ Text(status, color = Color.White)
+ }
+ }
+ }
+ }
+}Reviewed by gpt-6.1-sol-high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete PR diff against merge base b2c3ea6e, at f270a9e9. No verified baseline was supplied, so this pass covers the whole pinned diff and inherits no earlier coverage.
No new actionable code findings.
Confirming deletion runs PubkyRepo.deleteProfile() once. An uncertain failure stays on the existing failure dialog. Back or an outside tap dismisses that dialog without another deletion or sign-in. Retry and Sign out remain explicit actions. refreshSessionIfPossible still serves pubky.app profile writes; profile deletion no longer calls it. A concurrent-update conflict still retries only that one pubky.app delete. bitkit-ios#1122 at a214b06b likewise makes one deletion attempt and shows failure options. Session restore on dependency bitkit-android#1686 at b2c3ea6e does not start profile deletion.
#1687 remains open. Deletion and disconnect still clean up without capturing the sign-in that started the operation. This diff leaves that ownership boundary in place. Earlier revoked-session Disconnect results, including the report on 46d59014, were not repeated on this head.
Device testing: not performed in this review. The author's J1 note refers to tree fe1d9686. PubkyRepo has since taken parent recovery changes, so that run is not evidence for this revision.
Recommended before device testing: Repeat journeys/profile/delete-profile-uncertain-response.xml on this build, including Back and a cold restart without Retry. The fault-injection proxy described in the journey is required. CI and Lint succeeded on this head. E2E also succeeded and does not run that proxy journey. This review did not execute the unit suite.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — revoked-session Disconnect still fails; the inherited Spark route also rejects a valid mixed-price request.
Findings:
- MEDIUM — Clear the unselected invoice when choosing a Spark leg.
AppViewModel.kt:4862retainsdecodedInvoicewhile Spending remainsSendMethod.LIGHTNING. For a USD 2.50 request withbtc=0.00001andbtc-lightning=0.00002, the selected on-chain terms require 2,500 sats while the retained invoice requires 5,000. The Spark confirmation opens at 2,500, but confirming is rejected by invoice-only validation invalidateIncomingPaymentRequestbeforepayServiceLegruns. The added ViewModel regression fails on this head and passes when the Service pick clearsdecodedInvoice;route.invoicealready retains the invoice for a later direct route. Guard invoice-only validation for service legs or clear the active invoice. This enters through the parent update in the reviewed delta; the current six-file deletion patch does not edit this file.
The failed Disconnect case repeats the previous result. It is tracked separately:
- bitkit-android#1687 Bind profile deletion cleanup to the original sign-in.
- bitkit-android#1693 fix: bind profile removal to its original owner.
QA for f270a9e: Android 16 / API 36 ARM64 emulator, committed Paykit 0.1.1-rc8 and Breez Spark 0.28.2. I built the unmodified head. Profile testing uses an unpublished Debug provisioning/entry-logging hook with ordinary editor, repository and native decisions. Both QA builds' native libraries match the pristine APK. New recordings play at 2× with all changed frames retained.
Revoked session deletion, Disconnect and recovery ❌ Disconnect reports HTTP 401 and retains Edit Profile; later cold recovery succeeds.
R1: database readback confirmed one active grant became zero while local credentials remained. One explicit deletion failed during contact loading without automatic sign-in or a second deletion. Disconnect showed “Failed to disconnect profile” and acquire Pubky write lock (HTTP 401), leaving the editor open.
Cold startup entered normal initialization and sign-in. A later explicit deletion removed both contacts, entered each profile deletion once, and reached profile onboarding. Counts and revocation were checked in entry/server logs and database output; visible states were checked in recordings and UI layouts. Recovery does not pass Disconnect.
| Revoked-session deletion and Disconnect | Cold recovery and explicit deletion |
|---|---|
r1-revoked-2x.2x.mp4 |
r1-recovery-2x.2x.mp4 |
| One failed attempt, then the editor remains. | Ordinary recovery, explicit deletion and onboarding. |
Spark connection in App Status ✅ The pristine build shows Connected and Using Spark, and the Spark row opens its details.
S1: with no Lightning channels, Spark sits directly below Lightning Connection. The latter reads “Using Spark.” Tapping Spark opens its provider, address and balance screen. This drives settings/app-status-spark-row.xml.
s1-2x.2x.mp4
App Status and Spark detail navigation on the unmodified build.
Pay an on-chain contact from Spark ✅ A 20,000-sat Spark withdrawal reaches Bitcoin Sent and pays the contact exactly 20,000 sats on chain.
S2: a separate disposable wallet received 100,000 real regtest sats through a Spark invoice, confirmed by the fixture payment acknowledgement and Home layout. With Savings at zero, the contact's sole on-chain endpoint opened Amount with SPENDING selected. Entering 20,000 reached Confirm; Show Details displayed SPENDING, the contact and ROUTE “Spark,” with a 3,081-sat fee.
One swipe dispatched the real native withdrawal. Its initial PENDING response showed Payment Pending; after mining and normal SDK settlement, the screen changed to Bitcoin Sent for 20,000. Details showed Confirmed, the contact, the 3,081-sat fee and BoostDisabled. Transaction readback confirms output 0 pays exactly 20,000 sats to the fixture issuer; the withdrawal journal is cleared and persisted Spark balance is 76,919 sats.
This drives paykit-spark-route.xml on bitkit-docker#37 feat: local spark regtest network at 5ef4f09. The Debug configuration points the published native SDK at that local operator/chain fixture and disables fee ladders as its public operator requires. Provisioning, contact-link completion and private-list delivery supplied the prerequisites. Routing, quoting, dispatch, journal settlement and receipt decisions were unchanged. Hosted faucet funding was refused; no hosted Lightspark route result is claimed.
| Amount, Spark route and dispatch | Settlement and confirmed receipt |
|---|---|
s2-send-2x.2x.mp4 |
s2-settlement-2x.2x.mp4 |
| One withdrawal reaches its honest pending state. | Normal settlement reaches Bitcoin Sent; Details opens the receipt. |
The following four passes are retained from the report at 8a96806. Deletion, editor controls, session recovery and private-cleanup behavior are unchanged, and Paykit remains 0.1.1-rc8. The new payment-router and Spark packages do not reach these profile operations. These are earlier executions.
Lost pubky.app deletion acknowledgement with contacts, then restart ✅ Passed at 8a96806; retained for unchanged paths.
J1: accepted pubky.app deletion followed by a native timeout, Back dismissal and cold startup without automatic replay. The earlier report checked request and entry-log counts.
| Failure and dismissal | Cold startup |
|---|---|
j1-fault-2x.2x.mp4 |
j1-restart-2x.2x.mp4 |
Earlier J1 recordings.
Lost Paykit profile deletion acknowledgement, then restart ✅ Passed at 8a96806; retained for unchanged paths.
J1b: the later Paykit-copy deletion returned one honest timeout without full-flow replay through dismissal and restart, as checked in the earlier report.
| Failure and dismissal | Cold startup |
|---|---|
j1b-fault-2x.2x.mp4 |
j1b-restart-2x.2x.mp4 |
Earlier J1b recordings.
Adopted identity unlink after a lost acknowledgement ✅ Passed at 8a96806; retained for unchanged paths.
R2: one unlink timeout followed by explicit Disconnect and persisted onboarding. The earlier run used a same-signature Ring-compatible credential fixture; Ring's own UI was not exercised.
| Unlink timeout and Disconnect | Persisted onboarding |
|---|---|
r2-unlink-2x.2x.mp4 |
r2-onboarding-2x.2x.mp4 |
Earlier R2 recordings.
Interrupted deletion and explicit Retry ✅ Passed at 8a96806; retained for unchanged paths.
R3: outage failures stayed explicit; one Retry after the route returned completed deletion. The earlier report checked attempt and request counts.
| Outage and dismissal | Explicit Retry after reconnect |
|---|---|
r3-outage-2x.2x.mp4 |
r3-retry-2x.2x.mp4 |
Earlier R3 recordings.
Read cached balances and receiving details during node startup ✅ Passed on parent 89f3bc6; retained for unchanged hydration and reservation paths.
B1 / parent manual case 6: the parent report independently held native startup and cache loading. After cache release, Home showed 70,000 sats and Spending 20,000, and the cached Savings QR worked before startup completed. Amounts were display fixtures. Readbacks confirmed address, invoice and BIP21; fresh native zero balances replaced them after startup release. Auto/Spending receive retained its refresh/node gates; no usable pre-start Lightning invoice UI is established.
startup-cache-ui-2x.2x.mp4
Earlier cache/startup recording; its opening error toast comes from the Debug launch URI used by that harness.
Check: Exact head build and focused regressions ✅ The pristine APK builds and affected complete unit classes pass.
assembleDevDebug and focused routing, Spark, proof, request/allowance, health, balance, profile, SDK-lock and backup suites passed. Finalized JUnit XML has no failures, errors or skips. Full local unit, lint and Compose instrumentation gates were not repeated.
Check: Mixed-price payment request dispatched through Spark ❌ The additional regression reproduces the retained-invoice finding.
The test uses the real ViewModel/request-pricing flow with mocked native/network boundaries: open the USD request with a 5,000-sat invoice, bind its 2,500-sat on-chain Spark leg, continue and confirm. payServiceLeg is never invoked on this head. Clearing decodedInvoice in the Service branch makes the same test pass. That diagnostic change was restored before both device builds. This is component evidence, not a device execution of the mixed-price request.
No planned case remains unrun. Withdrawal crash/timing, proof and allowance races rely on source inspection and focused units. Complete-WALLET recovery, bounded native Reset and original-owner removal remain separate work in the linked parent/removal reviews.
Scope: Contiguous delta 46d5901..f270a9e, all 54 incoming files, the six-file deletion patch's integration and its open Disconnect case, with an independent source reviewer. The inherited files match parent b2c3ea6e. Device cases cover App Status, an on-chain Spark contact payment and revoked-session deletion; broader parent restore, hardware and allowance journeys were not freshly driven.
Twin: bitkit-ios#1122 fix: stop automatic profile deletion replay, source checked at a214b06, matches single-attempt deletion without error-triggered full-flow replay. Original-session guards and profile storage targets differ; the uncertain-response journey matches. No iOS device run was performed.
Reviewed by gpt-6.1-sol-high
November 12 · validated finished-exit parent composition
Published
1bc89c17on parentd28a1578(treedd1ad1ac). The profile-deletion recovery changes remain intact, including refusal to renew/replay a deletion after an uncertain identity response. The inherited finished-exit balance regression passes in this child.All five local gates passed: 5,980 unit passes plus two explicit opt-in regtest skips; six static reports have zero findings. All 24 live-base commits verify locally and on GitHub. Original hosted CI, Lint and E2E passed on matching synthetic
a14bb466: 5,980 unit passes plus two skips, 257 Compose passes and 39 first-attempt E2E passes. Settings08 and six migration jobs were skipped. External suite:ce9c0e2b; no workflow/job rerun.Complete diagnostics retain 37 stale-element helper notices and a caught optional-alert lookup. No fatal signal marker was detected in this child's diagnostic set. The observed fee disagreement/force-close/commitment broadcast, restore-wait guards, metadata refusals/later acknowledgements and stock wrong-PIN wipe remain qualified. These results do not prove cooperative-close funds confirmation, authenticated profile cleanup, complete WALLET or native Reset Wallet recovery. Hosted unit XML/Gradle Problems HTML were unavailable; local reports are separate evidence. All earlier failures and authored QA remain below.
November 12 · validated parent balance and startup corrections
Head
46d59014composes parent89f3bc6bwithout changing the profile-deletion controls. Cached balances and receiving details can load during the serialized native startup read; Spark balance updates and payment-result screens retain the parent corrections. All 21 live-base commits verify locally and on GitHub.The actual composed tree passed all five final local commands: formatting, compilation, 5,441 unit tests, lint and APK assembly, with no failures, skips or case retries and zero findings in six native static reports. The first original CI, Lint and E2E runs passed on synthetic
49a2f4da, whose tree exactly matches the locally validated composition. Hosted results: 5,441 units, 256 Compose tests, six zero-finding static reports and 39 enabled E2E cases passing on their first internal attempt. Settings08 stayed disabled and all six migration jobs were skipped; those are not passes. Both the executed suite and selected reusable workflow were0ca01e0a.Complete diagnostics retain three stock restore-wait guards, secondary-account sync deadlines and the incremental NestedSegwit timeout. Later generic completion and app retry do not prove each account synchronized. The transfer UI passed despite the peer-fee disagreement, force-close and commitment broadcast, also observed on the actual parent; no cooperative close or confirmed returned funds is claimed. Four guarded METADATA refusals received acknowledgements. The Stopping-state payment read during Electrum restart, optional backup categories, no-session stock cleanup warnings and unavailable hosted unit XML/Gradle Problems HTML remain qualified.
The earlier Send failure and built-in retry remain attributed to their original revision. Current stock suites do not establish authenticated profile deletion or complete WALLET/native Reset acceptance. The source-confirmed explicit deletion-retry limitation is still open in the child review; these parent corrections do not restore broad session renewal or claim that limitation fixed. Existing QA case IDs and authored notes below remain unchanged.
Current composed result — November 12, 2026
Signed head
8d10218f6074517d7fae5019796a6e3f48a3eb1eincludes parent #1686 at2dc86ae0b6e6c85570c8bf6becd6f65ff06d27f1. The six-file profile deletion patch is preserved. All 19 commits against that base verify locally and on GitHub. Required local checks passed on tree174158c9266740180afbd7d86ec662500772cecf, including 5,430 unit tests and zero introduced lint findings.Original hosted CI, Lint and E2E completed on synthetic
9524e42a73f210525773c8ef6bc78e4032739a86, whose tree matches the local candidate: 5,430 unit tests, 256 Compose tests and 39 distinct E2E cases passed. Send 2 first failed while the Move To Spark sheet covered the wallet, then passed the workflow's built-in second attempt. That original failure remains recorded. One Settings case was intentionally skipped, two already-passed Send cases were filtered from the second attempt, and six migration jobs were skipped. No workflow rerun was requested.Complete diagnostics retain the transfer fee disagreement and force-close, four NestedSegwit sync timeouts without an explicit later completion for that address type, and a restore that completed with WALLET absent after a startup wait timeout. Five METADATA guard refusals have later acknowledgements in the same process. These results do not establish cooperative channel closure, confirmed returned funds, authenticated profile deletion, full WALLET recovery or bounded native Reset.
The current-head external review reproduces the existing revoked-session Disconnect failure and reports no new delta finding. Its earlier J1/J1b and adopted-identity evidence remains scoped to
8a96806c; the separate original-owner repair remains in #1693. Existing QA notes and acceptance limits below are retained. João, Piotr and Ovi already have pending requests.Earlier composed result — November 11, 2026
Signed additive head
8a96806c29e05abe004c1c94ea163202040f66acincorporates #1686cd1d1290784d62ef274110529b6242d907a6863e, including the reproduced deferred-refresh identity correction. The destructive profile path still returns an identity error or uncertain result without automatic sign-in or replay. All 18 live-base commits verify locally and on GitHub; the PR remains ready.Required format, compile, full tests, lint and APK build passed on tree
90c3c46edd4f983bec56e5e97bdedbf276a03b4c: 5,398 units, no failures/skips and zero static findings. The two affected-delta reviews cover the exact composed inputs. First hosted CI, Lint and E2E passed on synthetic1ff1aed679961dcac28fab982f94c3c4b760af86, whose tree equals the local tree: 5,398 units, 256 instrumented and 39 E2E cases. All original attempts were 1; one Settings case and six migration jobs were skipped. No CI rerun.Complete diagnostics retain 47 helper notices, compiler/CloseGuard warnings, and the transfer fee disagreement that forced a channel close and commitment broadcast. Passing UI/removal assertions do not prove cooperative closure or confirmed returned funds. The same signature was observed on the actual parent-base hosted tree. A cancelled tag-list read has unknown initiating cause; no introduced defect or failed tag assertion was established. Three metadata guards have later acknowledgements in the same process. These generic checks do not establish authenticated deletion, uncertain-response recovery, reset or complete-wallet acceptance. The sole audit inspected 16 complete logs and 10 diagnostic archives, with 432 sealed payloads; report SHA-256
88fe802581db73bbcb5321eb299bb396b71ffde39e3574cc5d2e1b48d8082e95.Ovi's later J1/J1b and credential-fixture review executed the previous
dc9a9813product. Its known revoked-session Disconnect finding and setup ANRs retain their original scope; those results are not new8a96806capp acceptance. Existing SDK/private-fixture and wallet limits below remain unchanged. João, Piotr and Ovi already have pending review requests.Previous composed result — November 11, 2026
Signed additive head
dc9a981381082a2c14aad29a27e3b1874f7ab030incorporates #1686cd0befc67f00d2f3d956f369663cd117d0aa11af. The composed profile path preserves ordinary session renewal for saves while an identity error or uncertain destructive result returns to the caller without automatic sign-in or deletion replay. Conditional revision-conflict handling stays bound to the original identity.On tree
6604f1f77ae2a30e87b0d164763b1b309aa15d76, requiredjust format,just compile,just test,just lintandassembleDevDebugpassed: 5,395 unit tests, zero failures/skips, and zero static findings. Two scoped independent reviews cover the exact composed profile blobs. All 17 commits against the live base verify locally and on GitHub; the PR remains ready.The first hosted CI, Lint and E2E passed on synthetic
da3e86ad7307736ee159d74793193a77b1c1ed89, with the same tree and Paykit rc7: 5,395 JVM, 256 Compose and 39 E2E passes. All seven shards used their first internal attempt on suite53e8b45bfbd06b5458b86375286c1406ab55bcc5. One Settings case was disabled; six migration jobs were skipped.Complete diagnostics retain 47 stale-element/helper recovery lines, the caught optional permission-helper timeout, four metadata exclusion guards followed by acknowledgements, unchanged cursor/compiler warnings and external suite dependency advisories. No global clean-runtime claim. The audit inspected 16 logs and 10 single-acquired diagnostic archives; its sealed report SHA-256 is
50c22c0017108d7729a0d63529cc87edacd245c619eac443100a8cc46084510e.These are composed component/hosted results. Historical J1, original-grant, SDK and wallet-recovery limits below remain at their recorded revisions; no fresh authenticated deletion journey is claimed.
Admitted-backup parent correction — November 4
Signed additive merge
a8c3fd3fc687df940d9e40037e0c52e244376325incorporates dependency #1686 ata2c58f2427635719b63263b9441b8d22a7b6c858. The inherited correction counts a replacement backup as Running only after original-wallet admission; retirement before dispatch cannot strand the prior job's status. Both incoming source/test files match the reviewed parent. All six profile-deletion child paths and every other input are unchanged, with no conflicts.Final
just format,just compile, fulljust testandjust lintpassed on 1,799 identical inputs: 4,888 unit tests across 264 suites, zero failures/errors/skips, and eight fresh static reports with zero findings. Complete logs, XML/HTML case ledgers, SARIF and nested Gradle diagnostics were inspected. Existing cursor-close/JVM observations and historical Detekt deprecation reports match actual-parent evidence. The independent composition check reuses the exact parent concurrency reviews; all 2,266 sealed evidence files match. All 16 live-parent commits verify locally and on GitHub; published history anddraft=falseare preserved.Current-head CI attempt 1 has a preserved pre-test Compose failure: job
126689415113could not establish adb readiness after the emulator self-reported boot, then reached the 45-minute timeout. Zero instrumentation cases ran; no Compose result artifact exists. Build and 4,888 units passed, six lint reports contain zero findings, and E2E passed 39 cases with one explicit Settings skip; two successful in-case address tap retries remain recorded. Six release-gated migration jobs were skipped. Complete original diagnostics and all 1,498 sealed evidence files are inspected and verified; source checkout matches the exact published tree. The single explicitly authorized Compose retry is now complete: 256 tests across 60 classes passed, zero failures/errors/skips, and the dependent CI gate passed. All XML/HTML/instrumentation/protobuf case identities and 256 logcats (29,538 lines) were inspected; all 386 additional sealed files verify. The 101 app diagnostic stack rows match sealed parent evidence; native-store initialization, Paykit bootstrap and post-test cleanup warnings remain documented. Passing jobs were carried forward unchanged, not rerun. The original failure remains preserved; zero retry authority remains. This result does not establish authenticated cleanup or complete-WALLET recovery. Evidence:work/validation/hosted-android1688-a8c3-20261104/inspection/report.mdand its separateattempt-2/inspection/report.md.The previous
5cf930faLightning-security shard was cancelled by the newer same-PR run after stalling in AVD snapshot setup, before APK download or tests. Its other six E2E shards passed 37 cases with one skip; its aggregate failed. That historical incomplete outcome and all 1,301 verified evidence files remain preserved without an old-head retry. This necessary source update is not a claim that the infrastructure failure was repaired. Parent J3 is not a new child-device run. Existing deletion/uncertain-response acceptance, the known Disconnect limitation in #1687/#1693, and terminal complete-WALLET work retain their separate scope and evidence below.Evidence:
work/validation/android1688-admitted-parent-final-audit-20261104/report.mdandwork/validation/android1688-admitted-parent-20261104/publication.json.Additional external review: 5773777978 reports an authenticated, nonempty-contact lost-acknowledgement case at exact head
a8c3fd3f, including Back and cold restart without automatic deletion replay. It also repeats the revoked-session Disconnect failure tracked in #1687, with no new delta finding, and identifies descendant #1693 as the original-owner removal work. This parent intentionally removes automatic replay; its broader Disconnect failure remains documented. Debug controls, raw request/DB artifacts and installation binding were not independently inspected by the author. No new author runtime pass or dismissal of the changes request is implied.Queued-status parent composition — November 4
Signed additive merge
5cf930fa7composes current parent #1686 atb93444c35968be6d827a00a20132f6a14a947ffd. Scheduled backups retain Running through their wait and clear that status inside the original wallet admission. All five incoming files match the reviewed parent; this PR's six child-specific files are unchanged, with no overlapping edits. The genuine stack is preserved.Final
just format,just compile, fulljust testandjust lintpassed on 1,799 identical inputs: 4,887 tests /264 suites, zero failures/errors/skips, and eight fresh static reports with zero findings. Complete logs, XML/HTML case identities, SARIF and embedded Gradle diagnostics were inspected. Four verified existing cursor-close chains, three JVM notices and the base Detekt deprecation remain recorded. All 15 commits against the live parent verify locally and on GitHub as true/valid. New-head hosted CI is pending complete inspection. Evidence:work/validation/android1688-status-parent-final-audit-20261104/report.md.Parent #1686's exact-APK J3 Settings Running-to-acknowledgement result is attributed to that parent; it is not a new child-device execution. Existing profile deletion/uncertain-response and authenticated acceptance evidence and limitations remain revision-scoped below. The parent's separate brief Transaction Log pre-scheduling presentation gap and terminal complete-WALLET work remain distinct.
VSS parent composition — November 4
Signed additive merge
916c6d5efincorporates live #1686 at4056d2c0036e5a4e51698e26b3d1615825dd77f5. All six profile-delete child files are unchanged. The inherited fix keeps VSS setup, backup operations, queued retries and completion callbacks with their original wallet, and drains them before credential replacement. Profile deletion retains its existing single attempt and explicit retry after an uncertain response.Final
just format,just compile, fulljust testandjust lintpassed on the same 1,798 inputs: 4,885 unit cases /264 suites, zero failures/errors/skips, with fresh Detekt and Android Lint reports containing zero findings. Complete logs, XML/HTML/SARIF and embedded Gradle diagnostics were inspected; all 2,216 producer files were hash-verified. The existing JVM/cursor warnings match the parent source and prior diagnostic provenance. All retained parent cases executed; the difference from the parent's count is the verified profile-delete test delta. The affected composition review is clean and reuses the exact inherited source reviews. All 14 live-parent commits verify locally and on GitHub;draft=false.New-head hosted CI, Lint and E2E passed on their first attempt. Complete diagnostics are audited: 4,885 unit passes, 256 Compose passes without skips, zero static findings, and 39 E2E passes with one existing Settings skip. All app checkouts resolve to the exact published tree; the external E2E suite is
80e26d4e. Six migration jobs were skipped. All 302 sealed evidence files match; the post-test ADB shutdown message occurred after successful instrumentation. No CI rerun was needed for this head. No new authenticated profile-delete journey, literal wallet wipe, running-LDK quiescence or terminal complete-WALLET acceptance is claimed. Prior failures and runtime evidence below remain revision-specific. Evidence:work/validation/android1688-vss-parent-diagnostics-20261104/report.mdandwork/validation/android1688-vss-parent-20261104/github-signatures.json.Authorized Compose retry completed — November 3
The additional explicitly authorized retry completed on unchanged head
3d8a328d6ea8e438ddd4059cca7e1de57d3a28b0: attempt 3, job 126042956081 passed all 256 Compose cases, zero failures/skips. The dependent aggregate passed. Complete logs, XML/HTML/protobuf results and 260 nested diagnostic streams were inspected; all 418 evidence payloads and archive CRCs/digests match. The executed synthetic tree equals the published tree against live parent43e0f3fe.The two earlier emulator-bootstrap failures remain recorded; this successful attempt does not prove their cause or a source fix. Post-test teardown and component native-initialization diagnostics remain documented. Unchanged sealed results remain 4,871 unit passes, six static reports with zero findings, and 39 E2E passes with one explicit disabled Settings skip; they were not rerun or credited as new. Migration skips and broader authored runtime gaps remain separate. The Compose infrastructure blocker is cleared. The known #1687 Disconnect limitation is unchanged here, with its separate fix in child #1693; this retry does not resolve that finding.
Evidence:
work/validation/android1688-additional-authorized-retry-20261103/diagnostic-audit/STRUCTURED-REPORT.json.Current parent composition — November 3
Signed additive merge
3d8a328d6ea8e438ddd4059cca7e1de57d3a28b0incorporates dependency #1686 at43e0f3fe04705d397a81f493cf8e57239a0be73e, including current master44bf50ec. The profile-deletion patch is byte-for-byte unchanged relative to its new parent; its single-attempt and explicit recovery behavior is preserved. Published history is preserved.Final
just format,just compile,just testandjust lintpassed on this actual composed tree: 4,871 unit tests across 263 suites, zero failures/errors/skips, and zero Detekt/Android Lint findings. All four phases share the same 1,796 frozen inputs. Complete logs, XML/HTML/SARIF, fresh structured static reports andignoreFailuresoutputs were inspected. Four cursor-close diagnostics remain recorded and trace to emitter sources identical to the actual parent; this is not a claim of runtime harmlessness. The independent affected-composition review is clean. All 13 live-parent range commits are locally and GitHub signature-verified.Current-head hosted unit/static/E2E results are audited: 4,871 unit passes, zero static findings and 39 unique E2E passes, plus one disabled Settings skip. Required hosted Compose remains blocked. The original attempt failed during emulator startup; the single human-authorized retry (job 125932512103) restored its API30 AVD, reported boot complete, then failed
adb input keyevent 82withNo service published for: inputbefore the repository UI script, APK installation or instrumentation. It subsequently reached the 45-minute job limit; the dependent aggregate failed and no UI report was uploaded. This is zero Compose execution credit, not a pass or skipped test. The two infrastructure failure signatures are retained without claiming a shared cause or an application defect. A healthy hosted Android input service is needed before this lane can execute; no further retry, cache deletion or timeout change was made. Retry supplement:work/validation/android1688-1689-authorized-infra-retries-20261103/diagnostic-audit/STRUCTURED-REPORT.json(133 new sealed payloads); original full audit and failures remain preserved. Earlier hosted and authored/external runtime results below retain their original revision, artifact and fixture limits; this composition adds no new device or authenticated-journey claim. Evidence:work/validation/android1688-1689-master44bf-final-audit-20261103/STRUCTURED-REPORT.jsonandwork/validation/android1688-master44bf-composition-20261103.Final parent ancestry — November 1
Signed additive merge
2efaff0431dcf12b6d0850ee49c9cca5605b8d4eincludes #1686 atb6751e22956653c4f2a80112143880b47298e30d. The resulting tree remains exactly4bedb60795aefe4a9a510261582f88432f57c9c8: all 1,780 file inputs and the directory symlink are unchanged. The independent composition review verifies that the original parent diagnostic test was already present here and the complete owned patch is preserved.The existing final required compile, 4,823 tests, lint and format evidence therefore applies without a redundant rerun. The prior-head hosted CI also passed 4,823 tests, including all three manifest cases, on this exact tree; two complete logs and all HTML/digest/CRC records were inspected. That success produced no failure-time component state and does not explain or waive #1686's earlier routing failure. All 12 live-parent range commits verify locally and on GitHub. New-head hosted jobs remain separately subject to inspection. Evidence:
work/validation/android1688-parent-b675-20261101/review-a/review.jsonandhosted-android1688-9c145-ci-diagnostic-20261101-a/REPORT.md.Current parent composition — November 1, 23:16 UTC
Signed additive merge
9c145d988c1628f75d93afe75bb735605806b886incorporates #1686 at181d664c38a40df156cad4388168b5ab2df132c4(tree4bedb60795aefe4a9a510261582f88432f57c9c8). The inherited allowance fix resumes a newly admitted activation after an off/on toggle during contact loading; it keeps old offers revoked. This PR's complete owned patch is byte-identical, with one clean affected composition review and matching independent parent concurrency reviews.Final
just format,just compile,just testandjust lintpass: 4,823 tests /260 suites, zero failures/errors/skips and zero findings in all six fresh static reports. All 1,780 frozen file inputs match. Complete logs and structured diagnostics were inspected, including ignoreFailures outputs; four CloseGuard observations and JVM notices remain disclosed. Stale copied Gradle reports are excluded from new-result credit. All 11 live-base commits verify locally and on GitHub (true/valid).New-head hosted checks await complete inspection. Earlier authenticated acceptance remains tied to its recorded inputs; this composition adds no authenticated journey pass or causal repair of historical failures. Evidence:
work/validation/android1688-parent181d-20261101/final-diagnostic-audit/and itspublication/receipt.Previous-head hosted validation — November 1
At
2621f81afc9a671f49a79221b013666732b9b6a4, all seven E2E shards and the status job have completed: 39 distinct eventual passes, with one disabled settings case.settings_10failed its first execution and passed its automatic retry; the failed media and unresolved cause remain preserved. Retry skips are not additional passes. The earlier audited 4,822 unit tests and six zero-finding static reports remain tied to this exact head and parent1b1426fb.The sole audit combined prior immutable evidence with only the missing complete shard logs and checksum/CRC-verified report archives. Runtime diagnostics, npm advisories and skipped migration jobs remain explicit. No CI rerun, causal repair of the historical manifest failure, or new authenticated J1 pass is claimed. Evidence:
work/validation/hosted-android1688-2621-20261101-tail-b/REPORT.md.Previous parent composition — November 1, 21:48 UTC
Signed additive merge
2621f81afc9a671f49a79221b013666732b9b6a4incorporates live #1686 at1b1426fbebeccd616c29311fa5bf652089727a84; final treed950dda6c3985212ab08124f23e58eb43a616fa3. The owned child patch is unchanged. The incoming queued allowance-admission and pending-profile-setup corrections have one clean affected composition review, reusing the matching independent parent concurrency reviews.Required
just format,just compile,just testandjust lintpassed 4,822 tests /260 suites, zero failures/errors/skips, and zero findings across all six Detekt/Android Lint reports. All 1,781 tracked inputs match the composed tree. Complete logs and structured reports were inspected; four CloseGuard allocation observations and JVM notices remain recorded, and stale Gradle report copies are excluded. All 10 live-base commits verify locally and on GitHub (true/valid).Current-head hosted CI awaits complete inspection. No new authenticated app journey or causal repair of prior E2E failures is claimed. The terminal complete-WALLET limitation and separately scoped profile-removal gaps remain unchanged. Earlier acceptance and failures below retain their exact tested revisions. Evidence:
work/validation/android1688-parent1b14-review-20261101/andwork/validation/android1688-parent1b14-final-a/.Earlier parent composition — November 1
Signed additive merge
3f4b19f7f58b8bff053f7e265ce50f0079ece197incorporates live #1686/78269d8a1cae1ac127be93a96fba16f7d68d1019; treecf23f0f40a60327e9e1fc699560c1b4ddab09731. It includes queued-offer admission and preserves that admission across a same-active-identity contact refresh. The owned recovery patch remains unchanged, with one clean affected composition review and the independent parent concurrency reviews retained.Required final formatting, compile, full tests and lint passed 4,815 tests /260 suites, zero failures/errors/skips. All 1,780 frozen inputs match; complete logs and fresh Detekt/Android Lint reports contain zero findings. Three JVM notices and four verified unchanged-provider cursor warnings remain recorded; stale copied reports are excluded. All nine live-base commits verify locally and on GitHub. New-head hosted CI is pending inspection; this composition does not add an authenticated app-journey pass.
The earlier
e3384edahosted E2E failure remains unwaived: Docker port39388 prevented two cases from executing, and multi-address initially missed SendSuccess before its internal retry passed. The earlier alias assertion, unknown-outcome Disconnect and complete-WALLET limitations are not causally repaired by this parent update. Revision-specific J1 evidence and prior failures remain below. Evidence:work/validation/android1688-parent7826-final-a/inspection/andwork/validation/android1688-parent7826-20261101/publication/.Historical composition and executed J1 — 31 October 2026
Signed additive merge
e3384eda382f7ca98bf5518bc5f964186c8dfceeincorporates actual parent #1686 at71fa609c8f34fa903a60bb38f575eb029078489f. The seven-file owned patch is byte-identical; the final tree isfe1d968611b5057d1afccf20bd0f62a3268c4a8f. Required format, compile, full unit tests and lint passed 4,812 tests / 260 suites, zero failures/errors/skips, with fresh Detekt and Android Lint reports containing zero findings. All 1,780 frozen source inputs match. Complete logs and structured reports were audited; three JVM notices, four unchanged-provider cursor warnings and a stale prior Gradle deprecation report remain attributed and retained. One fresh combined source/integration review is clean. All seven live-base commits are locally and GitHub verified (true/valid). Current-head CI passed 4,812 unique unit tests and lint has zero findings. E2E failed: lightning/security could not bind Docker port39388, so its two cases never ran. Separately, multi-address attempt1 hit a native Electrum incremental-sync deadline, WalletOperationTimeout and SyncUnhealthyError before the 30-second SendSuccess assertion failed; attempt2 passed. The underlying timeout cause and introduction versus base remain unproven. There were37 unique eventual passes, one genuinely disabled settings case and two retry-only skips of already-passed cases. Complete14 job logs, annotation pages, eight verified archives and295,867 nested-log lines were audited; the owner verified465 sealed evidence files. The synthetic tree exactly equals this head and external suite is611509ee. Six migration jobs were skipped. No CI retry, blanket E2E pass or causal repair is claimed. Evidence:work/validation/hosted-android1688-e338-20261101/REPORT.md; private fixture logs remain local.All six saved J1 actions passed in an author-executed, finite empty-contact native-timeout case. A fresh unfunded API 36 wallet/profile used the real app editor, native published Paykit RC2 and an isolated localhost Pubky/Postgres fixture. The server committed one authenticated profile DELETE with HTTP 204 at 23:01:36.480 UTC. Its acknowledgement was held for 35 seconds; the app reported the genuine native
request timeoutat 23:02:06.328, and fresh failure UI was saved 2.428 seconds before the fixture eventually emitted 503. This distinguishes a real client timeout from the earlier immediate-503 variant.The failure showed Retry/Disconnect and retained the editor on Back. Through the failure wait, Back dismissal and 61.351 seconds after one cold-start completion, the recorded result remained one contact-cleanup completion, one forwarded profile DELETE and no automatic deletion replay. The original grant fingerprint stayed unchanged; native stored-session restoration refreshed its bearer only after restart, with no app fallback sign-in marker. The profile remained absent remotely. No Retry or Disconnect was selected, and neither this operation nor the earlier fixture operation was reset or replayed. The consumed UUID remains retained and the fixture is back in accept mode.
The independent acceptance audit consumed all 6,130 app-log lines, both server logs, unique UI hierarchies, cold-start receipts and database/journal snapshots. Installed APK SHA-256:
1a97379b1a778788678f8c734d47011ddfb6ff89aeefad3b2978bd370f568140; audit manifest:27be210a9729cf1acb57d73e7e28f42d7bcb53e9756e960265e9fc317716186b(151 files independently checked by the owner). Device logcat UTC+02 and embedded app/host UTC are explicitly reconciled. The logs include retained DHT, Ring-provider, sync, graphics and missing-profile observations; this is not a clean-log claim. Source control flow and snapshots support no success navigation/toast; no continuous toast video is claimed. Early actions used fresh ADB hierarchies; final layouts used the repository Android CLI, with mixed tool provenance retained.This result covers the saved journey’s bounded empty-contact variant. It does not certify indefinite blackholes, nonempty contact cleanup/CAS/ABA, Ring, manual Retry/Disconnect or complete-wallet restoration. The revoked-session Disconnect finding remains assigned to #1693/#1687 and is not converted into a pass here. The historical alias-routing failure and prior E2E first-attempt failures below remain unexplained; no assertion was weakened and no unchanged CI retry was requested. The isolated QA fixture requires no product homeserver dependency or deployment.
Prior diagnostic follow-up —
3001faf3, October 30, 22:35 UTCSigned
3001faf36a6e74e04e8cc1aac80c4bc8a7f4e714adds failure-time phase, alias override/activity and resolved-target details toPubkyAuthManifestTest.kt. The original routing assertions, flags and state transitions remain unchanged; diagnostics preserve the original assertion even if a diagnostic getter fails. The earlier CI failure below remains unexplained and unwaived. No unchanged workflow retry was requested.Required local format, compile, full test and lint commands passed: 4,805 tests / 260 suites, zero failures/errors/skips. Complete logs and XML/HTML/SARIF were audited; fresh Detekt is clean, changed-test Android Lint analysis ran and the final report was accepted as up-to-date with zero findings. Production compilation was also up-to-date. All 1,776 frozen regular inputs match; prior cursor/JVM/Gradle warnings remain attributed to unchanged source/configuration. One independent affected source review is clean. All six live-parent range commits are locally verified and GitHub
verified=true/reason=valid. That head’s CI passed 4,805 units, and lint has zero findings. E2E reached 39 unique passes, with one explicit settings skip, after two retained first-attempt failures: missing Sent activity and missing PriceWidget during restoration. Each failed case passed its second in-job attempt; neither cause is established. Complete available logs, all uploaded unit/lint reports and failure media were inspected; nine report archives match their digests and ZIP CRCs. Hosted JUnit XML and Gradle problems HTML were not uploaded. Synthetic34a84319equals this exact head tree against78a0ff7a; migration skipped. This diagnostic-only pass does not establish a causal repair of the earlier alias failure or authenticated J1 acceptance.External review 5718677066 on the prior
ff5aa32fhead repeats the revoked-session Disconnect failure; its full report was read, but raw reviewer artifacts were not independently audited. The limitation remains assigned to #1693/#1687, with no feedback dismissed or journey marked passed.Prior hosted failure —
ff5aa32fAt
ff5aa32f4da1ec33fb8be63e0b84597d463c82b1, unit CI failed one of 4,805 tests:PubkyAuthManifestTestresolved the signup URL during its initial all-disabled phase, before component overrides were applied. The full failure report also records queued Robolectric main-loop work; the cause remains unproved. The test, manifests, dependency pins and CI workflow match the actual parent byte-for-byte. That comparison does not establish an identical parent runtime or make the failure harmless. No unchanged CI retry was requested.Hosted lint passed with zero findings. E2E passed 39 cases on seven first-attempt shards, with one explicit settings skip, using suite
611509ee2d6dc44094276b567ac51949d63760f9. The synthetic checkout96131945exactly matches this head's tree. Complete job logs, all structured unit/lint reports and 303,296 lines across 32 runtime files were inspected; ten published artifact digests and ZIP CRCs are verified. Emulator, sessionless endpoint-cleanup, sync/fee and metadata-guard observations remain retained; no full authenticated deletion-fault or terminal-WALLET acceptance is inferred. Six migration jobs were skipped.The diagnostic follow-up was initially held by ENOSPC. That failure and its preserved source remain recorded; the completed publication and new validation are described above.
Earlier combined validation —
ff5aa32f, 30 October 2026Signed additive merge
ff5aa32f4da1ec33fb8be63e0b84597d463c82b1incorporates parent #1686 at78a0ff7aa48b30a30a4cf70e64b794ecef5c1f5b, preserving the six-file deletion-replay change and the genuine stack. The final combined treeaca4468138efdc01fcd15b5059fc8c53cdf31439passed required format, compile, 4,805 unit tests in 260 suites (zero failures/errors/skips), and full Detekt/Android Lint with zero findings. Complete logs and fresh XML/HTML/SARIF were inspected; 44 historical Gradle report copies were attributed to their exact earlier producers. Three JVM notices and four unchanged-provider CloseGuard stacks remain disclosed against both actual parents.The affected independent source review is clean. All five live-base commits verify locally and on GitHub (
verified=true,reason=valid). This PR remains ready; the hosted failure for that earlier head is recorded above. Earlier runtime evidence below remains tied to its tested revision; the uncertain-response journey and descendant #1693 recovery gaps are not converted into current-head passes.Earlier device-review disposition — 30 October 2026
Review 5707701492 exercised the earlier exact head
2aac53edwith the committed Paykit RC2 dependency and an unfunded disposable identity. The reviewer reports one accepted profile DELETE with its response withheld, honest timeout feedback, and no automatic deletion replay through dismissal and cold restart. This is attributed external runtime evidence; the author has not independently inspected the raw proxy, operation-entry and finalized test artifacts, so J1 is not marked independently verified.The same run failed revoked-session Disconnect: endpoint cleanup returned HTTP 401 and the editor remained open. A successful deletion after cold-session recovery does not turn that failed assertion into a pass. The path is unchanged in this narrow replay-removal PR; original-owner/local-only disconnect is tracked in #1687 and the descendant #1693. The review remains outstanding; no request is dismissed, no failure is waived, and the descendant's behavior is not claimed to be present in this head. Earlier statements below that the journey was unexecuted describe the author's earlier evidence, not this newly reported external run.
At the earlier head
2aac53edc1faf2b4fd509b7e8a36701ce4289231, hosted CI passed 4,707 unit tests, zero failures/skips, and lint reported zero findings. E2E reached 39 unique passes and one true settings skip after two failed onboarding steps: emulator SDK installation failed before tests; the next attempt had a 30-second Slide1 timeout; the final attempt passed that case and selection-skipped six already-passed cases. Both failure annotations are retained despite the final green workflow. Failure media were not uploaded after eventual success; causation is unproven. Complete 14 logs, reports and four checksum/CRC-verified artifacts were inspected. Synthetic8116071chas this exact tree against live parent217b42ef. Migration skipped; these stock suites do not establish authenticated J1.Earlier parent composition — 29 October 2026
Signed additive merge
2aac53edc1faf2b4fd509b7e8a36701ce4289231incorporates #1686 at217b42ef889952a6c1f3a30b0c44a8b1001db4a5, preserving the six-file child change and genuine stack. Final combined tree864ea6aa3816ef68efd16a1d0994ce6e5eccad38passed required format, compile, 4,707 unit tests in 256 suites (zero failures/errors/skips), and full Detekt/Android Lint with zero findings. All 1,761 frozen inputs and six symbolic-link targets match. Complete logs, all XML/HTML/SARIF, per-gate Gradle problems and configuration-cache diagnostics were inspected. Three JVM notices, four unchanged-provider CloseGuard stacks and the existing Detekt plugin deprecation remain disclosed against the actual parent; this is not a warning-free claim.One independent affected-composition review is clean; the existing deletion-replay removal and both parent/child deltas remain intact. All four live-base commits verify locally and on GitHub (
verified=true,reason=valid). At the earlier referenced head, hosted checks have been triggered and remain pending inspection. The complete authenticated lost-acknowledgement journey and original-owner/manual-retry gaps retain their scopes below; no component/native pass is counted as that journey.The following earlier evidence is revision-scoped and is not a current-head CI claim.
Earlier parent composition — 28 October 2026
Signed additive merge
5fe0bf6c4f0851035632abd87dd5fb14b43507d0incorporates parent #1686 at89e2702e, preserving the existing stack. The final tree99e455e77passed 4,605 unit tests in 254 suites, zero failures/errors/skips, plus requiredjust format,just compileandjust lint. Complete logs, all XML/HTML/SARIF and embedded Gradle reports were inspected; Detekt and Android Lint have zero findings. All 1,754 frozen file inputs and the directory link match. Three JVM notices, four unchanged-provider CloseGuard stacks, the inherited manifest notice and Detekt deprecation remain disclosed against exact parent source/configuration; no fresh parent runtime baseline or warning-free claim is made.All 157 original feature patch operations are unchanged. The new parent preserves an already restored identity during optional setup; it does not reintroduce automatic deletion replay. One independent affected-composition review is clean, retaining prior matching feature review coverage. The original-owner/manual-retry limitations remain assigned to #1693/#1687; this PR's complete authenticated lost-acknowledgement journey remains unexecuted.
All 3 live-base commits verify locally and on GitHub (
verified=true,reason=valid), with exact sets matching. This PR remains ready. At the earlier referenced head, hosted CI passed 4,605 unit tests in 254 classes with zero failures/skips, and lint has zero Detekt/Android Lint findings. E2E reached 39 distinct passes and one explicit settings skip: send_1 first failed its 30-second InsufficientSavingsToast assertion, then passed automatic attempt 2. Earlier failures and process annotations are retained; their causes remain unproven. This is not an all-first-attempt pass or the authored authenticated fault journey. The complete 14 executed job logs, all unit/lint reports and four SHA-256/CRC-verified archives were inspected; the executed merge tree equals this head and its tested parent matches the live stack. Failure-only media was not uploaded after eventual success. No workflow rerun was requested. Migration was skipped, not passed.Twin: synonymdev/bitkit-ios#1122
Dependency: #1686
Description
Out of Scope
Design
N/A — existing progress, failure options and navigation.
Preview
An uncertain deletion failure remains a failure instead of silently repeating contact cleanup and profile deletion.
QA Notes
Setup
Use committed released dependencies and synthetic identities for unit tests. J1 requires a disposable test identity and a homeserver fault proxy that accepts one profile deletion, drops its acknowledgement and retains a redacted request counter. The author-owned localhost fixture has now executed the finite empty-contact J1 case below with a fresh unfunded identity and the published RC2 dependency. Preserve the consumed operation and use a new disposable identity for any distinct case; do not use a personal profile, reset wallets or use live funds.
Journeys
profile/delete-profile-uncertain-response.xml— all six saved actions passed on treefe1d9686in the finite empty-contact native-timeout case: accepted deletion, lost successful acknowledgement, honest failure, no error-triggered cleanup/deletion/sign-in replay, Back dismissal and cold restart without selecting Retry. The bounded window and excluded cases are stated above.Automated Checks
fe1d9686: required format/compile/full unit/lint gates passed 4,812 tests in 260 suites, zero failures/errors/skips, fresh Detekt and Android Lint zero findings. All 1,780 frozen inputs match; full diagnostics and one fresh affected source review are complete. The new published head’s hosted checks are pending their own complete audit.Earlier revision-specific results:
just compile,just testandjust lintpassed. All 4,623 tests across 254 reports passed with zero failures/errors/skips. Complete Detekt SARIF and Android Lint XML/SARIF contain zero findings; all Gradle problem diagnostics match the unchanged base.Evidence and gaps
Android's pre-fix repository suite executed 223 tests with two expected counterexamples: a lost deletion response causes a second delete and false success, and the old refresh helper can authenticate an identity after sign-out. Removing the helper removes its sole production entry. The repaired 219-test repository suite passes, including one-delete/error preservation and held deletion failure after sign-out. The remaining manual deletion/finalization ownership issue is separate.
The iOS UI and helper call graph has the same automatic replay, and the patch removes that call path while preserving its existing ownership and failure guards. The full native suite is regression coverage; it is not an executed iOS view/proxy lost-acknowledgement journey. Android’s finite native-timeout J1 is now verified as described above; this does not establish the corresponding iOS view/proxy journey. No historical contact-failure attribution is claimed.
Models used
gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:high)