Repository navigation
fix: serialize backup observation lifecycle - #1123
ben-kaufman wants to merge 28 commits into
Conversation
|
Triage: 3.1.0. It fixes shipped backup handling, and 3.1.0 is due 28 Oct. |
talosmachina
left a comment
There was a problem hiding this comment.
Serializes backup observation start/stop on MainActor, bumps a scheduling generation so queued work from a stopped observation is dropped, and makes stop drain cancelled scheduled and running jobs (and both writers) before it returns. While cleanup runs, manual uploads and fresh USDT requests wait for it instead of being skipped.
No findings. Reviewed head f204e05 against its base fix/restore-identity-background (stacked on #1121), full tier.
Merge confidence: 4/5. No open findings. It is held below 5 because Run Tests was still pending when I reviewed, and the lifecycle manual cases 1 to 3 have not been run on a device.
What I checked
- iOS does not build on this reviewer's Linux host, so this review is reasoned from the code and CI. SwiftFormat is green and
Run Testswas pending. - Stop ordering:
observationTaskchaining means a start queued behind a stop runs only after the drain,isStoppingBackupsand the generation bump happen in one.backupqueue block, and the drain awaits job values outside that queue, so cleanup that hops back onto.backup(runScheduledBackupremoving its key,runBackupremoving its task) cannot deadlock against it. - No wait cycle through
waitForObservationTransition. A cancelled scheduled job skips its tailscheduleBackup. A job that removed itself before the stop snapshot callsscheduleBackupwith the old generation and returns at the generation check. ScheduledtriggerBackupduring a stop returns.busy, and its cancelled loop then exits. So no task the drain awaits is itself waiting onobservationTask. - Generation capture:
markBackupRequiredafter the bump (the USDT path while observers are stopped) carries the new generation, waits out the stop and then schedules. Work captured before the bump is dropped, but its status stays required, and the restarted observer'sprepend(getAllBackupStatuses())picks it up again. runningis now set only inside the admittedscheduleBackupblock rather than bymarkBackupRequired, so a dropped schedule no longer leaves a stale running flag.- Callers:
AppScene.handleNodeLifecycleChange(start on.running, stop otherwise) andAppReset.wipeLocalboth call from MainActor and fit the new@MainActorsignatures.BackupViewModelandrewriteMigratedBackupsreach the manualtriggerBackuppath, which now waits for cleanup instead of returning early. - Ruled out: the stop now also awaits in-flight running uploads, so a wipe can wait on a VSS put. The VSS setup is bounded by a 30 s timeout and both writers were already awaited before this change, so the behaviour is intended.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the run metadata available to this reviewer does not report a reasoning setting)
jvsena42
left a comment
There was a problem hiding this comment.
Code scan: no findings so far.
Checked and clean:
- A backup is marked synced only after the put returns success.
stopObservingBackupsdrains every task beforeremovePersistentDomain, so after a wipe and restore the old wallet's pending data can't upload under the new wallet. BackupItemStatusis persisted in the same format as v3.0.0, so nothing to migrate.- Twin synonymdev/bitkit-android#1689 fences by generation instead.
Performance-relevant. The PR removes the 500 ms .debounce from the settings, widgets, activities, metadata, hw-names, appState and blocktank listeners (BackupService.swift:682-774). Each change event now does a UserDefaults JSON decode, encode and write and spawns a MainActor task, so a sync of N activities costs N round-trips instead of one per burst. The gate counts backupStatuses writes during a sync of a couple of hundred payments, head vs master, and times stopObservingBackups during a wallet reset with a put in flight. If the write count grows with N, skipping saveBackupStatuses when the status is unchanged would bring it back down.
Device gate (partial): 1ad665f — not run yet; pending that measurement. Device results will follow in a separate review.
There was a problem hiding this comment.
Verdict: ⛔️ Request changes — manual test 2 fails for a real VSS connection outage; the same failure reproduces on the PR base.
Findings: I found no correctness defect introduced by this diff. The QA failure below is inherited: the released VSS binding returns VssError.UnknownError for the failed connection, but BackupService.isTemporaryBackupFailure excludes that error, so the pending recovery backup receives no automatic retry.
Reviewed commit: 1ad665f.
QA for 1ad665f: iPhone 17 Pro simulator, iOS 26.5, Xcode 27. The component and navigation tests used the unmodified head. Manual lifecycle checks used local holds and logging over the same head, a disposable wallet and staging VSS. No payment was submitted.
Test 1: cleanup, manual admission and restart ✅ Stop and the manual backup waited for cleanup; the latest wallet and settings state reached VSS.
I held final scheduled cleanup, requested stop and a manual metadata backup, queued restart, then edited wallet/settings state. No capture began before release. I also held a settings upload, canceled it and changed settings during restart. The later upload preserved the new requirement. Hook logs and decrypted VSS reads confirmed the final wallet records and setting matched local state.
lifecycle-12x.2x.mp4
Lifecycle controls on the disposable app; the assertions were checked through logs and VSS reads.
held cleanup
released cleanup
stop-done elapsed=1.692
manual-done result=completed
held upload-SETTINGS
upload-settings swipe=0
upload-settings swipe=1
verify-settings matches=true value=Optional(true)
verify-wallet matches=true
Test 2: USDT recovery with observation stopped ❌ A connection failure left recovery pending without the expected automatic retry.
A fresh USDT recovery request waited for cleanup and completed without restarting observation. I then pointed only the custom VSS endpoint at an unreachable loopback port. The real native request failed after about 10.5 seconds with VssError.UnknownError. The job ended with .failed; no retry occurred during the following observation window of over two minutes.
I reproduced the same classifier and stopped-job behavior on base ec20b264. Restoring the endpoint and making a fresh explicit request completed successfully there. The required marker survived the failure. This is an inherited limitation, not a new inline finding, but it fails the transient-retry expectation in manual case 2.
| Head | Base comparison |
|---|---|
lifecycle-22x.2x.mp4 |
transport-base2x.2x.mp4 |
The app remains on Home; native and hook logs establish the request outcomes.
head: usdt-recovery-put
head: UnknownError(errorDetails: "error sending request for url (http://127.0.0.1:1/putObjects)")
head: backup-done USDT_RECOVERY result=failed
base: backup-done USDT_RECOVERY result=failed
base: status USDT_RECOVERY synced=1793040118 required=1793040578 running=false
base: route-ready vss-normal
base: usdt-recovery-ack
base: backup-done USDT_RECOVERY result=completed
Test 3: repeated observation lifecycle ✅ Stopped listeners ignored later wallet edits, and a received settings change survived restart.
I repeated start/stop and a subsequent real wallet-record edit 25 times. The hook log showed unchanged wallet backup status and no wallet capture after each stop. A settings event received before stop remained required and uploaded after restart.
lifecycle-32x.2x.mp4
Repeated lifecycle controls; status comparisons and upload acknowledgement were checked in the hook log.
Test 4: backup component regressions ✅ The focused backup suite passed without skips.
xcodebuild test-without-building -only-testing:BitkitTests/BackupServiceTests passed. The finalized XCResult confirms the cancellation, final-write drain, restart, stopped-listener and bounded-backoff assertions.
backup2x.2x.mp4
App-hosted XCTest execution; the result was checked in XCResult.
Test 5: USDT back navigation ✅ Both affected ordinary navigation cases passed with their existing assertions.
I ran testUnknownBalanceAndManualEntryUseConsistentAmountsInCommaRegion and testReceiveAndInvalidPaymentStaySeparateFromBitcoin, then repeated them for the recordings. Amount entry, recipient retention, receive draft cancellation and back navigation passed. The UI runs reported QoS priority-inversion warnings. The recordings retain all changed frames; capture stutters remain.
| Amount and recipient | Receive and invalid payment |
|---|---|
usdt-entry-smooth2x.2x.mp4 |
usdt-receive-smooth2x.2x.mp4 |
Unmodified-head UI tests, with finalized XCResults checked.
Test 6: activity-notification burst ✅ The head/base measurement completed; status-write amplification is confirmed.
For 200 actual activity-publisher notifications, the head wrote backup status 200 times during the burst; the base coalesced them into one write. Notification dispatch took about 0.206 seconds at the head versus 0.002 seconds at the base. The head's subsequent stop returned in about 0.007 seconds with no held upload.
This measures the notification boundary, not a complete sync of 200 payments or the latency of a stalled network upload. It supplies the write-count evidence requested in the performance review; it does not establish a user-visible sync slowdown.
| Head | Base |
|---|---|
performance-head2x.2x.mp4 |
performance-base2x.2x.mp4 |
App startup and the measurement run; timings and write counts came from the hook log.
All three listed manual cases were driven; case 2 failed. I did not run a funded payment or cross-platform restore. The author's released-native signing/restore evidence in this PR's QA notes remains scoped to its recorded revisions.
Full review: ec20b264..1ad665f, including all changed files and lifecycle callers. The native suites, live cleanup holds and navigation runs exercised the runtime changes; the changelog and journey text were inspected. SwiftFormat lint passed for the changed Swift files.
Twin: synonymdev/bitkit-android#1689 documents the same drain and pending-change preservation intent, with separate Android wipe-admission guards.
Head lifecycle and performance controls — local test hook, not pushed
Holds, logging, synthetic record edits and an endpoint-only outage control; the backup decisions and native transport results are unchanged.
diff --git a/Bitkit/BitkitApp.swift b/Bitkit/BitkitApp.swift
index 961935aa..bfc5d58b 100644
--- a/Bitkit/BitkitApp.swift
+++ b/Bitkit/BitkitApp.swift
@@ -83,6 +83,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
}
+ #if DEBUG
+ ReviewBackupControl.start() // test hook
+ #endif
return true
}
diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift
index 76885696..44d6d41a 100644
--- a/Bitkit/Constants/Env.swift
+++ b/Bitkit/Constants/Env.swift
@@ -372,7 +372,12 @@ enum Env {
}
static var vssServerUrl: String {
- switch network {
+ #if DEBUG // test hook
+ if ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1", FileManager.default.fileExists(atPath: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup/vss-fault").path) { // test hook
+ return "http://127.0.0.1:1" // test hook
+ } // test hook
+ #endif // test hook
+ return switch network {
case .bitcoin: "https://bitkit.to/vss_rs_auth"
default: "https://bitkit.stag0.blocktank.to/vss_rs_auth"
}
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
index 7903e1c8..d414ce84 100644
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -155,7 +155,17 @@ class BackupService {
backupData: ((BackupCategory) async throws -> Data)? = nil,
usdtRecoveryBackup: UsdtRecoveryBackupStore = UsdtRecoveryBackupStore(),
uploadBackup: @escaping (String, Data) async throws -> Void = { key, data in
+ #if DEBUG
+ await ReviewBackupControl.hold("upload-\(key)") // test hook
+ if key == "SETTINGS", let payload = try? SettingsBackupV1.decode(from: data) { // test hook
+ ReviewBackupControl.log("upload-settings swipe=\(payload.settings["enableSwipeToHideBalance"] ?? "missing")") // test hook
+ } // test hook
+ ReviewBackupControl.log("upload-start \(key)") // test hook
+ #endif
_ = try await VssBackupClient.shared.putObject(key: key, data: data)
+ #if DEBUG
+ ReviewBackupControl.log("upload-ack \(key)") // test hook
+ #endif
},
waitForBackupDelay: @escaping () async throws -> Void = {
try await Task.sleep(nanoseconds: 5_000_000_000)
@@ -292,6 +302,9 @@ class BackupService {
}
private func runBackup(category: BackupCategory, isScheduled: Bool) async -> BackupOutcome {
+ #if DEBUG
+ ReviewBackupControl.log("capture-start \(category.rawValue)") // test hook
+ #endif
Logger.debug("Backup starting for: '\(category.rawValue)'", context: "BackupService")
var outcome = BackupOutcome.failed
var requiredAtStart: UInt64 = 0
@@ -349,6 +362,9 @@ class BackupService {
}
}
_ = try? await ServiceQueue.background(.backup) { self.runningBackupTasks.removeValue(forKey: category) }
+ #if DEBUG
+ ReviewBackupControl.log("backup-done \(category.rawValue) result=\(outcome)") // test hook
+ #endif
return outcome
}
@@ -857,6 +873,9 @@ class BackupService {
while !Task.isCancelled {
do {
if outcome == .temporarilyUnavailable {
+ #if DEBUG // test hook
+ ReviewBackupControl.log("retry \(category.rawValue) delay=\(retryDelay)") // test hook
+ #endif // test hook
try await waitForTemporaryFailureDelay(retryDelay)
retryDelay = min(retryDelay * 2, 300_000_000_000)
} else {
@@ -873,6 +892,9 @@ class BackupService {
break
}
}
+ #if DEBUG
+ await ReviewBackupControl.hold("cleanup") // test hook
+ #endif
updateBackupStatus(category: category) { status in
BackupItemStatus(synced: status.synced, required: status.required, running: false)
}
@@ -1017,6 +1039,9 @@ class BackupService {
do {
let data = try JSONEncoder().encode(encoded)
+ #if DEBUG
+ ReviewBackupControl.countWrite() // test hook
+ #endif
defaults.set(data, forKey: backupStatusesKey)
backupStatusesSubject.send(statuses)
diff --git a/Bitkit/Services/UsdtRecoveryBackupStore.swift b/Bitkit/Services/UsdtRecoveryBackupStore.swift
index f804d076..f546805c 100644
--- a/Bitkit/Services/UsdtRecoveryBackupStore.swift
+++ b/Bitkit/Services/UsdtRecoveryBackupStore.swift
@@ -27,7 +27,14 @@ struct UsdtRecoveryBackupStore {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(UsdtRecoveryBackupV1(version: 1, usdtWallet: snapshot, paykitUsdtState: paymentState))
+ #if DEBUG // test hook
+ await ReviewBackupControl.hold("upload-USDT_RECOVERY") // test hook
+ ReviewBackupControl.log("usdt-recovery-put") // test hook
+ #endif // test hook
try await put(Self.key(for: data), data)
+ #if DEBUG // test hook
+ ReviewBackupControl.log("usdt-recovery-ack") // test hook
+ #endif // test hook
}
func snapshots() async throws -> [UsdtRecoveryBackupV1] {
diff --git a/Bitkit/Services/ReviewBackupControl.swift b/Bitkit/Services/ReviewBackupControl.swift
new file mode 100644
index 0000000..aac9b26
--- /dev/null
+++ b/Bitkit/Services/ReviewBackupControl.swift
@@ -0,0 +1,108 @@
+#if DEBUG // test hook
+import Foundation // test hook
+import LDKNode // test hook
+
+enum ReviewBackupControl { // test hook
+ static let enabled = ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1" // test hook
+ static let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup") // test hook
+ static let lock = NSLock() // test hook
+ static var writes = 0 // test hook
+ static func countWrite() { if enabled { lock.withLock { writes += 1 } } } // test hook
+
+ static func log(_ message: String) { // test hook
+ guard enabled else { return } // test hook
+ lock.withLock { // test hook
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) // test hook
+ let file = directory.appendingPathComponent("events.log") // test hook
+ var data = (try? Data(contentsOf: file)) ?? Data() // test hook
+ data.append(Data("\(Date().timeIntervalSince1970) \(message)\n".utf8)) // test hook
+ try? data.write(to: file) // test hook
+ } // test hook
+ } // test hook
+
+ static func hold(_ name: String) async { // test hook
+ guard enabled else { return } // test hook
+ let file = directory.appendingPathComponent("hold-\(name)") // test hook
+ guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+ log("held \(name)") // test hook
+ while FileManager.default.fileExists(atPath: file.path) { // test hook
+ await Task.detached { try? await Task.sleep(nanoseconds: 100_000_000) }.value // test hook
+ } // test hook
+ log("released \(name)") // test hook
+ } // test hook
+
+ @MainActor static func start() { // test hook
+ guard enabled else { return } // test hook
+ log("control-ready head=1ad665f") // test hook
+ Task { // test hook
+ let end = Date().addingTimeInterval(1200) // test hook
+ while Date() < end { // test hook
+ let file = directory.appendingPathComponent("command") // test hook
+ if let command = try? String(contentsOf: file, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) { // test hook
+ try? FileManager.default.removeItem(at: file) // test hook
+ let service = BackupService.shared // test hook
+ log("command \(command)") // test hook
+ switch command { // test hook
+ case "stop": Task { // test hook
+ let before = Date() // test hook
+ await service.stopObservingBackups() // test hook
+ log("stop-done elapsed=\(Date().timeIntervalSince(before))") // test hook
+ } // test hook
+ case "start": service.startObservingBackups() // test hook
+ case "wallet": // test hook
+ do { // test hook
+ var records = try WatchOnlyAccountStore.load() // test hook
+ if records.isEmpty { // test hook
+ records.append(WatchOnlyAccountRecord(id: UUID(), walletIndex: 0, accountIndex: 42, addressType: LDKNode.AddressType.nativeSegwit.stringValue, xpub: "tpubDDWohsp5dx2iMJ9N7iHbgAEDhH4BJB9NWW1fEW3yA3AFNDREmpzteCXNqppMLUmKFY5q5e3PXtS5CuqWCQbYcGhpPqYAgQSYdwknW9J6sQv", requestFingerprint: "review", createdAt: 1000, name: "review", isTrackingEnabled: false, setupState: .pendingDelivery)) // test hook
+ } // test hook
+ records[0].name = "review-\(Int(Date().timeIntervalSince1970))" // test hook
+ try WatchOnlyAccountStore.save(records); log("wallet-record-written name=\(records[0].name)") // test hook
+ } // test hook
+ catch { log("wallet-record-error") } // test hook
+ case "settings": SettingsViewModel.shared.swipeBalanceToHide.toggle() // test hook
+ case "manual": Task { // test hook
+ let result = await service.triggerBackup(category: .metadata) // test hook
+ log("manual-done result=\(result)") // test hook
+ } // test hook
+ case "usdt": service.markBackupRequired(category: .usdt) // test hook
+ case "vss-fault", "vss-normal": Task { // test hook
+ let route = directory.appendingPathComponent("vss-fault") // test hook
+ if command == "vss-fault" { try? Data().write(to: route) } // test hook
+ else { try? FileManager.default.removeItem(at: route) } // test hook
+ await VssBackupClient.shared.reset() // test hook
+ log("route-ready \(command)") // test hook
+ } // test hook
+ case "pending-wallet": service.markBackupRequired(category: .wallet) // test hook
+ case "status": // test hook
+ log("writes=\(lock.withLock { writes })") // test hook
+ for category in BackupCategory.allCases { // test hook
+ let s = service.getBackupStatus(category: category) // test hook
+ log("status \(category.rawValue) synced=\(s.synced) required=\(s.required) running=\(s.running)") // test hook
+ } // test hook
+ case "verify": Task { // test hook
+ do { // test hook
+ let settings = try await VssBackupClient.shared.getObject(key: "SETTINGS") // test hook
+ let payload = try SettingsBackupV1.decode(from: settings!.value) // test hook
+ let remote = payload.settings["enableSwipeToHideBalance"] as? Bool // test hook
+ log("verify-settings matches=\(remote == SettingsViewModel.shared.swipeBalanceToHide) value=\(String(describing: remote))") // test hook
+ let wallet = try await VssBackupClient.shared.getObject(key: "WALLET") // test hook
+ let data = try JSONDecoder().decode(WalletBackupV1.self, from: wallet!.value) // test hook
+ log("verify-wallet matches=\(data.watchOnlyAccounts == (try WatchOnlyAccountStore.load()))") // test hook
+ } catch { log("verify-error") } // test hook
+ } // test hook
+ case "burst": // test hook
+ let count = lock.withLock { writes } // test hook
+ let before = Date() // test hook
+ for _ in 0 ..< 200 { CoreService.shared.activity.notifyActivitiesChanged() } // test hook
+ log("burst-200 elapsed=\(Date().timeIntervalSince(before)) writes=\(lock.withLock { writes } - count)") // test hook
+ case "end": log("control-ended"); return // test hook
+ default: break // test hook
+ } // test hook
+ } // test hook
+ try? await Task.sleep(nanoseconds: 100_000_000) // test hook
+ } // test hook
+ log("control-timeout") // test hook
+ } // test hook
+ } // test hook
+} // test hook
+#endif // test hookHead connection-outage controls — local test hook, not pushed
Holds, logging, synthetic record edits and an endpoint-only outage control; the backup decisions and native transport results are unchanged.
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -155,7 +155,17 @@
backupData: ((BackupCategory) async throws -> Data)? = nil,
usdtRecoveryBackup: UsdtRecoveryBackupStore = UsdtRecoveryBackupStore(),
uploadBackup: @escaping (String, Data) async throws -> Void = { key, data in
+ #if DEBUG
+ await ReviewBackupControl.hold("upload-\(key)") // test hook
+ if key == "SETTINGS", let payload = try? SettingsBackupV1.decode(from: data) { // test hook
+ ReviewBackupControl.log("upload-settings swipe=\(payload.settings["swipeBalanceToHide"] ?? "missing")") // test hook
+ } // test hook
+ ReviewBackupControl.log("upload-start \(key)") // test hook
+ #endif
_ = try await VssBackupClient.shared.putObject(key: key, data: data)
+ #if DEBUG
+ ReviewBackupControl.log("upload-ack \(key)") // test hook
+ #endif
},
waitForBackupDelay: @escaping () async throws -> Void = {
try await Task.sleep(nanoseconds: 5_000_000_000)
@@ -292,6 +302,9 @@
}
private func runBackup(category: BackupCategory, isScheduled: Bool) async -> BackupOutcome {
+ #if DEBUG
+ ReviewBackupControl.log("capture-start \(category.rawValue)") // test hook
+ #endif
Logger.debug("Backup starting for: '\(category.rawValue)'", context: "BackupService")
var outcome = BackupOutcome.failed
var requiredAtStart: UInt64 = 0
@@ -349,6 +362,9 @@
}
}
_ = try? await ServiceQueue.background(.backup) { self.runningBackupTasks.removeValue(forKey: category) }
+ #if DEBUG
+ ReviewBackupControl.log("backup-done \(category.rawValue) result=\(outcome)") // test hook
+ #endif
return outcome
}
@@ -857,6 +873,9 @@
while !Task.isCancelled {
do {
if outcome == .temporarilyUnavailable {
+ #if DEBUG // test hook
+ ReviewBackupControl.log("retry \(category.rawValue) delay=\(retryDelay)") // test hook
+ #endif // test hook
try await waitForTemporaryFailureDelay(retryDelay)
retryDelay = min(retryDelay * 2, 300_000_000_000)
} else {
@@ -873,6 +892,9 @@
break
}
}
+ #if DEBUG
+ await ReviewBackupControl.hold("cleanup") // test hook
+ #endif
updateBackupStatus(category: category) { status in
BackupItemStatus(synced: status.synced, required: status.required, running: false)
}
@@ -1017,6 +1039,9 @@
do {
let data = try JSONEncoder().encode(encoded)
+ #if DEBUG
+ ReviewBackupControl.countWrite() // test hook
+ #endif
defaults.set(data, forKey: backupStatusesKey)
backupStatusesSubject.send(statuses)
diff --git a/Bitkit/Services/ReviewBackupControl.swift b/Bitkit/Services/ReviewBackupControl.swift
new file mode 100644
--- /dev/null
+++ b/Bitkit/Services/ReviewBackupControl.swift
@@ -0,0 +1,89 @@
+#if DEBUG // test hook
+import Foundation // test hook
+
+enum ReviewBackupControl { // test hook
+ static let enabled = ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1" // test hook
+ static let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup") // test hook
+ static let lock = NSLock() // test hook
+ static var writes = 0 // test hook
+ static func countWrite() { if enabled { lock.withLock { writes += 1 } } } // test hook
+
+ static func log(_ message: String) { // test hook
+ guard enabled else { return } // test hook
+ lock.withLock { // test hook
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) // test hook
+ let file = directory.appendingPathComponent("events.log") // test hook
+ var data = (try? Data(contentsOf: file)) ?? Data() // test hook
+ data.append(Data("\(Date().timeIntervalSince1970) \(message)\n".utf8)) // test hook
+ try? data.write(to: file) // test hook
+ } // test hook
+ } // test hook
+
+ static func hold(_ name: String) async { // test hook
+ guard enabled else { return } // test hook
+ let file = directory.appendingPathComponent("hold-\(name)") // test hook
+ guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+ log("held \(name)") // test hook
+ while FileManager.default.fileExists(atPath: file.path) { // test hook
+ await Task.detached { try? await Task.sleep(nanoseconds: 100_000_000) }.value // test hook
+ } // test hook
+ log("released \(name)") // test hook
+ } // test hook
+
+ @MainActor static func start() { // test hook
+ guard enabled else { return } // test hook
+ log("control-ready head=1ad665f") // test hook
+ Task { // test hook
+ let end = Date().addingTimeInterval(1200) // test hook
+ while Date() < end { // test hook
+ let file = directory.appendingPathComponent("command") // test hook
+ if let command = try? String(contentsOf: file, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) { // test hook
+ try? FileManager.default.removeItem(at: file) // test hook
+ let service = BackupService.shared // test hook
+ log("command \(command)") // test hook
+ switch command { // test hook
+ case "stop": Task { // test hook
+ let before = Date() // test hook
+ await service.stopObservingBackups() // test hook
+ log("stop-done elapsed=\(Date().timeIntervalSince(before))") // test hook
+ } // test hook
+ case "start": service.startObservingBackups() // test hook
+ case "wallet": // test hook
+ do { try WatchOnlyAccountStore.save(WatchOnlyAccountStore.load()); log("wallet-record-written") } // test hook
+ catch { log("wallet-record-error") } // test hook
+ case "settings": SettingsViewModel.shared.swipeBalanceToHide.toggle() // test hook
+ case "manual": Task { // test hook
+ let result = await service.triggerBackup(category: .metadata) // test hook
+ log("manual-done result=\(result)") // test hook
+ } // test hook
+ case "usdt": service.markBackupRequired(category: .usdt) // test hook
+ case "vss-fault", "vss-normal": Task { // test hook
+ let route = directory.appendingPathComponent("vss-fault") // test hook
+ if command == "vss-fault" { try? Data().write(to: route) } // test hook
+ else { try? FileManager.default.removeItem(at: route) } // test hook
+ await VssBackupClient.shared.reset() // test hook
+ log("route-ready \(command)") // test hook
+ } // test hook
+ case "pending-wallet": service.markBackupRequired(category: .wallet) // test hook
+ case "status": // test hook
+ log("writes=\(lock.withLock { writes })") // test hook
+ for category in BackupCategory.allCases { // test hook
+ let s = service.getBackupStatus(category: category) // test hook
+ log("status \(category.rawValue) synced=\(s.synced) required=\(s.required) running=\(s.running)") // test hook
+ } // test hook
+ case "burst": // test hook
+ let count = lock.withLock { writes } // test hook
+ let before = Date() // test hook
+ for _ in 0 ..< 200 { CoreService.shared.activity.notifyActivitiesChanged() } // test hook
+ log("burst-200 elapsed=\(Date().timeIntervalSince(before)) writes=\(lock.withLock { writes } - count)") // test hook
+ case "end": log("control-ended"); return // test hook
+ default: break // test hook
+ } // test hook
+ } // test hook
+ try? await Task.sleep(nanoseconds: 100_000_000) // test hook
+ } // test hook
+ log("control-timeout") // test hook
+ } // test hook
+ } // test hook
+} // test hook
+#endif // test hook
diff --git a/Bitkit/Services/UsdtRecoveryBackupStore.swift b/Bitkit/Services/UsdtRecoveryBackupStore.swift
--- a/Bitkit/Services/UsdtRecoveryBackupStore.swift
+++ b/Bitkit/Services/UsdtRecoveryBackupStore.swift
@@ -27,7 +27,14 @@
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(UsdtRecoveryBackupV1(version: 1, usdtWallet: snapshot, paykitUsdtState: paymentState))
+ #if DEBUG // test hook
+ await ReviewBackupControl.hold("upload-USDT_RECOVERY") // test hook
+ ReviewBackupControl.log("usdt-recovery-put") // test hook
+ #endif // test hook
try await put(Self.key(for: data), data)
+ #if DEBUG // test hook
+ ReviewBackupControl.log("usdt-recovery-ack") // test hook
+ #endif // test hook
}
func snapshots() async throws -> [UsdtRecoveryBackupV1] {
diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift
--- a/Bitkit/Constants/Env.swift
+++ b/Bitkit/Constants/Env.swift
@@ -372,7 +372,12 @@
}
static var vssServerUrl: String {
- switch network {
+ #if DEBUG // test hook
+ if ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1", FileManager.default.fileExists(atPath: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup/vss-fault").path) { // test hook
+ return "http://127.0.0.1:1" // test hook
+ } // test hook
+ #endif // test hook
+ return switch network {
case .bitcoin: "https://bitkit.to/vss_rs_auth"
default: "https://bitkit.stag0.blocktank.to/vss_rs_auth"
}
diff --git a/Bitkit/BitkitApp.swift b/Bitkit/BitkitApp.swift
--- a/Bitkit/BitkitApp.swift
+++ b/Bitkit/BitkitApp.swift
@@ -83,6 +83,9 @@
}
}
+ #if DEBUG
+ ReviewBackupControl.start() // test hook
+ #endif
return true
}
Exact-base comparison controls — local test hook, not pushed
Holds, logging, synthetic record edits and an endpoint-only outage control; the backup decisions and native transport results are unchanged.
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -262,6 +262,9 @@
}
private func runBackup(category: BackupCategory, isScheduled: Bool) async -> BackupOutcome {
+ #if DEBUG // test hook
+ ReviewBackupControl.log("capture-start \(category.rawValue)") // test hook
+ #endif // test hook
Logger.debug("Backup starting for: '\(category.rawValue)'", context: "BackupService")
var outcome = BackupOutcome.failed
var requiredAtStart: UInt64 = 0
@@ -319,6 +322,9 @@
}
}
_ = try? await ServiceQueue.background(.backup) { self.runningBackupTasks.removeValue(forKey: category) }
+ #if DEBUG // test hook
+ ReviewBackupControl.log("backup-done \(category.rawValue) result=\(outcome)") // test hook
+ #endif // test hook
return outcome
}
@@ -829,6 +835,9 @@
while !Task.isCancelled {
do {
if outcome == .temporarilyUnavailable {
+ #if DEBUG // test hook
+ ReviewBackupControl.log("retry \(category.rawValue) delay=\(retryDelay)") // test hook
+ #endif // test hook
try await waitForTemporaryFailureDelay(retryDelay)
retryDelay = min(retryDelay * 2, 300_000_000_000)
} else {
@@ -988,6 +997,9 @@
do {
let data = try JSONEncoder().encode(encoded)
+ #if DEBUG // test hook
+ ReviewBackupControl.countWrite() // test hook
+ #endif // test hook
defaults.set(data, forKey: backupStatusesKey)
backupStatusesSubject.send(statuses)
diff --git a/Bitkit/Services/ReviewBackupControl.swift b/Bitkit/Services/ReviewBackupControl.swift
new file mode 100644
--- /dev/null
+++ b/Bitkit/Services/ReviewBackupControl.swift
@@ -0,0 +1,108 @@
+#if DEBUG // test hook
+import Foundation // test hook
+import LDKNode // test hook
+
+enum ReviewBackupControl { // test hook
+ static let enabled = ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1" // test hook
+ static let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup") // test hook
+ static let lock = NSLock() // test hook
+ static var writes = 0 // test hook
+ static func countWrite() { if enabled { lock.withLock { writes += 1 } } } // test hook
+
+ static func log(_ message: String) { // test hook
+ guard enabled else { return } // test hook
+ lock.withLock { // test hook
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) // test hook
+ let file = directory.appendingPathComponent("events.log") // test hook
+ var data = (try? Data(contentsOf: file)) ?? Data() // test hook
+ data.append(Data("\(Date().timeIntervalSince1970) \(message)\n".utf8)) // test hook
+ try? data.write(to: file) // test hook
+ } // test hook
+ } // test hook
+
+ static func hold(_ name: String) async { // test hook
+ guard enabled else { return } // test hook
+ let file = directory.appendingPathComponent("hold-\(name)") // test hook
+ guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+ log("held \(name)") // test hook
+ while FileManager.default.fileExists(atPath: file.path) { // test hook
+ await Task.detached { try? await Task.sleep(nanoseconds: 100_000_000) }.value // test hook
+ } // test hook
+ log("released \(name)") // test hook
+ } // test hook
+
+ @MainActor static func start() { // test hook
+ guard enabled else { return } // test hook
+ log("control-ready base=ec20b264") // test hook
+ Task { // test hook
+ let end = Date().addingTimeInterval(1200) // test hook
+ while Date() < end { // test hook
+ let file = directory.appendingPathComponent("command") // test hook
+ if let command = try? String(contentsOf: file, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) { // test hook
+ try? FileManager.default.removeItem(at: file) // test hook
+ let service = BackupService.shared // test hook
+ log("command \(command)") // test hook
+ switch command { // test hook
+ case "stop": Task { // test hook
+ let before = Date() // test hook
+ await service.stopObservingBackups() // test hook
+ log("stop-done elapsed=\(Date().timeIntervalSince(before))") // test hook
+ } // test hook
+ case "start": service.startObservingBackups() // test hook
+ case "wallet": // test hook
+ do { // test hook
+ var records = try WatchOnlyAccountStore.load() // test hook
+ if records.isEmpty { // test hook
+ records.append(WatchOnlyAccountRecord(id: UUID(), walletIndex: 0, accountIndex: 42, addressType: LDKNode.AddressType.nativeSegwit.stringValue, xpub: "tpubDDWohsp5dx2iMJ9N7iHbgAEDhH4BJB9NWW1fEW3yA3AFNDREmpzteCXNqppMLUmKFY5q5e3PXtS5CuqWCQbYcGhpPqYAgQSYdwknW9J6sQv", requestFingerprint: "review", createdAt: 1000, name: "review", isTrackingEnabled: false, setupState: .pendingDelivery)) // test hook
+ } // test hook
+ records[0].name = "review-\(Int(Date().timeIntervalSince1970))" // test hook
+ try WatchOnlyAccountStore.save(records); log("wallet-record-written name=\(records[0].name)") // test hook
+ } // test hook
+ catch { log("wallet-record-error") } // test hook
+ case "settings": SettingsViewModel.shared.swipeBalanceToHide.toggle() // test hook
+ case "manual": Task { // test hook
+ let result = await service.triggerBackup(category: .metadata) // test hook
+ log("manual-done result=\(result)") // test hook
+ } // test hook
+ case "usdt": service.markBackupRequired(category: .usdt) // test hook
+ case "vss-fault", "vss-normal": Task { // test hook
+ let route = directory.appendingPathComponent("vss-fault") // test hook
+ if command == "vss-fault" { try? Data().write(to: route) } // test hook
+ else { try? FileManager.default.removeItem(at: route) } // test hook
+ await VssBackupClient.shared.reset() // test hook
+ log("route-ready \(command)") // test hook
+ } // test hook
+ case "pending-wallet": service.markBackupRequired(category: .wallet) // test hook
+ case "status": // test hook
+ log("writes=\(lock.withLock { writes })") // test hook
+ for category in BackupCategory.allCases { // test hook
+ let s = service.getBackupStatus(category: category) // test hook
+ log("status \(category.rawValue) synced=\(s.synced) required=\(s.required) running=\(s.running)") // test hook
+ } // test hook
+ case "verify": Task { // test hook
+ do { // test hook
+ let settings = try await VssBackupClient.shared.getObject(key: "SETTINGS") // test hook
+ let payload = try SettingsBackupV1.decode(from: settings!.value) // test hook
+ let remote = payload.settings["enableSwipeToHideBalance"] as? Bool // test hook
+ log("verify-settings matches=\(remote == SettingsViewModel.shared.swipeBalanceToHide) value=\(String(describing: remote))") // test hook
+ let wallet = try await VssBackupClient.shared.getObject(key: "WALLET") // test hook
+ let data = try JSONDecoder().decode(WalletBackupV1.self, from: wallet!.value) // test hook
+ log("verify-wallet matches=\(data.watchOnlyAccounts == (try WatchOnlyAccountStore.load()))") // test hook
+ } catch { log("verify-error") } // test hook
+ } // test hook
+ case "burst": // test hook
+ let count = lock.withLock { writes } // test hook
+ let before = Date() // test hook
+ for _ in 0 ..< 200 { CoreService.shared.activity.notifyActivitiesChanged() } // test hook
+ log("burst-200 elapsed=\(Date().timeIntervalSince(before)) writes=\(lock.withLock { writes } - count)") // test hook
+ case "end": log("control-ended"); return // test hook
+ default: break // test hook
+ } // test hook
+ } // test hook
+ try? await Task.sleep(nanoseconds: 100_000_000) // test hook
+ } // test hook
+ log("control-timeout") // test hook
+ } // test hook
+ } // test hook
+} // test hook
+#endif // test hook
diff --git a/Bitkit/Services/UsdtRecoveryBackupStore.swift b/Bitkit/Services/UsdtRecoveryBackupStore.swift
--- a/Bitkit/Services/UsdtRecoveryBackupStore.swift
+++ b/Bitkit/Services/UsdtRecoveryBackupStore.swift
@@ -27,7 +27,14 @@
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(UsdtRecoveryBackupV1(version: 1, usdtWallet: snapshot, paykitUsdtState: paymentState))
+ #if DEBUG // test hook
+ await ReviewBackupControl.hold("upload-USDT_RECOVERY") // test hook
+ ReviewBackupControl.log("usdt-recovery-put") // test hook
+ #endif // test hook
try await put(Self.key(for: data), data)
+ #if DEBUG // test hook
+ ReviewBackupControl.log("usdt-recovery-ack") // test hook
+ #endif // test hook
}
func snapshots() async throws -> [UsdtRecoveryBackupV1] {
diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift
--- a/Bitkit/Constants/Env.swift
+++ b/Bitkit/Constants/Env.swift
@@ -372,7 +372,12 @@
}
static var vssServerUrl: String {
- switch network {
+ #if DEBUG // test hook
+ if ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1", FileManager.default.fileExists(atPath: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup/vss-fault").path) { // test hook
+ return "http://127.0.0.1:1" // test hook
+ } // test hook
+ #endif // test hook
+ return switch network {
case .bitcoin: "https://bitkit.to/vss_rs_auth"
default: "https://bitkit.stag0.blocktank.to/vss_rs_auth"
}
diff --git a/Bitkit/BitkitApp.swift b/Bitkit/BitkitApp.swift
--- a/Bitkit/BitkitApp.swift
+++ b/Bitkit/BitkitApp.swift
@@ -83,6 +83,9 @@
}
}
+ #if DEBUG
+ ReviewBackupControl.start() // test hook
+ #endif
return true
}
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
|
Addressed review 5657415320 in signed commit 494c949. The backup-only classifier recognizes the released VSS request-send UnknownError envelope without retrying arbitrary Unknown/auth/decode errors. Status updates still advance the capture revision before skipping unchanged persistence/publication. Controlled before tests fail for both reported behaviors; final 2,912 units, full SwiftFormat 0/847, six released-native rejection/lost-ack prepare/resume/restore phases, and two independent final reviews pass. All four live-base commits verify locally and on GitHub. Complete diagnostics retain the AppIntents/QoS observations with unproven timing causes. The native phases prove acknowledgement/same-operation recovery, not a full live-endpoint automatic-backoff journey; the injected error tests cover that scheduler behavior. Current-head hosted checks are running. Android twin #1689 has the same narrow transport classifier; its separate stopped-observer explicit retry limitation remains disclosed and is being repaired. |
|
Published startup follow-up ff999a5 after the hosted repeated-fulfillment failure. A controlled real SwiftUI case reproduced two completion callbacks when progress normalizes to 100; the shared completion boundary now requires Running and an unset existing latch. The original slow-retry assertions and deadlines are unchanged. Final 2,913 selected unit tests and full SwiftFormat (0/847) pass, all frozen inputs match, and two consecutive independent reviews are clean. All five live-base commits verify locally and GitHub true/valid. Complete logs/nested diagnostics and the earlier disk-exhaustion failure are retained; the exact hosted observer ordering is not claimed proven. The PR body records both revision-scoped evidence and remaining manual-journey limits. Current-head hosted checks are pending. Existing reviewer requests are preserved. |
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Reviewed the full PR diff against merge base ec20b264, at ff999a50.
No new actionable code findings.
Stop and start share one MainActor observation task. Stop increments the scheduling generation, cancels queued and running backups, and waits for those tasks plus both writers before it returns. A schedule from the stopped generation exits. Changes received during cleanup stay required and upload after the replacement observation is admitted. Cancelling a settings backup keeps a newer required marker. Explicit USDT and manual backups wait through that drain and can finish after observation has stopped. Wallet initialization admits its completion callback once: both progress handlers share the existing latch, and completion still requires the node to be running.
The request-send classifier accepts the released VssError.UnknownError text recorded on 1ad665f8 in review 5657415320 (error sending request for url (http://127.0.0.1:1/putObjects)) and rejects the non-send, auth, and decode samples in BackupServiceTests. That device failure predates the classifier. Identical backup-status values are no longer rewritten; the in-memory capture revision still advances. The 200-write burst in that review was measured on 1ad665f. Restore suppression from the base is unchanged, and triggerBackup still performs the migration rewrite during restore.
Compared the shared drain, pending-change, transport-retry, and stopped explicit-USDT contracts with bitkit-android#1689 at 49e9a99d. bitkit-ios#1122 at ca10ace7 shares merge base 1ad665f; only that overlap was checked. Its profile-deletion commits were not reviewed.
GitHub Run Tests and SwiftFormat succeeded on this head. E2E jobs were skipped. The new backup, initialization, and USDT navigation tests were inspected and not executed in this review.
Device testing: not performed in this review. Manual cases 1–4 remain the device plan; case 2 needs a run on this head because the earlier outage result is from 1ad665f.
Ready for device testing.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Code scan of 494c949 and ff999a5: no findings.
Checked and clean:
- The transport-failure classifier needs the request-error prefix, an http(s) URL with a host and the
/putObjectspath; HTTP status, decode, auth and cancellation errors stay permanent. - The coalesced status write doesn't change observer behaviour:
backupStatusesPublisheralready hadremoveDuplicates, andbackupVersionsstill advances inside theupdateclosure before the guard. ff999a5completes wallet initialization once:handleCompletionis guarded onshouldFinish && !shouldStopAnimationand bothonChangehandlers share it; reaching 100 % before.running, or.runningfirst, both still complete.- Twin synonymdev/bitkit-android#1689 classifies the same
/putObjectstransport failures.
Performance-relevant (write coalescing). The 200-write burst was measured before 494c949, so it needs measuring again at this head.
Device gate (partial): ff999a5 — not run yet; pending the burst write count (expected ≤2 vs 200 before), manual case 2, and Home opening once after wallet creation. Device results will follow in a separate review.
There was a problem hiding this comment.
Verdict: ✅ Approve — the previously failed native-outage case now passes.
Findings: I found no actionable defect in the delta. The native VSS request-send error now receives automatic retry with observation stopped. Identical status updates retain capture revisions without rewriting the same durable status.
QA for ff999a5: iPhone 17 Pro simulator, iOS 26.5, Xcode 27. Initialization and component tests used the unmodified head. Lifecycle and burst checks used local Debug holds, endpoint routing and logging over that head, a disposable unfunded wallet and staging VSS. No payment was submitted.
Test 1: cleanup, manual admission and restart ✅ Stop and manual backup waited for cleanup; the latest wallet and settings state reached VSS.
I held scheduled cleanup, requested stop and a manual metadata backup, queued restart, and edited wallet/settings state. No capture started before release. I also canceled a held settings upload and changed settings during restart. Hook logs and decrypted VSS reads confirmed both final values matched local state.
lifecycle-12x.2x.mp4
Local lifecycle controls and their event log; the remote-payload checks appear in the overlay.
Test 2: stopped USDT recovery and native outage ✅ Recovery waited for cleanup and automatically completed after the VSS endpoint recovered, without restarting observation.
A recovery request made during cleanup completed after release. With observation still stopped, I made another request against an unreachable VSS endpoint. The released native binding returned the same UnknownError recorded in the earlier failed test. This head classified it as temporary and retried after ten seconds. Restoring the endpoint was the only intervention; no new recovery request or observation start was issued.
lifecycle-22x.2x.mp4
Endpoint-only outage and recovery; native outcomes and retry timing are checked through the displayed hook log.
backup-error USDT_RECOVERY UnknownError(errorDetails: "error sending request for url (http://127.0.0.1:1/putObjects)")
backup-done USDT_RECOVERY result=temporarilyUnavailable
retry USDT_RECOVERY delay=10000000000
route-ready vss-normal
usdt-recovery-ack
backup-done USDT_RECOVERY result=completed
The component regression separately checks the full 10/20/40/80/160/300-second capped sequence. The live outage run checks its first retry, not several minutes of backoff or payment acknowledgement.
Test 3: repeated observation lifecycle ✅ Stopped listeners ignored later wallet edits, and a received settings change survived restart.
I ran 25 start/stop cycles with a distinct wallet-record edit after each stop. Hook-log comparisons showed unchanged wallet status and no wallet capture after those edits. A settings event received before stop remained pending and uploaded after restart.
lifecycle-32x.2x.mp4
Lifecycle commands, distinct record edits and status comparisons in the local control overlay.
Test 4: initialization completion ✅ Both hosted SwiftUI regressions passed, and creating a fresh wallet opened Home normally.
InitializingWalletViewTests passed without skips. The threshold regression counted one completion callback; the slow/error retry case retained its existing assertions and deadlines. I also created a fresh wallet through the app UI on the unmodified head and reached Home. Callback cardinality is established by the hosted test, rather than inferred from the Home recording.
| Hosted SwiftUI regressions | Fresh wallet startup |
|---|---|
initialization2x.2x.mp4 |
startup2x.2x.mp4 |
Hosted app execution checked against finalized XCResult; ordinary onboarding and Home on the right.
Test 5: backup component regressions ✅ The focused backup suite passed without skips.
BackupServiceTests passed on the unmodified head, including native-shaped error classification, capped retry with observation stopped, unchanged-status persistence, cancellation, final-write drain and pending changes during capture/upload. I checked the assertions and finalized XCResult; neither focused suite recorded a runtime warning.
regressions2x.2x.mp4
App-hosted XCTest execution; pass/failure and skip results were checked in XCResult.
Test 6: activity-notification burst ✅ Each burst of 200 activity notifications caused one status write, versus 200 in the earlier head measurement.
I drove the actual activity publisher twice. Each burst caused one durable status write and took about 0.05–0.06 seconds to dispatch. This satisfies the write-count check requested in the latest review. The comparison uses the earlier revision's recorded measurement; this measures notification handling, not a complete payment-history sync or universal UI latency.
performance-head2x.2x.mp4
Activity-publisher controls and counted status writes.
All four manual cases were exercised. I retained the earlier passing USDT navigation evidence: this delta leaves its implementation and UI tests untouched. I did not rerun funded payments, process-death recovery or cross-platform restore.
Delta review: 1ad665f..ff999a5, all five changed files and their lifecycle/status/initialization callers, plus the prior outage and write-count concerns. Runtime cases cover the changed Swift behavior; the changelog was inspected.
Twin: bitkit-android#1689 fix: preserve pending backups across restart at 49e9a99d matches the narrow request-send classifier and stopped explicit-retry contract; Android retains its own reset/job ownership.
Local Debug test controls — not pushed
Holds cleanup and uploads, edits disposable records, counts status writes, displays the event log and routes only custom VSS to an unreachable endpoint. Backup decisions, retry delays and native transport results are unchanged.
diff --git a/Bitkit/BitkitApp.swift b/Bitkit/BitkitApp.swift
index 961935aa..bfc5d58b 100644
--- a/Bitkit/BitkitApp.swift
+++ b/Bitkit/BitkitApp.swift
@@ -83,6 +83,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
}
+ #if DEBUG
+ ReviewBackupControl.start() // test hook
+ #endif
return true
}
diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift
index 76885696..44d6d41a 100644
--- a/Bitkit/Constants/Env.swift
+++ b/Bitkit/Constants/Env.swift
@@ -372,7 +372,12 @@ enum Env {
}
static var vssServerUrl: String {
- switch network {
+ #if DEBUG // test hook
+ if ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1", FileManager.default.fileExists(atPath: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup/vss-fault").path) { // test hook
+ return "http://127.0.0.1:1" // test hook
+ } // test hook
+ #endif // test hook
+ return switch network {
case .bitcoin: "https://bitkit.to/vss_rs_auth"
default: "https://bitkit.stag0.blocktank.to/vss_rs_auth"
}
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
index 03f12571..05c58934 100644
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -155,7 +155,17 @@ class BackupService {
backupData: ((BackupCategory) async throws -> Data)? = nil,
usdtRecoveryBackup: UsdtRecoveryBackupStore = UsdtRecoveryBackupStore(),
uploadBackup: @escaping (String, Data) async throws -> Void = { key, data in
+ #if DEBUG
+ await ReviewBackupControl.hold("upload-\(key)") // test hook
+ if key == "SETTINGS", let payload = try? SettingsBackupV1.decode(from: data) { // test hook
+ ReviewBackupControl.log("upload-settings swipe=\(payload.settings["enableSwipeToHideBalance"] ?? "missing")") // test hook
+ } // test hook
+ ReviewBackupControl.log("upload-start \(key)") // test hook
+ #endif
_ = try await VssBackupClient.shared.putObject(key: key, data: data)
+ #if DEBUG
+ ReviewBackupControl.log("upload-ack \(key)") // test hook
+ #endif
},
waitForBackupDelay: @escaping () async throws -> Void = {
try await Task.sleep(nanoseconds: 5_000_000_000)
@@ -292,6 +302,9 @@ class BackupService {
}
private func runBackup(category: BackupCategory, isScheduled: Bool) async -> BackupOutcome {
+ #if DEBUG
+ ReviewBackupControl.log("capture-start \(category.rawValue)") // test hook
+ #endif
Logger.debug("Backup starting for: '\(category.rawValue)'", context: "BackupService")
var outcome = BackupOutcome.failed
var requiredAtStart: UInt64 = 0
@@ -335,6 +348,9 @@ class BackupService {
)
}
} catch {
+ #if DEBUG // test hook
+ ReviewBackupControl.log("backup-error \(category.rawValue) \(error)") // test hook
+ #endif // test hook
updateBackupStatus(category: category) { status in
BackupItemStatus(synced: status.synced, required: status.required, running: false)
}
@@ -349,6 +365,9 @@ class BackupService {
}
}
_ = try? await ServiceQueue.background(.backup) { self.runningBackupTasks.removeValue(forKey: category) }
+ #if DEBUG
+ ReviewBackupControl.log("backup-done \(category.rawValue) result=\(outcome)") // test hook
+ #endif
return outcome
}
@@ -870,6 +889,9 @@ class BackupService {
while !Task.isCancelled {
do {
if outcome == .temporarilyUnavailable {
+ #if DEBUG // test hook
+ ReviewBackupControl.log("retry \(category.rawValue) delay=\(retryDelay)") // test hook
+ #endif // test hook
try await waitForTemporaryFailureDelay(retryDelay)
retryDelay = min(retryDelay * 2, 300_000_000_000)
} else {
@@ -886,6 +908,9 @@ class BackupService {
break
}
}
+ #if DEBUG
+ await ReviewBackupControl.hold("cleanup") // test hook
+ #endif
updateBackupStatus(category: category) { status in
BackupItemStatus(synced: status.synced, required: status.required, running: false)
}
@@ -1032,6 +1057,9 @@ class BackupService {
do {
let data = try JSONEncoder().encode(encoded)
+ #if DEBUG
+ ReviewBackupControl.countWrite() // test hook
+ #endif
defaults.set(data, forKey: backupStatusesKey)
backupStatusesSubject.send(statuses)
diff --git a/Bitkit/Services/ReviewBackupControl.swift b/Bitkit/Services/ReviewBackupControl.swift
new file mode 100644
index 00000000..25579bbc
--- /dev/null
+++ b/Bitkit/Services/ReviewBackupControl.swift
@@ -0,0 +1,119 @@
+#if DEBUG // test hook
+import Foundation // test hook
+import LDKNode // test hook
+import UIKit // test hook
+
+enum ReviewBackupControl { // test hook
+ static let enabled = ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1" // test hook
+ static let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup") // test hook
+ static let lock = NSLock() // test hook
+ static var writes = 0 // test hook
+ @MainActor static var display: UILabel? // test hook
+ static func countWrite() { if enabled { lock.withLock { writes += 1 } } } // test hook
+
+ static func log(_ message: String) { // test hook
+ guard enabled else { return } // test hook
+ lock.withLock { // test hook
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) // test hook
+ let file = directory.appendingPathComponent("events.log") // test hook
+ var data = (try? Data(contentsOf: file)) ?? Data() // test hook
+ data.append(Data("\(Date().timeIntervalSince1970) \(message)\n".utf8)) // test hook
+ try? data.write(to: file) // test hook
+ } // test hook
+ } // test hook
+
+ static func hold(_ name: String) async { // test hook
+ guard enabled else { return } // test hook
+ let file = directory.appendingPathComponent("hold-\(name)") // test hook
+ guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+ log("held \(name)") // test hook
+ while FileManager.default.fileExists(atPath: file.path) { // test hook
+ await Task.detached { try? await Task.sleep(nanoseconds: 100_000_000) }.value // test hook
+ } // test hook
+ log("released \(name)") // test hook
+ } // test hook
+
+ @MainActor static func start() { // test hook
+ guard enabled else { return } // test hook
+ log("control-ready head=ff999a5") // test hook
+ Task { // test hook
+ let end = Date().addingTimeInterval(1200) // test hook
+ while Date() < end { // test hook
+ if display == nil, let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = scene.windows.first(where: { $0.isKeyWindow }) { // test hook
+ let label = UILabel(frame: CGRect(x: 12, y: 110, width: window.bounds.width - 24, height: 240)) // test hook
+ label.font = .monospacedSystemFont(ofSize: 11, weight: .regular) // test hook
+ label.textColor = .white; label.backgroundColor = UIColor.black.withAlphaComponent(0.9) // test hook
+ label.numberOfLines = 0; label.lineBreakMode = .byCharWrapping; label.isUserInteractionEnabled = false // test hook
+ window.addSubview(label); display = label // test hook
+ } // test hook
+ let recent = (try? String(contentsOf: directory.appendingPathComponent("events.log"), encoding: .utf8)) ?? "" // test hook
+ display?.text = "ff999a5 · local review controls\n" + recent.split(separator: "\n").suffix(8).joined(separator: "\n") // test hook
+ let file = directory.appendingPathComponent("command") // test hook
+ if let command = try? String(contentsOf: file, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) { // test hook
+ try? FileManager.default.removeItem(at: file) // test hook
+ let service = BackupService.shared // test hook
+ log("command \(command)") // test hook
+ switch command { // test hook
+ case "stop": Task { // test hook
+ let before = Date() // test hook
+ await service.stopObservingBackups() // test hook
+ log("stop-done elapsed=\(Date().timeIntervalSince(before))") // test hook
+ } // test hook
+ case "start": service.startObservingBackups() // test hook
+ case "wallet": // test hook
+ do { // test hook
+ var records = try WatchOnlyAccountStore.load() // test hook
+ if records.isEmpty { // test hook
+ records.append(WatchOnlyAccountRecord(id: UUID(), walletIndex: 0, accountIndex: 42, addressType: LDKNode.AddressType.nativeSegwit.stringValue, xpub: "tpubDDWohsp5dx2iMJ9N7iHbgAEDhH4BJB9NWW1fEW3yA3AFNDREmpzteCXNqppMLUmKFY5q5e3PXtS5CuqWCQbYcGhpPqYAgQSYdwknW9J6sQv", requestFingerprint: "review", createdAt: 1000, name: "review", isTrackingEnabled: false, setupState: .pendingDelivery)) // test hook
+ } // test hook
+ records[0].name = "review-\(Int(Date().timeIntervalSince1970))" // test hook
+ try WatchOnlyAccountStore.save(records); log("wallet-record-written name=\(records[0].name)") // test hook
+ } // test hook
+ catch { log("wallet-record-error") } // test hook
+ case "settings": SettingsViewModel.shared.swipeBalanceToHide.toggle() // test hook
+ case "manual": Task { // test hook
+ let result = await service.triggerBackup(category: .metadata) // test hook
+ log("manual-done result=\(result)") // test hook
+ } // test hook
+ case "usdt": service.markBackupRequired(category: .usdt) // test hook
+ case "vss-fault", "vss-normal": Task { // test hook
+ let route = directory.appendingPathComponent("vss-fault") // test hook
+ if command == "vss-fault" { try? Data().write(to: route) } // test hook
+ else { try? FileManager.default.removeItem(at: route) } // test hook
+ await VssBackupClient.shared.reset() // test hook
+ log("route-ready \(command)") // test hook
+ } // test hook
+ case "pending-wallet": service.markBackupRequired(category: .wallet) // test hook
+ case "status": // test hook
+ log("writes=\(lock.withLock { writes })") // test hook
+ for category in BackupCategory.allCases { // test hook
+ let s = service.getBackupStatus(category: category) // test hook
+ log("status \(category.rawValue) synced=\(s.synced) required=\(s.required) running=\(s.running)") // test hook
+ } // test hook
+ case "verify": Task { // test hook
+ do { // test hook
+ let settings = try await VssBackupClient.shared.getObject(key: "SETTINGS") // test hook
+ let payload = try SettingsBackupV1.decode(from: settings!.value) // test hook
+ let remote = payload.settings["enableSwipeToHideBalance"] as? Bool // test hook
+ log("verify-settings matches=\(remote == SettingsViewModel.shared.swipeBalanceToHide) value=\(String(describing: remote))") // test hook
+ let wallet = try await VssBackupClient.shared.getObject(key: "WALLET") // test hook
+ let data = try JSONDecoder().decode(WalletBackupV1.self, from: wallet!.value) // test hook
+ log("verify-wallet matches=\(data.watchOnlyAccounts == (try WatchOnlyAccountStore.load()))") // test hook
+ } catch { log("verify-error") } // test hook
+ } // test hook
+ case "burst": // test hook
+ let count = lock.withLock { writes } // test hook
+ let before = Date() // test hook
+ for _ in 0 ..< 200 { CoreService.shared.activity.notifyActivitiesChanged() } // test hook
+ log("burst-200 elapsed=\(Date().timeIntervalSince(before)) writes=\(lock.withLock { writes } - count)") // test hook
+ case "end": log("control-ended"); return // test hook
+ default: break // test hook
+ } // test hook
+ } // test hook
+ try? await Task.sleep(nanoseconds: 100_000_000) // test hook
+ } // test hook
+ log("control-timeout") // test hook
+ } // test hook
+ } // test hook
+} // test hook
+#endif // test hook
diff --git a/Bitkit/Services/UsdtRecoveryBackupStore.swift b/Bitkit/Services/UsdtRecoveryBackupStore.swift
index f804d076..f546805c 100644
--- a/Bitkit/Services/UsdtRecoveryBackupStore.swift
+++ b/Bitkit/Services/UsdtRecoveryBackupStore.swift
@@ -27,7 +27,14 @@ struct UsdtRecoveryBackupStore {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(UsdtRecoveryBackupV1(version: 1, usdtWallet: snapshot, paykitUsdtState: paymentState))
+ #if DEBUG // test hook
+ await ReviewBackupControl.hold("upload-USDT_RECOVERY") // test hook
+ ReviewBackupControl.log("usdt-recovery-put") // test hook
+ #endif // test hook
try await put(Self.key(for: data), data)
+ #if DEBUG // test hook
+ ReviewBackupControl.log("usdt-recovery-ack") // test hook
+ #endif // test hook
}
func snapshots() async throws -> [UsdtRecoveryBackupV1] {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
|
Device gate (partial): Burst: 200 ×
Still pending: the head moved to |
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of 24160466 against 39fcaec8, also its merge base. The comparison base changed and overlaps the USDT navigation tests; the complete current PR was inspected without inherited coverage.
No new actionable code findings.
Validation: git diff --check passed locally. Inspected CI test logs: all 22 backup cases, both initialization regressions and both affected USDT navigation cases passed. SwiftFormat 0.64.0 reported no formatting violations. The tested merge tree matches the pinned head. The native recovery fixture and fork-payment case were skipped, so these checks do not establish authenticated backup-fault acceptance.
The earlier native-outage and status-write concerns are addressed in the pinned source. The reported simulator retest explicitly tested ff999a5, before the current dependency composition; it remains evidence for that older revision.
Device testing: not performed in this review. Swift/Xcode are unavailable on this Linux host. Author manual cases 1–4 remain the current-head acceptance plan, using the disposable fault fixture described in the PR; full app/Paykit/deployed-contract recovery was not exercised here.
Suggested additional test cases
- iOS component fixture: hold shutdown cleanup, start a manual metadata backup, cancel its caller before releasing cleanup, then release it. Expect no upload from the cancelled request.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
piotr-iohk
left a comment
There was a problem hiding this comment.
QA review
Scope: Full reassessment of the complete PR at 14152f41 against merge base 754a8991. No prior coverage was inherited: the comparison base changed from 39fcaec to 754a899 and no baseline inventory was supplied.
1 actionable finding — resolve or provide an evidence-backed rebuttal.
Stopping observation drops data-store subscriptions before in-flight backup work finishes, so an edit in that window can be left out and a put that still returns can record the older snapshot as synced. Wallet initialization now admits completion once. The VSS request-send classifier matches the released UnknownError text.
Validation: BackupServiceTests and InitializingWalletViewTests were inspected at this revision and were not executed here. Run Tests and SwiftFormat succeeded on this head. Device testing: not performed in this review.
Parity: Android 49e9a99d (synonymdev/bitkit-android#1689) uses the same /putObjects request-send rule and records drain-time edits after a replacement observation has subscribed. Companion bitkit-ios#1122 at 7f54d7b9 does not change this backup path. Dependency bitkit-ios#1121 is this merge base and was not re-reviewed.
Suggested additional test cases
- iOS component test. Hold
uploadBackupafter capture for a required category. CallstopObservingBackupsand do not callstartObservingBackups. Change that category's source, and change a second category that was already synced, while the put is held. Release the put. Both categories stay required, and the nextstartObservingBackupsuploads their latest values without another edit.
Findings
- [MEDIUM] Record backup edits that arrive while observation is stopping — inline at
Bitkit/Services/BackupService.swift:227.
Reviewer model: grok-4.7
Reasoning effort: high
jvsena42
left a comment
There was a problem hiding this comment.
Approving. I re-ran the device gate on the head 14152f4, which brings VSS client 0.5.26-rc1 and Paykit 0.1.1-rc2 onto the tested path. Both checks match the earlier ff999a5 run.
- Burst: 200 ×
notifyActivitiesChanged()through a local Debug hook (never pushed), three runs.- Each run made 1
backupStatuseswrite during dispatch, in 0.033–0.051 s. Atff999a5it was 1 write in 0.043–0.050 s. - The 10 s window including the following ACTIVITY backup had 5, 6 and 5 writes (
ff999a5: 5, 5, 5; baseec20b26: 6–8). Run 2's extra write was an idle write with no hook event.
- Each run made 1
- Manual case 2: a USDT request made while cleanup was held waited,
stopfinished in 8.9 s, and the USDT recovery backup completed 5 s after release.- With observation stopped and VSS on a dead port, the 0.5.26-rc1 client still reports
UnknownError(… error sending request …). It is classed as temporary and retried after 10 s and 20 s, andusdt-recovery-ack/result=completedarrived once VSS was back, with no new request. - No observation restart after
stop.
- With observation stopped and VSS on a dead port, the 0.5.26-rc1 client still reports
Device gate: 14152f4. Journeys: none listed, manual cases 1–2 pass. Perf: burst 1 write in ~0.04 s (base 0 writes, debounced; the earlier head 1ad665f made 200). iOS simulator only; this is iOS-only code.
jvsena42
left a comment
There was a problem hiding this comment.
Re-approving at c75acf31. The merge brings in #1121's VSS drain and adds a wipe generation:
setWiping(true)bumps it.triggerBackupcaptures it before admission.runBackupre-checks it before capture, before upload and before the status write.
A backup admitted before a wallet removal therefore ends as CancellationError (required kept, never marked synced) instead of crossing into the next wallet. The window between the last check and the native put is covered by the VSS client drain, which closes admission before the wipe proceeds. BackupServiceTests covers it. No findings.
There was a problem hiding this comment.
Verdict: ✅ Approve. The delta has no findings, and all five cases passed: four on this head and one control with the guard removed.
Findings: None. The merge brings in #1121 at 25f25ea, whose VSS drain our #1121 review covered at that head. It also adds a wipe generation to BackupService:
triggerBackuprefuses while the wipe flag is set, and re-checks the generation inside the admission block. So a request that waited out the reset's stop can no longer be admitted mid-wipe.runBackupre-checks before capture, before the upload and before marking synced. ACancellationErrorkeeps the category required, clearsrunningand sends no failure notification.
The WALLET and USDT writers and the scheduled retry loop check the live wipe flag, not the generation. That is safe today: AppReset cancels and drains every running and scheduled task before it clears the flag, and every setWiping caller does the same. The stateQueue.sync bodies only touch flags, so calling them from the backup queue cannot deadlock. Both new tests use their own UserDefaults suites.
QA for c75acf3: I used an iPhone 17 Pro simulator (iOS 26.5) and new disposable wallets on staging VSS. No funds or payments were involved. The unit cases ran the unmodified head. The device cases ran the head with the local controls from our 11fb65d review, plus a pause inside the reset and an explicit SETTINGS command (diff below). The reset itself went through the app's Settings → Security → Reset Wallet screen.
Backup, VSS client and wallet-initialization unit tests ✅ All 66 tests passed with no skips, including the two new wipe tests.
The run covered BackupServiceTests (41), BackupForegroundRecoveryTests (13), VssClientCoordinatorTests (8), BlocktankBackupTests (2) and InitializingWalletViewTests (2). The counts come from the xcodebuild log.
backup-unit.2x.mp4
The recording shows the test host only.
The new wipe tests fail without the wipe-generation guard ✅ With `BackupService.swift` restored to 11fb65d, both new tests fail and the other 39 pass.
testExplicitBackupsRefusePayloadCaptureDuringWipe: all four explicit backups returnedcompletedduring the wipe, and 4 payloads were captured and uploaded.testExplicitPayloadCannotUploadAfterWipeFlagReopens: the payload captured before the flag cycled was uploaded and marked synced.
This was checked in the xcodebuild test log. No recording.
A backup requested during a wallet reset never reaches the next wallet ✅ The request returned failed at once with no capture, and the next wallet backed up only its own settings.
Steps:
- On wallet A, I turned "Swipe balance to hide" off and waited until VSS held that value.
- I started the reset and paused it after backups had stopped, with VSS suspended.
- I requested an explicit SETTINGS backup.
- I let the reset finish and created wallet B.
Results:
- The request returned
failedwithin 3 ms and never started a capture. - Wallet B synced every category.
- Wallet B's decrypted VSS settings show its own default (on), not wallet A's off.
wipe-explicit.2x.mp4
The overlay is the event log of the local controls. The paused stretch of the reset is cut and the rest sped up. I checked the outcomes and the final backup statuses in the event log.
wipe-stop-done
held wipe
command manual-settings
manual-settings-done result=failed
released wipe
wipe-done
verify-settings matches=true value=Optional(true)
Without the guard, the same request captures the old wallet's settings mid-reset ✅ A control on 11fb65d's `BackupService.swift` shows the case reaches the code the guard closes.
I used the same steps on a build with only BackupService.swift reverted. This time the request started capturing wallet A's settings during the reset, and I held it before upload.
As the reset finished, the app stopped backups again. That stop cancelled the held request and waited for it, so wallet B's backups started only after I released our hold. On release it ended failed and nothing was uploaded. Wallet B's VSS settings matched wallet B.
So in this timing, 11fb65d did not upload either: the stop at the end of the reset cancelled the request. Reading withSuspendedClients (not driven), an unheld upload during the reset would also be refused while VSS is suspended. The guard now refuses the request before it reads any old-wallet data.
wipe-explicit-control.2x.mp4
The overlay is labelled as the control build. The wait while our hold blocked wallet B is cut and the rest sped up. I checked the outcomes in the event log.
command manual-settings
capture-start SETTINGS
captured SETTINGS scheduled=false
held explicit-upload
released wipe
wipe-done
released explicit-upload
backup-done SETTINGS result=failed
A backup requested while backups restart still completes ✅ An explicit backup made during a stop completed after the restart. A cancelled one did not upload.
With the stop held in scheduled-job cleanup, an explicit METADATA backup waited. It completed once the stop drained and observation restarted (manual-done result=completed). A second request, cancelled while the stop drained, ended failed with no METADATA capture or upload.
stop-manual.2x.mp4
I checked the outcomes in the event log.
The auth-outage, auth-refusal and USDT-outage cases from our 11fb65d review were not re-driven, because the merge does not change the failure classifier. Reset then new wallet was re-driven here, inside the reset case above. Reset then restore was driven on #1121's own tree at 25f25ea in our #1121 review, and not re-driven on this merged tree. jvsena42's re-approval at c75acf3 reads the same guard and reports no findings.
Review scope: the 11fb65d..c75acf3 delta. The merge's own changes are in BackupService.swift, its tests and the changelog. No device case drove retryPendingBackups' new generation capture on foreground; the unit run covers it.
Twin: bitkit-android#1689 at 1a6048a matches in behaviour. It binds explicit backups to an owner generation and drops ones from a retired wallet. Its mechanism is different: it ties ownership to the VSS client owner, not to a wipe counter.
Added local controls (on top of the 11fb65d controls, not pushed)
--- a/Bitkit/Utilities/AppReset.swift
+++ b/Bitkit/Utilities/AppReset.swift
await BackupService.shared.stopObservingBackups()
+ ReviewBackupControl.log("wipe-stop-done") // test hook
try await VssBackupClient.shared.withSuspendedClients {
+ await ReviewBackupControl.hold("wipe") // test hook
try await wipeWithClientsSuspended(app: app, wallet: wallet, session: session, toastType: toastType)
}
+ ReviewBackupControl.log("wipe-done") // test hook
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
let data = try await getBackupDataBytes(category: category)
+ ReviewBackupControl.log("captured \(category.rawValue) scheduled=\(isScheduled)") // test hook
+ if !isScheduled { await ReviewBackupControl.hold("explicit-upload") } // test hook
--- a/Bitkit/Services/ReviewBackupControl.swift
+++ b/Bitkit/Services/ReviewBackupControl.swift
+ case "manual-settings": Task { // test hook
+ let result = await service.triggerBackup(category: .settings) // test hook
+ log("manual-settings-done result=\(result)") // test hook
+ } // test hookThe 11fb65d review's VssBackupClient route and auth-relay hooks were not used.
Reviewed by claude-opus-5-5-medium
There was a problem hiding this comment.
Verdict: ✅ Approve for the backup-lifecycle delta.
Findings: None.
QA for f514d19: iPhone 17 Pro simulator, iOS 26.5, Xcode 27.0, disposable unfunded wallets and staging VSS. No payments were made.
The focused unit run used the unmodified head. App cases used the previously disclosed timing and command controls, with the reset pause moved inside AppReset.withLocalStateWipeScope. The outage case used the disclosed VSS endpoint switch. Native capture, authentication, encryption and backup guards were unchanged.
Backup observation stop order and wallet ownership ✅ All 111 focused tests passed without failures or skips.
The selected backup, foreground recovery, VSS, initialization and wipe suites passed, including the six new observation-order/drain tests. Results were checked in the xcodebuild log and XCResult.
lifecycle-units.2x.mp4
The recording shows the test host; the assertions are checked in the test results.
Manual backups wait for cleanup and respect cancellation ✅ The waiting request completed; the cancelled request captured and uploaded nothing.
Manual 1: an explicit METADATA request waited while scheduled cleanup was held, then completed after the drain and restart. A second request cancelled during that drain returned failed without METADATA capture or upload. Checked in the event log; a second recorded execution also passed.
stop-manual.2x.mp4
The overlay shows the local controls' event log.
Edits during a stopped upload remain pending ✅ SETTINGS and WALLET edits survived the drain and uploaded after restart.
Manual 1: with an acknowledged SETTINGS put held before return, I stopped observation and changed settings plus a previously synced watch-only record; both stayed required through shutdown. After restart, decrypted SETTINGS and the watch-only account list matched local state.
drain-edits.2x.mp4
Outcomes were checked in the event log and native VSS readback.
Stopped backup observation ignores edits and restarts once ✅ Three cycles preserved stopped status, and one fresh edit produced one WALLET upload.
Manual 3: post-stop wallet edits left saved backup status unchanged and produced no capture, while a settings marker received before stop survived. After restart, one fresh wallet edit uploaded once and the selected readbacks matched local state.
stopped-listeners.2x.mp4
Outcomes were checked in the event log and native VSS readback.
USDT recovery works while ordinary backup observation is stopped ✅ Recovery backups completed after cleanup and a VSS connection outage.
Manual 2 stop/retry path: a USDT backup waited out ordinary backup cleanup and completed with observation stopped. A native putObjects connection refusal stayed pending and scheduled the 10-second retry; restoring the endpoint let it complete without another request or observation restart.
Checked in event logs and native acknowledgements; the second recorded execution also passed. This was backup execution, without a payment or restore.
usdt-cleanup.2x.mp4
The overlay shows the local controls' event log.
Reset refuses old-wallet backups and backs up the new wallet ✅ Reset refused capture, and the new wallet reached Home and synced its own settings.
With the old wallet’s swipe-to-hide off and synced, an explicit SETTINGS request during the paused reset returned failed before capture. After reset, the new wallet reached Home, every category synced, and decrypted SETTINGS matched its own default on value.
Checked in the UI, event log and native VSS readback. This also checks Manual 4’s ordinary new-wallet startup; its progress-normalization race is covered by the focused unit run.
The first attempt ended before this readback because the harness accepted cleared zero statuses too early. The complete repeat required nonzero synced revisions before checking completion.
reset-new-wallet.2x.mp4
The recording shows the reset and new-wallet flow; backup outcomes were checked in the event log and VSS reads.
The auth-relay recovery and invalid-challenge cases from the 11fb65d review were not re-driven: the classifier and dependency pins are unchanged. A genuine server authorization rejection was not driven; the typed-denial branch passed its unit test.
Review scope: the c75acf3..f514d19 delta, including the backup/wipe integration with bf4061f. Other inherited parent features were not device-tested here.
These cases do not close the parent's terminal complete-WALLET finding or establish running-LDK persistence quiescence. WALLET readback above checks the watch-only account list, not the complete envelope or restoration.
Pair PR: bitkit-android#1689 fix: preserve pending backups across restart at 1cf5d39 matches the intended pending-backup and retired-wallet protection. Android retains native owner handles; iOS uses wipe generations and queued observation transitions.
Reviewed by gpt-6.1-sol-high
There was a problem hiding this comment.
Verdict: ✅ Approve for the cleanup-admission delta.
Findings: No new findings. The parent's terminal complete-WALLET backup finding remains open.
QA for 003ea7e: iPhone 17 Pro simulator, iOS 26.5, Xcode 27.0. Built the unmodified head with committed package revisions; the installed app matches the build.
Expired cleanup cannot modify a replacement wallet ✅ The suspended-read and link-recovery regressions passed, alongside valid-cleanup and cancellation controls.
PaykitSdkIdentityCheckTests, PaykitSdkOperationLockTests and AppResetTests passed without failures or skips. The five cleanup cases hold registry, backup-revision, capability or peer reads, or an already-started link recovery, until the wipe ends and a replacement SDK exists. No subsequent mutation reaches either SDK and the old key-generation entry stays deleted. Current-session cleanup remains usable, and already-admitted writes retain their drain behavior.
Assertions were checked in finalized XCResult and XCTest output. The recorded repeat used the same unmodified build and also passed. These are app-hosted tests with isolated SDK fakes.
cleanup-regressions.2x.mp4
Simulator capture at 2× during test-host installation and execution; the visible UI does not show the assertions. The first passing run had no recording because capture setup failed.
Check: retained device evidence ✅ Earlier successful reports cover the unchanged lifecycle and cleanup boundaries.
- Manual 1–4's previously exercised boundaries, cancelled manual admission, edits during shutdown, stopped observation, USDT outage/retry and reset/new-wallet startup retain the f514d19 report. Backup observation, reset, initialization and package inputs are unchanged in this delta.
- The inherited cleanup/reset boundary retains the 485a9dc parent report: a real SDK revision response was held beyond the cleanup deadline, reset finished, and releasing old work after replacement-wallet creation stopped at cancelled admission while Receive remained usable. The cleanup, deadline, ownership, reset and Receive sources match; this is attributed parent execution, not a fresh app journey on this child.
- Authentication transport recovery and invalid-challenge refusal retain the 11fb65d report; the classifier and dependency pins are unchanged. Genuine server authorization refusal was not driven on iOS.
No selected delta case remains unrun. No fresh authenticated app journey, populated-wallet restore, payment replay or running-LDK persistence-quiescence test was driven. The guards prevent another mutation from starting; they do not roll back a native mutation already in flight.
Reviewed the contiguous f514d19..003ea7e delta, all three changed files and affected callers. Changed-file SwiftFormat 0.64.0 and whitespace checks pass. The changelog received source inspection only.
Pair PR: bitkit-android#1689 fix: preserve pending backups across restart at 1cf5d39 matches pending-backup retention and retired-wallet protection. Android uses native owner generations; iOS queues observation transitions and checks wipe generations. This delta hardens iOS SDK cleanup admission; Android device testing was not repeated.
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 003ea7e8 against merge base 485a9dc1. The comparison base changed and touches reviewed backup paths; the entire PR and affected callers were reassessed without inherited coverage. Android #1689 and iOS #1121/#1122 received targeted contract comparisons only.
No new actionable code findings.
The shutdown lost-edit thread is resolved and its mechanism is fixed in the pinned source. The separate terminal identity restoration/complete-WALLET concern remains open and present at both base and head: later ordinary wallet changes can remain pending until identity recovery. This PR does not resolve it.
Validation: local git diff --check passed. Inspected native CI logs, including passing backup, foreground recovery, initialization, activation and affected USDT navigation tests, and SwiftFormat. Their merge commit contains the pinned head/base and has the same tree as the reviewed head. Native recovery/funded-fork fixtures were skipped; integration and Appium acceptance were not established.
Device testing: not performed in this review. Swift/Xcode are unavailable locally. Manual cases 1–4 remain the acceptance plan; earlier reported executions retain their original revisions and fixture limits.
Suggested additional test cases
- iOS, disposable wallet and authenticated fault fixture: make the LNURL-auth server explicitly deny authorization during a scheduled SETTINGS backup. Observe beyond the initial 10-second retry interval; expect no automatic app retry and a retained pending marker. This checks genuine denial separately from the previously reported invalid-challenge case.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
There was a problem hiding this comment.
Verdict: 💬 Comment — the metadata-resumption delta is verified; inherited parent findings remain open.
Findings: No new actionable findings. The parent's terminal-identity complete-WALLET failure and stalled-authentication drain concern remain. Their implementation and dependency pins are unchanged here; these results do not close them.
QA for 675f8bd: iPhone 17 Pro simulator, iOS 26.5, Xcode 27.0, committed dependency pins, disposable unfunded staging-regtest wallet. Native checks used the unmodified head. App cases used the local Debug controls described below; all 128 installed bundle files matched the controlled build.
Backup lifecycle, metadata recovery and payment regressions ✅ The focused native checks passed without failures or skips, as checked in XCTest output and finalized XCResult.
Built and ran xcodebuild test-without-building for backup scheduling/foreground recovery, cached Blocktank capture, VSS coordination, initialization, wipe/reset, SDK identity/operation locking, private/public contact payments, Add Contact, scanned-payment routing, send preparation and USDT links. The new metadata-retirement test covers active observation and stopping before retirement. SwiftFormat 0.64.0 passed on changed Swift files; translation validation passed with 17 warnings. Both native executions passed the same 431 tests without skips. Each result retains one QoS runtime warning, without an established cause. Optional simulator diagnostic collection was stopped after the final tests finished, and Xcode finalized successfully. This is not a complete simulator-diagnostics claim.
focused-native.2x.mp4
Clean-head test-host recording at 2×; assertions are checked in the test results. The initial passing execution had only a partial recording; the complete recorded repeat uses the unmodified head after removing the app controls.
Pending Tags back up after a failed wallet backup recovers ✅ Recovering WALLET resumed Tags after the failed metadata job released its slot; decrypted VSS readback matched the new tag.
Injected a permanent WALLET upload failure and changed the stored recent tag. METADATA failed on its WALLET prerequisite and remained pending. Held its scheduled-job retirement, removed the failure and explicitly backed up WALLET. No METADATA upload occurred while its old slot was held. Releasing retirement automatically uploaded METADATA without another tag edit; both categories became synced.
Checked in timestamped control logs and native encrypted VSS write/readback. The unchanged scheduler used its normal debounce. This passed twice; the recording is the repeat without the text overlay.
metadata-recovery.2x.mp4
Data Backups at 2×; the held-slot ordering and payload equality are checked in logs and native readback.
Stopping observation prevents retired Tags work from restarting ✅ Tags stayed pending after stop and uploaded only after observation restarted, with matching remote readback.
Repeated the held failed-METADATA retirement and recovered WALLET. Stop waited for the held job. After release, stop completed and no METADATA capture or upload occurred during a seven-second stopped interval, beyond the normal debounce. Restart then uploaded the pending tag without another edit.
metadata-stop.2x.mp4
Data Backups at 2×; stop ordering, the quiet interval and payload equality are checked in the control log and native VSS readback.
Explicit authorization denial leaves Settings pending without automatic retry ✅ A signed native callback received an authorization denial; Settings remained pending with no scheduled app retry for 25 seconds after failure.
The local LNURL fixture issued a valid challenge and returned {"status":"ERROR","reason":"permission denied by disposable QA authorization fixture"} to the native signed callback. The released native client produced AuthError("authorization was refused: LNURL Auth failed, reason is: …"). Its internal retries made six challenge/callback pairs within the single scheduled backup attempt. After that attempt returned failed, there was no further SETTINGS capture or app retry during the 25-second observation, and the pending marker remained idle.
Restoring normal staging authentication and making the next settings edit uploaded successfully; decrypted VSS settings matched. This supplies the distinct denial case requested in the previous QA review.
auth-denial.2x.mp4
Data Backups at 2×; the callback response, attempt count, observation interval and final readback are checked in the fixture/control logs.
Check: retained lifecycle and incoming payment evidence ✅ Earlier successful reports cover the unchanged paths; source comparisons establish applicability.
- Manual 1–4 retain the f514d19 lifecycle report: manual admission/cancellation, edits during shutdown, stopped listeners, USDT outage/retry and reset/new-wallet startup. Their stop/listener, initialization, reset, VSS and package inputs remain unchanged; the new metadata callbacks receive fresh coverage above.
- Authentication transport recovery and invalid-challenge refusal retain the 11fb65d report. The failure classifier and native pins match; genuine callback denial is newly driven above.
- Saved-contact pending behavior, mutual-save recovery and private resolution retain the e6956960 device report. The private resolver, Add Contact and journey match; the public-payment entry function also matches despite unrelated publication changes elsewhere in that file.
- Fixed-amount route/view/scanner sources match
1730836; the relevant amount/coin methods also match despite unrelated wallet-lifecycle changes. Retained reports:
All selected cases passed. No new populated-wallet restore, funded USDT send, hardware signing, private-link fault injection, no-Noise peer, narrow funding-switch race, payment/proof replay or running-LDK quiescence result is claimed. The metadata readback checks the changed tag, not complete-WALLET restoration. The 30-minute notification window and stalled-auth reset were not driven.
Coverage: contiguous 003ea7e..675f8bd, all 47 changed files, reached callers and prior unresolved findings. Primary and independent review found no new defect. Localization, changelog and journey text received source checks; inherited payment journeys were credited from the linked reports.
Twin: bitkit-android#1689 fix: preserve pending backups across restart at 1cf5d39 matches pending-backup retention, draining and retired-wallet protection. Android already rearms pending metadata after WALLET success through revision updates and its shared mutex; iOS closes the corresponding occupied-job-slot window with a retirement callback. Android device testing was not repeated.
Local Debug controls used for the app cases
Not pushed. Controls invoke production backup/observation APIs, change a disposable recent tag or setting, hold the existing waitForScheduledBackupCompletion injection point, and read real VSS payloads. Only the metadata cases inject a typed permanent WALLET upload error. The denial case instead routes authentication to a local HTTP challenge/callback fixture; recovery forwards the real staging challenge unchanged. Capture, encryption, normal debounce/backoff, cancellation, generations and scheduling decisions remain unchanged.
// BackupService's upload closure: only while the metadata failure control is enabled.
if ReviewBackupControl.enabled, key == "WALLET",
FileManager.default.fileExists(atPath: ReviewBackupControl.directory.appendingPathComponent("wallet-deny").path) {
throw VssError.AuthError(errorDetails: "permission denied by local metadata recovery control")
}
_ = try await VssBackupClient.shared.putObject(key: key, data: data)
// Existing scheduled-completion injection point, before job-map retirement.
if category == .metadata, outcome == .walletDependencyFailed {
await ReviewBackupControl.hold("metadata-retirement")
}The command control marks WALLET required, writes a recent tag and marks METADATA required, requests explicit WALLET backup, starts/stops observation, clears the cached VSS client before changing the auth fixture, and compares decoded remote fields. Logging observes capture, acknowledgement, outcome and retry delay. A text overlay was used in the stopped-generation recording. The final native run restores every source file and removes the command control.
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 675f8bd, covering the entire PR against merge base 1c3cde65 after the comparison base changed. No coverage was inherited. Android #1689 and iOS #1121/#1122 received targeted contract comparisons only.
No new actionable code findings.
The shutdown lost-edit thread is resolved and its fix remains present. The parent's terminal identity/WALLET availability concern and stalled-authentication wipe drain remain open and present at both base and head. They require separate remediation; the pinned VSS dependency does not contain the authentication deadline fix.
Validation: inspected passing backup, foreground recovery, initialization, activation and affected USDT navigation results in native CI, plus SwiftFormat. The tested merge contains the exact pins and has the head's tree. Local diff checking and 24 Swift URL predicate checks passed; the latter used Linux Foundation with an IPv6 shim, not the iOS app. Native recovery/funded-fork fixtures were skipped; integration and Appium acceptance were not established.
Device testing: not performed in this review. Xcode/iOS SDK are unavailable locally. Manual cases 1–4 remain the acceptance plan; attributed earlier runs retain their revisions and fixture limits.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
There was a problem hiding this comment.
Verdict: 💬 Comment — the restart delta is verified; inherited parent findings remain open.
Findings: No new actionable defect in this delta. These existing parent findings remain open and are unchanged here:
- Terminal identity recovery blocks complete-WALLET backups.
- Stalled authentication delays the wipe drain.
QA for 61537eb: iPhone 17 Pro simulator, iOS 26.5, Xcode 27.0, committed dependency revisions and a disposable unfunded regtest wallet. Native tests used the unmodified head. App cases used the local Debug controls described below; both controlled installations matched their complete built bundles.
Backup stop, restart and metadata regression tests ✅ Focused native assertions passed without failures or skips, checked in XCTest output and finalized XCResult.
Built the head and ran BackupServiceTests, BackupForegroundRecoveryTests, VssClientCoordinatorTests, AppResetTests, WipeCoordinatorTests, WalletViewModelWipeTests and InitializingWalletViewTests. All three new regressions passed: delayed-backup restart, failed-METADATA retirement across restart, and cancelled METADATA capture draining before restart. The result contains no runtime-warning entry. Changed-file SwiftFormat 0.64.0 and whitespace checks also passed.
native2x.2x.mp4
Simulator recording at 2× during app-hosted tests; the assertions are established by the test output and XCResult, rather than the visible screen.
Delayed Settings backup resumes after stop and restart ✅ The cancelled reservation cleared, the pending revision survived, and restart uploaded once without another edit, checked in logs and VSS readback.
Held the scheduled debounce, stopped observation and queued restart. Both transitions waited for release. During the drain, SETTINGS had no scheduled reservation and was pending with running=false. After release, the replacement uploaded once and decrypted SETTINGS matched local state.
delayed-restart2x.2x.mp4
Simulator at 2×; timing, reservation state and payload equality were checked in the control log and native VSS readback.
Retiring Tags backup cannot erase its replacement ✅ Stop and restart drained the retiring job, then the pending tag uploaded once with matching remote readback.
Injected a permanent WALLET upload failure and held the failed METADATA job at retirement. Recovered WALLET, requested stop and restart, and checked that METADATA's reservation was absent while both transitions waited. Releasing retirement admitted the replacement; METADATA uploaded once without another tag edit. Decrypted native VSS readback matched the saved recent tag.
metadata-restart2x.2x.mp4
Data Backups at 2×; ordering, upload count and tag equality were checked in the control log and native VSS readback.
Cancelled Tags capture drains before restart ✅ The cancelled capture made no upload; its replacement uploaded the pending tag after the drain, checked in logs and native readback.
Held METADATA capture after reading its app cache, then requested stop and restart. The pending revision survived and neither transition completed before release. The cancelled capture uploaded nothing; after restart one replacement uploaded and decrypted METADATA matched the recent tag.
metadata-read2x.2x.mp4
Data Backups at 2×; capture ordering and tag equality were checked in the control log and native VSS readback.
Manual backup and new edits wait for shutdown cleanup ✅ Manual admission waited, a cancelled request captured nothing, and newer Settings and Wallet edits survived, checked in logs and native readback.
Manual 1: held scheduled retirement, requested stop and a manual METADATA backup, queued restart, then edited settings and a synthetic watch-only record. No new capture started before release; the waiting manual backup subsequently completed. A second manual request cancelled during a held drain returned failed without METADATA capture.
Separately held a successful SETTINGS acknowledgement before caller return, stopped observation, and edited SETTINGS plus a previously synced WALLET record. Both remained pending after the drain. No new capture began during the stopped interval; restarting uploaded the edits, and decrypted SETTINGS and the watch-only account list matched local values.
manual-drain2x.2x.mp4
Data Backups at 2×; admission, cancellation, pending markers and selected-field payload equality were checked in the control log and VSS readback.
USDT backup retries while observation is stopped ✅ Recovery waited for cleanup and recovered automatically from native connection refusal, with matching encrypted VSS readback.
Manual 2: used a real, unfunded native USDT snapshot with chain and bundler endpoints pointed at an unreachable local endpoint. A new recovery backup waited for ordinary shutdown cleanup, then received a staging-VSS acknowledgement while observation stayed stopped.
A second request encountered a native VSS connection refusal, remained pending and scheduled the existing ten-second retry. Restoring only the VSS endpoint allowed it to complete without another request or observation restart. Reading the content-addressed recovery records through the native client found the current local snapshot. No payment or chain transaction was made.
stopped-usdt2x.2x.mp4
Data Backups at 2×; retry timing, native acknowledgement and snapshot equality were checked in the control log and VSS readback.
Stopped listeners ignore edits and restart once ✅ Three cycles preserved stopped wallet status, retained pre-stop Settings changes and produced one upload after a fresh wallet edit, checked in logs.
Manual 3: after each completed stop, changed the synthetic watch-only record and checked unchanged saved WALLET status with no WALLET capture. A Settings change received immediately before each stop remained pending and uploaded after restart. After the final restart, one fresh wallet edit produced one WALLET acknowledgement; decrypted SETTINGS and the watch-only account list matched local state.
stopped-listeners2x.2x.mp4
Data Backups at 2×; status comparisons, upload count and selected-field equality were checked in logs and native VSS readback.
Authorization denial leaves Settings pending ✅ A signed native callback received an explicit denial; Settings stayed pending without an app retry for 25 seconds, checked in fixture and control logs.
The local LNURL fixture issued a valid challenge and returned status=ERROR with a permission-denied reason to the signed callback. The native client retried internally within one scheduled SETTINGS attempt. After that attempt failed, the pending marker remained idle and no second SETTINGS capture or app retry occurred during the 25-second observation.
Restored normal staging authentication, reset the native client and made the next settings edit. The upload succeeded and decrypted SETTINGS matched local state. Both executions passed; the recording is the repeat.
auth-denial2x.2x.mp4
Data Backups at 2×; signed callback denial, observation interval and recovered payload equality were checked in fixture/control logs and native VSS readback.
Check: retained initialization and payment evidence ✅ Source comparisons preserve the earlier successful reports for paths outside this two-file delta.
- Manual 4's controlled progress/Running transition retains the earlier lifecycle report. Initialization source is unchanged, and both initialization regressions also passed in the fresh native run above.
- Incoming-payment and fixed-amount evidence retains the previous review's linked reports and source comparisons. Those production paths, native pins and test inputs are unchanged in this delta.
No new payment, populated-wallet restore or running-LDK persistence-quiescence result is claimed. WALLET readback here checks watch-only accounts, rather than complete-envelope restoration. The inherited terminal-WALLET and stalled-authentication findings are not closed by these passing cases.
Coverage: contiguous 675f8bd..61537eb, both changed files and reached admission, generation, cancellation, writer and lifecycle callers. Primary and independent code review found no new defect. No selected case remains unrun. The new DEBUG task-inspection accessor was exercised by native tests and app controls; this delta changes no UI or backup format.
Twin: bitkit-android#1689 fix: preserve pending backups across restart at 1cf5d39 matches pending-work retention, draining and retired-owner refusal. Android uses native owner/job identity and mutexes; iOS uses ordered observation drains and scheduling/wipe generations. Android device testing was not repeated.
Local Debug controls
Controls invoke production backup/start/stop APIs, edit a disposable recent tag, setting or synthetic watch-only record, and inspect existing scheduled reservations. Timing holds sit at the debounce, scheduled-retirement callback, acknowledged SETTINGS return and METADATA app-cache capture. Only the failed-WALLET setup injects a typed permanent upload error. The outage changes only the VSS endpoint; the denial case uses an HTTP LNURL challenge/callback fixture. Normal capture, cancellation checks, debounce/backoff, authentication, encryption and scheduling decisions remain in place. USDT uses native local snapshot export and real encrypted VSS records.
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 entire pinned PR at 61537eb against eff0dadf, also its merge base, after the comparison base changed. No coverage was inherited. Android #1689 and iOS #1122/#1121 received comparison-only checks of backup lifecycle/retry, session-backup and VSS/wipe contracts.
No new actionable code findings.
The resolved shutdown lost-edit report remains fixed. Two parent concerns remain open and present at both base and head: terminal identity failure prevents current WALLET/METADATA backups, and stalled native authentication holds the wipe drain. Recovery while credentials remain unusable and adoption of a bounded authentication implementation remain outstanding.
Validation: inspected passing affected backup, foreground, initialization, activation and USDT navigation results in native CI. Its merge has exactly the pinned parents and the head's tree; one USDT UI fixture was skipped. Local diff checking, syntax parsing of all changed Swift files and 40 focused classifier assertions passed. The classifier probe used Linux Foundation, stub error enums and a POSIX IPv6 shim; no local native build or XCTest run was possible without Xcode/iOS SDK.
Device testing: not performed in this review. The current-head external lifecycle results remain attributed tests using disclosed Debug controls; they do not establish complete-WALLET restoration or close the parent concerns.
Reviewer model: gpt-6.1-sol
Reasoning effort: high
jvsena42
left a comment
There was a problem hiding this comment.
Delta 61537ebd5..6c40b8d5f: no findings. The three UI tests now launch with -appUpdateIgnoreTimestamp set to now, so the optional update sheet stays deferred and can't cover Home. This is test-only.
Reviewer model: Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
There was a problem hiding this comment.
Verdict: 💬 Comment — timeout retry is verified at 5f8a226. The head advanced to 6c40b8d during testing; this review covers 5f8a226.
The PR moved to 6c40b8d after this run; these results are for 5f8a226.
Findings: No new actionable defect. These existing concerns remain open at the reviewed head:
- Terminal identity recovery blocks complete-WALLET backups.
- Stalled native authentication delays the wipe drain.
QA for 5f8a226: iPhone 17 Pro simulator, iOS 26.5, Xcode 27.0, committed dependency revisions and a disposable unfunded regtest wallet. Native and UI tests used the unmodified head. The two backup app cases used the DEBUG controls below; all 125 installed bundle files matched their build.
Pending backups retry after VSS setup times out ✅ Focused native regressions passed without failures or skips, checked in XCTest output and the finalized XCResult.
Built with xcodebuild build-for-testing, then ran test-without-building for BackupServiceTests, BackupForegroundRecoveryTests and VssClientCoordinatorTests. This includes the new stopped WALLET/USDT setup-timeout regression, permanent authorization denial, bounded backoff, cancellation, restart and retired-owner checks. Changed-file SwiftFormat 0.64.0 and whitespace checks passed.
native2x.2x.mp4
Simulator recording at 2×; assertions are established by the test output and XCResult.
Fixed USDT amount survives returning to the recipient ✅ The fixed amount opened review, header Back returned to the recipient, and the retained amount became editable, checked in the recorded UI test and XCTest assertions.
testReceiveAndInvalidPaymentStaySeparateFromBitcoin passed unchanged. It checks $ 1.23 without a keypad on fixed-amount review, selects the Confirm header's Back button, changes the recipient and verifies editable $ 1.234567 with six-decimal precision. The receive and invalid-Bitcoin-address assertions also passed; no payment was submitted.
The initial attempt did not reach USDT: the onboarding Continue tap opened Safari and the Home prerequisite timed out. After completing first-run setup, the entire unchanged test passed.
usdt-navigation2x.2x.mp4
Passing XCTest run at 2×. The recording has capture gaps; the 2× copy preserves every captured changed frame. A separate manual replay also reached the expected amount but had more capture gaps.
Wallet backup retries after VSS setup times out ✅ The pending watch-only edit survived the typed timeout and uploaded after one automatic retry, checked in logs and native encrypted VSS readback.
Stopped observation, reset the app VSS client, held the real setup operation and changed an inactive synthetic watch-only record. The production 30-second timer fired while setup was held. Releasing the hold after about 36 seconds let setup drain and return AppError with SetupTimeout; WALLET remained required and idle. The unchanged ten-second backoff then admitted the replacement without another request or observation restart. One acknowledgement followed, and decrypted WALLET readback matched the edited watch-only accounts.
The final run first established Settings upload/readback equality. An earlier additional Settings comparison lacked that baseline and failed; that attempt is not credited.
setup-held
setup-released
setup-error typedTimeout=true
backup-done WALLET result=temporarilyUnavailable
retry WALLET delay=10000000000
upload-ack WALLET
verify-settings matches=true
verify-wallet matches=true
timeout-wallet2x.2x.mp4
Data Backups at 2×; typed error, ordering, retry delay and payload equality were checked in the control log and native VSS readback.
USDT recovery backup retries after VSS setup times out ✅ The native snapshot stayed pending through the typed timeout and recovered automatically, with matching encrypted VSS readback.
With observation still stopped, reset VSS and requested a scheduled USDT backup. Held setup beyond the unchanged 30-second timer, then released it after about 36 seconds. The typed timeout left the recovery backup pending; the existing ten-second retry acknowledged its content-addressed record without another request or Lightning restart. Native readback contained the current local snapshot. Chain and bundler endpoints were unreachable local endpoints, and no transaction was submitted.
setup-held
setup-released
setup-error typedTimeout=true
backup-done USDT_RECOVERY result=temporarilyUnavailable
retry USDT_RECOVERY delay=10000000000
usdt-recovery-ack
verify-usdt matches=true count=1
timeout-usdt2x.2x.mp4
Data Backups at 2×; pending state, retry timing, native acknowledgement and snapshot equality were checked in logs and native VSS readback.
Check: retained lifecycle evidence ✅ Source comparison preserves the earlier successful cases outside the timeout change.
The previous review at 61537eb establishes manual admission and drain edits, stopped listeners, metadata retirement/cancellation, explicit authorization denial and initialization. Their decisions and dependency pins are unchanged here. The new setup-timeout path received the fresh app cases above.
No selected case remains unrun. These results do not establish complete-WALLET restoration, terminal identity recovery or a hard native-authentication deadline. The timeout returns after its operation drains; the inherited native-authentication wipe concern remains separate. Dedicated-LDK Debug setup was inspected, not driven.
Coverage: contiguous 61537eb..5f8a226, all four changed files, reached setup/error wrapping, scheduling, cancellation and navigation paths, plus prior open findings. Primary and independent code review found no new defect. This delta changes no backup format or payment behavior.
Twin: bitkit-android#1689 fix: preserve pending backups across restart at be4e1ba also treats its typed setup timeout as temporary while retaining owner/cancellation checks. Android device testing was not repeated.
Local DEBUG controls
Not pushed. Controls invoke existing backup/reset/stop APIs, edit an inactive synthetic watch-only record, inspect pending status and compare native decrypted VSS payloads. The hold sits at the start of the real setup operation and waits for explicit release; the production timer and backoff remain unchanged. No SDK result or error is injected. This exercises timeout classification and recovery after setup drains, not cancellation of a stalled native HTTP request.
diff --git a/Bitkit/BitkitApp.swift b/Bitkit/BitkitApp.swift
index 03273d8a..4a67bd99 100644
--- a/Bitkit/BitkitApp.swift
+++ b/Bitkit/BitkitApp.swift
@@ -83,6 +83,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
}
+ ReviewBackupControl.start() // test hook
return true
}
diff --git a/Bitkit/Services/BackupService.swift b/Bitkit/Services/BackupService.swift
index fcb76ce6..992bcff5 100644
--- a/Bitkit/Services/BackupService.swift
+++ b/Bitkit/Services/BackupService.swift
@@ -167,7 +167,9 @@ class BackupService {
backupData: ((BackupCategory) async throws -> Data)? = nil,
usdtRecoveryBackup: UsdtRecoveryBackupStore = UsdtRecoveryBackupStore(),
uploadBackup: @escaping (String, Data) async throws -> Void = { key, data in
+ ReviewBackupControl.log("upload-start \(key)") // test hook
_ = try await VssBackupClient.shared.putObject(key: key, data: data)
+ ReviewBackupControl.log("upload-ack \(key)") // test hook
},
waitForBackupDelay: @escaping () async throws -> Void = {
try await Task.sleep(nanoseconds: 5_000_000_000)
@@ -518,6 +520,7 @@ class BackupService {
} else if category == .metadata, outcome == .walletDependencyFailed {
await resumeMetadataAfterWalletBackup(generation: generation)
}
+ ReviewBackupControl.log("backup-done \(category.rawValue) result=\(outcome)") // test hook
return outcome
}
@@ -1123,6 +1126,7 @@ class BackupService {
while !Task.isCancelled {
do {
if outcome == .temporarilyUnavailable {
+ ReviewBackupControl.log("retry \(category.rawValue) delay=\(retryDelay)") // test hook
try await waitForTemporaryFailureDelay(retryDelay)
retryDelay = min(retryDelay * 2, 300_000_000_000)
} else {
diff --git a/Bitkit/Services/UsdtRecoveryBackupStore.swift b/Bitkit/Services/UsdtRecoveryBackupStore.swift
index f804d076..9a0bd8d2 100644
--- a/Bitkit/Services/UsdtRecoveryBackupStore.swift
+++ b/Bitkit/Services/UsdtRecoveryBackupStore.swift
@@ -27,7 +27,9 @@ struct UsdtRecoveryBackupStore {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(UsdtRecoveryBackupV1(version: 1, usdtWallet: snapshot, paykitUsdtState: paymentState))
+ ReviewBackupControl.log("usdt-recovery-put") // test hook
try await put(Self.key(for: data), data)
+ ReviewBackupControl.log("usdt-recovery-ack") // test hook
}
func snapshots() async throws -> [UsdtRecoveryBackupV1] {
diff --git a/Bitkit/Services/VssBackupClient.swift b/Bitkit/Services/VssBackupClient.swift
index d884743f..6c3973d9 100644
--- a/Bitkit/Services/VssBackupClient.swift
+++ b/Bitkit/Services/VssBackupClient.swift
@@ -169,6 +169,7 @@ class VssBackupClient {
private func setup(walletIndex: Int = 0) async throws {
do {
try await withSetupTimeout { [self] in
+ await ReviewBackupControl.holdSetup() // test hook
Logger.debug("VSS client setting up…", context: "VssBackupClient")
let vssUrl = Env.vssServerUrl
Logger.debug("Building VSS client with vssUrl: '\(vssUrl)'", context: "VssBackupClient")
@@ -186,6 +187,7 @@ class VssBackupClient {
Logger.info("VSS client setup with server: '\(vssUrl)'", context: "VssBackupClient")
}
} catch {
+ ReviewBackupControl.log("setup-error typedTimeout=\((error as? AppError)?.underlyingError is SetupTimeout)") // test hook
Logger.error("VSS client setup error: \(error)", context: "VssBackupClient")
throw error
}
diff --git a/Bitkit/Services/ReviewBackupControl.swift b/Bitkit/Services/ReviewBackupControl.swift
new file mode 100644
index 00000000..6692adf2
--- /dev/null
+++ b/Bitkit/Services/ReviewBackupControl.swift
@@ -0,0 +1,96 @@
+#if DEBUG // test hook
+import Foundation // test hook
+import BitkitCore // test hook
+import LDKNode // test hook
+ // test hook
+enum ReviewBackupControl { // test hook
+ static let enabled = ProcessInfo.processInfo.environment["REVIEW_BACKUP_CONTROL"] == "1" // test hook
+ static let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("review-backup") // test hook
+ static let lock = NSLock() // test hook
+ static func log(_ message: String) { // test hook
+ guard enabled else { return } // test hook
+ lock.withLock { // test hook
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) // test hook
+ let file = directory.appendingPathComponent("events.log") // test hook
+ var data = (try? Data(contentsOf: file)) ?? Data() // test hook
+ data.append(Data("\(Date().timeIntervalSince1970) \(message)\n".utf8)) // test hook
+ try? data.write(to: file) // test hook
+ } // test hook
+ } // test hook
+ static func holdSetup() async { // test hook
+ guard enabled else { return } // test hook
+ let file = directory.appendingPathComponent("hold-setup") // test hook
+ guard FileManager.default.fileExists(atPath: file.path) else { return } // test hook
+ log("setup-held") // test hook
+ while FileManager.default.fileExists(atPath: file.path) { // test hook
+ await Task.detached { try? await Task.sleep(nanoseconds: 100_000_000) }.value // test hook
+ } // test hook
+ log("setup-released") // test hook
+ } // test hook
+ @MainActor static func start() { // test hook
+ guard enabled else { return } // test hook
+ log("control-ready head=5f8a226") // test hook
+ Task { // test hook
+ let end = Date().addingTimeInterval(7200) // test hook
+ while Date() < end { // test hook
+ let file = directory.appendingPathComponent("command") // test hook
+ if let command = try? String(contentsOf: file, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines) { // test hook
+ try? FileManager.default.removeItem(at: file) // test hook
+ let service = BackupService.shared // test hook
+ log("command \(command)") // test hook
+ switch command { // test hook
+ case "stop": Task { await service.stopObservingBackups(); log("stop-done") } // test hook
+ case "start": Task { await service.startObservingBackups()?.value; log("start-done") } // test hook
+ case "auth-reset": Task { await VssBackupClient.shared.reset(); log("auth-reset-done") } // test hook
+ case "settings": SettingsViewModel.shared.swipeBalanceToHide.toggle(); service.markBackupRequired(category: .settings) // test hook
+ case "wallet": // test hook
+ do { // test hook
+ var records = try WatchOnlyAccountStore.load() // test hook
+ if records.isEmpty { // test hook
+ records.append(WatchOnlyAccountRecord(id: UUID(), walletIndex: 0, accountIndex: 42, addressType: LDKNode.AddressType.nativeSegwit.stringValue, xpub: "tpubDDWohsp5dx2iMJ9N7iHbgAEDhH4BJB9NWW1fEW3yA3AFNDREmpzteCXNqppMLUmKFY5q5e3PXtS5CuqWCQbYcGhpPqYAgQSYdwknW9J6sQv", requestFingerprint: "review", createdAt: 1000, name: "review", isTrackingEnabled: false, setupState: .pendingDelivery)) // test hook
+ } // test hook
+ records[0].name = "review-\(Int(Date().timeIntervalSince1970))" // test hook
+ try WatchOnlyAccountStore.save(records) // test hook
+ service.markBackupRequired(category: .wallet) // test hook
+ log("wallet-edited") // test hook
+ } catch { log("wallet-edit-error \(error)") } // test hook
+ case "prepare-usdt": Task { // test hook
+ guard let wallet = service.usdtWallet else { log("prepare-usdt-missing"); return } // test hook
+ await wallet.refresh() // test hook
+ do { log("prepare-usdt stored=\(try wallet.hasStoredWallet) nonempty=\(try await wallet.backupSnapshot()?.isEmpty == false)") } // test hook
+ catch { log("prepare-usdt-error \(error)") } // test hook
+ } // test hook
+ case "usdt": service.markBackupRequired(category: .usdt) // test hook
+ case "verify-usdt": Task { // test hook
+ do { // test hook
+ guard let wallet = service.usdtWallet, let current = try await wallet.backupSnapshot() else { log("verify-usdt-absent"); return } // test hook
+ let records = try await UsdtRecoveryBackupStore().snapshots() // test hook
+ log("verify-usdt matches=\(records.contains { $0.usdtWallet == current }) count=\(records.count)") // test hook
+ } catch { log("verify-usdt-error \(error)") } // test hook
+ } // test hook
+ case "verify": Task { // test hook
+ do { // test hook
+ let settings = try await VssBackupClient.shared.getObject(key: "SETTINGS") // test hook
+ let payload = try SettingsBackupV1.decode(from: settings!.value) // test hook
+ let remote = payload.settings["enableSwipeToHideBalance"] as? Bool // test hook
+ log("verify-settings matches=\(remote == SettingsViewModel.shared.swipeBalanceToHide)") // test hook
+ let wallet = try await VssBackupClient.shared.getObject(key: "WALLET") // test hook
+ let data = try JSONDecoder().decode(WalletBackupV1.self, from: wallet!.value) // test hook
+ log("verify-wallet matches=\(data.watchOnlyAccounts == (try WatchOnlyAccountStore.load())) version=\(wallet!.version)") // test hook
+ } catch { log("verify-error \(error)") } // test hook
+ } // test hook
+ case "status": // test hook
+ for category in BackupCategory.allCases { // test hook
+ let s = service.getBackupStatus(category: category) // test hook
+ log("status \(category.rawValue) synced=\(s.synced) required=\(s.required) running=\(s.running)") // test hook
+ } // test hook
+ case "end": log("control-ended"); return // test hook
+ default: break // test hook
+ } // test hook
+ } // test hook
+ try? await Task.sleep(nanoseconds: 100_000_000) // test hook
+ } // test hook
+ } // test hook
+ } // test hook
+} // test hook
+#endif // test hookReviewed 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 entire PR at 6c40b8d against merge base 0ad55417, after the comparison base changed from eff0dadf. No baseline coverage was inherited. bitkit-android#1689 at f8b2ffc, bitkit-ios#1122 at a214b06, and bitkit-ios#1121 at 0ad55417 were comparison-only.
No new actionable code findings.
The resolved shutdown lost-edit thread remains fixed: data-store listeners stay until cancelled uploads and both backup writers drain, and edits in that window stay required. Two parent concerns are unchanged on this head and remain open: terminal identity failure blocks current WALLET backups and stalled LNURL authentication holds the VSS wipe drain. The setup-timeout change classifies the existing 30-second wrapper as retryable; a native auth call that ignores cancellation can still hold closeAdmission().
The temporary-failure split matches Android #1689 at f8b2ffc: UnknownError retries only putObjects request-send failures, auth request-send failures retry without that path suffix, and plain authorization denials do not. Metadata resumption after a completed wallet backup is still present for #1122, whose pin is the older backup head 675f8bd.
Validation: Run Tests and SwiftFormat passed on 6c40b8d5f3897cb648de34d7d4f422b48370288a. Those run identities were checked against the pin. XCTest and SwiftFormat were not executed locally; this environment has no Xcode or iOS SDK. E2E and migration jobs on this head were skipped.
Device testing: not performed in this review. The latest attributed device pass is for 5f8a226. The only later commit adds -appUpdateIgnoreTimestamp to HomeHeaderUITests and UsdtWalletUITests. Manual cases 1–4 were not rerun at this head.
Reviewer model: grok-4.7
Reasoning effort: high
There was a problem hiding this comment.
Verdict: 💬 Comment — the UI-test launch delta is verified at 6c40b8d; inherited parent findings remain open.
Findings: No new actionable defect. These unchanged parent concerns remain unresolved:
- Terminal identity recovery blocks complete-WALLET backups.
- Stalled native authentication delays the wipe drain.
QA for 6c40b8d: Built the unmodified head with Xcode 27.0 and ran the affected UI tests on an iPhone 17 Pro 2x simulator, iOS 27.0, with a disposable unfunded regtest wallet. All installed app-bundle files matched the build.
Home header buttons and balance toggle remain usable ✅ Drawer, profile navigation and balance toggling passed, checked in the recording and unchanged XCTest assertions.
The first execution also completed Terms, Skip Intro and New Wallet before reaching Home, as checked in its XCTest log. The entire test passed again for the recording repeat. No assertion or wait was changed.
home-header-repeat2x.2x.mp4
Repeat execution at 2×; first-run onboarding is established by the first execution's XCTest log.
Unknown USDT balance and decimal entry in a comma region ✅ Unknown balance, dot decimal entry and retained manual recipient checks passed in `de_DE`, checked in the recording and XCTest assertions.
With unreachable chain and bundler endpoints, Home and the USDT wallet kept the balance unknown. The test entered $ 1.5, checked network selection and returned to the original recipient. Both executions passed.
usdt-comma-repeat2x.2x.mp4
Repeat execution at 2×.
USDT receive and invalid payment remain separate from Bitcoin ✅ Receive state, invalid-address refusal and fixed-amount return navigation passed, checked in the recording and XCTest assertions.
The test covered receive-tab swipes, QR amount commit/cancel, camera authorization and rejection of a Bitcoin-shaped recipient. A fixed URI opened review at $ 1.23 without a keypad; Confirm-header Back returned to the recipient, and changing it retained an editable amount with six-decimal precision. Both executions passed; no payment was submitted.
usdt-receive2x.2x.mp4
Passing execution at 2×.
The XCTest results retain QoS priority warnings without an established cause. Recordings have capture gaps after a repeat; the 2× copies preserve every captured changed frame. No selected case remains unrun.
Check: retained backup lifecycle evidence ✅ Production code and dependency pins are identical to the previously tested head, checked across the complete source trees.
- The 5f8a226 review establishes focused native regressions and stopped WALLET/USDT setup-timeout retry with encrypted readback.
- The 61537eb lifecycle review establishes manual admission/cancellation, edits during drain, stopped listeners, metadata retirement/capture cancellation, explicit authorization denial and retained initialization coverage.
Those successful cases were not repeated: all production inputs are unchanged. Their original boundaries remain: selected-field WALLET readback is not complete-WALLET restoration, and setup-timeout retry does not impose a hard deadline on stalled native authentication.
Coverage: contiguous 5f8a226..6c40b8d, both changed test files, the defaults-backed reminder timer, critical-update handling and prior open findings. Primary and independent review found no new defect. All changed launches received simulator coverage; production backup behavior retains the linked reports.
Twin: bitkit-android#1689 fix: preserve pending backups across restart at f8b2ffc retains matching temporary-error classification and original-owner/drain semantics. This iOS test-launch change has no Android production counterpart; Android device testing was not repeated.
Reviewed by gpt-6.1-sol-high
November 9 ·
6c40b8d5With observation stopped, WALLET and USDT retry setup timeouts with bounded backoff starting at 10s. Cancellation and retirement still stop the old work. In the first hosted run, an optional update covered Terms in three UI tests. Their launches defer that reminder. Fresh onboarding passed Terms, Skip Intro and New Wallet on the first tap. Local and hosted suites each passed 3,192 tests with five skips. All 28 commits verified against
0ad55417. Corrected hosted runNovember 6 ·
61537ebdObservation stops cancel reservations and drain admitted work. Delayed backup, retiring METADATA and cancelled METADATA-read controls passed. Local and hosted runs on
3173e93ceach recorded 3,191 passes, five skips and zero failures. SwiftFormat was clean on 851 files. The 26 commits againsteff0dadfverified. Local runtime warnings numbered 29 and hosted linker warnings 286. Terminal WALLET and stalled VSS reads remained open. Hosted runNovember 5 ·
675f8bdWith #1121/
1c3cde6, pending METADATA resumes after WALLET recovery while its old scheduling slot is occupied. Two independent reviews covered observation, wipe generations, explicit intent and stop/drain ownership. All 25 live-parent commits verified locally and on GitHub. The PR was ready for review.Local Xcode 27.2 and hosted Xcode 16.4 on iOS 26.2 (23C54) each recorded 3,177 unit and 40 physical UI passes, 3,188 distinct passes, five skips and zero failures or test retries. METADATA and USDT camera/invalid-payment controls passed. Formatting was clean on 851 files. All 17 translation warnings matched the parent. Native run · Formatting
The audit verified 16,615 evidence files including 71 attachments. It retained 286 duplicate-symbol warnings, system diagnostics and 23 local runtime warnings, including two timing observations of unknown cause. Nine migration jobs skipped. Other branch-limited workflows did not trigger. The neutral external check and diagnostics are recorded in
work/validation/hosted-ios1123-675f-20261105/owner-qualification.json.External QA used unpublished Debug controls for METADATA retirement, stop/restart, encrypted VSS tag readback and explicit authentication denial. Terminal WALLET and stalled authentication remained open. Reviewer evidence
Durable wipe parent integration — November 4, 21:33 UTC
Published signed additive merge
f514d199e1cceb99167d36a426e91bf73cfa2a33onto live #1121 parentbf4061f50acffd64fde206a4da1afe380895934b. This preserves the backup observer FIFO/drain and explicit-backup wipe-generation refusal while incorporating the parent's durable wipe and concurrent-reset serialization. All 22 commits in the live-parent range are locally verified and GitHubverified=true,reason=valid.Final combined-tree checks on Xcode 27.2 (27B52), preserved iOS 26.2 (23C54): 3,137 unit and 40 physical UI passes, five explicit skips; 3,148 unique passes. Thirty configured launch variants account for the extra invocations; no failed-test or interaction retry. SwiftFormat 0/851; translations zero errors with 17 warnings byte-matched to the actual parent. All 1,722 frozen inputs match and both independent affected source reviews are clean. The complete 13,181-file audit includes nested XCResult diagnostics and retains 28 runtime warnings, crash-observer/symbolication limitations and fixture exclusions. These results do not establish running-LDK quiescence, terminal complete-WALLET availability or unexecuted authenticated journeys. Current-head native CI and SwiftFormat passed on attempt 1. Synthetic checkout
9361a34a899adb775199fb13cfec8a8343378180has exactly treef248b230d5eea5b291e84479aec3ed3c6a122d59: 3,137 unit and 40 physical UI passes (3,148 unique passes), five explicit skips and no failed-test retries. Hosted format is 0/851 with 114 configured exclusions. Integration, E2E and hosted translations did not trigger for this non-master target; nine migration jobs were skipped. These are not passes. The exact-tree local translation result above remains applicable.The complete hosted audit covers both job logs, all nested XCResult diagnostics, all 71 attachments and the checksum/CRC-verified artifact. The owner verified all 16,452 evidence files. Retained diagnostics include 571 duplicate-symbol warnings (286 unique symbols), raw runtime messages with unresolved causes, and four explicitly reconstructed interleaved log tokens. Hosted Xcode 16.4 differs from local 27.2; no same-toolchain base runtime comparison is claimed. Evidence:
work/validation/hosted-ios1123-f514-20261104/report.md,report.jsonandowner-integrity.json. Earlier evidence below remains tied to its original revisions. PR remains ready,draft=false.Original-wallet backup ownership — November 4
Published signed additive merge
c75acf3197d9e188a363df103c6c5ccc6b83ebb9from live parent #1121 at25f25eaed54e0c0cb7f39d863c96412cd3a3f94c. The composed scheduler retains the original VSS owner through payload capture, upload and cleanup. A reproduced composition race is fixed: an explicit backup queued before wallet removal cannot capture or upload after that removal generation changes. Ordinary observation stop/restart still permits explicit backups.Author validation on this composed tree: 2,983 unit passes, one opt-in unit fixture skip, and one affected ProfileRecovery UI pass; all 1,690 frozen inputs match. Required SwiftFormat reports 0/830 with 106 configured exclusions. The two new refusal controls failed before the guard and passed afterward; the ordinary stop/manual-backup control passed both. Two independent affected source reviews are clean. Complete logs and nested XCResult diagnostics were inspected; retained notices and native Paykit QoS observations do not establish a new timing cause. All 21 commits against the live parent are locally verified and GitHub
verified=true,reason=valid.Hosted native CI and SwiftFormat passed on attempt 1. Synthetic checkout
cc621b88258b90e78e76a3dfa9d2f7ea2a01c077has exactly the published tree: 2,983 unit passes and 39 physical UI passes, five explicit fixture skips, zero failures and no testcase retries. The launch testcase has 30 distinct configurations. All 65 affected backup, foreground-recovery, VSS and ProfileRecovery cases passed. Hosted formatting reports 0/830, with 106 configured exclusions; nine migration jobs were skipped.Complete logs, all nested XCResult references, 67 attachments and checksum/CRC-verified artifacts were inspected. The owner verified all 18,840 evidence files. The 286 unique linker warnings exactly match retained actual-parent and previous-child evidence. Runtime diagnostics, including prior CoreGraphics NaN messages, retain their provenance and unresolved causes; runtime issue detection and leak checking were disabled, so no detector-clean claim is made. Evidence:
work/validation/hosted-ios1123-c75a-20261104/REPORT.mdandSTRUCTURED-REPORT.json.These results do not establish literal wallet removal, running-LDK persistence quiescence, or terminal complete-WALLET availability. Existing acceptance IDs, external evidence limits and unresolved terminal recovery findings below remain applicable.
Paired authentication transport classification — November 3
Signed commit
11fb65d780055a10081bd9d7e12fee61949d140drecognizes the released VSSAuthErrorrequest-send form as temporary for scheduled backups. The raw HTTP(S) form and one exact native external-request wrapper are accepted. Authorization denial, response/decode errors, repeated wrappers, unrelated errors and malformed URLs remain permanent. Valid configured callback paths, IPv6 and punycode remain supported. ExistingUnknownErrortransport handling stays restricted to/putObjects. No credential, restore, owner, cancellation or scheduler admission rule changed.Controlled before tests reproduced the classifier and scheduled SETTINGS failures, while denial stayed permanent. The final candidate passes 2,973 selected unit tests and one ProfileRecovery UI test, with one explicit local-protocol fixture skip, zero failures and zero retries. Four workflow integration classes remain excluded. All 1,689 frozen hashes match. SwiftFormat0.64.0 reports 0/829 requiring formatting, with106 configured exclusions; whitespace checks and two independent affected correctness/security reviews pass. Original repeated-wrapper and punycode failures, formatter findings and their corrections are retained. Complete raw logs and all2,975 canonical results were inspected. Two AppIntents notices and five QoS observations remain disclosed; all five share prior stack prefixes, four have identical normalized frame sequences, and no new timing cause is claimed.
Evidence:
work/validation/ios1123-auth-transport-final-b-audit-20261103/report.json, SHA256351b376b7ba253018b14d40a6e819738ad19b36d806be87dae6f16df7cea5b78; source delta SHA256c88ed7de25535e0980496c095d391de3efdacc1da29aee14939707e33114e0aa. The owner verified6,192 sealed evidence files. The full20-commit range against live parentb9f3bbda22cf8010e79c2c6bd24b69002f543be7is locally and GitHub verified. Current-head native CI and formatting passed on their first attempts. The synthetic checkout2983c260has the exact published tree. Canonical results record 2,973 unit passes and 39 UI passes, five explicit fixture/hardware skips and no failures or retries; thirty distinct launch configurations are separate physical executions. Hosted formatting is 0/829, with 106 configured exclusions. Migration was skipped. Complete logs, all nested XCResult references and checksum/CRC-verified artifacts were inspected, with 9,324 sealed evidence members verified. The 573 normalized build diagnostic lines match the previous head; runtime diagnostics remain retained without a causal or warning-free claim. Evidence:work/validation/hosted-ios1123-auth-transport-11fb-20261103/report.json.Scope of runtime evidence: these new iOS controls exercise scheduled SETTINGS backup with observation stopped and refusal remaining pending. They are native tests, not a new authenticated full-app auth journey. The Backups-screen direct Retry returns its outcome without enqueueing a follow-up; this change preserves that behavior. Android twin #1689 at
e7fb0ac37has separate actual-app transport recovery and authorization-denial evidence, which is not counted as iOS execution. Existing Manual1–4 app evidence below retains its tested revisions and stated boundaries. Terminal complete-WALLET availability remains a separate parent finding.Attributed current-head app evidence
External review 5760648183 reports scheduled SETTINGS recovery after auth-relay refusal, invalid-challenge failure remaining pending for 25 seconds, and recovery from a USDT VSS connection failure at
11fb65d7, using disclosed local DEBUG controls and staging authentication. The author inspected the full report, inline logs and added control diff, and verified its revision. Original build/install receipts, raw relay/VSS artifacts and media were not independently inspected, so these remain attributed external results. Invalid-challenge evidence is distinct from a genuine server authorization rejection, which that reviewer did not drive. Neither result establishes direct-UI automatic Retry behavior or terminal complete-WALLET availability. Ovi's separate current-head human approval is recorded; existing João and Piotr requests remain pending, without duplicate notifications.Scheduled-backup completion repair — November 3
Signed
4e6c06ed79d0b916529b293fc83cd98d8acb0d33removes scheduled-job admission before publishing idle, within the same serial backup-queue operation. A foreground retry can no longer observe idle while the completed job still blocks it. Latest required/synced revisions and cancellation drains are preserved.The controlled app-queue regression fails before the repair and passes afterward; all 13 affected tests pass. The original test-fixture mistake (holding the test module’s separate queue) is retained and superseded by the explicitly app-qualified control. Final combined validation passed 2,970 units and one Profile Recovery UI test, zero failures/retries, with one opt-in native-fixture skip. All 1,689 frozen inputs match. SwiftFormat 0.64.0 reports 0/829 (106 exclusions). Two independent affected reviews are clean; complete diagnostics retain six native Paykit QoS observations and two AppIntents notices, without a timing-cause claim. All 19 commits against live #1121
b9f3bbdaverify locally and on GitHub.This repair follows #1124’s authorized CI retry: its camera case passed, but a late-retry scheduling test failed. The independently reproduced admission gap is fixed here; the exact historical job-map state was not captured, so the CI failure’s cause is not conclusively established. The new-head hosted inspection below is complete; dependent PR composition continues. Existing authenticated acceptance and terminal complete-WALLET gaps remain scoped below. Evidence:
work/validation/ios1123-idle-publication-final-a-20261103/inspection/REPORT.mdandwork/validation/ios1123-idle-publication-20261103.Hosted validation at
4e6c06ed— November 3Native run 41704088643 passed 2,970 unit tests and 39 UI executions, with five explicit skips, zero failures and no test-method retries. Both scheduled-backup regression cases passed. One N5 tap recovered on its second action attempt. SwiftFormat reports 0/829, with 106 configured exclusions; migration jobs were skipped. The synthetic checkout and published head have the exact same tree against live #1121
b9f3bbda.The complete logs, nested results, 6,181 archive entry CRCs and 9,332 sealed evidence payloads were inspected and verified. The 572 canonical linker warnings are 286 repeated messages matching the verified base/pin provenance. Simulator and UI advisories remain documented; this is not a warning-free or full authenticated fixture claim. Evidence:
work/validation/hosted-ios1123-4e6c-20261103/REPORT.md.Historical hosted validation at
c6336881— November 3Native run 41670750702 on
c6336881d41b8aefe9a967e282b11c2a9301f3ebpassed 2,969 unit tests and 39 UI executions, with five explicit skips and no failures or retries. Thirty launch executions cover 15 locale × two appearance configurations. Formatting is 0/829. The actual synthetic checkout, published head and local composed tree match exactly against live #1121/b9f3bbda.Full logs, nested results and diagnostic references were inspected; 9,352 evidence payloads and all archive CRCs/digests were verified. The 286 distinct linker messages match sealed prior provenance and actual-base dependency/project bytes. Simulator, keyboard/gesture, optional-wait and SwiftUI diagnostics remain documented, with no blanket harmlessness or detector-clean claim. Migration and fixture skips are not passes; this adds no terminal complete-WALLET, remote restoration or funded-USDT acceptance. Evidence:
work/validation/hosted-ios1123-c633-20261103/REPORT.md.Cached-backup parent composition — November 3
Signed
c6336881d41b8aefe9a967e282b11c2a9301f3ebadditively incorporates #1121 atb9f3bbda22cf8010e79c2c6bd24b69002f543be7. Unreadable cached Blocktank records now refuse upload, preserving the acknowledged backup and pending revision; valid absent optional info remains supported. The backup observer/stop/drain/retry behavior is preserved. The merge had no conflicts.The actual combined tree passed 2,969 unit tests and one Profile Recovery UI test, with zero failures/retries and one opt-in USDT native-fixture skip. All 1,689 source inputs match. SwiftFormat 0.64.0 passed 0/829 with 106 configured exclusions, and diff checking passed. The initial PATH formatter stopped for an insufficient version; it produced no clean result and is preserved separately. Complete nested diagnostics retain five QoS observations and two AppIntents extraction notices in unchanged paths, without claiming a reproduced parent runtime baseline. Two independent parent source reviews and the fresh affected composition review are clean. The full 18-commit live-parent range verifies locally and on GitHub.
New-head hosted CI is pending inspection. These combined native checks add no authenticated journey or terminal complete-WALLET acceptance; the existing case-specific evidence and gaps below remain distinct. Detailed local receipts:
work/validation/ios1123-cached-blocktank-composition-20261103andwork/validation/ios1123-cached-blocktank-diagnostics-20261103.Authored lifecycle acceptance — November 2
All four finite manual cases below passed on disposable, unfunded iOS26.2 simulators using
ff3ba674plus independently reviewed, local DEBUG timing/observation controls. The installed app manifests match all105 built files. Cases1–3 bind final-c; case4 binds final-d, which adds only cosmetic initial progress and completion logging. Published source, real capture, native LNURL authentication, encrypted VSS transport, node-ready admission and completion guards remain unchanged. No wallet reset, funds, release or server deployment was used.Complete build/runtime diagnostics and native/proxy acknowledgement correlations are inspected in
work/validation/ios1123-lifecycle-app-20261102/. The stock local server authorizes real native requests. Retained limits: selected-field readback is not complete-WALLET equality or restoration; tracked cleanup/post-ACK cancellation is not proof of cancelling in-flight native HTTP; loopback refusal is not literal external transport loss. Ancillary node/balance/RGS/system diagnostics and failed fixture preparations remain disclosed. The separate terminal complete-WALLET availability finding remains open. Current published CI and signatures are unchanged; no CI rerun or source push was needed for this evidence update.Retained lifecycle acceptance evidence — November 2
The recovered controls and traces supplement the historical
262a447Manual 1/3 ande12df99Manual 2 executions. The author read the complete report, verified all 19 disclosed control-hunk contexts against those exact source revisions, and inspected the four event/readback excerpts. They record drain-before-manual-admission, cancellation, acknowledged-upload edits, stopped-listener cycles, and native request-send failure followed by bounded retry. The stopped-listener trace ends withverify-wallet matches=false, consistent with its deliberately unobserved wallet edits; it is not a successful complete-WALLET readback.The reviewer explicitly cannot recover the contemporaneous historical installed-executable/build binding. The original unredacted patch and complete-trace hashes remain reviewer assertions; only the disclosed redacted patches/excerpts were inspected. Those earlier
ff3ba674executable hashes bound the component tests alone; the new authored installation receipts above separately bind the finite runtime cases. These are attributed historical executions, not independently bound current-head runtime passes.Seven previously inspected lifecycle/native helper/view/queue/lockfile inputs match from
262a447toff3ba674. The oldere12df99composition has subsequent BackupService and LDK changes, so no whole-composition equivalence is claimed. Manual 4 remains component evidence. Those historical provenance gaps remain; the new authored finite cases above supply separate current-composition evidence. No repeat historical artifact request was made. Evidence:work/validation/ios1123-recovered-controls-20261102/and the earlier retained-evidence report.Current-head native CI inspected — November 1, 23:22 UTC
At
ff3ba674464c796057415188d460e6009371496a, native CI passed 2,967 unit tests and 39 physical UI executions, with five explicit skips, zero failures and no retries. The synthetic checkout tree exactly matches this published head; its actual CI parent is recorded in the audit. Earlier exact-head formatter evidence is reused, and skipped migration jobs remain skipped.Complete logs, all nested physical test summaries and 67 attachments were inspected and checksum/CRC verified. The complete linker-warning multiset matches the actual parent: 571 occurrences /286 symbols, with no introduced source compiler finding. Runtime evidence retains 72 CoreGraphics invalid-number messages and 2 system-gesture timeouts, haptic/service errors and fixture limitations; passing assertions do not establish their cause or harmlessness. This is not an authenticated recovery or complete-WALLET acceptance pass; the terminal availability finding remains open. Evidence:
work/validation/hosted-ios1123-ff3b-20261101-current-a/NATIVE-REPORT.mdandwork/validation/hosted-ios1121-1123-native-root-integrity-20261101.json.Validated parent composition — November 1, 22:00 UTC
Signed additive merge
ff3ba674464c796057415188d460e6009371496aincorporates #1121/a6f233df2dcefbd75b0f7b3c838db23d1a984493. The nine-file owned patch is unchanged. On the composed tree, 2,967 unit tests and one ProfileRecovery UI test passed; one opt-in USDT fixture test was skipped. SwiftFormat 0.64.0 reported 0/828, and all 1,688 frozen inputs matched. Complete logs and nested diagnostics were inspected; six unit QoS observations and three AppIntents notices remain documented. No introduced compiler/format finding was identified. All 17 commits in the live-base range verify locally and on GitHub.These are native/component results, not a new authenticated complete-WALLET or profile-removal journey. The terminal complete-WALLET finding remains open. Current-head native CI is now fully inspected above; the newly reported parent #1120 authorization-test timeout is unresolved and has not been waived or retried. Existing acceptance gaps and QA IDs below remain applicable.
Description
Stopping backup observation could crash while startup concurrently modified its subscriptions. Serialize subscription and timer ownership on MainActor, and finish canceled backup cleanup before shutdown returns.
Invalidate queued work from the stopped observation generation. Received changes now remain pending while restart drains old cleanup. Pending subscriptions record dirty state immediately, and observed uploads wait for admitted observation. Cancellation preserves required state for ordinary categories as well as wallet/USDT. Fresh explicit USDT retries remain usable without restarting Lightning observation, and manual migration uploads wait for cleanup instead of being silently skipped.
A released VSS request-send failure is now retried at the backup boundary, including explicit USDT work after observation stops. The compatibility rule accepts only the typed native UnknownError envelope with an HTTP(S) host and
putObjectspath; unrelated unknown/authentication/decode errors and cancellation retain their behavior. Same-status notifications still advance capture revisions, but no longer rewrite identical persisted status.Initialization completion also admits its callback only once after the node reaches Running. Progress normalization can notify the view twice; both handlers now share the existing completion latch before scheduling any delayed callback. Existing startup waits and assertions are unchanged.
Out of Scope
Full-wallet format separation and the unresolved identity-restoration availability gap in #1121 remain separate. Android's related setup/job-ownership repair is in its focused twin synonymdev/bitkit-android#1689. Native Paykit/Pubky artifact adoption and historical incident attribution are unchanged.
Design
N/A — no UI changes.
Preview
N/A — background backup lifecycle repair.
QA Notes
Setup
Use the committed released Core 0.8.0-rc11, Paykit 0.1.1-rc2 and VSS 0.5.26-rc1 dependencies. Component tests use private defaults and controlled delay/capture/upload gates. Manual lifecycle checks require a disposable app installation and the dedicated fault fixture described in
journeys/backup-restore/wallet-snapshot-consistency.md; do not use a personal wallet or reset wallets.Journeys
N/A — not drivable; see Manual Tests.
Manual Tests
1. Scheduled cleanup/final-status drain, manual admission, edits during restart and canceled SETTINGS newer-marker retention — authored final-c app cases passed; exact boundaries and limits above.
2. Direct USDT recovery during cleanup and stopped observation, plus native refusal and existing10s retry — authored final-c app cases passed without another manual request/Lightning restart.
3. Repeated observation, stopped wallet edit/status stability, retained pre-stop settings marker and one live listener upload — authored final-c app case passed.
4. Actual New Wallet initialization with cosmetic99.95% fixture control, real Running transition, one admitted completion/callback and Home — authored final-d app case passed. Existing current-head SwiftUI regressions retain separate normalization/slow-node coverage.
Automated Checks
UsdtWalletUITests.swift— bounded navigation synchronization when a transient toast obstructs the back button.BackupServiceTests.swift— stopped observers, canceled-work drain, final status-write ownership, manual admission, restart and independent USDT retry.Current final validation — 1 November 2026
Signed additive merge
2c0c1002e03aa9c91828b47bf4db73067270463fincorporates actual #1121 parentd83a21721b67045fdc4e6cb2816fb1988d5e924b. Tree41d374cbefe6c40c977330187dcf4810738a7a37preserves this child's normalized delta and composes the parent's queued-allowance admission correction. No deadlines, restore gates or shutdown drains changed.Final native build and CI-selected units plus the affected ProfileRecovery UI passed 2,966 unit executions and one UI execution, with one explicit native-fixture skip, no failures or retries. All 1,688 frozen inputs match. Complete logs, every one of 2,968 canonical executions, all nested issues, 3,175 successful action sections and 30 attachments were inspected; the owner verified 12,066 sealed evidence files. SwiftFormat 0.64.0 reports 0/828, with 106 configured exclusions; whitespace checks pass. Unchanged translation inputs retain the prior zero-error/15 actual-parent-warning evidence.
Five unsymbolized QoS observations, three AppIntents notices and the recorded application/simulator diagnostics remain disclosed without a proven introduced cause. Canonical results supply three successes and five warnings omitted by the wrapper. Other UI was unselected, and integration exclusions/skips are not passes. Interrupted final-a produced no complete result; final-b was the first bounded recovery after verifying absent processes and identical inputs. No assertion or timeout was weakened.
The independent affected composition review is clean; prior shared concurrency reviews retain their scope. The new Android same-identity contact-sync finding was separately traced: iOS contact sync does not reactivate allowances, and its startup cache rejects repeated same-session activation. No speculative iOS change was added. All 16 live-base commits verify locally and on GitHub (
verified=true,reason=valid), without history rewriting.At this earlier composition checkpoint, hosted inspection and manual cases1–4 were pending. Current-head CI and the subsequently authored finite cases are recorded above. Broader authenticated-fault limits and #1121's terminal complete-WALLET availability remain separate; no such repair is claimed.
Earlier final validation — 31 October 2026
Signed additive merge
fb25166fd830f8d59368c09079545a1abcdb5cb9incorporates live #1121 parent624d7a38995280e9ad59684705c72e405b1e02bd. Tree2f1fa9e1dafbb9ce2a65c4959f26474a7e8c089cpreserves the child delta against its actual previously composed ancestor and includes the validated parent/master admission changes.The combined native build and selected suite passed 2,965 unit executions and 39 UI executions, with five explicit skips, zero failures or failure-driven retries (2,975 unique logical passes). All 1,688 frozen source inputs match. Every one of 3,009 canonical execution summaries, nested diagnostics and 67 attachments was inspected; the owner verified 12,414 evidence files and 27 external inputs. SwiftFormat0.64 reports 0/828 with106 exclusions. Reused translations have zero errors and15 verified actual-parent warnings on identical21 inputs.
One fresh combined composition review is clean; earlier unaffected reviews retain their original scope. All 15 live-base commits verify locally and on GitHub (
verified=true,reason=valid). No history rewrite occurred.Current-head hosted native run41241011565, attempt1, and its formatter passed on synthetic merge7d071143 whose tree exactly equals this head: 2,965 unit +39 physical UI passes, five explicit skips, zero failures or failure-driven retries (2,975 logical passes). The complete logs, all3,009 canonical execution summaries, nested diagnostics,67 attachments and digest/CRC-verified artifact13057914768 were inspected; the owner verified all18,651 sealed evidence files and17 external inputs. Hosted SwiftFormat0.64 reports0/828,106 exclusions. Nine migration jobs and four workflow-excluded integration classes are not passes.
Hosted diagnostics retain286 distinct dependency linker warnings/571 emissions, two AppIntents notices and48 NaN messages. A keyboard-queue timeout, copied into three exported logs and followed by
No stack!, occurred during a passing SendConfirmationLifecycle test; it has no established source cause. Runtime issue/leak detection and test timeouts are disabled in that hosted configuration, so zero canonical runtime warnings is not general runtime-safety evidence. The profile and camera cases passed once; neither is a causal repair for prior failures. Authenticated manual fault cases and terminal complete-WALLET availability remain open.The complete diagnostics retain14 QoS warnings,48 UI NaN messages and three AppIntents notices, without a proven introduced cause. The profile-recovery case passed once in62.472s and the camera case in58.858s; neither explains earlier/sibling failures. Canonical records supply three successful cases omitted by the wrapper and expose warnings that wrapper diagnostics omitted. Dependency pins/resolution match; missing alternate-object stores still prevent complete dependency working-tree verification, including Core's unreadable
CLAUDE.mddiff. The five runtime skips and four excluded integration classes are not passes. Manual cases1–4 retain their documented coverage limits; no newly authenticated full-app journey or terminal complete-WALLET repair is claimed.Earlier final validation — 29 October 2026
Head
1dd6d2fb8974cffc674802180d5f8ef620a60959is stacked on #1121 atd0246d1c. The latest commit changes only the activation test: credential setup has a separate bounded deadline, while the existing two-second activation-return assertion and production publication cap remain unchanged. A controlled implementation that awaited publication fails that original assertion; restoring the exact production bytes passes.Hosted native run 40969995082 passed on its first attempt: 2,880 unit passes and 39 physical UI passes (10 logical cases), five fixture skips, zero failures or retries. Its synthetic merge
c94e38cfhas the exact head tree17d7efe4and parentsd0246d1c/1dd6d2fb. Hosted Xcode 16.4, SDK 18.5 and iPhone 16/iOS 26.2 remain distinct from local execution. SwiftFormat 0.64.0 reports 0/824, with 98 exclusions.Complete 5,824-line native output, all 2,924 nested test records, 17 annotations, eight exported text diagnostics and 54 unique attachment payloads were inspected. Artifact
12941895164matches GitHub SHA25692e7b34838d6ef113ef70bec25535e63cdb8d75f6cd6f4845a75941de3477bd1; all 5,997 archive members pass integrity checks. The owner verified all 12,075 sealed evidence files. The 286 distinct duplicate-symbol warnings match the verified parent producer set; two AppIntents notices remain. Runtime output retains 48 CoreGraphics NaN messages across the two passing USDT UI cases plus the disclosed node/cache/network and unit fault-case diagnostics. Their numeric cause is unproven; this is not a runtime-clean claim.Skipped: the encrypted protocol fixture, three Trezor cases and the local fork payment. Four workflow-excluded test classes remain unrun. The activation test's 2.076786-second whole duration includes its two-second inverted expectation; it is not activation latency. Earlier failed hosted attempts remain retained, and this test-only correction is not claimed as a production causal repair.
The final local gate passed 2,880 selected units and two USDT UI cases, with two prerequisite skips and zero failures/retries; all 1,667 frozen inputs match. Required formatting passed. The independent affected-delta review is clean under the current risk-based policy, with earlier unaffected reviews scoped to their original inputs. All 13 commits in the complete live-base PR range verify locally and on GitHub (
verified=true,reason=valid). No history was rewritten.Gaps
The native protocol tests use real signing and VSS encryption but do not execute the complete app UI/Paykit/deployed-contract journey. Manual cases 1–4 have the revision-scoped authored app evidence above; they were not rerun on this composition. The newly inherited metadata dependency-wake case remains unrun in an authenticated app because its isolated wallet-failure and remote-readback fixture is unavailable; parent #1121 Manual 1 records that prerequisite. Component passes do not replace it. Android twin synonymdev/bitkit-android#1689 repairs late setup, retained cleanup, dirty-marker loss and manual/USDT reset ownership; its latest explicit-retry follow-up at49e9a99d passed4,696 unit tests and full compile/format/lint, with component-test limits documented there. No claim that this crash or these races caused the historical contact/acquisition failures.
Earlier revision evidence and retained failed attempts
These dated notes retain their original scope. Earlier pending states are superseded by the current validation above.
Earlier-head external evidence — October 30, 21:50 UTC
Automated review 5718993346 at
262a4474reports authenticated staging-VSS shutdown/drain, pending-edit retention and stopped-observer cases, plus component/navigation checks; it reports no new findings. The owner verified the revision and read the complete report, but has not independently audited its raw logs, XCResults or media. These are attributed external results; the author's hosted nested-result audit remains incomplete. João and Ovi have separately approved this exact head. The inherited terminal complete-WALLET limitation remains open on #1121.Earlier combined-tree validation — 30 October 2026
Signed additive merge
262a44747ffd8ef522178bfd3850fe78da15cce5incorporates live #1121 parentd896dba7e78d39d859ed9a844863a194efb4b546. The combined tree52be49877b6d7ba84b9500b1feb2fe198fc6e8d0preserves generation-owned observation and shutdown drains, adopts the parent's metadata-before-restore/public-address guards, and makes metadata join an existing wallet writer without canceling another owner's operation. The corrected lifecycle regression uses ACTIVITY because the parent now intentionally completes WALLET before METADATA; its drain, restart and required-state assertions remain unchanged. Its initial failed attempt is retained.The final native build and selected suite passed 2,956 units and two USDT UI cases, with two prerequisite skips, zero failures/retries. All 1,681 frozen inputs match. Complete 22,530-line output, every nested result, eight exported diagnostic files and 37 attachments were inspected; the owner verified all 6,145 evidence hashes. SwiftFormat 0.64.0 reports 0/827 with 104 exclusions; translation validation reports zero errors and 15 actual-parent warnings. Two independent affected composition reviews are clean. All 14 live-base commits verify locally and on GitHub (
verified=true,reason=valid).Seven QoS observations, 72 CoreGraphics NaN emissions with unproven numeric cause, two AppIntents notices and symbolication diagnostics remain disclosed. Local Xcode 26.1/iOS 26.2 evidence is distinct from hosted execution. The encrypted protocol/local-fork skips and four excluded workflow-integration classes are not passes. Current-head hosted CI is pending inspection; the historical results below do not certify this composition. Manual cases 1–4 retain their recorded component/runtime limits, and #1121's terminal complete-WALLET availability remains separate.
Earlier parent integration — 29 October 2026, 20:05 UTC
Signed additive merge
8947f493a681f57cb76486d96bbc36da3f9999a6incorporates live #1121 parentd0246d1c782c8f2167300383f85e12f1696c4234. Tree04884bd9d6c2bfadf5e0e1b022a025027d208c69retains this PR's stronger ordered observation shutdown and complete writer/task drain byte-for-byte, while importing the parent's repeated-restart regression and related private-publication/admission changes.Final combined-tree validation passed 2,880 units and two UsdtWallet UI cases, zero failures/retries, with two prerequisite skips (encrypted native recovery and local Arbitrum fork). Other UI was unselected. All 1,667 frozen inputs and four symlink targets match. Full SwiftFormat0.64 reports 0/824, 98 exclusions; the initial system formatter rejected the minimum version before checking and is retained as a failed setup attempt. Unchanged workflow/localization checks reuse exact parent input evidence.
Complete 24,156-line raw output, all 2,884 nested summaries, eight diagnostic logs and 35 attachments were inspected; the owner verified all 13,732 sealed rows. Nine QoS traces/eight nested warnings remain, including a newly observed fee-estimate RwLock sub-stack in parent-identical code; two AppIntents notices and simulator haptic messages are retained without a causal claim. One clean affected composition review preserves earlier full feature reviews. All 12 live-base commits verify locally and on GitHub (
true/valid). The current-head hosted native run, attempt 1, failed four UI launch executions: 2,880 unit and 35 UI passes, five prerequisite skips, four failures, no retries. Complete logs, all 2,924 nested summaries, 55 attachment payloads and 4,894,996 simulator system-log events were inspected; all 10,525 sealed evidence rows were verified. XCTest timed out its testing-assertion requests after ten seconds and then deliberately terminated the targets. RunningBoard only received those testing assertions 20–60 seconds after the original requests, after all four targets had exited. No preceding failed-target crash/fired watchdog was established; splash frames do not prove completed startup or general app health. The underlying cause remains unknown. Exactly one bounded native-job retry was requested on that observed testing-service delay; its completed failure and the subsequent test-only correction are recorded above. The original failures remain. Hosted formatting passed 0/824; nine migration jobs skipped. The 286 dependency warnings match verified producer evidence; two AppIntents notices, 24 NaN emissions and a keyboard timeout in passing tests remain disclosed.Upstream #1120's native attempt1 failed two plain launch tests. System logs show delayed RunningBoard assertion acquisition after XCTest's timeout and subsequent test-driven termination with no demonstrated preceding app crash; the next 30 launch configurations passed. That separate bounded retry has now passed, with its original failed attempt retained. The original failures and unknown underlying service-delay cause remain disclosed; this child publication does not claim they are fixed or that upstream CI is green. Authenticated manual cases below and terminal complete-WALLET availability remain open.
Earlier parent integration — 29 October 2026
Signed additive merge
d33547cba681f8cf826a715de5f51a2ba89c28efincorporates live parent #1121 at2d4fc7199b476f41ce56ae05c786f3ee1981bed5. The final tree5a80e5e87368aab5487436d2aa47fb5e271a87b3retains this PR’s observation ownership, shutdown drains and restart behavior, and adds the parent’s allowance-change publisher to the existing backup change stream.The final combined-tree native build and tests passed 2,865 unit cases + 39 UI executions, zero failures, with five explicit prerequisite skips. That is 2,875 logical passing tests; the launch case runs 30 declared UI configurations. All 1,660 frozen inputs match. Complete logs, all 2,909 nested summaries, and exported diagnostics were inspected; the owner verified the 13,837-row evidence seal. Three AppIntents tool notices and 12 QoS observations remain disclosed without a causal-fix or uniformly pre-existing claim. Full SwiftFormat reports 0/823, and translation validation reports zero errors / 15 warnings verified against the actual parent. The formatter cache-write notice remains. Original static process completion was reconciled without rerunning checks.
One independent review of the changed composition is clean; prior unchanged review coverage is retained. Every commit in the 11-commit live-base range verifies locally and on GitHub (
verified=true,reason=valid). The immediate post-push API briefly returned the older head; one read-only reconciliation verified the exact new range, with no repeated push. Current-head native tests and SwiftFormat passed on synthetic mergeb089302c, whose tree exactly equals the published head: 2,865 unit + 39 physical UI passes, zero failures/retries, five explicit skips; formatter 0/823. Complete logs, all 2,909 nested summaries, diagnostic exports and the SHA/CRC-verified result archive were inspected; the owner verified 12,003 sealed evidence rows. All 286 distinct linker warnings match the sealed actual-parent set. Two AppIntents notices, a setup metadata-pipeline broken pipe and 24 CoreGraphics NaN events in a passing USDT UI case remain disclosed without causal claims. Nine migration jobs were skipped; no current-head integration/external E2E result is claimed. This does not complete the authenticated manual fault cases below or close #1121’s terminal-identity/full-WALLET finding. Skipped native/fork/hardware cases and workflow integration exclusions are not passes. Earlier evidence remains scoped to its recorded revision.Parent integration — 28 October 2026
Signed additive merge
566b2ff8ba7abdd0968544dda5cdba32d8f518f0incorporates current parent #1121 atc0b1e86f, including the merged independent-USDT/master composition. The only adaptation inside this PR’s existing production delta removes two obsoletetrytokens from now-nonthrowing metadata-cache calls; actor hops, backup generations, stop drains and complete-WALLET checks remain unchanged.The exact tree
9ce5290cpassed 2,830 unit executions + 39 UI executions, zero failures, with three explicitly disabled Trezor skips. These are 2,840 distinct passing identifiers: the launch test executes 30 declared UI configurations. Complete 29,980-line logs and all 2,872 nested execution summaries were inspected. All 1,643 frozen inputs match; formatter passed 0/821 with 91 exclusions. Two AppIntents notices, ten nonfailing QoS observations and the formatter cache-write notice remain disclosed; timing causation and exact-parent runtime recurrence are unproven. Actual local toolchain is Xcode 26.1/17B55 on iOS 26.2/23C54. Excluded workflow integration/protected native/fork cases are not passes.One independent affected-composition review is clean, with unchanged earlier full-PR review coverage retained. All ten commits against the live parent verify locally and on GitHub (
verified=true,reason=valid); exact ranges match. The PR remains ready and mergeable. Current-head native tests and formatter now pass on the exact9ce5290ctree: 2,830 unit + 39 UI passes, five explicit fixture/hardware skips, zero failures/retries; formatter 0/821. Complete logs, all 2,874 nested summaries and 5,889 CRC/SHA-verified artifact members were inspected; the owner verified 8,858 sealed evidence files. The build retains 571 duplicate-symbol occurrences/286 symbols, with identical actual-parent build/pin configuration but no fresh parent runtime baseline. One keyboard queue timeout occurred during the passing 3.629-second pending-wallet test; causation remains unproven. Excluded integration/native fixture cases are not passes. This integration does not resolve #1121’s terminal-identity/full-WALLET availability finding or establish a new authenticated recovery journey.Earlier validated composition — 27 October 2026
Signed additive merge
e12df99e2e91944308314d82bc8d71e9aaa24f66incorporates live parent #1121 at73b7d58ab6854114a76f7577452677d91172a04a. Foreground recovery now keeps the original observation generation, joins the existing startup/teardown drains, and hands recoverable failures to the existing bounded scheduler. Queued work from a stopped generation cannot publish or upload; received edits remain required through acknowledged cleanup. No deadline or passing-path UI scheduling was weakened.The final tree
ffac48ab56f4210e256f518c180c17a5e933a937passed 2,990 selected unit tests with zero failures/skips and 39 UI executions with zero failures and three explicitly disabled Trezor skips. Workflow integration/native-fixture suites and the expired local-fork payment case were excluded, not passed. SwiftFormat 0.64.0: 0/852, with 90 excluded files; translation validation has zero errors and 15 verified actual-parent warnings. All 1,683 frozen inputs match. Two consecutive independent final reviews are clean.Complete 23,007-line unit and 4,732-line UI logs and every nested result were inspected. Five unit and twelve UI QoS observations, AppIntents and simulator/accessibility notices remain disclosed without a timing-cause claim. Earlier full-suite failures and controlled before/after evidence are retained: the tests now toggle actual persisted Settings values, restore exact prior values/absence, and isolate unrelated coalesced Settings changes; all original lifecycle, age, cooldown and upload assertions remain. These are fixture corrections, not historical-incident causation.
All nine commits against the live parent verify locally and on GitHub (
verified=true,reason=valid). New-head hosted checks are pending. The parent #1120's separately recorded hosted ProfileRecovery UI timeout is not claimed fixed by this local pass. Authenticated lifecycle acceptance and the terminal-identity/full-WALLET limitation remain scoped as described below. All earlier head-specific evidence is historical.Earlier repair — retain edits through shutdown drain
Signed commit
bfc4a5e87b2694cf4906d1c6aa784e2fa1c3e78bfixes the lost-edit window reported in review thread 4390767903. Shutdown stops status-driven scheduling immediately, but retains data-store listeners until scheduled/running uploads and both serialized writers have drained. Changes during that interval remain required; the stopped observation cannot upload them. Restart uploads the latest values, including an independently changed category that was already synced. Listeners are removed before stop returns.Controlled before/after evidence uses actual settings/watch-only notifications and a cancellation-insensitive acknowledged upload: the original implementation fails the one regression with six diagnostics; the repair passes all 23 BackupService tests. Final combined-tree validation passed 2,969 selected units, zero failures/skips, plus 39 UI executions, zero failures, with three disabled Trezor fixture skips. The expired local-fork payment case and workflow integration/native-fixture suites were excluded, not passed. SwiftFormat 0.64.0 reports 0/849. All 1,676 frozen inputs match and two consecutive independent final reviews are clean.
Complete 22,096-line unit and 4,733-line UI logs and every nested test result were inspected. Each run retains two AppIntents notices. Four unit QoS sequences recur in the verified actual parent; nine UI QoS occurrences have unsymbolized zero-address stacks and remain unattributed. The simulator also logged a duplicate accessibility class. These observations are disclosed without a timing-cause claim. This controlled regression and ordinary UI suite do not claim a new authenticated held-upload device journey; the external before-fix staging reproduction remains attributed to its original head.
All eight commits against live parent
754a899182bb39869b37d1f3e9601afecad8c593verify locally and on GitHub (verified=true,reason=valid). New-head hosted checks are pending. Earlier evidence below is revision-scoped.Parent composition — 27 October 2026
Signed additive merge
14152f41adda22319e531f3ac979c1fa0a4120a5incorporates current parent #1121 (754a8991). The resulting source tree is exactlye34dbbd507e6ba6f975f82e9abc0e11c163f9384, identical to the previously validated head: this child already confines listener ownership to the main actor and serializes observation teardown. Two independent composition reviews are clean; all 1,676 native-validation inputs match. Recorded validation remains 2,968 unit passes and 39 UI executions with three Trezor skips; no new test execution is claimed for this ancestry-only update. All seven commits against the live parent verify locally and on GitHub. Newly triggered hosted checks remain pending.Twin: synonymdev/bitkit-android#1689
Companion: #1122
Dependency: #1121
At signed
ff999a50e24326ad45fb033c61155b95c041d1be, a real hosted SwiftUI regression first fails with two completion callbacks versus one, then passes with the shared completion guard. The original slow/error retry case also passes without changing its assertions or deadlines. Final native build/selected unit run passes 2,913 tests, zero failures/skips; SwiftFormat0.64.0 reports 0/847. All1,665 frozen inputs match and two consecutive independent final reviews are clean. The full five-commit live-base range verifies locally and GitHub reports true/valid for every commit. Current-head hosted run40402278397 now passes: 2,913 unit and 39 UI executions, zero failures, one native-fixture unit and four UI skips (2,923 unique passes / 5 unique skips after parameterized-case reconciliation). Both initialization tests pass. Complete5,850-line log and artifact12690973387 were SHA256/CRC checked and every nested XCResult issue inspected. The CI merge tree equals this head. All286 distinct duplicate-symbol warnings match the actual parent; there are no runtime-warning entries. This does not establish the exact prior failure's observer ordering or complete the skipped/manual journeys.The complete20,360-line final log and nested XCResult issues were inspected. Two AppIntents notices and five QoS observations remain; their native construction stacks recur in the actual prior base, but counts and test associations differ and timing causes are unproven. The first final run failed on disk exhaustion, including early database/file errors and result-finalization failure. Its partial-suite successes are not a pass. After reclaiming only inactive task-owned compiler cache, one exact-input retry produced the complete result above.
Prior-head
494c9492hosted run40390697155 failedtestSlowRetryWaitsForRunningNodeBeforeCompletingthrough repeated XCTest fulfillment. Its full log and integrity-checked XCResult are retained. The view/test blobs at that revision equal the actual PR base. The new controlled threshold regression proves a reachable duplicate-completion mechanism; the exact observer ordering in the hosted failure is not claimed proven. Manual case4 has hosted SwiftUI coverage, not a newly executed device journey.At signed
494c9492596100e198b0bdb91c712ebe5888d929, three controlled iOS regressions fail before this repair: native-shaped connection failure classification, stopped-observer retry/backoff, and repeated identical status writes. Final 2,912 selected unit tests pass with zero failures/skips, including all22 backup cases. Full SwiftFormat0.64.0 reports 0/847. Complete23,101-line log and nested XCResult diagnostics were inspected: two AppIntents notices and four QoS warnings remain; three test/message identities match prior evidence, while the SendSheetLifecycle association is new. Relevant source files are unchanged, but the timing cause is unproven. Initial test-helper compilation failure is retained separately from the controlled product failures.The retry test preserves its payload across nine attempts with delays10/20/40/80/160/300/300/300seconds. The status test proves200 identical pending notifications produce no additional status writes; existing first-marker, restart and in-flight-change tests still pass. This is not a universal UI latency claim. Six fresh released-native rejection/lost-acknowledgement prepare/resume/restore phases also pass through the production backup handler, preserving original signed bytes, each process's own acknowledgement before submission, one unique operation and the complete WALLET sentinel. They use loopback protocol/mock authorization, not the complete authenticated manual journey. Two consecutive final independent reviews are clean; all four then-current live-base commits verify locally and on GitHub. Its later hosted failure is recorded above.
Signed follow-up
1ad665f8repairs the hosted UI failure in run40158322810: an Internet Connectivity Issues toast covered NavigationBack during an immediate hit-test query. The verified XCResult recording and accessibility log locate the failure at line 38; the relevant UI/test code was unchanged from the actual base. Back navigation now waits up to ten seconds for a hittable button and retains all existing assertions. Both affected navigation UI cases passed locally, full SwiftFormat checked 847 files with zero violations, and two consecutive independent reviews were clean. The complete three-commit actual-base range verifies locally and on GitHub (verified=true,reason=valid). Hosted formatter40162108133and test workflow40162108190now pass: 2,909 unit tests and 39 UI executions, zero failures, with one native-fixture unit and four UI fixture skips. Complete logs were inspected; formatter reports 0/847, and test-log notices are the installed xcbeautify and multiple-matching-destinations messages. These existing UI suites do not execute the authenticated fault journeys.The broader local UI class also selected the existing fork-payment test, which failed before confirmation. Read-only RPC calls reproduced its cause outside the app: the long-lived fork at block 517943110 could no longer fetch required historical account/EntryPoint state from its upstream RPC. No new operation was broadcast. That full class is not claimed green; the previous successful fork journeys retain their original revision/fixture scope. A healthy isolated archive-backed or fresh fork is needed for another payment journey.
October 25 follow-up (signed head
538f45c7): controlled restart-event and canceled-settings regressions failed before the fixes and pass afterward. All 19 backup component cases and the full selected native unit suite passed (2,909 tests, zero failures/skips). Full SwiftFormat 0.64.0 checked 847 files with zero violations. Complete compiler and finalized XCResult diagnostics were inspected; five QoS priority-inversion warnings remain in contacts/SDK/send tests, with no claim that their cause is fixed. The older Xcode legacy reader crashed; the current reader read the same finalized result successfully without rerunning tests. Two consecutive independent final reviews were clean. The full two-commit actual-base range verifies locally and GitHub reportsverified=true,reason=valid. Six fresh released-native rejected-backup/lost-acknowledgement prepare, resume and restore phases also passed: the same signed operation is retained, no attempt precedes its required backup acknowledgement, one unique operation is recorded and the complete WALLET sentinel remains unchanged. These exercise the app backup handler with real native signing/VSS encryption, not a full app/Paykit or deployed-contract outage journey. Revised manual cases 1 and 3 have component coverage; full fault-injection journeys remain unrun.Hosted #1122 run
40142115983crashed in subscription-set teardown; the backup source was unchanged from #1121. Controlled unchanged-source tests fail for late observer callbacks and premature shutdown/manual upload. A separate held-final-write test fails before the cleanup-order repair.At the earlier
f204e05erevision, the selected native unit/build suite passed 2,906 tests, zero failures/skips. Full SwiftFormat 0.64.0: 0/847 files. Complete structured diagnostics inspected. Three runtime priority warnings match the actual parent's test/message pairs; two additional runtime observations are in unchanged profile-destination/send-confirmation tests and SDK paths. Their timing causes remain unproven. The two compiler notices are the unchanged AppIntents metadata notices.All six released-native phases passed: rejected backup and lost acknowledgement, each with prepare, process restart/resume and restore. The signed operation stayed identical, no operation was attempted before an acknowledged backup, each scenario contained one unique operation, and the complete WALLET sentinel stayed unchanged. Complete result bundles and fixture event logs were inspected.
Two consecutive final independent reviews found no actionable findings after the ownership fixes. The backup format, required restore gates and acknowledgement-before-broadcast contract are unchanged.
Hosted workflow
40151127771forf204e05ealso passed: 2,906 unit tests and 39 UI executions, zero failures, with one native-fixture unit and four UI fixture skips. The complete log was inspected; diagnostics were limited to the installed-xcbeautify notice and multiple-matching-destinations warning. These standard CI tests do not execute the new authenticated fault journeys.Earlier parent composition at 2416046
Signed additive merge
24160466a4b6665dcff62d01ea1546b338d0b4deincorporates current #1121 at39fcaec8and the released Core RC11, Paykit RC2 and VSS RC1 adoption stack. The seven-file child delta retains immutable backup acknowledgement, observer lifecycle ownership and the existing navigation helper.Final combined-tree validation passed 2,968 selected units with zero failures/skips and 39 UI executions with zero failures (three Trezor fixture skips; the expired local-fork payment case was explicitly excluded). Full SwiftFormat 0.64.0: 0/849. Translation validation: zero errors and 15 actual-parent warnings. All 1,676 frozen input hashes match. Complete 29,901-line unit and 4,795-line UI logs and nested XCResult data were inspected. Two AppIntents notices occur in each run; unit diagnostics retain three native DHT/SDK QoS observations. UI diagnostics retain seven QoS summaries across 20 occurrences; all 534 exported frames lack symbols, so their precise cause is unproven.
Two consecutive independent final reviews are clean. Every commit in the complete six-commit current-parent range verifies locally and on GitHub (
verified=true,reason=valid). New-head hosted checks are pending. Opt-in native/integration exclusions and skipped fixtures are not passes. This composition does not establish authenticated downstream J1 or full-device backup fault acceptance; the earlier evidence below retains its exact revisions.Hosted validation at
bfc4a5e8— October 27Tests and SwiftFormat passed on the exact head tree. Complete logs, all nested XCResult test summaries, annotations and the digest/CRC-verified artifact were inspected. Unit execution counts are 2,969 passes and one native-fixture skip; UI counts are 39 passes and four fixture skips. Suite exclusions and migration-skipped jobs are not passes.
The build retains 286 distinct duplicate-symbol linker warnings. The actual-base local run does not emit them, although the seven-file PR diff leaves dependency/build/workflow configuration unchanged. Their cause and matching-hosted-base status remain unproven; they are not labelled pre-existing. No structured test failure or runtime issue was found. The pending current-master dependency composition will receive its own final validation; these results remain tied to
bfc4a5e8.Models used
gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh)gpt-6-astra(reasoning:xhigh),gpt-6.1-sol(reasoning:high)