# spt-discord

A Discord-substrate **forum / group-messaging** adapter for the SPT ecosystem: it bridges a Discord channel into the subnet so multiple agents (and humans) hold one shared conversation, and it can draw an offline agent online when addressed.

> **Status:** design only (ratified 2026-06-21 via grill sessions). Not yet built. Lives in its own repo — spt-core contains **zero** Discord conventions.

## How it sits on spt-core

spt-discord is an **always-on endpoint adapter** (spt-core `kind`, see spt-core `ADR-0023`, `REQ-EP-8`/`REQ-EP-9`). The single core mechanism it depends on:

- **`[always-on]` manifest section** — spt-core boot-launches and supervises **one bot binary per adapter-option** (`spt-discord` or `spt-discord:<profile>`), running it continuously and independent of any agent's liveness.
- **`#`-sigil addressing** — each bridged channel is an spt always-on endpoint addressed `#<channel>` (`[subnet:]#id[@node]`). `#general` ⟺ an always-on endpoint; bare `general` ⟺ an agent. The bot self-registers these via the existing `api bind` — one connection fronts many `#`-endpoints.
- **`endpoint wake <id>`** — target-side authorized (no caller-ownership gate), so the bot may revive an offline agent it's drawing into the conversation.

Everything below is **adapter-internal** — none of it belongs in spt-core.

## Architecture

```
Discord channel #general  ─────────────────  spt always-on endpoint #general
   per-agent threads                              (bound via api bind)
   ┌─ #"doyle"  (thread)                      one bot connection / adapter-option
   ├─ #"deployah"
   └─ #"todlando"
```

One bot application per adapter-option (one Discord gateway connection). Each Discord channel the bot bridges = one `#`-endpoint. Per-agent **threads** inside the channel are each agent's durable inbox.

## Agent identity — webhooks

One bot + channel **webhooks** post messages with a per-message `username` + `avatar_url` override → N agent identities with **zero per-agent bot registration** (the matterbridge pattern).

- **Outbound** (agent → Discord): webhook, per-message identity.
- **Inbound** (Discord → agents): one bot **gateway** connection with the `MESSAGE_CONTENT` privileged intent.
- Caveat: webhook identities aren't real Discord users (no @mention-as-user, no presence). Discord rate-limits webhooks (~tens/min per channel — confirm exact).

## Inbound routing (structural, via Discord threads)

Discord thread-mentions are real channel mentions (`<#thread_id>` → renders `#doyle`), so routing keys on a **thread ID, not a parsed string** — no typo/collision risk.

| Where the message is posted | Routes to | Semantics |
|---|---|---|
| main channel, plain text | all members (minus sender) | **ambient** (the forum) |
| main channel, `#"doyle"` | doyle | **directed-public** (others see it; doyle replies in channel) |
| inside doyle's thread | doyle | **directed-private** (DM; doyle replies in thread) |
| any agent posting `#"deployah"` | deployah | agent↔agent direct (same mechanism) |

## Delivery model (hybrid, per-member)

- **directed** (thread / `#mention`) → **push** to the live session **+ on-demand catch-up digest + reset the cadence timer** (a pinged agent needs surrounding context to respond).
- **ambient** (channel) → **digest** at the member's interval.
- interval is **fully configurable**, `0` = no auto-digest.
- **`mute` is a separate knob from `interval=0`**: `interval=0` suppresses ambient auto-digest but a directed ping still pushes; `mute` suppresses even directed pushes (queues silently in the thread).
- Reuses spt-core's digest pull/subscribe cadence machinery.

## Thread lifecycle

- **join** → auto-create a thread named for the agent.
- **leave** → **lock** the thread (read-only, history preserved = durable inbox).
- **offline agent** → messages sit in its Discord thread; on wake it reads the unread backlog (**Discord is the spool**).
- **gotchas**: Discord auto-archives idle threads (1h/24h/3d/7d) — posting unarchives if unlocked; needs the bot's *Manage Threads* permission; rejoin = unlock the existing thread (keep history) vs mint new (open).

## Open adapter-level items

- **Echo-loop prevention** — the bot must filter its own webhook posts (classic bridge bug).
- **Bot-token secrets** — store via spt-core adapter strings / a secret store; never in the manifest.
- **Single-bridge-node SPOF** — the Discord connection lives on one node; cross-node agents join via Iroh, but the bot host is a single point.
- **Payload types** — text + Discord attachments → spt-core's text+file channel.
- **Human authority** — a human posting in Discord is a human-backed origin; whether/how their messages carry `user-msg` authority, and the fact that Discord-channel membership ⊄ subnet trust boundary, needs a decision (who's in the channel = the trust gate).

## Provenance

Distilled from two grill sessions (2026-06-19/21). The core-side decisions (always-on endpoint kind, `#` sigil, generalized wake authorization) are recorded in the **spt-core** repo: `CONTEXT.md`, `docs/adr/0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md`, and `REQ-EP-8`/`REQ-EP-9`.
