agent-session-kit 0.3.0
Sessions learn to say what they were asked for and how to get back into
them, Grok Bot goes live, discovery and liveness stop paying per-session for
answers that are per-machine, and the package learns how to cut its own
releases.
Added
SessionBriefon everySessionSnapshot. The state machine says what a
session is doing; it could not say what anybody asked it for. The brief
carries the assignment (firstPrompt, with thefirstPromptAtit was given
at), the latest instruction, the last thing the model said in prose, and when
a turn last closed — folded bySessionStateReducerfromuserPrompt,
assistantText, andturnEnded, the three events all eight live adapters
already emit. No adapter changed.- A filter for the things a person did not type. Half of what a harness
records as a "user message" is machinery: Claude Code spells a slash command
as<command-name>…</command-name>, injects context as<system-reminder>,
and spills hook output into<local-command-…>; Codex prepends
<environment_context>and<user_instructions>.
SessionBrief.instruction(_:)strips those blocks — including the ones a
200-character preview cut in half, from either end — and refuses what is left
when it is empty or a bare slash command. A rejected prompt moves nothing:
not the assignment, not the latest prompt, notlastPromptAt. Every field
guards its own clock, so a turn flushed out of order cannot overwrite a newer
one, and the brief survives an explicit restart. SessionResume— "how do I get back to this one?", answered from a
SessionIdentity.resumeCommand(for:)returns a cwd-aware
cd '<dir>' && <command>ornil;availability(for:)returns the same
answer plus the sentence to put in a disabled menu item, because Claude
Cowork, Cursor, and Grok Bot have no command-line entry point at all and a
menu item that quietly disappears explains nothing.Harness.sessionProvider
is the inverse ofSessionProvider.defaultHarness, which is not injective —
Codex and ChatGPT Work share one rollout tree.SourceAdapter.discover(home:activeSince:under:)— discovery narrowed to
one directory.nilmeans the whole store, and the default implementation
sweeps, so an adapter outside this package is unaffected and merely as
expensive as it always was. Every adapter here narrows: Grok reads a scope
positionally against~/.grok/sessions, Codex recognises a<yyyy>/<MM>/<dd>
directory by shape, Claude Code and Cowork resolve a project directory from
any path below it, Cursor resolves a workspace or an agent, and AntiGravity
resolves which of its two roots a change was in. Each of them refuses to
narrow what it cannot —~/.claude/sessions, Codex's lock directory,
AntiGravity's summaries store — and sweeps instead, because an entry
appearing in any of those can make a session far older than the cutoff worth
tailing.FSEventBatch.isDirectory(_:)— whether FSEvents said a delivered path is
itself a directory. Meaningful only underCreateFlags.fileEvents, which is
what makes FSEvents report per-item flags at all.IngestConfiguration.discoveryDebounce— how long a routed discovery
waits for the rest of its burst. Default 250 ms.GrokBotLiveAdapter— Grok Bot is now live, not just indexed. A
conversation is a JSON document the desktop client rewrites whole, so
GrokBotTranscriptTailerdiffs the file against itself rather than walking
it: a poll asks which entries are new and which of the ones already read have
stopped streaming, and the cursor is.blobHead(<id of the last entry consumed>)because entry ids survive a rewrite and a byte offset does not.
Twostatcalls short-circuit a poll with nothing to read — the client
rewrites the file on every step of a streaming reply. A streaming entry
producesthinkingand nothing else; the words come out of the read that
finds the flag cleared.send-messageis the bot answering the person and a
messagewithrole: "user"is inbound, matchingGrokBotSessionAdapter
exactly, and renames, automations, widgets, secret requests, and attachments
ride innoterather than pretending to be turns.- A needs-you signal for Grok Bot. The roster slice travels as an auxiliary
path and itsawaitingUserResponsebecomes
permissionRequested(id: "grokbot:<bot>", tool: nil), resolving when the flag
clears, stamped with the roster file's own mtime. It is the only field
anywhere in this store that says a person is needed, and a conversation
carrying it is discovered however old its timestamps are. - Liveness about the client, not the conversation. The conversation runs on
xAI's servers, so noGrok Botprocess and no fresh
~/.grokbot/local-exec-supervisor.jsonheartbeat ends every conversation at
once, a running client with a replica written in the last two minutes is
alive, and a running client with a quiet conversation answersunknown
however long the quiet has lasted — idle is not ended. That heartbeat is the
only file in~/.grokbotthis package opens, and the directory is
deliberately not watched: its neighbours hold a daemon token and a
credential. AgentSessionKitInfo— the version of the package, written down where
a statically linked host can read it. There is nothing else to ask: once
this code is compiled into somebody's binary there is no bundle, no
Info.plist, and no dylib left to interrogate.versionis pinned to the
top released section of this file byAgentSessionKitInfoTests, so the
constant and the changelog cannot drift apart without failing the suite.
repositoryURLandreleaseNotesURL(for:)are there so a host showing the
number has somewhere to send the reader; the tag normalizer tolerates the
vprefix this project does not use, because a caller reading a tag out of
the GitHub API should not have to know that.- A release workflow. Pushing a bare
X.Y.Ztag runs
.github/workflows/release.yml: it refuses the tag unless
AgentSessionKitInfo.versionand a## [X.Y.Z]changelog section both
agree with it, builds, tests, and then publishes a GitHub Release whose
notes are that changelog section. Every check fails loudly — a tag that
does not match its own source is not a release, and the workflow would
rather publish nothing than publish a lie. - RELEASING.md — the semver policy while this package is
0.x, the exact steps to cut a version, what a consumer has to do
afterwards, and the rule that a published tag never moves.
Changed
AgentSessionLive.eventSchemaVersionis 2. A field was added to
SessionSnapshot, which is encoded structurally, so a host that persisted
snapshots re-seeds rather than decoding rows from a model it no longer
speaks. That is the contract the constant already documented.- A file-system change is routed, not broadcast. An unrecognised path used
to restart discovery for every adapter over every store, throttled to once
per three seconds; with a transcript gaining a line a second, that is a sweep
of the whole machine every three seconds. A change now goes to the adapters
whose declaredwatchRootscontain it, about the one directory it happened
in. Nothing checked containment before, andmightBeSessionFileis a rule
about names: Codex's "any*.lockcould be a thread" claimed every writer
lock Grok rewrites and every presence file AntiGravity heartbeats, and
Cursor's "any*.jsonl" claimed every Claude Code transcript. Each of those
was a full sweep. FSEvents also announces every watch root as a directory
event the moment a stream arms, which woke every adapter once per launch for
news that was not news. IngestConfiguration.rediscoverEveryis 60 s, was 15 s. It is the safety
net now rather than the mechanism: a session that appears while the pipeline
runs is found by routing, in well under a second. It is also still the only
pass that drops a source, because "nobody discovered this" needs somebody to
have looked everywhere.rediscoverThrottlestill defaults to 3 s and is now
per adapter — Grok being rewritten is no reason to make Claude Code wait.- Discovery does not re-read what cannot have changed. Each adapter keeps
what it derived from a file until that file moves: Claude Code and Cowork key
a transcript's head on the inode, because an append-only file that grew by
a thousand lines has the same first three; Grok keys a source on its
summary.json; Cursor keys the conversation card on the store's own stamp;
AntiGravity keys the summaries index on the store and its WAL, since in WAL
mode that is where the writes land. - Grok's writer-lock probe is asked last, and remembered. It is a
readdir
plus anF_GETLKper lock file, Grok keeps one lock per mutable file, and it
ran for every session in the store on every pass — about seven hundred
syscalls, and the single most expensive thing the pipeline did at rest. It is
asked only after the mtimes and the registry have failed to answer, and only
when the answer can have changed: taking a lock creates a file, which moves
the directory's mtime, and a session doing anything moves one of its logs. A
held verdict additionally expires after thirty seconds, because a lock is
released without leaving a trace on disk. - Codex's second pass no longer re-walks history for the same unresolvable
ids. The whole-tree walk that finds a locked thread whose rollout predates
the cutoff kept running, every year of it, for lock files left behind by
threads whose rollouts are long gone.CodexRolloutIndexremembers where a
rollout was found and which ids have none; a rollout that appears later is
found by the first pass, on the notification that creates it. - A liveness probe reads a machine-wide registry once per pass, not once per
session.ClaudeLiveAdapter.probeLivenessanswers by reading
~/.claude/sessions— a directory listing, a JSON parse per entry, a
ctime(3)parse per entry — and it is asked once per session, so two hundred
sessions read the same directory two hundred times every three seconds. Grok
did the same withactive_sessions.json.RegistrySnapshotholds such an
answer while the file or directory it came from has not moved and it was
read within a second, which is well under the interval a resolver ticks at.
Discovery does not use it: it asks once per pass and wants the freshest
answer there is. - AntiGravity asks the cheap questions first. Whether a conversation is
worth tailing is a disjunction of five facts, four of themstats already
half in hand and the fifth anopen/F_GETLK/closeon somebody else's
presence file. The probe was asked first, so it was paid for every
conversation in the store on every pass. Same answer, asked last. GrokBotSessionAdapter's key vocabulary is public. The store path, the
blob extension, the variant,decodedKey,transcriptKey,rosterKey,
rosterURL, androsterRowsare what the live adapter reads the same
directory with; a second copy of any of it is how a store ends up listed on
one screen and missing from the other. No behaviour changed and nothing was
renamed.RosterRowgainsawaitingUserResponse, absent reading asfalse.
Measured over a synthetic home of 743 sources across five stores, a second
sweep with nothing changed went from 494 directory listings, 301 lock probes
and 677 file reads to 271, 1 and 0. On a real machine with 645 sessions
across six harnesses, the ingest pipeline leaves a sample of a live host at
rest altogether: no discover frame anywhere, and the liveness tick down from
886 samples to 74, of which three are Claude Code's probe rather than 715.
Note
- Nothing here is breaking, so 0.3.0 is a minor bump over 0.2.0 (
fd0c95a).
It is the first tag to carry a published GitHub Release; 0.1.0 and 0.2.0
exist as tags only. A host pinning this packageexact:sees none of it
until it bumps the pin and ships a build of its own — see
RELEASING.md.