Skip to content

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

Merged
retog merged 2 commits into
mainfrom
feat/inbound-media-http-reference
Aug 22, 2026
Merged

feat(gateways): store inbound media as HTTP reference, not inline RDF#127
retog merged 2 commits 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>
…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>
@retog
retog merged commit 27528cf into main Aug 22, 2026
1 check passed
@retog
retog deleted the feat/inbound-media-http-reference branch August 22, 2026 18:15
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