Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ public function run( array $input ): array|\WP_Error {

if ( $this->budget_expired($deadline) ) {
$result['evidence']['budget_exhausted'] = true;
$result['continuation'] = $this->build_budget_continuation($step_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_before_stage');
$result['next_commands'][] = (string) $result['continuation']['next_command'];
break 2;
}

Expand Down Expand Up @@ -182,13 +184,23 @@ public function run( array $input ): array|\WP_Error {
if ( is_wp_error($bounded) ) {
return $bounded;
}
if ( $this->budget_expired($deadline) ) {
$result['evidence']['budget_exhausted'] = true;
$result['continuation'] = $this->build_budget_continuation($default_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_after_bounded_apply');
$result['next_commands'][] = (string) $result['continuation']['next_command'];
break;
}

$removed_or_would = (int) ( $bounded['summary']['removed'] ?? 0 ) + (int) ( $bounded['summary']['would_remove'] ?? 0 );
if ( 0 === $pass_marked && 0 === $removed_or_would ) {
break;
}
}

if ( ! empty($result['continuation']) ) {
return $this->finalize_result($result, $apply, $force, $limit, $passes, $until_budget, $started_at, $active_no_signal_drain);
}

if ( $apply ) {
$prune = $this->execute_ability($abilities['prune'], array());
if ( is_wp_error($prune) ) {
Expand Down Expand Up @@ -507,8 +519,7 @@ private function run_bounded_apply( object $ability, array &$result, bool $apply
private function build_continuation( string $stage, array $step, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain = false ): array {
$pagination = (array) ( $step['pagination'] ?? $step['continuation'] ?? array() );
$next_offset = isset($pagination['next_offset']) ? max(0, (int) $pagination['next_offset']) : 0;
$operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned';
$command = sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $next_offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : '');
$command = $this->build_continuation_command($stage, $next_offset, $limit, $passes, $force, $until_budget, $active_no_signal_drain);
$current = (int) ( $pagination['offset'] ?? 0 );
$mutated = (int) ( $step['summary']['written'] ?? 0 ) + (int) ( $step['summary']['removed'] ?? 0 );
$restart = ! empty($pagination['partial']) && $next_offset <= $current && $mutated > 0;
Expand All @@ -527,6 +538,22 @@ private function build_continuation( string $stage, array $step, int $limit, int
return $continuation;
}

private function build_budget_continuation( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain, string $reason ): array {
return array(
'stage' => $stage,
'offset' => max(0, $offset),
'next_command' => $this->build_continuation_command($stage, max(0, $offset), $limit, $passes, $force, $until_budget, $active_no_signal_drain),
'reason' => $reason,
'budget_exhausted' => true,
'hint' => 'Budget expired after safe progress. Re-run next_command to continue the drain from the next safe boundary.',
);
}

private function build_continuation_command( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain ): string {
$operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned';
return sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : '');
}

private function drain_pages( object $ability, array $base_input, bool $apply, ?float $deadline = null ): array|\WP_Error {
$pages = array();
$summary = array();
Expand Down
53 changes: 53 additions & 0 deletions tests/smoke-abandoned-cleanup-orchestrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,57 @@ static function () use ( &$clock_index, $clock_values ): float {
abandoned_cleanup_assert(! empty($restart_result['continuation']['candidate_set_changed_restart_required']), 'restart continuation exposes candidate set changed evidence');
abandoned_cleanup_assert('active-no-signal-drain' === explode(' ', (string) $restart_result['continuation']['next_command'])[5], 'restart next command uses active/no-signal drain');

$stage_budget_abilities = array(
'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => new AbandonedCleanupFakeAbility('finalized', array( 'inspected' => 1, 'written' => 1 ), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility('active_no_signal_report', array( 'total_active_no_signal' => 0, 'inspected' => 0, 'by_suggested_action' => array() ), array( 'complete' => true, 'total' => 0 )),
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )),
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
);
$clock_index = 0;
$clock_values = array( 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1002.0, 1002.0 );
$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator(
static fn( string $name ) => $stage_budget_abilities[ $name ] ?? null,
static function () use ( &$clock_index, $clock_values ): float {
$value = $clock_values[ min($clock_index, count($clock_values) - 1) ];
++$clock_index;
return $value;
}
);
$stage_budget_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 1, 'until_budget' => '1s' ));
abandoned_cleanup_assert(! is_wp_error($stage_budget_result), 'active/no-signal stage budget result succeeds');
abandoned_cleanup_assert('budget_exhausted_before_stage' === $stage_budget_result['continuation']['reason'], 'stage budget continuation explains boundary');
abandoned_cleanup_assert('equivalent-clean' === $stage_budget_result['continuation']['stage'], 'stage budget continuation resumes at next safe stage');
abandoned_cleanup_assert(0 === count($stage_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'stage budget continuation skips prune after budget exhaustion');
abandoned_cleanup_assert(str_contains((string) $stage_budget_result['continuation']['next_command'], '--stage=equivalent-clean --offset=0'), 'stage budget continuation command resumes exact safe boundary');

$bounded_budget_abilities = array(
'datamachine-code/workspace-worktree-reconcile-metadata' => new AbandonedCleanupFakeAbility('reconcile_metadata', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-finalized-apply' => new AbandonedCleanupFakeAbility('finalized', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility('active_no_signal_report', array( 'total_active_no_signal' => 0, 'inspected' => 0, 'by_suggested_action' => array() ), array( 'complete' => true, 'total' => 0 )),
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 1, 'removed' => 1 ), array( 'complete' => true )),
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
);
$clock_index = 0;
$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator(
static fn( string $name ) => $bounded_budget_abilities[ $name ] ?? null,
static function () use ( &$clock_index ): float {
$value = $clock_index < 14 ? 1000.0 : 1002.0;
++$clock_index;
return $value;
}
);
$bounded_budget_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'limit' => 10, 'passes' => 2, 'until_budget' => '1s' ));
abandoned_cleanup_assert(! is_wp_error($bounded_budget_result), 'active/no-signal bounded budget result succeeds');
abandoned_cleanup_assert('budget_exhausted_after_bounded_apply' === $bounded_budget_result['continuation']['reason'], 'bounded budget continuation explains boundary');
abandoned_cleanup_assert('finalized' === $bounded_budget_result['continuation']['stage'], 'bounded budget continuation resumes as full safe drain');
abandoned_cleanup_assert(0 === count($bounded_budget_abilities['datamachine-code/workspace-worktree-prune']->calls), 'bounded budget continuation skips prune after budget exhaustion');
abandoned_cleanup_assert(str_contains((string) $bounded_budget_result['continuation']['hint'], 'Re-run next_command'), 'bounded budget continuation has operator hint');

fwrite(STDOUT, 'abandoned cleanup orchestrator smoke passed' . PHP_EOL);
Loading