From fe63533526e2cd97128a82aa287ce19513d8f1ff Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Tue, 28 Jul 2026 19:38:59 -0400 Subject: [PATCH] fix: bound emergency cleanup task results --- .../WorkspaceDiskEmergencyCleanupTask.php | 89 +++++++++++++- ...-disk-emergency-cleanup-durable-result.php | 109 ++++++++++++++++++ 2 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 tests/workspace-disk-emergency-cleanup-durable-result.php diff --git a/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php b/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php index 642981fd..dc04b780 100644 --- a/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php +++ b/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php @@ -16,8 +16,9 @@ class WorkspaceDiskEmergencyCleanupTask extends SystemTask { - - + private const MAX_DURABLE_REASONS = 10; + private const MAX_DURABLE_REASON_BYTES = 120; + private const MAX_DURABLE_JOB_IDS = 25; /** * PluginSettings key that gates threshold-triggered emergency cleanup. */ @@ -125,6 +126,7 @@ public function executeTask( int $jobId, array $params ): void { ! empty($result['action_required']) ? 'yes' : 'no' ) : 'Workspace disk emergency cleanup skipped: disk thresholds not crossed.'; + $durable_report = $this->compact_durable_report($result); do_action( 'datamachine_log', @@ -133,11 +135,90 @@ public function executeTask( int $jobId, array $params ): void { array( 'task' => $this->getTaskType(), 'jobId' => $jobId, - 'report' => $result, + 'report' => $durable_report, ) ); - $this->completeJob($jobId, $result); + $this->completeJob($jobId, $durable_report); + } + + /** + * Project a recurring task result into bounded durable evidence. + * + * Workspace abilities and CLI commands intentionally retain the complete + * plan for explicit operator inspection. Recurring jobs retain only the + * operational facts needed to diagnose disk pressure and scheduled work. + * + * @param array $result Full emergency cleanup result. + * @return array + */ + private function compact_durable_report( array $result ): array { + $budget = (array) ( $result['disk_budget'] ?? array() ); + $summary = (array) ( $result['scheduled_summary'] ?? array() ); + $selection = (array) ( $result['inode_recovery_plan'] ?? array() ); + $capacity = (array) ( $result['capacity_evidence'] ?? array() ); + + return array( + 'success' => ! empty($result['success']), + 'triggered' => ! empty($result['triggered']), + 'skipped' => ! empty($result['skipped']), + 'dry_run' => ! empty($result['dry_run']), + 'generated_at' => (string) ( $result['generated_at'] ?? '' ), + 'trigger_reasons' => $this->compact_reasons($budget['trigger_reasons'] ?? array()), + 'selected_artifact_count' => (int) ( $result['selected_artifact_count'] ?? 0 ), + 'selected_worktree_count' => (int) ( $result['selected_worktree_count'] ?? 0 ), + 'scheduled' => array( + 'chunks' => (int) ( $summary['scheduled_chunks'] ?? 0 ), + 'artifact_rows' => (int) ( $summary['scheduled_artifact_rows'] ?? 0 ), + 'batch_job_id' => (int) ( $summary['batch_job_id'] ?? 0 ), + 'direct_job_ids' => array_slice(array_map('intval', (array) ( $summary['direct_job_ids'] ?? array() )), 0, self::MAX_DURABLE_JOB_IDS), + ), + 'measured_recovery' => array( + 'target_bytes' => (int) ( $budget['target_recovery_bytes'] ?? 0 ), + 'target_inodes' => (int) ( $budget['target_recovery_inodes'] ?? 0 ), + 'planned_bytes' => (int) ( $selection['planned_measured_recovery_bytes'] ?? 0 ), + 'planned_inodes' => (int) ( $selection['planned_measured_recovery_inodes'] ?? 0 ), + 'target_met' => ! empty($selection['target_met']), + 'reclaimed_bytes' => is_numeric($capacity['reclaimed_bytes'] ?? null) ? (int) $capacity['reclaimed_bytes'] : null, + 'reclaimed_inodes' => is_numeric($capacity['reclaimed_inodes'] ?? null) ? (int) $capacity['reclaimed_inodes'] : null, + ), + 'action_required' => ! empty($result['action_required']), + 'action_required_reasons' => $this->compact_reasons($result['action_required_reasons'] ?? array()), + 'capacity_evidence' => $this->compact_capacity_evidence($capacity), + ); + } + + /** + * @param mixed $reasons Candidate reason values. + * @return array + */ + private function compact_reasons( mixed $reasons ): array { + $compact = array(); + foreach ( array_slice((array) $reasons, 0, self::MAX_DURABLE_REASONS) as $reason ) { + $compact[] = substr((string) $reason, 0, self::MAX_DURABLE_REASON_BYTES); + } + return $compact; + } + + /** + * @param array $capacity Full capacity snapshots. + * @return array + */ + private function compact_capacity_evidence( array $capacity ): array { + $fields = array( 'filesystem_total_bytes', 'filesystem_free_bytes', 'filesystem_free_inodes', 'workspace_allocated_bytes' ); + $compact = array( + 'reclaimed_bytes' => is_numeric($capacity['reclaimed_bytes'] ?? null) ? (int) $capacity['reclaimed_bytes'] : null, + 'reclaimed_inodes' => is_numeric($capacity['reclaimed_inodes'] ?? null) ? (int) $capacity['reclaimed_inodes'] : null, + 'before' => array(), + 'after' => array(), + ); + + foreach ( $fields as $field ) { + $compact['before'][ $field ] = is_numeric($capacity['before'][ $field ] ?? null) ? (int) $capacity['before'][ $field ] : null; + $compact['after'][ $field ] = is_numeric($capacity['after'][ $field ] ?? null) ? (int) $capacity['after'][ $field ] : null; + } + + return $compact; } /** diff --git a/tests/workspace-disk-emergency-cleanup-durable-result.php b/tests/workspace-disk-emergency-cleanup-durable-result.php new file mode 100644 index 00000000..267f836f --- /dev/null +++ b/tests/workspace-disk-emergency-cleanup-durable-result.php @@ -0,0 +1,109 @@ +completed[ $job_id ] = $result; + } + + public function failJob( int $job_id, string $message ): void {} + } +} + +namespace DataMachineCode\Workspace { + class Workspace { + public static array $result; + + public function workspace_disk_emergency_cleanup( array $opts ): array { + return self::$result; + } + } +} + +namespace { + if ( ! defined('ABSPATH') ) { + define('ABSPATH', __DIR__ . '/fixtures/'); + } + + $GLOBALS['emergency_cleanup_logs'] = array(); + function do_action( string $hook, mixed ...$args ): void { + if ( 'datamachine_log' === $hook ) { + $GLOBALS['emergency_cleanup_logs'][] = $args; + } + } + + require_once dirname(__DIR__) . '/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php'; + + function durable_result_assert( bool $condition, string $message ): void { + if ( ! $condition ) { + throw new RuntimeException($message); + } + } + + $candidates = array(); + for ( $index = 0; $index < 128; ++$index ) { + $candidates[] = array( + 'handle' => 'repo@candidate-' . $index, + 'evidence' => str_repeat('candidate-evidence-', 1024), + ); + } + + DataMachineCode\Workspace\Workspace::$result = array( + 'success' => true, + 'triggered' => true, + 'dry_run' => true, + 'generated_at' => '2026-07-28T00:00:00+00:00', + 'disk_budget' => array( + 'trigger_reasons' => array( 'filesystem_free_bytes_below_threshold' ), + 'target_recovery_bytes' => 123456, + 'target_recovery_inodes' => 789, + ), + 'emergency_plan' => array( 'artifact_candidates' => $candidates, 'worktree_candidates' => $candidates ), + 'apply_plan' => array( 'artifact_candidates' => $candidates, 'worktree_candidates' => $candidates ), + 'selected_artifact_count' => 128, + 'selected_worktree_count' => 0, + 'inode_recovery_plan' => array( 'planned_measured_recovery_bytes' => 123456, 'planned_measured_recovery_inodes' => 789, 'target_met' => true ), + 'action_required' => true, + 'action_required_reasons' => array( 'worktree_deletion_requires_human_approval' ), + 'scheduled_summary' => array( 'scheduled_chunks' => 1, 'scheduled_artifact_rows' => 128, 'batch_job_id' => 44, 'direct_job_ids' => array( 45, 46 ) ), + 'capacity_evidence' => array( + 'reclaimed_bytes' => 900, + 'reclaimed_inodes' => 12, + 'before' => array( 'filesystem_total_bytes' => 10000, 'filesystem_free_bytes' => 100, 'filesystem_free_inodes' => 10, 'workspace_allocated_bytes' => 9000, 'unbounded' => str_repeat('x', 1024 * 1024) ), + 'after' => array( 'filesystem_total_bytes' => 10000, 'filesystem_free_bytes' => 1000, 'filesystem_free_inodes' => 22, 'workspace_allocated_bytes' => 8000, 'unbounded' => str_repeat('x', 1024 * 1024) ), + ), + ); + + $full_size = strlen((string) json_encode(DataMachineCode\Workspace\Workspace::$result, JSON_THROW_ON_ERROR)); + durable_result_assert($full_size > 4 * 1024 * 1024, 'Fixture must contain a multi-megabyte explicit workspace report.'); + + $task = new DataMachineCode\Tasks\WorkspaceDiskEmergencyCleanupTask(); + $task->executeTask(42, array( 'dry_run' => true )); + $log_context = $GLOBALS['emergency_cleanup_logs'][0][2]; + $completed = $task->completed[42]; + $log_size = strlen((string) json_encode($log_context, JSON_THROW_ON_ERROR)); + $completed_size = strlen((string) json_encode($completed, JSON_THROW_ON_ERROR)); + + durable_result_assert($log_size <= 8192, 'Durable log context must remain at or below 8192 serialized bytes.'); + durable_result_assert($completed_size <= 8192, 'Completed job result must remain at or below 8192 serialized bytes.'); + durable_result_assert(! isset($log_context['report']['emergency_plan']) && ! isset($completed['apply_plan']), 'Durable task records must not persist candidate plans.'); + durable_result_assert(128 === $completed['selected_artifact_count'] && array( 45, 46 ) === $completed['scheduled']['direct_job_ids'], 'Durable result must retain selected counts and scheduled job IDs.'); + durable_result_assert(array( 'filesystem_free_bytes_below_threshold' ) === $log_context['report']['trigger_reasons'], 'Durable log context must retain trigger reasons.'); + durable_result_assert(900 === $completed['measured_recovery']['reclaimed_bytes'] && 12 === $completed['measured_recovery']['reclaimed_inodes'], 'Durable result must retain measured recovery totals.'); + durable_result_assert(true === $completed['action_required'] && array( 'worktree_deletion_requires_human_approval' ) === $completed['action_required_reasons'], 'Durable result must retain action-required evidence.'); + durable_result_assert(100 === $completed['capacity_evidence']['before']['filesystem_free_bytes'] && 1000 === $completed['capacity_evidence']['after']['filesystem_free_bytes'], 'Durable result must retain compact capacity evidence.'); + + echo "workspace-disk-emergency-cleanup-durable-result: log={$log_size} completed={$completed_size} bytes\n"; +}