Keep trying an overnight sign-in instead of giving up on it - #92
Merged
Merged
Conversation
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBe4cVzZUazesEcLLaktV4
Follow-up on the same overnight failure: the first pass kept Frost trying, but paced the retries 5 minutes → hourly, and ended the attempt the moment a page turned out to need a human. Both work against what the retry is actually for. Continuous refreshing is the premise of the app, so a gap between a device code dying and the next one starting is a gap in the credentials, and an attempt ended early is one that cannot come through on its own. So there is no Frost-side delay left. AWS's device code lives about ten minutes; when one dies the next sign-in starts at once, all night, and the pacing is AWS's rather than ours. - An unattended login is no longer aborted when the page needs the user. It is *parked*: nothing is shown, the page keeps being driven — so a slow identity provider or a dropped network still completes it with nobody the wiser — and the live page is handed over the moment somebody is there, on the next poll. HANDOVER_MIN_CODE_LIFE_MS keeps a code with seconds left from being handed to a user who would watch it die; the replacement is immediate, so they get a fresh page instead. - Presence is read wherever it matters instead of once per run: a ten-minute login can start with nobody there and end with the user watching it. The `attended` flag that threaded through refresh() is gone. - MIN_LOGIN_CYCLE_MS (30s) is the only floor, and it is not a backoff — it never grows, and nothing waits for it unless a login fails the instant it starts, which would otherwise spin attempts as fast as AWS could refuse them. - A parked run held `isWorking`, which would have made "Refresh now" and the hotkey no-ops all night and the tray read "Refreshing…" while a page sat waiting for a password. A refresh the user asks for now shows the login Frost is holding (showParkedLogin), the hotkey drops its own isWorking guard, and the tray says "Sign-in needed — waiting for you". - The "Sign-in Needed" notification is gone. It existed because Frost stopped trying and that was invisible; the replacement login is the notice now, and a notification on top of it is the thing this whole change is meant to stop. - Notification mode and manual approval cannot get through unattended at all, so they no longer burn a device code to find that out: Frost waits for the user and starts then, recording one run rather than one every half minute. - retryDelayMsAfterError() splits into errorRetryDelayMs() (unchanged 1m→30m for failures with no login page involved) and nextLoginAttemptDelayMs(), with loginRetryAction() deciding each tick. AGENTS.md and the docs pages follow. Verified with `npm run build`, `npm run lint`, and the headless harness (dist/aws-sso.js against stubbed electron including powerMonitor idle time, a faked SSO OIDC client and a fake clock): 53 checks over ten scenarios, plus the real dist/tray.js rendering the held-login label. Eight simulated hours away with a page that needs a human: 46-49 device codes, worst gap between attempts 10m30s (the code's life plus the poll that notices), nothing shown, nothing notified, and the login page appears 10s after the first keypress. A slow identity provider coming through mid-hold completes the refresh unattended. Six hours away in default-browser mode: zero tabs, one tab when the user returns. Notify mode, four hours away: zero device codes, one run. Costs, deliberately accepted: a hidden login window and its 400ms approval scan live continuously while a login is held rather than only during a refresh, and the Activity page gains an entry per device code (~6/hour) while a machine is away with a sign-in that needs a human. Not checked here: real notifications, a real tray, and idle-time reporting on a real desktop — the harness stubs all three. On a Linux session where getSystemIdleTime() is unimplemented it reports 0, which reads as "somebody is here", so those sessions get the visible login page rather than a held one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBe4cVzZUazesEcLLaktV4
popen2
enabled auto-merge
September 14, 2026 05:58
popen2
pushed a commit
that referenced
this pull request
Sep 14, 2026
Notify mode is a promise not to put a login page in front of the user unannounced, and it kept that promise by announcing every refresh: notification, wait for the hotkey, then the login window. With automatic approval that is backwards. Most refreshes now ask the user for nothing at all, so notify mode was interrupting them once a working day about work Frost was about to do without them. The notification moves to the moment a refresh turns out to need a person. Frost works through the approval off screen first; if the page asks for a password, a one-time code or a security key, notify mode says so and waits for the hotkey or a click before showing anything. A refresh the identity provider session covers passes in silence. With automatic approval off nothing changes: a login page is still about to open, so it is still announced first. That also makes notify mode able to finish with nobody at the machine, which is what #92 needed canRefreshUnattended() for. It excluded notify mode because the old flow waited for a hotkey nobody was there to press; the new one only waits when the page actually needs a person, so an overnight refresh in notify mode now renews credentials like any other instead of stopping before it asks AWS for a device code. Two details that go with it: the pending trigger is cancelled when the run ends, or hasPendingAuth() keeps saying yes and swallows the next hotkey press; and a refresh the user asks for while the notice is unanswered is them saying yes, so it reaches the page Frost is already holding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018XK1mi3rSPLzxu7XUhyC6a
This was referenced Sep 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-inNeeded". 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.
What this does
Continuous refreshing is the premise of the app, so there is no backoff of any
kind on a login nobody finished. AWS keeps a device code alive for about ten
minutes; when one dies the next sign-in starts at once, all night, and the pacing
is AWS's rather than ours.
turns out to need a person and nobody is at the machine
(
powerMonitor.getSystemIdleTime()againstAWAY_IDLE_SEC), the attempt isparked: nothing on screen, and the page keeps being driven — so a slow
identity provider or a dropped network still completes the refresh with nobody
the wiser. The live page is handed over the moment somebody is there, on the
next poll.
HANDOVER_MIN_CODE_LIFE_MSkeeps a code with seconds left frombeing handed to a user who would watch it die; the replacement is immediate, so
they get a fresh page instead.
ten-minute login can start with nobody there and end with the user watching it.
MIN_LOGIN_CYCLE_MS(30s) is the only floor, and is not a backoff — itnever grows, and nothing waits for it unless a login fails the instant it
starts, which would otherwise spin attempts as fast as AWS could refuse them.
isWorking, which would have made"Refresh now" and the hotkey no-ops all night and the tray read "Refreshing…"
while a page sat waiting for a password. A refresh the user asks for now shows
the login Frost is holding, and the tray says Sign-in needed — waiting for
you.
trying and that was invisible; the replacement login is the notice now, and a
notification on top of it is the thing this change is meant to stop.
so they no longer burn a device code to find that out: Frost waits for the user
and starts then, recording one run rather than one every half minute.
the identity provider — still stops the replacements.
retryDelayMsAfterError()splits intoerrorRetryDelayMs()(unchanged 1m→30mfor failures with no login page involved) and
nextLoginAttemptDelayMs(), withloginRetryAction()deciding each tick. AGENTS.md and the docs pages follow.The first commit here kept Frost trying but on a 5-minute-to-hourly backoff, and
ended an attempt as soon as it needed a human; the second replaces both with the
above. Reviewing the second commit alone is enough to see the final behaviour.
Verification
npm run build,npm run lint, and a headless harness drivingdist/aws-sso.jsagainst stubbed electron (including
powerMonitoridle time), a faked SSO OIDCclient and a fake clock: 53 checks over ten scenarios, plus the real
dist/tray.jsrendering the held-login label.worst gap between attempts 10m30s (the code's life plus the poll that notices),
nothing shown, nothing notified, and the login page appears 10s after the first
keypress.
unattended.
user returns.
main, the same night ends with one dead login page, one notification,and no sign-in until the user presses the hotkey.
Costs, deliberately accepted: a hidden login window and its 400ms approval scan
live continuously while a login is held rather than only during a refresh, and
the Activity page gains an entry per device code (~6/hour) while a machine is
away with a sign-in that needs a human.
Not checked here: real notifications, a real tray, and idle-time reporting on a
real desktop — the harness stubs all three. On a Linux session where
getSystemIdleTime()is unimplemented it reports 0, which reads as "somebody ishere", so those sessions get the visible login page rather than a held one.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PBe4cVzZUazesEcLLaktV4