Repository navigation
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 session; now the next seq is read from the outbox file's last line, as it already was when the counter file was missing.
-
dc14f84:
turn.progress: a message absorbed by a running turn, and text the person never typed, no longer open a turn of their own. 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.isInjectedalso recognises a wrapper tag with attributes (<cross-session-message from="…">…</cross-session-message>). -
cb36e47: 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 ofsend(). -
d4b085c: Stop the transcript cursor before a last line the harness is still writing. 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.