From 6810cf5d097826b6bf1f37fd5d49a24cbab18cb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 10:09:05 +0000 Subject: [PATCH 1/2] Keep trying an overnight sign-in instead of giving up on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A refresh that came due while nobody was at the machine ended the same way every time: the page needed the user, Frost showed a login window (or opened a browser tab) into an empty room, AWS expired the device code ten minutes later, and scheduleAfterFailure() then stopped refreshing altogether and raised "Sign-in Needed". By morning that notification pointed at a code that had died in the night, the tray said "Sign-in needed" with nothing scheduled, and credentials had been expired for hours — with Auto-open browser set, which is the setting that asks Frost not to leave it to the user. Not retrying was the right fix for #83 (a login page every half second, a tab a minute in default-browser mode) and the wrong one for this. The pile-up came from retrying *visibly* with nobody there, so that is what changes: an abandoned login is retried, and an attempt made while the machine is idle is silent. - schedule.ts: only the user ending a login themselves (`cancelledByUser`) still stops the retries. A login nobody finished gets its own backoff, 5 minutes doubling to hourly, and `loginRetryAction()` decides each tick: the backoff says try again anyway, the user turning up says try again now. - aws-sso.ts: every refresh Frost starts itself now carries whether anyone is there (`powerMonitor.getSystemIdleTime()` against AWAY_IDLE_SEC); the ones the user asks for are attended by definition. An unattended refresh still runs — that is what recovers a silent approval beaten by a slow identity provider — but handOverToUser() ends the attempt rather than showing a window or opening a tab, and notify mode and manual approval end before a device code is even issued. `windowOpen` becomes an `abort` error so both endings travel the same path out of the poll loop. - The "Sign-in Needed" notification is now once per streak and only when somebody was there to miss the login, and it names the next attempt. A user who was away gets the retry itself instead of an unread notification. - The tray says "Sign-in needed — retrying in 10 minutes" rather than a bare "Sign-in needed", which read as Frost having given up — because it had. - AGENTS.md (schedule.ts rules, automatic approval, verification) and docs/docs/{credential-refresh,login,settings-behavior,troubleshooting, app-window}.html describe the new behaviour. Verified with `npm run build`, `npm run lint`, and a headless harness driving dist/aws-sso.js against stubbed electron (idle time included), a faked SSO OIDC client and a fake clock: 46 checks over eight scenarios — away with popup and with default browser, notify mode, automatic approval off, a transient failure healing itself unattended, an attended login left unfinished, a window the user closed, a network failure, and a post-token failure — plus the real dist/tray.js rendering the retry label. One simulated night (away from 8pm, token due at 2am, the page needing the user) against main and against this branch: main this login pages shown to nobody 1 0 browser tabs opened to nobody 1 0 (default-browser mode) notifications raised overnight 1 0 signed in after sitting down never 30s, unprompted Not checked here: real notifications, a real tray, and idle-time reporting on a real desktop — the harness stubs all three. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PBe4cVzZUazesEcLLaktV4 --- AGENTS.md | 34 +++- docs/docs/app-window.html | 1 + docs/docs/credential-refresh.html | 5 +- docs/docs/login.html | 28 ++-- docs/docs/settings-behavior.html | 4 +- docs/docs/troubleshooting.html | 39 ++++- src/aws-sso.ts | 269 +++++++++++++++++++++++++----- src/schedule.ts | 130 +++++++++++++-- src/tray.ts | 26 ++- 9 files changed, 441 insertions(+), 95 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1124b4c..4ee5db6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,13 +38,17 @@ missing one fails at runtime only. - **`src/run-log.ts`** — the per-run step log behind the Activity panel. One run at a time; `refresh()` guards on `isWorking` because a second run would overwrite the current-run slot. -- **`src/schedule.ts`** — every delay `setNextTokenRefresh()` may use. Three - rules: a retry delay is never derived from the stored token expiry (after a - failure it is in the past, which collapses to the floor and reopens the login - page in a loop); a login nobody completed is not retried on a timer; a - failure *after* the token was renewed keeps the expiry schedule rather than - an error retry, which would reopen the login page for an unrelated failure. - No electron imports. +- **`src/schedule.ts`** — every delay `setNextTokenRefresh()` may use, plus + `loginRetryAction()`, the decision behind the retry of a login nobody + finished. Four rules: a retry delay is never derived from the stored token + expiry (after a failure it is in the past, which collapses to the floor and + reopens the login page in a loop, #83); a failure *after* the token was + renewed keeps the expiry schedule rather than an error retry, which would + reopen the login page for an unrelated failure; only the user ending a login + themselves (`cancelledByUser`) stops the retries; and a login nobody finished + is retried on its own slower backoff — what keeps that from piling up login + pages is the attempts being *silent* while nobody is at the machine, not + their absence. No electron imports. - **`src/page-script.ts`** — `loadPageScript()` / `injectIntoEveryFrame()`, used by both injected scripts. Injection follows sub-frames because `executeJavaScript` on a `WebContents` reaches the top frame only, and a @@ -180,6 +184,14 @@ of them says the user is needed (issue #1). Keep these true: default-browser mode, `shell.openExternal`, destroying the hidden probe only once the browser is up. A new way for the flow to end without arriving there is a refresh that hangs invisibly until the device code expires. +- **Unattended, "somewhere" is the retry, not the screen.** When nobody is at + the machine (`powerMonitor.getSystemIdleTime()`, see `AWAY_IDLE_SEC`), the + same `onUserNeeded` ends the attempt instead of showing anything, and + `scheduleAfterFailure()` comes back later or as soon as the user does. A login + page opened into an empty room is dead in ten minutes, which is what made an + overnight refresh a morning of expired credentials. The silent attempt itself + still runs unattended — that is what recovers a refresh beaten by a slow + identity provider, with nobody the wiser. - **`backgroundThrottling: false` on the window.** Chromium throttles timers in a window that is not visible, and the driver's scan loop is a timer. - **Clicking is deliberately narrow.** Only on the device-authorization hosts @@ -546,8 +558,12 @@ Worth doing headlessly, since nothing else covers it: `AWS_IAM_AUTHENTICATOR_PATH` at a stub that prints an `ExecCredential` and kubectl walks the whole exec path with no AWS account. Cover an existing config carrying an entry the old loader rejected — a context with no cluster. -- **`src/schedule.ts`**: pure and electron-free, so its delay arithmetic can be - exercised directly. +- **`src/schedule.ts`**: pure and electron-free, so its delay arithmetic and + `loginRetryAction()` can be exercised directly. The behaviour around it — + which refreshes are unattended, what an unattended one is allowed to put on + screen, when a retry fires — needs `dist/aws-sso.js` loaded against stubbed + `electron` (including `powerMonitor.getSystemIdleTime`), a faked SSO OIDC + client and a fake clock, driving a whole night in a second. Windows behaviour — Squirrel install and update events, the tray icon, toast notifications, the login item — needs a real Windows machine. CI proves the diff --git a/docs/docs/app-window.html b/docs/docs/app-window.html index 7d83e4c..6610434 100644 --- a/docs/docs/app-window.html +++ b/docs/docs/app-window.html @@ -54,6 +54,7 @@

The tray

ItemWhat it does Next refresh in 7 hoursNot clickable — when the current token expires, and therefore when Frost will refresh. Updates every 30 seconds. + Sign-in needed — retrying in 10 minutesNot clickable — a sign-in nobody finished, and when Frost will offer it again. It also offers it as soon as you are back at the machine, whichever comes first. Refresh nowStarts a refresh immediately. Only shown once Frost is configured. Settings… / Get StartedOpens the app window. Reads Get Started until you have saved a start URL. About FrostOpens this website in your browser. diff --git a/docs/docs/credential-refresh.html b/docs/docs/credential-refresh.html index 3f67a96..af8ea1b 100644 --- a/docs/docs/credential-refresh.html +++ b/docs/docs/credential-refresh.html @@ -145,7 +145,7 @@

Auto-open or notify first

See behavior settings for the trade-off between the two.

@@ -155,7 +155,8 @@

When a run fails

kinds of failure want opposite treatment: