Releases: aleck31/agent-session-butler
Release list
v0.8.4
Full Changelog: v0.8.3...v0.8.4
v0.8.3
v0.8.2
v0.8.1
v0.8.0
v0.7.4 — self-update, and Hermes message counts
Self-update, plus the three gaps that were still open after 0.7.3.
asbutler update
Upgrading previously meant running install.sh from a checkout — fine if you have the source, useless if you installed a binary.
asbutler update # replace this binary with the latest release
asbutler update --check # report whether a newer release exists, install nothingasbutler version now also mentions a newer release when there is one.
Two details that matter more than they look:
- The binary is replaced by rename, never written in place. macOS caches a code signature per inode and SIGKILLs a binary whose bytes changed underneath it — exactly the v0.7.2 defect, where the installer reported success and left an
asbutlerthat died with exit 137. Verified end to end: the replaced binary runs immediately afterwards. - The download is verified before it is trusted. The staged file's own
versionhas to run and match the tag before the rename, so a truncated or wrong-platform download fails while the working binary is still in place. A build ahead of the latest release says so and stops, rather than downgrading itself.
The version check is confined to version and never runs in list, whose output other tools parse. It is bounded to one network call per day with a three-second timeout, caches its failures so an unreachable network is not retried every run, and prints nothing at all when anything goes wrong — version is what you run when something is already broken, so it works offline. ASBUTLER_NO_UPDATE_CHECK=1 turns it off.
While the repo is private, both this and install.sh need the GitHub CLI.
Hermes message counts were wrong
Hermes reported sessions.message_count, which is the raw row count of the messages table — a third of which is tool traffic on a real store (554 of 1,663 rows). One session claimed 43 messages against 16 actual turns.
The other three agents deliberately count conversation turns, so Hermes was the only agent whose number answered a different question, in a field the list JSON exposes. It now counts user and assistant rows only. Size still covers every row: that is disk usage, not conversation length.
If you compare messageCount for Hermes sessions across versions, expect it to drop.
Test coverage for two things that were only ever verified by hand
- The CLI's process contract. Exit codes, which stream output lands on, and the JSON shape — none of it observable from inside the process, so the tests exec the built binary. Pinned: a clean listing exits 0 with JSON on stdout and silence on stderr; a usage error exits 2 and explains itself on stderr while leaving stdout clean; a failed operation exits 1, so a caller can tell "you asked wrong" from "it did not work"; batch delete and relocate return one result per id and fail the process if any single id failed.
- Kiro's scan optimisation. The 17.7× speedup comes from stopping the metadata read before
session_state, which holds the whole conversation. Nothing checked that, so a change in Kiro's key order would have silently cost the speedup while staying correct. Asserted as bytes consumed rather than elapsed time — 512 bytes of a 400 KB document — because timing tests are flaky and the property that matters is "does not read the big value".
Also
The version reported to Codex's app-server was a hardcoded literal that had to be remembered at every release. It now comes from the build, with a test that fails if a literal reappears.
This is also the first release built and published by CI rather than by hand.
v0.7.3 — rename sessions
Titles come from a session's first prompt, so resuming the same task leaves several rows that read identically — four in one directory in the case that prompted this, differing only in message count. You can now give a session a name you will recognise.
asbutler rename <id> <title>Or click the title in the web UI: it becomes an input, Enter commits, Escape cancels.
Written into the agent's own metadata
The label goes into the agent's own title field, not a sidecar of ours, so the rename is visible in that agent too. All four backends turned out to have a supported write path, each verified against real data:
| Agent | How the title is set |
|---|---|
| Kiro | the title field in the session's .json |
| Claude Code | the ai-title rows in the session's .jsonl |
| Codex | the app-server's JSON-RPC thread/name/set |
| Hermes | hermes sessions rename |
Nothing writes around an agent. Codex and Hermes go through their own tools, which keeps their indexes and sidecar files consistent — Codex updates session_index.jsonl alongside its database, and a direct write would leave that stale.
Caveat for Claude Code: it re-emits its own ai-title row as a session progresses, so resuming a renamed session may let it supersede your title. It never changes an existing value — verified across 47 real sessions, none held two distinct titles — it just appends its own again.
Rename takes one session at a time on purpose: applying a single title to several would rebuild the ambiguity it exists to remove. And there is no length limit, because agents store far longer titles themselves (a Codex title is the raw first prompt, nearly 10,000 characters in one real case) — capping the input would make it impossible to put back a title that was already there.
Removed
The short session-id suffix that 0.7.1 appended to colliding titles is gone. It did not work: the session table clips the title column, so the suffix was invisible in exactly the place the collisions are read. It also answered the wrong question — the Session ID column already told them apart; what the titles lacked was meaning.
v0.7.2 — batch move/copy, target validation, display fixes
Batch move/copy — the feature this release was meant to carry — plus target-path validation and three display fixes.
Batch move / copy
Select all previously only fed batch delete; move and copy were one row at a time.
Web UI — a Move N selected button beside the batch delete, reusing the move/copy dialog with a plural title. The button counts the sessions that can actually be relocated rather than the whole selection, and its tooltip says how many were skipped, so it does not promise work it will not do.
CLI — mv and cp take any number of ids with the destination last, as the shell's own mv does and as rm already allowed:
asbutler mv sid1 sid2 sid3 ~/repos/projectBoth are best-effort and report per id. A locked session, or an agent that cannot relocate, fails on its own without stranding the rest — rejecting the whole batch over one of those would leave no way to make progress, and a move cannot be rolled back anyway, so an all-or-nothing guarantee was never on offer. The CLI's mv/cp JSON is now an array of {id, newCwd, copied, error}, the same shape rm returns, with a non-zero exit if any id failed.
Target directories are now validated
Only an empty string used to be rejected. Everything else was written into the session verbatim:
| Target | Before | Now |
|---|---|---|
relative/path |
stored as-is | rejected, showing the absolute path it resolved to |
~/repos/x |
stored with a literal ~ |
expanded |
| a path pointing at a file | stored as-is | rejected |
| a directory that does not exist | stored as-is | rejected — "create it first" |
/some/dir/ |
stored with the trailing slash | normalised |
A session's cwd is what lets an agent resume it there and what this tool groups on, so any of those produced a session no directory query could reach — silently, and indistinguishable from one more orphan. The realistic failure is a typo in a destination path.
Rejecting a missing directory is deliberate: the point of relocating is that the agent can resume there, which it cannot if the directory is absent, and mkdir -p is a one-line answer.
mv/cp now report the normalised path rather than echoing the input, since what gets stored is no longer necessarily what was passed in. The ~-and-absolute logic moved into one shared helper — the duplication is why list --path handled ~ while relocate did not.
Fixes
The sidebar showed a different path than the detail pane. /Users/x/y rendered as Users/x/y/ — no missing leading slash and no added trailing one, the same slash relocated. direction: rtl is what makes the ellipsis eat the start of a long path so the informative tail stays visible, but it also leaves the leading / bidi-neutral, so the layout puts it at the far end. A U+200E prefix tags the string left-to-right; verified in headless Chrome that this keeps left-truncation, which both unicode-bidi: plaintext and dropping the rtl lose.
The row move button was enabled for Codex, which cannot be relocated, and had simply not been updated when Codex support landed. The capability check is now one predicate shared by the row button, the batch button and the count.
Cannot read properties of null after a move. Clearing the dialog state made Alpine re-evaluate its bindings once more before tearing the dialog down. Latent in the single-session path too; the batch flow made it reproducible.
Also in this release
The macOS upgrade fix that was briefly published under this tag: ./install.sh wrote the new binary over the existing one, and macOS caches a binary's code signature per inode, so an overwritten executable gets SIGKILLed on the next run — the installer reported success and left an asbutler that died with exit 137. Downloads and builds now stage beside the target and rename into place.
If you installed the short-lived earlier v0.7.2 and asbutler version prints nothing, rerun ./install.sh from this tag.
v0.7.1 — Codex support, a test suite, and a 10x faster scan
Everything since v0.6.1. Two headline changes — a fourth supported agent, and the first test suite — plus a scan that got roughly ten times faster once it was actually profiled.
Codex support
Codex sessions are now discovered alongside Kiro, Claude Code and Hermes. It is a hybrid backend: conversation content lives in rollout .jsonl files, but every session is indexed in the threads table of $CODEX_HOME/state_<n>.sqlite. The index is authoritative — Codex ships migrate-rollouts, which calls the files legacy — so a scan is one read-only indexed query with no file reads, making Codex the cheapest agent to scan. A rollout is opened only to count messages.
Deletion goes through codex delete. Removing a rollout file directly would leave a dangling threads row, which Codex's own session picker would list as a session whose content is gone.
Notes on the shape of the support:
- Every session is listed, subagent threads included. Guardian reviews and spawned children carry a real working directory and real bytes, and nothing ever reclaims them — Codex has no retention setting, and 12-day-old spawn edges were still marked
open. They were 31% of Codex's disk use on the machine this was developed against. - No lock detection. Codex exposes only a global zero-byte coordination file with no PID, so nothing is reported as locked and
codex deleteis left to refuse a live session. - No move/copy.
cwdsits in the read-only index as well as the rollout file, with no CLI to change either. - Message counts are conversation turns, excluding reasoning, tool traffic,
developermessages, and theuser-role wrappers Codex injects. - The
_<n>filename suffix is a schema version, so the newest state DB is selected numerically rather than by a fixed name.
Tests
The repo had none. It now has ~3,000 lines covering the agents, the store, both view shapes, the CLI's scope rules and the HTTP layer, running against sandboxed HOME/CODEX_HOME/HERMES_HOME temp dirs so they never touch real session data.
They pin the invariants that were established empirically and are easy to break by accident: Claude Code's lossy project-directory encoding, the rule that relocating a Claude session must rewrite both the encoded directory and every line's in-file cwd, Hermes' read-only access, Codex's schema-versioned state DB, the (profile, cwd) grouping key, stale-lock detection, and the mtime cache.
The list JSON is also now pinned as a contract, since it has a downstream consumer outside this repo: a field rename fails a test instead of shipping.
Performance
list --path . went from ~0.58s to ~0.06s.
Profiling put Kiro at 494 ms per scan against Claude Code's 22 ms and Codex's 11 ms — 95% of the cost, and not where it had been assumed to be. A Kiro session's .json carries session_state, the whole conversation: 274 KB on average, up to 2.7 MB, 47.6 MB across 178 sessions. The scanner read and fully unmarshalled every one of them to extract two short strings that sit in the first few hundred bytes. It now streams and stops early: 494 ms → 27.9 ms.
Benchmarks for this are included, per agent and end to end, so the next such question starts from numbers.
Fixes
- A stale lock file could make a session permanently undeletable.
syscall.Kill(0, 0)signals the caller's own process group and succeeds, so a lock holding"pid": 0read as a live process. Non-positive PIDs are now rejected, on both Unix and Windows. install.shinstalled an older release over a newer checkout. Running it from a tree ahead of the latest release silently went backwards while reporting success. It now compares the source version against the release tag and builds from source when the checkout is ahead.install.shcould not reach release assets at all while the repo is private — anonymous URLs 404 there. It now prefersgh release download, falling back to an anonymous download.- The web UI's storage bar vanished at narrow window widths. The header strip had a fixed height, its legend wraps, and the 9px bar was the smallest flex item — so flex crushed it to nothing. Verified fixed at 1400/900/620/520 px in light and dark.
- Sessions with identical titles are now distinguishable. A title is the session's first prompt, so resuming the same task repeatedly produced rows identical except for message count. Colliding titles within a directory get a short id suffix; unique titles are untouched.
titleis a display string — useidas the identity. - An unrecognised
listflag is now an error. A typo like--paths ~/fooused to fall back to the current directory and return the wrong scope with a success exit code.
Also settled
messageCount disagreed with kiro-cli chat -l (202 vs 342). Both are right, for different questions: that session's .jsonl has exactly 342 lines — 171 AssistantMessage + 140 ToolResults + 31 Prompt — so kiro-cli reports raw events and this reports conversation turns. Deliberately unchanged; turns are the right answer for a session browser, and Codex's rule was written to match.
v0.6.1
Fixed
asbutler listis now scoped to one directory instead of the whole machine. The JSON path enriches every session it returns (reading each file to count messages), so a machine-wide listing walked ~1.5 GiB of history and took ~36s; a single directory takes ~0.8s.
Changed
asbutler listdefaults to the current working directory (equivalent to--path .).asbutler list --path <path>lists one directory only — exact match, no recursion into subdirectories. Accepts~, relative paths, and symlinks (macOS/tmp→/private/tmp).asbutler list --allrestores the previous machine-wide behaviour.asbutler list -onow implies--all, since orphan groups have no reachable directory to scope to.
Previously list had no path filter and always returned every session — pass --all for that behaviour.
Note: releases before this one stopped at v0.5.3, so install.sh was serving 0.5.3 binaries. This release also picks up the v0.5.4–v0.6.0 changes (JSON output by default with -H/--human for text, webui rename with auto-open, and mv/cp to re-home a session).