feat(support): lazy per-workspace support-bot provisioning + is_bot flag (US-069)#32
Merged
Conversation
Add the per-workspace support bot as an ordinary auth.users + workspace_membership(role='member', is_bot=true) row - a FLAG, not a content role (ADR-0008, ADR-0002 intact). - migration 20260624120000_workspace_membership_is_bot.sql: adds is_bot boolean not null default false and a partial unique index (workspace_id) where is_bot - the DB-layer one-bot-per-workspace race guard. is_bot is administrative metadata, never in any visibility predicate; member listings must filter `not is_bot`; room left for an optional explicit write-deny policy. - backend/support_bot.py:provision_workspace_bot(workspace_id) -> bot_user_id: lazy, idempotent provisioning via the GoTrue admin API + service-role PostgREST; fail-closed on missing SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY; orphan-cleanup on a lost race. The returned id populates conversations.bot_user_id. Designed for US-072 to call on first widget-key issuance (not wired here - out of scope). - backend/test_us069_bot_provisioning.py: unit layer (always runs) + integration layer (skips cleanly without local Supabase / is_bot column / API) proving one bot per workspace across two calls, role=member, member-listing exclusion, no bot content-role, and the partial-unique-index race guard. - main.py: note SUPABASE_SERVICE_ROLE_KEY is also the provisioning key. - AGENTS.md: US-069 invariant section.
…rphan-cleanup status failures
…ice-role bot-provisioning use
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Retrieval eval — PR vs
|
| Mode | recall@5 | MRR | nDCG@5 |
|---|---|---|---|
| vector | 0.860 (±0.000) | 0.772 (±0.000) | 0.779 (±0.000) |
| keyword | 0.110 (±0.000) | 0.120 (±0.000) | 0.112 (±0.000) |
| hybrid | 0.860 (±0.000) | 0.759 (±0.000) | 0.769 (±0.000) |
Per-category recall@5
| Mode | single_chunk | multi_hop | adversarial | paraphrase |
|---|---|---|---|---|
| vector | 0.900 (±0.000) | 0.933 (±0.000) | 0.600 (±0.000) | 1.000 (±0.000) |
| keyword | 0.250 (±0.000) | 0.033 (±0.000) | 0.000 (±0.000) | 0.000 (±0.000) |
| hybrid | 0.900 (±0.000) | 0.933 (±0.000) | 0.600 (±0.000) | 1.000 (±0.000) |
Comment is updated in place on each push by .github/workflows/retrieval-eval.yml (US-035). Comment-only — never blocks the build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
20260624120000_workspace_membership_is_bot.sql: ais_bot boolean not null default falseflag onworkspace_membership(administrative metadata only, never in any visibility/retrieval predicate) plus a partial unique indexworkspace_membership_one_bot_per_workspace ... where is_botthat enforces exactly one bot per workspace at the DB layer as the race-loss serialization point.backend/support_bot.py:provision_workspace_bot(workspace_id)- an async, lazy, idempotent primitive that creates the botauth.usersrow via the GoTrue admin API (service-role key, fail-closed at call time) then inserts arole='member'membership row; a lost race is gated on the23505unique-violation marker (other 409s, e.g. FK23503, surface as errors), the orphanauth.usersrow is dropped with status-failure logging, and the existing bot id is returned. Backed bybackend/test_us069_bot_provisioning.py(unit + skip-clean integration layers).SUPPORT_BOT_EMAIL_DOMAINenv (defaultbots.support.internal) and documented the service-role key's bot-provisioning use acrossbackend/.env.example,backend/main.py,README.md, andAGENTS.md.Risk Assessment
✅ Low: Well-bounded new support-bot provisioning primitive with a DB-enforced one-bot-per-workspace guard; the two prior-round findings were correctly and minimally fixed, and no new material issues remain.
Testing
Ran the existing US-069 unit+integration test against a real running local Supabase (migration already applied) - all 11 assertions pass - then produced reviewer-visible product evidence with two harnesses that drive the real provisioning primitive against the live GoTrue admin API and Postgres: the first captures the full lifecycle (no bot at creation, a real GoTrue auth.users bot row with is_support_bot/workspace_id app_metadata, idempotent second call, member-listing exclusion, DB race guard), and the second exercises the two branches the review-fix commit changed using real PostgREST SQLSTATE responses (23505 race loss returns the winner and cleans up the orphan; a 23503 FK 409 raises rather than being swallowed). No rendered UI surface exists for this backend/DB change, so evidence is CLI transcripts plus JSON of persisted DB state and live API responses rather than screenshots. All fixtures were cleaned up and the worktree is clean; no issues found.
Evidence: End-to-end provisioning lifecycle transcript (real Supabase: GoTrue user + persisted is_bot membership, idempotency, member-listing exclusion, DB race guard)
=== 2. provision_workspace_bot(W) -> bot id === returned_bot_id: f7512fed-a31b-4897-a277-1fd5d1c49939 persisted_membership: [human role=member is_bot=False, f7512fed... role=member is_bot=True] === 3. bot id resolves to a REAL auth.users row (GoTrue admin GET) === http_status: 200 email: support-bot-1bdc26eb...@bots.support.internal app_metadata: {'is_support_bot': True, 'workspace_id': '368c0ad0-...'} user_metadata: {'display_name': 'Support Bot'} === 4. Second provision idempotent: same id, ONE is_bot row === ids_equal: True is_bot_count: 1 === 5. Member listing (not is_bot) excludes bot, keeps human === === 6. DB race guard: second is_bot insert rejected by workspace_membership_one_bot_per_workspace ===Evidence: Review-fix (e4eb66c) branch transcript: 23505 race loss vs non-23505 FK 409, with real PostgREST SQLSTATE bodies
=== A. Genuine lost race: insert hits 23505 -> returns WINNER, drops orphan === returned_is_winner: True orphan_auth_user_cleaned_up: True is_bot_rows_for_workspace: 1 === B. Non-23505 (23503 FK violation): provision RAISES, NOT swallowed as race loss; orphan dropped === raised_runtime_error: True error_snippet: support-bot provisioning failed during membership insert: HTTP 409 {"code":"23503","details":"Key (workspace_id)=(...) is not present in table "workspaces"." mentions_23503: True orphan_auth_user_cleaned_up: TrueEvidence: Lifecycle evidence JSON (machine-readable persisted state per step)
Evidence: Race/error-path evidence JSON
Pipeline
Updates from git push no-mistakes
⏭️ **intent** - skipped
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed ✅
backend/support_bot.py:214- _insert_bot_membership treats any HTTP 409 as a one-bot-per-workspace race loss, but PostgREST also returns 409 for foreign-key violations (SQLSTATE 23503). workspace_membership.workspace_id references public.workspaces(id), so a valid-UUID-but-nonexistent workspace creates an auth.users row, FK-fails the membership insert (409 -> return False), deletes the bot, then raises the misleading 'membership conflicted but no existing bot was found ... (unexpected partial state)' instead of a clear 'workspace does not exist'. Gate the return-False on the 23505 marker specifically and let other 409s fall through to _provisioning_error. The two '23505' text sub-checks are also dead given the bare status_code==409, and '"23505"' in text is subsumed by '23505' in text.backend/support_bot.py:233- _delete_bot_user is documented as best-effort with a warning log, but it never calls raise_for_status, so a non-2xx DELETE response (4xx/5xx from the GoTrue admin API) returns silently with no log - only transport errors (httpx.HTTPError) reach the warning. Orphan auth.users rows are harmless but accumulate invisibly when the most likely failure mode (an error status) leaves no breadcrumb. Check the response status (e.g. r.raise_for_status() inside the try) so HTTP-status failures also log.🔧 Fix: gate membership-insert race loss on 23505, log orphan-cleanup status failures
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
python3 -m backend.test_us069_bot_provisioning(unit + integration against running local Supabase; all 11 assertions pass)End-to-end lifecycle harnessus069_evidence.py: seed workspace with no bot, provision twice, fetch the real GoTrue user via admin API, verify idempotency, member-listing exclusion (where not is_bot), and the partial-unique-index race guardReview-fix branch harnessus069_raceloss_evidence.py: forced a genuine 23505 race loss (provision returns winner, drops orphan auth.users row) and a non-23505 409 (real 23503 FK violation -> RuntimeError raised, orphan dropped)Verified no leaked DB fixtures (0 support-bot users, 0 is_bot rows, 0 US069 workspaces, 0 @test.local users) and a clean worktree viagit status --porcelain✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.