docs/adr/0064-possessor-voice-seam.md

ADR 0064: The possessor voice seam

Status: superseded in scope (2026-08-19) by ADR 0129.

Current status (2026-08-19)

The no-script, push-only, loopback design survives as neutral @clankie/play-voice, but only for Clankie's own local or hosted play. External harnesses receive no bearer, narration path, room state, or room input. The possessor terminology, clankie_possessor_voice credential, and diagram below describe the retired architecture.

Context

A harness can possess Clankie's GBA body and play FireRed, and the activity plane lets people watch the frames. What nobody could do is hear him talk about it. clankie_say and clankie_listen default to deniedSpeechPort and deniedHearingPort as their only implementations — correct refusals with no counterpart, so every call failed with a reason and commentary is impossible.

The refusal exists for a real reason rather than an unfinished wire. Speaking through the Clankie service's POST /v1/discord/presence-actions requires a live presence claim: the session id, phase, and monotonic revision the bridge publishes while it holds the gateway. Only the process holding the gateway can mint one, which is precisely what fences an action against a session that is live right now. A possessor holds no gateway, so it holds no claim, and no amount of wiring inside the possessor can produce one.

Two further constraints shape the answer:

  • A possessor is a local process by construction, and it is the less trusted side of this boundary. It should not hold Discord credentials, and the credential-holding process should not dial out to it.
  • The bridge deliberately retains no transcripts. Raw and generated PCM are zeroed after use. Any hearing mechanism that let a possessor ask for "the last N lines" would silently force the bridge to start retaining them.

Decision

A possessor never speaks directly. It reports to the process that owns the body in Discord, over a loopback seam, and that process speaks.

ADR 0064: The possessor voice seam

Four properties make this the same fence rather than a hole in it:

The possessor supplies the event; the persona supplies the words. Narration is seeded with createTextItem and never spoken verbatim. What Clankie says about walking into a wall is his, composed in the voice the briefing supplies and folded in with whatever the room is saying. A possessor that could script his dialogue would be choosing how he sounds, which is exactly what ADR 0051 reserves to the character.

The transport is loopback-only with a brokered bearer. The listener binds 127.0.0.1 and never a routable interface; the token is minted into the credential broker under clankie_possessor_voice on the bridge's first start and is a hard startup error in the environment. The bearer is the second lock, not the only one — the same shape as the activity producer listener.

The wire carries two messages and nothing else. narrate in, utterance out. A possessor cannot choose an audience, join or leave a channel, or reach any other presence action from here. It drives the character; it does not pick new rooms.

Hearing is push, not pull. The bridge hands each attributed line to live subscribers as it happens, so its retention stays at zero. What a possessor then holds is bounded, its own, and discarded when the lease ends. Nothing captured outside the existing consent registry crosses the seam, and raw audio never does. Which lines qualify is widened by ADR 0098 (room text) to include admitted text; the retention floor and the audio fence are unchanged.

Rate-limiting narration, not narration's content

A play loop reports constantly: every step, every bump, every menu. Answering each report would turn a voice channel into a monologue nobody can talk over. Seeding is therefore unbounded and responding is rate-limited (narrationMinIntervalMs, default 12 s). He always knows what his body just did — the alternative is narrating a past he never saw — but he speaks about it at the pace of a person watching a game, not a process emitting events. A drop is receipt-visible as possessor_narration_suppressed with reason playing, rate_limited, or responding — the last covering a response already asked for but not yet audible, which the realtime session would reject as a second active response. Asked play mints one speechDeliveryId on the journal turn and carries it across the seam so the submission, the spoken response, and the drop share a join key. The receipts stay content-free.

Consequences

  • clankie_say and clankie_listen work when the bridge is up, the seam is enabled, and he is in a voice channel; they refuse with a specific reason otherwise. Deny-by-default is unchanged: absent credential, absent bridge, or absent voice session all mean silence with an explanation.
  • The bridge gains one loopback listener and one public method on the voice session. It gains no new credential class, no inbound public surface, and no retention.
  • Narration cannot be used to make Clankie say a specific sentence. That is the point, and it means the seam is unsuitable as a general text-to-speech path — anything needing verbatim output belongs on the presence-action path with a live claim.
  • A possessor that dies mid-session simply stops reporting. The room does not wait for it, and nothing is queued on its behalf.