feat: refresh __Secure-1PSIDTS so the session survives past an hour - #1
Merged
Conversation
Google rotates __Secure-1PSIDTS and stops honouring the previous value, so a cookie file that is written once goes stale within roughly an hour and the session silently drops to logged-out. The failure is easy to misread. Anonymous Gemini still answers text prompts and content-push.googleapis.com accepts uploads with no credentials at all, so both of those paths keep looking healthy; only the account-only work fails, surfacing as BardErrorInfo [1100] on image input rather than as an auth error. Watching Set-Cookie on StreamGenerate replies does not help — those carry only NID, never a rotated __Secure-1PSIDTS. The rotation endpoint has to be called explicitly, so a daemon thread polls accounts.google.com/RotateCookies every cookie_refresh_interval_sec (default 540s; the endpoint's own hint is 600) and merges any new value back into cookie_file. - writes only __Secure-1PSIDTS / __Secure-3PSIDTS, leaving the long-lived cookies in the jar alone - re-reads the file under a lock before merging, so a concurrent write from a cookie-sync browser extension is merged into rather than overwritten - writes through a temp file + os.replace at mode 600, so a crash cannot leave a truncated session file - updates gemini.py's mtime-keyed cache so in-flight requests see the new value - a 401 (signed out) leaves the file byte-for-byte unchanged and backs the loop off instead of hammering the endpoint - supports both the JSON and plain-cookie-string file formats - opt out with cookie_refresh_enabled=false when an external tool owns the file Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a copy Live-testing against the real deployment (Docker, cookie file bind-mounted) turned up three defects in the first cut: - os.replace() cannot swap a bind-mounted file -- the mount point is busy -- so every rotation failed with EBUSY on the exact layout this is meant for. Fall back to rewriting the existing inode when replace reports EBUSY/EXDEV. - the temp file was left behind whenever replace failed, so a full copy of the session sat in the directory unreferenced. Remove it on every path. - that failure was reported as "unchanged", making a cookie file the process cannot write look like there was simply nothing to do. persist_rotated_cookies now returns an explicit status and a read-only file says so, with a hint. Verified against Google from the deployment VM: the read-only mount now reports `write_failed` with the hint, leaves the file byte-identical, and leaves no temp file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Based on
pr-80(upstream PR Sophomoresty#80 head,434cb65) — the branch this deployment actually runs.Problem
A cookie file written once goes stale in roughly an hour. Google rotates
__Secure-1PSIDTSand stops honouring the previous value; a browser keeps up because it keeps callingaccounts.google.com/RotateCookies, but a file-backed jar never does.The failure is easy to misread, which is what made it expensive to diagnose:
content-push.googleapis.comaccepts uploads with no credentials at all (verified: zero cookies, bogusX-Tenant-Id— still returns a/contrib_service/ttl_1d/...ref), soImage uploaded:in the log proves nothingOnly the account-only work fails, and it surfaces as
BardErrorInfo [1100]on image input rather than as anything auth-shaped.Why not passive
Set-CookiecaptureMeasured against the live endpoint — Gemini's own replies carry only
NID, never a rotated__Secure-1PSIDTS:So the rotation endpoint has to be called explicitly.
Change
A daemon thread polls
accounts.google.com/RotateCookieseverycookie_refresh_interval_sec(default 540s; the endpoint's own hint in its 401 body is600) and merges any new value back intocookie_file.__Secure-1PSIDTS/__Secure-3PSIDTS, leaving long-lived cookies aloneos.replacewhere possible, falling back to an in-place rewrite onEBUSY— a bind-mounted cookie file cannot be replaced, and that is the normal Docker layoutwrite_failedis reported distinctly fromunchanged, with a hint, so a read-only cookie file cannot look like "nothing to do"gemini.py's mtime-keyed cache so in-flight requests pick the new value up401leaves the file byte-for-byte unchanged and backs the loop off ×4cookie_refresh_enabled: falseto opt out; never creates the fileVerification
python -m unittest discover -s testsruff checkclean on every changed file (the remainingE722is pre-existing inserver.py, untouched)Live against Google from the deployment VM, signed-in session — success path:
Live — signed-out session:
rotate -> False unauthorized, file byte-identical, no temp file.Live — read-only bind mount:
rotate -> False write_failedwith the hint, file byte-identical, no temp file.The first three of those behaviours were found by live testing: the initial cut failed with
EBUSYon the bind mount, left a full copy of the session in the directory, and reported the failure asunchanged. Fixed in the second commit.Deployment note
docker-compose.ymlmounts the auth file read-only:- ./gemini-auth.json:/app/gemini-auth.json:roDrop the
:roor the refresher can never persist anything — it will logwrite_failedevery cycle and the session will still expire.Rate limiting
RotateCookiesreturns429if called repeatedly in quick succession, which is worth knowing before loweringcookie_refresh_interval_sec.🤖 Generated with Claude Code