You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Slice 9 of the media sync program (spec section 6.3): on the device that linked a photo, nothing is called missing merely because the app could not see it. Harness scenario S7 is unskipped and green.
notFound is the one verdict that orphans a row, and the orphan flag syncs to every device, so a wrong one on the linking device spreads everywhere. Three paths produced it without evidence of absence: a limited photo selection, a gallery query that threw, and a lost Android read grant. Each is now inconclusive (accessDenied), which writes nothing.
The change
Resolution never prompts.AssetResolutionService called checkPermission, which on mobile is a request and could show the OS prompt from a thumbnail render. It now reads through a new, non-prompting PhotoPickerService.currentPermission; the prompt comes only from the picker and the new "Allow full access" action. A permission read or gallery query that throws is accessDenied (the query used to be unavailable, which read as notFound on the linking device), logged with its stack trace under the media category.
Limited access is inconclusive. A limited selection was admitted as full access, so a photo outside it went unfound, was cached as unresolved and orphaned. A miss under limited access is now accessDenied flagged limitedAccess (on ResolutionResult and UnavailableData) and caches nothing.
A lost Android grant searches first. The native handler already reports a lost grant as PERMISSION_DENIED; LocalFileResolver ignored the code. On the linking device a content URI that stops reading now searches the photo library by the metadata tiers (new AssetResolutionService.findInLibrary, which needs no stored asset id); a lost grant the search cannot recover is accessDenied, and verify reports it as such. Another device's URI is not searched. The branch is now injectable, so it runs in the Linux test shards.
The ways back (decided 2026-09-23). Grid tiles read "Not in your allowed photos". The full-screen viewer and the media info panel offer "Allow full access" (system settings) and "Choose photo again" (the system's limited-selection sheet, PhotoManager.presentLimited; the row keeps its link), and look for the photo again when the user returns. Strings in all 11 locales.
#1625 stays open: per spec 10 it closes once the reporter or the hardware pass confirms the fix on a device.
Spec 6.3 records the decisions; the plan (docs/superpowers/plans/2026-09-23-media-sync-phase2-android-limited.md) records where execution differed from it. Main (with slices 8 and 11) is merged in.
Verification
dart format .: no changes. flutter analyze: no issues.
Full suite on the merged tree: 31,976 passed, 21 skipped, 0 failed. No two-device scenario remains skipped.
New tests for the non-prompting read, the query-failure verdict, both limited exits, findInLibrary, the content-URI branch (recovered, lost grant, failed read, peer row, verify), the placeholder, the actions widget, the viewer and tile gating, and the info panel, plus S7.
12 mutations, each compiling and failing its named test: the non-prompting read, the query-failure verdict, both limited exits (S7 and the tier-3 test), the resolver's pass-through, the grant-lost classification, the peer-row skip, serving a recovered photo, verify's mapping, the viewer-only actions gate, the panel's gallery-only gate, and the limited placeholder message.
Resolution called checkPermission, which on mobile is a request and shows
the OS prompt from a thumbnail render. It now reads the state through a
new PhotoPickerService.currentPermission. A gallery query that throws is
accessDenied rather than unavailable, which on the linking device read as
notFound and orphaned the row, and both failures log their stack trace
under the media category.
Resolution admitted a limited selection as full access, so a photo the
user had not selected went unfound, was cached unresolved, and on the
device that linked it read as notFound, which orphans the row everywhere.
Under limited access a miss is now accessDenied, flagged limitedAccess
through to UnavailableData, and caches nothing. Turns S7 green.
…thing is missing
On the device that linked a file, a content URI that stopped reading was
notFound whatever the reason, which orphans the row. The native handler
already reports a lost grant as PERMISSION_DENIED; the resolver now
searches the photo library by the metadata tiers first (a new
AssetResolutionService.findInLibrary that needs no stored asset id), and a
lost grant the search cannot recover is accessDenied, which verify reports
as such. The branch is now injectable, so it runs in the test shards.
…is out of reach
A photo outside the user's limited selection now reads 'Not in your
allowed photos' on its tile. The full-screen viewer and the info panel
offer 'Allow full access', which opens the system settings, and 'Choose
photo again', which opens the system's limited-selection sheet, and look
for the photo again when the user returns. Grid tiles show the
placeholder alone. Strings in all 11 locales.
Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the submersion-macos.zip inside it to get a runnable submersion.app. The build is ad-hoc signed — right-click → Open on first launch.
Implements media sync slice 9 by making photo resolution non-prompting, treating limited or failed access as inconclusive, and adding recovery paths for Android grants.
Changes:
Adds accessDenied handling and origin-device library re-resolution.
Adds limited-access recovery actions and localized messaging.
Expands automated coverage, documentation, and S7 scenario validation.
A cached gallery mapping whose asset stopped reading (dropped from a
limited selection, or re-indexed) fell straight to the missing verdict;
resolve and verify now search again first and keep an accessDenied
answer. The lost-grant search reduced a search that could not look to
null, so a failed read became notFound; librarySearchOutcome keeps it
accessDenied. The site media viewer offers the access actions too, and
Allow full access refreshes when the app resumes, since opening the
settings returns at once.
findInLibrary returned a cached asset id unproven, and a mapping gone
stale (a second re-index) was then served as nothing on every render,
which on the linking device reads as notFound and never clears, since
resolved entries do not expire. It runs only after a read has failed, so
it now checks the mapping still loads and searches again if not. Also
moves _afterFailedUriRead out of _volumeOnlineOrAssumed's doc comment,
which the insertion had split.
resolveAssetId and findInLibrary honoured an unexpired unresolved entry
before reading permission, so a miss cached under limited access (which
every older build wrote) or before the user narrowed access still read as
notFound on the linking device. The backoff now reads permission without
prompting: it stands under full access, and is inconclusive otherwise,
flagged limited under a limited selection.
The limited-access decision can use stale gallery results: _galleryQueryCache is keyed only by time range and lives for 30 seconds, so this search may reuse a result captured under full access (or before the user changes the limited selection). After "Choose photo again", that can keep the newly allowed asset invisible; in the opposite direction, a cached full-access candidate can be matched and cached even though it is now hidden, then the resolver falls through to notFound. Make the query cache permission/selection-aware or invalidate it when access changes before applying this verdict.
…taken under
Gallery queries are shared for 30 seconds, keyed only by time window, so
a limited search could reuse one taken under full access and match a
photo that is now hidden, and a photo just added through Choose photo
again stayed invisible until the entry expired. The key now carries the
permission, and coming back from either access action drops the shared
queries, since a changed selection keeps the same permission.
The limited-access provider's no-library and failed-read fallbacks, the
desktop currentPermission, findInLibrary with no photo library, a backoff
whose permission read throws, a library search that throws on a failed
content-URI read, verify re-finding a photo under a new id, and the info
panel re-reading access after an action.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2121
Refs #1625
Part of #2090
Slice 9 of the media sync program (spec section 6.3): on the device that linked a photo, nothing is called missing merely because the app could not see it. Harness scenario S7 is unskipped and green.
notFoundis the one verdict that orphans a row, and the orphan flag syncs to every device, so a wrong one on the linking device spreads everywhere. Three paths produced it without evidence of absence: a limited photo selection, a gallery query that threw, and a lost Android read grant. Each is now inconclusive (accessDenied), which writes nothing.The change
AssetResolutionServicecalledcheckPermission, which on mobile is a request and could show the OS prompt from a thumbnail render. It now reads through a new, non-promptingPhotoPickerService.currentPermission; the prompt comes only from the picker and the new "Allow full access" action. A permission read or gallery query that throws isaccessDenied(the query used to beunavailable, which read asnotFoundon the linking device), logged with its stack trace under the media category.accessDeniedflaggedlimitedAccess(onResolutionResultandUnavailableData) and caches nothing.PERMISSION_DENIED;LocalFileResolverignored the code. On the linking device a content URI that stops reading now searches the photo library by the metadata tiers (newAssetResolutionService.findInLibrary, which needs no stored asset id); a lost grant the search cannot recover isaccessDenied, andverifyreports it as such. Another device's URI is not searched. The branch is now injectable, so it runs in the Linux test shards.PhotoManager.presentLimited; the row keeps its link), and look for the photo again when the user returns. Strings in all 11 locales.#1625 stays open: per spec 10 it closes once the reporter or the hardware pass confirms the fix on a device.
Spec 6.3 records the decisions; the plan (
docs/superpowers/plans/2026-09-23-media-sync-phase2-android-limited.md) records where execution differed from it. Main (with slices 8 and 11) is merged in.Verification
dart format .: no changes.flutter analyze: no issues.findInLibrary, the content-URI branch (recovered, lost grant, failed read, peer row,verify), the placeholder, the actions widget, the viewer and tile gating, and the info panel, plus S7.verify's mapping, the viewer-only actions gate, the panel's gallery-only gate, and the limited placeholder message.