Skip to content

Safe cleanup wall-clock budget is discarded on the dry-run path (10s budget produces a 104s run) #1280

Description

@chubes4

Problem

workspace cleanup safe --dry-run ignores its wall-clock budget. An explicit
10-second budget produces a ~104-second run:

$ time studio wp datamachine-code workspace cleanup safe --dry-run --until-budget=10s
safe dry-run with 10s budget: 103866 ms

WP-CLI/Studio bootstrap is not the cause — a trivial wp option get siteurl
on the same install costs 4.4s.

WorkspaceSafeCleanupOrchestrator::DEFAULT_BUDGET is 45s, so even the
unspecified default is overrun by ~2.7x.

Root cause

The budget is computed and plumbed, then dropped on the only stage the safe
dry-run actually runs.

WorkspaceCleanupPlan::workspace_cleanup_plan() resolves until_budget
(WorkspaceCleanupPlan.php:48) and forwards it to the worktree branch
only:

// WorkspaceCleanupPlan.php:96 — worktree branch receives the budget
$worktree_args['until_budget'] = $inputs['until_budget'];

The artifact branch never receives it (WorkspaceCleanupPlan.php:61-73):

$artifact_plan = $this->worktree_cleanup_artifacts( array(
    'dry_run'        => true,
    'force'          => $inputs['force_artifact_cleanup'],
    'full_workspace' => $inputs['full_workspace'],
    'limit'          => $inputs['limit'],
    'offset'         => $inputs['offset'],
    'sort'           => $inputs['artifact_sort'],
    'older_than'     => $inputs['worktree_older_than'],
    // no 'until_budget'
) );

WorkspaceArtifactCleanup::worktree_cleanup_artifacts() has no wall-clock
budget parameter at all — its documented $opts are dry_run, force, allow_active_artifact_cleanup, allow_unavailable_process_probe, apply_plan, limit, offset, exhaustive, safety_probes, older_than, only_handles. It is
bounded only by limit.

The safe dry-run then disables the one branch that honors the budget.
WorkspaceSafeCleanupOrchestrator.php:167-173:

$artifact_input = $dry_run ? array(
    'mode'              => 'artifacts',
    'include_artifacts' => true,
    'include_worktrees' => false,   // the budgeted branch is off
    'limit'             => $limit,
) : ...

So on the safe dry-run path, 100% of the resolved budget is discarded. The
orchestrator's own pre-stage checks (budget_partial_result()) still work, but
they only gate between stages — they cannot bound a stage that runs long.

Impact

  • --until-budget is inert on the documented safe-cleanup preview path.
  • The operation cannot be used as a bounded, resumable preview as designed.
  • Callers that budget around it are terminated mid-inventory. Homeboy's
    worktree-provider cleanup delegates here and reports
    inventory_completeness: partial with 0 reclaimable bytes on every run,
    while a direct DMC cleanup on the same workspace reclaimed ~147 GiB across
    93 worktrees.

Measured stage costs

Timed individually against this workspace (~300 worktrees):

stage budget wall clock
worktree locks --prune-stale --dry-run n/a 2.9s
bounded-cleanup-eligible-apply --dry-run 5s 3.2s (honored)
cleanup-artifacts --dry-run --limit=25 n/a 3.5s
cleanup safe --dry-run 10s 103.9s

The individually-timed stages are all cheap, so the ~104s is not yet
attributed to a single stage. Note the low-level cleanup plan --mode=artifacts
CLI defaults (limit=100, include_worktrees=true) differ from the
orchestrator's inputs (limit=25, include_worktrees=false), so that command
is not a faithful proxy — per-stage timing evidence from the orchestrator
itself is still needed to attribute the remaining time.

Acceptance criteria

  • worktree_cleanup_artifacts() accepts and enforces a wall-clock budget.
  • workspace_cleanup_plan() forwards until_budget to the artifact branch.
  • cleanup safe --dry-run --until-budget=<d> returns within <d> plus a
    bounded reporting margin, emitting a resumable continuation instead of
    overrunning.
  • Regression coverage asserts a short budget produces a short run and a
    continuation, rather than only asserting the option is accepted.
  • Per-stage elapsed timing is reported in the run evidence so a future overrun
    is attributable without external timing.

AI assistance disclosure: investigated and written with Claude Sonnet 4.5 (Anthropic) driving OpenCode via the Kimaki Discord bridge. The model ran the timings above and traced the budget plumbing through the source; a human directed the work. An earlier hypothesis that sort=size forced a full-workspace scan was tested and disproven (3.5s), and is recorded here so it is not retried.

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