Repository navigation
Releases: spacesheep-dev/sessionpipe
Release list
v0.11.0
0.11.0
Minor Changes
- 91a1e57: Session names from claude.ai, off until the person turns them on (
sessionpipe titles on, config.jsonremote_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. 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 inconformance/remote/claude-code/), a minute apart until it has one, then every half hour, within 3 s, and sends it assession.titlewithtitle_source: "harness"; acustomtitle still wins.titles onandtitles refresh [--days N]also ask about the last 30 days' sessions and send each one already sent under another name again assession.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 toapi.anthropic.comalone, refuses redirects, never refreshes an expiring token and never writes it anywhere. Adapters gain an optionalremoteFactsandBackfillRow.transcript;Configgainsremote_titles. spec/ADAPTERS.md, spec/PRIVACY.md §3 and thesession.backfillrow of spec/PROTOCOL.md say so. - 3f074d8: A new tier-1 event,
limits.observed: the account's usage limits as the harness last reported them, onewindows[]entry each (id,used_percent,resets_at?,window_minutes?) and an optionalplan. The numbers belong to the account, so a receiver keeps the newest per (harness,session.account_id). Codex's adapter reads them from the rollout (token_count→rate_limits, the account'scodexbucket only; newlimits()on the adapter); Claude Code's maps them from its status line'srate_limits(a newStatusLineinput) and, withstatusLine: true, installs that status line insettings.json, never over one the person set. Schemas:event-datagainslimits.observed. - 5bfc8cb: The account pool's contract (CONTROL.md §2
pool, §6pool; PRIVACY.md §3): a hello MAY carrypool(on,harness,accountsin the machine's order withstatus,current,until,limited_byand the idle poll'swindows; present in every hello of a daemon that knows the pool,on: falseand no accounts when nothing is set up), and a new signed kindpoolon the pseudo-sessionsessionpipe:pool(pool_action: on, off, enable, disable, remove, order, refresh, login.begin, login.complete;pool_account,pool_orderof at most 20,pool_codeof one line ≤ 512 characters), verified likekey.add: each action takes exactly its own fields (poolProblem,POOL_SESSION,POOL_ACTIONS).limits.observedgainsaccount_id, the account the numbers belong to when a pooled session was served by another than its own; tier 1 carries it andpii@1drops it as it dropssession.account_id. Schemas:control-command,control-hello,event-data; control vectors 137–164.
Patch Changes
- 83929e3: Codex's config.toml keeps its CRLF line ends through
installanduninstall: the notify line goes in with the file's own line end, anduninstallleaves the file byte-identical, as it already did for an LF file. - b5285de:
install,statusanddoctorread Gemini CLI's version on Windows, where the CLI is npm'sgemini.cmdshim; before, every Windows install said "could not rungemini --version" and the 0.46 check never happened. - 1891dab: 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
installhooks it instead of reporting "not plain JSON; not touched", anduninstallleaves it byte-identical. The harnesses read such a file themselves.
v0.10.0
0.10.0
Minor Changes
-
5ea2f11: Control can start, prompt and cancel any agent that speaks the Agent Client Protocol (CONTROL.md §6,
apimode; ADAPTERS.md "Agents driven through ACP"). 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 withsession/cancel, reopens an idle session withsession/loadwhere 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 whereSESSIONPIPE_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 theirmodesincludeapiwhenever an ACP agent is listed. -
7b65074: A Claude Code home that runs on another API now has an account. Such a home has no claude.ai login, and its
settings.jsonsetsenv.ANTHROPIC_BASE_URL(Kimi K3 on a Modal endpoint, GLM on Z.ai). Its events carryaccount_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, inaccounts[].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. -
27c4194: Control: device keys (CONTROL.md §2, §3.2).
sessionpipe-core/controlverifies a device key's signature ({cred, sig}, raw r‖s oversessionpipe.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;keyUsablecloses key fields and checks a device key's rules (type: "device",alg -7,store: "hardware",uv: "each-use", id = SHA-256 of the SPKI);verifyCommandrefuses akey.addwhose key could never be used (bad_fields).verifyAssertiontakes the purpose. New exportsDEVICE_CONTEXT,deviceMessage,deviceKeyId,KEY_TYPES,DEVICE_SIG_FIELDS,DeviceSignature,DeviceGrant. The daemon advertiseskey_types: ["passkey","device"]when pairing and in every hello, stores a device key whole (from a pairing or akey.add), andcontrol keysshows each key's kind. 26 new shared vectors (111–136). -
abcba7f: Control: a third machine mode,
inherit, so sessions you message run the way your own sessions on that machine run.sessionpipe control mode inherit(or--mode inheritoncontrol pairandconnect, 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 itsconfig.toml'sapproval_policyandsandbox_modesay, 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:autoran each messaged session in Claude Code's default mode, which refused every command that needed approval, andsaferefuses them by design. Schemas:control-pairandcontrol-hellomodegainsinherit. -
531b391: Control: a machine runs in
inheritmode only with receivers that know it. A receiver that predatesinheritshows 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 withcontrol.inherit: truein its well-known file (HTTP.md §1, CONTROL.md §2), andsessionpipe control pair --mode inherit(andconnect --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 inheritchecks 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-knowncontrol.inherit(optional boolean). Conformance:delivery/23-control-inherit. -
fc0fedd: 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.sessionpipe-core/service: the launchd agent and the systemd user unit for any process (serviceFiles,installServiceanduninstallService, each with a dry run that answers the files and commands and touches nothing, andensureLinger).sessionpipe-core/version:compareVersions,isNewer,parseVersion,UPDATE_EXITandsupervise, 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'sExecStartquotes every argument. -
2865e0d: 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).
- Closed command field set. Each command kind carries only its own fields; any other field inside the signed
cmdis refusedbad_fields, by the verifier and on the machine (0.9.0 ignored it). Keep your own data besidecmd, 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 asinherit, 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 sayscontrol.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/pairand/hello(conformancedelivery/23).- Refusal code
modeis nowpermission, plusrefusedmodelandfailedmodel. Map both codes; a 0.9.0 machine still saysmode. - Hello:
start(per harness:permissionsamongread,safe,edits,auto, capped by the mode; the defaultpermission, which may beinherit;models,any_model),prompt_files: true, and nohome: a start'scwdmay be~or~/…instead.harnesseslists at most 20, andmodesincludeapiwhen an ACP agent is listed. - Acks: a start's
takenanddeliveredcarryapplied(permissionmay beinherit). Showapplied, never what was asked. - Events:
session.attended,turn.ended.background,turn.started.submitted_byandinjected,session.launched_by, andaccount_idapi-<16 hex>for a Claude Code home on another API, named inaccounts[]bynamewith noemail. All optional, tier 0. - ACP agents (gemini-cli, opencode, goose, cursor, copilot-cli, droid and more) appear in
harnessesand in acksessionrefs, acked in modeapi; 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}(plusstrin a grant) where an assertion would stand; a 0.10.0 machine sayskey_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 akey.add'skey, pass a{cred, sig}grant, confirm or pairing proof through untouched, and saycontrol.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 akey.adda trusted key signed. A receiver that doesn't know them keeps working, passkeys only. - While machines run 0.9.0: send
permissionormodelonly where the last hello'sstartadvertises them (0.9.0 ignores them silently);fileson apromptonly withprompt_files: true(0.9.0 refuses them); a~folder only to a machine whose hello hasstart; neverinheritas a command's level; a device key only for a machine whose last hello listsdeviceinkey_types(0.9.0 would store it as a passkey and refuse its signatures); read an absentattended,background,submitted_by,injectedorlaunched_byas unknown, never as false, 0 or a person.
- Closed command field set. Each command kind carries only its own fields; any other field inside the signed
-
a6f8bb7: A control
promptcan carryfiles, as astartcan (CONTROL.md §3.1, §6prompt), to a machine whose hello saysprompt_files: true. 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 ackedfailedwith codefileand the message is not delivered. A receiver no longer needs to put a link to an image into the message's text. -
d51fd31:
session.attendedsays 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, neverfalse). Claude Code fills it fromCLAUDE_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 says0, a Remote Control session included, and aclaude -pstarted inside one inheritsCLAUDE_CODE_ENVIRONMENT_KIND=bridge, so the link is what tells them apart. The hook carriesCLAUDE_CODE_SESSION_ATTENDEDand `SESSIONPIPE_CONTROL_JO...
v0.9.0
0.9.0
Minor Changes
-
55320c7: Control:
sessionpipe-core/controlacceptsfileson astart(1–10 files, each{name, type, size, sha256}, 25 MiB each, 50 MiB together, names safe to write and unique ignoring case) and refuses them on any other kind,nullor empty asbad_fields; new exportsfilesProblem,ControlFileand the limits. Schemas:control-commandgainsfiles,control-pair/control-hello/ the well-knowncontrolobject gainfiles: true. 24 new shared vectors (66–89). -
d525320: A
devinadapter: Devin CLI (Cognition) sessions are reported like any other harness.sessionpipe install(or--devin) writes the eight event names Devin accepts into thehookskey of its user config file; the payload maps to the protocol's events, the folder comes fromDEVIN_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 newreaders/sqlite.tsthat works on Node 20 too (in-processnode:sqlite, else a short child process, else thesqlite3CLI, else no turns). Eight fixtures recorded from real runs on 3000.11.3. Also: theclaude-codeadapter now drops a payload with notranscript_pathand 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. -
3fd0041: Review fixes to the Devin adapter and two gaps it exposed.
devin: a turn is now timed by the message's ownmetadata.created_at, not bymessage_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()reportsinactive, notcurrent, when the config file holds any event name Devin does not know — Devin then loads no hooks from that file at all, ours included — andinstall()says so too. Atool_responsethat is not an object no longer loses its output.facts()takes the hook's environment (new optional argument on theAdapterinterface), so a job swept by a worker another harness's hook spawned still reads the right config and session store.sessionpipe uninstalltakes the same--<harness>selectioninstallhas, removes the shared hook launcher only on a full uninstall, and refuses an all-harnesses uninstall whenSESSIONPIPE_CONFIG/SESSIONPIPE_STATEpoint away from this machine while other harnesses would still be read from$HOME(pass--allto mean it). The hook forwardsXDG_CONFIG_HOME,XDG_DATA_HOMEandAPPDATAin the job file, beside the per-harness variables already there. The control wait — up to 125 s on aPermissionRequest— now applies the same "this is not a Claude Code payload" test the worker does, so our entry in~/.claude/settings.jsonrun 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
sqlite3binary wherenode:sqliteis absent, so they run on the Node 20 floor — where thecliengine 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;inlineParamsand the adapter's own chain query are covered. -
40fb6e5: 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. 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. -
0940788:
turn.progress(tier 2): a turn still running goes out as it runs, not only when it ends. 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 itsturn.transcriptwill carry. Over 20 000 characters the reply keeps its end. A 21-minute turn used to show nothing on its receiver for 21 minutes.
Patch Changes
-
d5c7511: Treat a hook stdin that is JSON but not an object (
null, a number, a string, an array) as no payload in every adapter. Such a stdin madefromHookthrow 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. -
8184e2a: An Antigravity session in a git checkout now opens its own conversation from its link, not Antigravity's home. Antigravity keeps a git checkout's folder under
gitFolderin~/.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). -
91265cb: A busy session's events no longer wait 6 s and go back to the queue behind its own lock. 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).
-
366043f: Claude Code: a message the person sends while a turn is running now reaches
turn.transcriptandturn.progress. Claude Code records it as aqueued_commandattachment rather than auserrecord, so the transcript reader dropped it from tier 2. -
94c1b2f: Skip the parent's replayed history in forked Codex rollouts when reading session
facts and transcripts. 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. -
81764cc: Codex: only the person's own threads become sessions.
- An ephemeral thread is no longer reported: its hooks carry
transcript_path: nulland 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 doescodex 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_idwith 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
expectis a payload the adapter must report nothing for.
- An ephemeral thread is no longer reported: its hooks carry
-
4213f4e: Internal cleanup: fallbacks no input could reach are gone from the adapters, the privacy filter, the secrets ruleset, the outbox reader and the ULID. No behaviour changes; the public API is unchanged.
-
ace786d: Write an
ssh://git@host/owner/repo.gitorigin ashost/owner/repoinsession.repo, the same as thegit@host:owner/repo.gitform of that remote. Before, the two spellings of one repository reached a receiver as two different repos. -
bea1cae: Internal: the home directory is read when a path is asked for, not when the package loads. A process whose HOME changes after import (a test with a sandbox home) gets the new one; no user-facing change.
-
1ca23ee: No hook event is lost to a worker's 15 s deadline, and none waits for an unrelated hook. 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.
-
434d10c: A machine under a flood of hook events no longer floods its sinks. 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. -
cdb1dbf: 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. A 10 MB session drained in batches of 50 decoded 55 MB.
-
67cc4fc: Recover a session's seq counter when its file is empty or not a number (a crash between truncate and write leaves it empty). Before, the counter restarted at 0 and the receiver's deduplication silently dropped every later event of that ses...