Skip to content

feat(gateways): store inbound media as HTTP reference, not inline RDF - #127

Open
retog wants to merge 1 commit into
mainfrom
feat/inbound-media-http-reference
Open

feat(gateways): store inbound media as HTTP reference, not inline RDF#127
retog wants to merge 1 commit into
mainfrom
feat/inbound-media-http-reference

Conversation

@retog

@retog retog commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

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 blob
through a kb:attachment triple resolved over the gateway's own token-gated
GET /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 files payload, not just its (often garbled) transcript.

How

inbound_store.py

  • New kb:attachment predicate (multi-valued IRI object). The media type is
    deliberately not stored in RDF — it is returned by the HTTP response's
    Content-Type when the reference is resolved, which is where a media type
    belongs once the payload lives behind a URL.
  • store_media() / load_media(): blobs keyed by token_hex(16) (never an
    untrusted filename), with a <id>.type sidecar. load_media validates the id
    against a strict hex regex before touching the filesystem (path-traversal
    safe). Blobs carry no RDF extension, so qlever-dir — which indexes only
    .nt/.ttl/.n3 plus declared converters — ignores them: the binaries sit on
    the same volume as the message .nt files without ever entering the graph.
  • write_message() / undelivered() carry attachment_urls (deduped, empties
    dropped).

signal / whatsapp / telegram gateways

  • Read the media bytes once and persist the durable reference before
    transcription, so a failed or garbled transcript never costs the recording.
  • The durable reference is stored regardless of size; only the transient,
    base64-encoded files payload forwarded to triage honours
    MAX_INBOUND_FILE_BYTES.
  • Add the original voice-note audio to the forwarded files payload (size
    permitting) so the conversation carries the recording alongside its
    transcript.
  • Serve 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.py updated for the new (files, attachment_urls) / (voice, files, attachment_urls) return contracts and the
reworded attachment note, including the oversized-image case (payload dropped,
durable reference kept). tests/test_inbound_store.py and the image-forward
suite pass; all four scripts compile.

Branch policy

Tier 3 (framework scripts/), so this goes through a PR.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant