Skip to content

feat(auth): re-mint an expired Flow session without a click - #2

Merged
kittinan merged 2 commits into
main-tun-tmp-fixfrom
feature/auto-flow-relogin
Sep 18, 2026
Merged

kittinan merged 2 commits into
main-tun-tmp-fixfrom
feature/auto-flow-relogin

Conversation

@kittinan

Copy link
Copy Markdown
Owner

The labs.google NextAuth cookie expires on its own schedule, independently of the Google SSO session. When it did, REST calls failed with AisandboxAuthError: no access_token in /fx/api/auth/session (exit 3) and the user had to click Flow's "Sign in" by hand - a click that needs no input, because Google still holds the SSO session.

FlowApiClient._fetch_access_token now performs that round trip itself on the first missing (or non-JSON) session: NextAuth's csrf + signin/google API returns the Google OAuth URL, a temporary page follows it, and Google redirects straight back with a fresh session. Selector-free, so locale-invariant. One attempt per client, serialized across pooled pages; a password / 2FA / CAPTCHA / account-chooser page stops it at once and the existing exit 3 stands. Log event: auth.flow_session_refresh.

Live-verified (zero credits) on a copy of a real profile by tests/e2e/test_auto_relogin_e2e.py: clear the session cookie in a live context, get a fresh access token back, outcome=ok.

Summary

Validation

  • Focused tests added or updated for behavior changes
  • Documentation updated or explicitly marked not applicable
  • uv run python scripts/ci/check_doc_links.py
  • uv run ruff check src tests
  • uv run pyright src
  • Relevant pytest command:

Contribution Checklist

  • This PR targets develop, unless it is a release or emergency fix
  • My commits use my real Git identity or GitHub noreply email
  • External contribution commits include Signed-off-by: (git commit -s)
  • I did not include secrets, cookies, account tokens, signed URLs, or private captured data
  • I reviewed any AI-assisted changes before submitting

The labs.google NextAuth cookie expires on its own schedule, independently
of the Google SSO session. When it did, REST calls failed with
`AisandboxAuthError: no access_token in /fx/api/auth/session` (exit 3) and
the user had to click Flow's "Sign in" by hand - a click that needs no
input, because Google still holds the SSO session.

FlowApiClient._fetch_access_token now performs that round trip itself on
the first missing (or non-JSON) session: NextAuth's csrf + signin/google API
returns the Google OAuth URL, a temporary page follows it, and Google
redirects straight back with a fresh session. Selector-free, so
locale-invariant. One attempt per client, serialized across pooled pages;
a password / 2FA / CAPTCHA / account-chooser page stops it at once and the
existing exit 3 stands. Log event: auth.flow_session_refresh.

Live-verified (zero credits) on a copy of a real profile by
tests/e2e/test_auto_relogin_e2e.py: clear the session cookie in a live
context, get a fresh access token back, outcome=ok.
Conflicts: CHANGELOG.md (both entries kept), client.py imports (both kept).

The base added session-expiry reporting (#e72c2d15, #ce547f42) that measured
a lapsed session and concluded it "cannot be revived". That holds for
re-reading the session endpoint, not for a fresh NextAuth sign-in. Measured
2026-09-18 on a copy of a genuinely lapsed profile (past `expires`,
`error: ACCESS_TOKEN_REFRESH_NEEDED`): the round trip returned a future
`expires` and no `error`, with no click. The merge therefore adapts:

- The refresh fires on a stale session, not only a missing token: a lapsed
  session still carries its old `access_token`, so absence never triggered.
- A successful refresh writes the new deadline to `.gflow_session_expires`.
- At client start, when that cache says the session lapsed, it is revived
  before the run (cookie-authenticated tRPC calls would otherwise just 401).
  Failure is logged, never raised.
- verification: `_is_stale` / `_parse_expires` made public for the client.
- The pre-flight hint, KNOWN_ISSUES, AUTHENTICATION.md and the base's
  CHANGELOG entry no longer claim a lapsed session cannot be revived.

Live: lapsed copy of kittinansr2 through FlowApiClient start ->
client.flow_session_expired, auth.flow_session_refresh outcome=ok, cache
moved to the next day.
@kittinan
kittinan merged commit 06a55dc into main-tun-tmp-fix Sep 18, 2026
14 of 15 checks passed
kittinan pushed a commit that referenced this pull request Oct 3, 2026
…oliva#895) (ffroliva#899)

* fix(video): retry the signed-media download and type its failure (ffroliva#895)

A finished, billed Veo clip was discarded when its transfer hit a transient
connection reset. The GET was issued once, with no retry, and the resulting
`playwright.async_api.Error` escaped the typed-error contract entirely,
surfacing as `Unexpected error ... exit code 1, retryable: False` -- which told
the user nothing about a clip they had already paid for.

The transfer now survives a dropped connection using Playwright's own
`max_retries`, not a new retry policy: it matches on the driver's
`e.code === "ECONNRESET"`, never on an HTTP status, and its backoff is charged
to the same timeout rather than multiplying it. That last property is what
makes it safe here -- our download holds `_generate_lock`, shared with every
other generation on the client, so a per-attempt timeout would have turned one
180 s wait into nine minutes.

Both claims were measured rather than read from the driver source, A/B-controlled
against a local socket emitting a real TCP RST (docs/superpowers/spikes/
2026-09-22-playwright-max-retries-econnreset.md): the retry recovers, a control
with `max_retries=0` fails on the identical fault, and three attempts at a 5 s
timeout finish in 1.57 s rather than ~15 s.

Fixed at all three download sites, not just the one the traceback named. The
second is `gflow data download`'s own GET -- the escape hatch could be stranded
by the fault it exists to rescue.

An exhausted transfer now raises `NetworkError` (exit 6, correctly retryable)
naming the media id and the recovery command. Exit 6 is not new: USAGE.md
already documented it as "Network failure persisted across 3 attempts". The
Playwright message is deliberately not forwarded into `detail` -- it embeds the
driver's server-side call log including the request URL, and `detail` reaches
stderr and --json stdout without passing through redaction.

Also fixed on the same paths: every 4xx branch fell through to
`WireFormatError`'s class default, "retry with a simpler prompt text", on routes
carrying no prompt and no payload -- the same wrong-advice class removed in ffroliva#875.
And the labs download, whose route is a 302 by design and so cannot use
`max_redirects=0`, now verifies where the chain landed before writing the bytes.

Both MCP twins flattened this worse than the CLI and are fixed by the typing:
`gflow_download_media` returned "Unexpected Error; details were logged
server-side" with no remediation key at all, and the queued `gflow_generate_video`
path returned `detail: "sha256:<hash>"` with neither remediation_hint nor
retryable. No new option or param -- worker/codec.py has no error-path code.

Refs ffroliva#896, ffroliva#897, ffroliva#898 (the data-layer root cause this surfaced, filed separately).

* fix(worker): keep the media id on a failed queue row (ffroliva#895)

The remediation this branch adds names the media id and tells the user to
recover the clip with it. Over MCP that advice was unusable.

`daemon.py`'s success branch records `flow_media_id` on the queue row; the
failure branch did not. So a generation that finished, billed, and then lost its
download left the row's `flow_media_id` NULL, and `gflow_generate_video`'s failed
envelope carried no id at all. The agent's only copy was a UUID buried in an
English sentence -- one that suggests a shell command an MCP client cannot run,
while its actual tool, `gflow_download_media`, takes `media_id=`.

`started_media_ids` is already in scope at that call site (it is passed to
`record_failed_operation_safe` two lines above) and `update_task_status` already
COALESCEs, so a task that failed before Flow named anything still writes NULL.

Found by the ffroliva#896 design council reviewing this branch's blast radius, not by
the tests -- no test covered a failure that occurs *after* on_started fires. Two
now do: the fix, and a control proving None does not resurrect a stale id.

* docs(895): commit the predict artifact and correct the ADR note

PLAN.md and SCENARIO.md both cited the predict verdict at a `tmp/` path, which
is gitignored -- a dangling reference for anyone reading the plan later. The
artifact now lives beside them as PREDICT.md.

The plan's ADR note also claimed Track B's second-callback design was compatible
with both ADRs. Track B's own predict refuted that: ADR ffroliva#9 is satisfied, but
ADR #2 / ARCHITECTURE.md:66 bars the DDD scaffolding, and that predict returned
STOP. Correcting it in place rather than leaving a plan that vouches for a shape
the council rejected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant