You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
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.
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.
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.
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.
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.
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:
Wait for idle: the last turn has ended and no tool call is open.
Interrupt if the owner asked not to wait, using the ladder below.
Stop the runner and its agent.
Start the agent's UI in the same pane with the agent's resume flag and the
same conversation id.
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 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:
Interrupt as the agent's own stop (SIGINT, or turn/interrupt). The agent
ends the turn and records the interruption.
Wait a bounded time for the agent to go idle.
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.
Does --resume keep the same conversation id and transcript file?
How does Claude Code handle an interrupted turn on resume, and a tool call
with no result?
What is the registry's messagingSocketPath for, and does it offer a stop?
Does pi stop its turn cleanly on SIGINT, and does --session append to the
same file?
Can a second client drive a codex thread another client has open, and what
is the daemon socket's handshake?
Does the permission hook's ancestry check hold when the agent runs under a
tmux pane?
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.
Activity
barelyworkingcode commentedon Oct 20, 2026
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.
ssh -Tdetach_sessionspersistThe 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
conversation id and its transcript file. Every agent relay supports keeps a
transcript on disk and can resume from it.
the agent: a stream-json runner or a terminal UI. Swapping the driver keeps
the session, its name and its transcript.
dropped_in. Nothing types into a terminal UI on a program's behalf.through files, so a lost connection loses nothing and every message has a
receipt.
is for latency. After a swap or a reconnect, state comes from the transcript.
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.
sendwrites a file intoinbox/. The runner writes it to the agent's stdinand moves it to
sent/. The move is the receipt, so a send is deliveredexactly when it returns.
out.jsonlby byte offset. A cursor survives a readerrestart, so lines arrive once and in order.
process group.
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 -kswaps 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:
same conversation id.
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
out.jsonl.when switching between the two.
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/sessions/<pid>.json:sessionId,cwd,startedAt,procStart,status,kind,entrypoint~/.codex/thread-writer-locks/<id>.lock, held open by the daemon;threadstable instate_5.sqlite~/.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 turnstatusis idle andstatusUpdatedAtis fresh; no unmatchedtool_use; no child processtask_completestopReason: stopturn/interrupt--resume <id>--session <id>and--session-dirkindother than interactive,entrypointother than the command lineObserved 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.
high CPU.
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:
turn/interrupt). The agentends the turn and records the interruption.
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
Staff gains no new authority. Whether it may adopt on its own is an open
question in the epic.
applies from the resume on.
it acts, as every other launch and send does.
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.
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,claudeandtmuxare 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.
--resumekeep the same conversation id and transcript file?with no result?
messagingSocketPathfor, and does it offer a stop?--sessionappend to thesame file?
is the daemon socket's handshake?
tmux pane?
ompfollow pi's layout?Out of scope
already offers.
barelyworkingcode commentedon Oct 20, 2026
Coordinator approval: owner asked, 2026-10-20
barelyworkingcode commentedon Oct 20, 2026
Merged into the epic branch by relay#533. Left open for the close child, relay#532.
barelyworkingcode commentedon Oct 21, 2026
Done by #533, merged into epic/525-session-adoption. Verified on the epic head b6f530d (see #532).