Chats are Claude Code sessions
A chat in Paddock is not a Paddock-invented construct — it is a Claude Code session, persisted on disk as a transcript and resumable across page reloads, socket reconnects, server restarts, and even different devices. Paddock adds the UI, the streaming transport, and the project attribution; the session itself is Claude Code’s.
Persisted on disk as a transcript
Section titled “Persisted on disk as a transcript”Each chat is one JSONL transcript file, <sessionId>.jsonl, written by the
Claude Code CLI — Paddock only reads it. Claude Code stores transcripts under
<claudeHome>/projects/<encoded-cwd>/, where the encoded name is the agent’s
absolute working directory with non-alphanumeric characters replaced by -. So
the working directory is the session key — no separate database of chats.
Paddock always runs Claude Code against a Claude home it owns —
<dataDir>/claude-home — so that path is
<dataDir>/claude-home/projects/<encoded-cwd>/.
Paddock then symlinks that encoded directory, and
claude.transcripts decides where the
symlink points (ensureProjectChats() in transcripts.ts). Under the default
transcripts: own it targets the project’s .chats/ folder, so the transcript
physically lives inside the project directory and rides the same backup. Under
transcripts: host it targets the user’s real ~/.claude/projects/<encoded-cwd>/
instead, so chats are shared with the machine’s own terminal claude history.
Listing, reading, and resuming resolve transparently through the symlink either
way. Deleting does not. Under transcripts: host the transcript is the user’s
own claude history rather than Paddock’s copy, so HerdctlService.deleteSession
releases the chat instead of removing it ({removed: false, retained: true}) and
the file stays on disk. Releasing only drops the adoption record, so the chat is
still listed afterwards — the engine rediscovers the transcript structurally on
the next listing. That gap is tracked as
issue #693.
The transcript is the authoritative record of the conversation. Everything
else about a chat is either derived from it (previews, token/context usage, the
rendered message list) or a small piece of side-metadata in a
server sidecar —
its archived flag (ArchiveStore), your last-seen timestamp (ReadStateStore),
and any queued follow-up message (QueuedMessageStore).
Resumable
Section titled “Resumable”Starting a chat sends chat:send with sessionId: null; the session id is
minted by Claude Code and arrives mid-stream (Paddock captures it and attributes
the running session to the project so the chat appears in the sidebar before
the turn finishes — issue #100). Every later turn on that chat sends the same
sessionId, and Claude resumes the existing session (resume: <sessionId>).
Resumption is robust to interruptions at several layers:
- Reload / new device — the client hydrates the chat from the REST transcript endpoint; because the transcript is on disk and per-user read-state is a server sidecar, the same chat (and its unread state) appears anywhere you log in.
- Mid-turn reconnect — the client re-attaches over the WebSocket with
chat:subscribe { wantReplay: true, lastSeq }, and the SessionHub replays the buffered frames it missed (or tells it to re-hydrate from REST if the buffer aged out). A live turn keeps streaming to whoever attaches. - Server restart — the transcript and all sidecars are on disk, so chats survive; a resumed turn picks up from the persisted session.
Token-by-token streaming
Section titled “Token-by-token streaming”A reply can accrete into the live bubble token-by-token as the model produces it, rather than landing in one drop when the turn ends. This is a property of the runtime, not the transport:
- Session mode (SDK runtime) opts into partial (streaming) assistant messages
— herdctl surfaces incremental
text_deltachunks, which the WebSocket layer forwards aschat:responseframes that append to the bubble as they arrive. - Batch mode (CLI runtime) renders each assistant message whole when it completes; there’s no intra-message streaming.
The drive mode is set by PADDOCK_DRIVE_MODE (with a per-project
driveMode override) — see
Agents. Everything else about a chat is
identical either way: the transport was already delta-shaped, so
re-attach and replay behave the same whether or not tokens stream.
Forking
Section titled “Forking”A chat can be forked into a parallel child: forkSession copies the
transcript and mints a new session id, so the child diverges without touching the
parent. (Contrast with promotion, which moves a root chat into a project of its own —
see Agents.) Forked children run under the same agent (up to
KEEPER_MAX_CONCURRENT in parallel) and are full chats in their own right —
resumable, forkable, archivable.
In the sidebar a fork is filed underneath the chat it was forked from, so
splitting a conversation three ways gives you one foldable family rather than
three unrelated rows. That’s true however the fork was made — by you from the
message hover rail, or by Claude calling fork_chat — and note it nests under
its source, which for a Claude-made fork isn’t necessarily the chat that made
it. See Provenance for how
that edge is recorded, and for the spawn-depth consequence of forking a fork.
In one line
Section titled “In one line”A chat is a resumable Claude Code session whose transcript lives on disk inside its project; Paddock streams it live and lets you pick it back up from anywhere.