Add force re-upload from cache for expired assets#530
Conversation
Adds manual re-upload capability for expired assets from the debug view: - Enhanced ExpiredAssetRecoveryHandler with optional cache recovery support - Added forceReuploadAssetFromCache to SessionManagerProtocol - Implemented in SessionManager with proper writer initialization - Wired through debug view hierarchy with context menu action
How to use the Graphite Merge QueueAdd the label merge-queue to this PR to add it to the merge queue. You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
Claude finished @lourou's task —— View job Code Review Complete ✅I've completed the code review of PR #530. Here are my findings organized by category: Critical Issues 🔴1. Race Condition in Debug UI (DebugAssetRenewalView.swift:156, 164)The "Renew Asset" and "Force Re-upload from Cache" buttons can run concurrently on the same asset. The renew button checks Recommendation: // Line 156 and 164 - ensure both operations are mutually exclusive
.disabled(isRenewing || isReuploading || asset.key == nil)2. Missing renewedKeys Accumulation (AssetRenewalManager.swift:122)In Recommendation: // Line 124 - include accumulated renewedKeys
return AssetRenewalResult(renewed: totalRenewed, failed: totalFailed, expiredKeys: allExpiredKeys, renewedKeys: allRenewedKeys)Medium Issues 🟡3. Deferred Recovery Falls Through to Clear (SessionManager.swift:577-582)When Recommendation: Check the actual recovery result before creating a second handler. If 4. Silent Clear Failures (ExpiredAssetRecoveryHandler.swift:46-49)The Recommendation: Log clear failures explicitly in 5. Missing onRecoveryDeferred in Manual Recovery (SessionManager.swift:664-673)The Recommendation: Either add the callback or add a comment explaining why deferred is impossible in this context. Low Priority Issues 🟢6. Profile Avatar Recovery for Other Users (SessionManager.swift:617-636)For 7. MockInboxesService Missing Writers (MockInboxesService.swift:138)The mock creates Positive Observations ✅
Performance Considerations ⚡
Security Considerations 🔒No security concerns identified. The implementation:
Test Coverage Assessment 📊Strengths:
Gaps:
Recommendations Summary
Overall, this is a well-architected feature with good test coverage. The issues identified are mostly edge cases and minor bugs that should be straightforward to fix. |
…ssing deferred callback, renewedKeys accumulation

Add force re-upload from cache for expired assets
Adds manual re-upload capability for expired assets from the debug view:
Fix asset reupload compile regressions and start assessment log
Assess renewal accounting and document asset renewal findings
Fix renewal timestamp accounting using explicit renewed keys
Preserve avatar renewal timestamps during conversation sync
Defer expired asset recovery from startup via queued foreground processing
Add tests for deferred asset recovery queue and handler behavior
Update assessment log with deferred recovery implementation status
Update recovery handler test cache stub for latest image cache protocol
Harden deferred asset recovery flow with capped retries and serialized processing
Expand deferred recovery tests for queue semantics and defer result
Align app settings and debug asset view with project style conventions
Add group image recovery-path coverage for expired asset handler
Note
Recover expired assets from cache in
ExpiredAssetRecoveryHandlerwith deferred processingExpiredAssetRecoveryHandler.handleExpiredAssetnow attempts to recover assets from cache usingMyProfileWriterorConversationMetadataWriter, defers processing when writers are unavailable viaDeferredAssetRecoveryQueue, and only clears URLs as a last resortSessionManagerprocesses deferred assets in background on app foreground with retry logic (max 3 attempts), and exposesforceReuploadAssetFromCacheto manually trigger cache recovery for a specific assetAssetRenewalResultincludesrenewedKeysfield to track which keys were successfully renewed, fixing timestamp updates to only apply to explicitly renewed assetsConversationWriter.storepreservesavatarLastRenewedwhen avatar URLs remain unchanged during conversation sync📊 Macroscope summarized f83030c. 13 files reviewed, 8 issues evaluated, 7 issues filtered, 0 comments posted
🗂️ Filtered Issues
Convos/Debug View/DebugAssetRenewalView.swift — 0 comments posted, 1 evaluated, 1 filtered
.disabled(isReuploading)modifier doesn't account for whenisRenewingis true, and similarly the renew button doesn't checkisReuploading. This allows both operations to run concurrently on the same asset, potentially causing race conditions or inconsistent state when bothrenewSingleAsset()andforceReuploadFromCache()execute simultaneously. [ Low confidence ]ConvosCore/Sources/ConvosCore/Assets/AssetRenewalManager.swift — 0 comments posted, 1 evaluated, 1 filtered
performRenewal, the returnedAssetRenewalResultat line 122 does not include accumulatedrenewedKeys. WhileallExpiredKeysis properly accumulated across batches, there is no correspondingallRenewedKeysaccumulation. The result uses the default empty array forrenewedKeys, meaning callers offorceRenewal()will always receive an emptyrenewedKeysarray even when assets were successfully renewed. This is inconsistent with howexpiredKeysis handled and could break callers that depend on knowing which keys were renewed. [ Out of scope ]ConvosCore/Sources/ConvosCore/Assets/ExpiredAssetRecoveryHandler.swift — 0 comments posted, 1 evaluated, 1 filtered
clearAssetUrlmethod swallows database write errors silently (lines 152-154), buthandleExpiredAssetreturns.clearedregardless of whether the clear operation succeeded (lines 37-38, 43-44). This could mislead callers into believing the asset URL was cleared when the database write actually failed. [ Out of scope ]ConvosCore/Sources/ConvosCore/Inboxes/MockInboxesService.swift — 0 comments posted, 1 evaluated, 1 filtered
makeAssetRenewalManager, theExpiredAssetRecoveryHandleris created withimageCachebut withoutmyProfileWriter,conversationMetadataWriter, oronRecoveryDeferred. IfattemptRecoveryFromCachefinds a cached image but no writer is available, it returns.deferred. SinceonRecoveryDeferredis nil,handleExpiredAssetwill then clear the asset URL even though the cached image exists and recovery might have been possible with proper writers configured. This could result in unnecessary asset loss in mock scenarios. [ Low confidence ]ConvosCore/Sources/ConvosCore/Sessions/SessionManager.swift — 0 comments posted, 4 evaluated, 3 filtered
processDeferredAssetRecoveries, whenforceReuploadAssetFromCachereturnsfalsebecause the recovery handler inside it already returned.cleared(meaning the asset URL was already cleared in the database), the code at lines 569-570 creates anotherExpiredAssetRecoveryHandlerand callshandleExpiredAssetagain. This results in redundant database operations attempting to clear an already-cleared asset URL. While not a crash, this wastes database writes. [ Low confidence ]forceReuploadAssetFromCachefor.profileAvatarassets (line 617-624), the code usesassetInboxIdfrom theRenewableAssettuple to obtain the messaging service. If theRenewableAsset.profileAvatarwas created for another user's cached avatar (not the current user's profile), this would attempt to get a service for an inbox that doesn't belong to us. The call tomessagingService(for:inboxId:)would fail, causing retries until exhaustion and eventual clearing of a URL that may have been legitimate. This depends on whetherAssetRenewalURLCollectoronly collects the current user's avatars. [ Low confidence ]ExpiredAssetRecoveryHandlercreated inforceReuploadAssetFromCacheat lines 652-657 does not include anonRecoveryDeferredcallback. If for any reason the recovery returns.deferred(though currently the writers are provided so this shouldn't happen), the handler will fall through to clearing the asset with a warning log rather than properly deferring it for later processing. [ Low confidence ]