The pairing came back perfectly clean, and it is the whole diagnosis:
has library_file_id → refused (11/11)
no library_file_id → deleted (7/7)
And /files/{libfile_id}/download answered 200 — not 403, not 404 — with
{status, error_code, error_type, error_message}. That endpoint accepts the
Library id; it is returning an application-level error inside an HTTP 200.
The probe printed only the keys and dropped the message, which is the one
thing that says what the call is missing.
- show() now prints the full body for every response, and detects a
non-JSON 200 as possible raw bytes.
- Added routes worth ruling in or out: the same call as POST, with a
conversation_id, /files/{lib}/content, /content?asset_pointer=, and four
Library listing endpoints — a listing usually reveals both the id form
the UI uses and the route that actually serves bytes.
- Probes a second Library id too, so one odd file cannot mislead us.
Dumping the raw message metadata found the identity the asset pointer never
carried:
"id": "file_000000003454722f9481506b96aed510" ← refused
"library_file_id": "libfile_4eb82f478fe081919127e2eba9886e86"
"source": "local"
The exporter only knows the sediment id from the asset_pointer and asks
/files/{sediment}/download, which 403s for these. The Library is a separate
store with its own ids, so we have been asking for the conversation-scoped
copy of a file that now lives in the Library. That also fits the 2026-07-14
creation-time cluster: a Library migration would mint exactly these new
records.
Neither gizmo_id, conversation_id, a /gizmos path nor a project Referer
helped (403/404), so scope was never the missing piece — identity was.
library_download_probe.py pairs each refused sediment id with its
library_file_id from message.metadata.attachments and tries the endpoints
that could serve it.