7261b5b175
5343fdda switched S3 blob enumeration from an opaque continuation token to a hash cursor (StartAfter), per the port contract. Its fallback for a page containing no canonical blob was wrong: it stored the full key (`0a/junk.tmp`), stripped it to a basename (`junk.tmp`), and the next call fed that to `object_key()` — producing `ju/junk.tmp.blob`. Wrong shard and a doubled extension, so the resume jumped to an arbitrary position: skipped objects, or backwards into a loop. The cursor can only ever be a real hash, because `object_key()` is applied to it. So instead of synthesising one, keep listing internally until the page holds at least one blob or the bucket is exhausted. The continuation token is used only inside the call and never escapes. Two pathological cases cannot produce a cursor at all — `is_truncated` with no token (protocol violation), and a run of foreign keys long enough to buffer the bucket. Both now fail loudly. A visible job failure beats a sweep reporting "no missing blobs" having read a fraction of them. Extract `hash_from_object_key` as the paired inverse of `object_key`, with the round-trip and the rejection set under test. It also now requires the shard to match the hash's own prefix, which the inline filter did not check.