ADR-0003: tmux for per-chat session persistence

Status: Accepted Date: 2026-04-23 Deciders: Jacob

Context

Each chat needs a persistent shell/REPL that holds Claude Code context across turns. Options:

Option Persistence Attach from browser? Claude Code fit
Short-lived claude --print per turn none N/A simplest but loses agentic state
Raw PTY + node-pty keep-alive in-memory via WS loses on process restart
tmux session per chat disk-backed, survives restart (with tmux resurrect) tmux attach inside WS PTY excellent
screen / dtach similar to tmux similar OK but tmux has better tooling

Cortex uses tmux (see cloudcli/server). Claude Code itself is often used inside a tmux split by Jacob.

Decision

tmux, one session per chat, session name picortex:<chat_id>.

Consequences

Positive

  • Survives backend restarts.
  • Web terminal (xterm.js) can attach via a backend WebSocket that shells into tmux attach -t picortex:<chat_id>.
  • Multiple browser tabs can attach to the same session (tmux natively multiplexes).
  • tmux capture-pane / pipe-pane gives a structured way to scrape Claude Code's output for reply routing.
  • Zero memory cost when idle (bash + tmux).
  • Consistent with how Jacob already works.

Negative

  • tmux protocol has quirks when streamed over WebSocket (scrollback, resize, escape sequences).
  • send-keys + output tailing is fragile for structured reply parsing — need a small protocol on top (e.g. unique sentinel before/after each turn).
  • tmux must be installed on the server (trivial but still an explicit dep).

Mitigations

  • Use pipe-pane -o to stream output to a logfile per chat; parse the logfile for reply extraction, not the terminal.
  • Use a delimiter protocol: before each turn, tmux send-keys "echo '<<PICORTEX-TURN-$n-START>>'"; after, "echo '<<PICORTEX-TURN-$n-END>>'". Reply = content between delimiters, stripped of ANSI.
  • Resize: send tmux refresh-client -S on WS resize events; xterm.js reports cols/rows.
  • Fallback plan: if reply parsing turns out unreliable, swap to claude --print per turn in S3.1 and keep tmux only for the user-visible terminal attach, not for reply capture.

Alternatives considered

  • One long-lived claude --print per turn, no tmux: Simpler, but loses the web-terminal-attach feature, which is an explicit v1 goal (FR-12). tmux covers both.
  • Reuse Cortex's cloudcli PTY mux: Heavier; carries Cortex's container assumptions. Not worth the integration cost for v1.
[[curator]]
I'm the Curator. I can help you navigate, organize, and curate this wiki. What would you like to do?