Skip to content

Releases: spacesheep-dev/sessionpipe

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 18:17

0.11.0

Minor Changes

  • 91a1e57: 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. 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.
  • 3f074d8: A new tier-1 event, limits.observed: the account's usage limits as the harness last reported them, one windows[] entry each (id, used_percent, resets_at?, window_minutes?) and an optional plan. 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's codex bucket only; new limits() on the adapter); Claude Code's maps them from its status line's rate_limits (a new StatusLine input) and, with statusLine: true, installs that status line in settings.json, never over one the person set. Schemas: event-data gains limits.observed.
  • 5bfc8cb: 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). 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.

Patch Changes

  • 83929e3: 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.
  • b5285de: 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.
  • 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 install hooks it instead of reporting "not plain JSON; not touched", and uninstall leaves it byte-identical. The harnesses read such a file themselves.

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 22:11

0.10.0

Minor Changes

  • 5ea2f11: 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"). 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.

  • 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.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.

  • 27c4194: Control: device keys (CONTROL.md §2, §3.2). 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).

  • 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 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.

  • 531b391: Control: a machine runs in inherit mode only with receivers that know it. 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.

  • 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, 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.

  • 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 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.
  • a6f8bb7: 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. 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.

  • d51fd31: 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). 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_JO...

Read more

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 10:43

0.9.0

Minor Changes

  • 55320c7: Control: sessionpipe-core/control accepts files on a start (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, null or empty as bad_fields; new exports filesProblem, ControlFile and the limits. Schemas: control-command gains files, control-pair / control-hello / the well-known control object gain files: true. 24 new shared vectors (66–89).

  • d525320: A devin adapter: Devin CLI (Cognition) sessions are reported like any other harness. 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.

  • 3fd0041: Review fixes to the Devin adapter and two gaps it exposed.

    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.

  • 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 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.

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 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.

  • 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 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).

  • 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.transcript and turn.progress. Claude Code records it as a queued_command attachment rather than a user record, 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: 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.
  • 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.git origin as host/owner/repo in session.repo, the same as the git@host:owner/repo.git form 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...

Read more