← Corpus / augment-it / issue
Session expiry turns the app into a zombie — the 12h JWT dies mid-use, the UI stays up, and every invoke times out
The `didi_session` cookie lives 30 days, the JWT inside ~12h, and nothing calls `/api/session/refresh` — the transport 4401-loops at 2/sec.
- Path
- issues/Session-Expiry-Turns-The-App-Into-A-Zombie.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Fable 5
- Tags
- Issue · Augment-It · Identity · Workspace-Transport · Shell · Session-Expiry
Session expiry turns the app into a zombie
The incident (2026-07-28, during the workspace-auth human gate)
The operator, mid-walk-through on production: “When I search, nothing loads. And usually the list on the left loads correctly… Now I see nothing.” Gut diagnosis: “we didn’t set up the DB access correctly on Railway.” Then the smoking gun surfaced in the UI itself:
resolver.search timed out after 120s — the workspace did not reply; retry the action
That message is the transport’s client-side deadline (gh #58’s fix) firing — meaning the invoke was queued but never delivered, because the WebSocket never (re)established. Server logs showed the counterpart: a continuous ~2/second stream of
ws reject: didi auth required, no valid didi_session
— the operator’s own browser, reconnect-looping against a 4401.
Meanwhile the deployed backend was verified healthy end to end (a fresh
minted session over the same wss://ws.augment.didi.sh returned the
441-org reach-edu roster and instant resolver.search results). Nothing
was wrong with SurrealDB, Railway env, or the tenancy build. The
session had simply expired.
Root cause — two designs that don’t meet
- id-didi-sh’s token model (by design, per
session_cookie.ex): the cookie jar lives 30 days rolling, but the EdDSA JWT inside expires ~12h after mint and is meant to be re-minted viaPOST /api/session/refresh. Verification always checks the token’s ownexp. - Nothing on the augment-it side ever calls refresh. Not the
shell, not
packages/workspace. So every session hard-dies ~12h after sign-in — while the tab is open and working.
The failure PRESENTATION is what makes it expensive:
- The UI stays fully rendered — the workspace chip (“Reach Edu”), the flow strip, the workbench shell all come from localStorage + loaded code, none of it gated on a live session.
- The transport receives close code 4401 and treats it like any transient drop: reconnect in ~500ms, forever. The reject storm pollutes server logs and burns battery.
invoke()calls enqueue (the gh #41 queue/claim protocol) waiting for a socket that will never open, then die at the 120s deadline with a message (“the workspace did not reply”) that points at the SERVER.- Net: the app looks like its database fell over. The operator spent a troubleshooting round on Railway env vars. Stephenie would hit the same wall ~12h after her first login, with no operator instincts to fall back on.
The fix (two halves, both needed)
- Shell keeps the token fresh. Call
POST /api/session/refresh(credentialed, againstPUBLIC_ID_BASE) on an interval well inside the 12h TTL — e.g. hourly — and onvisibilitychange/focus, so a tab that sleeps overnight refreshes before its next invoke. The endpoint re-mints the JWT into the same cookie; no UI involved. - Transport recognizes auth-death. On WS close code 4401/4403: stop the reconnect loop (or back off to something glacial), reject queued invokes immediately with an auth-shaped error (“session expired — sign in again”, not “the workspace did not reply”), and surface the state to the shell so the SignInWall reappears over the zombie UI. One attempted refresh-then-retry before declaring death would make the two halves compose: expiry mid-flight heals invisibly when the 30-day cookie is still good.
See also
ai-labs/id-didi-sh/lib/id_didi_sh_web/session_cookie.ex— the 30d jar / 12h token split and the refresh contract.packages/workspace/src/transport.ts— reconnect loop + invoke queue + the 120s deadline whose error message misdirected here.shell/src/SignInWall.svelte— the surface that should reappear on auth-death.- [[Search-And-Add-Invokes-Never-Reach-The-Workspace]] — gh #58, whose deadline machinery correctly detected this but mis-attributed it.
- [[../plans/Open-Augment-Didi-Sh-To-Reach-Edu]] — the run whose human gate surfaced this; arguably a launch blocker for Stephenie’s onboarding.