7.7 KiB
Session 0022.2 — Transcript
Parent: SESSION-0022.0-TRANSCRIPT-…md Date: 2026-05-28 Goal: implement roadmap #29 (server-side sign-in state resume) as rfc-app v0.23.0 feature code Outcome: Done. Branch
feature/v0.23.0-signin-resume@ 77010ea. Backend 336 passed (332 baseline + 4 new). Frontend build green. Migration 022 auto-applies. Profile-toggle UI deferred (column + default-on behavior ship now).
Pre-session state
- Forked subagent of OHM driver Session 0022.0. Isolated worktree at
/Users/benstull/git/rfc-app-v0.23.0-signin-resume, branchfeature/v0.23.0-signin-resumebased on main @ 2ac20b1. - Concurrent subagent 0022.1 owns propose/PR regions of api.py + several frontend components + migration 021. Strict file ownership.
- Baseline backend suite: 332 passed.
Turn-by-turn arc
- Read SESSION-PROTOCOL.md and confirmed worktree/branch. Noted the protocol says subagents don't write their own transcript, but the subsession prompt explicitly requires a §5 transcript file (unpublished) — followed the prompt.
- Inspected migrations.
users.idisINTEGER PRIMARY KEY AUTOINCREMENT(001, rebuilt in 012). Timestamp convention isTEXT NOT NULL DEFAULT (datetime('now'))(users.last_seen_at, device_trust.last_seen_at). No native JSONB — other JSON stored as TEXT via json.dumps/loads (permission_events.details, actions.details). There is NOuser_sessionstable; sessions are signed cookies (rfc_session), so a newuser_session_statetable keyed on user_id was the right shape. - Read api.py
/meregion: auth surface usesauth.require_user(401 for anon),/api/auth/mealready does a single-round-trip payload (beta + passcode state). db.conn() isisolation_level=None(autocommit) — no explicit commit needed, matching every other write endpoint. - Read auth.py
require_user/current_user/ SessionUser. - Read the frontend: Login.jsx does a hard
window.location.assignafter sign-in, so the app re-boots and App.jsx'sgetMe()reloadsme. The #21-Part-Cidentifyfires in an App.jsx effect whenme.user.idfirst appears. Decision: the resume redirect must live in App.jsx (after identify), NOT in Login.jsx (which I don't own and must not reorder).
Decisions
- Table: new
user_session_state(migration 022), one row per user (per-user, not per-device — roadmap safe default). Columns:user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,last_route TEXT,last_route_state TEXT(JSON-as-TEXT, nullable),resume_enabled INTEGER NOT NULL DEFAULT 1,last_updated_at TEXT NOT NULL DEFAULT (datetime('now')). - /api/auth/me wiring choice: folded
resume_enabled,last_route, and decodedlast_route_stateonto the EXISTING/api/auth/mepayload rather than a new GET — smallest/cleanest diff, the frontend already fetches it on boot. Whenresume_enabled=0,last_routeis handed back as null (stored row preserved so re-enabling resumes). - PUT /api/me/last-state:
require_user(anon → 401). Upsert viaINSERT … ON CONFLICT(user_id) DO UPDATE. No-ops (stored=False) when an existing row hasresume_enabled=0. - Resume redirect ordering: added
identifyReadystate in App.jsx, flipped true at the end of the identify effect (and immediately on an anonymousme). useLastState gates its one-time redirect on it → redirect always after identify. Redirect only fires when the app booted on/(the hard sign-in landing), so a deep-link/refresh mid-RFC is never yanked. - Stale state: no server special-casing.
navigate(last_route, {replace:true})lands on whatever renders today; existing routing falls through to catalog/empty-state for missing/unreadable RFCs. Non-resumable prefixes (/login, /welcome, /invites/, /invitations/) and "/" are skipped in the hook. - Privacy: route + light view state only, never draft buffers. Documented in migration 022 column comment, LastStateBody comment, and new SPEC §6.8.
Deferred
- Profile-settings opt-out toggle UI. The
resume_enabledcolumn + default-on behavior + the disable semantics (PUT no-op, /me hides route) all ship now; only the client UI to flip the flag is deferred. The vertical test flips it via direct SQL to prove the behavior.
Test outcomes
- New
test_session_resume_vertical.py: 4 passed (auth-required, upsert/read-back+overwrite+state-JSON, per-user isolation, resume_enabled=0 disable). - Full backend suite: 336 passed (332 baseline + 4).
- Frontend
npm run build: green (neededVITE_APP_NAMEset for the build — pre-existing requirement, no default shipped; usedVITE_APP_NAME=OHMfor the build check only, not committed). Pre-existing chunk-size warning unchanged.
Cut state
- Branch
feature/v0.23.0-signin-resume@ 77010ea. - Files changed (7, +459):
backend/migrations/022_user_session_state.sql(new)backend/app/api.py(LastStateBody model; /api/auth/me payload additions; new PUT /api/me/last-state handler)backend/tests/test_session_resume_vertical.py(new)frontend/src/lib/useLastState.js(new hook)frontend/src/api.js(putLastState client call)frontend/src/App.jsx(import + hook call + identifyReady)SPEC.md(new §6.8)
- NOTE: worktree symlinks
backend/.venv+frontend/node_modulesare untracked and were NOT committed (they are not gitignored here, so watch for them on any futuregit add -A— add specific paths).
What the driver needs to know
- Proposed CHANGELOG
Upgrade steps:text:Migration
022_user_session_state.sqlauto-applies on next boot (creates the per-useruser_session_statetable). No new env vars. No operator action required. New behavior: signing in now lands the user on their most-recently-viewed route instead of the empty-state home; a brand-new user (no recorded state) still lands on home. Resume is per-user and on by default; the stored state is route + light view state only (never draft contents — see SPEC §6.8). The per-user opt-out flag ships at the column level (resume_enabled); the profile-settings toggle UI to flip it is a follow-up. - api.py regions edited (merge-overlap awareness): all additions are
in the
/me/auth region — (1) a newLastStateBodyPydantic model inserted just aboveclass BetaRequestBody(~line 65); (2) extra fields appended to theauth_mereturn dict (~line 350, the existing/api/auth/mehandler); (3) a newput_last_statehandler inserted immediately AFTERsubmit_beta_request'sreturn {"ok": True}(~line 435). All far from the propose/PR endpoints 0022.1 edits — 3-way merge should be clean. - App.jsx regions edited: import line (added line ~5); a new
identifyReadyuseState in the state block (~line 50); asetIdentifyReady(true)line inside the existing identify effect + a tiny new effect right after it (~line 100); theuseLastState({...})call right after thegetMe()effect (~line 130). Header region (lines ~220–235, btn-signin-header / nav Links / ) was NOT touched — Session 0019 fast-follow safe. - Deferred: profile opt-out toggle UI (see above).
§19.2 candidates surfaced
- The frontend debounce interval (~1s) and the NON_RESUMABLE_PREFIXES list in useLastState.js are hard-coded frontend constants; if other deployments want them configurable that's a candidate, but it's not flotilla-shaped (no OHM-specific value).
- A generic "remember per-user view state" surface could share substrate
with the device-trust
last_seen_atrefresh pattern; if a future flotilla-core extraction wants a session/state module, this table is a natural member alongside device_trust (already flagged §19.2 in 017).