2026-08-28 · Updated 2026-08-28 · 10 min read

Durable coding-agent sessions and handoffs

Keep coding-agent state alive across interruptions: where session evidence persists, how continuation is scoped per shell, how sessions branch and clone, and the handoff manifests that let another operator pick up the thread.

By Juno AI INC · sessions · continuity · yylo

The interruption is never on the schedule. The laptop lid closes mid-refactor, the SSH connection drops, the quota window ends, or the person driving the agent goes home and someone else opens the laptop at 9 a.m. staring at a dead terminal. What was lost? Almost nothing, if the session was treated as durable state — and almost everything, if the conversation only ever lived in a window that no longer exists. The questions that bring people here are the same regardless of harness: how to resume a session, where sessions are saved, how to fork one thread into two experiments, and how to hand a thread to the next operator without retyping the story. This guide owns those practices for coding-agent work under YYLO, where they are commands and files rather than hopes.

Separate the three layers before anything else, because they fail independently and recover differently:

  • The thread — the conversation the harness holds, addressed by a session id. The id, not the terminal, is the durable handle for everything below.
  • The record — the metadata about runs: prompts, services, models, costs, session ids, timestamps. This is what makes the past greppable instead of remembered.
  • The work — the working tree and its commits. This layer belongs to Git, and the worktree guide owns how change is isolated; this guide stays on the other two.

An interruption is survivable exactly when all three layers can be re-addressed. The rest of this page is the practice, command by command.

Where session state lives, and why it leaves the tree

Durable session state has one non-negotiable requirement: it must not live inside the working tree it describes. YYLO writes its session metadata under a shared state root keyed to the repository's Git common directory. Every yylo run appends to session_history.json there — an unlimited, newest-first log whose per-run entries carry the initial prompt and its timestamp, the subagent, model, and settings used, the total cost, turn and message counts, the session ids, and the timestamp of the last message. The continuity files — session_branches.json, continue_scope_runtime.json, and the versioned session_continuity.v2.json — resolve through the same root, which is what keeps linked worktrees from dirtying tracked product paths or overwriting another repository's state. Projects outside Git get an identity-keyed user directory instead, and YYLO_SESSION_METADATA_DIRECTORY pins an explicit location when you need one.

The placement is the durability. Branch switches, worktree churn, git reset, a fresh clone — none of it touches the session record, because the record was never part of the bytes being switched. And the id you will need later is surfaced at the moment you can still capture it: every run's summary prints its statistics (total cost, completed-at, average duration) plus a Session ID(s) section with per-session cost when available. The first habit of durable sessions is small: when a run finishes, the session id goes into the task record, the commit message, or your notes — wherever the next person will look.

Resume explicitly, continue by scope

Getting back into a thread has three distinct moves, and picking the right one is most of the discipline:

sh
yylo session list                # every session, with ids
yylo session info abc123         # one session's details
yylo --resume abc123 -p 'continue'   # one exact thread, by id
yylo --continue -p 'keep going'      # most recent session, backend-native
yylo continue 'next prompt'          # last session id + its runtime settings snapshot

--resume <id> is the precise instrument: identity matters, so name the thread. --continue reaches the most recent session through the backend's own mechanism. yylo continueyy cc in short form — reuses the last session id together with the runtime settings that produced it, so the continuation runs with the same shape as the original instead of whatever defaults drifted since.

What makes these commands safe to type casually is that continuation is scoped, not global. Each shell or pane is its own continue scope: scope detection prefers terminal markers like TMUX_PANE, WEZTERM_PANE, or TERM_SESSION_ID and falls back to the parent shell's PID, so two tabs each get their own scope and neither can accidentally continue the other's thread. When tabs should share, say so explicitly with YYLO_CONTINUE_SCOPE=<name>. The scoped state lives in session_continuity.v2.json, where each scope records its source, creation and last-use timestamps, pin state, active branch, and branch sessions; one backing service validates, locks, re-reads, and atomically replaces the document, and .env.yylo stays untouched user configuration.

Scripts get the same truth without parsing any of that: yy continue-scope --json reports the current scope's hash and one of four statuses — running, finished, not_found, error — a short hash prefix looks a scope up, and --parent-pid reports the scope a caller's child will see. Continuation is resolved in the parent before dispatch, and children do not inherit scoped session or settings keys — resume requests and settings travel as typed data, so a dispatched workflow step cannot hijack its parent's thread by accident. One boundary note: resume stays inside one harness. Moving the work to a different engine entirely is a switch, and that is a separate discipline with its own guide.

Branch and clone sessions for parallel exploration

The moment a thread needs to try two designs, the wrong move is to make the one thread hold both. Session branching forks the conversation: each clone becomes an independent session with its own id, continuable on its own, while the original stays untouched. YYLO builds this on Pi's native fork, so this surface is the Pi service's — the Pi composition guide covers the full pairing:

sh
ypl 'start the refactor'              # ypl expands to yy pi --live
yy clone 'explore the streaming design'  # auto-named b1, b2, ...
yy clone C 'explore approach C'          # named branch C
yy clone --from C --name M 'explore M'   # branch C forked into M
yy branches                           # this shell's branches, active marked
yy switch C                           # C becomes the active branch
yy cc 'continue approach C'

The semantics have teeth, and knowing them prevents the classic mistakes. A named clone — an explicit C or an auto-assigned b1 — runs its prompt immediately but does not switch the active branch, so yy cc afterwards continues the branch you were on, not the new one; yy switch C (or the wraparound cycling of yy switch + / yy switch -) changes that. --from C forks an existing branch instead of main, --name main is rejected because main is reserved, and a new root run resets that shell's branches to main only. Unnamed forks behave differently: yylo --resume <session-id> --clone 'prompt' turns that exact thread into a fresh clone with no branch name, and the cloned session id is persisted to the shell scope, so future continue runs follow the clone. And because ypl expands to yy pi --live, ypl clone C ... would pass "clone C" as prompt text — run clone, switch, and branches through yy or yylo.

Keep the two branching systems straight and they compose. Session branches fork the *conversation*: two approaches to one change, explored from one shared history of intent. Task worktrees fork the *change*: two different tasks to one repository, isolated in bytes. Comparing designs is clone-a-session-inside-one-worktree; parallel tasks is one-worktree-each. Mixing them up is how teams end up with two agents editing one tree — the failure mode the worktree guide opens with.

Ship the session id, not the scrollback

A handoff is a promise that the next operator, agent, or script can re-enter the thread from data. The session id is the handle; the manifest is the envelope that carries it. Under YYLO the envelope is written for you at every scale:

  • Workflow runs capture sessions per step. Agent steps record their session metadata unless the step declares capture_session: false, and later steps template the id directly as {{ steps.<id>.session_id }} rather than pasting it around. When the run ends, its detected ids are printed and the run's last agent session is persisted into the continue-scope registry behind yy cc — the pickup after a workflow is the same one-command continuation as after an interactive run. To hand off a different step than the last, declare continue_from_step; the selection is strict, and a step that produced no session id fails the declaration. The run-directory record around all of this is the auditable-workflow guide's depth.
  • Tmux handoffs give every task a pane that outlives attention. With --tmux-handoff, completed panes are never reused — each keeps its scrollback plus a per-task JSON result containing the session ID and the final response, and --max-panes-per-session N splits overflow into auditable child sessions with a tmux_handoff_manifest.json.
  • Parallel batches write per-item records. parallel_runner_status.json, per-task *.json, and aggregation_*.json retain each item's final response, session id, commit, and status, so review reads artifacts instead of reconstructing history.

After any of them, the pickup is one command — yy continue <session_id> — and the loop-level continuity modes (iteration, run, shell) decide which steps of a repeated loop share a scope. The anti-pattern all of this replaces is the screenshot-and-scrollback handoff: context that lives only in a window is context the next shift does not have.

Recover when continuation breaks

Breakage has a small number of shapes, and each has a direct recovery:

  • A new tab cannot find the branches. The report No named session branches found for this shell scope means exactly that: the tab resolved a different scope. Run ypl 'init' in that tab, run from the original tab, or set a shared YYLO_CONTINUE_SCOPE=<name> before starting runs that should share branch state.
  • The session file is gone. A missing session fails the continuation without deleting its continuity record and without quietly routing into another scope; the error tells you to resume an explicit session id or begin a new run. The record outlives the session on purpose — provenance survives even when the thread does not.
  • Old scopes aged out. Automatic retention expires unprotected implicit lookup metadata after 30 days and then keeps only the 128 most recently used inactive scopes. Current, proven-live, explicitly pinned, and non-main named-branch scopes are protected from expiry, and retention never inspects or deletes Pi session files. When a scope must outlive the policy, yylo continuity pin protects it deliberately.
  • Unknown state, before touching anything. yylo continuity doctor --json audits the continuity state; yylo continuity clean runs as a dry-run inventory only. Neither is a prerequisite for continuing — they are for the days something looks wrong.

The rule underneath all four: never delete state to fix a routing problem. Diagnose the scope first — yy continue-scope --json is the script-facing truth — then act on what the report says, because a session you cannot find and a session that is gone are different failures with different recoveries.

Make the next interruption cheap

Durability is a set of habits more than a feature list, and every one of them is one command deep:

  • Bank the session id when the run finishes — it is printed for you.
  • Keep session metadata outside the working tree; let the Git-common state root hold it.
  • Resume explicitly when identity matters; continue by scope when convenience is enough.
  • Branch before experimenting, never inside the one thread you cannot afford to tangle.
  • Ship a manifest with every handoff; the pickup should be yy continue <session_id>, not archaeology.

Practice it once before it matters. Install YYLO, put one bounded outcome on the board, and close the terminal on purpose once the session is recording:

sh
npm install -g @yylo/cli
yylo init --task "Describe one verifiable outcome" --subagent claude
yy pi 'take the first step'
yylo session list
yy cc 'continue from where that stopped'

The interruption you rehearsed is the one that costs nothing. The durable-session habits then compound with the rest of the system — bounded loops for the work, worktrees for the bytes, manifests for the evidence — and the terminal becomes what it should have been all along: a view, not the store.