Skip to content

Commit fdf60ae

Browse files
committed
More cleanup
1 parent d575c88 commit fdf60ae

File tree

13 files changed

+77
-158
lines changed

13 files changed

+77
-158
lines changed

cli/src/components/freebuff-session-countdown.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

cli/src/components/freebuff-superseded-screen.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { TextAttributes } from '@opentui/core'
2-
import { useKeyboard } from '@opentui/react'
3-
import React, { useCallback } from 'react'
2+
import React from 'react'
43

4+
import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
55
import { useLogo } from '../hooks/use-logo'
66
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
77
import { useTheme } from '../hooks/use-theme'
8-
import { exitFreebuffCleanly } from '../utils/freebuff-exit'
98
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
109

11-
import type { KeyEvent } from '@opentui/core'
12-
1310
/**
1411
* Terminal state shown after a 409 session_superseded response. Another CLI on
1512
* the same account rotated our instance id and we've stopped polling — the
@@ -26,16 +23,7 @@ export const FreebuffSupersededScreen: React.FC = () => {
2623
blockColor,
2724
})
2825

29-
// Ctrl+C exits. Stdin is in raw mode, so SIGINT never fires — the key comes
30-
// through as a normal OpenTUI key event.
31-
useKeyboard(
32-
useCallback((key: KeyEvent) => {
33-
if (key.ctrl && key.name === 'c') {
34-
key.preventDefault?.()
35-
exitFreebuffCleanly()
36-
}
37-
}, []),
38-
)
26+
useFreebuffCtrlCExit()
3927

4028
return (
4129
<box

cli/src/components/waiting-room-screen.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { TextAttributes } from '@opentui/core'
2-
import { useKeyboard, useRenderer } from '@opentui/react'
3-
import React, { useCallback, useMemo, useState } from 'react'
2+
import { useRenderer } from '@opentui/react'
3+
import React, { useMemo, useState } from 'react'
44

55
import { AdBanner } from './ad-banner'
66
import { Button } from './button'
77
import { ChoiceAdBanner } from './choice-ad-banner'
88
import { ShimmerText } from './shimmer-text'
9+
import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
910
import { useGravityAd } from '../hooks/use-gravity-ad'
1011
import { useLogo } from '../hooks/use-logo'
1112
import { useNow } from '../hooks/use-now'
@@ -16,7 +17,6 @@ import { exitFreebuffCleanly } from '../utils/freebuff-exit'
1617
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
1718

1819
import type { FreebuffSessionResponse } from '../types/freebuff-session'
19-
import type { KeyEvent } from '@opentui/core'
2020

2121
interface WaitingRoomScreenProps {
2222
session: FreebuffSessionResponse | null
@@ -77,16 +77,7 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
7777
forceStart: true,
7878
})
7979

80-
// Ctrl+C exits. Stdin is in raw mode, so SIGINT never fires — the key comes
81-
// through as a normal OpenTUI key event. Shared with the top-right X button.
82-
useKeyboard(
83-
useCallback((key: KeyEvent) => {
84-
if (key.ctrl && key.name === 'c') {
85-
key.preventDefault?.()
86-
exitFreebuffCleanly()
87-
}
88-
}, []),
89-
)
80+
useFreebuffCtrlCExit()
9081

9182
const [exitHover, setExitHover] = useState(false)
9283

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useKeyboard } from '@opentui/react'
2+
import { useCallback } from 'react'
3+
4+
import { exitFreebuffCleanly } from '../utils/freebuff-exit'
5+
6+
import type { KeyEvent } from '@opentui/core'
7+
8+
/**
9+
* Bind Ctrl+C on a full-screen freebuff view to `exitFreebuffCleanly`. Stdin
10+
* is in raw mode, so SIGINT never fires — the key arrives as a normal OpenTUI
11+
* key event and we route it through the shared cleanup path (flush analytics,
12+
* release the session seat, then process.exit).
13+
*/
14+
export function useFreebuffCtrlCExit(): void {
15+
useKeyboard(
16+
useCallback((key: KeyEvent) => {
17+
if (key.ctrl && key.name === 'c') {
18+
key.preventDefault?.()
19+
exitFreebuffCleanly()
20+
}
21+
}, []),
22+
)
23+
}

docs/freebuff-waiting-room.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
The waiting room is the admission control layer for **free-mode** requests against the freebuff Fireworks deployment. It has three jobs:
66

7-
1. **Drip-admit users** — admit at a steady trickle (default 1 per 15s) so load ramps up gradually rather than stampeding the deployment when the queue is long.
7+
1. **Drip-admit users** — admit at a steady trickle (default 1 per `ADMISSION_TICK_MS`, currently 15s) so load ramps up gradually rather than stampeding the deployment when the queue is long.
88
2. **Gate on upstream health** — before each admission tick, probe the Fireworks metrics endpoint with a short timeout (`isFireworksAdmissible` in `web/src/server/free-session/admission.ts`). If it doesn't respond OK, admission halts until it does — this is the primary concurrency control, not a static cap.
99
3. **One instance per account** — prevent a single user from running N concurrent freebuff CLIs to get N× throughput.
1010

@@ -132,14 +132,13 @@ One pod runs the admission loop at a time, coordinated via Postgres advisory loc
132132
Each tick does (in order):
133133

134134
1. **Sweep expired.** `DELETE FROM free_session WHERE status='active' AND expires_at < now() - grace`. Runs regardless of upstream health so zombie sessions are cleaned up even during an outage.
135-
2. **Admit.** `admitFromQueue()` first calls `isFireworksAdmissible()` (short-timeout GET against the Fireworks metrics endpoint). If the probe fails, returns `{ skipped: 'health' }` — admission pauses and the queue grows until recovery. Otherwise opens a transaction, takes `pg_try_advisory_xact_lock(FREEBUFF_ADMISSION_LOCK_ID)`, and `SELECT ... WHERE status='queued' ORDER BY queued_at, user_id LIMIT MAX_ADMITS_PER_TICK FOR UPDATE SKIP LOCKED``UPDATE` the rows to `status='active'` with `admitted_at=now()`, `expires_at=now()+sessionLength`. Staggering at `MAX_ADMITS_PER_TICK=1` / 15s keeps Fireworks from a thundering herd of newly-admitted CLIs.
135+
2. **Admit.** `admitFromQueue()` first calls `isFireworksAdmissible()` (short-timeout GET against the Fireworks metrics endpoint). If the probe fails, returns `{ skipped: 'health' }` — admission pauses and the queue grows until recovery. Otherwise opens a transaction, takes `pg_try_advisory_xact_lock(FREEBUFF_ADMISSION_LOCK_ID)`, and `SELECT ... WHERE status='queued' ORDER BY queued_at, user_id LIMIT 1 FOR UPDATE SKIP LOCKED``UPDATE` the row to `status='active'` with `admitted_at=now()`, `expires_at=now()+sessionLength`. One admit per tick keeps Fireworks from a thundering herd of newly-admitted CLIs.
136136

137137
### Tunables
138138

139139
| Constant | Location | Default | Purpose |
140140
|---|---|---|---|
141-
| `ADMISSION_TICK_MS` | `config.ts` | 15000 | How often the ticker fires |
142-
| `MAX_ADMITS_PER_TICK` | `config.ts` | 1 | Upper bound on admits per tick |
141+
| `ADMISSION_TICK_MS` | `config.ts` | 15000 | How often the ticker fires. One user is admitted per tick. |
143142
| `FREEBUFF_SESSION_LENGTH_MS` | env | 3_600_000 | Session lifetime |
144143
| `FREEBUFF_SESSION_GRACE_MS` | env | 1_800_000 | Drain window after expiry — gate still admits requests so an in-flight agent can finish, but the CLI is expected to block new prompts. Hard cutoff at `expires_at + grace`. |
145144

@@ -224,6 +223,7 @@ For free-mode requests (`codebuff_metadata.cost_mode === 'free'`), `_post.ts` ca
224223

225224
| HTTP | `error` | When |
226225
|---|---|---|
226+
| 426 | `freebuff_update_required` | Request did not include a `freebuff_instance_id` — the client is a pre-waiting-room build. The CLI shows the server-supplied message verbatim. |
227227
| 428 | `waiting_room_required` | No session row exists. Client should call POST /session. |
228228
| 429 | `waiting_room_queued` | Row exists with `status='queued'`. Client should keep polling GET. |
229229
| 409 | `session_superseded` | Claimed `instance_id` does not match stored one — another CLI took over. |
@@ -249,13 +249,11 @@ This is a **trust-the-client** design: the server still admits requests during t
249249
Computed in `session-view.ts` from the drip-admission rate:
250250

251251
```
252-
ticksAhead = ceil((position - 1) / maxAdmitsPerTick)
253-
waitMs = ticksAhead * admissionTickMs
252+
waitMs = (position - 1) * admissionTickMs
254253
```
255254

256255
- Position 1 → 0 (next tick admits you)
257-
- Position `maxAdmitsPerTick` + 1 → one tick
258-
- and so on.
256+
- Position 2 → one tick, and so on.
259257

260258
This estimate **ignores health-gated pauses**: during a Fireworks incident admission halts entirely, so the actual wait can be longer. We choose to under-report here because showing "unknown" / "indefinite" is worse UX for the common case where the deployment is healthy.
261259

web/src/app/api/v1/chat/completions/_post.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ const STATUS_BY_GATE_CODE = {
147147
waiting_room_queued: 429,
148148
session_superseded: 409,
149149
session_expired: 410,
150+
freebuff_update_required: 426,
150151
} satisfies Record<GateRejectCode, number>
151152

152153
export async function postChatCompletions(params: {
@@ -412,30 +413,8 @@ export async function postChatCompletions(params: {
412413
if (isFreeModeRequest) {
413414
const claimedInstanceId =
414415
typedBody.codebuff_metadata?.freebuff_instance_id
415-
const gate = await checkSession({
416-
userId,
417-
claimedInstanceId,
418-
})
416+
const gate = await checkSession({ userId, claimedInstanceId })
419417
if (!gate.ok) {
420-
// Old freebuff clients (pre-waiting-room) never send an instance_id.
421-
// Return a 426 with a clear "please restart to upgrade" message that
422-
// their existing error banner will render verbatim.
423-
if (!claimedInstanceId) {
424-
trackEvent({
425-
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
426-
userId,
427-
properties: { error: 'freebuff_update_required' },
428-
logger,
429-
})
430-
return NextResponse.json(
431-
{
432-
error: 'freebuff_update_required',
433-
message:
434-
'This version of freebuff is out of date. Please restart freebuff to upgrade and continue using free mode.',
435-
},
436-
{ status: 426 },
437-
)
438-
}
439418
trackEvent({
440419
event: AnalyticsEvent.CHAT_COMPLETIONS_VALIDATION_ERROR,
441420
userId,

web/src/app/api/v1/freebuff/session/__tests__/session.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ function makeSessionDeps(overrides: Partial<SessionDeps> = {}): SessionDeps & {
3434
rows,
3535
isWaitingRoomEnabled: () => true,
3636
admissionTickMs: 15_000,
37-
maxAdmitsPerTick: 1,
3837
graceMs: 30 * 60 * 1000,
3938
now: () => now,
4039
getSessionRow: async (userId) => rows.get(userId) ?? null,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { InternalSessionRow } from '../types'
1212

1313
const SESSION_LEN = 60 * 60 * 1000
1414
const TICK_MS = 15_000
15-
const ADMITS_PER_TICK = 1
1615
const GRACE_MS = 30 * 60 * 1000
1716

1817
function makeDeps(overrides: Partial<SessionDeps> = {}): SessionDeps & {
@@ -38,7 +37,6 @@ function makeDeps(overrides: Partial<SessionDeps> = {}): SessionDeps & {
3837
_now: () => currentNow,
3938
isWaitingRoomEnabled: () => true,
4039
admissionTickMs: TICK_MS,
41-
maxAdmitsPerTick: ADMITS_PER_TICK,
4240
graceMs: GRACE_MS,
4341
now: () => currentNow,
4442
getSessionRow: async (userId) => rows.get(userId) ?? null,
@@ -329,7 +327,9 @@ describe('checkSessionAdmissible', () => {
329327
expect(result.code).toBe('session_superseded')
330328
})
331329

332-
test('active + missing instance id → session_superseded (fails closed)', async () => {
330+
test('missing instance id → freebuff_update_required (pre-waiting-room CLI)', async () => {
331+
// Classified up front regardless of row state: old clients never send an
332+
// id, so we surface a distinct code that maps to 426 Upgrade Required.
333333
await requestSession({ userId: 'u1', deps })
334334
const row = deps.rows.get('u1')!
335335
row.status = 'active'
@@ -342,7 +342,7 @@ describe('checkSessionAdmissible', () => {
342342
deps,
343343
})
344344
if (result.ok) throw new Error('unreachable')
345-
expect(result.code).toBe('session_superseded')
345+
expect(result.code).toBe('freebuff_update_required')
346346
})
347347

348348
test('active inside grace window → ok with reason=draining', async () => {

web/src/server/free-session/__tests__/session-view.test.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { estimateWaitMs, toSessionStateResponse } from '../session-view'
55
import type { InternalSessionRow } from '../types'
66

77
const TICK_MS = 15_000
8-
const ADMITS_PER_TICK = 1
98
const GRACE_MS = 30 * 60_000
109

1110
function row(overrides: Partial<InternalSessionRow> = {}): InternalSessionRow {
@@ -25,34 +24,24 @@ function row(overrides: Partial<InternalSessionRow> = {}): InternalSessionRow {
2524

2625
describe('estimateWaitMs', () => {
2726
test('position 1 → 0 wait (next tick picks you up)', () => {
28-
expect(estimateWaitMs({ position: 1, admissionTickMs: TICK_MS, maxAdmitsPerTick: ADMITS_PER_TICK })).toBe(0)
27+
expect(estimateWaitMs({ position: 1, admissionTickMs: TICK_MS })).toBe(0)
2928
})
3029

31-
test('position N → (N-1) ticks ahead at 1 admit/tick', () => {
32-
expect(estimateWaitMs({ position: 2, admissionTickMs: TICK_MS, maxAdmitsPerTick: 1 })).toBe(TICK_MS)
33-
expect(estimateWaitMs({ position: 10, admissionTickMs: TICK_MS, maxAdmitsPerTick: 1 })).toBe(9 * TICK_MS)
34-
})
35-
36-
test('batched admission divides wait', () => {
37-
// 5 admits/tick: positions 2-6 all sit one tick ahead.
38-
expect(estimateWaitMs({ position: 2, admissionTickMs: TICK_MS, maxAdmitsPerTick: 5 })).toBe(TICK_MS)
39-
expect(estimateWaitMs({ position: 6, admissionTickMs: TICK_MS, maxAdmitsPerTick: 5 })).toBe(TICK_MS)
40-
// Position 7 enters the second tick.
41-
expect(estimateWaitMs({ position: 7, admissionTickMs: TICK_MS, maxAdmitsPerTick: 5 })).toBe(2 * TICK_MS)
30+
test('position N → (N-1) ticks ahead', () => {
31+
expect(estimateWaitMs({ position: 2, admissionTickMs: TICK_MS })).toBe(TICK_MS)
32+
expect(estimateWaitMs({ position: 10, admissionTickMs: TICK_MS })).toBe(9 * TICK_MS)
4233
})
4334

4435
test('degenerate inputs return 0', () => {
45-
expect(estimateWaitMs({ position: 0, admissionTickMs: TICK_MS, maxAdmitsPerTick: 1 })).toBe(0)
46-
expect(estimateWaitMs({ position: 5, admissionTickMs: 0, maxAdmitsPerTick: 1 })).toBe(0)
47-
expect(estimateWaitMs({ position: 5, admissionTickMs: TICK_MS, maxAdmitsPerTick: 0 })).toBe(0)
36+
expect(estimateWaitMs({ position: 0, admissionTickMs: TICK_MS })).toBe(0)
37+
expect(estimateWaitMs({ position: 5, admissionTickMs: 0 })).toBe(0)
4838
})
4939
})
5040

5141
describe('toSessionStateResponse', () => {
5242
const now = new Date('2026-04-17T12:00:00Z')
5343
const baseArgs = {
5444
admissionTickMs: TICK_MS,
55-
maxAdmitsPerTick: ADMITS_PER_TICK,
5645
graceMs: GRACE_MS,
5746
}
5847

web/src/server/free-session/admission.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { env } from '@codebuff/internal/env'
22

33
import {
44
ADMISSION_TICK_MS,
5-
MAX_ADMITS_PER_TICK,
65
getSessionGraceMs,
76
getSessionLengthMs,
87
isWaitingRoomEnabled,
@@ -153,7 +152,7 @@ export function startFreeSessionAdmission(): boolean {
153152
if (typeof interval.unref === 'function') interval.unref()
154153
runTick() // fire first tick immediately
155154
logger.info(
156-
{ tickMs: ADMISSION_TICK_MS, maxAdmitsPerTick: MAX_ADMITS_PER_TICK },
155+
{ tickMs: ADMISSION_TICK_MS },
157156
'[FreeSessionAdmission] Started',
158157
)
159158
return true

0 commit comments

Comments
 (0)