From 5db92d51271e429432ddcce120913ab5578ae759 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 27 Feb 2026 00:18:22 -0500 Subject: [PATCH] fix(watcher): fix TickCount wrap and resume retry on API failure - Use 32-bit Environment.TickCount with unchecked uint arithmetic so GetIdleTime stays correct after the ~49.7 day uint wrap boundary - Only clear _pausedTaskId after successful resume, not before the API call - Add retry path: if resume failed, retry on next poll cycle while user is still active Co-Authored-By: Claude Opus 4.6 --- WindowWatcher/NativeMethods.cs | 3 ++- WindowWatcher/Worker.cs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/WindowWatcher/NativeMethods.cs b/WindowWatcher/NativeMethods.cs index b541419..82248bf 100644 --- a/WindowWatcher/NativeMethods.cs +++ b/WindowWatcher/NativeMethods.cs @@ -29,6 +29,7 @@ internal static partial class NativeMethods var info = new LASTINPUTINFO { cbSize = (uint)Marshal.SizeOf() }; if (!GetLastInputInfo(ref info)) return TimeSpan.Zero; - return TimeSpan.FromMilliseconds(Environment.TickCount64 - info.dwTime); + // Use 32-bit TickCount so both values wrap at the same boundary as dwTime (uint) + return TimeSpan.FromMilliseconds(unchecked((uint)Environment.TickCount - info.dwTime)); } } diff --git a/WindowWatcher/Worker.cs b/WindowWatcher/Worker.cs index b8d99df..390b84f 100644 --- a/WindowWatcher/Worker.cs +++ b/WindowWatcher/Worker.cs @@ -73,7 +73,12 @@ public class Worker( // Idle detection var idleTime = NativeMethods.GetIdleTime(); - if (!_isIdle && idleTime.TotalMilliseconds >= config.IdleTimeoutMs) + if (!_isIdle && _pausedTaskId is not null && idleTime.TotalMilliseconds < config.IdleTimeoutMs) + { + // Retry resume from a previous failed attempt + await ResumeIdlePausedTaskAsync(ct: stoppingToken); + } + else if (!_isIdle && idleTime.TotalMilliseconds >= config.IdleTimeoutMs) { _isIdle = true; logger.LogInformation("User idle for {IdleTime}, pausing active task", idleTime); @@ -167,7 +172,6 @@ public class Worker( return; var taskId = _pausedTaskId.Value; - _pausedTaskId = null; try { @@ -180,6 +184,7 @@ public class Worker( if (response?.Data is null || response.Data.Status != "Paused") { logger.LogDebug("Task {TaskId} is no longer paused, skipping auto-resume", taskId); + _pausedTaskId = null; return; } @@ -189,13 +194,18 @@ public class Worker( new { note = "Auto-resumed: user returned" }, ct); if (resumeResponse.IsSuccessStatusCode) + { logger.LogInformation("Auto-resumed task {TaskId}", taskId); + _pausedTaskId = null; + } else - logger.LogWarning("Failed to resume task {TaskId}: {Status}", taskId, resumeResponse.StatusCode); + { + logger.LogWarning("Failed to resume task {TaskId}: {Status}, will retry", taskId, resumeResponse.StatusCode); + } } catch (Exception ex) { - logger.LogWarning(ex, "Failed to resume task {TaskId} after idle", taskId); + logger.LogWarning(ex, "Failed to resume task {TaskId} after idle, will retry", taskId); } } }