Persist terminal layouts incrementally and reap zmx sessions by attach state#369
Merged
Conversation
zmx ls --short emits only the session name, so the reaper cannot tell whether another instance still holds a client. Parse the full ls format through a pure ZmxSessionListParser (name=/clients=, supa- prefix filter, clients=nil for unreachable lines) behind a listSessionsWithClients field that returns nil on probe failure, so an unprobeable session is never reaped. The reaper consumes this exclusively, so the name-only listSessions is gone.
Per-mutation persistence cannot run the whole-dict encode and atomic write synchronously on the main actor. LayoutsIncrementalWriter is a single FIFO actor that re-reads layouts.json, splices in only the keys a flush carries (positive snapshot or explicit delete tombstone), and writes atomically through the temp+rename storage. It skips the write when the splice leaves the dict unchanged so high-frequency projection ticks do not churn the file. A transient read failure aborts the flush so siblings are not clobbered, while corrupt bytes are rotated aside to layouts.json.corrupt-<timestamp> and persistence recovers, mirroring SidebarPersistenceKey. LayoutsKey.save becomes a no-op so the actor is the sole disk writer.
…h state Layouts were only written on quit, so a second instance launched mid-session pruned tabs the first instance had just opened and terminated their sessions. Drive a per-worktree debounced markLayoutDirty off the settled tab callbacks (create, close, projection drift, removal, rename, selection), capturing the freshest snapshot plus live agent records at fire time and merging off main; prune deletes immediately so a queued save cannot resurrect a removed worktree, and the on-quit synchronous flush stays the terminal write. reapOrphanSessions and the orphan subset of terminateAllSessions now spare any session that still has a client attached (or an unknown count), so a live instance keeps its sessions. A tab rename now routes through WorktreeTerminalState so a custom title persists incrementally instead of only at quit, while the layout-restore path keeps seeding setCustomTitle directly. Also fixes the close-last-surface-via-close_surface path that returned without emitting a projection, leaving the closed surface's session orphaned.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
layouts.jsonwas only written on quit (plus a couple of rare paths), so launching a second instance mid-session (e.g.make run-app) saw a stale snapshot: it pruned tabs the first instance had just opened and terminated their sessions. Two root causes, fixed independently.Attach-aware zmx reaping
zmx ls --shortemits only the session name, so the reaper had no way to tell whether another live instance still had a client attached to a session. We now parse the fulllsformat (name=/clients=) through a pure, unit-testedZmxSessionListParserbehind alistSessionsWithClientsquery that returnsnilon probe failure.reapOrphanSessionsand the orphan subset ofterminateAllSessionsspare any session with a client attached (clients > 0) or an unknown count (nil), so a concurrently-running instance keeps its sessions.layouts.jsonis only a hint, so this fixes the second-instance-kills-first-instance bug regardless of snapshot freshness.Incremental layout persistence
A per-worktree debounced
markLayoutDirtyfires off the settled tab callbacks (create, close, projection drift, removal, rename, selection), captures the freshest snapshot plus live agent records at fire time, updates the in-memory@Shared(.layouts)dict on main, and merges to disk off the main actor.LayoutsIncrementalWriteris a single FIFO actor: it re-readslayouts.json, splices in only the keys a flush carries (positive snapshot or explicit delete tombstone), and writes atomically via temp+rename.LayoutsKey.savebecomes a no-op so the actor is the sole disk writer.layouts.json.corrupt-<timestamp>and persistence recovers, mirroringSidebarPersistenceKey.WorktreeTerminalStateso a custom title persists incrementally instead of only at quit; the layout-restore path keeps seedingsetCustomTitledirectly so hydration does not trigger a redundant save.Fixes along the way
The close-last-surface-via-
close_surfacepath returned without emitting a projection, soonTabRemovednever fired and the closed surface's session was re-orphaned at next launch. It now emits a projection on that branch.Tests
New coverage for the parser (clients/err/arrow/non-supa/malformed lines), attach-aware reaping (spare attached, skip unknown, kill tracked, nil-probe reaps nothing), and incremental persistence (debounce coalesce, delete-beats-queued-save, no-resurrect-after-prune, flush-on-quit, agent-record-survives-mutation, skip-if-unchanged, corrupt-file rotate-and-recover, rename persists). Full suite green.