Skip to content

Session adoption design note: docs/session-adoption.md #526

Closed

Description

@barelyworkingcode

User story

As the owner, I want the session model and its open questions written down and answered from vendor documentation, so that the adopt work builds on facts and not on guesses.

Acceptance criteria

  • docs/session-adoption.md exists and states the session model, the runner, the driver swap and the per-agent adapters.
  • Each unverified item in the note carries an answer and a source link, or a line saying the vendor documents nothing.
  • The note uses neutral stand-ins: no machine, user or client names, no real paths.
  • No test is run against a live session to answer an item.

What I'd notice

Nothing on screen. A doc the other children point to.

Depends on

Nothing. This lands first, on the epic branch.

Trivial change: a doc only. It skips the gate and the subagent split.

Parent

#525. The design is in docs/session-adoption.md (relay child "Design note", below).

Tasks

  • Planner: write the ## Contract.
  • Start from the draft in the comment on this issue.
  • Answer unverified items 1 to 7 from vendor docs; mark any the docs leave open.
  • Grep the diff for private names before the push.
  • Playwright or e2e tests from the criteria; update the feature map row. Verification task, last.

Activity

  1. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Draft text for docs/session-adoption.md. The first PR on this issue commits it, after the unverified items are answered.

    Draft

    Session adoption

    One session model for every agent relay hosts, and a way to take over an agent
    relay did not start. This note is the design the session-adoption epic builds.
    Status: proposed. Items marked unverified are settled from the vendors'
    documentation before the code that depends on them, not by testing against live
    sessions.

    Why

    Relay reaches an agent in three different ways today, and each way has its own
    start path, its own limits and its own way of losing the agent.

    Transport Used by Survives a dropped link Survives a relay restart
    Stream-json over a child's pipes headless Claude, local n/a no
    Stream-json over ssh -T headless Claude on a host no no
    Stream-json under a detached runner on the host headless Claude on a Unix host with detach_sessions yes yes
    A pty through the shim terminal sessions, local n/a no
    A pty inside the host's tmux a host terminal template with persist yes yes

    The Chief of Staff start has two paths. Headless launches and then sends the
    prompt through the send door. Terminal launches with the prompt in argv. The
    two paths have different refusals, different reach (a terminal cannot start on
    a host, an entry is headless only) and different audit shapes. A session
    started in terminal mode cannot be messaged by the Chief of Staff, and cannot
    become headless.

    Drop-in moves a headless Claude session into a terminal and hands it back, but
    only for sessions relay started. A Claude, codex or pi session the owner began
    in an ordinary terminal is visible (outside sessions) and read-only.

    Principles

    1. A session is a conversation, not a process. Its identity is the agent's
      conversation id and its transcript file. Every agent relay supports keeps a
      transcript on disk and can resume from it.
    2. The driver is replaceable state. A driver is whatever currently talks to
      the agent: a stream-json runner or a terminal UI. Swapping the driver keeps
      the session, its name and its transcript.
    3. One writer at a time. While a terminal holds a session, sends refuse with
      dropped_in. Nothing types into a terminal UI on a program's behalf.
    4. The file channel is the control channel. Messages in and events out move
      through files, so a lost connection loses nothing and every message has a
      receipt.
    5. The transcript is the source of truth for catch-up. The live event stream
      is for latency. After a swap or a reconnect, state comes from the transcript.
    6. Adoption applies relay's policy. An adopted session runs under its
      project's sandbox and permission policy. That is the purpose, not a side
      effect.

    The runner and the file channel

    A runner is a small program that owns an agent's stdin and stdout. It is the
    same program for every agent. It knows no protocol.

    <state dir>/<session>/
      inbox/        one file per message to deliver, in order
      sent/         a message moves here when it is written to the agent
      out.jsonl     every line the agent printed, appended
      runner.json   pid, start time, generation, cursor
    
    • send writes a file into inbox/. The runner writes it to the agent's stdin
      and moves it to sent/. The move is the receipt, so a send is delivered
      exactly when it returns.
    • Readers follow out.jsonl by byte offset. A cursor survives a reader
      restart, so lines arrive once and in order.
    • The runner outlives its caller. It is detached on Unix and runs in its own
      process group.
    • The protocol lives in the provider, as today. Claude speaks stream-json,
      codex speaks the app-server JSON-RPC, pi speaks its rpc mode. The runner only
      moves lines.

    What tmux does. tmux does not move files. It hosts the pane, gives the
    session a stable name, lets a person attach, and lets the pane's program change
    without losing the name. The pane runs either the runner (headless) or the
    agent's own UI (terminal). tmux respawn-pane -k swaps one for the other.
    Where a host has no tmux, a headless session still runs under the runner alone.

    Swapping the driver

    Headless to terminal:

    1. Wait for idle: the last turn has ended and no tool call is open.
    2. Interrupt if the owner asked not to wait, using the ladder below.
    3. Stop the runner and its agent.
    4. Start the agent's UI in the same pane with the agent's resume flag and the
      same conversation id.
    5. Mark the session held. Sends refuse with dropped_in.

    Terminal to headless is the reverse. The UI exits, the hold clears, the runner
    starts, and the agent resumes from the transcript. This is what hand-back does
    today, except the resume happens at once and not at the next message.

    A session starts headless with a known conversation id. The Chief of Staff has
    one start path. "Start in a terminal" becomes "start, then swap".

    Reading a session

    • Live: follow out.jsonl.
    • Catch-up and terminal mode: read the transcript. Deduplicate by record id
      when switching between the two.
    • A pending prompt in terminal mode is visible only on screen. The pane capture
      is the fallback, and it is a hint, not a protocol.

    Adopting an agent relay did not start

    Adopt takes an idle agent that someone started in an ordinary terminal and puts
    it under a runner or its daemon, on the same conversation.

    Flow: discover, check idle, interrupt if needed, take over, tell the agent what
    happened.

    Per-agent adapters

    Claude Code Codex Pi
    Who owns the session the process a shared per-user daemon; the terminal UI is a client the process
    Find live sessions ~/.claude/sessions/<pid>.json: sessionId, cwd, startedAt, procStart, status, kind, entrypoint the thread id in ~/.codex/thread-writer-locks/<id>.lock, held open by the daemon; threads table in state_5.sqlite none. Match the process start time and cwd to the transcript file name
    Transcript ~/.claude/projects/<encoded cwd>/<id>.jsonl ~/.codex/sessions/YYYY/MM/DD/rollout-<created>-<id>.jsonl, created at the first turn <sessions dir>/<encoded cwd>/<start UTC>_<id>.jsonl, created at the first turn
    Idle registry status is idle and statusUpdatedAt is fresh; no unmatched tool_use; no child process last turn record is task_complete last message is an assistant message with stopReason: stop
    Interrupt SIGINT, then SIGTERM, then SIGKILL turn/interrupt SIGINT (unverified)
    Take over stop the process; resume under the runner with --resume <id> connect to the daemon's socket and drive the thread; stop nothing (unverified: a second client on a thread another client has open) stop the process; resume under the runner with --session <id> and --session-dir
    Not adopted kind other than interactive, entrypoint other than the command line a thread with a running turn and no interrupt consent an ambiguous match

    Observed versions: Claude Code 2.1.304, codex 0.162.1, pi 1.0.2. Pi and codex
    create their transcript at the first turn, so a session with no turn has
    nothing to adopt and nothing to lose.

    The idle check

    Several signals combine. A signal can veto idle. Only the agent's own state can
    confirm it.

    • Confirm: the agent's status field or turn-boundary record says idle.
    • Veto: an open tool call in the transcript, a child process of the agent, or
      high CPU.
    • Low CPU never confirms. An agent waiting on the model or a long tool uses
      almost none.

    Input typed but not sent is invisible to every signal. A person is the owner of
    that input. Adoption by a button is a decision by that person.

    The interrupt ladder

    Waiting for idle is the default. When the owner asks not to wait, or a timeout
    passes:

    1. Interrupt as the agent's own stop (SIGINT, or turn/interrupt). The agent
      ends the turn and records the interruption.
    2. Wait a bounded time for the agent to go idle.
    3. SIGTERM, wait, then SIGKILL.

    An interrupt loses the turn in flight and any session-only state: background
    shells, subagents, permission grants made for the session. It cannot undo side
    effects that already happened: a half-run command, a half-written file.

    The first message after a takeover

    After a takeover that interrupted a turn, relay sends the agent one message,
    marked with its origin, built from the transcript tail: what it was doing, which
    tool call was open, and an instruction to check the state of anything that call
    touched before continuing. The same message follows a crash resume.

    Crash and adoption are one operation

    An agent that exits unexpectedly (an API failure, a kill) is resumed from its
    transcript by the same code path. The supervisor sees the exit, and resumes
    with the recovery message.

    Authority and audit

    • Adopt, interrupt and swap are owner actions in this design. The Chief of
      Staff gains no new authority. Whether it may adopt on its own is an open
      question in the epic.
    • Only sessions in a registered project are adopted, and a project's policy
      applies from the resume on.
    • Every adopt, interrupt, swap and recovery message writes an audit row before
      it acts, as every other launch and send does.
    • Transcript and registry text is untrusted display data and grants no
      authority. A registry file never chooses a path to read. The path is derived
      from the home folder, the working folder and a validated id.
    • A process whose start time does not match its registry entry is not adopted.
    • An ambiguous match is never adopted.

    Local and remote

    The same adapters run on a host over ssh, using the same reads the outside
    probe already uses. The runner and tmux are installed or probed per host, as
    node, claude and tmux are today.

    Windows has no tmux, no SIGINT for a non-console process and no detached
    runner today. The runner is plain standard-library code and should work. The
    terminal path and the interrupt need a decision (see open questions).

    Unverified items

    Settle each from vendor documentation before the dependent code. Do not test on
    live sessions.

    1. Does --resume keep the same conversation id and transcript file?
    2. How does Claude Code handle an interrupted turn on resume, and a tool call
      with no result?
    3. What is the registry's messagingSocketPath for, and does it offer a stop?
    4. Does pi stop its turn cleanly on SIGINT, and does --session append to the
      same file?
    5. Can a second client drive a codex thread another client has open, and what
      is the daemon socket's handshake?
    6. Does the permission hook's ancestry check hold when the agent runs under a
      tmux pane?
    7. Does omp follow pi's layout?

    Out of scope

    • Agents relay does not support (a new provider is a separate story).
    • Moving a conversation between machines.
    • A shared view of several clients on one terminal, beyond what the tmux viewer
      already offers.
  2. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Coordinator approval: owner asked, 2026-10-20

  3. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Merged into the epic branch by relay#533. Left open for the close child, relay#532.

  4. barelyworkingcode commented on Oct 21, 2026

    @barelyworkingcode
    OwnerAuthor

    Done by #533, merged into epic/525-session-adoption. Verified on the epic head b6f530d (see #532).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttaskOne batch of an epic

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions