(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
* 15s admission ticks that run `evictBanned`. */
userBanned?: boolean
deps?: SessionDeps
})
| 363 | * a non-null view (queued or active-unexpired), so the cast below is sound. |
| 364 | */ |
| 365 | export async function requestSession(params: { |
| 366 | userId: string |
| 367 | model: string |
| 368 | accessTier?: FreebuffAccessTier |
| 369 | userEmail?: string | null | undefined |
| 370 | countryAccess?: FreeSessionCountryAccessMetadata |
| 371 | /** True if the account is banned. Short-circuited here so banned bots never |
| 372 | * create a queued row — otherwise they inflate `queueDepth` between the |
| 373 | * 15s admission ticks that run `evictBanned`. */ |
| 374 | userBanned?: boolean |
| 375 | deps?: SessionDeps |
| 376 | }): Promise<RequestSessionResult> { |
| 377 | const deps = params.deps ?? defaultDeps |
| 378 | const accessTier = params.accessTier ?? 'full' |
| 379 | const model = resolveFreebuffModelForAccessTier(params.model, accessTier) |
| 380 | const now = nowOf(deps) |
| 381 | if (params.userBanned) { |
| 382 | return { status: 'banned' } |
| 383 | } |
| 384 | if ( |
| 385 | !deps.isWaitingRoomEnabled() || |
| 386 | isWaitingRoomBypassedForEmail(params.userEmail) |
| 387 | ) { |
| 388 | return { status: 'disabled' } |
| 389 | } |
| 390 | |
| 391 | // Rate-limit check runs before joinOrTakeOver so heavy users never even |
| 392 | // create a queued row. Premium models share one daily Pacific-time |
| 393 | // session-unit pool; Minimax falls through unchanged as unlimited. |
| 394 | // |
| 395 | // Takeover/reclaim exception: a user who already holds a queued or |
| 396 | // active+unexpired row on this same model is re-anchoring (CLI restart, |
| 397 | // same-account tab switch) rather than starting a new session. Admit |
| 398 | // counts are written at promotion time, so the quota only needs to gate |
| 399 | // fresh admissions — blocking a reclaim here would strand a user with an |
| 400 | // active 5th session unable to reconnect after a CLI restart. |
| 401 | let existing = await deps.getSessionRow(params.userId) |
| 402 | if (existing && !isSessionRowCompatibleWithAccessTier(existing, accessTier)) { |
| 403 | await deps.endSession({ |
| 404 | userId: params.userId, |
| 405 | now, |
| 406 | sessionLengthMs: deps.sessionLengthMs, |
| 407 | }) |
| 408 | existing = null |
| 409 | } |
| 410 | const isReclaim = |
| 411 | !!existing && |
| 412 | existing.model === model && |
| 413 | (existing.access_tier ?? 'full') === accessTier && |
| 414 | (existing.status === 'queued' || |
| 415 | (existing.status === 'active' && |
| 416 | !!existing.expires_at && |
| 417 | existing.expires_at.getTime() > now.getTime())) |
| 418 | |
| 419 | if (!isReclaim && !isFreebuffModelAvailable(model, now)) { |
| 420 | return { |
| 421 | status: 'model_unavailable', |
| 422 | requestedModel: model, |
no test coverage detected