Get started
Markdownllms.txt

Changelog

Every release of the four packages (they share one version number), newest first. Built from the repository's changesets each time the site deploys, so this page cannot drift from what shipped. Each entry links the commit that made it. Subscribe with the Atom feed.

0.15.1 next

Merged to main and waiting for the release. The version follows from the largest bump below.

  • patch

    conformance: a fixture run reads only the folder it was given, and holds an event's data to the fixture exactly

    Details

    Four changes to sessionpipe-conformance, from a security scan of the repository's tooling.

    An event's data now has to match the fixture field for field: a field the adapter sends that the fixture does not name is a miss ([0].data.output: not in the fixture). Before, the fixture's fields were compared and anything extra passed, so an adapter could start sending more than its fixtures vouch for and still read as conformant. "*" is still a wildcard for a value, and a session's facts are compared as before (the fixture's fields; a harness's environment adds others).

    A symbolic link under the folder you name is an error that names the link (exit code 2), instead of being followed to a file or a folder somewhere else on the machine.

    A fixture whose fields are the wrong kind (input.argv that is not a list of strings, an expect entry that is not an object, an env that is not a map of strings) is a FAIL row that says which, instead of an exception that ends the run.

    Control characters in a fixture's path or in an error are printed as \xNN, so a file name cannot move the cursor or recolour the terminal of whoever runs the suite.

    2b42b0d · conformance

0.15.0

  • minor

    cli: output a person can read at a glance, and one place that says whether this computer is fine

    Details

    sessionpipe doctor now starts with a verdict (All good, or how many things need attention), sets out what it looked at in groups of aligned rows, each with a mark and a word, and ends with To fix: one plain sentence and, where there is something to run, one command each, in the order to run them. sessionpipe status is the same in a few lines. --json keeps its shape on both.

    Trouble that only the daemon's log knew is on that page: a disk that filled up, a key the service can't read from the keychain, a pool with no account left to try, a receiver that hasn't answered. Whether Claude Code is signed in is asked of the running service over its socket (a new, read-only health request), because a shell over ssh can't read a Mac's login keychain; before, doctor told people who were signed in to sign in again. A service that launchd or systemd doesn't have at all is said to be not loaded, where it used to read as offline.

    sessionpipe control status is a page at a terminal, and exactly the JSON it always printed when piped or given --json. --help is one page of every command in groups, each with what it is for in a line that fits 80 columns; a command's own --help folds its usage between options.

    Colour comes from one small module with no dependency: on at a terminal, off there with --no-color, NO_COLOR or TERM=dumb, on there with FORCE_COLOR whatever NO_COLOR says. A pipe is never coloured. Nothing is said by colour alone. Lines fold at the terminal's width under their own text, never inside a command; a pipe gets whole lines, as before. Nothing a program reads has changed: hook output, the worker, the status line, every --json, the daemon's log.

    What changes for a script, and what does not:

    • doctor's exit code. sessionpipe doctor exits 1 when To fix holds a command to run, and 0 otherwise; it used to exit 0 always. A finding with nothing to type (a slow hook, a receiver that hasn't answered yet, a disk that was full and has room again, hooks an agent's own setting turns off) is on the page and counted in the verdict, and leaves the code at 0, so the code does not change with this computer's load. doctor --json exits 0 whatever it finds, as before. status exits 0, as before.
    • A command that fails. Where stderr is a terminal it prints two lines: error with what is wrong, and try with the one command to run next. That command is the one the code that failed names, or the failing command's own --help; it is never read out of the message, which may be a receiver's or npm's words. Where stderr is not a terminal it is the one line it always was, sessionpipe: <message>, and exit 1.
    • control pair when a program runs it. Not at a terminal its lines are the ones it has always written, to the byte: the link, The page shows these digits too: 123456 or enter: 123 456, ✓ Paired with …, Daemon: …. At a terminal the link and the digits stand each on a line of their own (open:, check: or enter:), and the last line starts Background service:.
    • Lines that are worded anew, at a terminal and in a pipe. doctor, status and --help are new pages, and control mode sets each mode out on its own line. install says This computer: <name> where it said Machine: <name>, No coding agent found on this computer where it said No supported harness found on this machine, and Nothing leaves this computer yet where it said this machine. ask on, ask off and pool on say Background service: … where they said Daemon: …. Renamed the background service, The background service uses the new list within seconds and The background service uses the new setting within seconds said the control daemon. sink list and pool keep their old lines in a pipe and get a layout that fits at a terminal. The line ask <agent> writes to stderr when the service goes away is unchanged.
    • --no-color is taken out of the arguments wherever it stands, before the command reads them, so it is never the value of the option before it.
    • doctor --json. The shape is the same. claude_accounts[].signed_in is now what the running background service sees, where it can be asked, and this shell's view only where it can't. warnings keeps its old strings and may hold three new ones: the background service isn't loaded: <launchd|systemd> doesn't have it, this computer's disk is full: <n> MB left (or this computer's disk was full <when> ago, by the background service's log), and the pool has no account with room left: Claude Code sessions here are being refused. update.off keeps the words it had.
    • Words that are not sessionpipe's own (a receiver's reason or last error, npm's last lines, a name from a file) are shown as words: escape sequences, other control characters and the characters that turn the writing direction round are taken out of every line written for a person, and a line break inside such a value is a space.
    • Commands printed for pasting (under To fix, and after try) quote a path, a name or an address a shell would otherwise act on.

    sessionpipe-core/service gains serviceState: whether launchd or systemd has a service now, without changing anything.

    6dd3f98 · cli, core

  • minor

    update: one command leaves the whole computer current

    Details

    sessionpipe update used to update only the copy it ran from and ask the daemon to restart "as soon as nothing is in hand". On a Mac with two copies (the one typed, and another the background service ran) the typed command stayed at an old version while everything looked updated; on a computer where some session is always working the restart never came, and people were told to type launchctl kickstart.

    Now, when it ends well, the copy you type, the copy the background service runs and the hooks are one version, the newest; the service is running it and has heard from its receivers; and the last line says so. It installs npm's latest into this copy, writes the hooks again from it, puts the service on this copy, and looks that it answers with the new version and has had an answer from each receiver since it started (it waits up to 40 seconds for that, the time a long-poll's first answer can take).

    How the service gets onto this copy depends on who runs it. One that launchd or systemd has, and that runs another copy or doesn't answer, is replaced with the safe replace from 0.13.7; it keeps the environment its service file gave it (its PATH, a SESSIONPIPE_CLAUDE added by hand), and only the program changes. One whose file is there with nothing running is put back. One that runs this copy restarts itself. One you run yourself (control pair --no-service, tmux, a container, WSL without systemd) is asked over its socket to restart itself, and no service is installed that nobody asked for.

    When it can't get there it stops with one line that says what is wrong and one that says what to run next: sessionpipe update again when npm can't be reached (what needs no download is still done, and nothing is ever moved back to an older release); a sudo npm install … && sessionpipe update line for a prefix this user can't write, with the prefix quoted (on Windows: sessionpipe update from a terminal opened as administrator); npx sessionpipe@latest update for a copy npm didn't install; sessionpipe control run where nothing runs the service and there is no service file for it. Where launchd or systemd refuses the new service, the one that was there before is put back and started again, and the command says whether anything answers now.

    Exit 0 means everything is current, 3 that the service finishes by itself, 1 that it stopped. A receiver that hasn't answered the service is not a failed update: the last line says the computer is up to date and names the receiver, in yellow, and the exit code is 0. SESSIONPIPE_NO_UPDATE_CHECK=1 sessionpipe update changes nothing, as before: it says that updating is off because the variable is set, and how to run it once without it.

    A restart is now held back only by what the daemon itself holds: a turn it runs, a message in hand, a permission prompt it holds for an answer, a pooled request being answered. A session someone is working in, mid-turn in its own window, no longer holds it back: that it is mid-turn is written to the daemon's state at a stop and read back at the next start, so a message to it still waits for its Stop. update gives the daemon 25 seconds to finish what it holds; past that it says the service restarts by itself when that is done, and there is nothing more to type. Just before it replaces a running service it asks it to stop taking messages, in the same step in which the service is found to hold nothing. A service from before 0.15 does none of this, and restarts itself only once no session on the computer is mid-turn: where launchd or systemd has it, update replaces it at once, after writing down for the new one which sessions the old one knew to be mid-turn; one run by hand can only restart itself by its older rule, and update says so and exits 3. Into a pipe a failed update is one line that ends Next: <command>.

    The daemon's own daily update takes the same steps (installEverywhere): its own copy, the sessionpipe on its PATH when that is another install (the copy a person types), the hooks, then its restart. npm is only ever run on a folder that has dist/cli.js and a package.json that says it is sessionpipe. sessionpipe doctor sees a split between the three, says which, and names sessionpipe update as the one fix; it no longer sends anyone to sessionpipe install from an older copy, which was refused or, forced, put the hooks back. Where the service isn't running and there is no service file for it, doctor and update both name sessionpipe control run.

    Also: the registry asked for latest is the one npm_config_registry names, when it names one, since that is where npm installs from. A sessionpipe that keeps its files somewhere other than this computer's usual place (SESSIONPIPE_STATE, SESSIONPIPE_CONFIG, XDG_* or HOME point elsewhere: a test, a throwaway world, a profile of your own) never looks at, replaces or stops this computer's usual service, and says so; SESSIONPIPE_SERVICE_LABEL=org.sessionpipe.<name> gives it a service of its own, and no other name is taken.

    sessionpipe-core/service: serviceState also answers the program a service file names (serviceArgv), and says manager: null where launchd or systemd can't be asked from this process; serviceEnvironment reads back the environment a service file gives; installService restarts a systemd unit that was running onto the new file, and where the manager refuses the new service it puts back the file that was there and starts that one again (ServiceRefused says which).

    1240735 · cli, core

0.14.2

  • patch

    outbox: a line that is not valid UTF-8 no longer costs a later event

    Details

    Outbox.read counted each line's length from its decoded text. Bytes that are not UTF-8, such as the end of an append cut off inside a character with the next append joined onto it, decode to U+FFFD, which is three bytes and can stand for one to three bad bytes, so the offset it returned could run a few bytes past the line's end. The next read then started inside a line, which failed to parse and was skipped without a word: the drain, sessionpipe tail and the GUI's outbox source all missed that event. Lines are now found and counted in the bytes as read, so the offset is always the byte after the newline.

    554f990 · cli, core

0.14.1

  • patch

    pool: a request over the body cap gets the 413 that says why, not a connection reset

    Details

    The pool answered a request over its 32 MiB cap the moment the cap was passed, while the client was still sending, and closed the connection. On macOS and Windows the bytes still on their way could make the connection reset, throwing away the 413 before the client read it, so Claude Code saw a connection error instead of "request body over … bytes". The pool now reads the rest of the body and drops it (nothing past the cap is kept), and answers when the request ends. A body more than 8 MiB past the cap, or one still arriving 5 seconds after it passed the cap, is answered then and the connection closed, so an endless or stalled upload holds nothing; that far out a reset can still happen.

    44349eb · cli

0.14.0

  • minor

    Grok Build: sessions report under their own name, sessionpipe install --grok-build

    Details
    • A new adapter, grok-build, for xAI's Grok Build CLI. sessionpipe install writes a hooks file of its own, ~/.grok/hooks/sessionpipe.json, and reports the session, its turns and tools, a permission prompt that is waiting, and (at tier 2) the conversation from the session's updates.jsonl. Title, model, folder and times come from the session's summary.json. backfill lists past sessions.
    • Grok Build also runs the hooks in ~/.claude/settings.json, with a payload that looks like Claude Code's. Until now sessionpipe's Claude Code entry there reported every Grok session as a Claude Code session. That entry now tells a Grok payload apart and reports nothing; the Grok adapter reports it.
    • Recorded on Grok Build 1.0.77; PostToolUseFailure, StopFailure and PreCompact are mapped from Grok's documentation and still wait for a recorded payload.

    052c0c9 · cli

  • patch

    ask: a level or a model on a new ACP session is refused, and ask status lists ACP agents

    Details
    • sessionpipe ask grok-build "…" --permission read started the agent with the flag dropped: an ACP agent takes no level (CONTROL.md §6) and ran as the machine's mode said, so on an auto machine a "read" ask could write. It is refused now, in words that name the mode, as an ask into an existing ACP session already was. --model the same.
    • sessionpipe ask status listed only the agents that take start options, so an installed ACP agent (Grok Build, Gemini CLI, …) the daemon could start was missing. It lists what the daemon says an ask can start.

    3173ef3 · cli

0.13.8

  • patch

    A poll that never ends no longer holds the control daemon for ever, unseen.

    Details

    The daemon ends a request it has held too long by aborting it, and then waited for the abort to take effect; where it took none, it waited without limit. Seen on a laptop on Node 23.6.1, a second after its clock was set forward thirteen minutes: one poll held for 103 minutes with no connection left and no error, sessionpipe control status reading "polling: yes" while the receiver showed the machine offline, until the daemon was restarted. Why the request never ended is not known. Now, ten seconds after its abort a poll is left behind and asked again on a new request; the log says so ("a poll never ended, and aborting it ended nothing; asking again") and sessionpipe doctor shows it as the receiver's last error. Three in a row and the daemon restarts itself when nothing is in hand, the one remedy known to work, at most once an hour; until then that receiver is asked every ten minutes. A hello, an ack and the look at a receiver's well-known are left behind the same way: an ack that never ended kept every later ack from being sent, and the daemon from ever being idle enough to update.

    ae93ff7 · cli

0.13.7

  • patch

    Pairing a Mac again no longer leaves it with no daemon.

    Details

    Installing the service over one that is running (sessionpipe control pair on a machine that is already paired, sessionpipe ask on) booted the old one out and bootstrapped the new one at once; launchd answers a bootout before the old process has exited and refuses a bootstrap until it has, so the daemon was stopped and nothing started it again: the machine read offline until someone ran launchctl bootstrap by hand. The install now waits for launchd to let go of the old service (up to 10 s) and then starts the new one, and when launchd still refuses, the error says what launchctl answered and the command that starts it. On a Mac this happened to, pair again or run launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.sessionpipe.control.plist.

    0c3702a · cli, core

0.13.6

  • patch

    The PermissionRequest hook names a prompt by its tool call's id where Claude Code's payload carries one, as the attention.needed it reports already did.

    Details

    The hook always used the hash of the prompt, the tool and its input, so on a Claude Code that names the tool call the page's Allow / Deny would have named a prompt the daemon did not hold. None does to date: nothing changes on the versions recorded so far.

    310ba20 · cli

  • patch

    A permission question can be answered from the page while a subagent is running.

    Details

    The daemon holds a PermissionRequest hook so that the page's answer can be handed to it, and let go of it at the session's next tool result, Stop or new prompt, taking that to mean the terminal had answered. A session's hooks all report under its id, a subagent's among them: with a subagent reading files in the background, the main thread's question was let go at the subagent's next tool call, a second or two after it was asked and with the terminal still asking, and the answer was refused no_attention. A held question is now let go only by an event that says that question is over: a tool result of the thread that asked (or of its own tool call), a Stop or a new prompt where the main thread asked, the subagent's own stop (or a Stop with no background work left) where a subagent asked, and the session's end. A tool result, a Stop or a new prompt from another thread lets go of nothing, and neither does an event whose hook fired before the question, however late it is reported. The hook and the worker tell the daemon which thread and tool call each is about; a hook older than this talking to a newer daemon, or a newer hook to an older daemon, behaves as before. Two limits remain. A thread that runs several tool calls side by side still ends its own question at a sibling call's result, with the terminal still asking. And a question that is over with no event to say so (the terminal refused it and only other threads report afterwards, or a subagent asked and the turn was interrupted) stays held until the hook's 125 seconds are up: an answer in that time is acked delivered and changes nothing.

    85ef4ae · cli

0.13.5

  • patch

    A message sent in the first moments of a turn typed in an open Claude Code window is no longer answered in a copy.

    Details

    The window's registry row says it is busy a few hundred milliseconds before its hooks' report of the turn reaches the daemon, and for a moment after they report the turn's end: a message that landed in between found a window that was busy (so not written into its inbox) and not mid-turn (so not handed over at its Stop), and ran in a copy. Every later message for that session then went to the copy, "ran in the copy made earlier", while the window sat idle. Where a window is busy by its row and its hooks have not said so yet, the daemon now waits up to two seconds for them (the message is acked taken meanwhile) and routes it again: it is handed over at that turn's Stop, or written into the inbox of a window that is idle after all. A session whose hooks have never reported to the daemon is not waited for. This applies where the daemon can read the window's own inbox at all: on Windows, and for a window that offers no inbox, such a message is still answered in a copy, as before.

    967e639 · cli

  • patch

    A run that was asked for a copy and ran as the original instead no longer frees the original when it ends.

    Details

    A Claude Code that takes no notice of --fork-session runs in the session it was told to copy, and its hooks report under that session's id: when such a run ended by itself, its own Stop and SessionEnd read as the original's, the write that had held the original counted as accounted for, and the next message was resumed in place. After a copy's run that is not known to have run in another session, the original now stays held for 90 seconds, as it is by any write of unknown hand, so the next message is copied (or waits) instead.

    551bf27 · cli

  • patch

    An ACP agent that goes away with a message to it still unwritten no longer raises an error nobody catches.

    Details

    A message longer than a pipe holds waits in the pipe; when the agent then exits or is closed, the write fails afterwards, on the agent's input stream, which had no error listener: an uncaught write EPIPE in the daemon. The failed write is now heard like the agent's exit is: whoever is waiting for the answer gets " stopped taking input: …" (or its exit), and the session is closed so the next message opens another.

    af298b3 · cli, core

  • patch

    A turn that was stopped no longer reads as answered with "No response requested."

    Details

    When a Claude Code session is resumed after its last turn was ended mid-way (a Stop sent through sessionpipe control, a killed process, a crash), Claude Code writes "No response requested." into the transcript as the assistant's, under its own <synthetic> model. sessionpipe sent that on as what the agent had said, so a receiver drew a stopped turn as answered with those words. It is now read as the end of a turn that was stopped, the way [Request interrupted by user] is: the turn goes out with what it had really said, which may be nothing. Claude Code's other synthetic rows, such as an API error, are still the turn's reply.

    54736f5 · cli, core

  • patch

    A Claude Code session is found only under the id its transcript is filed as.

    Details

    On a disk that ignores case (macOS, Windows) a command naming claude-code:8C132906-… opened the transcript of 8c132906-…, while the session's registry row and its hooks, which are what say it is open in a window or mid-turn, are kept under the id as Claude Code wrote it: the session looked idle and was resumed in place, claude -p --resume 8C132906-…, beside the person's own window. Such a command is now refused no_session ("no Claude Code session 8C132906 on this machine") on every platform, as it already was on Linux. A new session's id that differs from an existing session's only by case is still refused as taken (bad_session): on such a disk it would be written into that session's file.

    4d1a18e · cli, core

  • patch

    A Stop for a session of one harness no longer stops another harness's session that has the same id.

    Details

    The daemon looked up the turn it was running, and the session it was holding, by the id alone: a Stop for claude-code:<id> ended its run of codex:<id>, and a Stop for codex:<id> interrupted a Claude Code session it held under that id. Both are found by the whole session ref now.

    3407fc3 · cli

  • patch

    A Stop that arrives together with the turn it was pressed for stops it.

    Details

    The daemon takes the messages of one poll side by side, and a Stop has the least to check: where it was through its checks before the new session or the message it was sent after, it found nothing in hand and said so ("no Claude Code session … on this machine" for a start, "nothing is running in that session…" for a message), and the turn then began. With the two in one poll that was one or two starts in twenty-five, and every time the poll listed the Stop first. A Stop now waits the moment it takes for a start or a message for its session that has already arrived, and was signed no later than the Stop, to be in hand, and stops it. It does not wait for anything signed after it, nor for a message or a start that is itself waiting behind another turn.

    edb4f09 · cli

  • patch

    A message for a session whose copy is in the middle of a turn is no longer run into that turn.

    Details

    A session's later messages go to its copy, and the copy's hooks report under the copy's own id, but only the original's own mid-turn state was looked at: with a turn running in the copy and no registry row to say so, a message that named the original was delivered mode=resume in the copy, beside the process that had it. A turn the copy's own hooks report now holds the copy, and the message is refused: unsupported, "another Claude Code process on this computer was running the copy this session's messages go to, and a copy is never itself copied: send it again once that turn has ended (a turn that was cut short stops counting ten minutes after it was last heard from)". That ack is final. Nothing is delivered when the copy's turn ends; the message has to be sent again, and is then resumed in the copy. Where the process in the copy died without its hooks saying so, the refusal goes on for up to ten minutes after its last hook event (0.13.4 resumed the copy 90 seconds after its last write); a run of the daemon's own that ends, however it ends, lifts it at once.

    fbe5ee5 · cli

  • patch

    A session that a window has open is no longer resumed in place because a run of the daemon's own was in the way when the message was routed.

    Details

    While the daemon runs a turn in a session, the next message for it is not copied: it waits for that run and resumes the session afterwards. Whether it may be resumed was decided when the message arrived and never looked at again, so a message that waited was run with claude -p --resume beside a window that had opened the session meanwhile; a run that was only copying a session counted as a run in it; and a Codex session the daemon started counted as a run in the Claude Code session of the same id. The daemon now looks again when the waiting message's turn comes, and if something holds the session by then the message is routed afresh: into the window's own inbox, into a copy, or acked unsupported with the reason. A copy's run no longer stands for a run in the session it copies, and the daemon's record of its own runs is kept per harness.

    afeaa7d · cli

0.13.4

  • patch

    A copy that is stopped before it has written anything is not recorded as the session's copy.

    Details

    A fork names its new session in its first line of output, before there is a transcript behind the name, so a copy counts as the session's only once its transcript is on disk. Stopped before that, the turn's ack names no copy, and the next message is routed afresh instead of being sent to resume a session that does not exist.

    17968a2 · cli

  • patch

    A copy of a session that fails no longer makes the machine treat the original as free.

    Details

    The end of the daemon's own run counted as the end of whatever had last written to the session it was started from, also when that run was a copy (--fork-session), which runs in the copy and never in the original: so after a copy failed, a message sent a second later was resumed in place, in a session that something else had written to seconds before. A copy's run now accounts only for the copy, however it ends (by itself, by failing, by a Stop): the original stays held for as long as it was going to be, and a turn the person is in there is still a turn in progress, so the next message is copied again or waits for their Stop.

    2ee7f71 · cli

  • patch

    A message to a session whose headless run has just exited now resumes it in place at once.

    Details

    A transcript written in the last 90 seconds used to count as a session in use whoever wrote it, so a message sent 7, 9 or 39 seconds after a claude -p at a terminal had exited ran in a copy, and the reply said the session was open in a window. The session's own end, as its hooks report it (SessionEnd), now accounts for that write the way the end of the daemon's own run already did; a write nothing accounts for still makes a copy, since a writer may be behind it. Where a copy is made the ack says why, as the machine found it: detail opens "ran in a copy, because" and then "the session was open in Claude Code on this computer (a terminal window or the Claude app)", "another Claude Code process on this computer was running the session", or "the transcript of the session had been written to in the last 90 seconds by a process this computer could not find", followed where it applies by "; it could not be reached in place: …" with what the session's own inbox said. A later message that resumes the copy says "ran in the copy made earlier, because …". The copy itself is told "the original is open in a window" only when it is. The refusals for a held session that can't be copied say the same three things instead of "the session is open".

    2e80bf1 · cli, core

  • patch

    A Stop finds the turn under either of a session's two names.

    Details

    A session whose messages go to a copy is on the board twice, under its own id and under the copy's, and the fork's ack and the copy's own events send a person to the copy. A Stop pressed on the copy stops the turn the daemon is running for a message sent to the session, also while that turn is still being prepared, and while the copy is still being made once it has named itself; a Stop pressed on the session stops a turn running in its copy for a message that was sent to the copy. Where a message is waiting behind the turn that is running, the running one is what is stopped, and the one that waited then runs. The daemon's own child in a copy is never described as "a Claude Code process this computer's daemon didn't start".

    f127a21 · cli

  • patch

    A turn stopped while its files are still being downloaded gets one final ack, and the download ends there: the request in flight is dropped, no further file is asked for, and a file that arrives all the same is not written into the folder.

    Details

    A message stopped then is acked delivered with "(The turn was cancelled.)", and a new session failed with code cancelled and nothing of it left in the project.

    54580af · cli

  • patch

    A session whose run the daemon ended is no longer left mid-turn.

    Details

    The child's own hooks mark the session mid-turn when its turn begins, and a child that is killed reports no Stop: after a run ended at the 30-minute cap the next message for that session waited for a Stop that was never coming, up to 30 minutes. When a run of the daemon's own ends, however it ends (by itself, at the cap, by a Stop), the session it ran in is no longer mid-turn, and whatever was waiting for its Stop is delivered another way. So after a Stop the next message runs at once, and a second Stop answers "nothing is running in that session as far as this computer can tell, so there was nothing to stop".

    ca925d5 · cli

  • patch

    Nothing of a message's id reaches a session unless it is a whole ULID.

    Details

    The id is the receiver's and nobody signs it, and the last twelve characters of whatever it was went into the text written into a live session's inbox, inside the header that says "signed on the sender's device". This release puts that mark on every message handed to a live session, and makes it only from a 26-character ULID: messageMark in sessionpipe-core/control answers null for anything else, and frameMarked then frames the message without one. A message whose id is not a ULID is still delivered, as far as it can be without a mark. It is not handed to a session's sessionpipe channel and not written into its inbox, the two ways in whose only proof of delivery is the mark, so it takes the next rung instead: the session's Stop where a turn is running, else a resume or a copy (a copy's ack says "it could not be reached in place: the receiver gave this message an id that is not a ULID, so the turn it would cause there could not be told from any other"). Handed over at the session's Stop or to sessionpipe wait, it is acked delivered with no reply when the session's turn ends. Receivers that follow HTTP.md already send ULIDs and see no change.

    8fb6b5d · cli, core

  • patch

    When the daemon can't read a Claude Code's options, the refusal says why in the command's own words: claude --help failed: env: node: No such file or directory (the first line it wrote to stderr), "no such file", "the file can't be run (permission denied)", or its exit code.

    Details

    It used to say only "failed", which gave the person who sent the message nothing to act on. The line is one line of at most 200 characters, with the home folder written ~ and anything shaped like a secret removed.

    04d615f · cli, core

  • patch

    Closing an ACP agent that has already exited signals nothing.

    Details

    The daemon closes a held agent it finds gone, and the close armed its two timers all the same (SIGTERM after 2 seconds, SIGKILL after 12), whose only disarming was an exit that had already happened: both went to the number of a process group that no longer existed, or that the system had since given to another process.

    d41c1ff · cli, core

  • patch

    A headless run that reaches its 30-minute cap no longer signals its process group ten seconds after the child has exited.

    Details

    The follow-up SIGKILL was on a timer that was never cleared, so it went to the group's number even when the group was gone: the one way the daemon could signal a process it had not started (a pid reused as a group leader inside those ten seconds). The timer is cleared when the child exits, whatever the child left in its group (a tool that ignores SIGTERM) is killed at that moment, once, and a cap that arrives after the child has exited signals nothing and no longer marks the finished run as timed out. A run ended by a Stop, which this release adds, is ended the same way.

    489494e · cli, core

  • patch

    The control daemon builds the words around a delivered message with the same definitions the transcript readers take them off with (sessionpipe-core/control), so the three wordings this release adds are left out of a turn's user text like the rest: the header that names the message where it is handed to a live session at its Stop or through its waiter (Message via <host> (sessionpipe control, signed on the sender's device; message XXXXXXXXXXXX):), the note to a copy whose original is not open in a window but in use by another process, and the mark in an ask handed over in place (…with sessionpipe ask, message XXXXXXXXXXXX. Answer here in full: …).

    Details

    The paragraph that says an agent asked stays, in its plain form ((Asked by <harness>:<id> on this machine with sessionpipe ask. Answer here in full: …)), whichever way the task arrived. sessionpipe-core/control exports messageMark (null for an id that is not a ULID), frameMarked, copyNote(why) and unframe, and frameAsk takes the mark as a third argument.

    d9def02 · cli, core

  • patch

    Two messages can no longer run in one copy's transcript at the same time.

    Details

    A session's messages go to its copy, and the copy is on the board under its own id, so a person can message either. The daemon queued runs under the id each message named, so one sent to the session and one sent to the copy were started side by side in the copy whenever the copy's own hooks had not yet marked it mid-turn (always, where that account has no hooks). Runs are queued by the transcript they run in.

    4772e34 · cli

  • patch

    Messages for one session run one at a time in the order they were sent.

    Details

    Three messages sent half a second apart behind a running turn ran first, third, second: the two that arrived in one poll were verified side by side, and the one whose verification finished first went first. A message now takes its place in its session's line as it arrives, before it is verified, and the line goes by the time each message was signed (iat), then by arrival. That holds on the resume, fork and turn rungs, for a message that has to wait for its session's start, and when the message ahead is stopped, fails or is refused.

    6231d19 · cli

  • patch

    A refusal for a session this machine doesn't have names it by the start of its own id, the one a receiver's page and claude --resume use: "no Claude Code session 88749ed7 on this machine".

    Details

    It used the protocol's short id, a hash no page shows ("66a04e0a" for that session), so the person was told about a session they had never seen. The same for Codex sessions and for the daemon's own log lines. A machine whose start limit is 0 now says "this machine is set to start no new sessions (the start limit in its control config is 0)" instead of "already starting 0 new session(s); it starts at most 0 at once". A message rerouted at a Stop to a session whose transcript has gone is refused with that reason, not with a bare code. And two sessions that each have a copy are no longer taken for one session by sessionpipe ask's loop rule, which refused an ask between them as a session asking itself.

    2a06d7e · cli

  • patch

    The reply to a message delivered in place (at a session's Stop, to its waiter, into its inbox) is now the answer to that message, or nothing.

    Details

    Three things went wrong on one day: a message handed over at a Stop came back with the sentence the session had written before the message arrived (Claude Code writes its transcript a moment after the fact, so the turn's last words landed after its Stop hook had asked); a message to a session that also had a copy was read from the copy's transcript, so its ack waited 215 seconds and carried a later message's answer; and another such message was acked 30 minutes late with no reply at all. The message now names itself in its frame, by the last twelve characters of its id (…signed on the sender's device; message XXXXXXXXXXXX):, and for an ask …with sessionpipe ask, message XXXXXXXXXXXX.), the reply is what the session wrote after the rows its transcript records for that message and before anything else begins (the next message, a prompt the person types, a turn a background task starts), it is read from the transcript of the session the message was handed to, and the final ack goes out when the session's own hooks say that turn ended. Where no answer can be put down to the message the ack is delivered without a reply, and no longer carries a file path in detail when the transcript can't be read.

    ce3f0ab · cli, core

  • patch

    A Stop reaches a turn from the moment its message's turn comes, not only once its process is running.

    Details

    Before a child process exists the daemon checks the harness's login (claude auth status, seconds when it is not cached), downloads the message's files and may wait for a free slot: a Stop in any of those means the turn never starts. The Stop is acked taken and then delivered ("stopped the turn this computer was running for that session"), a message's own ack is delivered with "(The turn was cancelled.)", and a start that never began is acked failed with code cancelled and "stopped before it started, as asked: no session was created". For a session whose start is still running, a Stop stops that start and not a message waiting for it, whichever of the two the daemon got through its checks first, and it never waits for a start to end.

    eece6aa · cli

  • patch

    Stop (cancel) now stops a turn the daemon itself is running.

    Details

    A session started or resumed from a page runs as a claude -p (or codex exec, agy) child of the daemon, and a Stop for it used to be acked unsupported while the turn ran to its end. The daemon now ends that child the way it ends one at the 30-minute cap (SIGTERM to its process group, then SIGKILL to whatever is left, ten seconds later or as the child exits), acks the cancel taken and then delivered with the turn's mode and "stopped the turn this computer was running for that session", and acks the stopped turn's own message delivered with what it had written, ending "(The turn was cancelled.)". A message queued behind the stopped turn still runs next, in order. A turn in a process the daemon did not start is never signalled: the cancel is unsupported and says who has the session ("…open in Claude Code on this computer (a terminal window or the Claude app)… press Esc there", or the pid of someone else's headless run), and a session nothing is running in is expired ("nothing is running in that session as far as this computer can tell, so there was nothing to stop"). spec/CONTROL.md §6 cancel says so, with fixtures conformance/delivery/30–33.

    26eec6c · cli, core

  • patch

    What 0.13.0 to 0.13.2 told a copy ("You are a copy of that session, started to answer this message: …") is taken off a turn's user text like the notes written today.

    Details

    No daemon says that sentence any more, but every copy those versions made has it in its transcript after the person's words, where a backfill reads it. Where another agent's task went to such a copy, the paragraph that says who asked is the last of the turn's user text, as for any other ask.

    4d9654f · cli, core

  • patch

    A turn in a Claude Code terminal session reaches a tier-2 sink when it ends, not when the next one does.

    Details

    Claude Code writes its transcript up to 100 ms after the fact and a terminal's Stop hook fires sooner than that, so the worker used to read a file that did not hold the turn's last message yet: the receiver got "the turn ended" and then the prompt and the reply one turn late, or only when the session closed. The Stop names the turn's last words, and the worker now waits for the file to hold them: 400 ms with the turn's end, then for 5 s more behind it, then at the session's next hook. A turn is sent once, and whole: one that said something before a tool call is no longer sent as that half with the rest as a second turn that nobody asked. The hook itself does no more than before.

    A turn stopped with Esc or Ctrl-C is no longer lost. No hook fires for it, so at the session's next prompt, compaction, start or end the worker reads the interruption from the transcript and sends the turn with what it had said, and a turn.ended with reason: interrupt where the turn was still held as running. The next prompt is a turn of its own instead of being joined to the one that was stopped, and [Request interrupted by user] is never part of anyone's prompt.

    b6ed157 · cli, core

  • patch

    A turn's user text is what the person wrote, and nothing the machine wrote around it.

    Details

    A receiver draws that text as the person's own message, and three things were getting into it from Claude Code sessions. A subagent's report: when a subagent hands its report back, Claude Code records it the way it records a message from another session, and it was read as part of the prompt ("[Subagent hand-back] The text below is the final report of a subagent…"). sessionpipe's own words around a message it delivers: "(Sent via with sessionpipe control, signed on the sender's device.)", "Message via (sessionpipe control, …):", the note to a copy of a session ("You are a copy of that session, started because the original is open in a window…"), and the notes on how an answer travels. And [Request interrupted by user]. All are left out now, in turn.transcript, in turn.progress from its first moment, and in the title a session takes from its first ask; the list of attached files stays. sessionpipe's sentences come off only where the daemon puts them (the line before the words, the line or the note after them): the same words typed or pasted by the person stay, and a prompt that is nothing but one of them is sent as typed.

    Two of sessionpipe's own lines stay where they were, because a receiver tells an agent's words from the person's by them: the paragraph after a task another agent asked with sessionpipe ask ("(Asked by : on this machine with sessionpipe ask. …)", the last paragraph of the turn's user text however the task arrived), and the first line of a handoff's answer ("Answer from : (sessionpipe handoff, …):").

    A message delivered into a terminal session at a Stop hook is now a turn of its own: its user text is the message, marked injected like any message that arrived from elsewhere, and the reply is that turn's. Its reply used to be added to the turn before, or sent as a turn with no prompt.

    sessionpipe-core/control exports the sentences (frame, frameStart, frameMarked, frameAsk, frameHandoff, IN_PLACE_ANSWER, copyNote, RE_ARM) and deliveredWords, which takes off a text the ones that are not the message.

    59d0716 · cli, core

  • patch

    The protocol's JSON Schemas (schemas/v1/*.json) are generated from proto/sessionpipe/v1/*.proto, the protocol's one definition (RFC 0001, step 1), byte-identical to what the Zod source produced.

    Details

    The Zod file stays this release and is held equal to the proto by a test; nothing on the wire changes.

    d8523d5 · core

0.13.3

  • patch

    A claude installed under a node version manager is found by the daemon. launchd and systemd start the daemon with a bare PATH, so a Claude Code that npm install -g had put under nvm was never found; with no binary, its capabilities read as none, and a message to an open session was refused with "this Claude Code can't copy it (no --fork-session)" for a Claude Code that had the flag.

    Details

    The lookup now also checks nvm, fnm, volta and asdf (their default folders, or the ones NVM_DIR, FNM_DIR, VOLTA_HOME and ASDF_DATA_DIR name), newest node first. sessionpipe connect writes the pairing shell's PATH and the claude it found (as SESSIONPIPE_CLAUDE) into the service file, so the daemon sees what the person's shell saw. When no claude can be found at all, the daemon says "Claude Code not found on this computer" and what to do, in its log at start and in the failed ack, instead of blaming the flag.

    646042a · cli, core

  • patch

    A copy of an open session does what the message asks.

    Details

    Its note used to say "nothing you do here appears… don't carry on the original's work", so a copy asked to change a page answered that it couldn't publish.

    a0de39d · cli

  • patch

    A machine's hello says, per Claude Code account, whether the command line is signed in to it (accounts[].signed_in, CONTROL.md §2), from the same claude auth status the daemon asks before a resume.

    Details

    A session signed in through the Claude app runs fine in its window while the claude the daemon resumes with has no login of its own, and until now the first message found that out, failing with "run claude auth login"; a receiver can now say so before anyone sends one. The daemon logs the change ("claude isn't signed in as me@example.com as the daemon sees it …"), once per change. Absent when the daemon can't tell.

    0d718db · cli, core

0.13.2

  • patch

    A subagent's end now carries the background count it restates (PROTOCOL.md §4, #153). 0.12.0 added background to subagent.ended so a receiver holding a session as waiting on its own work learns the count fell before the session wakes, but the privacy allowlist still kept only agent_id and agent_type for that event, so the count never left the machine.

    Details

    turn.ended.background was unaffected.

    bf55b04 · cli, core

0.13.1

  • patch

    A config.json that does not parse (a hand edit's trailing comma, a byte-order mark, a file cut short) is no longer written over with the defaults, which erased the machine's name and every sink with its token.

    Details

    A command that would save it stops with an error naming the file and what is wrong with it; install, connect, uninstall and control pair stop before they change anything, and sink add before it asks the receiver anything. A config.json that is there but cannot be read is not written over either.

    5892379 · cli, core

  • patch

    A machine running workflows with hundreds of parallel subagents no longer runs a receiver out of requests (#208).

    Details

    A receiver is now sent a subagent's finished tool calls (tool.ended) at most one per session per minute, plus the one that shows a subagent's permission prompt was answered, the main thread's tool events as they happen, and at most one request a second from the machine, carrying everything that piled up meanwhile. Turns, prompts, permission questions, subagents and file changes are never thinned, and the local outbox still keeps every event. sessionpipe sink add … --tools all|main|beat picks what a sink is sent: all (every tool call, as before) is the default; a receiver that meters requests asks for main with tools in its well-known file, which the machine reads at sink add and connect and again once a day; beat thins the main thread too. No receiver's view changes on this upgrade unless it asked. sink list, status and doctor show each sink's setting; replay says how many tool events it thinned and takes --tools all to send every one once. A permission prompt now carries agent_id when a subagent asked, and a Codex subagent's tool calls carry one too. A worker near the end of its 15 s life no longer starts a drain it can't finish, and hands the work to a fresh worker instead; two workers can no longer both take over the same stale lock. In a simulated workflow of 100 subagents making 30 tool calls each over three minutes (6,214 events), a receiver got up to 6,214 requests before (one per event, with the jobs run one after another; real concurrent workers merged some) and 76 with the default; a replay of the issue's real events put the same rules at about 9,900 requests a day, against 39,500 with the one-a-second pace alone.

    439d8a6 · cli, core

  • patch

    sessionpipe forget takes the session out of every sink's cursor along with its outbox file, so a session that goes on after it (still running, or resumed later) is sent from its new file's first event.

    Details

    Before, each cursor kept the old file's end offset, and the sinks read nothing from the new file until it grew past that offset, then started mid-way, so the first events were never sent. The cursors are the drain's, so forget and replay wait for its lock (up to 10 s) before they change anything; past that they change nothing, say so and exit 1. A cursor file holding null or a list reads as empty instead of stopping a drain.

    2f7f39c · cli, core

  • patch

    sessionpipe install, uninstall and backfill refuse a bare word (uninstall codex, uninstall --keep-state codex, install claude-code), say to pass --<harness>, and exit 2 without touching anything.

    Details

    The word was ignored, so uninstall codex unhooked every harness and install claude-code hooked every harness found. A word that is an option's value (install --machine codex) is read as before.

    60528c6 · cli

  • patch

    sessionpipe install exits 1 when every harness it worked on was left alone (a settings file that isn't plain JSON, say), and its last line says nothing was hooked.

    Details

    It exited 0, so a script couldn't tell. One harness hooked is still exit 0.

    2cd2845 · cli

  • patch

    A harness config file the system won't let sessionpipe write (read-only: EACCES, or EPERM on Windows after attrib +r; a read-only filesystem's EROFS) is reported for that one file with the code — ! claude-code: ~/.claude/settings.json — can't write it (EACCES); not touched — the way a file that is not plain JSON already was, and install goes on to the next harness, so Codex and Gemini CLI are still hooked.

    Details

    Before, the first such file ended the whole command with EACCES: permission denied and left every harness after it unhooked. The refusal is caught where each file is written, so a file the adapter wrote before it (Codex's hooks.json before config.toml, one Claude Code account's settings.json before another's) keeps its report, created included, which uninstall needs. uninstall reports a file it can't write or delete the same way and goes on. Only a write that fails after the file was opened (ENOSPC) says the file may be cut short instead of "not touched".

    a0197d5 · cli, core

  • patch

    A drain skips an outbox file it can't read (a 0600 file a hook left as another user under the same HOME, or, on macOS and Linux, a folder named <id>.jsonl; Windows reads a folder as an empty file) with a line in the log naming the file and the error, leaves that session's cursors where they were, and still sends every other session to every sink.

    Details

    Before, the read error threw out of the drain: no sink took the sessions listed after that file, nothing was logged, and the prune that follows the drain never ran.

    6456357 · cli

  • patch

    pii@1 keeps an ISO date: released 2026-10-31 stays as written instead of becoming released [phone], and 2026-10-31 12:00 no longer becomes [phone]:00.

    Details

    A date (years 1900–2099, month 01–12, day 01–31) is kept only when nothing after it could carry the run on: no letter or digit, and no separator then two digits (a phone's group; 2026-10-31 5 files keeps its date) unless that is a time's hour ( 12:00, 00–23), and not right after a ) or a + with a country code. A date after a + (a diff's added line) is kept too. A date that a phone's digit group follows directly, as in 2026-10-31 415-555-0132 or 2026-10-31 0132:, is read as before (a (, + or , between them starts a new number, and the date stays).

    f24d3c2 · cli, core

  • patch

    A sink added with --secret and no --token (it signs with Standard Webhooks and sends no bearer) sends again once its secret is in the macOS Keychain or the Secret Service, which connect does by itself on a Mac: the secret was stored, but reading it back gave up without a token, so every drain treated the sink as locked and its events waited for good, sessionpipe secrets said it couldn't be read, and secrets move file left it there.

    Details

    Each item the store holds is read now, so a sink an older release left stuck comes back the same way, and secrets move file brings it home. A sink with both items whose token item alone is gone sends signed and without a bearer, and the receiver's 401 pauses it (doctor says so) instead of its events waiting silently; a locked store still leaves them waiting. sink add with --token on such a sink keeps the secret its store holds, instead of dropping it.

    a66c937 · cli

  • patch

    A batch the receiver answers 413 is halved, and when one half then gets a 400, only that half is dropped (and logged with its size); the other half is still sent.

    Details

    Before, the 400 on the first half was taken as the answer for the whole batch: the cursor moved past both halves, so the second half's events were never sent, and the log named only the 400.

    f954c5e · cli

  • patch

    --tier takes a whole number: sink add, install --sink, connect, control pair and tail refuse --tier with its value left off, empty, or not written in digits (--tier one, --tier 1.5, --tier 0x2, --tier --name x), with "--tier takes 0, 1, 2 or 3", before doing anything.

    Details

    The other sink, install and control subcommands refuse such a --tier too, where they ignored it. connect --tier "" used to fall back to the default tier and sink add --tier "" stored 0; both are refused now. Until now sink add stored --tier one as null (and --tier 1.5 as 1.5, which no event can carry) and printed "tier NaN sends: undefined", and stored --tier with its value left off as tier 0 without a word. A value out of range is still held to 0–3, and a tier of 1.5 already in config.json sends as 1. The hook reads a sink's tier the same way, capped by the receiver's max_tier too: a stored 0.5, or a sink a receiver caps at 0, no longer turns every tool event, or every status line's limits, into a job. A sink with no tier in config.json (a hand edit) sends at tier 0, where its events carried tier: null, which no receiver takes.

    ed73aa5 · cli

  • patch

    sessionpipe update with a word other than off or on (update OFF, update of) prints its usage and exits 2.

    Details

    It installed the latest release instead, so someone switching updates off got an upgrade and a daemon restart.

    21f0ebb · cli

  • patch

    On Windows, status, doctor, secrets and connect say where a key lives (a file under your profile, not "0600": modes mean nothing there) and that updates run only while sessionpipe control run is running, since no service runs the daemon on Windows yet.

    5c0408c · cli

  • patch

    Devin: the transcript walk ends at a node it reaches a second time, so a session store whose parent chain loops back on itself no longer hangs the reader.

    4eb5a68 · core

  • patch

    A session whose outbox file was pruned (untouched for keep_days, 30 by default) and then resumed under the same id is sent again.

    Details

    Before, pruning also deleted the session's seq counter but left each sink's cursor at the old file's end, so the resumed session restarted at seq 0, which a receiver's (harness, session, seq) deduplication drops as already seen, and the sinks read nothing from the new, shorter file until it grew past the old offset, then started mid-line. Now the counter stays and each sink's cursor into the pruned file is removed.

    7295fc9 · core

  • patch

    sessionpipe-conformance runs a single fixture file as well as a folder, takes HTTPS:// in any case, and says in one line which path it could not read (the target, or an entry under it) or that a folder holds no .json fixtures (exit 2), instead of a stack trace.

    Details

    A file that is not an adapter fixture (a control vector, a delivery or redaction fixture), is not JSON, or cannot be read is a failure row naming why, instead of "no adapter undefined" or a throw that ends the run.

    898c7af · conformance

0.13.0

  • minor

    sessionpipe ask <agent|session> "<task>": a session hands a task to another agent on this machine and gets the answer on stdout (CONTROL.md §13).

    Details

    The request reaches the daemon on its local socket, never through a receiver. The daemon checks, in order and each with its code, that asking is on, that the agent is one it can run headlessly or a session it can message, the folder allowlist, the depth (A asks B, B may ask C, C may not), loops and the start caps, then runs the ask down the same start, prompt and headless paths a receiver's command takes, at safe, or read when asked (--permission), never wider than the machine's mode whatever it is set to (a session already live keeps its own). The daemon counts depth and loops for itself from the asks it has in hand, under the names it knows the sessions by, and takes three asks at a time per asker and per session asked; a reply_to it can't place when the command arrives makes the command no handoff at all (nothing relayed, no event). ask on from inside a session is refused unless --yes: allowing a folder and installing a service is the person's call. Words after -- are the task, flags or not. The child's first message is the task and who asked; its environment carries SESSIONPIPE_ASKED_BY and SESSIONPIPE_ASK_CHAIN (the hook carries the first into the job, an Agent SDK session gets both, and a worker never leaks its own), so the child's events say delegated_by and the asker's say delegation.started and delegation.ended. At the terminal the id and progress go to stderr and the answer alone to stdout, exit 0; a refusal or a failure to stderr, exit 1; a usage error 2; --json prints {ask_id, session, reply, ms}. A prompt or a start with reply_to is a handoff: after its final delivered ack the daemon delivers the reply into that session as a message (§6 Handoff), and the events on reply_to say whether it was delivered, unsupported or failed. The pairing request and every hello say ask and handoff: true. sessionpipe ask on|off|status: on writes ask: true into control.json and runs the daemon as a service even with no receiver paired (control run serves the socket alone unless asking is off too; control off keeps the daemon when asking was turned on by hand); off sets it false and removes the service when no receiver remains; status prints the switch, the daemon, the agents it can ask and the folders. sessionpipe ask setup [--remove] writes a marked section into every Claude Code CLAUDE.md and ~/.codex/AGENTS.md so a session knows the command exists and must never use it to send its own reply; hooks still inject nothing.

    aa44c74 · cli, cli, core, core

  • minor

    sessionpipe channel: a Claude Code channel, so a message reaches a live session in its own thread.

    Details

    Claude Code runs it as one session's MCP server (claude mcp add --scope user sessionpipe -- sessionpipe channel, then claude --dangerously-load-development-channels server:sessionpipe while channels are a research preview). It parks on the daemon's local socket as that session's waiter for as long as the session runs, and pushes each message in as a notifications/claude/channel: the session takes it in its own permission mode, as the person's own words, and shows it as a message from sessionpipe in the terminal and in the Claude app. The daemon routes to it as a waiter while the session is idle and reads the answer back from the transcript. The transcript reader now keeps a channel's message (origin.kind: "channel", wrapped in <channel source="…">) as its turn's ask: a turn asked through a channel, Telegram's included, used to reach a receiver with nothing asked.

    aedb6e9 · cli, cli, cli, core, core, core

  • patch

    Control: a claude --help that couldn't be read is never taken for a Claude Code with no permission modes (#152).

    Details

    detectCaps marks a run that times out, or fails without listing --permission-mode, as unread (and takes a timeout, 30 s by default); modeFlags then refuses every level but inherit, saying why ("this machine couldn't read its Claude Code's options (claude --help timed out after 30 s); try again in a minute"), where it used to refuse auto as a mode that Claude Code lacks and to run safe with no permission flag at all. The daemon asks an unread --help again a minute later, and logs each one, instead of keeping it until a restart. A daemon that started 82 s after a Mac's reboot, beside 28 other Claude Code processes, kept such a read for 8 h 12 min, and refused the message sent in that time.

    While that --help is unread, the hello offers no Claude Code start at all (an offered safe was taken, and then run with no permission flag), and a start is refused before it is taken, never run with an empty argv.

    b4a0d39 · cli, core

0.12.0

  • minor

    A session waiting on its own background work no longer reads idle on a receiver until the work ends (#153).

    Details

    turn.ended.background leaves out sessionpipe's own waiter (sessionpipe wait, which ends only when a message arrives, and so held every reachable session at "working"), subagent.ended carries background, the count once that subagent is out of it, and PROTOCOL.md §4 says a receiver SHOULD keep such a session as working until its next turn.ended or session.ended, not until the next event, which was always the workflow's own. Fixtures: harness/claude-code/stop-waiter (recorded on 2.1.312) and subagent-stop.

    6e4890b · cli, core, receiver, conformance

  • patch

    Control: a permission prompt in a turn the daemon itself runs (a session started or continued from a receiver, held through the Agent SDK or resumed headless) now waits for the owner's signed answer, as a terminal session's does.

    Details

    It used to skip that wait, so the receiver showed Allow and Deny, every Allow was refused no_attention ("that permission is no longer open"), and the turn sat on the prompt with the next message queued behind it. The Stop hook of such a turn still never asks: the daemon hands that session its next message itself.

    e8c0da2 · cli

  • patch

    Windows: a harness installed with npm install -g is a .cmd shim (claude.cmd, codex.cmd, gemini.cmd), which node refuses to spawn without a shell (EINVAL, since 20.12).

    Details

    doctor and connect died with spawn EINVAL on such a machine, claude --help silently read as a Claude Code with no capabilities, and every headless turn the daemon ran for Claude Code, Codex or an ACP agent failed the same way (#165). Now the shim's own script (the one npm's shim names) is run under the node sessionpipe runs on, with no shell, so a prompt reaches the harness byte for byte; a probe with fixed words (--help, auth status, login status, --version) whose launcher can't be read that way runs through a shell as one quoted command; and a headless turn whose launcher can't be read says so instead of spawning it. Elsewhere nothing changes.

    46f9ef1 · cli, core

0.11.0

  • minor

    Session names from claude.ai, off until the person turns them on (sessionpipe titles on, config.json remote_titles). claude.ai names a session the Claude app starts seconds after its first message and never writes that name to the machine, so until now such a session went out under its first prompt cut to 50 characters while the Claude app showed another name.

    Details

    With it on, the worker asks claude.ai for the name of each Claude Code session with a claude.ai link (GET https://api.anthropic.com/v1/code/sessions/<session_…>, as Claude Code reads a session; the answer recorded in conformance/remote/claude-code/), a minute apart until it has one, then every half hour, within 3 s, and sends it as session.title with title_source: "harness"; a custom title still wins. titles on and titles refresh [--days N] also ask about the last 30 days' sessions and send each one already sent under another name again as session.backfill, so finished sessions are renamed too; a backfill takes these names as well. It bears the sign-in Claude Code keeps for that config dir (the macOS keychain item, else .credentials.json), sends it to api.anthropic.com alone, refuses redirects, never refreshes an expiring token and never writes it anywhere. Adapters gain an optional remoteFacts and BackfillRow.transcript; Config gains remote_titles. spec/ADAPTERS.md, spec/PRIVACY.md §3 and the session.backfill row of spec/PROTOCOL.md say so.

    91a1e57 · cli, core

  • minor

    limits.observed reaches tier-1 sinks: after a Codex turn when its rollout holds new numbers, and from Claude Code's status line with install --status-line (or connect --status-line; --no-status-line takes it away; remembered in the config).

    Details

    A status line hides most of Claude Code's footer hints, so it is opt-in. The line shows the limits (5h 23% · week 41%) and hands the worker a job only when they changed, at most once a minute per config dir and never without a tier-1 sink. Before this a receiver could not tell which account still had room in its 5-hour or weekly window.

    3f074d8 · cli, core

  • minor

    The pool: several Claude accounts behind one loopback listener the control daemon runs, so a Claude Code session keeps going when one account's limit is reached.

    Details

    sessionpipe pool add --import copies this machine's Claude Code logins into the pool (--login signs another account in: a URL, then the code pasted to --code -), pool on writes the env block into every Claude Code settings.json and installs the daemon's service if it is missing, pool lists the accounts in order with the room each has left, order, enable, disable, remove and refresh shape the roster, and pool off removes exactly what on wrote. A request goes to the first account in your order that has room; a session stays on its account while that account has room; the numbers an account reports on a session's request become a tier-1 limits.observed with account_id on that session, through the outbox like a hook's. Every hello carries the pool (pool object), a receiver's signed pool command drives it (on, off, remove, order, disable and login.complete need a fresh confirm, never a day grant alone: confirm_required), and while the daemon runs the CLI's commands go to it over the local socket, since it holds the listener, the pins and the credentials. The daemon now runs for the pool alone, with no receiver paired, and control off keeps its service while the pool is on; sessionpipe uninstall runs pool off first. Nothing of this happens until you run pool add or pool on yourself.

    227e247 · cli

  • minor

    The account pool's contract (CONTROL.md §2 pool, §6 pool; PRIVACY.md §3): a hello MAY carry pool (on, harness, accounts in the machine's order with status, current, until, limited_by and the idle poll's windows; present in every hello of a daemon that knows the pool, on: false and no accounts when nothing is set up), and a new signed kind pool on the pseudo-session sessionpipe:pool (pool_action: on, off, enable, disable, remove, order, refresh, login.begin, login.complete; pool_account, pool_order of at most 20, pool_code of one line ≤ 512 characters), verified like key.add: each action takes exactly its own fields (poolProblem, POOL_SESSION, POOL_ACTIONS).

    Details

    limits.observed gains account_id, the account the numbers belong to when a pooled session was served by another than its own; tier 1 carries it and pii@1 drops it as it drops session.account_id. Schemas: control-command, control-hello, event-data; control vectors 137–164.

    5bfc8cb · core

  • patch

    Codex's config.toml keeps its CRLF line ends through install and uninstall: the notify line goes in with the file's own line end, and uninstall leaves the file byte-identical, as it already did for an LF file.

    83929e3 · cli, core

  • patch

    install, status and doctor read Gemini CLI's version on Windows, where the CLI is npm's gemini.cmd shim; before, every Windows install said "could not run gemini --version" and the 0.46 check never happened.

    b5285de · cli, core

  • patch

    A hooks file that starts with a UTF-8 byte-order mark (PowerShell 5's UTF8 encoding, older Windows editors) is read like any other and written back with the mark, so install hooks it instead of reporting "not plain JSON; not touched", and uninstall leaves it byte-identical.

    Details

    The harnesses read such a file themselves.

    1891dab · cli, core

  • patch

    sink add and sink remove rewrite the hooks when a sink crosses tier 0, as install's own message says they will; before, the hooks stayed the lean set, status read stale, and doctor blamed an old node or script path.

    Details

    doctor now says which set the hooks are and why that no longer matches, when that is the cause.

    2153f5b · cli

  • patch

    sessionpipe tail names the outbox with the platform's own separator: ~\AppData\Local\sessionpipe\outbox on Windows, where it read …sessionpipe/outbox.

    caa12b5 · cli

0.10.0

  • minor

    Control can start, prompt and cancel any agent that speaks the Agent Client Protocol (CONTROL.md §6, api mode; ADAPTERS.md "Agents driven through ACP").

    Details

    The daemon runs the agent's own CLI as a child on the launch line the ACP registry publishes (Gemini CLI, OpenCode, goose, Copilot CLI, Cursor, Droid, Devin, Kilo, Qwen Code, Cline and twenty more, 2026-10-22), is its JSON-RPC client, keeps the process for the next message, cancels with session/cancel, reopens an idle session with session/load where the agent can, and answers its permission requests from the machine's mode (safe refuses and the reply says what; auto allows). Only an agent already installed is ever run (on PATH, or where SESSIONPIPE_ACP_<HARNESS> points); the hello advertises the ones found. The pairing request and every hello list at most 20 harnesses, the schema's limit (Claude Code, Codex and Antigravity first, then the agents in the registry's order), and their modes include api whenever an ACP agent is listed.

    5ea2f11 · cli, core

  • minor

    A Claude Code home that runs on another API now has an account.

    Details

    Such a home has no claude.ai login, and its settings.json sets env.ANTHROPIC_BASE_URL (Kimi K3 on a Modal endpoint, GLM on Z.ai). Its events carry account_id: api-<16 hex>, made from the endpoint's origin and the same on every machine that uses it. Control's pair and hello name it by the endpoint's host, in accounts[].name, a new optional field. Before this, its sessions carried no account, so a board filed them with the harnesses that report none (a person's Kimi sessions sat in "No account" beside 1,331 Codex ones). Only that one settings key is read, never the token or the headers beside it.

    7b65074 · cli, core

  • minor

    Control: device keys (CONTROL.md §2, §3.2).

    Details

    sessionpipe-core/control verifies a device key's signature ({cred, sig}, raw r‖s over sessionpipe.device.v1⏎<purpose>⏎<rp id>⏎<challenge>) for a grant, a confirm or a pairing proof, chosen by the machine's own record of the key; keyUsable closes key fields and checks a device key's rules (type: "device", alg -7, store: "hardware", uv: "each-use", id = SHA-256 of the SPKI); verifyCommand refuses a key.add whose key could never be used (bad_fields). verifyAssertion takes the purpose. New exports DEVICE_CONTEXT, deviceMessage, deviceKeyId, KEY_TYPES, DEVICE_SIG_FIELDS, DeviceSignature, DeviceGrant. The daemon advertises key_types: ["passkey","device"] when pairing and in every hello, stores a device key whole (from a pairing or a key.add), and control keys shows each key's kind. 26 new shared vectors (111–136).

    27c4194 · cli, core

  • minor

    Control: a third machine mode, inherit, so sessions you message run the way your own sessions on that machine run.

    Details

    sessionpipe control mode inherit (or --mode inherit on control pair and connect, or the whole word at pairing's question) passes no permission flag, so each harness starts every turn in its own default there: Claude Code in its settings' permissions.defaultMode (a resume too, and through the Agent SDK too), Codex as its config.toml's approval_policy and sandbox_mode say, Antigravity with no flag, as in safe. No mode passes a bypass flag. Before this, a machine whose Claude Code settings turn auto mode off (permissions.disableAutoMode) had no mode that fit: auto ran each messaged session in Claude Code's default mode, which refused every command that needed approval, and safe refuses them by design. Schemas: control-pair and control-hello mode gains inherit.

    abcba7f · cli, core

  • minor

    Control: a machine runs in inherit mode only with receivers that know it.

    Details

    A receiver that predates inherit shows such a machine as safe, while its sessions run with no permission flag. A receiver now says it shows the mode as what it is with control.inherit: true in its well-known file (HTTP.md §1, CONTROL.md §2), and sessionpipe control pair --mode inherit (and connect --mode inherit) refuses, before anything is sent, a receiver that doesn't: "… doesn't know inherit mode yet; it would show this machine as safe while the sessions you message run with no permission flag". sessionpipe control mode inherit checks every paired receiver the same way and keeps the mode it had when one doesn't know it, or can't be read. Schema: well-known control.inherit (optional boolean). Conformance: delivery/23-control-inherit.

    531b391 · cli, core

  • minor

    For receivers. What a 0.10.0 machine sends and accepts, and how to adopt it while some machines still run 0.9.0 (CHANGELOG.md, "For receivers (0.10.0)", has the detail).

    Details
    • Closed command field set. Each command kind carries only its own fields; any other field inside the signed cmd is refused bad_fields, by the verifier and on the machine (0.9.0 ignored it). Keep your own data beside cmd, never in it.
    • mode: "inherit" in pairing requests and hellos: no permission flag, so a start that names no level runs as the person's own sessions there do, permission checks off included where their settings say so. Show it as inherit, never as safe or auto. A 0.10.0 machine pairs in inherit, or switches to it, only with a receiver whose well-known file says control.inherit: true; it tells everyone else's person "… doesn't know inherit mode yet; it would show this machine as safe". Set the flag once you show the mode truthfully and accept it in /pair and /hello (conformance delivery/23).
    • Refusal code mode is now permission, plus refused model and failed model. Map both codes; a 0.9.0 machine still says mode.
    • Hello: start (per harness: permissions among read, safe, edits, auto, capped by the mode; the default permission, which may be inherit; models, any_model), prompt_files: true, and no home: a start's cwd may be ~ or ~/… instead. harnesses lists at most 20, and modes include api when an ACP agent is listed.
    • Acks: a start's taken and delivered carry applied (permission may be inherit). Show applied, never what was asked.
    • Events: session.attended, turn.ended.background, turn.started.submitted_by and injected, session.launched_by, and account_id api-<16 hex> for a Claude Code home on another API, named in accounts[] by name with no email. All optional, tier 0.
    • ACP agents (gemini-cli, opencode, goose, cursor, copilot-cli, droid and more) appear in harnesses and in ack session refs, acked in mode api; they take no start options.
    • Device keys: an app that can't run WebAuthn signs with a hardware-held P-256 key, {id, alg: -7, spki, type: "device", store: "hardware", uv: "each-use"}, its signature {cred, sig} (plus str in a grant) where an assertion would stand; a 0.10.0 machine says key_types: ["passkey","device"] when pairing and in every hello. To accept them, store and carry the key whole (all six fields) in a pairing's and a key.add's key, pass a {cred, sig} grant, confirm or pairing proof through untouched, and say control.signing.key_types: ["passkey","device"] in your well-known file. They are added only as passkeys are: at the terminal's pairing code, or by a key.add a trusted key signed. A receiver that doesn't know them keeps working, passkeys only.
    • While machines run 0.9.0: send permission or model only where the last hello's start advertises them (0.9.0 ignores them silently); files on a prompt only with prompt_files: true (0.9.0 refuses them); a ~ folder only to a machine whose hello has start; never inherit as a command's level; a device key only for a machine whose last hello lists device in key_types (0.9.0 would store it as a passkey and refuse its signatures); read an absent attended, background, submitted_by, injected or launched_by as unknown, never as false, 0 or a person.

    2865e0d · cli, core

  • minor

    A control prompt can carry files, as a start can (CONTROL.md §3.1, §6 prompt), to a machine whose hello says prompt_files: true.

    Details

    The daemon fetches and checks them as for a start, writes them to <cwd>/.sessionpipe/files/<session>/<nonce>/ (the command's signed nonce, never the receiver's unsigned message id), and appends the same "Attached files" block before the message is handed over in any mode: a resume, a fork, a held SDK session, an ACP agent (api), a waiter or a Stop. A file that fails is acked failed with code file and the message is not delivered. A receiver no longer needs to put a link to an image into the message's text.

    a6f8bb7 · cli, core

  • minor

    session.attended says whether a person attends the session: an optional tier-0 boolean in the harness's own word, absent when the harness does not say (unknown, never false).

    Details

    Claude Code fills it from CLAUDE_CODE_SESSION_ATTENDED, counting a control turn and a session with a link (its Remote Control bridge, the desktop app) as attended: every headless run says 0, a Remote Control session included, and a claude -p started inside one inherits CLAUDE_CODE_ENVIRONMENT_KIND=bridge, so the link is what tells them apart. The hook carries CLAUDE_CODE_SESSION_ATTENDED and SESSIONPIPE_CONTROL_JOB into the job, and the worker takes those and CLAUDE_CODE_HOST_SESSION_ID from the job alone, so a worker another session's hook spawned no longer lends a swept job its own. Codex, Gemini CLI, Antigravity and Devin leave it absent.

    d51fd31 · cli, core

  • minor

    session.launched_by: the session that started this one, as <harness>:<id>, read from SESSIONPIPE_LAUNCHED_BY in the hook's environment (PROTOCOL.md §3).

    Details

    A script that runs claude -p for a session sets it, and a receiver can group the run under its launcher without reading it as a subagent. A subagent keeps its parent_id and gets no launched_by.

    fae77a5 · cli, core, conformance

  • minor

    A control start can say how much the new session may do and which model it runs (CONTROL.md §2, §3.1, §5 step 8, §6 Options; ADAPTERS.md "Start options").

    Details

    permission is one of four levels, read, safe, edits and auto, with nothing above auto; model is the harness's own id, passed as one argument. The machine's mode is the ceiling, and a start above it, or at a level the harness has no flag for, is refused permission before anything is taken or run (the daemon's old mode code). On a machine in inherit mode a start that names no level runs with no permission flag, as the person's own sessions there do, and one that names a level may name only read or safe, since sessionpipe can't rank the person's own default; inherit is never a level a command names. The hello advertises what each harness takes here (start, capped by the mode: Claude Code's levels from its --help, Codex's models from its own cache; a harness's default may be inherit). A start's cwd may be ~ or ~/…, read under the daemon user's home, so a receiver can start a session in a folder an event reported without the machine ever telling it its home. A start's level and model are recorded with the session, and every later resume or fork runs at the narrower of that and the machine's mode then (at most safe on an inherit machine for a session that named a level). The taken and delivered acks of a start carry applied (its permission may be inherit); a model the harness rejects ends failed with code model. An ACP agent takes neither option: a start that names one is refused, never run without it. Each command kind now has a closed set of fields, so a field a machine doesn't know is refused rather than ignored. sessionpipe control status shows the options per harness. A start whose text begins with a dash no longer reaches claude -p as a flag.

    bd49ef7 · cli, core

  • minor

    turn.ended carries background: how much background work the harness reports still in flight as the turn ends, each piece of which wakes the session when it finishes.

    Details

    Claude Code's Stop hook lists it as background_tasks (in every Stop recorded since 2.1.258): a background shell command, a monitor, a subagent, a workflow. A session that ended its turn waiting on a two-hour background command read as finished on its receiver, under "Last 24 hours", until the command woke it. Tier 0, a count only: never what that work runs.

    d7269b4 · cli, core

  • minor

    turn.started can now say who started the turn: two optional tier-0 fields, submitted_by (the harness's own word for who submitted the prompt, forwarded verbatim and never mapped onto a vocabulary of the spec — a different vocabulary from session.started.source) and injected (true when the prompt text is a block the harness wrote rather than words a person typed).

    Details

    Claude Code fills submitted_by from UserPromptSubmit.source when a payload carries that key — its hook schema documents user · sdk · system · loop_wakeup · schedule_wakeup · poll_event and says a payload may omit it while the field rolls out, and no build through 2.1.304 sends it — and sets injected from a closed list of its own prompt wrappers (<task-notification>, <cross-session-message …>, <system-reminder>, <local-command-stdout>, <command-…>), so a task notification, a peer message or a slash-command expansion is distinguishable from a person typing. injected: true says only that the prompt is not the person's words, never that no person caused the turn: a Talk or sessionpipe wait delivery arrives inside a <task-notification>. Both fields are absent when the harness does not say, and an absent field is unknown, never "the person". sessionpipe tail prints both. Codex, Gemini CLI, Antigravity and Devin leave both absent.

    f8770b9 · cli, core

  • patch

    Two Node-only subpaths in sessionpipe-core, so a receiver of the person's own (a board on their machine) runs as a service with the daemon's code rather than a copy of it.

    Details

    sessionpipe-core/service: the launchd agent and the systemd user unit for any process (serviceFiles, installService and uninstallService, each with a dry run that answers the files and commands and touches nothing, and ensureLinger). sessionpipe-core/version: compareVersions, isNewer, parseVersion, UPDATE_EXIT and supervise, the parent that restarts a process onto new code where no service manager will. The root export is unchanged and still runs in a Worker and a browser. The control daemon now uses both; its service is the same, except that the unit's ExecStart quotes every argument.

    fc0fedd · cli, core

0.9.0

  • minor

    Control daemon: a start may bring files, for every harness it can start (Claude Code, Codex, Antigravity).

    Details

    The daemon fetches each from {control}/files/{sha256} with the machine's token, stops reading past the signed size, checks length and hash, writes them under <cwd>/.sessionpipe/files/<session>/ (self-ignoring .gitignore, no symlinks followed, exclusive create, mode 0600) and starts the session with the text plus the list of relative paths. Any file problem acks failed with code file and starts nothing. Pair and hello now say files: true.

    55320c7 · cli, core

  • minor

    sessionpipe control folders [add|remove <dir>…]: change the folders that sessions you message may run in, at the machine, without pairing again.

    Details

    Before this, the folders were set only by control pair --folder. On 2026-10-13 a Kimi session in ~/claude-kimi on a machine paired with ~/claude was refused ("that session's folder isn't one this machine allows"), and the only fix was a new pairing, with its passkey approval. A folder to add must exist and can't be the home folder or above it (pair's rule). The running daemon picks up the new list within seconds, and when the folders or the mode change it sends a hello at once, so the receiver's folder picker doesn't wait up to ten minutes for the next one.

    ac26495 · cli

  • minor

    A devin adapter: Devin CLI (Cognition) sessions are reported like any other harness.

    Details

    sessionpipe install (or --devin) writes the eight event names Devin accepts into the hooks key of its user config file; the payload maps to the protocol's events, the folder comes from DEVIN_PROJECT_DIR (nothing in the payload names it), and tier 2, the model, the title and the times are read read-only from Devin's own SQLite session store — through a new readers/sqlite.ts that works on Node 20 too (in-process node:sqlite, else a short child process, else the sqlite3 CLI, else no turns). Eight fixtures recorded from real runs on 3000.11.3. Also: the claude-code adapter now drops a payload with no transcript_path and a non-UUID session id — that is another harness running our entry in ~/.claude/settings.json, not Claude Code — and the worker logs a dropped event instead of discarding it in silence when no adapter owns its harness name.

    d525320 · cli, core, receiver, conformance

  • minor

    Review fixes to the Devin adapter and two gaps it exposed.

    Details

    devin: a turn is now timed by the message's own metadata.created_at, not by message_nodes.created_at — that column is the time the row batch was last saved, one value for the whole chain, rewritten on every save and not monotonic along it, so every turn of a session was reported at the same wrong instant. The chain read is now bounded by the cursor and caps from the newest end: past 4000 nodes the old query kept the 4000 oldest and then delivered nothing ever again, and every Stop re-read the chain's root (a 17 kB system prompt and four more system blocks). installed() reports inactive, not current, when the config file holds any event name Devin does not know — Devin then loads no hooks from that file at all, ours included — and install() says so too. A tool_response that is not an object no longer loses its output. facts() takes the hook's environment (new optional argument on the Adapter interface), so a job swept by a worker another harness's hook spawned still reads the right config and session store.

    sessionpipe uninstall takes the same --<harness> selection install has, removes the shared hook launcher only on a full uninstall, and refuses an all-harnesses uninstall when SESSIONPIPE_CONFIG/SESSIONPIPE_STATE point away from this machine while other harnesses would still be read from $HOME (pass --all to mean it). The hook forwards XDG_CONFIG_HOME, XDG_DATA_HOME and APPDATA in the job file, beside the per-harness variables already there. The control wait — up to 125 s on a PermissionRequest — now applies the same "this is not a Claude Code payload" test the worker does, so our entry in ~/.claude/settings.json run by another harness returns at once. A payload no adapter claims is logged rather than dropped in silence.

    Tests: the Devin session-store tests build their fixture through the sqlite3 binary where node:sqlite is absent, so they run on the Node 20 floor — where the cli engine is the only way into a database, and where all seven used to skip. Each engine is asserted where it works and skipped only for itself; inlineParams and the adapter's own chain query are covered.

    3fd0041 · cli, core, receiver, conformance

  • minor

    turn.progress (tier 2): a turn still running goes out as it runs, not only when it ends.

    Details

    The prompt is sent as the turn starts, then what the assistant has said so far, at most every 20 s on a tool call and only when it changed, under the number its turn.transcript will carry. Over 20 000 characters the reply keeps its end. A 21-minute turn used to show nothing on its receiver for 21 minutes.

    0940788 · cli, core

  • patch

    A batch a receiver refuses with 413 is halved and sent again until it fits, and one event too large on its own is dropped and logged (HTTP.md §2).

    Details

    Before, the sink stopped for the run and the next worker sent the same batch again, so one oversized batch stopped a sink for good.

    6ee80e1 · cli

  • patch

    An Antigravity session in a git checkout now opens its own conversation from its link, not Antigravity's home.

    Details

    Antigravity keeps a git checkout's folder under gitFolder in ~/.gemini/config/projects/<id>.json ({ "gitFolder": { "folderUri": … } }) and only a plain folder's at the top of the resource, so the session's project was never found for a repo: the link had no ?p=c/<conversation>?section=<project>, or named an enclosing plain folder's project instead (6 of 8 projects on one machine, 2026-10-20).

    8184e2a · cli, core

  • patch

    A busy session's events no longer wait 6 s and go back to the queue behind its own lock.

    Details

    A lock whose worker is gone (its 15 s deadline ended it without a release) is taken at once instead of after 5 min; a job polls for the lock every 50 ms instead of doubling its wait, so siblings can't keep taking it in between; and the sinks drain at once, so the lock is held for the slowest round trip, not the sum of them (33 re-queues in one day's log on a machine with three sinks, 2026-10-13).

    91265cb · cli, core

  • patch

    Claude Code: a message the person sends while a turn is running now reaches turn.transcript and turn.progress.

    Details

    Claude Code records it as a queued_command attachment rather than a user record, so the transcript reader dropped it from tier 2.

    366043f · cli, core

  • patch

    The sessionpipe command's entry runs only when invoked as the script (through its npm bin link too), so every command can be tested in process.

    Details

    Output and exit codes are unchanged.

    023993a · cli

  • patch

    The usage table behind --help is exported (usageTable), so sessionpipe.org's docs page prints the same usage lines the CLI does; --help output is unchanged.

    Details

    The README's closing links sit at its end again, after "Help and status JSON".

    8253085 · cli

  • patch

    Skip the parent's replayed history in forked Codex rollouts when reading session facts and transcripts.

    Details

    Wait for an event naming the fork's own thread, preserve its original transcript timestamps and cursors, and leave its title unset until it contains a real user ask of its own.

    94c1b2f · cli, core

  • patch

    Codex: only the person's own threads become sessions.

    Details
    • An ephemeral thread is no longer reported: its hooks carry transcript_path: null and its notify finds no rollout. Codex Desktop runs its own background work this way (ambient suggestions and their safety review, a new chat's title), and so does codex exec --ephemeral. Each used to arrive as an untitled session with no turns, often with / as its folder.
    • A subagent's thread is no longer a session of its own. Its parent already reports it (SubagentStart / SubagentStop, its tool calls), so its notify and its rollout (backfill) report nothing. A fork the person made is still a session.
    • Inside a subagent, Codex sends the parent's session_id with the subagent's own rollout. That file is no longer read as the parent's transcript, so the parent's running turn (turn.progress) no longer shows a subagent's words.
    • Conformance: a harness fixture with an empty expect is a payload the adapter must report nothing for.

    81764cc · cli, core, conformance

  • patch

    The control daemon retries pending events for every non-paused sink once a minute, including sinks whose token is in config.json.

    Details

    A session whose final delivery was refused no longer needs another hook event to reach its sink after the retry wait. The existing per-sink wait and machine-wide delivery lock still apply.

    e2ba5c8 · cli

  • patch

    sessionpipe control pair names a first pairing the way the hooks name the machine: machine from config.json, else the hostname without its .local, .lan or .home suffix.

    Details

    On macOS the daemon used to register as <name>.local while events said <name>, so a receiver showed the same machine twice and could not route approvals to it. --name and an existing pairing's name still win.

    e252398 · cli

  • patch

    Retry control polls after a 401 with a capped backoff, resume immediately when a hello accepts the token, and honour Retry-After on 503 responses.

    Details

    Show polling state, time since the last successful poll, and the last control error in status and doctor.

    1b286e6 · cli

  • patch

    The control daemon's socket is now mode 0600, as spec/CONTROL.md §8 says.

    Details

    It used to take the umask (0755 on a default macOS), so only its 0700 parent kept other users out.

    56cb3ef · cli

  • patch

    The hook and worker entries run only when invoked as the script, so both can be tested in process.

    Details

    What they do, print and exit with is unchanged; the hook still imports node builtins only and its timing is the same.

    ab5c975 · cli

  • patch

    Ignore Claude Code's lone SessionEnd hooks (for example, from claude mcp list) when there is no outbox, stored facts or existing transcript.

    Details

    These hooks no longer create an outbox or reach a sink or the local daemon. Real sessions still send their end, including when their transcript was deleted but an outbox or facts remain.

    2d8a8ae · cli

  • patch

    No hook event is lost to a worker's 15 s deadline, and none waits for an unrelated hook.

    Details

    A worker used to delete its job file before running it, so one ended mid-run (a sink that hung, a lock held out) took the event with it. A job is now claimed by renaming its file and the claim is released only once its events are in the outbox; a sweep gives a claim whose worker is gone back to the queue. A job the session's lock held out used to wait in the queue for the next hook's sweep, however long that took; a fresh worker now starts for it.

    1ca23ee · cli, core

  • patch

    A machine under a flood of hook events no longer floods its sinks.

    Details

    One worker drains the sinks at a time; the others leave their events in the outbox for it and it sends them in full batches of 50, so four busy sessions cost a request per batch, not per event. A sink that answered 429 is not tried again by any worker before its retry-after, and one that could not be reached waits 2 s · 10 s · 60 s · 5 min · 30 min, then every 30 min, as HTTP.md §2 has said all along (a machine with four busy sessions was refused 828 times in two minutes and had 5 192 jobs queued, 2026-10-14). The session's lock now covers only the append, not the round trips.

    434d10c · cli, core

  • patch

    control pair: a code nobody approved before it expired is renewed in place, with fresh digits, for up to an hour.

    Details

    A person approving three headless machines from a phone ran out of ten-minute codes (2026-10-08). The expired code is dead on the receiver, so nothing lives longer than it did; a keyed pairing keeps the machine row its first code made. The terminal now also says how long a code is good for.

    6ea4095 · cli

  • patch

    turn.progress: a message absorbed by a running turn, and text the person never typed, no longer open a turn of their own.

    Details

    Claude Code reports a message sent mid-turn (the person's, another session's, a task notification) as a prompt carrying the running turn's id; its reading went out numbered as the next turn, which the transcript never delivers, so a receiver kept showing it, with an empty reply, instead of the running turn. Such a prompt now leaves the reading on the turn the transcript is in, and a task notification that starts a turn is not shown as its ask. isInjected also recognises a wrapper tag with attributes (<cross-session-message from="…">…</cross-session-message>).

    dc14f84 · cli, core

  • patch

    sessionpipe connect --machine NAME --rename-machine (and install) now renames a paired control daemon too, so the hooks and the daemon stay one machine on a receiver.

    Details

    Before, setup refused to touch a paired daemon's name, and a machine whose pairing predates the fix in 0.9.0 that names pairings like the hooks had no way to merge its two cards. The daemon says a new name in a hello at once, as it does for folders and mode.

    1a77ec3 · cli

  • patch

    After a 429, the HTTP sink waits as retry-after says when the header is an HTTP date (RFC 9110), not only when it is a number of seconds.

    Details

    Before, a date fell back to the 2 s backoff ladder.

    9314032 · cli

  • patch

    A repeat install or connect on a config without a machine name keeps the name the hooks have been using (the hostname without .local), and renames a control daemon paired under another name to it, instead of adopting the daemon's.

    Details

    Before, a Mac paired before 0.9.0 (daemon <host>.local, events <host>) had its events switch to <host>.local on the first update, so a receiver showed its new sessions under a second machine. Receivers key a pairing by id, so the daemon's rename costs nothing.

    fd9c880 · cli

  • patch

    A session's short id (PROTOCOL.md §3): the first 8 hex digits of sha256("sessionpipe.session:" + harness + ":" + id), what a person reads in a log line, a notification or a search box.

    Details

    Derived, never sent. Not a prefix of the id: Codex ids are time-ordered, and four of ninety on one machine shared their first eight characters. shortId() in core; the worker's log and the daemon's lines that name a session use it.

    40fb6e5 · cli, core

  • patch

    Refuse connect/install downgrades of newer global packages or persistent hook copies, including newer prereleases, unless --allow-downgrade is explicit.

    Details

    Preserve the configured machine name when --machine differs; require --rename-machine for an unpaired rename and refuse names that would split sessions from a configured daemon.

    92bba52 · cli

  • patch

    Treat a hook stdin that is JSON but not an object (null, a number, a string, an array) as no payload in every adapter.

    Details

    Such a stdin made fromHook throw on the first field it read, and the worker job for that hook died with it; it is now handled exactly like an empty stdin, so the hook reports nothing and the session carries on.

    d5c7511 · core

  • patch

    Internal cleanup: fallbacks no input could reach are gone from the adapters, the privacy filter, the secrets ruleset, the outbox reader and the ULID.

    Details

    No behaviour changes; the public API is unchanged.

    4213f4e · core

  • patch

    Write an ssh://git@host/owner/repo.git origin as host/owner/repo in session.repo, the same as the git@host:owner/repo.git form of that remote.

    Details

    Before, the two spellings of one repository reached a receiver as two different repos.

    ace786d · core

  • patch

    Internal: the home directory is read when a path is asked for, not when the package loads.

    Details

    A process whose HOME changes after import (a test with a sandbox home) gets the new one; no user-facing change.

    bea1cae · core

  • patch

    Draining a large session file no longer re-reads the rest of it for every batch: a read takes 1 MB (more only for one event longer than that), not the remaining file up to 8 MB, and counts its offset per line rather than from the start.

    Details

    A 10 MB session drained in batches of 50 decoded 55 MB.

    cdb1dbf · core

  • patch

    Recover a session's seq counter when its file is empty or not a number (a crash between truncate and write leaves it empty).

    Details

    Before, the counter restarted at 0 and the receiver's deduplication silently dropped every later event of that session; now the next seq is read from the outbox file's last line, as it already was when the counter file was missing.

    67cc4fc · core

  • patch

    The stdout sink answers with a retry result when its writer fails (a closed pipe under sessionpipe tail | head), as the file and HTTP sinks do, instead of throwing out of send().

    cb36e47 · core

  • patch

    Stop the transcript cursor before a last line the harness is still writing.

    Details

    Before, a prompt whose line had no newline yet when the worker read the file was counted as read and never delivered, so the reply that followed arrived as a turn with an empty prompt. A complete last line without a trailing newline is still read.

    d4b085c · core

  • patch

    The CLI entry runs only when invoked as the script, so the package can be imported by tests.

    Details

    Output is unchanged.

    a68bf97 · receiver, conformance

0.8.0

  • minor

    Control's pairing request and every daemon hello name the Claude Code accounts signed in on the machine (accounts: {harness, id, email} per config dir, read again for each hello), so a receiver the person paired with can label session.account_id with the account's email instead of showing an id.

    Details

    Events are unchanged: account_id stays an id and no event carries an email. sessionpipe control pair and connect say which accounts they named.

    3c5d8fb · cli, core

  • patch

    Handle --help and -h before dispatching every CLI command, including nested commands, so asking for usage cannot install hooks, queue work, or contact a receiver.

    Details

    Implement status --json with doctor's names for shared facts plus pending session counts and queued jobs, preserving the human status output.

    93a8420 · cli

  • patch

    Build on TypeScript 7 (baseUrl dropped from every tsconfig; paths already resolved relative to the file).

    Details

    Dev-only: published output is unchanged.

    ea1d43a · cli, core, receiver, conformance

0.7.0

  • minor

    Control: messages reach Codex and Antigravity sessions too.

    Details

    The daemon starts a new session with codex exec --json or agy -p … --output-format stream-json, and resumes one with codex exec resume or agy --conversation, the way it drives claude -p: acked taken, the answer streamed as progress, then delivered with the reply. Both harnesses pick their own session ids, so the daemon records the one a start got (state/control/aliases.json) and keeps the start's id everywhere it reports the session: acks, later messages, and the hook events. A message sent right behind its start, even in the same poll, waits for it and resumes what it named. Safe mode runs Codex in its workspace-write sandbox and agy with no permission flag; auto mode passes --approve-for-me / --mode accept-edits; the bypass flags are never used. Hello and pairing advertise the harnesses installed on the machine. Core gains the runners (codexControl, antigravityControl), a shared headless spawner (runHeadless) and drivableHarnesses; the verifier accepts a start for any harness.

    5431264 · cli, core

  • patch

    Codex: a session's turns and its title skip the AGENTS.md instructions Codex writes into the session as a "user" message, so a turn shows what the person asked.

    653245d · cli, core

0.6.1

  • patch

    Antigravity: stop hooking PreToolUse.

    Details

    Antigravity reads a PreToolUse answer without a decision as a deny, so the {} sessionpipe printed blocked every tool call on every machine it was installed on, with no reason shown. Tool activity now comes from PostToolUse (a "still working" beat, since Antigravity names no tool there). sessionpipe install / update (and the daemon's auto-update) rewrite the old entry; a conversation that started before the rewrite still has it loaded, and gets an ask with a reason instead of a silent deny until it is restarted.

    9035be1 · cli, core

0.6.0

  • minor

    The control daemon updates itself.

    Details

    About a minute after it starts and then once a day (the last check is kept in state/control/update.json, so restarts and sleeping machines don't ask more often), it reads npm's latest; when that is newer it waits until nothing is in hand (no delivery, headless run, start, SDK turn, open permission prompt, session mid-turn or ack flush), pauses intake, installs with npm install --global --ignore-scripts into the prefix it runs from, re-arms the hooks of the harnesses already set up, and restarts onto the new code: exit 75 under launchd or systemd (every service file ever written restarts on a non-zero exit; new ones also carry SESSIONPIPE_SERVICE and RestartForceExitStatus=75), and otherwise the old process stays as the new daemon's parent. Off with sessionpipe update off (update_check: false, now actually read), SESSIONPIPE_NO_UPDATE_CHECK, or when the copy isn't a writable global npm install. status and doctor show whether it's on, the last check and the latest version.

    One daemon per machine: control run no longer takes the socket from a daemon that answers on it (it says so and exits 0), steps aside if another daemon replaces its socket file, and never deletes another daemon's socket. sessionpipe update installs through the same path (npm run by the daemon's own node, the copy on disk checked, only the configured harnesses re-armed) and asks a running daemon to restart onto it once idle.

    f62bad9 · cli

  • patch

    A started session's first message puts the person's words first and the "sent via" line after, so Claude Code names the session from what they asked instead of "Message via spacesheep.dev (sessionpipe control, s…".

    e474c81 · cli

0.5.0

  • minor

    sessionpipe connect <receiver>: one command and one approval set a machine up — hooks for every agent and every Claude Code account, a sink at tier 2 with the key the receiver hands over at pairing, signed messages, keys in the keychain, a daemon that outlives logout, 30 days backfilled.

    Details

    It asks nothing (safe mode unless --mode auto; folders from --folder, the folder it ran in, or where recent sessions ran — never the home folder) and a second run only fixes what's missing.

    • Hooks run a launcher, ~/.local/share/sessionpipe/sessionpipe-hook, with its own copy of the hook: a Node upgrade, nvm uninstall or a moved npm prefix no longer silently stops reporting. Re-installs move old entries to it in place.
    • Keys (sink tokens and secrets, machine tokens) live in the macOS Keychain or a desktop session's Secret Service when one is writable, else in the 0600 files as before; sessionpipe secrets [move keychain|secret-service|file]. A token a locked keychain won't hand over leaves events waiting (never unsigned); the daemon drains them from your session.
    • The Linux daemon turns on systemd linger, so closing ssh no longer stops it; on a Mac with nobody at the screen it runs as a background launchd agent instead of failing.
    • Before a headless run the daemon asks Claude Code whether that account is signed in (claude auth status) and fails the message with what to do — on a Mac, that a keychain login can't be read with nobody logged in at the screen — instead of Claude Code's "please run /login". connect and doctor list each account's state.
    • sink add keeps a sink's existing key when only the tier changes (it used to drop it); pairing fills in a sink that has no key and takes --tier; control pair run from the home folder no longer allows all of it; control.json keeps the machine's own caps across a write.

    6a5cda8 · cli, core

  • patch

    Run control pairing from the global install when invoked through npx, and use a stable Homebrew Node path for the daemon service.

    Details

    Existing service files are repaired by pairing again with the fixed CLI; install and update alone do not rewrite them.

    10d19eb · cli

0.4.2

  • patch

    A message for a session the daemon's own run is still working on (a start, a resume) waits for that run and resumes the session, instead of forking a copy that would work the same task in parallel.

    4b39a06 · cli

0.4.1

  • patch

    control pair says the approval happens on any device where you're signed in to the receiver, not necessarily the machine being paired (which may have no screen), and that several machines are paired one by one, each with its own digits.

    98374b0 · cli

0.4.0

  • minor

    key.add: a device the machine already trusts vouches for a new one (a phone with a different keychain), signed and verified like any command; the daemon adds the key to its store.

    Details

    Five new conformance vectors.

    b5cf64b · cli, core

  • minor

    sessionpipe control pair asks once, at the machine, whether sessions you message run in auto or safe mode (no more silent safe default), and sessionpipe control mode auto|safe changes it; the running daemon picks it up within seconds.

    Details

    Message text may be 20 000 characters (command 48 000): a 4 000 cap silently cut real task briefs.

    37546aa · cli, core

  • minor

    A receiver's well-known control.session_tools names MCP servers its sessions may use without asking (spacesheep names its own), so a safe-mode run answering a message can publish the page it was asked for.

    Details

    MCP server names only; built-in tools stay behind the person's approval rules.

    765b95a · cli, core

  • minor

    Answers stream: a non-final progress ack carries the answer so far (whole, with a rising seq).

    Details

    The daemon streams headless runs from Claude Code's stream-json output (token by token) and in-place deliveries from the session's transcript (message by message); the final ack still carries the whole answer.

    2cd4475 · cli, core

0.3.0

  • minor

    Open pairing: sessionpipe control pair spacesheep.dev needs no key where the receiver declares control.open_pairing.

    Details

    One link, one passkey approval; the daemon proves itself on the status poll with a poll_key only it holds, prints which account approved, and adds the sessions key the receiver hands over as its events sink (tier 0). A bare host means https, and a --token with a character no key has (a pasted ss_… placeholder) is refused in words.

    79fc0a4 · cli, core

0.2.0

  • minor

    The control daemon (M6): sessionpipe control pair | status | keys | off | run and sessionpipe wait.

    Details

    One daemon per computer verifies every command against keys enrolled at its terminal and delivers it in place when it can (a parked waiter, the session's next Stop, an Agent SDK session it started), else as a headless resume or fork; Stop and PermissionRequest hooks answer only for verified messages, and only where control is paired. Claude Code's PermissionRequest hook timeout is now 130 s (re-run sessionpipe install). A machine caps new sessions itself (2 at once, 10 an hour, limits in control.json) and runs at most 3 headless Claude Code processes at once; messages are handled concurrently, one session's in order; after a receiver outage the daemon is polling again within seconds and logs back after N s.

    54820be · cli, core

  • patch

    Control (M6): sessionpipe-core/control, the one verifier for signed control commands (WebCrypto only, safe in Workers and browsers), with 60 shared conformance vectors; the control schemas rewritten for signed, per-machine delivery.

    Details

    The CLI ignores sink add --control (control is per machine) and a retried batch reuses its webhook-id.

    87d9a65 · cli, core

0.1.1

  • patch

    The sink drain packs events from many sessions into one batch of up to 50 (a first install's backfill is a handful of POSTs, not one per session); cursors move only after the batch was acknowledged.

    8ae0d49 · cli

  • patch

    sink test sends a heartbeat and then its session.forgotten, so a receiver keeps no phantom session from the test.

    be95792 · cli

  • patch

    Fixes from the first outside install (Yaroslav Bulatov, issues #8–#14 and advisory GHSA-8f6f-c3c9-j5p6): a config file that is not plain JSON is never written back; installs keep a file's indent and final newline and uninstall deletes files sessionpipe created; jobs that lose the session-lock race are retried and swept, never lost; Claude Code StopFailure → turn.ended with reason: error; status/doctor say when Gemini CLI won't run hooks (tools.enableHooks) and word the Codex review honestly; doctor measures the wall clock a harness waits; with no sink above tier 0 only one tool event per harness is hooked; the npx bootstrap installs into ~/.local when npm's prefix is Homebrew's versioned Cellar; a bare pass: is no longer a secret name; the outbox holds redacted strings at rest in 0600 files under 0700 directories.

    b8b8599 · cli, core

  • patch

    READMEs carry the published (unscoped) package names.

    08b5d89 · core, receiver, conformance

  • patch

    Published under unscoped names (sessionpipe-core, sessionpipe-receiver, sessionpipe-conformance) until the @sessionpipe npm org exists.

    a360ee8 · core, receiver, conformance

0.1.0

  • minor

    Core: adapters for Claude Code, Codex, Gemini CLI and Antigravity; the secrets@1 and pii@1 rulesets and the tier filter; outbox, cursors, locks; file, stdout and HTTP sinks.

    Details

    CLI: install, uninstall, sink, status, doctor, tail, backfill, forget, replay, update; the hook entry imports only node: builtins. Conformance: adapter fixture runner.

    c75aaa0 · cli, core, conformance

  • minor

    Protocol v1 draft: the Zod source of truth (packages/core/schema/v1.ts) and the generated schemas/v1/*.json; inferred runtime types.

    e918475 · core

Protocol

Per-package changelogs are written by Changesets into each packages/*/CHANGELOG.md and released in lockstep. This file records protocol-level changes.

Unreleased

For receivers (0.13.3)

A hello's accounts entries MAY carry signed_in (CONTROL.md §2): whether the harness's command line is signed in to that account as the daemon sees it. A receiver that sees false SHOULD say "run claude auth login on " before a message is sent, where it used to learn that from the first message's failed ack. Absent means unknown, never signed out. Fixture: delivery/24 carries it.

For receivers (0.13.1)

A receiver can now ask a machine to thin the tool events it is sent (PROTOCOL.md §4, Thinned tool events): tools: "main" in its well-known file (HTTP.md §1), read at sink add and connect and again by a worker once a day. A receiver that says nothing is sent every tool event, as before — nothing changes on a machine's upgrade unless the receiver asked, or the machine's owner set the sink's tools. Additive on protocol 1, and every rule a receiver may already rely on still holds; where it asked:

  • A subagent's tool calls (data.agent_id set) arrive as at most one tool.ended per session per 60 s, and never as tool.started. The main thread's arrive as before. beat (opt-in) thins the main thread's the same way. Nothing but tool.* is thinned.
  • A thread's first tool.ended after its attention.needed is always sent, so "the next event that proves the prompt was resolved" (attention.cleared) still arrives. attention.needed now carries agent_id when a subagent asked.
  • seq has gaps where events were thinned; a gap is never a lost event (conformance/delivery/29-seq-gap.json). A receiver MUST NOT expect a tool.ended for every tool.started or the reverse, nor count the tool events it got as the session's tool calls.
  • A machine starts at most one request a second (HTTP.md §6); a receiver that limits requests per machine SHOULD allow at least that.
  • A receiver that asked for main and later needs every tool event says all (or drops the field); the machine reads the file again within a day. replay --tools all re-sends them once.
  • Codex: a subagent's tool calls and permission prompts carry agent_id (ADAPTERS.md).

For receivers (0.13.0)

What a 0.13.0 daemon or hook sends and accepts beyond 0.12.0. Everything is additive on protocol 1, and a receiver sees each capability in the machine's hello before it may use it.

  • Hello and pairing request say ask and handoff (CONTROL.md §2). ask is whether sessions on that machine may ask another agent (sessionpipe ask, §13: on unless the person turned it off); handoff: true says a start or a prompt may carry reply_to. A machine that says neither is 0.12.0 or older: send it no reply_to.

  • reply_to on a start or a prompt (CONTROL.md §3.1, §6 Handoff): <harness>:<id> of a session on the same machine, never the command's own session, inside the signed bytes. The command runs and is acked exactly as before, one final ack; after a delivered ack the machine delivers the reply into reply_to as a message framed Answer from <harness>:<short id> (sessionpipe handoff, asked via <receiver host>):, through its normal prompt routing, never wider than reply_to's own mode. On any other kind, malformed, or naming the command's own session: bad_fields (vectors 165–169). A reply_to the machine can't place when the command arrives (no such session there, or one outside its allowlist) makes the command no handoff: it runs and is acked as usual, nothing is relayed, and no event is emitted on that session. The relay is not a command: it is not acked and never relayed further.

  • Two event types on the asking session (PROTOCOL.md §4, tier 0): delegation.started (ask_id, via: ask · handoff, harness, to?; tier 1 adds text, ≤ 200 chars) and delegation.ended (ask_id, to?, ok, ms, error?, relayed?: delivered · unsupported · failed; tier 2 adds reply, ≤ 20 000 chars). They travel through the session's own outbox with its own seq. For a handoff, delegation.started comes when the machine takes the command (to is the command's session) and delegation.ended after its final ack; a command whose final ack isn't delivered relays nothing, and its delegation.ended has ok: false and no relayed. For an ask that starts a new Codex or ACP session, to is the id the daemon minted, the name the child's own events go by.

  • session.delegated_by (PROTOCOL.md §3, tier 0): <harness>:<id> of the session this one was started or resumed for, on every event of the child, from SESSIONPIPE_ASKED_BY the way launched_by comes from SESSIONPIPE_LAUNCHED_BY. Lineage across harnesses, never authority: join it with delegation.*.to, and never read it as a subagent.

  • Nothing a receiver sends can start an ask. It arrives on the daemon's local socket only (CONTROL.md §13); a receiver learns of it through the events.

  • Delivery mode inbox (CONTROL.md §6, §6.3), between turn and api. A message for a session a live Claude Code holds open is written into that session's own local inbox, so the turn runs in that process, in the mode the person left it in, in the window or thread they are already looking at — where today it is answered by a copy. A hello lists inbox in modes where the mode exists; an ack's mode is inbox where it was used, with the session's own reply. A receiver shows that answer as the session's own, never as a copy's; one that doesn't know the value sees an unknown mode on a final ack that carries the reply anyway.

  • failed with code: "held" (CONTROL.md §6.3, §10). The message was written into the session's own inbox and that session did not take it: no turn of its own began, or the turn that began was asked for by something else. Nothing was duplicated and no other mode was tried. A receiver MUST NOT redeliver it: the mode stays shut for that session while the message it wrote can still be released, so the same words again would put two copies of them in one place and a released hold would deliver both. Conformance delivery/25.

  • refused with code: "ambiguous_home" (CONTROL.md §5, §6.1). One session id is filed in more than one of the harness's accounts on that machine and none — or more than one — holds it open, so the machine cannot tell which account the message is for. detail names the accounts by their directory name. Nothing ran.

  • refused with code: "copy_limit" (CONTROL.md §5, §6 prompt). The machine has made the copies it makes in a day. A copy replays a whole conversation, so machines cap them; detail says so in words, and at a cap of zero says the machine makes none.

  • Ack copy_of (CONTROL.md §6 prompt, §10), a SessionRef: this answer came from a copy of the session the command named, not from it. A fork's ack carries the copy in session and the original here, and its detail may name the mode the copy's own turn ran in. A receiver shows whose answer the person is reading. Conformance delivery/27.

  • Event field injected (PROTOCOL.md; optional, tier 2) on turn.transcript and turn.progress: the turn's ask reached the session from somewhere other than its own keyboard — another session on that computer handed it over, which is how the inbox mode reaches a session. The words are the person's; the arrival was not. It says of a turn's ask what turn.started.injected says of a prompt, and like it is never a claim that no person is behind the ask. Absent means a turn asked for where the session runs.

  • A channel delivers in place too (CONTROL.md §8). sessionpipe channel is a Claude Code channel (its --channels MCP mechanism, the way the Telegram plugin reaches a session) that parks as the session's waiter and pushes each message into the live session, in its own thread and permission mode, as the person's own words; the daemon reads the answer back from the transcript as for any waiter. A channel is used only while the session is idle, and before inbox; its delivery is proven by the turn it causes, and a channel message the session began no turn for is acked failed with code: "held" like an inbox write (conformance delivery/28). A receiver sees the reply as the session's own, acked mode: "waiter"; a channel's ask is injected.

  • An inbox answer is a teammate's, to the session. Claude Code tells the model a message arriving through its inbox came from another Claude session, not its user, so the session won't change its permission settings, CLAUDE.md or configuration for one, or take one as the person's approval of a pending prompt (CONTROL.md §6.3). A receiver SHOULD say so beside an answer acked mode: "inbox".

Adopting 0.13.0 while machines still run 0.12.0:

  • Offer a handoff (reply_to) only for a machine whose latest hello says handoff: true; an older machine refuses the command bad_fields, since its field set is closed.
  • Read an absent ask or handoff as "not on this machine", an absent delegated_by as unknown, and keep a delegation.* event you don't know as you keep any other type.
  • A receiver that pre-checks commands with a copy of the verifier re-vendors 0.13.0's sessionpipe-core/control and its vectors (165–169 are new).

For receivers (0.12.0)

What a 0.12.0 daemon or hook sends beyond 0.11.0: one reading in PROTOCOL.md §4, and one optional field. Nothing a receiver has to adopt; the rest of 0.12.0 (Windows .cmd shims, a permission prompt in a turn the daemon runs, a socket test) changes nothing on the wire.

  • A session waiting on its own background work stays "working" until its next turn.ended (PROTOCOL.md §4, the turn.ended and subagent.ended rows; issue #153). The turn.ended row said a receiver MAY show such a session as working "until its next event"; the next event was always the work's own (a subagent.started seconds after the Stop), so a session that had handed its turn to a background workflow read idle while the workflow ran. It now says SHOULD, until the session's next turn.ended or session.ended, and that subagent.* and a subagent's tool events are not the session waking. Two things from the sender help: subagent.ended MAY carry background, the count once that subagent is out of it (schemas/v1/event-data.json; fixture harness/claude-code/subagent-stop), so a receiver sees the count fall to 0 before the next turn; and turn.ended.background no longer counts sessionpipe's own waiter (sessionpipe wait, CONTROL.md §8; fixture harness/claude-code/stop-waiter), which ends only when a message arrives and so held every reachable session at "working". Additive: a receiver that ignores the new field, or still drops the reading at the next event, keeps working as before.

For receivers (0.11.0)

What a 0.11.0 daemon or hook sends, what it accepts, and how a receiver adopts it while some machines still run 0.10.0 (a hello says which). Everything is additive on protocol 1: one new event type, one new hello object, one new command kind.

  • limits.observed (PROTOCOL.md §4; PRIVACY.md §1; schemas/v1/event-data.json): a new tier-1 event, OPTIONAL for senders, carrying the account's usage limits as the harness last reported them. windows[] (1–8), each id the harness's own name for the window, passed through as it spells it (five_hour, seven_day, primary …), used_percent (0–100; above 100 once a spend limit is passed), resets_at? and window_minutes? (only where the harness or its documentation says the length); plan?; and account_id?, the account the numbers belong to, spelled as session.account_id is. Each event is the whole set: a window it leaves out is not reported (Claude Code drops one once it resets), and a window whose resets_at has passed has reset, so a receiver SHOULD NOT show its last used_percent as current. The numbers belong to the account, not the session: a receiver SHOULD keep the newest per (harness, data.account_id, else session.account_id), or per machine when there is neither. A sender MUST set account_id when the numbers belong to another account than the session's own (a pooled session served by a second account) and MAY set it otherwise; absent, they are the session's account's. Three sources (ADAPTERS.md, the Limits rows): Codex's rollout after a turn (primary, secondary; only the account's own codex bucket), Claude Code's status line where the person opted in with install --status-line (five_hour, seven_day, spend_limit; window_minutes 300 and 10 080 for the first two), and the machine's account pool reading the vendor's rate-limit headers for a request it served. pii@1 drops data.account_id as it drops session.account_id (PRIVACY.md §2). There is no recorded fixture for it yet; the schema is the contract.
  • A hello MAY carry pool (CONTROL.md §2; schemas/v1/control-hello.json; conformance delivery/24): the machine's account pool, several accounts of one harness behind the daemon so a session keeps going when one account's limit is reached. on, harness, and accounts (at most 20) in the machine's order, each with id (the same string session.account_id and limits.observed.account_id carry for it, so a receiver joins the three without a map), email or label where known (they travel only here), enabled, a status — ready, limited (with until where the machine knows when), error (sign in again), disabled — current: true on the one serving new sessions now, and optionally windows, the account's freshest numbers in the shape of limits.observed.windows (from the idle usage poll, since an idle account's numbers ride on no session), and limited_by when the status is limited or error: the window's id (seven_day), or rejected or error for the causes that are no window. A daemon that knows the pool sends pool in every hello, on: false and accounts: [] when nothing is set up, so a receiver tells "no pool" from "an old daemon"; it sends a hello at once when the pool changes, as it does for its folders. A receiver reads windows as it reads limits.observed and MUST NOT send it on as an event; it is a hello's reading, for the receiver's own page. A hello without pool is a daemon that predates the pool: a receiver MUST NOT offer pool controls for it.
  • The pool command (CONTROL.md §3.1, §5, §6 pool; schemas/v1/control-command.json; vectors 137–164): a new signed kind on the pseudo-session sessionpipe:pool, the only kind besides key.add that names no coding session. A receiver can only ask; the machine decides, acts and acks, and no command changes which account serves a request (the first in the person's order that has room; a session stays on its account while that account has room). pool_action is required and closed — on, off, enable, disable, remove, order, refresh, login.begin, login.complete; any other value is bad_fields — and each action takes exactly its own fields: pool_account (an id as the hello lists it, or a pending sign-in's, [A-Za-z0-9._:-]{1,128}) is required by enable, disable, remove and login.complete and optional on refresh; pool_order (1–20 ids, no repeats, every account of the pool once) belongs to order alone; pool_code (one line, ≤ 512 characters) to login.complete alone. A pool_* field on any other kind, or on a pool whose action doesn't take it, is bad_fields. Fresh confirm: on, off, remove, order, disable and login.complete are verified by a fresh confirm (§3.2) or not at all; one that arrives on a day grant alone is refused confirm_required by the machine (vector 164 is the confirmed form). enable, refresh and login.begin take a grant, as a prompt does. A receiver MUST ask for the confirm where the machine will want it, and MUST show which machine it is for. Acks: delivered with no mode and detail = the machine's one-line summary of the pool, or for login.begin the pending login id, one space, and the URL to open; the daemon also sends a hello at once, which is the state a receiver shows. refused pool_account (an account or pending login the machine does not hold), pool_order (an order that does not name every account exactly once), pool_off (refresh, login.begin or login.complete while the pool is off). failed with code login (the vendor would not take the code) or pool (the vendor call a refresh or login.complete needs did not go through); detail says what, never a token. unsupported when the daemon knows the kind but this machine cannot pool. A daemon that predates the kind refuses it unknown_kind at the verifier. No command adds a credential from the receiver's side: login.complete carries a one-time code the vendor issued to the person, and the exchange happens on the machine.
  • The privacy contract has two opt-in exceptions now (PRIVACY.md §3; ADAPTERS.md), both off until the person turns them on at the machine, and neither puts a token in an event, an ack, a log or a receiver. With sessionpipe titles on (remote_titles), the Claude Code adapter asks claude.ai for a session's name, the one fact claude.ai never writes to the machine; with pool on or pool add, the daemon holds the credentials of the accounts the person added and sends each one only to that vendor's own endpoints, for the person's own requests. A receiver learns the pool's accounts (ids, labels, emails) only in the hello, and their usage only as limits.observed.
  • Session names from claude.ai (PROTOCOL.md, the session.backfill row; ADAPTERS.md Claude Code Facts; PRIVACY.md §3): with remote_titles on, a Claude Code session with a claude.ai link arrives as session.title with title_source: "harness", a custom title still winning, and a session already sent under another name is sent again as session.backfill with the session's next seq; a receiver applies its session block as it does any event's. No new field; the recorded answers are in conformance/remote/claude-code/ (session-named.json, invalid-id.json), fixtures for the adapter, not the receiver.
  • The rest of 0.11.0 (sink tier rehooks, tail's Windows path, Codex config.toml line ends, Gemini CLI's version on Windows, hook files with a byte-order mark) changes nothing a receiver reads, except that a BOM-marked settings.json on a home on another API now yields its api-<16 hex> session.account_id where it reported none.

Adopting 0.11.0 while machines still run 0.10.0:

  • Keep limits.observed as the account's, not the session's: the newest per (harness, data.account_id, else session.account_id), per machine when there is neither, and read a window whose resets_at has passed as reset. A 0.10.0 machine never sends it.
  • Show a pool only from the latest hello that carries pool, never from a guess, and show nothing for a hello without it. Keep email and label from the hello; no event carries them.
  • Never send pool to a machine whose last hello had no pool: an older daemon refuses it unknown_kind, and nothing happens. Gate per machine on its latest hello.
  • Ask for a fresh confirm before on, off, remove, order, disable and login.complete, and name the machine it is for; a day grant alone is refused confirm_required.
  • Take confirm_required, pool_account, pool_order, pool_off, and failed login and pool, as codes you may not have seen, and show detail; an unknown code is still a refusal or a failure, never success.
  • A receiver that pre-checks commands with a copy of the verifier re-vendors 0.11.0's sessionpipe-core/control and its vectors (137–164 are new).

0.10.0 — 2026-10-26

What a 0.10.0 daemon or hook sends, what it accepts, and how a receiver adopts it while some machines still run 0.9.0 (a hello says which). Everything is additive on protocol 1 except the first item, which tightens what a machine accepts, on purpose.

  • The command field set is closed (CONTROL.md §3.1). Each kind carries the fields its table row names, plus v, t, machine, kind, session, nonce, iat and note. Any other field inside the signed cmd is refused bad_fields, by the verifier and on the machine; 0.9.0 ignored it. A receiver keeps its own data (a match, an origin) beside cmd, never inside it, so a field a machine doesn't know fails closed.
  • mode: "inherit" (CONTROL.md §2) in a pairing request and a hello. The machine passes no permission flag, so each harness runs as the person's own sessions there do: Claude Code in its settings' permissions.defaultMode, bypassPermissions included, and Codex as its config.toml says. A start that names no level runs that way. A receiver shows the machine as inherit, on its pairing page and wherever it shows a mode, and never as safe or auto; an unknown mode is unknown, not safe. The gate: a 0.10.0 machine pairs in inherit, or switches to it, only with receivers whose well-known file says control.inherit: true (HTTP.md §1). Elsewhere the person is told "<receiver> doesn't know inherit mode yet; it would show this machine as safe while the sessions you message run with no permission flag", and the machine keeps its mode. A receiver sets the flag once it shows the mode truthfully and accepts mode: "inherit" in /pair and /hello (conformance delivery/23).
  • The refusal code mode is now permission (CONTROL.md §5), beside a new refused model, and failed model when the harness rejects a start's model. A 0.9.0 machine still says mode, so a receiver maps both; detail always has the words.
  • New hello fields (CONTROL.md §2). start: per harness, the permissions a start may name there (among read, safe, edits, auto, already capped by the mode; on an inherit machine at most safe), the default permission (which may be inherit), and models / any_model. prompt_files: true: the machine takes files on a prompt. files: true is as in 0.9.0. There is no home: no hello names the machine's home folder, which would tell every receiver the OS user name. Instead a start's cwd may be ~ or ~/…, the way events write a session's folder, and the machine reads it under its own home. harnesses lists at most 20 (Claude Code, Codex and Antigravity first), and modes include api whenever an ACP agent is listed.
  • Acks carry applied (CONTROL.md §6, §10): a start's taken and delivered say what it runs with, {"permission": …, "model"?: …}, where permission may be inherit. A receiver shows applied, never what was asked; a delivered start with no applied reached a machine that ignored the options.
  • New event fields (PROTOCOL.md; all optional, tier 0): session.attended, turn.ended.background, turn.started.submitted_by and injected, session.launched_by, and session.account_id api-<16 hex> for a Claude Code home on another API, named in the pair and hello accounts[] by name (the host) with no email. A receiver that doesn't know them keeps them as extra keys.
  • ACP harnesses (CONTROL.md §6, ADAPTERS.md "Agents driven through ACP"): a hello's harnesses may name any agent from the ACP registry that is installed (gemini-cli, opencode, goose, cursor, copilot-cli, droid, qwen-code and more); their turns are acked in delivery mode api, and an ack's session may be <acp-harness>:<id>. They take no start options. A receiver shows a harness it has no label for by its raw name.
  • Device keys (CONTROL.md §2, §3.2; HTTP.md §1). A desktop or phone app whose web view can't run WebAuthn signs with a P-256 key held in hardware and verified each use, {id, alg: -7, spki, type: "device", store: "hardware", uv: "each-use"}, and its signature is {cred, sig} (plus str in a grant) where a passkey's {cred, ad, cd, sig} would stand. A 0.10.0 machine says key_types: ["passkey","device"] in its pairing request and every hello. To accept device keys, a receiver stores and carries such a key whole, all six fields, in a pairing's key and in a key.add's key (never trimmed to id, alg, spki); passes a {cred, sig} grant, confirm or pairing proof through as it came; and says control.signing.key_types: ["passkey","device"] in its well-known file. A device key reaches a machine only as a passkey does: approved at the terminal's pairing code, or by a key.add that a key the machine already trusts signed. A receiver that doesn't know device keys keeps working: with no key_types it is passkeys only, an app offers it none, and nothing about passkeys changed.

Adopting 0.10.0 while machines still run 0.9.0:

  • Send permission or model on a start only when the last hello's start[harness] lists that level in permissions, or has any_model or models. A 0.9.0 machine ignores both in silence and runs at its own mode.
  • Send files on a prompt only to a machine whose hello says prompt_files: true; a 0.9.0 machine refuses the prompt bad_fields.
  • Send a ~ folder as a start's cwd only to a machine whose hello carries start; give any other an absolute one (from folders).
  • Never sign inherit as a command's permission: it only ever comes from the machine.
  • Read an absent attended, background, submitted_by, injected or launched_by as unknown: never false, 0 or "a person". Keep a submitted_by you don't know.
  • Offer a device key for a machine only when its latest hello lists device in key_types. A 0.9.0 machine would store it as a passkey and refuse every signature it makes (vector 122).
  • Gate per machine on its latest hello, cloud boxes included: one asleep on an old snapshot may still poll on 0.9.0.
  • A receiver that pre-checks commands with a copy of the verifier re-vendors 0.10.0's sessionpipe-core/control and its vectors (69 is now files-on-permission-answer; 90–136 are new; verifyAssertion now takes the signature's purpose).

Protocol changes

  • Control: a session asks another agent, and a handoff brings the answer back (CONTROL.md §2, §3.1, §6 Handoff, new §13; PROTOCOL.md §3, §4, §5; PRIVACY.md §1; still Draft). sessionpipe ask <harness|session> "<text>" reaches the machine's daemon on its local socket, never through a receiver; the daemon checks, in order, that asking is on, that the agent is one it can run headlessly or a session it can reach, the folder allowlist, the depth (two deep at most), loops and the start caps, each with its code (ask_off · harness · folder · depth · loop · start_limit · bad_fields), runs the child at safe (or read), never wider than the machine's mode, frames the task so the model answers in place, and sets SESSIONPIPE_ASKED_BY and SESSIONPIPE_ASK_CHAIN in the child's environment. A prompt or a start MAY carry reply_to, a session on the same machine that gets the answer as a message once the command's turn ends, through the normal routing and never wider; the verifier takes it on those two kinds only, never the command's own session. Two event types on the asking session, delegation.started and delegation.ended (tier 0; text at 1, reply at 2), and one session field, delegated_by, read by the hook like launched_by. Hello and pairing request gain ask and handoff. Schemas: control-command gains reply_to, control-hello and control-pair gain ask and handoff, event gains session.delegated_by, event-data the two types. Five new control vectors (165–169).
  • Control: device keys (CONTROL.md §2, §3.2, §5, §11; HTTP.md §1; still Draft). An app whose web view can't run WebAuthn (a desktop or phone app) signs with a P-256 key held in hardware and verified each use: {id, alg: -7, spki, type: "device", store: "hardware", uv: "each-use"}, its id the SHA-256 of its SPKI. Its signature is {cred, sig} (plus str in a grant), raw r‖s over sessionpipe.device.v1⏎<purpose>⏎<rp id>⏎<challenge>, purposes grant, confirm and pair; the challenge is the one a passkey would sign, so the day grant, confirms, pairing and key.add work unchanged. A machine verifies by its own record of the key, never the message. No new refusal codes. Machines advertise key_types in the pairing request and every hello, receivers in control.signing.key_types; a client offers a device key only where both list device. A key.add key is now checked like an enrollment (an SPKI the machine can import, and every device rule), bad_fields otherwise, so an unimportable key is never stored. Key fields are closed: a passkey carries id, alg, spki only. Schemas: control-key is a closed oneOf (passkey | device), control-command and control-paired use it, grant, confirm and pairing proof take either signature shape, control-pair, control-hello and the well-known control.signing gain key_types. 26 new control vectors (111–136).
  • turn.started can say who started the turn (PROTOCOL.md §4; PRIVACY.md §1; ADAPTERS.md). Two new OPTIONAL tier-0 fields: submitted_by, the harness's own word for who submitted the prompt, forwarded verbatim and never mapped onto a vocabulary of this spec (a bare word, [A-Za-z0-9][A-Za-z0-9_.-]{0,39}, because tier 0 is not scrubbed; a different vocabulary from session.started.source, which a receiver MUST NOT merge with it); and injected, true when the prompt text is a block the harness wrote rather than words a person typed. injected: true says only that the prompt is not the person's words — never that no person caused the turn, since a harness may deliver a person's message inside such a block. A receiver MUST read an absent field as unknown, MUST keep a submitted_by it does not recognise, and MUST NOT let a second turn.started for a turn_id it already accepted reclassify that turn. Claude Code fills injected from a closed list of its own wrappers and would fill submitted_by from UserPromptSubmit.source, which no build in the field sends yet; fixture conformance/harness/claude-code/user-prompt-submit-injected.json. Additive: a receiver that does not know the fields keeps them as extra keys on data.
  • session.attended: whether a person attends the session (PROTOCOL.md §3, §5; PRIVACY.md §1; ADAPTERS.md). A new OPTIONAL tier-0 boolean in the session block, in the harness's own word: true when a person is at the session or drives it from one of the harness's own clients, false for a run nobody watches. A receiver MUST read an absent field as unknown, never as false. Claude Code fills it from CLAUDE_CODE_SESSION_ATTENDED, counting a control turn and a session with a link (its Remote Control bridge or the desktop app) as attended, because every headless run says 0, a Remote Control session included. The hook carries CLAUDE_CODE_SESSION_ATTENDED and SESSIONPIPE_CONTROL_JOB into the job, and the worker reads those and CLAUDE_CODE_HOST_SESSION_ID from the job alone, never from its own environment. Additive: a receiver that does not know the field ignores it.
  • Control: a start may carry files (CONTROL.md §3.1, §6; HTTP.md §1, §3; still Draft). Up to 10 files, 25 MiB each and 50 MiB together, each named in the signed command by its SHA-256 (base64url), so a receiver cannot swap one. The receiver holds the bytes and serves them at GET {control}/files/{sha256} with the machine's token; the machine stops reading past the signed size, checks length and hash, writes them under <cwd>/.sessionpipe/files/<session id>/ (self-ignoring .gitignore, no symlinks followed, mode 0600) and starts the session — of any harness it can start — with the text plus the list of relative paths. Any file problem acks failed with code file and starts nothing. files on another kind, null or empty is bad_fields. Schemas: control-command gains files; control-pair and control-hello gain files: true (the machine takes attachments — a receiver should not send files to one that doesn't say so); the well-known control object may say files: true. 24 new control vectors (66–89).
  • turn.ended gains background (tier 0, optional): how much background work the harness reports still in flight as the turn ends, each piece of which wakes the session when it finishes. A receiver may keep such a session under working instead of reading "done" (PROTOCOL.md). Claude Code fills it from the Stop hook's background_tasks, in every Stop recorded since 2.1.258. Conformance: harness/claude-code/stop-background.json (a background shell command running, 2.1.305); stop.json now pins background: 1 (an Explore subagent still running) and stop-2.json background: 0. Additive: a receiver that doesn't know the field keeps it and carries on.
  • Protocol: session.launched_by (PROTOCOL.md §3): the session that started this one, as <harness>:<id>, read from SESSIONPIPE_LAUNCHED_BY in the hook's environment. A script that runs claude -p for a session sets it, and a receiver can group the run under its launcher without reading it as a subagent, which parent_id would say (#65). A subagent keeps its parent_id and gets no launched_by; a launcher that starts a further session resets the variable, so it names the nearest launcher only.
  • Control: start is no longer Claude Code's alone (CONTROL.md §6; asked for by the maintainer on 2026-10-04, so the spec change ships with its code). A start names a session of any harness the machine can drive headlessly — the reference daemon drives Claude Code, Codex (codex exec) and Antigravity (agy -p) and lists them in its hello — and resume covers Codex (codex exec resume) and Antigravity (agy --conversation). New rule: a machine whose harness names its own sessions keeps the command's id as the session's name in everything it reports (acks, later prompts, hook events); a prompt for a session whose start is still running waits for it. Conformance: control-vectors/53-start-other-harness.json (a Codex start) now verifies, where it was refused bad_fields. ADAPTERS.md: Codex and Antigravity gain a Control row, and Antigravity's row no longer lists the PreToolUse hook 0.6.1 removed.
  • Control rewritten before anyone implemented it (CONTROL.md, HTTP.md §3; still Draft, milestone M6). The old draft let whoever held a bearer token prompt an agent and answer "allow"; now:
    • Every command is signed on the person's device — a passkey vouches for a day key, the day key signs each command (or one passkey confirm per command) — and verified on the machine against keys enrolled at its own terminal (sessionpipe control pair). A receiver carries and may pre-check; it can never forge. One verifier, sessionpipe-core/control (WebCrypto only), and conformance/control-vectors/ (60 vectors) that every verifier must agree with.
    • One poll per machine, not per session: GET {control}?machine=… with the machine's own token, minted at pairing; the command names its session (<harness>:<id>). New: POST {control}/pair, GET {control}/pair?code=, POST {control}/hello, POST {control}/off.
    • Six delivery modes, first fit wins (sdk, waiter, turn, api, resume, fork), acked with the mode used; new kind start; new outcome refused with a code; acks may carry reply.
    • Schemas: control.json is now the carried envelope (cmd, csig, grant, confirm); new control-command, control-grant, control-key, control-pair, control-pair-started, control-paired, control-hello; control-ack gains mode, session, code, reply; the well-known control object gains signing (rp_id, algs).
    • Limits fixed where the two implementations it came from disagreed: nonce 22–64 base64url, text ≤ 4 000 characters, command ≤ 12 000, session id per adapter.
  • Fixed contradictions: a sink never carries control (the CLI's sink add --control is now ignored with a pointer to control pair); sessionpipe.org no longer says spacesheep runs "with control"; CONTROL.md's six delivery scenarios all exist (09–14); Gemini CLI's AfterAgent is in the Stop-block list; attention ids are specified as the adapters mint them; a retried batch reuses its webhook-id (derived from the events' ids); SECURITY.md and CONTRIBUTING.md name the one exception to "the hook never alters the harness" — a verified control message — and exempt the PermissionRequest wait from the 150 ms hook budget.
  • Protocol v1 (Draft): envelope, event vocabulary, tiers 0–3, HTTP binding, discovery, control channel. See spec/.