Files
kandev/docs/decisions/2026-08-30-compact-integrated-managed-branches.md
T
2026-09-16 23:20:37 +01:00

6.0 KiB

ADR-2026-08-30-compact-integrated-managed-branches: Compact Fully Integrated Managed Branches

Status: accepted Date: 2026-08-30 Area: backend, operations

Context

Task lifecycle cleanup deliberately removed Git worktree registrations while retaining every local branch ref. That protected unpublished commits and made archive recovery possible, but no bounded automatic consumer reclaimed managed branches after their commits were fully integrated. The result was permanent growth of redundant local refs.

A branch name or task-shaped suffix is not proof of ownership. A branch can be user-selected, shared with another environment, checked out in another Git worktree, or contain commits absent from the intended base. Archive recovery also cannot depend on a remote branch: unpublished state must restore exactly, and a safely removed integrated branch must remain locally recoverable.

Decision

Capture four internal fields on each task_environment_repos worktree row:

  • branch owner: kandev, external, or unknown;
  • intended integration ref, resolved from the task branch policy target before falling back to the task repository base branch;
  • exact recovery head SHA, initially empty.
  • branch-compaction completion timestamp, initially empty.

Legacy and incomplete rows default to unknown and empty refs. They are retained without inference.

At terminal worktree cleanup, after removing the worktree registration and while holding the existing repository lock, consider exactly one explicit local branch. Delete it only when all of these conditions hold:

  1. The persisted owner is kandev and exactly one durable row claims the repository/branch pair.
  2. No live Git worktree registration uses refs/heads/<branch>.
  3. The branch is not its base or integration ref and both exact commit refs resolve locally without fetching.
  4. git merge-base --is-ancestor <branch-head> <integration-head> succeeds.
  5. The exact branch head is persisted with compare-and-set, then an opaque worktree-identity-derived local recovery ref is created at that head before deletion. The recovery ref keeps the commit reachable if the integration ref is rewritten or Git prunes unreachable objects.
  6. git update-ref -d refs/heads/<branch> <expected-head-sha> atomically removes the single explicit local ref only if its head is unchanged.
  7. A post-delete liveness probe restores the exact ref with zero-OID compare-and-set if a Git worktree appeared during deletion; only a completed deletion records the separate completion timestamp.

Any missing metadata, ambiguous owner, active/shared reference, protected ref, failed probe, unique commit, compare-and-set loss, head race, or refused non-force deletion retains the branch. Remote refs are never deletion targets. Permanent task deletion keeps its separate explicit force-delete disposition.

Unarchive and recreate use a retained local branch directly. When safe compaction removed the local ref, they recreate it from the persisted exact head before attempting remote or pull-request recovery. Any existing same-named local ref must resolve to that exact head; a mismatch fails closed. Once the exact local branch exists, recovery clears the compaction marker and removes the opaque recovery ref, allowing a later archive without a session launch to be considered again.

All terminal paths share the manager policy: single-repository cleanup, multi-repository cleanup, task archive, task-environment reset, handoff cleanup, and aged automation cleanup. The task cleanup service delegates worktree removal to its batch cleaner exactly once. Aggregate attempted/deleted/retained receipts, fixed retained-reason counts, and matching expvar counters provide bounded observability without branch lists or repository data.

Archive cleanup is the first attempt, not the only attempt. A storage-maintenance provider selects at most 100 Kandev-owned rows whose tasks remain archived and whose worktrees are inactive and deleted. It delegates every selected row to the same manager policy and contains no branch-name scan or deletion logic. The manager conditionally persists the recovery head while the task is archived, rechecks archived state immediately before ref mutation, and rotates retained candidates by durable update time so bounded runs make progress. A safely compacted row keeps its recovery head, records its completion timestamp, and is no longer selected. A crash after recovery persistence but before deletion leaves the timestamp empty, so a later bounded pass revalidates and retries it. Rematerializing the worktree clears the completion timestamp.

Consequences

  • Fully integrated Kandev-created local branches no longer accumulate without bound after terminal cleanup.
  • Unpublished, external, legacy, protected, shared, inherited, and ambiguous branches remain fail-closed.
  • Archive/unarchive preserves exact state whether the branch ref was retained or safely compacted.
  • Branches archived before integration receive a bounded later retry without an install-wide Git ref scan.
  • New additive columns must survive SQLite and PostgreSQL ownership migrations and environment-repository CRUD.
  • Expected-SHA deletion conservatively retains a branch advanced after ancestry proof. Safety takes precedence over reclamation rate.

Alternatives Considered

  1. Set every cleanup call to removeBranch=true. Rejected because the existing force deletion discards unpublished work and makes some archived branches unrecoverable.
  2. Delete branches matching the generated naming pattern. Rejected because names do not prove ownership, ancestry, liveness, or task-environment identity.
  3. Run an install-wide periodic branch glob collector. Rejected because it loses the lifecycle metadata and repository lock already available at terminal cleanup, broadens the deletion surface, and creates duplicate attempts.
  4. Keep every local branch indefinitely. Rejected because redundant managed refs accumulate permanently after their commits are integrated.