Skip to content

Commit 3be09d4

Browse files
fix: ignore observer heartbeats in retention cleanup (#886)
1 parent 25b563d commit 3be09d4

2 files changed

Lines changed: 27 additions & 9 deletions

File tree

inc/Workspace/WorkspaceWorktreeCleanupEngine.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,7 +1524,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate
15241524
'row_type' => 'protected',
15251525
'reason_code' => 'recent_activity',
15261526
'protecting_reason' => 'recent_activity',
1527-
'reason' => sprintf('worktree metadata was seen %d second(s) ago; retention cleanup requires at least %d second(s) without activity', (int) $recent_activity['age_seconds'], self::RETENTION_RECENT_ACTIVITY_SECONDS),
1527+
'reason' => sprintf('worktree lifecycle %s was updated %d second(s) ago; retention cleanup requires at least %d second(s) without activity', (string) $recent_activity['activity_field'], (int) $recent_activity['age_seconds'], self::RETENTION_RECENT_ACTIVITY_SECONDS),
15281528
'metadata' => $metadata,
15291529
),
15301530
$recent_activity
@@ -1795,19 +1795,29 @@ private function worktree_cleanup_has_removable_lifecycle( array $metadata ): bo
17951795
}
17961796

17971797
/**
1798-
* Return a recent-activity protection row when metadata heartbeat is fresh.
1798+
* Return a recent-activity protection row when lifecycle activity is fresh.
1799+
*
1800+
* Observation heartbeats are deliberately excluded: inventory and
1801+
* reconciliation scans refresh them without worktree activity.
17991802
*
18001803
* @param array<string,mixed> $metadata Worktree metadata.
18011804
* @return array<string,mixed>|null
18021805
*/
18031806
private function worktree_cleanup_recent_activity_protection( array $metadata ): ?array {
1804-
$last_seen = (string) ( $metadata['last_seen_at'] ?? $metadata['observed_at'] ?? '' );
1805-
if ( '' === $last_seen ) {
1806-
return null;
1807+
$activity_field = '';
1808+
$timestamp = 0;
1809+
foreach ( array( 'created_at', 'finalized_at', 'cleanup_eligible_at' ) as $field ) {
1810+
$value = (string) ( $metadata[ $field ] ?? '' );
1811+
$time = strtotime($value);
1812+
if ( '' === $value || false === $time || $time <= $timestamp ) {
1813+
continue;
1814+
}
1815+
1816+
$activity_field = $field;
1817+
$timestamp = $time;
18071818
}
18081819

1809-
$timestamp = strtotime($last_seen);
1810-
if ( false === $timestamp ) {
1820+
if ( '' === $activity_field ) {
18111821
return null;
18121822
}
18131823

@@ -1817,7 +1827,8 @@ private function worktree_cleanup_recent_activity_protection( array $metadata ):
18171827
}
18181828

18191829
return array(
1820-
'last_seen_at' => $last_seen,
1830+
'activity_at' => gmdate('c', $timestamp),
1831+
'activity_field' => $activity_field,
18211832
'age_seconds' => $age,
18221833
'recency_window_seconds' => self::RETENTION_RECENT_ACTIVITY_SECONDS,
18231834
);

tests/worktree-retention-apply-protections.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,15 @@ private function git_get_remote( string $path ): ?string {
163163

164164
$recent_candidate = $base_candidate;
165165
$recent_candidate['metadata']['last_seen_at'] = gmdate('c', time() - 60);
166+
$recent_candidate['metadata']['observed_at'] = gmdate('c', time() - 60);
166167
$recent = $harness->revalidate($recent_candidate);
167-
retention_apply_protections_assert('recent_activity' === ( $recent['skipped']['reason_code'] ?? null ), 'recent last_seen_at rows are protected from apply removal');
168+
retention_apply_protections_assert(! isset($recent['skipped']), 'recent observation heartbeats do not protect cleanup-eligible rows from apply removal');
169+
170+
$recent_lifecycle_candidate = $base_candidate;
171+
$recent_lifecycle_candidate['metadata']['cleanup_eligible_at'] = gmdate('c', time() - 60);
172+
$recent_lifecycle = $harness->revalidate($recent_lifecycle_candidate);
173+
retention_apply_protections_assert('recent_activity' === ( $recent_lifecycle['skipped']['reason_code'] ?? null ), 'recent cleanup_eligible_at rows are protected from apply removal');
174+
retention_apply_protections_assert('cleanup_eligible_at' === ( $recent_lifecycle['skipped']['activity_field'] ?? null ), 'recent lifecycle protection identifies the lifecycle activity field');
168175

169176
GitHubAbilities::$mode = 'open';
170177
$open_pr = $harness->revalidate($base_candidate);

0 commit comments

Comments
 (0)