Repository navigation
fix(sessions): expiry goes through the one session teardown, and stops leaking worktrees - #125
Merged
Merged
Conversation
…s leaking worktrees
SESSION_TTL_DAYS ran a bare `DELETE FROM sessions`. Every expired session's git
worktree and ccs/session-* branch stayed in data/worktrees with no row left to own
it, and its queued_messages (no FK) were restored at every boot. Reported by a
third-party Docker image that had to prune the trees in its own entrypoint.
The three doors that delete sessions (DELETE, bulk delete, expiry) each carried
their own copy, and they had drifted: the single delete never killed the terminal
pane either. teardownSessions(rows, { deleteTasks }) is now the only code that
deletes a session; sessionHasUnmergedWork() is the one gate the doors ask first.
Expiry is unattended, so it skips a running session, a live terminal pane and a
worktree holding unmerged or uncommitted work (an unreadable tree counts as work),
and keeps the chat's Kanban cards (deleteTasks: false; the FK unlinks them).
WM.pruneOrphanWorktrees() reconciles the trees earlier versions leaked: only trees
no sessions/tasks/task_chains row owns, older than an hour, clean and merged, and
removed without --force. It checks for data/worktrees before probing git, so an
install with no worktrees spawns nothing at boot.
test/session-expiry.test.js boots a real server twice against one APP_DIR and ages
sessions in SQLite between the boots; reverting expiry to the bare DELETE fails 6
of its 17 checks.
…etter case Found by an adversarial review of the expiry teardown: an expiring chain session could take a live chain's tree (task_chains was not counted as a holder), and on a case-insensitive disk a row stored under a differently-cased path read as an orphan. Both are pinned in test/session-expiry.test.js and fail when reverted.
Lexus2016
added a commit
that referenced
this pull request
Oct 3, 2026
Lexus2016
added a commit
that referenced
this pull request
Oct 7, 2026
… unknown hasUnmergedWork() (hardened in the previous commit to fail closed) also failed closed on a unit whose directory AND branch were both gone. That is the normal end state of an auto-merged task or chain: removeWorktree() deletes both, while the session row keeps git_root/workdir/git_branch — nothing clears them. Such sessions read as holding unmerged work, so expiry kept them forever (the #125 leak, now for merged sessions) and a manual delete asked for a confirmation about nothing. When the directory is gone, ask git whether the branch still exists. for-each-ref answers 'absent' with an empty success, so a git FAILURE still throws and still reads as unknown — an unreadable project keeps its protection. Real unmerged commits on a branch whose directory vanished are still reported.
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.
SESSION_TTL_DAYS ran a bare
DELETE FROM sessions. Every expired session's gitworktree and ccs/session-* branch stayed in data/worktrees with no row left to own
it, and its queued_messages (no FK) were restored at every boot. Reported by a
third-party Docker image that had to prune the trees in its own entrypoint.
The three doors that delete sessions (DELETE, bulk delete, expiry) each carried
their own copy, and they had drifted: the single delete never killed the terminal
pane either. teardownSessions(rows, { deleteTasks }) is now the only code that
deletes a session; sessionHasUnmergedWork() is the one gate the doors ask first.
Expiry is unattended, so it skips a running session, a live terminal pane and a
worktree holding unmerged or uncommitted work (an unreadable tree counts as work),
and keeps the chat's Kanban cards (deleteTasks: false; the FK unlinks them).
WM.pruneOrphanWorktrees() reconciles the trees earlier versions leaked: only trees
no sessions/tasks/task_chains row owns, older than an hour, clean and merged, and
removed without --force. It checks for data/worktrees before probing git, so an
install with no worktrees spawns nothing at boot.
test/session-expiry.test.js boots a real server twice against one APP_DIR and ages
sessions in SQLite between the boots; reverting expiry to the bare DELETE fails 6
of its 17 checks.