diff --git a/0022/SESSION-0022.2-TRANSCRIPT-2026-05-28T12-04--2026-05-28T12-18.md b/0022/SESSION-0022.2-TRANSCRIPT-2026-05-28T12-04--2026-05-28T12-18.md new file mode 100644 index 0000000..3ff87a9 --- /dev/null +++ b/0022/SESSION-0022.2-TRANSCRIPT-2026-05-28T12-04--2026-05-28T12-18.md @@ -0,0 +1,138 @@ +# 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 ~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_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).