add 0022/SESSION-0022.2-TRANSCRIPT-2026-05-28T12-04--2026-05-28T12-18.md

This commit is contained in:
Ben Stull
2026-05-28 13:56:50 -07:00
parent 84d021bb30
commit 4602ffab04
@@ -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 ~220235, btn-signin-header / nav Links / <header>) 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).