From 0a21aac762736f7585d7b0aeca8f0cc91d3f6990 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 12 May 2026 18:38:59 -0700 Subject: [PATCH] Remove freebuff waiting room flag --- .env.example | 3 + cli/src/app.tsx | 4 +- cli/src/components/waiting-room-screen.tsx | 8 +- cli/src/hooks/helpers/send-message.ts | 2 +- cli/src/hooks/use-freebuff-session.ts | 3 +- cli/src/utils/error-handling.ts | 2 +- common/src/types/freebuff-session.ts | 12 +- docs/freebuff-waiting-room.md | 21 +--- packages/internal/src/env-schema.ts | 11 +- .../completions/__tests__/completions.test.ts | 7 +- web/src/app/api/v1/chat/completions/_post.ts | 8 +- .../session/__tests__/session.test.ts | 11 -- .../app/api/v1/freebuff/session/_handlers.ts | 8 +- .../free-session/__tests__/public-api.test.ts | 108 +----------------- web/src/server/free-session/admission.ts | 8 -- web/src/server/free-session/config.ts | 16 --- web/src/server/free-session/public-api.ts | 42 +------ 17 files changed, 35 insertions(+), 239 deletions(-) diff --git a/.env.example b/.env.example index 17aba42c79..99db8ded61 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,9 @@ LINKUP_API_KEY=dummy_linkup_key LOOPS_API_KEY=dummy_loops_key ZEROCLICK_API_KEY=dummy_zeroclick_key +# Freebuff +FREEBUFF_SESSION_LENGTH_MS=3600000 + # Discord Integration DISCORD_PUBLIC_KEY=dummy_discord_public_key DISCORD_BOT_TOKEN=dummy_discord_bot_token diff --git a/cli/src/app.tsx b/cli/src/app.tsx index 1d112af381..df5fe7c1c7 100644 --- a/cli/src/app.tsx +++ b/cli/src/app.tsx @@ -261,7 +261,7 @@ export const App = ({ } // Render project picker FIRST when at home directory or outside a project. - // This deliberately precedes the login/auth and waiting-room gates so the + // This deliberately precedes the login/auth and freebuff session gate so the // user always gets to pick a working directory before anything else — auth // failures or a banned/queued freebuff session would otherwise replace the // picker mid-flash and look like being kicked out of the app. @@ -340,7 +340,7 @@ interface AuthedSurfaceProps { } /** - * Rendered only after auth is confirmed. Owns the freebuff waiting-room gate + * Rendered only after auth is confirmed. Owns the freebuff session gate * so `useFreebuffSession` runs exactly once per authed session (not before * we have a token). */ diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx index 555dfca7c2..105bda2df6 100644 --- a/cli/src/components/waiting-room-screen.tsx +++ b/cli/src/components/waiting-room-screen.tsx @@ -496,11 +496,11 @@ export const WaitingRoomScreen: React.FC = ({ )} - {/* Server says the waiting room is disabled — this screen should not - normally render in that case, but show a minimal message just in - case App.tsx's guard is bypassed. */} + {/* Compatibility fallback for older servers without the session + endpoint. This should not normally render because App.tsx treats + it as admitted. */} {session?.status === 'disabled' && ( - Waiting room disabled. + Session gate unavailable. )} {/* Country outside the free-mode allowlist. Terminal — polling has diff --git a/cli/src/hooks/helpers/send-message.ts b/cli/src/hooks/helpers/send-message.ts index 0265e9fdf6..9712a21316 100644 --- a/cli/src/hooks/helpers/send-message.ts +++ b/cli/src/hooks/helpers/send-message.ts @@ -523,7 +523,7 @@ export const handleRunError = (params: { } /** - * Surface + recover from a waiting-room gate rejection. The server rejected + * Surface + recover from a freebuff session gate rejection. The server rejected * the request because our seat is no longer valid; update local state so the * UI reflects reality and we stop sending requests until we re-admit. */ diff --git a/cli/src/hooks/use-freebuff-session.ts b/cli/src/hooks/use-freebuff-session.ts index fd82a03c62..0a53f2bebb 100644 --- a/cli/src/hooks/use-freebuff-session.ts +++ b/cli/src/hooks/use-freebuff-session.ts @@ -73,8 +73,7 @@ async function callSession( signal: opts.signal, }) // 404 = endpoint not deployed on this server (older web build). Treat as - // "waiting room disabled" so a newer CLI against an older server still - // works, rather than stranding users in a waiting room forever. + // a compatibility bypass so a newer CLI against an older server still works. if (resp.status === 404) { return { status: 'disabled' } } diff --git a/cli/src/utils/error-handling.ts b/cli/src/utils/error-handling.ts index 2d25ae14db..404225298e 100644 --- a/cli/src/utils/error-handling.ts +++ b/cli/src/utils/error-handling.ts @@ -96,7 +96,7 @@ export const getCountryBlockFromFreeModeError = ( } /** - * Freebuff waiting-room gate errors returned by /api/v1/chat/completions. + * Freebuff session gate errors returned by /api/v1/chat/completions. * * Contract (see docs/freebuff-waiting-room.md): * - 428 `waiting_room_required` — no session row exists; POST /session to join. diff --git a/common/src/types/freebuff-session.ts b/common/src/types/freebuff-session.ts index 0ba7399c5b..ef23095e70 100644 --- a/common/src/types/freebuff-session.ts +++ b/common/src/types/freebuff-session.ts @@ -1,11 +1,9 @@ import type { FreebuffAccessTier } from '../constants/freebuff-models' /** - * Wire-level shapes returned by `/api/v1/freebuff/session`. Source of truth - * for the CLI (which deserializes these) and the server (which serializes - * them) — keep both in sync by importing this module from either side. - * - * The CLI uses these shapes directly; there are no client-only states. + * Shapes used by `/api/v1/freebuff/session` and the CLI. Most variants are + * wire-level responses serialized by the server; explicitly documented + * compatibility variants may be synthesized by the CLI for older servers. */ /** @@ -67,8 +65,8 @@ export type FreebuffIpPrivacySignal = export type FreebuffSessionServerResponse = | { - /** Waiting room is globally off; free-mode requests flow through - * unchanged. Client should treat this as "admitted forever". */ + /** Compatibility fallback for older servers without the session + * endpoint. Client should treat this as "admitted forever". */ status: 'disabled' } | { diff --git a/docs/freebuff-waiting-room.md b/docs/freebuff-waiting-room.md index 25999fb339..8d29a5a021 100644 --- a/docs/freebuff-waiting-room.md +++ b/docs/freebuff-waiting-room.md @@ -8,22 +8,14 @@ The waiting room is the admission control layer for **free-mode** requests again 2. **Gate on per-deployment health and hours** — a single fleet probe per tick (`getFleetHealth` in `web/src/server/free-session/fireworks-health.ts`) hits the Fireworks metrics endpoint and classifies each dedicated deployment as `healthy | degraded | unhealthy`. Only models whose deployment is `healthy` and currently available admit that tick; GLM 5.1 is available during 9am ET-5pm PT on weekdays, while MiniMax M2.7 is serverless and always available. 3. **One instance per account** — prevent a single user from running N concurrent freebuff CLIs to get N× throughput. -Users who cannot be admitted immediately are placed in the queue for their chosen model and given an estimated wait time. Admitted users get a fixed-length session (default 1h) bound to the model they were admitted on; chat completions use that model for the life of the session. +Users who cannot be admitted immediately are placed in the queue for their chosen model and given an estimated wait time. With the current high instant-admit capacities, most users go straight from model selection to an active session; the queue only appears when a model is actually saturated. Admitted users get a fixed-length session (default 1h) bound to the model they were admitted on; chat completions use that model for the life of the session. -The entire system is gated by the env flag `FREEBUFF_WAITING_ROOM_ENABLED`. When `false`, the gate is a no-op and the admission ticker does not start; free-mode traffic flows through unchanged. - -## Kill Switch +## Configuration ```bash -# Disable entirely (both the gate on chat/completions and the admission loop) -FREEBUFF_WAITING_ROOM_ENABLED=false - -# Other knob (only read when enabled) FREEBUFF_SESSION_LENGTH_MS=3600000 # 1 hour ``` -Flipping the flag is safe at runtime: existing rows stay in the DB and will be admitted / expired correctly whenever the flag is flipped back on. - ## Architecture ```mermaid @@ -186,9 +178,6 @@ Before any of those state transitions, the handler requires a resolved allowlist Response shapes: ```jsonc -// Waiting room disabled — CLI should treat this as "always admitted" -{ "status": "disabled" } - // In queue { "status": "queued", @@ -272,9 +261,7 @@ For free-mode requests (`codebuff_metadata.cost_mode === 'free'`), `_post.ts` ca | 409 | `session_superseded` | Claimed `instance_id` does not match stored one — another CLI took over. | | 410 | `session_expired` | `expires_at + grace < now()` (past the hard cutoff). Client should POST /session to re-queue. | -Successful results carry one of three reasons: `disabled` (gate is off), `active` (`expires_at > now()`, `remainingMs` provided), or `draining` (`expires_at <= now() < expires_at + grace`, `gracePeriodRemainingMs` provided). The CLI should treat `draining` as "let any in-flight agent run finish, but block new user prompts" — see [Drain / Grace Window](#drain--grace-window) below. The corresponding wire status from `getSessionState` is `ended`. - -When the waiting room is disabled, the gate returns `{ ok: true, reason: 'disabled' }` without touching the DB. +Successful results carry one of two reasons: `active` (`expires_at > now()`, `remainingMs` provided), or `draining` (`expires_at <= now() < expires_at + grace`, `gracePeriodRemainingMs` provided). The CLI should treat `draining` as "let any in-flight agent run finish, but block new user prompts" — see [Drain / Grace Window](#drain--grace-window) below. The corresponding wire status from `getSessionState` is `ended`. ## Drain / Grace Window @@ -314,8 +301,6 @@ The CLI: 8. **Handles chat-gate errors:** the same statuses are reachable via the gate's 409/410/428/429 for fast in-flight feedback, and the CLI calls the matching `markFreebuff*` helper to flip local state without waiting for the next poll. 9. **On clean exit**, calls `DELETE /api/v1/freebuff/session` so the next user can be admitted sooner. -The `disabled` response means the server has the waiting room turned off. CLI treats it identically to `active` with infinite remaining time — no countdown, and chat requests can omit `freebuff_instance_id` entirely. - ## Multi-pod Behavior - **`/api/v1/freebuff/session` routes** are stateless per pod; all state lives in Postgres. Any pod can serve any request. diff --git a/packages/internal/src/env-schema.ts b/packages/internal/src/env-schema.ts index b09d67c4ea..8f932e3ea5 100644 --- a/packages/internal/src/env-schema.ts +++ b/packages/internal/src/env-schema.ts @@ -56,14 +56,6 @@ export const serverEnvSchema = clientEnvSchema.extend({ // sweep but risks rate-limiting. BOT_SWEEP_GITHUB_TOKEN: z.string().min(1).optional(), - // Freebuff waiting room. Defaults to OFF so the feature requires explicit - // opt-in per environment — the CLI/SDK do not yet send - // freebuff_instance_id, so enabling this before they ship would reject - // every free-mode request with 428 waiting_room_required. - FREEBUFF_WAITING_ROOM_ENABLED: z - .enum(['true', 'false']) - .default('false') - .transform((v) => v === 'true'), FREEBUFF_SESSION_LENGTH_MS: z.coerce .number() .int() @@ -136,8 +128,7 @@ export const serverProcessEnv: ServerInput = { BOT_SWEEP_SECRET: process.env.BOT_SWEEP_SECRET, BOT_SWEEP_GITHUB_TOKEN: process.env.BOT_SWEEP_GITHUB_TOKEN, - // Freebuff waiting room - FREEBUFF_WAITING_ROOM_ENABLED: process.env.FREEBUFF_WAITING_ROOM_ENABLED, + // Freebuff session gate FREEBUFF_SESSION_LENGTH_MS: process.env.FREEBUFF_SESSION_LENGTH_MS, FREEBUFF_DEV_FORCE_LIMITED: process.env.FREEBUFF_DEV_FORCE_LIMITED, } diff --git a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts index 80ca4f02d1..01455607d3 100644 --- a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts +++ b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts @@ -82,11 +82,10 @@ describe('/api/v1/chat/completions POST endpoint', () => { let mockInsertMessageBigquery: InsertMessageBigqueryFn let nextQuotaReset: string - // Bypasses the freebuff waiting-room gate in tests that exercise free-mode - // flow without seeding a session. Matches the real return for the disabled - // path so downstream logic proceeds normally. + // Bypasses the freebuff session gate in tests that exercise free-mode flow + // without seeding a session. const mockCheckSessionAdmissibleAllow = async () => - ({ ok: true, reason: 'disabled' }) as const + ({ ok: true, reason: 'active', remainingMs: 60 * 60 * 1000 }) as const const mockResolveFreeModeCountryAccess = async ( _userId: string, req: Parameters[0], diff --git a/web/src/app/api/v1/chat/completions/_post.ts b/web/src/app/api/v1/chat/completions/_post.ts index 7b5a8a9ebc..c4069a2624 100644 --- a/web/src/app/api/v1/chat/completions/_post.ts +++ b/web/src/app/api/v1/chat/completions/_post.ts @@ -183,7 +183,7 @@ export async function postChatCompletions(params: { logger: Logger }) => Promise getUserPreferences?: GetUserPreferencesFn - /** Optional override for the freebuff waiting-room gate. Defaults to the + /** Optional override for the freebuff session gate. Defaults to the * real check backed by Postgres; tests inject a no-op. */ checkSessionAdmissible?: CheckSessionAdmissibleFn /** Optional override for the free-mode rate limiter. Tests inject this to @@ -527,16 +527,14 @@ export async function postChatCompletions(params: { let freeModeSessionGate: SessionGateResult | null = null - // Freebuff waiting-room gate. Usually enforced only when - // FREEBUFF_WAITING_ROOM_ENABLED=true. Runs before the rate limiter so - // rejected requests don't burn a queued user's free-mode counters. + // Freebuff session gate. Runs before the rate limiter so rejected requests + // don't burn a queued user's free-mode counters. if (isFreeModeRequest) { const claimedInstanceId = typedBody.codebuff_metadata?.freebuff_instance_id freeModeSessionGate = await checkSession({ userId, accessTier: freebuffAccessTier, - userEmail: userInfo.email, claimedInstanceId, requestedModel: typedBody.model, requireActiveSession: isFreebuffGeminiThinkerAgent(agentId), diff --git a/web/src/app/api/v1/freebuff/session/__tests__/session.test.ts b/web/src/app/api/v1/freebuff/session/__tests__/session.test.ts index 00c1d15889..631f082799 100644 --- a/web/src/app/api/v1/freebuff/session/__tests__/session.test.ts +++ b/web/src/app/api/v1/freebuff/session/__tests__/session.test.ts @@ -102,7 +102,6 @@ function makeSessionDeps(overrides: Partial = {}): SessionDeps & { let instanceCounter = 0 return { rows, - isWaitingRoomEnabled: () => true, graceMs: 30 * 60 * 1000, sessionLengthMs: 60 * 60 * 1000, // Keep instant-admit disabled in handler tests — they verify queue/state @@ -225,16 +224,6 @@ describe('POST /api/v1/freebuff/session', () => { }) }) - test('returns disabled when waiting room flag is off', async () => { - const sessionDeps = makeSessionDeps({ isWaitingRoomEnabled: () => false }) - const resp = await postFreebuffSession( - makeReq('ok'), - makeDeps(sessionDeps, 'u1'), - ) - const body = await resp.json() - expect(body.status).toBe('disabled') - }) - test('creates a limited DeepSeek Flash session for disallowed country', async () => { const sessionDeps = makeSessionDeps() const resp = await postFreebuffSession( diff --git a/web/src/app/api/v1/freebuff/session/_handlers.ts b/web/src/app/api/v1/freebuff/session/_handlers.ts index 196c0aab03..b87e9277cf 100644 --- a/web/src/app/api/v1/freebuff/session/_handlers.ts +++ b/web/src/app/api/v1/freebuff/session/_handlers.ts @@ -74,7 +74,7 @@ export interface FreebuffSessionDeps { type AuthResult = | { error: NextResponse } - | { userId: string; userEmail: string | null; userBanned: boolean } + | { userId: string; userBanned: boolean } async function resolveUser( req: NextRequest, @@ -94,7 +94,7 @@ async function resolveUser( } const userInfo = await deps.getUserInfoFromApiKey({ apiKey, - fields: ['id', 'email', 'banned'], + fields: ['id', 'banned'], logger: deps.logger, }) if (!userInfo?.id) { @@ -107,7 +107,6 @@ async function resolveUser( } return { userId: String(userInfo.id), - userEmail: userInfo.email ?? null, userBanned: Boolean(userInfo.banned), } } @@ -160,7 +159,6 @@ export async function postFreebuffSession( try { const state = await requestSession({ userId: auth.userId, - userEmail: auth.userEmail, userBanned: auth.userBanned, model: requestedModel, accessTier, @@ -207,7 +205,6 @@ export async function getFreebuffSession( const state = await getSessionState({ userId: auth.userId, accessTier, - userEmail: auth.userEmail, userBanned: auth.userBanned, claimedInstanceId, deps: deps.sessionDeps, @@ -244,7 +241,6 @@ export async function deleteFreebuffSession( try { await endUserSession({ userId: auth.userId, - userEmail: auth.userEmail, deps: deps.sessionDeps, }) return NextResponse.json({ status: 'ended' }, { status: 200 }) diff --git a/web/src/server/free-session/__tests__/public-api.test.ts b/web/src/server/free-session/__tests__/public-api.test.ts index b85c682cb3..00a3c0b8aa 100644 --- a/web/src/server/free-session/__tests__/public-api.test.ts +++ b/web/src/server/free-session/__tests__/public-api.test.ts @@ -72,7 +72,6 @@ function makeDeps(overrides: Partial = {}): SessionDeps & { currentNow = n }, _now: () => currentNow, - isWaitingRoomEnabled: () => true, graceMs: GRACE_MS, sessionLengthMs: SESSION_LEN, // Test default: instant-admit disabled (capacity 0) so existing FIFO @@ -227,17 +226,6 @@ describe('requestSession', () => { deps = makeDeps() }) - test('disabled flag returns { status: disabled } and does not touch DB', async () => { - const offDeps = makeDeps({ isWaitingRoomEnabled: () => false }) - const state = await requestSession({ - userId: 'u1', - model: DEFAULT_MODEL, - deps: offDeps, - }) - expect(state).toEqual({ status: 'disabled' }) - expect(offDeps.rows.size).toBe(0) - }) - test('banned user is rejected before joinOrTakeOver runs', async () => { const state = await requestSession({ userId: 'u1', @@ -897,12 +885,6 @@ describe('getSessionState', () => { deps = makeDeps() }) - test('disabled flag returns disabled', async () => { - const offDeps = makeDeps({ isWaitingRoomEnabled: () => false }) - const state = await getSessionState({ userId: 'u1', deps: offDeps }) - expect(state).toEqual({ status: 'disabled' }) - }) - test('banned user returns banned without hitting the DB', async () => { const state = await getSessionState({ userId: 'u1', @@ -1182,30 +1164,6 @@ describe('checkSessionAdmissible', () => { deps = makeDeps() }) - test('disabled flag → ok with reason=disabled', async () => { - const offDeps = makeDeps({ isWaitingRoomEnabled: () => false }) - const result = await checkSessionAdmissible({ - userId: 'u1', - claimedInstanceId: undefined, - deps: offDeps, - }) - expect(result.ok).toBe(true) - }) - - test('requireActiveSession ignores disabled shortcut and requires a row', async () => { - const offDeps = makeDeps({ isWaitingRoomEnabled: () => false }) - const result = await checkSessionAdmissible({ - userId: 'u1', - claimedInstanceId: 'inst-1', - requestedModel: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, - requireActiveSession: true, - deps: offDeps, - }) - expect(result.ok).toBe(false) - if (result.ok) throw new Error('unreachable') - expect(result.code).toBe('waiting_room_required') - }) - test('no session → waiting_room_required', async () => { const result = await checkSessionAdmissible({ userId: 'u1', @@ -1217,51 +1175,9 @@ describe('checkSessionAdmissible', () => { expect(result.code).toBe('waiting_room_required') }) - test('bypassed email (team@codebuff.com) → ok with reason=disabled, no DB read', async () => { - const result = await checkSessionAdmissible({ - userId: 'u1', - userEmail: 'team@codebuff.com', - claimedInstanceId: undefined, - deps, - }) - expect(result.ok).toBe(true) - if (!result.ok) throw new Error('unreachable') - expect(result.reason).toBe('disabled') - expect(deps.rows.size).toBe(0) - }) - - test('requireActiveSession ignores bypassed emails', async () => { - const result = await checkSessionAdmissible({ - userId: 'u1', - userEmail: 'team@codebuff.com', - claimedInstanceId: 'inst-1', - requestedModel: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, - requireActiveSession: true, - deps, - }) - expect(result.ok).toBe(false) - if (result.ok) throw new Error('unreachable') - expect(result.code).toBe('waiting_room_required') - }) - - test('bypassed email is case-insensitive', async () => { - const result = await checkSessionAdmissible({ - userId: 'u1', - userEmail: 'Team@Codebuff.COM', - claimedInstanceId: undefined, - deps, - }) - expect(result.ok).toBe(true) - }) - - test('requireActiveSession still admits Gemini thinker for smart model rows when waiting room is disabled', async () => { - // requireActiveSession=true forces a DB-backed row check even when the - // waiting room is globally off — the gemini-thinker child agent uses this - // path so its Gemini Pro call only succeeds when the parent session is - // bound to one of the smart freebuff models (Kimi or DeepSeek). - const offDeps = makeDeps({ isWaitingRoomEnabled: () => false }) - const now = offDeps._now() - offDeps.rows.set('u1', { + test('active smart model session admits Gemini thinker requests', async () => { + const now = deps._now() + deps.rows.set('u1', { user_id: 'u1', status: 'active', active_instance_id: 'inst-1', @@ -1278,7 +1194,7 @@ describe('checkSessionAdmissible', () => { claimedInstanceId: 'inst-1', requestedModel: FREEBUFF_GEMINI_PRO_MODEL_ID, requireActiveSession: true, - deps: offDeps, + deps, }) expect(result.ok).toBe(true) }) @@ -1517,20 +1433,4 @@ describe('endUserSession', () => { expect(deps.admits[0]?.session_units).toBe(0.3) }) - test('is no-op when disabled', async () => { - const deps = makeDeps({ isWaitingRoomEnabled: () => false }) - deps.rows.set('u1', { - user_id: 'u1', - status: 'active', - active_instance_id: 'x', - model: DEFAULT_MODEL, - queued_at: new Date(), - admitted_at: null, - expires_at: null, - created_at: new Date(), - updated_at: new Date(), - }) - await endUserSession({ userId: 'u1', deps }) - expect(deps.rows.has('u1')).toBe(true) - }) }) diff --git a/web/src/server/free-session/admission.ts b/web/src/server/free-session/admission.ts index afa2328af0..5b409a96ef 100644 --- a/web/src/server/free-session/admission.ts +++ b/web/src/server/free-session/admission.ts @@ -7,7 +7,6 @@ import { ADMISSION_TICK_MS, getSessionGraceMs, getSessionLengthMs, - isWaitingRoomEnabled, } from './config' import { getFleetHealth } from './fireworks-health' import { @@ -189,13 +188,6 @@ function runTick() { export function startFreeSessionAdmission(): boolean { if (interval) return true - if (!isWaitingRoomEnabled()) { - logger.info( - {}, - '[FreeSessionAdmission] Waiting room disabled — ticker not started', - ) - return false - } interval = setInterval(runTick, ADMISSION_TICK_MS) if (typeof interval.unref === 'function') interval.unref() runTick() // fire first tick immediately diff --git a/web/src/server/free-session/config.ts b/web/src/server/free-session/config.ts index da51cee0e7..4e5e4c0e95 100644 --- a/web/src/server/free-session/config.ts +++ b/web/src/server/free-session/config.ts @@ -20,22 +20,6 @@ export const FREEBUFF_ADMISSION_LOCK_ID = 573924815 export const ADMISSION_TICK_MS = 15_000 export const SESSION_GRACE_MS = 30 * 60 * 1000 -export function isWaitingRoomEnabled(): boolean { - return env.FREEBUFF_WAITING_ROOM_ENABLED -} - -/** Per-account override on top of the global kill switch. The internal - * `team@codebuff.com` account drives e2e tests in CI; landing it in the - * queue would make those tests flake whenever the waiting room is warm. - * Bypassed users behave exactly as if the waiting room were disabled. */ -const WAITING_ROOM_BYPASS_EMAILS = new Set(['team@codebuff.com']) -export function isWaitingRoomBypassedForEmail( - email: string | null | undefined, -): boolean { - if (!email) return false - return WAITING_ROOM_BYPASS_EMAILS.has(email.toLowerCase()) -} - export function getSessionLengthMs(): number { return env.FREEBUFF_SESSION_LENGTH_MS } diff --git a/web/src/server/free-session/public-api.ts b/web/src/server/free-session/public-api.ts index ccd5c16214..12ffdc103b 100644 --- a/web/src/server/free-session/public-api.ts +++ b/web/src/server/free-session/public-api.ts @@ -24,8 +24,6 @@ import { getInstantAdmitCapacity, getSessionGraceMs, getSessionLengthMs, - isWaitingRoomBypassedForEmail, - isWaitingRoomEnabled, } from './config' import { activeCountForModel, @@ -246,7 +244,6 @@ export interface SessionDeps { * force-enable / force-disable instant admit without mutating the * shared model registry. */ getInstantAdmitCapacity: (model: string) => number - isWaitingRoomEnabled: () => boolean /** Plain values, not getters: these never change at runtime. The deps * interface uses values rather than thunks so tests can pass numbers * inline without wrapping. */ @@ -265,7 +262,6 @@ const defaultDeps: SessionDeps = { listRecentPremiumAdmits, promoteQueuedUser, getInstantAdmitCapacity, - isWaitingRoomEnabled, get graceMs() { // Read-through getter keeps the default deps aligned with config while // tests can still inject a plain graceMs value through SessionDeps. @@ -348,8 +344,6 @@ export type RequestSessionResult = /** * Client calls this on CLI startup with the model they want to use. * Semantics: - * - Waiting room disabled → { status: 'disabled' } (model still respected - * downstream by chat-completions) * - No existing session → create queued row for `model`, fresh instance_id * - Existing active (unexpired), same model → rotate instance_id (takeover) * - Existing active (unexpired), different model → { status: 'model_locked' } @@ -366,7 +360,6 @@ export async function requestSession(params: { userId: string model: string accessTier?: FreebuffAccessTier - userEmail?: string | null | undefined countryAccess?: FreeSessionCountryAccessMetadata /** True if the account is banned. Short-circuited here so banned bots never * create a queued row — otherwise they inflate `queueDepth` between the @@ -381,12 +374,6 @@ export async function requestSession(params: { if (params.userBanned) { return { status: 'banned' } } - if ( - !deps.isWaitingRoomEnabled() || - isWaitingRoomBypassedForEmail(params.userEmail) - ) { - return { status: 'disabled' } - } // Rate-limit check runs before joinOrTakeOver so heavy users never even // create a queued row. Premium models share one daily Pacific-time @@ -549,7 +536,6 @@ async function attachRateLimit( * active capacity after the CLI receives `none`. * * Returns: - * - `disabled` when the waiting room is off * - `none` when the user has no row at all (or the row was swept past * the grace window) * - `superseded` when the caller's id no longer matches the stored one @@ -559,7 +545,6 @@ async function attachRateLimit( export async function getSessionState(params: { userId: string accessTier?: FreebuffAccessTier - userEmail?: string | null | undefined userBanned?: boolean claimedInstanceId?: string | null | undefined deps?: SessionDeps @@ -569,12 +554,6 @@ export async function getSessionState(params: { if (params.userBanned) { return { status: 'banned' } } - if ( - !deps.isWaitingRoomEnabled() || - isWaitingRoomBypassedForEmail(params.userEmail) - ) { - return { status: 'disabled' } - } const row = await deps.getSessionRow(params.userId) // Build a `none` response with live queue depths so the CLI's pre-join @@ -622,16 +601,9 @@ export async function getSessionState(params: { export async function endUserSession(params: { userId: string - userEmail?: string | null | undefined deps?: SessionDeps }): Promise { const deps = params.deps ?? defaultDeps - if ( - !deps.isWaitingRoomEnabled() || - isWaitingRoomBypassedForEmail(params.userEmail) - ) { - return - } await deps.endSession({ userId: params.userId, now: nowOf(deps), @@ -640,7 +612,6 @@ export async function endUserSession(params: { } export type SessionGateResult = - | { ok: true; reason: 'disabled' } | { ok: true; reason: 'active'; remainingMs: number } | { ok: true @@ -673,11 +644,9 @@ export type SessionGateResult = export async function checkSessionAdmissible(params: { userId: string accessTier?: FreebuffAccessTier - userEmail?: string | null | undefined claimedInstanceId: string | null | undefined - /** Forces a real active session row check even when the waiting room is - * globally disabled or the user email normally bypasses it. Use for - * subagent/model combinations that must be bound to trusted session state. */ + /** Forces a real active session row check for subagent/model combinations + * that must be bound to trusted session state. */ requireActiveSession?: boolean /** Model the chat-completions request is for. When provided, the gate * rejects requests whose model doesn't match the active session's model @@ -687,13 +656,6 @@ export async function checkSessionAdmissible(params: { }): Promise { const deps = params.deps ?? defaultDeps const accessTier = params.accessTier ?? 'full' - if ( - !params.requireActiveSession && - (!deps.isWaitingRoomEnabled() || - isWaitingRoomBypassedForEmail(params.userEmail)) - ) { - return { ok: true, reason: 'disabled' } - } // Pre-waiting-room CLIs never send a freebuff_instance_id. Classify that up // front so the caller gets a distinct code (→ 426 Upgrade Required) and the