Repository navigation
feat(ai): track login age, warn before a pool login ends, okena ai accounts - #324
Merged
Merged
Conversation
Member
Author
Review round 1 — review agent (claude-opus-5-5)Commit reviewed: Every row of the decision table matches the code. The state transitions, refresh lock and persistence, serde back-compat (old file + new proxy, new proxy + old daemon), time math and CLI edge cases are correct. No new log line carries a secret.
|
Member
Author
Response to review round 1The round approved with only minor and nit findings, so all are fixed in
Verified: |
A subscription login ends after about a month and the token reply does not say when. The pool now keeps the time of the interactive login (kept by refreshes, reset by a new login) and of the last successful refresh, sends both with the status, and logs every login, successful refresh and rejected refresh with the account id and login age. AiAccount::login_state derives ok / expiring (from day 25) / rejected for every client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings shows how long ago each pool account was logged in, warns from day 25 with Log In Again next to the account, and names a rejected login as such. The activity view shows the same states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One line per account: id, label, email, login time, last refresh and login state, or a JSON array with --json, so local tools can watch for logins about to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Names only, never values, to learn whether the provider states how long a login lasts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Log a token refused right after a refresh, so every rejected state has an account line in proxy.log. - Log only top-level token reply field names, and test the once-per-grant gating. - The E2E test seeds an old login time, so it proves a new login resets it, and checks the info lines for token values too. - One source for login state names; the usage widget says Login rejected like the other views. - Docs: which refresh outcomes are logged, and the downgrade caveat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workspace build enables serde_json's preserve_order, so key order differed from a single-crate build and the test failed in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matej21
force-pushed
the
feat/ai-login-age
branch
from
November 10, 2026 15:04
eccf8ff to
20b2228
Compare
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.
Problem
A Claude subscription login used by the AI proxy lasts about a month. When the provider rejects its refresh token, every proxied session ends its turn with "Please run /login" and nothing told the user beforehand. The token reply the proxy reads carries only the access token's
expires_in, so the proxy had no idea how old a login was.Change
accounts.jsonrecords per accountlogged_in_ms(set by an interactive login, kept by refreshes, reset by the next login) andrefreshed_ms(last successful refresh, cleared by a new login). Both reach clients as new optional fields onAiAccount; an older proxy or file reads asNone. Accounts logged in before this change have no login time until their next login.proxy.logrecords each login (login account=…), each successful refresh (token_refresh ok account=… login_age_days=…, info), each rejected refresh (token_refresh rejected account=… login_age_days=…, warning) and a token refused right after a refresh (token rejected after refresh account=…, warning). Account ids only, never token values.AiAccount::login_stateinokena-core(ok/expiring/rejected, thresholdAI_LOGIN_EXPIRING_DAYS = 25).okena ai accounts [--json]prints the pool without credentials, for other local tools:id<TAB>label<TAB>email<TAB>login_epoch<TAB>refresh_epoch<TAB>state(Unix epoch seconds, empty if unknown; stateok,expiringorrejected).--jsonprints[{id, label, email, logged_in_ms, refreshed_ms, login_age_days, state}]. It uses the existingai_proxyaction through the daemon, like the settings panel.token_reply_fields grant=… fields=…logs the top-level names (never values) of the fields in the token endpoint reply.docs/reference/ai-proxy.md(new Login age section), CLI skill reference.Compatibility with #323 (Log In Again visibility): this PR only changes the condition under which the button shows (
login_state != Okinstead ofneeds_login). #323's busy guard and re-login tracking sit inside that closure, andrender_ai_proxy.rsmerges cleanly. The only textual overlap is incrates/okena-ai/src/server.rs, where both PRs add a test at the same spot; whichever merges second keeps both.Decisions
crates/okena-ai/src/accounts.rs(StoredAccount::logged_in_ms,finish_login,refresh)accounts.rsAccounts::refresh,mark_needs_login; testlogin_time_survives_refreshes_until_a_new_loginasserts the lines and checks them for token valuesrender_ai_proxy.rs(login_age_text, English UI),ai_proxy_activity.rsokena-coreAI_LOGIN_EXPIRING_DAYS,AiAccount::login_state; settings row + activity viewokena ai accountsincrates/okena-cli;docs/reference/ai-proxy.md,skill.mdcrates/okena-ai/src/oauth.rstoken_reply/fields_to_log(once per process and grant, top-level names)claude setup-token"Testing
cargo test -p okena-core -p okena-ai -p okena-cli(new:login_state_warns_from_day_25_and_reports_a_rejected_login,an_account_from_an_older_proxy_has_no_login_time,login_time_survives_refreshes_until_a_new_login(E2E against the fake provider: login → refresh → rejected refresh → new login),reply_fields_are_logged_by_name_only_and_once,ai_account_lines_carry_login_times_and_state,login_age_reads_in_days)cargo test -p okena-agent-usagecargo clippy -p okena-app -p okena-ai -p okena-cli -p okena-core -p okena-daemon-core -p okena-agent-usage --all-targets -- -D warningsStatus
3db05164): approve; 5 minor + 4 nit findings fixed in89cf64cb, no second round89cf64cbfailed:reply_fields_are_logged_by_name_only_and_once. The workspace build turns on serde_json'spreserve_order, so key order differed; the names are now sorted (20b2228f).main(8972f3ef, version bump only, no conflicts); CI green on20b2228f05584787🤖 Generated with Claude Code