relay-ide started as a remote web IDE wrapping a single Claude Code session over a PTY. One agent, one terminal, one tab. That worked for the demo. It broke the second I wanted to run a kanban board of work across Claude, Codex, and OpenCode without juggling three browser windows.

Spent the past week wiring multi-agent support in. The multi-agent wiring was the easy part. What stuck were the places where the original IDE primitive leaked through.

Symptom — agents were tabs, not workers

The original model: each agent = one PTY = one browser tab. Running three agents meant opening three tabs. Comparing their outputs meant alt-tabbing. Handing off work meant copy-paste. It was a terminal multiplexer with worse keyboard shortcuts.

The kanban experiment (#353) made the gap obvious. I wanted to pick up a ticket, fan it to three agents in parallel, and have one place where the work and the agents lived next to each other. The IDE-as-terminal model couldn't get there without ten more tabs.

Cause — the PTY plugin assumed one agent per session

The PTY handler in relay-ide was the load-bearing primitive. It owned the session, the terminal, and the agent identity at the same time. Adding a second agent meant either spinning up a second session (with a second tab) or making the PTY handler aware of "which agent," which it wasn't designed for.

The Codex side was easier. Codex hooks are stateless and can multiplex naturally. OpenCode runs as a long-lived relay plugin, though, and Claude Code wants its own PTY. Three different shapes of "agent runtime", one PTY handler that thought it was talking to one of them.

What I built

  • A multi-agent slot in the PTY handler. Each PTY session can now own multiple agent slots, each with its own adapter (Claude / Codex / OpenCode). The session is the ticket rather than the agent. Agents come and go inside the session.
  • OpenCode relay plugin. Wraps the long-lived OpenCode process and exposes the same adapter interface as Claude's PTY adapter. From relay-ide's POV, it doesn't matter which runtime is in the slot — same tool calls, same event stream.
  • Codex hooks adapter. Maps Codex's stateless hook output into the same event stream. Codex stays stateless inside relay-ide; the session holds the state.
  • A kanban view (#368) where each card is a session, and each session can host agent slots in parallel. Hermes drives the board over Discord — [KyleKaster] use kanban multi agents to deal with this epic becomes "spin up N agent slots, fan out the subtasks, report back when each slot completes."

#317 is where this is going next: relay hub running on a remote host so my laptop stops being the bottleneck. The multi-agent primitive makes that ~obvious: once the session is the unit and the agent is a slot, "where does the session run" is a routing question rather than an architecture rewrite.

What helps when the primitive is wrong

A few things I'd save to memory if I could only save one paragraph:

  • The primitive leaks where you try to grow. PTY-per-agent worked at one. Three was the rewrite.
  • Adapters over inheritance. Three runtimes, three adapter shapes, one consumer. Trying to make Claude / Codex / OpenCode share a base class was the version of this I tried and abandoned in week 1.
  • The session owns the work; tools rotate through it. Once I named the session as "the ticket" instead of "the terminal", the slot model fell out of it.

Outcomes

  • 3 agent runtimes (Claude / Codex / OpenCode) share one adapter contract.
  • Kanban view (#368) ran four real tickets overnight against the relay-ide backlog. Three landed clean, one needed human follow-up — which is the right ratio for trusting it without trusting it blindly.
  • Relay hub (#317) is now a routing problem instead of a rebuild.

Notes

  • Hermes-as-coordinator-over-Discord is the load-bearing trick here. The IDE didn't need to grow a "fan out to N agents" UI — Discord already has channels, mentions, and threads. Reuse what your team already does to handle people, and let agents inherit the same surface.
  • I'm watching whether the adapter contract holds up when the next runtime ships. If it doesn't survive the fourth, the abstraction was wrong.