Skip to content

Commit 9d9e559

Browse files
committed
Clear incompatible Freebuff sessions on poll
1 parent 6ff21b4 commit 9d9e559

2 files changed

Lines changed: 86 additions & 21 deletions

File tree

web/src/server/free-session/__tests__/public-api.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { beforeEach, describe, expect, test } from 'bun:test'
22

33
import {
4+
FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
45
FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
56
FREEBUFF_GEMINI_PRO_MODEL_ID,
67
FREEBUFF_GLM_MODEL_ID,
@@ -936,6 +937,68 @@ describe('getSessionState', () => {
936937
).toEqual(expectedRateLimit(FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, 1))
937938
})
938939

940+
test('limited access deletes an incompatible queued row before returning none', async () => {
941+
await requestSession({ userId: 'u1', model: DEFAULT_MODEL, deps })
942+
expect(deps.rows.has('u1')).toBe(true)
943+
944+
const state = await getSessionState({
945+
userId: 'u1',
946+
accessTier: 'limited',
947+
deps,
948+
})
949+
950+
expect(state).toEqual({
951+
status: 'none',
952+
accessTier: 'limited',
953+
queueDepthByModel: {},
954+
})
955+
expect(deps.rows.has('u1')).toBe(false)
956+
})
957+
958+
test('limited access deletes a queued full-tier Flash row before returning none', async () => {
959+
await requestSession({
960+
userId: 'u1',
961+
model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
962+
deps,
963+
})
964+
expect(deps.rows.get('u1')?.access_tier).toBe('full')
965+
966+
const state = await getSessionState({
967+
userId: 'u1',
968+
accessTier: 'limited',
969+
deps,
970+
})
971+
972+
expect(state).toEqual({
973+
status: 'none',
974+
accessTier: 'limited',
975+
queueDepthByModel: {},
976+
})
977+
expect(deps.rows.has('u1')).toBe(false)
978+
})
979+
980+
test('limited access deletes an incompatible active row before returning none', async () => {
981+
await requestSession({ userId: 'u1', model: DEFAULT_MODEL, deps })
982+
const row = deps.rows.get('u1')!
983+
row.status = 'active'
984+
row.admitted_at = deps._now()
985+
row.expires_at = new Date(deps._now().getTime() + SESSION_LEN)
986+
987+
const state = await getSessionState({
988+
userId: 'u1',
989+
accessTier: 'limited',
990+
claimedInstanceId: row.active_instance_id,
991+
deps,
992+
})
993+
994+
expect(state).toEqual({
995+
status: 'none',
996+
accessTier: 'limited',
997+
queueDepthByModel: {},
998+
})
999+
expect(deps.rows.has('u1')).toBe(false)
1000+
})
1001+
9391002
test('active session with matching instance id returns active', async () => {
9401003
await requestSession({ userId: 'u1', model: DEFAULT_MODEL, deps })
9411004
const row = deps.rows.get('u1')!

web/src/server/free-session/public-api.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ const defaultDeps: SessionDeps = {
278278

279279
const nowOf = (deps: SessionDeps): Date => (deps.now ?? (() => new Date()))()
280280

281+
function isSessionRowCompatibleWithAccessTier(
282+
row: InternalSessionRow,
283+
accessTier: FreebuffAccessTier,
284+
): boolean {
285+
if (accessTier === 'limited' && (row.access_tier ?? 'full') !== 'limited') {
286+
return false
287+
}
288+
return isFreebuffModelAllowedForAccessTier(row.model, accessTier)
289+
}
290+
281291
async function viewForRow(
282292
userId: string,
283293
deps: SessionDeps,
@@ -389,14 +399,7 @@ export async function requestSession(params: {
389399
// fresh admissions — blocking a reclaim here would strand a user with an
390400
// active 5th session unable to reconnect after a CLI restart.
391401
let existing = await deps.getSessionRow(params.userId)
392-
const existingAccessTier = existing?.access_tier ?? 'full'
393-
if (
394-
existing &&
395-
(accessTier === 'limited'
396-
? existingAccessTier !== 'limited' ||
397-
!isFreebuffModelAllowedForAccessTier(existing.model, accessTier)
398-
: !isFreebuffModelAllowedForAccessTier(existing.model, accessTier))
399-
) {
402+
if (existing && !isSessionRowCompatibleWithAccessTier(existing, accessTier)) {
400403
await deps.endSession({
401404
userId: params.userId,
402405
now,
@@ -539,9 +542,11 @@ async function attachRateLimit(
539542
}
540543

541544
/**
542-
* Read-only check of the caller's current state. Does not mutate or rotate
543-
* `instance_id`. The CLI sends its currently-held `claimedInstanceId` so we
544-
* can return `superseded` if a newer CLI on the same account took over.
545+
* Check of the caller's current state. Does not rotate `instance_id`. The CLI
546+
* sends its currently-held `claimedInstanceId` so we can return `superseded`
547+
* if a newer CLI on the same account took over. Mutates only to clear rows
548+
* that the current access tier can no longer use, so they don't leak queue or
549+
* active capacity after the CLI receives `none`.
545550
*
546551
* Returns:
547552
* - `disabled` when the waiting room is off
@@ -593,11 +598,12 @@ export async function getSessionState(params: {
593598

594599
if (!row) return noneResponse()
595600

596-
if (
597-
accessTier === 'limited' &&
598-
((row.access_tier ?? 'full') !== 'limited' ||
599-
!isFreebuffModelAllowedForAccessTier(row.model, accessTier))
600-
) {
601+
if (!isSessionRowCompatibleWithAccessTier(row, accessTier)) {
602+
await deps.endSession({
603+
userId: params.userId,
604+
now: nowOf(deps),
605+
sessionLengthMs: deps.sessionLengthMs,
606+
})
601607
return noneResponse()
602608
}
603609

@@ -747,11 +753,7 @@ export async function checkSessionAdmissible(params: {
747753
}
748754
}
749755

750-
if (
751-
accessTier === 'limited' &&
752-
((row.access_tier ?? 'full') !== 'limited' ||
753-
!isFreebuffModelAllowedForAccessTier(row.model, accessTier))
754-
) {
756+
if (!isSessionRowCompatibleWithAccessTier(row, accessTier)) {
755757
return {
756758
ok: false,
757759
code: 'session_model_mismatch',

0 commit comments

Comments
 (0)