From b75098406079203b0a9af32f51b935ec04182374 Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 03:50:35 -0500 Subject: [PATCH 1/8] The changelog names this cycle's work: the vanished keypress, the shared rulebook, the key vocabulary, the self-registering sweep --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ SOURCE.json | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c43b4b6..960a2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to Session Kit are documented here. ### Fixed +- A pressed key could vanish when the machine was busy. Bash's timed `read` + abandons input it has already taken from the kernel if its timer fires in + the middle of the read, so a keypress landing on the timer's edge was + consumed and never delivered: a `q` could disappear and the Enter behind it + act alone, or a lone Enter be swallowed outright. No read that consumes the + terminal carries a timer any more. The picker sleeps on a private descriptor + that never has data, asks `read -t 0` whether real input is ready, and only + then reads it, untimed. The idle beat is measured on the wall clock rather + than counted in ticks, so load cannot stretch it, and keys queued ahead of a + Ctrl-C are answered in the order they were typed. A source-scan test fails + any future timed read of the terminal. +- `sp account sync-rules` skips a symlinked rulebook by name and skips a + rulebook that is not UTF-8 text, instead of aborting the whole run on the + first and crashing with a traceback on the second. The skipped file and the + reason appear in the report. +- The `sp` help table had lost the line naming what `sp account verify` does. - Both pickers now agree with their own list about which sessions are waiting. The 2026-08-15 ruling that a finished provider turn is `needs you` however the vendor spells it lives in `labels.STATE_WORDS`, and the list column reads @@ -44,8 +60,23 @@ All notable changes to Session Kit are documented here. `Kill confirmation`. `docs/voice.md` allows neither word, and the second named a confirmation step that no longer exists. +### Changed + +- Two keys, named for what they do: inside a session, Ctrl-Q leaves it + running and Ctrl-D closes it; in the picker, Esc and Ctrl-D quit. Every + footer, hint, and page now uses that vocabulary, and `leave` no longer + appears where `quit` is meant. + ### Added +- `sp account sync-rules [--check]`: one shared rulebook, written by hand in + one place, is rendered into every enrolled profile's rulebook between + markers. `--check` reports drift without writing. The doctor reports a + profile whose rulebook has drifted from the shared file. +- The sandbox sweep derives its temp-directory prefixes from the test sources + themselves instead of a hand-kept list of six, so a new test's sandboxes are + swept without anyone remembering to register them, and it refuses by name to + remove `.git`, `.github`, `.gitignore`, and `.shellcheckrc`. - `CODE_OF_CONDUCT.md`, the Contributor Covenant 2.1, with reports routed through GitHub so one about the maintainer still reaches someone. - The README's pictures are generated from the running picker rather than diff --git a/SOURCE.json b/SOURCE.json index 78738f9..3f9946e 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "e34b7b8078fe76f8643487ce380a25f3f5f80e16" + "source_commit": "b84a81e19d6b8b9c6c67e495ecb8ffe2027fa92f" } From 801c2d4152d414c17275f462cb8fc813205c4610 Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 05:41:54 -0500 Subject: [PATCH 2/8] Losing the publishing-lock fence race retries instead of crashing The fence that retires the legacy inventory.lock is a read-only file swapped in by rename. A process that checked the fence just before the swap then opens the fenced inode for writing and gets EACCES, which the retry path did not catch: it escaped as a raw PermissionError. CI hit the interleaving once under coverage instrumentation. The open now re-verifies the fence and retries through the new lock generation; a genuine permission problem still surfaces. A deterministic test pins both sides. --- CHANGELOG.md | 7 ++++ SOURCE.json | 2 +- lib/sessionkit_inventory/state_io.py | 6 ++-- tests/test_state_io.py | 54 ++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 960a2c8..0af455e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,13 @@ All notable changes to Session Kit are documented here. than counted in ticks, so load cannot stretch it, and keys queued ahead of a Ctrl-C are answered in the order they were typed. A source-scan test fails any future timed read of the terminal. +- Losing the publishing-lock fence race no longer crashes. Retiring the + legacy `inventory.lock` replaces it with a read-only fence file, and a + second process that had just checked the fence then opened the fenced + inode for writing and escaped with a raw `PermissionError`. That open now + re-verifies the fence and retries through the new lock generation, while a + genuine permission problem still surfaces. CI caught the interleaving once + under coverage instrumentation; a deterministic test now pins it. - `sp account sync-rules` skips a symlinked rulebook by name and skips a rulebook that is not UTF-8 text, instead of aborting the whole run on the first and crashing with a traceback on the second. The skipped file and the diff --git a/SOURCE.json b/SOURCE.json index 3f9946e..783b53c 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "b84a81e19d6b8b9c6c67e495ecb8ffe2027fa92f" + "source_commit": "674707e848e17990e972319bb0adadff8db63aee" } diff --git a/lib/sessionkit_inventory/state_io.py b/lib/sessionkit_inventory/state_io.py index 19566fb..0e98041 100644 --- a/lib/sessionkit_inventory/state_io.py +++ b/lib/sessionkit_inventory/state_io.py @@ -291,9 +291,11 @@ def _open_versioned_publishing_lock(self) -> int: return descriptor try: legacy_descriptor = self._open_and_lock(legacy) - except CollectionError: + except (CollectionError, PermissionError): # Another new process may have replaced the legacy inode while - # this process waited on it. The exact fence is the only safe + # this process waited on it -- or fenced it between this + # process's fence check and its O_RDWR open, which the 0400 + # fence answers with EACCES. The exact fence is the only safe # reason to retry through the new lock generation. if self._legacy_lock_is_fenced(legacy): continue diff --git a/tests/test_state_io.py b/tests/test_state_io.py index 36e6f6d..a4662e2 100644 --- a/tests/test_state_io.py +++ b/tests/test_state_io.py @@ -20,7 +20,9 @@ sys.path.insert(0, str(REPO / "lib")) from sessionkit_inventory.state_io import ( # noqa: E402 + _LEGACY_LOCK_FENCE, CollectionError, + StateLock, _read_state_json, atomic_write_json, atomic_write_private_json, @@ -149,5 +151,57 @@ def test_atomic_private_json_refuses_an_interposed_published_path(self) -> None: self._assert_interposed_publication_is_refused(atomic_write_private_json) +class PublishingLockFenceRaceTests(unittest.TestCase): + """A process that loses the fence race retries instead of crashing. + + The legacy `inventory.lock` is retired by replacing it with a mode-0400 + fence file. A second process that checked the fence just before the + replacement then opens the fenced inode with O_RDWR and receives EACCES. + CI caught that interleaving once under coverage instrumentation + (2026-08-19): `acknowledge_pending` escaped with a raw PermissionError. + The open must re-verify the fence and continue through the new lock + generation, and a genuine permission problem must still surface. + """ + + def setUp(self) -> None: + self._raw = tempfile.TemporaryDirectory(prefix=".state-io-") + self.addCleanup(self._raw.cleanup) + self.root = Path(self._raw.name) / "state" + self.root.mkdir(mode=0o700) + self.versioned = self.root / "inventory-v2.lock" + + def _fence(self) -> Path: + legacy = self.root / "inventory.lock" + legacy.write_bytes(_LEGACY_LOCK_FENCE) + legacy.chmod(0o400) + return legacy + + def test_losing_the_fence_race_retries_through_the_new_generation(self) -> None: + self._fence() + real = StateLock._legacy_lock_is_fenced + calls = {"n": 0} + + def stale_then_real(lock: StateLock, path: Path) -> bool: + calls["n"] += 1 + if calls["n"] == 1: + return False # the check that raced the fence publisher + return real(lock, path) + + with mock.patch.object(StateLock, "_legacy_lock_is_fenced", stale_then_real): + with StateLock(self.root, self.versioned): + pass # acquiring is the assertion: no PermissionError escapes + + def test_a_genuine_permission_problem_still_surfaces(self) -> None: + legacy = self.root / "inventory.lock" + legacy.write_bytes(b"not the fence\n") + legacy.chmod(0o000) + with mock.patch.object( + StateLock, "_legacy_lock_is_fenced", lambda lock, path: False + ): + with self.assertRaises(PermissionError): + with StateLock(self.root, self.versioned): + pass + + if __name__ == "__main__": unittest.main() From 956a1163793c534686c730d36d6676e767e2b734 Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 06:26:16 -0500 Subject: [PATCH 3/8] The fake app-server records a rename before receipting it The client treats the id reply as receipt and the test asserts the moment the push returns, so a rename appended after sendall loses that race on a loaded machine. One matrix job caught the interleaving; the handler now appends before it replies, which makes the receipt a guarantee instead of a hint. --- SOURCE.json | 2 +- tests/test_app_server_reaper.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/SOURCE.json b/SOURCE.json index 783b53c..e56501c 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "674707e848e17990e972319bb0adadff8db63aee" + "source_commit": "033ed9ebf84bfb27e8a52c6d7bd4e3a456daa5a8" } diff --git a/tests/test_app_server_reaper.py b/tests/test_app_server_reaper.py index ff92540..d46af74 100644 --- a/tests/test_app_server_reaper.py +++ b/tests/test_app_server_reaper.py @@ -107,13 +107,19 @@ def read_exact(count: int) -> bytes: if first & 0x0F != 1: continue message = json.loads(payload) + # Record before replying: the client treats the reply as receipt + # and the test asserts as soon as the push returns, so a rename + # appended after sendall loses that race on a loaded machine + # (one matrix job caught it, 2026-08-19). + renamed = message.get("method") == "thread/name/set" + if renamed: + self.renames.append(message.get("params") or {}) if message.get("id") is not None: reply = json.dumps( {"id": message["id"], "result": {}}, separators=(",", ":") ).encode() link.sendall(bytes([0x81, len(reply)]) + reply) - if message.get("method") == "thread/name/set": - self.renames.append(message.get("params") or {}) + if renamed: return From f0e4d4c6cdaa507b69d518e1ebc07ea6766b875a Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 09:31:23 -0500 Subject: [PATCH 4/8] CI fails fast on a hung machine and forgives one upstream stumble Every job now carries a timeout near double its slowest healthy run, so a wedged runner dies in minutes instead of holding the run for the six-hour default and mailing a failure nobody caused. The upstream shpool test suite gets one retry: its cleanup_socket test is timing flaky on loaded machines, and one stumble of someone else's test should not paint our run red. Our own tests still fail on first strike. --- .github/workflows/ci.yml | 9 ++++++++- SOURCE.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebb5ada..8cced35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ permissions: jobs: linux: name: Linux ${{ matrix.os }} / Python ${{ matrix.python }} + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -70,6 +71,7 @@ jobs: macos: name: macOS ${{ matrix.arch }} / Python 3.13 + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -153,6 +155,7 @@ jobs: quality: name: Quality, privacy, and public export + timeout-minutes: 60 runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -322,6 +325,7 @@ jobs: shpool-patch: name: Optional shpool patches apply, test, and build + timeout-minutes: 30 runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -351,6 +355,9 @@ jobs: "$GITHUB_WORKSPACE/shpool-patch/$patch" done cargo test --locked --manifest-path \ - "$RUNNER_TEMP/shpool/Cargo.toml" --workspace + "$RUNNER_TEMP/shpool/Cargo.toml" --workspace \ + || { echo "::warning::upstream shpool tests failed once; retrying"; \ + cargo test --locked --manifest-path \ + "$RUNNER_TEMP/shpool/Cargo.toml" --workspace; } cargo build --locked --release --manifest-path \ "$RUNNER_TEMP/shpool/Cargo.toml" --bin shpool diff --git a/SOURCE.json b/SOURCE.json index e56501c..7e7d08c 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "033ed9ebf84bfb27e8a52c6d7bd4e3a456daa5a8" + "source_commit": "b19c1fb89709ad33a8ae04172a4e487b847481ae" } From 7b7026c34425620cbd5a5001f547db9ae251af47 Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 09:40:09 -0500 Subject: [PATCH 5/8] Session Kit v0.4.3 Version references and the changelog heading name the release being cut: the picker no longer loses a keypress under load, one shared rulebook reaches every profile, the key vocabulary says quit where it means quit, the sandbox sweep registers its own prefixes, and losing the publishing-lock fence race retries instead of crashing. --- CHANGELOG.md | 3 ++- README.md | 2 +- SOURCE.json | 2 +- docs/install.md | 2 +- docs/maintainers/release-process.md | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af455e..e322219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to Session Kit are documented here. -## [Unreleased] +## [0.4.3] - 2026-08-19 ### Fixed @@ -343,6 +343,7 @@ All notable changes to Session Kit are documented here. attach. - Added patch `0006`, which coalesces bursts of client resize events. +[0.4.3]: https://github.com/dob323/session-kit/releases/tag/v0.4.3 [0.4.2]: https://github.com/dob323/session-kit/releases/tag/v0.4.2 [0.4.1]: https://github.com/dob323/session-kit/releases/tag/v0.4.1 [0.4.0]: https://github.com/dob323/session-kit/releases/tag/v0.4.0 diff --git a/README.md b/README.md index d146c01..062b089 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ One limit, stated up front rather than in a table further down: Claude Code repo Documentation

-> **Public beta, `v0.4.2`.** Linux with systemd, or macOS 14 and newer. +> **Public beta, `v0.4.3`.** Linux with systemd, or macOS 14 and newer. > > **Settled:** the picker, session identity and the guards around every action, install, update and rollback, and the Claude Code and Codex integrations. Around 2,700 tests run on both platforms for every change. > diff --git a/SOURCE.json b/SOURCE.json index 7e7d08c..f690b51 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "b19c1fb89709ad33a8ae04172a4e487b847481ae" + "source_commit": "dbed2b8a3b2e7cd73196643eb7e96f0ddc402fb7" } diff --git a/docs/install.md b/docs/install.md index 65b0813..521c65b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,6 +1,6 @@ # Install Session Kit -Session Kit `v0.4.2` is a public beta for Linux with systemd and macOS 14 or +Session Kit `v0.4.3` is a public beta for Linux with systemd and macOS 14 or newer. Install the accepted release artifact under a single-user account where provider conversations are recoverable. diff --git a/docs/maintainers/release-process.md b/docs/maintainers/release-process.md index 398df8c..f0323b2 100644 --- a/docs/maintainers/release-process.md +++ b/docs/maintainers/release-process.md @@ -6,7 +6,7 @@ and release assets. ## Version and support policy -The release documented by this tree is `v0.4.2` and remains beta software. +The release documented by this tree is `v0.4.3` and remains beta software. Follow Semantic Versioning and never move or reuse a tag. Before `1.0.0`, put every incompatible command, configuration, or private-state From ac4f5784542a84b9d28c9d23d317f7c5c8577524 Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 22:49:00 -0500 Subject: [PATCH 6/8] The front page leads with the problem, the picture, and why this exists A visitor used to read four paragraphs of identity model, colour allocation and subscription behaviour before reaching the screenshot, which made a small tool read as a systems project. The picture now sits under a two-line explanation, the reason the thing exists comes next in the author's own words, and the implementation detail moved into the sections that already covered it. The beta block became one line at the top with its promises kept in full further down. Two corrections rather than styling: the suite claim said 2,700 tests run on both platforms for every change, and macOS runs a selected set of modules, so it now says native Linux and macOS coverage in CI. The page also no longer surveys other session managers or tells a reader another tool would serve them better. --- README.md | 118 +++++++++++++++++++++++++++++++--------------------- SOURCE.json | 2 +- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 062b089..c5aeee1 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,18 @@ **Many AI coding sessions. One place to see which one needs you.** -Session Kit is a local picker for Claude Code and Codex. Type `kit` and every session you are running is in one list, each with a stable number, a name, and a state word that says whether it is waiting on you or still working. The sessions run on the host, so closing the terminal or dropping SSH does not end them. +Session Kit is a terminal home screen for Claude Code and Codex. Type `kit` and every session you are running is on one screen, each with a stable number, a name, and a word that says whether it is waiting on you or still working. Type its number to go straight back in. -One limit, stated up front rather than in a table further down: Claude Code reports a blocking prompt the moment it opens one, so `question` is exact. Codex does not expose that yet, so a Codex session reads `needs you` when its turn ends rather than the instant it asks you something. - -**What is on the screen is not what gets trusted.** The number, the name, the directory, the timestamps: all of that is display. Identity is the provider's conversation UUID together with the exact process generation, and every action that changes anything re-proves that against the live system in the moment before it runs, then refuses outright on evidence that is missing, stale, partial or contradictory. This is the part worth caring about: a picker one second out of date still cannot act on the wrong session, because the picker was never the evidence. - -**Every session names itself.** It takes a short title from its own first piece of work, keeps a number that does not move, and gets a colour of its own. All three follow the session *into* its own window. The tab title carries the name and number, and the session is tinted its colour in Claude Code and Codex themselves. So the window you are typing in tells you which session it is, and the picker and the session never disagree about it. Colours are kept distinct from the other live sessions: eight for Claude Code, six for Codex, chosen so no Claude session can ever share a colour with a Codex one. - -**Every session can use a different subscription.** Enrol several accounts for the same provider and hand each session whichever one you want: three Claude Code subscriptions running in three sessions side by side on one machine, at the same time. The account belongs to the session, not to the machine or to the terminal you launched from, which is the usual arrangement elsewhere. +The sessions run on the host, so closing the terminal or dropping SSH does not end them.

The Session Kit picker: seven sessions grouped into Ready and Open elsewhere, each row showing number, name, provider, account, model, state, and last activity

+

+ kit → see what needs you → type its number → back in the session. +

+

Install · The picker · @@ -30,55 +28,56 @@ One limit, stated up front rather than in a table further down: Claude Code repo Documentation

-> **Public beta, `v0.4.3`.** Linux with systemd, or macOS 14 and newer. -> -> **Settled:** the picker, session identity and the guards around every action, install, update and rollback, and the Claude Code and Codex integrations. Around 2,700 tests run on both platforms for every change. -> -> **Not settled:** the beta moves quickly, thirteen releases in its first three weeks, so expect to update. Codex does not report a blocking prompt yet. -> -> **What is promised:** this is a tool its author uses daily and publishes; it is not a supported product. Replies are best-effort, security fixes go into the current release with no dated windows and no back-ports, and nothing here is a security boundary against another process already running as your Unix user. Start on a single trusted Unix account where provider conversations can be recovered. +> **Public beta, `v0.4.3`.** Linux with systemd, or macOS 14 and newer. I use Session Kit daily, and the beta moves quickly, so expect to update. Full detail in [Beta and support](#beta-and-support). -## What you get +## Why I built it -| Capability | What it means | -|---|---| -| **One list** | Every managed Claude Code and Codex session in a single keyboard-first picker. | -| **Named without asking** | A session titles itself from its own first work. Rename it yourself and your name wins from then on. | -| **A colour per session** | Each session gets a colour the others do not have, in the picker and inside the session's own window. | -| **A subscription per session** | Enrol several accounts for one provider and give each session its own. Sessions do not share a single login. | -| **States that mean something** | `question`, `needs you`, `working`, and `idle` have exact definitions and sort consistently. | -| **Stable session numbers** | Open, inspect, close, and find sessions without copying internal shpool IDs. | -| **Survives disconnects** | The session stays on the host when a terminal window closes or SSH drops. | -| **Guarded actions** | Every mutation rechecks exact live identity immediately before it runs, and refuses on any doubt. | -| **Recoverable conversations** | Closing a Claude Code or Codex session records the exact conversation for restore. | -| **Isolated working copies** | A delegated session gets its own git worktree on its own branch, and hands it back when it closes. It is kept instead, with the reason named, whenever giving it back could lose work. | -| **Local only** | No hosted account, no analytics, no update beacon, no telemetry. | +I run several Claude Code and Codex sessions at the same time. Keeping them alive turned out to be the easy part. Remembering which one was doing what, which one had finished, and which one had been sitting there waiting on me the whole time was not. I kept opening window after window just to find the session that had asked me something. -## Is this for you? +Session Kit is the small home screen I wanted for that. I use it every day, on a local machine and over SSH to the host where the work actually runs. -It fits best if you run several AI coding sessions at once, work on a remote host over SSH, and want to know at a glance which session is blocked on you rather than opening each one to find out. +It is a passion project. There is no company behind it, no paid tier, and nothing to buy. It is MIT licensed, it collects nothing, and it talks to no server of mine. I published it because it fixed a problem I hit every single day, and if you hit the same one, it will probably fix yours. -Here is what it does **not** do: +## What it does -- **No diff review.** It will not show you what a session changed. That stays in git and your editor. -- **No cost or spend accounting.** It can read the provider's own quota percentages and tell you when one runs out, but it does not price or count tokens. -- **Not a multiplexer.** It runs on [shpool](https://github.com/shell-pool/shpool) and does not replace tmux, or try to. -- **Not a security boundary.** It protects you from acting on the wrong session, not from a hostile process running as you. +- **See every session in one list.** Claude Code and Codex together, on one screen. +- **Know what needs you.** `question`, `needs you`, `working`, and `idle` make the next session to check obvious. +- **Jump back in by number.** Every session keeps a stable number that does not move. +- **Survive terminal and SSH disconnects.** The session keeps running on the host. +- **Know which window is which.** Sessions name and colour themselves, and the name, number, and colour follow the session into its own window. +- **Use a different account per session.** The subscription belongs to the session, not to the terminal that launched it. +- **Get a closed conversation back.** Closing a session records the exact conversation for restore. +- **Keep delegated work apart.** A machine-origin session gets its own git worktree on its own branch, and hands it back when it closes. +- **Stay local.** No hosted account, no analytics, no telemetry, no update beacon. + +## Is this for you? -If what you actually need is reviewing the diff each agent produced, a different tool will serve you better. +If you regularly have enough sessions open that you lose track of which one needs you, this was built for exactly that, and you will feel it on the first run. -### Why not tmux, or one of the other session managers? +If you usually keep one or two sessions going, you probably do not need it yet. -**tmux** is already on your machine and keeps processes alive, which is most of the way there. What it does not do is tell you which of eleven panes is waiting on an answer, and it has no notion of which conversation a pane holds, so nothing stops you acting on the pane next to the one you meant. If you are running two sessions, tmux is enough and this is overkill. +It works the same on a local workstation or on a remote host over SSH. -**The other AI session managers** solve the listing problem, and several are pleasant. The difference is what they do with the list: they act on it. Here the list is treated as a cache that can be a second out of date, and identity is re-proved against the live system before anything is changed. That distinction only matters when you have enough sessions that the list *can* be wrong, which is also the point where you start needing this. +## Simple on purpose -**What it costs you:** this runs on shpool rather than tmux, so there is one more thing to install before you see anything. That was a deliberate trade: shpool gives a clean session per shell without fighting multiplexer semantics, which is what makes a session's identity provable at all. It is a real cost, and it is paid before you get to the payoff. +The interface is deliberately small: open `kit`, see what needs you, press a number. + +More is going on underneath. Before Session Kit changes a live session, it re-proves that the session is still the exact provider conversation and the exact process it expects. If it cannot prove that, it refuses instead of guessing. + +The picker is a view of your sessions. It is never trusted as evidence of which live session an action should affect. That distinction is the whole design, and [Safety model](#safety-model) has it in full. ## Install Session Kit installs per user, on the Linux or macOS machine where the work runs. +### Installing with an AI assistant + +The shortest path, if Claude Code, Codex, or another terminal agent is already open: give it this. + +> Install Session Kit from `https://github.com/dob323/session-kit`. Use the latest release artifact, not a clone of `main`. Download the release archive with its `.sha256` and `.provenance.json` files, verify the checksum, extract it, and run `./install.sh --check` first. Fix only the remedies that preflight explicitly names. Then run `./install.sh`, `session-kit doctor`, `session-kit services enable`, and `session-kit doctor` again. Never bypass a refused step. Show me the final doctor output and finish by telling me to type `kit`. + +### Installing it yourself + Artifacts are named by the exact commit they were built from, so there is no fixed download URL. This asks the release API which files belong to the current release, checks what arrived, and only then unpacks it. It needs nothing but Python 3 and `tar`, both of which the install needs anyway. ```bash @@ -125,12 +124,6 @@ session-kit doctor Prefer the GitHub CLI, installing without a network path to the API, or checking the provenance file by hand? [Install Session Kit](docs/install.md) has every route, plus supported shpool paths, provider setup, project import, and activation. -### Installing with an AI assistant - -If Claude Code, Codex, or another terminal agent is doing the installation, give it this: - -> Install Session Kit from `https://github.com/dob323/session-kit`. Use the latest release artifact, not a clone of `main`. Download the release archive with its `.sha256` and `.provenance.json` files, verify the checksum, extract it, and run `./install.sh --check` first. Fix only the remedies that preflight explicitly names. Then run `./install.sh`, `session-kit doctor`, `session-kit services enable`, and `session-kit doctor` again. Never bypass a refused step. Show me the final doctor output and finish by telling me to type `kit`. - ## First run ```bash @@ -160,6 +153,8 @@ The state words are deliberately small and literal: | `idle` | A needs-you transcript has not moved for the configured idle window. | | `pending` | A launch that has not finished, or a value Session Kit cannot currently read. It is not a fifth state. | +One limit worth stating plainly: Claude Code reports a blocking prompt the moment it opens one, so `question` is exact. Codex does not expose that yet, so a Codex session reads `needs you` when its turn ends rather than the instant it asks you something. + Pressing `a` narrows the list to just those sessions, with how long each has waited. @@ -182,6 +177,8 @@ The home screen keeps the common actions one key away: A session that is open elsewhere defaults to **Move it here** after a fresh identity check. The earlier window returns to its picker, and the provider conversation is not duplicated. +Every session also names and colours itself. It takes a short title from its own first piece of work, keeps a number that does not move, and gets a colour no other live session has. All three follow the session into its own window: the tab title carries the name and number, and the session is tinted its colour inside Claude Code and Codex themselves. So the window you are typing in tells you which session it is, and the picker and the session never disagree. + Press `?` for the full key reference: filtering, ranges, grouping, forking, renaming, and `g` to jump to the next session that needs you are all there. See [Picker navigation](docs/picker-navigation.md) for the cursor-driven picker, mouse behavior, action panels, machine sessions, and closed-session restore. ## Safety model @@ -200,10 +197,27 @@ Session Kit deliberately separates **what you see** from **what it trusts**. Before a proof-bound action, Session Kit can bind and recheck the session manager, terminal generation, managed shell, provider process and ancestry, exact provider conversation UUID, and frozen snapshot generation. The proof is owner-only and short-lived. -This protects against stale or ambiguous picker state selecting a different session than the one you intended. It does **not** isolate mutually hostile processes running with your own Unix-user privileges. +This is the part worth caring about: a picker one second out of date still cannot act on the wrong session, because the picker was never the evidence. + +It protects against stale or ambiguous picker state selecting a different session than the one you intended. It does **not** isolate mutually hostile processes running with your own Unix-user privileges. Read [Security and local data](docs/security-and-data.md) and [Architecture](docs/architecture.md) for the complete trust model. +## What it does not do + +- **No diff review.** It will not show you what a session changed. That stays in git and your editor. +- **No cost or spend accounting.** It can read the provider's own quota percentages and tell you when one runs out, but it does not price or count tokens. +- **Not a multiplexer.** It runs on [shpool](https://github.com/shell-pool/shpool) and does not replace tmux, or try to. +- **Not a security boundary.** It protects you from acting on the wrong session, not from a hostile process running as you. + +### Why not tmux? + +tmux keeps processes alive, which is most of the way there, and it is already on your machine. What it cannot tell you is which of eleven panes is waiting on an answer. It has no notion of which conversation a pane holds, so nothing stops you acting on the pane next to the one you meant. + +Session Kit knows both. Every session carries the identity of its provider conversation, and that identity is re-proved against the live system before anything is changed. + +It runs on [shpool](https://github.com/shell-pool/shpool) rather than tmux, so there is one more thing to install first. That was the trade: shpool gives a clean session per shell without fighting multiplexer semantics, and that is what makes a session's identity provable at all. + ## Accounts and subscriptions One machine, several logins for the same provider, one per session. Enrol each account once and it becomes a choice at launch: @@ -290,6 +304,14 @@ sp help # every command, with exit codes and selectors Session Kit runs on [shpool](https://github.com/shell-pool/shpool) and neither vendors nor replaces it. The repository carries optional shpool patches with their scope and checks; `doctor` records the shpool binary validated at installation and reports when it later changes. Read [`shpool-patch/`](shpool-patch/) before choosing a patched binary. +## Beta and support + +**Settled:** the picker, session identity and the guards around every action, install, update and rollback, and the Claude Code and Codex integrations. Around 2,700 tests run in CI, with native Linux and macOS coverage. + +**Not settled:** the beta moves quickly, thirteen releases in its first three weeks, so expect to update. Codex does not report a blocking prompt yet. + +**What is promised:** this is a tool its author uses daily and publishes; it is not a supported product. Replies are best-effort, security fixes go into the current release with no dated windows and no back-ports, and nothing here is a security boundary against another process already running as your Unix user. Start on a single trusted Unix account where provider conversations can be recovered. + ## Documentation **[Documentation index](docs/)**, every document grouped by what you are trying to do. diff --git a/SOURCE.json b/SOURCE.json index f690b51..100a61d 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "dbed2b8a3b2e7cd73196643eb7e96f0ddc402fb7" + "source_commit": "791fdc66fca8e3797a7d1520fedbb540322cb4d5" } From b84583f656a227d69754609556aa7c6dda4d1714 Mon Sep 17 00:00:00 2001 From: dob323 Date: Wed, 19 Aug 2026 23:31:38 -0500 Subject: [PATCH 7/8] A watcher that has not started yet says so, instead of blaming the interval The argv helper waited five seconds for a separate watcher process to write its argv file, then returned an empty string. On a loaded runner the watcher had not started in that window, so the caller's assertion reported that '--interval .25' was missing from '' and pointed at the interval value rather than at the process that never ran. CI caught it in the coverage job. The wait is now thirty seconds, a half-written file does not count as an answer, and running out of time raises with the reason. --- SOURCE.json | 2 +- tests/test_picker_events.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/SOURCE.json b/SOURCE.json index 100a61d..c09f816 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "791fdc66fca8e3797a7d1520fedbb540322cb4d5" + "source_commit": "7f670d2c2d798c71eaecab24cae809aef85621b4" } diff --git a/tests/test_picker_events.py b/tests/test_picker_events.py index 2a19a89..de343c1 100644 --- a/tests/test_picker_events.py +++ b/tests/test_picker_events.py @@ -960,12 +960,26 @@ def probe(self, value=None, *, ticks="1") -> tuple[str, str, str]: return pulse, interval, state def argv(self) -> str: - deadline = time.monotonic() + 5 + # The watcher is a separate process, so its argv file appears after the + # probe returns. Waiting five seconds was enough on an idle machine and + # not on a loaded CI runner, and the old timeout returned an empty + # string, so the caller's assertion blamed the interval value for a + # watcher that had simply not started yet (2026-08-20). Wait longer, + # require the write to be complete, and say what actually went wrong. + budget = 30 + deadline = time.monotonic() + budget while time.monotonic() < deadline: - if self.argv_file.exists(): - return self.argv_file.read_text(encoding="utf-8") + try: + recorded = self.argv_file.read_text(encoding="utf-8") + except FileNotFoundError: + recorded = "" + if recorded.strip(): + return recorded time.sleep(0.05) - return "" + raise AssertionError( + f"the watcher wrote no argv to {self.argv_file} within {budget}s; " + "it never started, so nothing here is a statement about its arguments" + ) def test_a_value_argparse_cannot_read_falls_back_to_the_default(self) -> None: marker = self.base / "pwn" From 3978d9845d6c5a2c2c64c1228c0d9d24c1ea4aff Mon Sep 17 00:00:00 2001 From: dob323 Date: Thu, 20 Aug 2026 04:38:27 -0500 Subject: [PATCH 8/8] The front page opens with what you close, what you keep, and how you get back The tagline names the trade the tool actually makes: a terminal window is a view, a session is the work, and closing the first does not touch the second. The line under it says what you get instead, in the three words that matter, and the third says why closing is safe. The identity model, the colour rules and the subscription system stay where they were, further down. --- README.md | 6 +++--- SOURCE.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c5aeee1..bab4abd 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ [![Latest release](https://img.shields.io/github/v/release/dob323/session-kit)](https://github.com/dob323/session-kit/releases) [![License](https://img.shields.io/github/license/dob323/session-kit)](LICENSE) -**Many AI coding sessions. One place to see which one needs you.** +**Close the terminal windows, not the AI coding sessions. One number takes you back in.** -Session Kit is a terminal home screen for Claude Code and Codex. Type `kit` and every session you are running is on one screen, each with a stable number, a name, and a word that says whether it is waiting on you or still working. Type its number to go straight back in. +Every Claude Code and Codex session in one menu: named, numbered, colour-coded. Open, close, and jump to whichever needs you. -The sessions run on the host, so closing the terminal or dropping SSH does not end them. +The sessions keep running on the host whether or not you are watching them.

The Session Kit picker: seven sessions grouped into Ready and Open elsewhere, each row showing number, name, provider, account, model, state, and last activity diff --git a/SOURCE.json b/SOURCE.json index c09f816..d26805a 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -1,5 +1,5 @@ { "exported_files": 315, "schema_version": 1, - "source_commit": "7f670d2c2d798c71eaecab24cae809aef85621b4" + "source_commit": "9a7af3a1e2c38eb8050afa8be018780ea6f777f2" }