feat(gateways): store inbound media as HTTP reference, not inline RDF - #127
Merged
Conversation
Inbound messenger attachments (voice notes, images) are persisted as a durable on-disk blob and referenced from the message record by a kb:attachment IRI resolved over the gateway's own token-gated GET /media/<id> — never embedded inline in RDF (no data: URI), regardless of size. Consistency over data-in-graph: one uniform mechanism for every attachment type keeps the life store pure triples. - inbound_store.py: kb:attachment predicate (multi-valued), store_media/ load_media (hex-keyed, traversal-safe, no RDF extension so qlever-dir ignores the blobs), write_message/undelivered carry attachment_urls. - signal/whatsapp/telegram gateways: read media bytes once, persist the durable reference BEFORE transcription so a failed/garbled transcript never costs the recording; add the original audio to the forwarded files payload (size-capped) so it rides into the dashboard conversation alongside its transcript; serve GET /media/<id> (token-gated). Co-Authored-By: Claude <noreply@anthropic.com>
…tp-reference Both sides changed how an inbound voice note is handled, for different reasons; the resolution keeps both mechanisms side by side rather than picking one: - main's never-drop path (#123) persists the message BEFORE transcription with a `kb:media` reference to a retained copy of the audio, so a failed or crashed STT run leaves a re-transcribable record. That reference is transient bookkeeping — `update_message(clear_media=True)` drops it, and the retained file is unlinked, the moment the transcript lands. - this branch's `kb:attachment` is the message's permanent media: an HTTP reference to a durable blob served by the gateway's token-gated GET /media/<id>, stored regardless of size and never inlined in RDF. They are orthogonal, so inbound_store carries both fields and every gateway threads both `media=` and `attachment_urls=` through the persist path. A voice note is therefore on disk twice between arrival and a successful transcript (the durable attachment blob plus the retry copy), and once after. Resolution details: - inbound_store: union of P_ATTACHMENT and P_MEDIA in _render/_parse, write_message and undelivered. - all three gateways: _persist_inbound and _forward_to_inbox take both `media`/`store_path` (never-drop) and `attachment_urls`; the up-front persist now carries the attachment references, and the gate/forward outcome flips the flag via _mark_delivered instead of re-persisting. - signal/whatsapp/telegram voice paths read the audio bytes and store the durable blob BEFORE _retain_media, which moves (telegram, whatsapp) or copies (signal) the temp file away. - telegram: image attachment refs are resolved ahead of the voice branch so the pre-persisted record is complete whichever media the message carries. Co-Authored-By: Claude <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What
Inbound messenger media (voice notes, images) is persisted as a durable
on-disk blob referenced by an IRI — never embedded inline in RDF as a
data:URI — regardless of size. The message record points at each blobthrough a
kb:attachmenttriple resolved over the gateway's own token-gatedGET /media/<id>.Rationale (owner's decision): consistency over data-in-graph. One uniform
mechanism for every attachment type — small or large, image or audio — keeps the
life store pure triples, instead of a size threshold that inlines some blobs and
references others.
This also fixes the original motivation: when an inbound voice note is surfaced
into a dashboard conversation, the original audio now rides along in the
forwarded
filespayload, not just its (often garbled) transcript.How
inbound_store.pykb:attachmentpredicate (multi-valued IRI object). The media type isdeliberately not stored in RDF — it is returned by the HTTP response's
Content-Typewhen the reference is resolved, which is where a media typebelongs once the payload lives behind a URL.
store_media()/load_media(): blobs keyed bytoken_hex(16)(never anuntrusted filename), with a
<id>.typesidecar.load_mediavalidates the idagainst a strict hex regex before touching the filesystem (path-traversal
safe). Blobs carry no RDF extension, so qlever-dir — which indexes only
.nt/.ttl/.n3plus declared converters — ignores them: the binaries sit onthe same volume as the message
.ntfiles without ever entering the graph.write_message()/undelivered()carryattachment_urls(deduped, emptiesdropped).
signal / whatsapp / telegram gateways
transcription, so a failed or garbled transcript never costs the recording.
base64-encoded
filespayload forwarded to triage honoursMAX_INBOUND_FILE_BYTES.filespayload (sizepermitting) so the conversation carries the recording alongside its
transcript.
GET /media/<id>(token-gated,_reply_raw).All new steps are best-effort and non-raising: any failure forwards/persists the
message without the media link rather than dropping the message.
Tests
tests/test_inbound_image_forward.pyupdated for the new(files, attachment_urls)/(voice, files, attachment_urls)return contracts and thereworded attachment note, including the oversized-image case (payload dropped,
durable reference kept).
tests/test_inbound_store.pyand the image-forwardsuite pass; all four scripts compile.
Branch policy
Tier 3 (framework
scripts/), so this goes through a PR.