Skip to content

feat(server): clean up idle jj workspaces - #20755

Draft
THEjacob1000 wants to merge 8 commits into
pingdotgg:mainfrom
THEjacob1000:feat/jj-cleanup
Draft

THEjacob1000 wants to merge 8 commits into
pingdotgg:mainfrom
THEjacob1000:feat/jj-cleanup

Conversation

@THEjacob1000

@THEjacob1000 THEjacob1000 commented Oct 17, 2026 •

Copy link
Copy Markdown

Problem

Git worktree cleanup can't safely remove jj workspaces. This adds the jj side. It's auxiliary: nothing else in the stack depends on it, so it can be deferred until the rest has merged.

Change

JjStorageCleanup only considers registered secondary workspaces with the expected managed bookmark, and never infers ownership from a path or touches the main workspace. Its fetch goes into private refs/t3/remotes/ like every other background fetch, so local main never moves. A workspace is kept, with the reason reported, if it has edits, unsnapshotted, oversized or ignored files, a description on @, conflicts, or commits not on a bookmark. Removal re-checks the files on disk at the last moment and restores the workspace if anything changed. Repo-wide prune and removal share a lock, so a concurrent prune can't forget a workspace mid-removal. The .jj keep guard stays for any repo not detected as jj, including colocated repos that fell back to Git.

All three of @omarshaarawi's items are fixed, each with a real jj test: the fetch moving main, removal deleting large files, and the dropped .jj guard.

Scope and approval

Approval is pending in #18468. Commits 1 to 6 are dormant on their own, #20756 is the one that turns jj on, and #20755 (idle workspace cleanup) sits on top so it can wait until the rest lands. Tracks #1126, the originals #11373, #11382, #11383, #11384, and ports #13816, #16380. @gergesh's port was a reference.

GitHub can't create stacked PRs from a fork, so the diff carries the PRs below. Review the top commit only.

Verification

Automatic startup cleanup removed an idle managed workspace, while unsnapshotted, 20 MiB and described ones were kept with their reasons. The cleanup fetch advanced only the private ref, and a T3 Git worktree with jj git init --colocate in it was kept.

Screenshots are from the integrated stack before the latest rebase onto current main. After rebasing, the stack was re-verified with the automated tests and the process comparison; the manual pass wasn't repeated.

Managed workspace and cleanup evidence
Scenario Result
Unsnapshotted file caused retention Unsnapshotted workspace retained
Oversized file caused retention Workspace with oversized file retained
Working-copy description caused retention Workspace with description retained
Idle managed workspace removed Idle managed workspace cleaned
Genuine managed Git worktree with jj retained Managed Git worktree with jj retained
Cleanup fetch ref observations
Ref Before / root value After cleanup fetch
refs/heads/main cca316e82517f0b164b828bc228c4d507c313c53 unchanged cca316e82517f0b164b828bc228c4d507c313c53
refs/remotes/origin/main cca316e82517f0b164b828bc228c4d507c313c53 unchanged cca316e82517f0b164b828bc228c4d507c313c53
refs/t3/remotes/origin/main stale before fetch advanced to 898a27a19f7395345e60958b81ec0b470e6440d7

@omarshaarawi

Copy link
Copy Markdown

I'd take this out of the stack and follow up later.

  • Cleanup runs plain jj git fetch --bookmark exact:"main", which fast-forwards the user's local main, or conflicts it if they have local commits (JjStorageCleanup.ts:172-182). Every other background fetch writes only to refs/t3/remotes/.
  • jj workspace remove never refuses. Files over snapshot.max-new-file-size get deleted, and late edits become an unreported orphan.
  • It removes the .jj -> keep guard in storageCleanup.ts:661-671 while detection is still off. A T3 git worktree where the user ran jj git init --colocate now gets git worktree removed with its .jj/. Reproduced.

The ownership and stranded-ancestry checks are stricter than the git side, which is good.

@THEjacob1000
THEjacob1000 force-pushed the feat/jj-cleanup branch 2 times, most recently from 5e02f3c to cbc2b11 Compare October 19, 2026 18:00
THEjacob1000 and others added 8 commits October 20, 2026 06:45
Co-authored-by: Лев Федорец <152093961+knocking4thcylinder@users.noreply.github.com>
Co-authored-by: Лев Федорец <152093961+knocking4thcylinder@users.noreply.github.com>
Co-authored-by: Omar Shaarawi <16183117+omarshaarawi@users.noreply.github.com>
…vcs kind

Co-authored-by: Лев Федорец <152093961+knocking4thcylinder@users.noreply.github.com>
Co-authored-by: Лев Федорец <152093961+knocking4thcylinder@users.noreply.github.com>
…cs actions

Co-authored-by: Лев Федорец <152093961+knocking4thcylinder@users.noreply.github.com>
Co-authored-by: Omar Shaarawi <16183117+omarshaarawi@users.noreply.github.com>
@omarshaarawi

Copy link
Copy Markdown

Rechecked at 244a503 with jj 0.46. The fetch and removal fixes hold. I ran the exact git fetch from a secondary workspace with local main diverged and with it behind, and main, main@origin and refs/remotes/origin/main didn't move. A late 2 MB untracked file, a late edit, and a late description all block removal now. Remote names with /, ., % and non-ASCII all resolve.

One new major:

  • The hourly pass can rewrite a stale workspace's files (JjStorageCleanup.ts:112-116 with JjProcess.ts:145-154). The cleanup's snapshotting jj status goes through the stale retry. I reproduced it on 0.46:

    1. A thread workspace has unsnapshotted edits (an edited a.txt and a new n.txt).
    2. The user runs jj rebase -s ws@ -d B from the main workspace.
    3. The cleanup's jj status gets "working copy is stale", and JjProcess runs jj workspace update-stale.

    n.txt disappears from disk and both edits end up in a divergent change nobody is told about. Cleanup then sees M a.txt and keeps the workspace, so nothing gets deleted, but a background job has still changed the user's files. Cleanup should opt out of the stale retry and keep the workspace with "workspace is stale". The status poller has the same path, but only while someone has the thread open.

Minor:

  • On jj, keepWhen: "tracked-changes" behaves like uncommitted-changes (:49). Any untracked file blocks removal, where the git branch runs git clean -ffdx first (storageCleanup.ts:905-908). So a jj workspace with one stray file is never cleaned. Either clear it in the held dir before forget, or document the difference.
  • primaryRemote is passed as a positional argument at :200 (and at JjStatus.ts:486). Git allows a remote named -x, which fetch would read as an option. Add -- before it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants