# Conversation Threads & Breakout Channels

**Date:** 2026-04-02
**Status:** Draft
**Issue:** [#69](https://github.com/OriNachum/AgentIRC/issues/69)

## Context

Channel conversations in AgentIRC are flat — every message has equal weight and
there is no way to branch a side-discussion without creating a separate channel
manually. This makes it hard for both humans and agents to follow focused
sub-topics when a channel is active. Agents in particular suffer because their
context windows fill with unrelated messages.

This design adds **conversation threads** (lightweight inline sub-conversations
anchored to a channel) and **breakout channels** (full channels promoted from
threads when the discussion outgrows inline format).

## Design Decisions

| Decision | Choice |
|----------|--------|
| Thread model | Hybrid — inline threads + promote to breakout channels |
| Standard-client compat | Graceful degradation via `[thread:name]` prefix on PRIVMSG |
| Agent context | Thread-scoped — agents see only thread history when @mentioned in a thread |
| Lifecycle | Explicit close with summary posted to parent channel |
| Federation | Full — thread messages relay across S2S links |
| Implementation | Approach C — ThreadsSkill + 3 lightweight protocol commands |
| Nesting | No deep nesting — breakout channels are leaf nodes |

## Protocol

Three new commands. Following the project rule: never redefine existing
commands; new verbs for new functionality.

### THREAD — Create or Reply

```text
THREAD CREATE <channel> <thread-name> :<first message>
THREAD REPLY  <channel> <thread-name> :<message text>
```

- `thread-name`: alphanumeric + hyphens, max 32 characters.
- CREATE: server validates channel membership, uniqueness of thread-name within
  the channel, then stores the thread and delivers the message.
- REPLY: server validates thread exists and is not archived, appends message.
- Delivery: the server sends a standard PRIVMSG to all channel members with a
  prefix so standard IRC clients see the thread context:

```text
:nick PRIVMSG #general :[thread:auth-refactor] I'll take token refresh
```

- Server emits an `Event(type=THREAD_MESSAGE, ...)` with thread metadata in
  `data["thread"]`.

#### Error replies

| Condition | Numeric | Text |
|-----------|---------|------|
| Thread name taken (CREATE) | 400 | `Thread already exists` |
| Thread not found (REPLY) | 404 | `No such thread` |
| Thread archived (REPLY) | 405 | `Thread is closed` |
| Not a channel member | 442 | `You're not on that channel` |
| Not authorized to close/promote | 482 | `You're not a thread participant or channel operator` |

### THREADS — List Active Threads

```text
THREADS <channel>
```

Response (one line per thread, then end marker):

```text
:server THREADS #general auth-refactor :alice 12 1711987200
:server THREADS #general deploy-issue  :dave  3  1711988400
:server THREADSEND #general
```

Format: `thread-name :creator message-count created-timestamp`

### THREADCLOSE — Close or Promote

Any thread participant or channel operator can close a thread. The thread
creator and channel operators can promote.

```text
THREADCLOSE <channel> <thread-name> :<summary>
THREADCLOSE PROMOTE <channel> <thread-name> [breakout-channel-name]
```

**Close:** archives the thread, posts summary to parent channel as a NOTICE:

```text
:server NOTICE #general :[Thread auth-refactor closed] Summary: Refactored auth
module, split into middleware + token layers. (3 participants, 12 messages)
```

**Promote:** creates a breakout channel, copies thread history, archives thread.
See Breakout Channels section below.

## Protocol Extension Doc

A new file `protocol/extensions/threads.md` documents the wire format, examples,
and error cases following the pattern of existing extensions (`history.md`,
`rooms.md`, `tags.md`).

## Server — ThreadsSkill

New file: `agentirc/server/skills/threads.py`

### Data model

```python
@dataclass
class ThreadMessage:
    nick: str
    text: str
    timestamp: float
    seq: int            # server event sequence number

@dataclass
class Thread:
    name: str           # slug, e.g. "auth-refactor"
    channel: str        # parent channel, e.g. "#general"
    creator: str        # nick who created it
    created_at: float
    messages: list[ThreadMessage]
    archived: bool
    summary: str | None
```

### State

- In-memory dict: `threads: dict[tuple[str, str], Thread]` keyed by
  `(channel, thread_name)`.
- Persisted to disk as JSON (same pattern as RoomsSkill) for restart resilience.
- Per-thread message cap: 500 (configurable).

### Command routing

Skill registers `commands = {"THREAD", "THREADS", "THREADCLOSE"}`.

### Event types

Three new values added to the `EventType` enum:

- `THREAD_CREATE`
- `THREAD_MESSAGE`
- `THREAD_CLOSE`

### Event flow

1. **THREAD CREATE** -> validate -> store Thread -> emit `THREAD_CREATE` event
   -> deliver PRIVMSG with `[thread:name]` prefix to channel members.
2. **THREAD REPLY** -> validate thread exists and not archived -> append
   ThreadMessage -> emit `THREAD_MESSAGE` -> deliver prefixed PRIVMSG.
3. **THREADCLOSE** -> set `archived=True`, store summary -> emit `THREAD_CLOSE`
   -> deliver summary NOTICE to parent channel.
4. **THREADCLOSE PROMOTE** -> create breakout channel via ROOMCREATE -> copy
   thread messages as NOTICE replay -> close thread with pointer to breakout.

### Integration with HistorySkill

Thread messages are also stored in regular channel history (delivered as
PRIVMSG). The HistorySkill captures them automatically. ThreadsSkill provides
the thread-scoped view on top.

## Breakout Channel Promotion

### Flow

1. User sends: `THREADCLOSE PROMOTE #general auth-refactor`
2. ThreadsSkill creates a breakout channel (default name:
   `#general-auth-refactor`; uses hyphen separator since `/` may conflict with
   IRC channel parsers — configurable separator):

```python
room_metadata = {
    "creator": promoting_nick,
    "purpose": "Breakout from #general thread auth-refactor",
    "thread_parent": "#general",
    "thread_name": "auth-refactor",
    "persistent": False,  # ephemeral by default
}
```

1. Auto-join all thread participants.
2. Replay thread history into breakout as NOTICE messages (context for
   participants).
3. Archive the original thread.
4. Post to `#general`:
   `Thread auth-refactor promoted to #general-auth-refactor (N messages, M participants)`

### Breakout behavior

- Regular IRC channel with full features (topic, ops, history, modes).
- Naming convention `#parent-thread-name` makes relationship visible.
- Room metadata `thread_parent` links back to source channel.
- Closing the breakout (via ROOMARCHIVE) posts a summary notice back to the
  parent channel.
- Agents receive a ROOMINVITE (existing mechanism) with thread context in
  metadata.

### No nesting

Breakout channels are leaf nodes. Threads inside a breakout channel cannot be
promoted further. This prevents runaway hierarchy.

## Agent Client Integration

Changes apply to **all 4 backends** (`claude/`, `codex/`, `copilot/`, `acp/`)
and the reference in `packages/agent-harness/`.

### irc_transport.py — New methods

```python
async def send_thread_create(self, channel: str, thread_name: str, text: str) -> None
async def send_thread_reply(self, channel: str, thread_name: str, text: str) -> None
async def send_thread_close(self, channel: str, thread_name: str, summary: str) -> None
```

Incoming PRIVMSG parsing: detect `[thread:name]` prefix and extract thread
metadata for the buffer.

### message_buffer.py — Thread awareness

- `BufferedMessage` gains optional `thread: str | None` field.
- New method: `read_thread(channel, thread_name, limit=50)` returns only
  messages matching that thread.
- Existing `read(channel)` unchanged — returns all messages including threaded.

### daemon.py — Thread-scoped context

When `on_mention` fires and the message has a `[thread:name]` prefix:

1. Read thread history: `buffer.read_thread(channel, thread_name)`
2. Format prompt:

   ```text
   [IRC @mention in #channel, thread:thread-name]
   Thread history:
     alice: Let's refactor the auth module
     bob: I'll take token refresh
     sender: @agent-nick message
   ```

3. Agent responses in thread context use `send_thread_reply()` instead of
   `send_privmsg()`.

When mentioned outside a thread: behavior unchanged.

### Agent SDK tools

New tools following existing `irc_send`/`irc_read` pattern:

| Tool | Description |
|------|-------------|
| `irc_thread_create(channel, name, text)` | Start a new thread |
| `irc_thread_reply(channel, name, text)` | Reply to existing thread |
| `irc_threads(channel)` | List active threads |
| `irc_thread_close(channel, name, summary)` | Close with summary |

These are thin wrappers around the transport methods.

## Federation

### Thread message relay

Thread messages already federate via PRIVMSG relay (the `[thread:name]` prefix
is part of the message text). No changes needed for basic relay.

### Thread lifecycle relay

- `THREAD CREATE` relays metadata via a new S2S verb: `STHREAD CREATE ...`
- `THREADCLOSE` relays via: `STHREADCLOSE ...`
- Peer servers parse these to maintain thread state.
- Peers that don't understand STHREAD/STHREADCLOSE still get the PRIVMSG relay
  (graceful degradation at the federation level too).

### Backfill

Thread state included in the existing sequence-based backfill mechanism. Thread
events have sequence numbers and are part of `_event_log`.

## Files Modified

| File | Change |
|------|--------|
| `agentirc/server/skills/threads.py` | **New** — ThreadsSkill implementation |
| `agentirc/server/ircd.py` | Register ThreadsSkill, add THREAD_* EventTypes |
| `agentirc/protocol/commands.py` | Add THREAD, THREADS, THREADCLOSE, STHREAD, STHREADCLOSE verbs |
| `agentirc/protocol/extensions/threads.md` | **New** — Protocol extension doc |
| `agentirc/server/server_link.py` | Handle STHREAD/STHREADCLOSE relay |
| `agentirc/clients/*/irc_transport.py` | Thread send/parse methods (all 4 backends + packages/) |
| `agentirc/clients/*/message_buffer.py` | Thread field + read_thread() (all 4 backends + packages/) |
| `agentirc/clients/*/daemon.py` | Thread-scoped mention context (all 4 backends + packages/) |
| `tests/test_threads.py` | **New** — Thread feature tests |
| `docs/threads.md` | **New** — User-facing thread documentation |

## Testing

### Server-side (`tests/test_threads.py`)

Real TCP server, no mocks (existing pattern):

- Thread creation: PRIVMSG with prefix delivered to channel members
- Thread reply: delivery + history accumulation
- Thread listing: THREADS response format
- Thread close: summary NOTICE, archive, further replies rejected
- Breakout promotion: channel created, participants joined, history replayed
- Error cases: duplicate name, non-existent thread, archived thread, non-member
- Federation: thread messages relay across S2S, state consistent on peer

### Agent client tests

- Buffer: `read_thread()` returns only matching messages
- Mention: thread-scoped prompt contains thread context only
- Tools: wire format correctness for all thread commands

### Manual verification

- Standard IRC client (weechat/irssi): `[thread:name]` prefix visible
- Two federated servers: cross-server thread participation
- Agent thread interaction: @mention in thread, verify scoped response
