Skip to content

Auto-clean demux state on pane/window/session close#54

Merged
rtalexk merged 10 commits into
mainfrom
feat/state-cleanup-on-close
Jun 1, 2026
Merged

Auto-clean demux state on pane/window/session close#54
rtalexk merged 10 commits into
mainfrom
feat/state-cleanup-on-close

Conversation

@rtalexk

@rtalexk rtalexk commented May 29, 2026

Copy link
Copy Markdown
Owner

Context

When a tmux pane is killed, demux automatically clears its tool-state so the TUI doesn't show stale rows. However, killing a window or session left orphaned state rows until a manual demux gc states run. This gap means users could see stale "working" or "error" states for windows/sessions they closed hours ago.

This PR extends the auto-cleanup to windows and sessions, cascading to their descendant panes, with a GC-sweep fallback. Panes already covered by the existing pane_closed hook are now backstopped by the cascade, improving robustness against tmux edge cases.

What does this PR do

Database layer

  • Adds StateDeleteByWindow(windowID) to cascade-delete a window's own state row and all pane rows belonging to that window (identified by window_id column).
  • Reuses existing StateDeleteBySession which cascades across all rows for a session (panes, windows, session).

Event handlers

  • Extracts sweepOrphanStates(d *db.DB) (deleted int64, skipped bool, err error) from runGCStates into a reusable helper. The sweep is indirected via var sweepOrphans for test stubbing.
  • Adds applyWindowClosed and applySessionClosed handlers (best-effort: log errors, always return nil so tmux hooks stay quiet). Each does a targeted cascade delete by hook ID, then sweeps orphans as a catch-all. Lock-free because state-only (serialized at SQLite).
  • Registers window_closed and session_closed cobra subcommands with --window and --session flags.

Tmux integration

  • Registers window-unlinked and session-closed hooks in DesiredHooks(), firing demux event window_closed and demux event session_closed respectively. Both backgrounded (run-shell -b) so never block the tmux command queue.
  • Hook set changes trigger auto-reconcile on next client attach (HooksHash changes).

Documentation

  • README: extended the "Hooks" section to mention automatic cleanup on pane/window/session close, with cascade semantics. Added quick-reference lines for the new event commands.
  • Code comments: document why the handlers skip withEventLock (state-only, no sticky mutation), and note the linked-window limitation (low-impact, acceptable, self-healing).

Out of scope

  • Sticky sidebar interaction: sidebar sweeps already handle windows left behind by pane kills; window/session close inherits those same sweeps.
  • Migration or schema change: state rows use pre-existing columns; no DB schema change.
  • New config field: hooks are auto-managed; no configuration user choice needed.

Manual QA

Feature adds automatic tmux hooks that fire on window/session close. Core logic (cascade delete, sweep fallback) is covered by unit tests (TestStateDeleteByWindow, TestApplyWindowClosed, TestApplySessionClosed). Manual verification confirms the e2e integration.

Prerequisites

1. demux binary is built and hooks are registered.
cd /Users/rtalex/com.github/demux/state-cleanup
go build -o /tmp/demux-qa .
/tmp/demux-qa hooks install --tool tmux

Expect output: "Registered demux hooks in the live tmux server" or "Updated ~/.tmux.conf".

Verify the hooks were installed:

tmux show-hooks -g | grep -E 'window-unlinked|session-closed'

Expect two lines (exact command strings).

Scenario 1 — Window close clears descendant panes

  1. In a tmux session, create a new window and a pane in it.
  2. Set state on the window and its pane (simulating a running tool):
    /tmp/demux-qa state set --target-id @5 --state working --tool claude
    /tmp/demux-qa state set --target-id %10 --state working --tool claude
    /tmp/demux-qa state ls --format text
    Expect: both rows present in the output.
  3. Kill the window:
    tmux kill-window -t @5
  4. Verify state is gone (no row for @5 or %10):
    /tmp/demux-qa state ls --format text
    Expect: neither row appears (rows were deleted by the window-unlinked hook).

Scenario 2 — Session close clears all windows and panes

  1. Create a new session with a couple of windows and panes.
  2. Set state on the session, a window, and panes:
    /tmp/demux-qa state set --target-id '$2' --state working --tool test
    /tmp/demux-qa state set --target-id '@7' --state working --tool test
    /tmp/demux-qa state set --target-id '%20' --state working --tool test
    /tmp/demux-qa state ls --format text
    Expect: three rows (session, window, pane).
  3. Kill the session:
    tmux kill-session -t 2
  4. Verify all three are gone:
    /tmp/demux-qa state ls --format text
    Expect: none of the three rows appear (session-closed hook deleted them).

Scenario 3 — Existing pane close still works

  1. Set state on a pane:
    /tmp/demux-qa state set --target-id '%1' --state working --tool test
  2. Kill the pane:
    tmux kill-pane -t %1
  3. Verify the pane state is gone:
    /tmp/demux-qa state ls --format text
    Expect: no row for %1 (existing after-kill-pane hook still works).

rtalexk added 10 commits May 29, 2026 16:04
Add documentation to applyWindowClosed clarifying that it does not wrap
withEventLock because it performs only DB state operations, similar to
applyPaneFocus. State writes serialize at the SQLite layer, unlike
pane_closed which must serialize tmux mutations.

Add TestApplySessionClosed_EmptyIDStillSweeps to verify that the orphan
sweep runs even when session ID is empty.
Extract applyClosedCleanup() from duplicate window_closed and session_closed
handlers. Both had identical debug/delete/sweep/return-nil logic, differing
only in event name, ID key, and delete function.

Add stubSweep(t, fn) test helper using t.Cleanup to auto-restore
sweepOrphans. Replaces 4x repeated save/defer pairs in tests, improving
readability and reducing cleanup boilerplate.

Changes behavior: None. All 822 tests pass.
@rtalexk rtalexk marked this pull request as ready for review May 30, 2026 22:08
@rtalexk rtalexk merged commit 805169a into main Jun 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant