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
17 changes: 14 additions & 3 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -1547,9 +1547,9 @@ private function registerAbilities(): void {
'description' => 'Captured session identifiers in a runtime-agnostic envelope. `primary_id` is the single renderer-friendly identifier downstream surfaces display. `ids` is a free-form map keyed by runtime ID (a string the integration layer chooses, e.g. via the `datamachine_code_worktree_runtime_signatures` filter); each entry is a string-map of subkeys (e.g. session_id, thread_id, thread_url, run_id) the integration chose to capture. DMC enumerates no runtime IDs and no subkeys.',
'properties' => array(
'primary_id' => array( 'type' => array( 'string', 'null' ) ),
'ids' => array(
'type' => 'object',
'description' => 'Map of runtime-id => { subkey => string|null }. Keys are opaque; DMC does not validate against a closed set.',
'ids' => array(
'type' => 'object',
'description' => 'Map of runtime-id => { subkey => string|null }. Keys are opaque; DMC does not validate against a closed set.',
'additionalProperties' => array(
'type' => 'object',
'additionalProperties' => array( 'type' => array( 'string', 'null' ) ),
Expand Down Expand Up @@ -2874,6 +2874,17 @@ public static function workspaceCleanupRun( array $input ): array|\WP_Error {
if ( isset( $input['older_than'] ) && '' !== trim( (string) $input['older_than'] ) ) {
$params['worktree_older_than'] = trim( (string) $input['older_than'] );
}
if ( 'artifacts' === $mode ) {
if ( isset( $input['limit'] ) ) {
$params['limit'] = (int) $input['limit'];
}
if ( isset( $input['offset'] ) ) {
$params['offset'] = (int) $input['offset'];
}
if ( ! empty( $input['exhaustive'] ) ) {
$params['exhaustive'] = true;
}
}

$context = array();
if ( isset( $input['user_id'] ) ) {
Expand Down
23 changes: 17 additions & 6 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,14 @@ public function adopt_repo( array $args, array $assoc_args ): void {
* : Pass an age gate such as 7d or 24h into cleanup task params.
*
* [--limit=<count>]
* : Maximum worktrees to scan in a `--mode=artifacts --dry-run` page.
* Defaults to 100 — keeps dry-run bounded on workspaces with hundreds of
* worktrees. Use 0 to disable the cap (combine with --exhaustive for a
* full audit).
* : Maximum worktrees to scan in a `--mode=artifacts` page. Dry-run reviews
* scan this bounded page synchronously; apply runs freeze eligible candidates
* from the same bounded page and schedule only those candidates. Defaults to
* 100. Use 0 to disable the cap (combine with --exhaustive for a full audit).
*
* [--offset=<count>]
* : Pagination offset (0-indexed) for `--mode=artifacts --dry-run`. Walk
* huge workspaces by feeding the previous response's
* : Pagination offset (0-indexed) for `--mode=artifacts` dry-run and apply
* pages. Walk huge workspaces by feeding the previous response's
* `pagination.next_offset` until `pagination.complete` is true.
*
* [--exhaustive]
Expand Down Expand Up @@ -465,6 +465,17 @@ private function cleanup_run_input( string $mode, array $assoc_args ): array {
if ( isset( $assoc_args['older-than'] ) && '' !== trim( (string) $assoc_args['older-than'] ) ) {
$input['older_than'] = trim( (string) $assoc_args['older-than'] );
}
if ( 'artifacts' === $mode ) {
if ( isset( $assoc_args['limit'] ) ) {
$input['limit'] = (int) $assoc_args['limit'];
}
if ( isset( $assoc_args['offset'] ) ) {
$input['offset'] = (int) $assoc_args['offset'];
}
if ( ! empty( $assoc_args['exhaustive'] ) ) {
$input['exhaustive'] = true;
}
}

return $input;
}
Expand Down
90 changes: 43 additions & 47 deletions inc/Tasks/WorkspaceRetentionCleanupTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ public function executeTask( int $jobId, array $params ): void {
* @return array<string,mixed>|\WP_Error
*/
private function schedule_job_backed_cleanup( int $jobId, Workspace $workspace, array $opts, array $params ): array|\WP_Error {
$started_at = microtime( true );
$chunk_rows = $this->build_cleanup_chunk_rows( $workspace, $opts, $params );
$started_at = microtime( true );
$chunk_rows = $this->build_cleanup_chunk_rows( $workspace, $opts, $params );
if ( $chunk_rows instanceof \WP_Error ) {
return $chunk_rows;
}
Expand All @@ -146,22 +146,22 @@ private function schedule_job_backed_cleanup( int $jobId, Workspace $workspace,

if ( array() === $item_params ) {
return array(
'success' => true,
'dry_run' => false,
'destructive' => false,
'job_backed' => true,
'generated_at' => gmdate( 'c' ),
'workspace_path' => $workspace->get_path(),
'chunk_row_counts' => $chunk_row_counts,
'chunks' => array(),
'report' => array(
'removed_count' => 0,
'bytes_reclaimed' => 0,
'freed_human' => '0 B',
'skipped_dirty_unpushed_count' => 0,
'remaining_disk_budget_human' => 'unknown disk',
'success' => true,
'dry_run' => false,
'destructive' => false,
'job_backed' => true,
'generated_at' => gmdate( 'c' ),
'workspace_path' => $workspace->get_path(),
'chunk_row_counts' => $chunk_row_counts,
'chunks' => array(),
'report' => array(
'removed_count' => 0,
'bytes_reclaimed' => 0,
'freed_human' => '0 B',
'skipped_dirty_unpushed_count' => 0,
'remaining_disk_budget_human' => 'unknown disk',
),
'evidence' => array(
'evidence' => array(
'elapsed_ms' => (int) round( ( microtime( true ) - $started_at ) * 1000 ),
'note' => 'No cleanup chunks were eligible after plan generation.',
),
Expand All @@ -188,34 +188,34 @@ private function schedule_job_backed_cleanup( int $jobId, Workspace $workspace,
}

return array(
'success' => true,
'dry_run' => false,
'destructive' => true,
'job_backed' => true,
'generated_at' => gmdate( 'c' ),
'workspace_path' => $workspace->get_path(),
'policy' => array(
'success' => true,
'dry_run' => false,
'destructive' => true,
'job_backed' => true,
'generated_at' => gmdate( 'c' ),
'workspace_path' => $workspace->get_path(),
'policy' => array(
'worktree_cleanup' => (bool) $opts['worktree_cleanup'],
'artifact_cleanup' => (bool) $opts['artifact_cleanup'],
'worktree_older_than' => (string) ( $opts['worktree_older_than'] ?? '14d' ),
'skip_github' => (bool) $opts['skip_github'],
'force' => (bool) $opts['force'],
),
'chunk_row_counts' => $chunk_row_counts,
'chunks' => $batch,
'report' => array(
'chunk_row_counts' => $chunk_row_counts,
'chunks' => $batch,
'report' => array(
'removed_count' => 0,
'bytes_reclaimed' => 0,
'freed_human' => 'pending child jobs',
'skipped_dirty_unpushed_count' => 0,
'remaining_disk_budget_human' => 'pending child jobs',
),
'evidence' => array(
'elapsed_ms' => (int) round( ( microtime( true ) - $started_at ) * 1000 ),
'planned_chunks' => count( $item_params ),
'planned_handles' => $this->cleanup_chunk_handles( $chunk_rows ),
'batch_job_id' => (int) ( $batch['batch_job_id'] ?? 0 ),
'direct_job_ids' => $batch['job_ids'] ?? array(),
'evidence' => array(
'elapsed_ms' => (int) round( ( microtime( true ) - $started_at ) * 1000 ),
'planned_chunks' => count( $item_params ),
'planned_handles' => $this->cleanup_chunk_handles( $chunk_rows ),
'batch_job_id' => (int) ( $batch['batch_job_id'] ?? 0 ),
'direct_job_ids' => $batch['job_ids'] ?? array(),
),
);
}
Expand All @@ -236,25 +236,21 @@ private function build_cleanup_chunk_rows( Workspace $workspace, array $opts, ar
);

if ( ! empty( $opts['artifact_cleanup'] ) ) {
$page_size = max( 1, (int) ( $params['artifact_chunk_size'] ?? 10 ) );
$artifact_page = $workspace->worktree_cleanup_artifacts(
$artifact_limit = isset( $params['limit'] ) ? max( 0, (int) $params['limit'] ) : 100;
$artifact_page = $workspace->worktree_cleanup_artifacts(
array(
'dry_run' => true,
'force' => ! empty( $opts['force'] ),
'limit' => 1,
'offset' => 0,
'dry_run' => true,
'force' => ! empty( $opts['force'] ),
'limit' => $artifact_limit,
'offset' => isset( $params['offset'] ) ? max( 0, (int) $params['offset'] ) : 0,
'exhaustive' => ! empty( $params['exhaustive'] ),
'safety_probes' => true,
)
);
if ( $artifact_page instanceof \WP_Error ) {
return $artifact_page;
}
$total = max( 0, (int) ( $artifact_page['pagination']['total'] ?? $artifact_page['summary']['pagination']['total'] ?? 0 ) );
for ( $offset = 0; $offset < $total; $offset += $page_size ) {
$rows['artifact_discovery'][] = array(
'offset' => $offset,
'limit' => $page_size,
);
}
$rows['artifacts'] = array_values( (array) $artifact_page['candidates'] );
}

if ( ! empty( $opts['worktree_cleanup'] ) ) {
Expand All @@ -270,7 +266,7 @@ private function build_cleanup_chunk_rows( Workspace $workspace, array $opts, ar
if ( $worktree_plan instanceof \WP_Error ) {
return $worktree_plan;
}
$rows['worktrees'] = array_values( (array) ( $worktree_plan['candidates'] ?? array() ) );
$rows['worktrees'] = array_values( (array) $worktree_plan['candidates'] );
}

return $rows;
Expand Down
25 changes: 18 additions & 7 deletions tests/smoke-workspace-retention-task.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,15 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array {
return array(
'success' => true,
'dry_run' => true,
'candidates' => array(),
'candidates' => array(
array(
'handle' => 'repo@active',
'repo' => 'repo',
'branch' => 'active',
'path' => '/tmp/dmc-retention-task-workspace/repo@active',
'artifacts' => array( array( 'path' => 'vendor', 'size_bytes' => 1024 ) ),
),
),
'skipped' => array(),
'summary' => array(
'pagination' => array(
Expand Down Expand Up @@ -180,7 +188,7 @@ function datamachine_code_retention_task_assert( bool $condition, string $messag
datamachine_code_retention_task_assert( empty( $completed[0][1]['skipped'] ), 'explicit CLI run bypasses disabled recurring schedule' );
datamachine_code_retention_task_assert( true === (bool) ( $completed[0][1]['dry_run'] ?? false ), 'explicit CLI run forwards task params' );

echo "\n[4] Artifact cleanup schedules bounded discovery chunks\n";
echo "\n[4] Artifact cleanup freezes bounded candidates before scheduling chunks\n";
\DataMachine\Engine\Tasks\TaskScheduler::$batches = array();
\DataMachineCode\Workspace\Workspace::$artifact_opts = array();
$task = new \DataMachineCode\Tasks\WorkspaceRetentionCleanupTask();
Expand All @@ -191,16 +199,19 @@ function datamachine_code_retention_task_assert( bool $condition, string $messag
'artifact_cleanup' => true,
'worktree_cleanup' => false,
'artifact_chunk_size' => 10,
'limit' => 100,
)
);
$completed = $task->{$completed_prop};
$batch = \DataMachine\Engine\Tasks\TaskScheduler::$batches[0] ?? array();
datamachine_code_retention_task_assert( 'worktree_cleanup_chunk' === ( $batch['task_type'] ?? '' ), 'retention task schedules cleanup chunk batch' );
datamachine_code_retention_task_assert( 3 === count( $batch['items'] ?? array() ), 'artifact inventory total fans out into bounded discovery pages' );
datamachine_code_retention_task_assert( 'artifact_discovery' === ( $batch['items'][0]['chunk_type'] ?? '' ), 'artifact cleanup uses discovery chunks instead of prebuilt artifact rows' );
datamachine_code_retention_task_assert( array( 0, 10, 20 ) === array_column( $batch['items'], 'offset' ), 'discovery chunks carry stable offsets' );
datamachine_code_retention_task_assert( empty( \DataMachineCode\Workspace\Workspace::$artifact_opts[0]['exhaustive'] ), 'parent does not run exhaustive artifact dry-run' );
datamachine_code_retention_task_assert( 3 === (int) ( $completed[0][1]['chunk_row_counts']['artifact_discovery'] ?? 0 ), 'completion report exposes discovery chunk count' );
datamachine_code_retention_task_assert( 1 === count( $batch['items'] ?? array() ), 'artifact candidates fan out proportionally to eligible rows' );
datamachine_code_retention_task_assert( 'artifacts' === ( $batch['items'][0]['chunk_type'] ?? '' ), 'artifact cleanup uses frozen candidate chunks instead of discovery pages' );
datamachine_code_retention_task_assert( 'repo@active' === ( $batch['items'][0]['rows'][0]['handle'] ?? '' ), 'artifact chunk carries reviewed candidate rows' );
datamachine_code_retention_task_assert( 100 === (int) ( \DataMachineCode\Workspace\Workspace::$artifact_opts[0]['limit'] ?? 0 ), 'parent forwards artifact scan limit' );
datamachine_code_retention_task_assert( true === ( \DataMachineCode\Workspace\Workspace::$artifact_opts[0]['safety_probes'] ?? false ), 'parent runs safety probes before scheduling artifact apply chunks' );
datamachine_code_retention_task_assert( 0 === (int) ( $completed[0][1]['chunk_row_counts']['artifact_discovery'] ?? -1 ), 'completion report shows no discovery chunks' );
datamachine_code_retention_task_assert( 1 === (int) ( $completed[0][1]['chunk_row_counts']['artifacts'] ?? 0 ), 'completion report exposes artifact candidate chunk count' );

echo "\nAll workspace retention task smoke tests passed.\n";
}
11 changes: 10 additions & 1 deletion tests/smoke-worktree-cleanup-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ public function execute( array $input ): array {
datamachine_code_cleanup_assert( str_contains( $cleanup_doc_comment, 'Control task-backed workspace cleanup runs.' ), 'workspace cleanup command documents task-backed controller surface' );
datamachine_code_cleanup_assert( str_contains( $cleanup_doc_comment, '<plan|apply|run|status|resume|cancel|evidence>' ), 'workspace cleanup synopsis exposes DB-backed and task-backed cleanup operations' );
datamachine_code_cleanup_assert( str_contains( $cleanup_doc_comment, '[--dry-run]' ), 'task-backed cleanup synopsis keeps synchronous dry-run review' );
datamachine_code_cleanup_assert( str_contains( $cleanup_doc_comment, 'apply runs freeze eligible candidates' ), 'workspace cleanup limit help clarifies artifact apply scoping' );
datamachine_code_cleanup_assert( str_contains( $doc_comment, 'Daily cleanup path: DB-backed plan, then apply only those rows after revalidation' ), 'worktree examples point daily cleanup to DB-backed run_id controller path' );
datamachine_code_cleanup_assert( str_contains( $doc_comment, 'workspace cleanup plan --mode=retention' ), 'worktree examples include DB-backed cleanup plan' );
datamachine_code_cleanup_assert( str_contains( $doc_comment, 'workspace cleanup run --mode=retention' ), 'worktree examples include task-backed cleanup run' );
Expand All @@ -640,11 +641,19 @@ public function execute( array $input ): array {
datamachine_code_cleanup_assert( 'retention' === ( $cleanup_run_ability->last_input['mode'] ?? '' ), 'cleanup run ability receives mode' );
datamachine_code_cleanup_assert( 'workspace_cleanup_cli' === ( $cleanup_run_ability->last_input['source'] ?? '' ), 'cleanup run ability identifies explicit CLI source' );

WP_CLI::$logs = array();
WP_CLI::$successes = array();
$command->cleanup( array( 'run' ), array( 'mode' => 'artifacts', 'limit' => 25, 'offset' => 50, 'format' => 'json' ) );
datamachine_code_cleanup_assert( 'artifacts' === ( $cleanup_run_ability->last_input['mode'] ?? '' ), 'cleanup run can schedule artifact mode' );
datamachine_code_cleanup_assert( 25 === (int) ( $cleanup_run_ability->last_input['limit'] ?? 0 ), 'cleanup run forwards artifact apply limit' );
datamachine_code_cleanup_assert( 50 === (int) ( $cleanup_run_ability->last_input['offset'] ?? 0 ), 'cleanup run forwards artifact apply offset' );
$last_scheduled_cleanup_run = $cleanup_run_ability->last_input;

WP_CLI::$logs = array();
WP_CLI::$successes = array();
$command->cleanup( array( 'run' ), array( 'mode' => 'artifacts', 'dry-run' => true, 'format' => 'json' ) );
datamachine_code_cleanup_assert( true === ( $artifact_ability->last_input['dry_run'] ?? false ), 'cleanup run --dry-run uses artifact cleanup ability directly' );
datamachine_code_cleanup_assert( 'retention' === ( $cleanup_run_ability->last_input['mode'] ?? '' ), 'cleanup run --dry-run does not schedule cleanup run ability' );
datamachine_code_cleanup_assert( $last_scheduled_cleanup_run === $cleanup_run_ability->last_input, 'cleanup run --dry-run does not schedule cleanup run ability' );

WP_CLI::$logs = array();
WP_CLI::$successes = array();
Expand Down
Loading