From ea84bc987671a8ef320beca5787b290a043d2dfc Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:03:31 +0200 Subject: [PATCH] fix(control): a Retry button that could never work, reported through the widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filed from /control via the feedback widget — "what is this? can you fix?" against an element reading: focus_tab → fleetcrown failed: tab not found: fleetcrown … Retry focus-failure.ts already exists to prevent exactly this. Its own note says a Retry beside "tab not found" is a lie, because retrying re-issues an identical command against a target that is identically absent. It also predicts how the protection would break: "a message and a classifier that each spell the same sentence independently are two definitions that drift the first time someone rewords the copy — and the drift is silent, since a misclassified failure still renders a plausible button." That is what happened, across a boundary the note did not consider. Two codebases word the SAME condition differently: cloud "no zellij tab with that name in any active session" → matched desktop "tab not found: " (desktop/src/main/poller.ts) → NOT matched So focusing a project through a local Fleet Runner fell through to RETRY and drew a button that could never succeed. Fixed in the classifier rather than only in the desktop app, deliberately: Fleet Runner is versioned and released independently, so every operator on an older build would keep sending the old wording. A classifier that only understands the newest client is one that lies about older ones. Tests use the reported string verbatim — including the trailing "4m agoOpen on ControlRetry" the element picker captured, since that is what actually reaches the function — plus casing, precedence against NO_TERMINAL, and two near-misses ("tab closed by user") that must stay retryable. Proven by mutation: removing the new match fails the assertion. --- scripts/test/focus-failure.ts | 30 ++++++++++++++++++++++++++++++ src/lib/terminals/focus-failure.ts | 19 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/scripts/test/focus-failure.ts b/scripts/test/focus-failure.ts index f818438a..9961f7d2 100644 --- a/scripts/test/focus-failure.ts +++ b/scripts/test/focus-failure.ts @@ -49,4 +49,34 @@ assert.equal( FAILURE_REMEDY.START_TERMINAL, ); +// The Fleet Runner desktop app words the SAME condition differently: +// `tab not found: ` (desktop/src/main/poller.ts). Unrecognised, it fell +// through to RETRY and drew a button that could never succeed — reported from +// /control through the feedback widget as "what is this? can you fix?". +// +// Verbatim from that report, including the trailing UI text the element picker +// captured, because that is what actually reaches this function. +assert.equal( + remedyForFailure("focus_tab → fleetcrown failed: tab not found: fleetcrown"), + FAILURE_REMEDY.START_SESSION, +); +assert.equal( + remedyForFailure( + "focus_tab → fleetcrown failed: tab not found: fleetcrown4m agoOpen on ControlRetry", + ), + FAILURE_REMEDY.START_SESSION, +); +assert.equal(remedyForFailure("tab not found: orangecat"), FAILURE_REMEDY.START_SESSION); +assert.equal(remedyForFailure("TAB NOT FOUND: evig"), FAILURE_REMEDY.START_SESSION); + +// Same precedence rule as the cloud wording: no terminal at all outranks it. +assert.equal( + remedyForFailure(`tab not found: x / ${FOCUS_FAILURE_PHRASE.NO_TERMINAL}`), + FAILURE_REMEDY.START_TERMINAL, +); + +// Must not over-match: a message that merely mentions a tab is not this failure. +assert.equal(remedyForFailure("tab closed by user"), FAILURE_REMEDY.RETRY); +assert.equal(remedyForFailure("could not find the window"), FAILURE_REMEDY.RETRY); + console.log("✓ focus-failure tests passed"); diff --git a/src/lib/terminals/focus-failure.ts b/src/lib/terminals/focus-failure.ts index 8b70d143..991858ea 100644 --- a/src/lib/terminals/focus-failure.ts +++ b/src/lib/terminals/focus-failure.ts @@ -26,6 +26,23 @@ export const FOCUS_FAILURE_PHRASE = { NO_TERMINAL: "no zellij session is running on the connected computer", /** Zellij is up, but this project has no tab and no running agent. */ NO_SUCH_TARGET: "no zellij tab with that name in any active session", + /** + * The SAME condition, worded by the Fleet Runner desktop app instead of the + * cloud: `tab not found: ` (desktop/src/main/poller.ts). + * + * This is the drift the note above predicts, across a boundary it did not + * anticipate. The desktop runner is a separately shipped codebase that + * composes its own message, so a project focused through a local Fleet + * Runner produced text this classifier did not recognise, fell through to + * RETRY, and drew a Retry button that could never succeed — reported from + * /control through the feedback widget as "what is this? can you fix?". + * + * Matched here rather than fixed only in the desktop app, because Fleet + * Runner is versioned and released independently: every operator still on an + * older build would keep sending the old wording. A classifier that only + * understands the newest client is a classifier that lies about old ones. + */ + NO_SUCH_TARGET_DESKTOP: "tab not found:", /** The tab exists and was found; focus just didn't take in time. */ FOCUS_TIMED_OUT: "focus did not take within", } as const; @@ -51,5 +68,7 @@ export function remedyForFailure(error: string | null | undefined): FailureRemed const text = (error ?? "").toLowerCase(); if (text.includes(FOCUS_FAILURE_PHRASE.NO_TERMINAL)) return FAILURE_REMEDY.START_TERMINAL; if (text.includes(FOCUS_FAILURE_PHRASE.NO_SUCH_TARGET)) return FAILURE_REMEDY.START_SESSION; + if (text.includes(FOCUS_FAILURE_PHRASE.NO_SUCH_TARGET_DESKTOP)) + return FAILURE_REMEDY.START_SESSION; return FAILURE_REMEDY.RETRY; }