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
8 changes: 6 additions & 2 deletions inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ private function stage_incomplete( array $step ): bool {
$next_offset = (int) $pagination['next_offset'];
$current = (int) ( $pagination['offset'] ?? 0 );
$total = isset($pagination['total']) ? (int) $pagination['total'] : null;
if ( ! empty($pagination['partial']) && $next_offset < $current ) {
return true;
}
if ( $next_offset === $current && ! empty($pagination['partial']) ) {
return true;
}
Expand Down Expand Up @@ -423,8 +426,9 @@ private function drain_pages( object $ability, array $base_input, bool $apply, ?
break;
}

$next_offset = isset($pagination['next_offset']) ? (int) $pagination['next_offset'] : null;
if ( null === $next_offset || $next_offset <= $offset ) {
$next_offset = isset($pagination['next_offset']) ? (int) $pagination['next_offset'] : null;
$mutation_count = (int) ( $result['summary']['written'] ?? 0 ) + (int) ( $result['summary']['removed'] ?? 0 );
if ( null === $next_offset || ( $next_offset <= $offset && ( $mutation_count <= 0 || empty($pagination['partial']) ) ) ) {
break;
}

Expand Down
3 changes: 1 addition & 2 deletions inc/Workspace/WorkspaceActiveNoSignalCleanup.php
Original file line number Diff line number Diff line change
Expand Up @@ -489,8 +489,7 @@ private function build_active_no_signal_apply_pagination( array $pagination, str
$limit = (int) ( $pagination['limit'] ?? 25 );
$next_offset = (int) $pagination['next_offset'];
if ( ! $dry_run && $written_count > 0 ) {
$current = (int) ( $pagination['offset'] ?? 0 );
$next_offset = max( $current, $next_offset - $written_count );
$next_offset = 0;
$pagination['next_offset'] = $next_offset;
}

Expand Down
57 changes: 47 additions & 10 deletions inc/Workspace/WorkspaceMetadataReconciliation.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,15 @@ private function drain_worktree_metadata_reconciliation_budget( int $limit, int
$next_offset = (int) $page_pagination['next_offset'];
} while ( ( $budget_seconds - ( microtime(true) - $started_at ) ) > $reserve_seconds );

$elapsed = microtime(true) - $started_at;
$complete = ! empty($last_pagination['complete']);
$partial = ! $complete;
$next_command = $partial ? sprintf(
$elapsed = microtime(true) - $started_at;
$complete = ! empty($last_pagination['complete']);
$partial = ! $complete;
$mutation_count = count($written);
$restart_offset = $mutation_count > 0 ? 0 : (int) ( $last_pagination['next_offset'] ?? $next_offset );
$next_command = $partial ? sprintf(
'studio wp datamachine-code workspace worktree reconcile-metadata --apply --limit=%d --offset=%d --until-budget=%s --format=json',
$limit,
(int) ( $last_pagination['next_offset'] ?? $next_offset ),
$restart_offset,
$budget_label
) : null;

Expand All @@ -244,7 +246,7 @@ private function drain_worktree_metadata_reconciliation_budget( int $limit, int
'scanned' => $scanned,
'partial' => $partial,
'complete' => $complete,
'next_offset' => $partial ? (int) ( $last_pagination['next_offset'] ?? $next_offset ) : null,
'next_offset' => $partial ? $restart_offset : null,
);
if ( null !== $next_command ) {
$pagination['next_command'] = $next_command;
Expand Down Expand Up @@ -644,7 +646,7 @@ private function build_worktree_metadata_reconciliation_row( array $wt, array &$
return $this->build_worktree_metadata_reconciliation_skip($base_row, $diagnostic);
}

$resolved_wt = array_merge(
$resolved_wt = array_merge(
$wt,
array(
'repo' => $repo,
Expand Down Expand Up @@ -1023,9 +1025,9 @@ private function has_stored_lifecycle_finalizer_context( array $metadata ): bool
* @return array{signal:string,reason:string,finalized_state?:string,pr_url?:string}|null
*/
private function detect_worktree_lifecycle_finalizer_signal( array $wt, array $metadata, array &$github_cache, array &$fetched ): ?array {
$repo = (string) ( $wt['repo'] ?? '' );
$branch = (string) ( $wt['branch'] ?? '' );
$pr_signal = $this->detect_stored_pr_merged_signal($metadata, $github_cache);
$repo = (string) ( $wt['repo'] ?? '' );
$branch = (string) ( $wt['branch'] ?? '' );
$pr_signal = $this->detect_stored_pr_merged_signal($metadata, $github_cache);
if ( null !== $pr_signal ) {
return $pr_signal;
}
Expand Down Expand Up @@ -1493,6 +1495,9 @@ private function apply_worktree_metadata_reconciliation_plan( array $plan ): arr

if ( isset($plan['pagination']) && is_array($plan['pagination']) ) {
$result['pagination'] = $plan['pagination'];
if ( ! empty($plan['direct_apply']) && count($written) > 0 ) {
$result['pagination'] = $this->restart_worktree_metadata_reconciliation_pagination( (array) $result['pagination'] );
}
}
if ( isset($plan['evidence']) && is_array($plan['evidence']) ) {
$result['evidence'] = array_merge(
Expand Down Expand Up @@ -1601,6 +1606,38 @@ private function build_worktree_metadata_reconciliation_pagination( int $total,
);
}

/**
* Restart follow-up apply pages after writes because the candidate set changed.
*
* @param array<string,mixed> $pagination Pagination payload.
* @return array<string,mixed>
*/
private function restart_worktree_metadata_reconciliation_pagination( array $pagination ): array {
if ( empty($pagination['partial']) || null === ( $pagination['next_offset'] ?? null ) ) {
return $pagination;
}

$pagination['next_offset'] = 0;
$pagination['next_command'] = sprintf(
'studio wp datamachine-code workspace worktree reconcile-metadata --apply --limit=%d --offset=0%s --format=json',
(int) ( $pagination['limit'] ?? self::METADATA_RECONCILE_DEFAULT_LIMIT ),
$this->worktree_metadata_reconciliation_budget_arg( (string) ( $pagination['next_command'] ?? '' ) )
);

return $pagination;
}

/**
* Extract the existing budget argument from a generated continuation command.
*/
private function worktree_metadata_reconciliation_budget_arg( string $command ): string {
if ( preg_match('/ --until-budget=([^ ]+)/', $command, $matches) ) {
return ' --until-budget=' . $matches[1];
}

return '';
}

/**
* Probe the dirty file count for a single worktree path.
*
Expand Down
59 changes: 59 additions & 0 deletions tests/smoke-abandoned-cleanup-orchestrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@ public function execute( array $input ): array {
}
}

final class AbandonedCleanupQueuedAbility {
/** @var array<int,array<string,mixed>> */
public array $calls = array();

/** @param array<int,array<string,mixed>> $responses */
public function __construct( private array $responses ) {}

/** @return array<string,mixed> */
public function execute( array $input ): array {
$this->calls[] = $input;
$response = array_shift($this->responses) ?: array();

return array_merge(
array(
'success' => true,
'mode' => 'queued',
'dry_run' => ! empty($input['dry_run']),
'applied' => empty($input['dry_run']),
'summary' => array(),
'pagination' => array( 'complete' => true ),
'skipped' => array(),
),
$response
);
}
}

function abandoned_cleanup_assert( bool $condition, string $label ): void {
if ( ! $condition ) {
fwrite(STDERR, 'failed: ' . $label . PHP_EOL);
Expand Down Expand Up @@ -106,4 +133,36 @@ function abandoned_cleanup_assert( bool $condition, string $label ): void {
abandoned_cleanup_assert(1 === $result['summary']['blocked'], 'blocked rows are counted');
abandoned_cleanup_assert(! empty($result['next_commands'][0]), 'next apply command is present');

$reconcile_restart = new AbandonedCleanupQueuedAbility(
array(
array(
'mode' => 'reconcile_metadata',
'summary' => array( 'inspected' => 1, 'written' => 1 ),
'pagination' => array( 'offset' => 0, 'limit' => 10, 'scanned' => 1, 'partial' => true, 'complete' => false, 'next_offset' => 0 ),
),
array(
'mode' => 'reconcile_metadata',
'summary' => array( 'inspected' => 1, 'written' => 0 ),
'pagination' => array( 'offset' => 0, 'limit' => 10, 'scanned' => 1, 'partial' => false, 'complete' => true, 'next_offset' => null ),
),
)
);
$restart_abilities = array(
'datamachine-code/workspace-worktree-reconcile-metadata' => $reconcile_restart,
'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-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )),
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
);
$orchestrator = new DataMachineCode\Workspace\WorkspaceAbandonedCleanupOrchestrator(
static fn( string $name ) => $restart_abilities[ $name ] ?? null,
static fn(): float => 1000.0
);
$restart_result = $orchestrator->run(array( 'apply' => true, 'limit' => 10, 'passes' => 1 ));
abandoned_cleanup_assert(! is_wp_error($restart_result), 'restart apply succeeds');
abandoned_cleanup_assert(2 === count($reconcile_restart->calls), 'mutating reconcile pagination restarts instead of stopping');
abandoned_cleanup_assert(0 === $reconcile_restart->calls[1]['offset'], 'second reconcile page restarts at offset zero');

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