From b5836d0fb637162357068ff3b63f5d24c0bd5113 Mon Sep 17 00:00:00 2001 From: avi Date: Fri, 18 Sep 2026 21:11:58 -0500 Subject: [PATCH] Keep watching the job through transient fetch failures; don't freeze the screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reprocessInPlace's poll loop swallowed fetchJobs failures as an empty list and broke on the resulting missing status, so ONE dropped poll during a 10-minute summarize ended the watch early: chips stayed disabled, the screen showed stale content, and only leaving to the library and back (fresh openDetail fetch) displayed the finished summary — the exact reported symptom. Now only a successful fetch reporting a terminal state ends the loop; transient failures keep polling for ~30s, and a genuinely-lost engine says so in plain words instead of silently stranding the screen. A missing job row on healthy fetches gets 3 grace cycles before giving up (engine rewrites rows mid-requeue). --- .../kotlin/com/shonar/desktop/DesktopState.kt | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt index f236365..5fc14a6 100644 --- a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -1632,11 +1632,30 @@ class DesktopState(private val appDir: File = defaultAppDir()) { return@launch } } - // Poll the target job to a terminal state. + // Poll the target job to a terminal state. A job this long + // (10+ min = hundreds of polls) WILL hit a transient fetch + // failure; treating one as terminal (the old `orEmpty()` → + // st == null → break) froze the screen on stale content until + // the user left and returned — exactly the reported symptom. + // So: only a SUCCESSFUL fetch reporting a terminal state + // breaks; failures keep polling up to ~30s of contact loss. + var miss = 0 + var gone = 0 while (true) { delay(2000) - val jobs = runCatching { parseJobs(provider.fetchJobs(remoteId)) }.getOrNull() - .orEmpty() + val fetched = runCatching { parseJobs(provider.fetchJobs(remoteId)) } + if (fetched.isFailure) { + if (++miss >= 15) { + d0.error = "Lost contact with the engine while " + + "processing — reopen this recording to check." + d0.busy = null + _detail.value = d0.copy() + return@launch + } + continue + } + miss = 0 + val jobs = fetched.getOrDefault(emptyList()) d0.jobs = jobs jobLabel(jobs)?.let { d0.busy = it } // Mirror live progress onto the library row too — otherwise @@ -1646,7 +1665,10 @@ class DesktopState(private val appDir: File = defaultAppDir()) { rescanStatuses() _detail.value = d0.copy() val st = jobs.firstOrNull { it.jobType == job }?.status - if (st == null || st in TERMINAL) break + if (st in TERMINAL) break + // Job row missing from a healthy fetch: give it a few + // cycles (the engine rewrites rows mid-requeue) then stop. + if (st == null && ++gone >= 3) break } setLive(d0.file.name, null) rescanStatuses()