Skip to content

agent-session-kit 0.3.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 10:09
· 74 commits to main since this release
84f0746

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

  • SessionBrief on every SessionSnapshot. 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 the firstPromptAt it was given
    at), the latest instruction, the last thing the model said in prose, and when
    a turn last closed — folded by SessionStateReducer from userPrompt,
    assistantText, and turnEnded, 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, not lastPromptAt. 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> or nil; 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 of SessionProvider.defaultHarness, which is not injective —
    Codex and ChatGPT Work share one rollout tree.
  • SourceAdapter.discover(home:activeSince:under:) — discovery narrowed to
    one directory. nil means 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 under CreateFlags.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
    GrokBotTranscriptTailer diffs 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.
    Two stat calls short-circuit a poll with nothing to read — the client
    rewrites the file on every step of a streaming reply. A streaming entry
    produces thinking and nothing else; the words come out of the read that
    finds the flag cleared. send-message is the bot answering the person and a
    message with role: "user" is inbound, matching GrokBotSessionAdapter
    exactly, and renames, automations, widgets, secret requests, and attachments
    ride in note rather than pretending to be turns.
  • A needs-you signal for Grok Bot. The roster slice travels as an auxiliary
    path and its awaitingUserResponse becomes
    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 no Grok Bot process and no fresh
    ~/.grokbot/local-exec-supervisor.json heartbeat 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 answers unknown
    however long the quiet has lasted — idle is not ended. That heartbeat is the
    only file in ~/.grokbot this 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. version is pinned to the
    top released section of this file by AgentSessionKitInfoTests, so the
    constant and the changelog cannot drift apart without failing the suite.
    repositoryURL and releaseNotesURL(for:) are there so a host showing the
    number has somewhere to send the reader; the tag normalizer tolerates the
    v prefix 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.Z tag runs
    .github/workflows/release.yml: it refuses the tag unless
    AgentSessionKitInfo.version and 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.eventSchemaVersion is 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 declared watchRoots contain it, about the one directory it happened
    in. Nothing checked containment before, and mightBeSessionFile is a rule
    about names: Codex's "any *.lock could 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.rediscoverEvery is 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. rediscoverThrottle still 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 an F_GETLK per 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. CodexRolloutIndex remembers 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.probeLiveness answers 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 with active_sessions.json. RegistrySnapshot holds 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 them stats already
    half in hand and the fifth an open/F_GETLK/close on 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, and rosterRows are 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. RosterRow gains awaitingUserResponse, absent reading as false.

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 package exact: sees none of it
    until it bumps the pin and ships a build of its own — see
    RELEASING.md.