Files
session-history/ohm/0022/SESSION-0022.2-TRANSCRIPT-2026-05-28T12-04--2026-05-28T12-18.md

7.7 KiB
Raw Permalink Blame History

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, branch feature/v0.23.0-signin-resume based 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

  1. 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.
  2. Inspected migrations. users.id is INTEGER PRIMARY KEY AUTOINCREMENT (001, rebuilt in 012). Timestamp convention is TEXT 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 NO user_sessions table; sessions are signed cookies (rfc_session), so a new user_session_state table keyed on user_id was the right shape.
  3. Read api.py /me region: auth surface uses auth.require_user (401 for anon), /api/auth/me already does a single-round-trip payload (beta + passcode state). db.conn() is isolation_level=None (autocommit) — no explicit commit needed, matching every other write endpoint.
  4. Read auth.py require_user / current_user / SessionUser.
  5. Read the frontend: Login.jsx does a hard window.location.assign after sign-in, so the app re-boots and App.jsx's getMe() reloads me. The #21-Part-C identify fires in an App.jsx effect when me.user.id first 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 decoded last_route_state onto the EXISTING /api/auth/me payload rather than a new GET — smallest/cleanest diff, the frontend already fetches it on boot. When resume_enabled=0, last_route is handed back as null (stored row preserved so re-enabling resumes).
  • PUT /api/me/last-state: require_user (anon → 401). Upsert via INSERT … ON CONFLICT(user_id) DO UPDATE. No-ops (stored=False) when an existing row has resume_enabled=0.
  • Resume redirect ordering: added identifyReady state in App.jsx, flipped true at the end of the identify effect (and immediately on an anonymous me). 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_enabled column + 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 (needed VITE_APP_NAME set for the build — pre-existing requirement, no default shipped; used VITE_APP_NAME=OHM for 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_modules are untracked and were NOT committed (they are not gitignored here, so watch for them on any future git add -A — add specific paths).

What the driver needs to know

  • Proposed CHANGELOG Upgrade steps: text:

    Migration 022_user_session_state.sql auto-applies on next boot (creates the per-user user_session_state table). 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 new LastStateBody Pydantic model inserted just above class BetaRequestBody (~line 65); (2) extra fields appended to the auth_me return dict (~line 350, the existing /api/auth/me handler); (3) a new put_last_state handler inserted immediately AFTER submit_beta_request's return {"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 identifyReady useState in the state block (~line 50); a setIdentifyReady(true) line inside the existing identify effect + a tiny new effect right after it (~line 100); the useLastState({...}) call right after the getMe() effect (~line 130). Header region (lines ~220235, 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_at refresh 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).