Skip to content

🤖 feat: cross-process quiescence for renaming and deleting sub-agent task checkouts #4476

Description

@ThomasK33

Summary

Let sub-agent task checkouts be renamed and deleted safely while several Xum processes share one XUM_ROOT. This is the missing prerequisite for the paused checkout-preparation stack (#4364 and its stack). Maintainer requirements: renames must work, and sub-agents must be cleaned up on archive and delete.

Why it is needed

The preparation stack refused every structural change to a task checkout: rename, remove, archive-delete/snapshot, restore. Two problems motivated that:

  1. Late clean-up hits a successor. One process rotated a task's attempt in config after another process's owner check but before its deleteWorkspace. The late delete destroyed the successor's checkout and session.
  2. Work may still be running elsewhere. Local "finished" state cannot prove that another cooperating process has no admitted work, tool child or MCP server still running in the checkout.

Required sequence

  1. Close new admission for the footprint.
  2. Establish quiescence, or return a truthful "busy" refusal. Never wait while holding a lock that completion or use-release needs.
  3. Revalidate exclusively and mutate.
  4. Reopen admission against the resulting state.

Proposed shape (design pass, 2026-09-25; not implemented)

  • Use records per footprint key (canonical checkout realpath), under XUM_ROOT/checkout-uses/<key>/: {useId, workspaceId, attemptId?, kind, owner: process identity, groups: [{pgid, leader birth}]}.
  • Mutation gate (gate.json): write the gate, then scan uses. Admission writes its use, then reads the gate, so one side always sees the other.
  • Busy refusal: a live owner, or a dead owner with a live group, means busy. Name the kind in the error, for example "MCP server in pid N".
  • Attempt-bound mutation: re-read the row with compare-and-swap on taskAttemptId and materializationId captured at decision time. Check the nonce before deleting. 🤖 feat(tasks): fence sub-agent attempts before durable workflow recovery #4308's auto-clean-up of finished leaves must bind expectedAttemptId.
  • Rename and preparation proof: capture identity and nonce before git worktree move. Re-capture after it: dev/ino, the .git pointer and the nonce must be unchanged, with only the path changed. Publish a re-signed proof in the same config edit as the name/path change. This amends the contract's "proof is immutable" rule for that one transition.
  • Crash recovery:
    • Delete: a pendingRemoval {removalId, materializationId, owner} marker set by compare-and-swap before deleting.
    • Rename: an intent journal {from, to, materializationId, phase, owner}. Roll back if only the old path exists; roll forward if only the new path exists with a matching nonce; otherwise refuse.

Estimate: about 1.2k production and 2–2.5k test lines, including multi-process SIGKILL harnesses. A smaller variant (about 600 lines, no journal) leaves the gate after an interrupted operation and refuses with an index.lock-style recovery message. That brings back a lockout, which the maintainer rejected.

Open design gaps (must be solved before implementation)

  • Spawn-to-record crash window: a child can start before its pgid is recorded. It needs a pre-exec handshake (for example sh -c 'read _ && exec …' released only after the record is durable).
  • Lifetimes that can't be proven: container-side processes of devcontainer execs, and escaped daemons (setsid, double fork). These must be declared out of contract explicitly, or covered some other way.
  • Linearization proof for admission versus mutation across publication, incomplete records, aliases and rename.
  • Clean-up versus successor creation: every relevant writer must take part in exclusion through the destructive effect.
  • Idle foreign MCP servers: they would make an idle workspace "busy". The owning process could stop its own servers first. Today stopServers runs after the checkout is deleted (workspaceService.ts remove path).

Delivery

  • Registry, gate, busy refusal and attempt-bound removal are useful on main on their own (see the linked issue on main's rename/delete gaps). They could land first.
  • The proof re-sign has to land with the preparation stack.
  • The stack then swaps its blanket refusal (workspaceStructuralMutationGuard.ts and its call sites) for the gate.

References


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions