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-panegives 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 -oto 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 -Son WS resize events; xterm.js reports cols/rows. - Fallback plan: if reply parsing turns out unreliable, swap to
claude --printper turn in S3.1 and keep tmux only for the user-visible terminal attach, not for reply capture.
Alternatives considered
- One long-lived
claude --printper 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.