6.7 KiB
Session 0094.0 — Transcript
App: ohm Start: 2026-06-09T04-45 (PST) End: 2026-06-09T05-37 (PST) Type: planning-and-executing Status: FINALIZED
Launch prompt (/goal)
Harden the two §9-surfaced framework fragilities as patch release(s) shipped
localhost→PPE→prod: (1) auth.provision_user 500s on a duplicate email — reconcile
by email; (2) the registry/startup reconcile is additive and never prunes removed
projects/collections/entries — add safe prune-on-reconcile that must NOT wipe on a
transient registry read error.
Outcome (one line)
Both fragilities fixed and shipped to OHM prod through the full §9 pipeline: v0.54.1 (provision_user reconciles by email) and v0.55.0 (registry prune-on-reconcile). prod live @ 0.55.0; both 0.55.0 startup prunes a verified no-op (no data loss).
Pre-state
Opened via /goal right after session 0093 finalized. Clean origin/main at
f7b93d7 (v0.54.0). Worked in an isolated worktree off origin/main.
Bug 1 — auth.provision_user dup-email 500 → v0.54.1 (patch)
Root cause: the Gitea-OAuth callback's provision_user matched a users row
only by gitea_id. A human who signed in first via OTC owns an email-only row
(gitea_id NULL); a later OAuth sign-in with the same email missed it and
INSERTed a duplicate → collided on the idx_users_email case-insensitive unique
index → IntegrityError → 500.
Fix: when no gitea_id row exists, reconcile by email and link the OAuth
identity (gitea_id/gitea_login/profile) onto the existing row — the mirror of
otc.provision_or_link_user. Owner-zero (§6.1) bootstrap applied on link
(matching the fresh-insert path); permission_state preserved so linking never
changes admission status as a side effect.
Tests: test_provision_user_email_link.py (3) — OTC→OAuth links onto the same
row (no 500), fresh OAuth still inserts (granted), returning gitea_id user not
duplicated. Ship: PR #50 → merged; tag v0.54.1 + mirror; PPE deploy
(deploys.id=31) + e2e regression green → prod (deploys.id=32, live=0.54.1).
Bug 2 — registry reconcile is additive → v0.55.0 (minor)
Root cause: registry.apply_registry upserts but never deletes. A re-pin to a
different registry stranded the old project's collections + cached entries as dead
rows that, at scale, starved writes (PPE's stale-ecomm 1238-row 524, reset by hand
in 0089).
Design (the careful part): the post-029 schema keys the 13 entry-corpus tables
on collection_id (not project_id), 7 tables keep project_id, and one
non-keyed descendant (thread_messages → threads) exists — established empirically
by building the live schema in a temp DB and classifying every table. New
projects.prune_absent_projects(config, present_ids) deletes a removed project +
its collections + all those rows in one FK-off transaction with a
PRAGMA foreign_key_check backstop that rolls back rather than leave a dangling
ref (mirrors restamp_default_project / reconcile_default_collection_id).
Safety scoping (each a deliberate decision):
- Whole-project granularity only — a present project is untouched (per-entry prune belongs to the best-effort content cache, whose list can transiently fail).
- The default project is never pruned, whatever the registry says.
- Reached only after a successful parse of a non-empty registry
(
parse_registryrejects empty; the read raises on transport error), so a transient read can't wipe. - Prune runs ONLY on the full-reconcile triggers — startup + the periodic
sweep (
prune=True) — never on incidental refreshes (collection create, webhook, in-app create-project;prune=False). This last decision was forced by thetest_s3_scope_roles_verticalregression: that test seeds DB projects directly and creating a collection triggers a refresh — pruning there would have wiped them. (In-app create-project DOES writeprojects.yamlfirst, so real projects are always registry-backed.)
Tests: test_prune_absent_projects.py (5) — removes an absent project + all
its data (FK-integrity backstop asserted), no-op when all present, never removes
default (even on empty present set), empty registry rejected, incidental refresh
(webhook, prune=False) does NOT prune. Ship: PR #51 → merged; tag v0.55.0 +
mirror; PPE deploy (deploys.id=33) — startup prune verified no-op (PPE projects
[ohm] + bdd(3) intact) + e2e regression green → prod (deploys.id=34,
live=0.55.0). Prod projects [ohm, ecomm, rfc-app] all survived the prune (the
session-0090 dogfood rfc-app project is in projects.yaml).
Cut state
- origin
ben.stull/rfc-appmain=c3efe89; tags v0.54.1 + v0.55.0 on origin- benstull mirror; mirror
mainfast-forwarded.
- benstull mirror; mirror
- OHM prod live @ v0.55.0 (
deploys.id=34); PPE @ v0.55.0 (deploys.id=33). Pins: prod.rfc-app-version=0.55.0, PPE.rfc-app-version.ppe=0.55.0. - backend 685 green (+8 across the two fixes); no migrations.
- Worktree removed; no open PRs. Local
maincheckout left atff88be2(behind origin) — untouched.
Deferred decisions
- Owner-zero bootstrap on the email-link path (bug 1): applied it so an
owner-login human who OTC'd first still becomes owner on OAuth (matches the
fresh-insert path). Preserved
permission_staterather than auto-granting a pending OTC row on link — the minimal, no-admission-change choice. Low-confidence on the grant question; preserve felt safest. - Prune scoped to startup + sweep only (bug 2): chose not to prune on every
refresh_registry(incidental refreshes add wipe-risk for no benefit, and broke the s3 vertical). Trade-off: a registry edit removing a project is reconciled at the next startup / periodic sweep, not instantly. Acceptable — the incident is a re-pin (a restart). - Shipped as two separate releases, not one bundle — isolates the destructive prune (0.55.0) from the safe auth fix (0.54.1) for blast-radius. Cost: two PPE→prod cycles.
- PPE metadata.spec SLICE-3 is red on PPE from stale-seed drift (prior runs
mutated bdd priorities; the spec needs a fresh seed) — unrelated to either change;
main-view-ask.spec.js(the auth/sign-in-exercising regression) is green on PPE for both releases.
Plan
(Two §9 fragilities, two focused releases each crossing localhost→PPE→prod — see the bug sections above for the realized design.)
Next-session prompt
The §9-fragility backlog is now empty. Next is a fresh roadmap pick:
/goal Pick the next milestone from ohm-rfc's roadmap (Phase G / next item) and execute it, per the ohm-rfc roadmap repo. (Optionally instead: the deferred full G-5 write-family slug de-dup — slug-keyed branch/thread/change/PR tables → collection-scoped — the larger migration G-15 left out.)