feat(aos): give the session shadow a lifecycle, and reap a squash-landed branch (#1260) #1272
No reviewers
Labels
No labels
burndown-2026-06
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/devrel
role/eval
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/sysadmin
role/tpm
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agentic-os!1272
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/claude/mu55-shadow-reap"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #1260. Tier 1 of the #1245 dispatch, and the one item that repo owns rather than aterm.
First, a correction to the issue's diagnosis
The issue says "There is no list, no release, no reap. A session cannot even declare itself finished." Two thirds of that is right.
cleanDeadNativeSessions,cleanNativeArtifact, andrunNativeWorkspaceSweepalready exist and already work: the registered worktree count is down from the 195 the issue measured to 72, with none prunable. What was missing is a way to see the state, a way to declare a session finished, and a way to run the sweep without launching another session.The branch half is worse than the issue described, and has a specific cause.
The branch leak, diagnosed
deleteNativeBranchIfRemoteaskedgit rev-list <branch> --not --remotes=origin. That is exactly right until the forge squash-merges and deletes the remote branch, which this one does (default_merge_style: squash,default_delete_branch_after_merge: true). After that the branch's own commits are on no remote ref and never will be, so the answer is "unpushed" forever:Patch identity does not rescue it either.
aos/claude/mu55-dryrun-exitsfrom earlier in this batch had 2 commits squashed into 1, andgit cherry origin/mainreports both as unmerged.Content does. A branch is spent when it changes nothing the default branch lacks:
Three gates keep it conservative, each with a test:
rev-listanswer is tried first and this is only the fallbackSession branches and the ID ledger
reapNativeMergedBranchesskipped everyaos/branch, with the comment that reaping one would hand out an ID still in use. That is the mechanism by which they never went. Narrowed rather than removed: a session branch is skipped while a lease or a worktree still names it, which is what "in use" actually means.nativeLiveWorktreesnow records branches alongside paths, and answers yes when uncertain.Two tests changed to match, and both changes are behavior revisions worth reading:
TestNativeSweepReapsFullyPushedBranchesOnlynow asserts a leased session branch survives and an unleased landed one does notTestNativeLaunchRetriesOccupiedShortSessionIDspushes its collision marker, because a pushed ref is what makes an ID taken now. A bare local branch nothing leases no longer is.Three verbs
--releasemarks the lease and never tears down a process that may still be running. The worktree goes on the next sweep, and a released lease skips the 24-hour grace, which exists only because a crash and a clean exit look identical from outside.The measurement, and a correction to my own first reading
Live on this host,
--list --json:My first version of the report called 52 of 70 releasable, because it applied the artifact predicate and ignored the grace. That is a dry run that overstates what it would delete, which is worse than no dry run, so the report now runs the same predicate the sweep enforces. The corrected reading is also the more interesting one: the sweep is working, and the 4.6 GB is mostly sessions still inside their grace rather than a permanent leak.
What remains permanent is the 14 held by unpushed commits, which is correct and which the existing stuck-lease reporting already names, plus the branch refs this PR unpins.
Verification
go test -C aos-cli ./...clean,just aos-lintcleanuv run pytest724 passed,pre-commit run --all-filescleannative_branch_landed_test.gobuilds a real squash merge with a deleted remote branch and asserts it reaps, plus three negative controls: never pushed, pushed then abandoned without landing, and landed then re-changed on mainnative_shadow_lifecycle_test.gocovers live, in-grace, past-grace, released, and unpushed-held sessions, and that release is idempotent and refuses an unknown id--listand--reap --dry-runexercised against the real host state above. The destructive--reapwas not run here; that is an operator action.Two things, both about shadows that only ever accumulate. A session branch is deleted once its commits exist somewhere else, which `rev-list <branch> --not --remotes=origin` answered until the forge started squash-merging and deleting the remote branch. After that the branch's own commits are on no remote ref and never will be, so the test read "unpushed" forever. Patch identity cannot see it either, since a two-commit branch squashed into one shows both unmerged under `git cherry`. Content can: the branch is spent when it changes nothing the default branch lacks. Gated on a configured upstream that has since been pruned, so a never-pushed branch stays out of the path, and a path main changed again fails the test and keeps the branch. Session branches are the ID ledger `reserveNativeSession` reads, so the sweep still skips one a lease or a worktree names. That narrows a rule that used to skip every `aos/` branch unconditionally, which is why they never went. Three verbs, since a session could not previously declare itself finished: --list [--json] every lease, its liveness, its worktrees, the commits on no remote, and what holds a session that cannot go --release [<id>] the session saying it is finished. Marks the lease, never tears down a live process, and skips the 24h grace --reap [--dry-run] the sweep, without launching another session to get it Measured on this host before the change: 70 leases, 4 live, 51 inside the grace, 14 held by commits on no remote, 1 releasable now. An earlier reading of this that called 52 releasable was wrong; it ignored the grace, and a dry run that overstates what it deletes is worse than no dry run, so the report now applies the same predicate the sweep enforces. Closes #1260 Co-authored-by: Kai Siren <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Agent-Role: platform