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

139 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).