Skip to content

feat(relay,mcpl): markdown-preserving split sends + opt-in inline audio (RFC-006) - #2

Open
wolframs wants to merge 3 commits into
anima-research:mainfrom
wolframs:feat/split-sends-inline-audio
Open

feat(relay,mcpl): markdown-preserving split sends + opt-in inline audio (RFC-006)#2
wolframs wants to merge 3 commits into
anima-research:mainfrom
wolframs:feat/split-sends-inline-audio

Conversation

@wolframs

@wolframs wolframs commented Jul 8, 2026

Copy link
Copy Markdown

Ports two capabilities from chapterx (PRs #12, #13, with membrane #29 context) onto portal's relay architecture. Full design + verification notes in PORTAL-RFC-006.

1. Markdown-preserving split sends (relay)

Previously a >2000-char send_message bubbled a raw discord.js error as INTERNAL. Now:

  • discord-markdown.ts (ported verbatim from chapterx, with its 140-test suite): splits content closing every markdown construct open at a chunk boundary and reopening it in the next chunk, recording the synthetic bridge strings per chunk.
  • Parts go out via WebhookPool.sendMany as one queue item — no same-webhook send can interleave. Files ride on the last part. Partial failure records the posted parts and names their relay ids in the error.
  • Bridge strings are persisted in the attribution store and stripped at buildPortalMessage (live events, history, pins) — agents always see their original unbroken markdown, across restarts.
  • SendMessageResult.messageIds + PortalMessage.partOf let clients group parts (additive; no protocol version bump).
  • edit_message re-splits across existing parts (surplus deleted; growth refused with INVALID_PARAMS); delete_message fans out over all parts.

2. Inline audio for agents (mcpl) + protocol audio metadata

  • New set_audio_visibility tool: per-channel, durable, default-OFF opt-in (mirrors the reactions pattern). Content-shaping only — never wakes.
  • Opted-in channels get audio attachments delivered as MCPL audio blocks: MIME normalization (MP3 aliases → audio/mp3), extension fallback for Discord's optional content-type, 12 MiB cap, max 2 clips/message, notes explaining anything not inlined.
  • Oversized clips get a native 🐘 reaction via the existing react RPC (shared-bot, idempotent across agents).
  • PortalAttachment gains duration/waveform (Discord voice-message fields).

Verification

  • 261 unit tests across the four packages (140 ported splitter tests + 11 split-send + 4 audio are new).
  • Live-verified on a real guild (scripts/rfc6-live.mjs, 21/21): 4-part split send, stripped live events/history, reassembly equality, partOf links, shrink/collapse/grow-refusal edits, delete fan-out, audio CDN round-trip, native 🐘.
  • Live-verified with a real model (scripts/mimo-audio-live.mjs): a spoken .wav posted by one persona reached a MiMo v2.5-driven persona as an mcpl audio block and the model quoted the spoken sentence verbatim.
  • The first live run exposed an own-echo/attribution race (gateway echo of a freshly-posted part beating store.record) — fixed via per-part recording (sendMany onSent callback) plus holding unrecorded own-webhook echoes until the send path flushes them; this also fixed a pre-existing single-send attribution race.

Version bumps left out on purpose (repo does chore(release) commits separately).

🤖 Generated with Claude Code

wolframs and others added 3 commits July 8, 2026 21:26
…io (RFC-006)

Ports chapterx PR #13 (markdown across message splits) and PR #12 (audio
input; membrane PR #29 context) onto portal's relay architecture.

Split sends (relay):
- discord-markdown.ts: splitter engine ported from chapterx — closes/reopens
  fences & emphasis at chunk boundaries, records synthetic bridge strings
- sendMessage splits >2000-char content; WebhookPool.sendMany keeps parts
  contiguous on the persona's webhook queue (send() now delegates to it);
  partial failures record the posted parts and name them in the error
- MessageStore persists bridgeOpen/bridgeClose/parts/partOf per part;
  buildPortalMessage strips bridges so agents see their original markdown
  (history, live events, pins), with PortalMessage.partOf for grouping
- edit_message re-splits across existing parts (surplus deleted, growth
  refused with INVALID_PARAMS); delete_message fans out over all parts and
  surfaces failures instead of silently succeeding
- SendMessageResult.messageIds lists all parts (additive, no version bump)

Inline audio (mcpl + protocol):
- PortalAttachment.duration/waveform captured from Discord voice messages
- set_audio_visibility tool: durable per-channel opt-in (default OFF),
  mirroring the reactions pattern; content-shaping only, never wakes
- buildContent inlines audio as MCPL audio blocks (12MB cap, max 2/message,
  MIME normalized: mpeg/mpeg3/x-mpeg-3 → mp3, extension fallback when
  Discord omits contentType); attachment fetches now run concurrently
- oversized clips flagged with a native 🐘 reaction (idempotent shared-bot)

Tests: 140 splitter tests ported from chapterx + new split-send integration
tests (contiguity, partial failure, store round-trip, strip/reassemble) and
audio tests (opt-in persistence, MIME normalization/detection). 257 total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ication

Live-tested RFC-006 on a real guild (scripts/rfc6-live.mjs, new): 3 of 21
checks failed, all in the live-event path —

- Split-send parts were recorded only after the whole batch resolved, but
  each part's gateway create echo arrives while later parts are still
  posting: live events went out with bridge markers unstripped and no
  partOf. sendMany now fires an onSent callback per posted part and the
  relay records attribution/bridge metadata immediately.
- Even per-part, the gateway echo can beat the REST response, so
  onDiscordMessage now holds an own-webhook echo that has no store ref
  until the send path records and flushes it (5s timer as safety net).
  This also fixes the pre-existing single-send attribution race.
- editMessage removed surplus-part store refs itself, so the gateway's
  delete echo emitted raw Discord ids instead of relay ids. Cleanup is
  now left to the echo, exactly like delete_message.

Re-run: 21/21 live checks green; 227 relay unit tests (2 new for onSent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…a persona

scripts/mimo-audio-live.mjs: one persona posts a spoken .wav, a second
persona driven by xiaomi/mimo-v2.5 (OpenRouter, provider order pinned like
chapterx's mimo.yaml) consumes it through portal-mcpl's real buildContent
(now exported for live verification) mapped to an input_audio part exactly
as membrane's adapter does. The model quoted the spoken sentence verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wolframs

wolframs commented Jul 8, 2026

Copy link
Copy Markdown
Author
image

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