Skip to content

worktree: a lane that hosted shared-workspace subagents can never be reaped on demand #1610

Description

@obra

What happened

After PR #1604 merged (squash), its worktree lane could not be reaped. The lane (mobile-repo-line) was clean, fully landed, its remote branch deleted, nothing running — but manage_worktree remove --force refused:

manage_worktree remove: live work under <lane>: 034Q0FmS… (subagent, retained — idle), 034Q0Fpx2… (subagent, retained — idle), 034Q0FtI… (subagent, retained — idle), 034Q0FwG… (subagent, retained — idle)

The four blockers were the simplify-code review delegates: read-only, shared-workspace subagents (no isolation="worktree"), completed, and stopped via job_stop. Their retained sessions list the lane as their working directory, and that is the whole blocker.

Repro

  1. A root session creates a managed lane (manage_worktree create) and enters it.
  2. It spawns shared-workspace delegates (the default) whose cwd is the lane; they complete their task and go retained-idle. job_stop them — the stop reports already_idle · stopped_by_parent, and the sessions stay retained (resumable terminal history).
  3. Exit the lane; land its work (e.g. squash-merge a PR and delete the remote branch), so the lane is clean and merged.
  4. manage_worktree remove <name> --delete-branch --force refuses on live work, with no dispose hint and no force cascade.

Root cause

The refusal is correct per the guard's design; the dead-end is in the cascade's coverage:

  • liveWorkUnder (agent/session_tools_worktree.go, ~L885) labels stopped shared-workspace children (subagent, retained — idle) — deliberately honest, so a model removing a clean merged lane gets a signal it can act on.
  • retainedIdleDelegateIDs (~L1010) resolves those blockers to disposable delegates through delegateController.ownedStableWorktreeSnapshots — a map that only worktree-isolated delegates appear in. A shared-workspace child has no stable worktree descriptor, so resolution fails closed (ok=false).
  • Both the dispose hint (disposeHintForRetainedIdle, ~L1043, returns "" when resolution fails → generic refusal) and remove's force cascade (~L2255: forceCascadeDlgIDs is only recorded when retainedIdleDelegateIDs(live) succeeds) therefore never arm for shared-workspace children.

So the "legitimate way forward" the retained-idle label exists to provide (the comment at ~L970: labeling such a child "running" would "dead-end a model trying to remove a clean, merged lane") does not exist for this configuration. The label was fixed; the shared-workspace case still dead-ends.

What does not work (verified this session)

  • job_stop does not de-retain: an idle delegate's stop is already_idle, and retained means resumable history, by design. No exposed tool deletes retained session state; doctor_evener is read-only.
  • Admission-time reclamation (max_retained_terminal) is capacity-driven only. A minimal probe delegate admission did not trigger it; the four subtrees stayed retained.
  • manage_worktree dispose only retires a delegate's own isolation lane; shared-workspace children have none.

The lane stays unreapable until capacity-driven reclamation happens to remove those quiescent retained subtrees, after which remove/prune succeeds. The lane is inert and harmless in the meantime — this is a hygiene dead-end, not a correctness bug.

Fix direction

The guard's fail-closed posture is right for other sessions' lanes and foreign children. The gap is the caller's own lane hosting the caller's own shared-workspace children. Suggested: let the force cascade resolve a blocker that is

  • owned by the calling session,
  • retained-idle (not running or driving),
  • shared-workspace, and
  • whose recorded working directory is under the lane being removed,

and dispose it through the same sanctioned cascade (the child holds no live work; its lane is the target itself, owned by the caller). Everything else — running shells, driving children, other sessions' lanes — keeps failing closed exactly as today.

Two side observations from the same session, no action implied:

  • manage_worktree's repeated-identical-call fuse blocked the third identical remove attempt and forced an approach change — that worked as intended.
  • git merge on this host needs an explicit --no-ff (merge.ff=only is set repo-wide); worth knowing when bringing PR branches up to date under strict branch protection.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Priority 2 (carried from kata; 0 = highest)complexity/SSmall: one file, localized, no new abstractiondelegateDelegate/subagent lifecycleneeds-decisionRequires a product, design or process ruling before work can startneeds-designRequires a design or architecture ruling before work can start

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions